Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add "cookie" property to rpc http messages #4317

Merged
merged 10 commits into from
Jun 4, 2019
Original file line number Diff line number Diff line change
Expand Up @@ -38,7 +38,7 @@ From within the Azure Functions language worker repo:

## CSharp
```
set NUGET_PATH=%UserProfile%\.nuget\packages
set NUGET_PATH="%UserProfile%\.nuget\packages"
set GRPC_TOOLS_PATH=%NUGET_PATH%\grpc.tools\<versionNumber>\tools\windows_x86
set PROTO_PATH=.\azure-functions-language-worker-protobuf\src\proto
set PROTO=.\azure-functions-language-worker-protobuf\src\proto\FunctionRpc.proto
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@ package AzureFunctionsRpcMessages;

import "google/protobuf/duration.proto";
import "identity/ClaimsIdentityRpc.proto";
import "shared/NullableTypes.proto";

// Interface exported by the server.
service FunctionRpc {
Expand Down Expand Up @@ -375,6 +376,44 @@ message RpcException {
string message = 2;
}

// Http cookie type. Note that only name and value are used for Http requests
message RpcHttpCookie {
// Enum that lets servers require that a cookie shouoldn't be sent with cross-site requests
enum SameSite {
None = 0;
Lax = 1;
Strict = 2;
}

// Cookie name
string name = 1;

// Cookie value
string value = 2;

// Specifies allowed hosts to receive the cookie
NullableString domain = 3;

// Specifies URL path that must exist in the requested URL
NullableString path = 4;

// Sets the cookie to expire at a specific date instead of when the client closes.
// It is generally recommended that you use "Max-Age" over "Expires".
NullableTimestamp expires = 5;

// Sets the cookie to only be sent with an encrypted request
NullableBool secure = 6;

// Sets the cookie to be inaccessible to JavaScript's Document.cookie API
NullableBool http_only = 7;

// Allows servers to assert that a cookie ought not to be sent along with cross-site requests
SameSite same_site = 8;

// Number of seconds until the cookie expires. A zero or negative number will expire the cookie immediately.
NullableDouble max_age = 9;
}

// TODO - solidify this or remove it
message RpcHttp {
string method = 1;
Expand All @@ -387,4 +426,5 @@ message RpcHttp {
bool enable_content_negotiation= 16;
TypedData rawBody = 17;
repeated RpcClaimsIdentity identities = 18;
repeated RpcHttpCookie cookies = 19;
}
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
syntax = "proto3";
// protobuf vscode extension: https://marketplace.visualstudio.com/items?itemName=zxh404.vscode-proto3

import "shared/NullableString.proto";
import "shared/NullableTypes.proto";

// Light-weight representation of a .NET System.Security.Claims.ClaimsIdentity object.
// This is the same serialization as found in EasyAuth, and needs to be kept in sync with
Expand Down

This file was deleted.

Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
syntax = "proto3";
// protobuf vscode extension: https://marketplace.visualstudio.com/items?itemName=zxh404.vscode-proto3

import "google/protobuf/timestamp.proto";

message NullableString {
oneof string {
string value = 1;
}
}

message NullableDouble {
mhoeger marked this conversation as resolved.
Show resolved Hide resolved
oneof double {
double value = 1;
}
}

message NullableBool {
oneof bool {
bool value = 1;
}
}

message NullableTimestamp {
oneof timestamp {
google.protobuf.Timestamp value = 1;
}
}
32 changes: 22 additions & 10 deletions src/WebJobs.Script/Binding/Http/HttpBinding.cs
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@
using Microsoft.AspNetCore.Mvc;
using Microsoft.AspNetCore.Mvc.WebApiCompatShim;
using Microsoft.Azure.WebJobs.Script.Description;
using Microsoft.Azure.WebJobs.Script.Rpc;
using Newtonsoft.Json;
using Newtonsoft.Json.Linq;

Expand Down Expand Up @@ -83,30 +84,32 @@ internal static IActionResult CreateResult(HttpRequest request, object content)
int statusCode = StatusCodes.Status200OK;
IDictionary<string, object> responseHeaders = null;
bool enableContentNegotiation = false;
List<Tuple<string, string, CookieOptions>> cookies = new List<Tuple<string, string, CookieOptions>>();
if (responseObject != null)
{
ParseResponseObject(responseObject, ref content, out responseHeaders, out statusCode, out enableContentNegotiation);
ParseResponseObject(responseObject, ref content, out responseHeaders, out statusCode, out cookies, out enableContentNegotiation);
}

return CreateResult(request, statusCode, content, responseHeaders, enableContentNegotiation);
return CreateResult(request, statusCode, content, responseHeaders, cookies, enableContentNegotiation);
}

internal static void ParseResponseObject(IDictionary<string, object> responseObject, ref object content, out IDictionary<string, object> headers, out int statusCode, out bool enableContentNegotiation)
internal static void ParseResponseObject(IDictionary<string, object> responseObject, ref object content, out IDictionary<string, object> headers, out int statusCode, out List<Tuple<string, string, CookieOptions>> cookies, out bool enableContentNegotiation)
{
headers = null;
cookies = null;
statusCode = StatusCodes.Status200OK;
enableContentNegotiation = false;

// TODO: Improve this logic
// Sniff the object to see if it looks like a response object
// by convention
object bodyValue = null;
if (responseObject.TryGetValue("body", out bodyValue, ignoreCase: true))
if (responseObject.TryGetValue(LanguageWorkerConstants.RpcHttpBody, out bodyValue, ignoreCase: true))
{
// the response content becomes the specified body value
content = bodyValue;

if (responseObject.TryGetValue("headers", out IDictionary<string, object> headersValue, ignoreCase: true))
if (responseObject.TryGetValue(LanguageWorkerConstants.RpcHttpHeaders, out IDictionary<string, object> headersValue, ignoreCase: true))
{
headers = headersValue;
}
Expand All @@ -116,19 +119,24 @@ internal static void ParseResponseObject(IDictionary<string, object> responseObj
statusCode = responseStatusCode.Value;
}

if (responseObject.TryGetValue<bool>("enableContentNegotiation", out bool enableContentNegotiationValue, ignoreCase: true))
if (responseObject.TryGetValue<bool>(LanguageWorkerConstants.RpcHttpEnableContentNegotiation, out bool enableContentNegotiationValue, ignoreCase: true))
{
enableContentNegotiation = enableContentNegotiationValue;
}

if (responseObject.TryGetValue(LanguageWorkerConstants.RpcHttpCookies, out List<Tuple<string, string, CookieOptions>> cookiesValue, ignoreCase: true))
{
cookies = cookiesValue;
}
}
}

internal static bool TryParseStatusCode(IDictionary<string, object> responseObject, out int? statusCode)
{
statusCode = StatusCodes.Status200OK;

if (!responseObject.TryGetValue("statusCode", out object statusValue, ignoreCase: true) &&
!responseObject.TryGetValue("status", out statusValue, ignoreCase: true))
if (!responseObject.TryGetValue(LanguageWorkerConstants.RpcHttpStatusCode, out object statusValue, ignoreCase: true) &&
!responseObject.TryGetValue(LanguageWorkerConstants.RpcHttpStatus, out statusValue, ignoreCase: true))
{
return false;
}
Expand Down Expand Up @@ -161,7 +169,7 @@ statusValue is long ||
return false;
}

private static IActionResult CreateResult(HttpRequest request, int statusCode, object content, IDictionary<string, object> headers, bool enableContentNegotiation)
private static IActionResult CreateResult(HttpRequest request, int statusCode, object content, IDictionary<string, object> headers, List<Tuple<string, string, CookieOptions>> cookies, bool enableContentNegotiation)
{
if (enableContentNegotiation)
{
Expand All @@ -171,7 +179,11 @@ private static IActionResult CreateResult(HttpRequest request, int statusCode, o
}
else
{
return new RawScriptResult(statusCode, content) { Headers = headers };
return new RawScriptResult(statusCode, content)
{
Headers = headers,
Cookies = cookies
};
}
}

Expand Down
19 changes: 19 additions & 0 deletions src/WebJobs.Script/Binding/Http/RawScriptResult.cs
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@
using Microsoft.AspNetCore.WebUtilities;
using Microsoft.Azure.WebJobs.Script.WebHost.Formatters;
using Newtonsoft.Json;
using Newtonsoft.Json.Linq;

namespace Microsoft.Azure.WebJobs.Script.Binding
{
Expand All @@ -36,6 +37,8 @@ public RawScriptResult(int? statusCode, object content)

public IDictionary<string, object> Headers { get; set; }

public List<Tuple<string, string, CookieOptions>> Cookies { get; set; }

public async Task ExecuteResultAsync(ActionContext context)
{
HttpResponse response = context.HttpContext.Response;
Expand Down Expand Up @@ -73,6 +76,22 @@ public async Task ExecuteResultAsync(ActionContext context)
response.StatusCode = StatusCode.Value;
}

if (Cookies != null)
{
mhoeger marked this conversation as resolved.
Show resolved Hide resolved
foreach (var cookie in Cookies)
{
// Item3 (CookieOptions) should not be null, but this will behave correctly if it is
if (cookie.Item3 != null)
{
response.Cookies.Append(cookie.Item1, cookie.Item2, cookie.Item3);
}
else
{
response.Cookies.Append(cookie.Item1, cookie.Item2);
}
}
}

await WriteResponseBodyAsync(response, Content);
}

Expand Down
21 changes: 14 additions & 7 deletions src/WebJobs.Script/Rpc/LanguageWorkerChannel.cs
Original file line number Diff line number Diff line change
Expand Up @@ -469,15 +469,22 @@ internal void InvokeResponse(InvocationResponse invokeResponse)
if (_executingInvocations.TryRemove(invokeResponse.InvocationId, out ScriptInvocationContext context)
&& invokeResponse.Result.IsSuccess(context.ResultSource))
{
IDictionary<string, object> bindingsDictionary = invokeResponse.OutputData
.ToDictionary(binding => binding.Name, binding => binding.Data.ToObject());
try
{
IDictionary<string, object> bindingsDictionary = invokeResponse.OutputData
.ToDictionary(binding => binding.Name, binding => binding.Data.ToObject());

var result = new ScriptInvocationResult()
var result = new ScriptInvocationResult()
{
Outputs = bindingsDictionary,
Return = invokeResponse?.ReturnValue?.ToObject()
};
context.ResultSource.SetResult(result);
}
catch (Exception responseEx)
{
Outputs = bindingsDictionary,
Return = invokeResponse?.ReturnValue?.ToObject()
};
context.ResultSource.SetResult(result);
context.ResultSource.TrySetException(responseEx);
}
}
}

Expand Down
10 changes: 9 additions & 1 deletion src/WebJobs.Script/Rpc/LanguageWorkerConstants.cs
Original file line number Diff line number Diff line change
Expand Up @@ -35,8 +35,16 @@ public static class LanguageWorkerConstants

public const int DefaultMaxMessageLengthBytes = 128 * 1024 * 1024;

//Logs
// Logs
public const string LanguageWorkerConsoleLogPrefix = "LanguageWorkerConsoleLog";
public const string FunctionConsoleLogCategoryName = "Host.Function.Console";

// Rpc Http Constants
public const string RpcHttpBody = "body";
public const string RpcHttpHeaders = "headers";
public const string RpcHttpEnableContentNegotiation = "enableContentNegotiation";
public const string RpcHttpCookies = "cookies";
public const string RpcHttpStatusCode = "statusCode";
public const string RpcHttpStatus = "status";
}
}
62 changes: 61 additions & 1 deletion src/WebJobs.Script/Rpc/MessageExtensions/Utilities.cs
Original file line number Diff line number Diff line change
@@ -1,11 +1,12 @@
// Copyright (c) .NET Foundation. All rights reserved.
// Licensed under the MIT License. See License.txt in the project root for license information.

using System;
using System.Collections.Generic;
using System.Dynamic;
using System.Linq;
using Microsoft.AspNetCore.Http;
using Microsoft.Azure.WebJobs.Script.Grpc.Messages;
using Microsoft.Azure.WebJobs.Script.Rpc;

namespace Microsoft.Azure.WebJobs.Script.Rpc
{
Expand All @@ -25,11 +26,70 @@ public static object ConvertFromHttpMessageToExpando(RpcHttp inputMessage)
expando.headers = inputMessage.Headers.ToDictionary(p => p.Key, p => (object)p.Value);
expando.enableContentNegotiation = inputMessage.EnableContentNegotiation;

expando.cookies = new List<Tuple<string, string, CookieOptions>>();
foreach (RpcHttpCookie cookie in inputMessage.Cookies)
{
expando.cookies.Add(RpcHttpCookieConverter(cookie));
}

if (inputMessage.Body != null)
{
expando.body = inputMessage.Body.ToObject();
}
return expando;
}

public static Tuple<string, string, CookieOptions> RpcHttpCookieConverter(RpcHttpCookie cookie)
{
var cookieOptions = new CookieOptions();
mhoeger marked this conversation as resolved.
Show resolved Hide resolved
if (cookie.Domain != null)
{
cookieOptions.Domain = cookie.Domain.Value;
}

if (cookie.Path != null)
{
cookieOptions.Path = cookie.Path.Value;
}

if (cookie.Secure != null)
{
cookieOptions.Secure = cookie.Secure.Value;
}

cookieOptions.SameSite = RpcSameSiteEnumConverter(cookie.SameSite);

if (cookie.HttpOnly != null)
{
cookieOptions.HttpOnly = cookie.HttpOnly.Value;
}

if (cookie.Expires != null)
{
cookieOptions.Expires = cookie.Expires.Value.ToDateTimeOffset();
}

if (cookie.MaxAge != null)
{
cookieOptions.MaxAge = TimeSpan.FromSeconds(cookie.MaxAge.Value);
}

return new Tuple<string, string, CookieOptions>(cookie.Name, cookie.Value, cookieOptions);
}

private static SameSiteMode RpcSameSiteEnumConverter(RpcHttpCookie.Types.SameSite sameSite)
{
switch (sameSite)
{
case RpcHttpCookie.Types.SameSite.Strict:
return SameSiteMode.Strict;
case RpcHttpCookie.Types.SameSite.Lax:
return SameSiteMode.Lax;
case RpcHttpCookie.Types.SameSite.None:
return SameSiteMode.None;
default:
return SameSiteMode.None;
}
}
}
}
Loading