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

Allowing Node.js functions to access raw request body (#293) #296

Closed
wants to merge 2 commits into from
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
@@ -1,3 +1,6 @@
{
"key": "hyexydhln844f2mb7hgsup2yf8dowlb0885mbiq1"
"keys": {
"a": "hyexydhln844f2mb7hgsup2yf8dowlb0885mbiq1",
"b": "m3vg59azmxzxb8ofwwjeg738f654qjve0bwmyhte"
}
}
Original file line number Diff line number Diff line change
@@ -1,3 +1,6 @@
{
"key": "1388a6b0d05eca2237f10e4a4641260b0a08f3a5"
"keys": {
"a": "1388a6b0d05eca2237f10e4a4641260b0a08f3a5",
"b": "5cv9ac6o0c482yocidjxdjyhlvys516p4k8vtgm1"
}
}
4 changes: 2 additions & 2 deletions src/WebJobs.Script.WebHost/App_Data/secrets/host.json
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
{
"masterKey": "t8laajal0a1ajkgzoqlfv5gxr4ebhqozebw4qzdy",
"functionKey": "zlnu496ve212kk1p84ncrtdvmtpembduqp25ajjc"
"masterKey": "t8laajal0a1ajkgzoqlfv5gxr4ebhqozebw4qzdy",
"functionKey": "zlnu496ve212kk1p84ncrtdvmtpembduqp25ajjc"
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
{
"key": "wEmJalWXnxeG4RA4KzAXvM1rzpaCm7EWfDUiw/pf32bA9bd/a6O2dg==",
"keys": null
}
Original file line number Diff line number Diff line change
@@ -1,3 +1,3 @@
{
"key": "yKjiimZjC1FQoGlaIj8TUfGltnPE/f2LhgZNq6Fw9/XfAOGHmSgUlQ=="
"key": "lr5jlquf0ynr3p7jsu012kif4auv21qfivjftg8e"
}
34 changes: 20 additions & 14 deletions src/WebJobs.Script.WebHost/Filters/AuthorizationLevelAttribute.cs
Original file line number Diff line number Diff line change
Expand Up @@ -57,40 +57,45 @@ internal static AuthorizationLevel GetAuthorizationLevel(HttpRequestMessage requ
// first see if a key value is specified via headers or query string (header takes precidence)
IEnumerable<string> values;
string keyValue = null;
string keyId = null;
if (request.Headers.TryGetValues(FunctionsKeyHeaderName, out values))
{
// TODO: also allow keyId to be specified via header

keyValue = values.FirstOrDefault();
}
else
{
var queryParameters = request.GetQueryNameValuePairs().ToDictionary(p => p.Key, p => p.Value, StringComparer.OrdinalIgnoreCase);
queryParameters.TryGetValue("code", out keyValue);
queryParameters.TryGetValue("id", out keyId);
}

// if a key has been specified on the request, validate it
if (!string.IsNullOrEmpty(keyValue))
{
// see if the key specified is the master key
HostSecrets hostSecrets = secretManager.GetHostSecrets();
if (!string.IsNullOrEmpty(hostSecrets.MasterKey) &&
SecretEqual(keyValue, hostSecrets.MasterKey))
if (hostSecrets != null)
{
return AuthorizationLevel.Admin;
}
// see if the key specified matches the master key
if (SecretEqual(keyValue, hostSecrets.MasterKey))
{
return AuthorizationLevel.Admin;
}

// see if the key specified matches the host function key
if (!string.IsNullOrEmpty(hostSecrets.FunctionKey) &&
SecretEqual(keyValue, hostSecrets.FunctionKey))
{
return AuthorizationLevel.Function;
// see if the key specified matches the host function key
if (SecretEqual(keyValue, hostSecrets.FunctionKey))
{
return AuthorizationLevel.Function;
}
}

// if there is a function specific key specified try to match against that
// see if the specified key matches the function specific key
if (functionName != null)
{
FunctionSecrets functionSecrets = secretManager.GetFunctionSecrets(functionName);
if (functionSecrets != null &&
!string.IsNullOrEmpty(functionSecrets.Key) &&
SecretEqual(keyValue, functionSecrets.Key))
SecretEqual(keyValue, functionSecrets.GetKeyValue(keyId)))
{
return AuthorizationLevel.Function;
}
Expand All @@ -116,7 +121,8 @@ private static bool SecretEqual(string inputA, string inputB)
return true;
}

if (inputA == null || inputB == null || inputA.Length != inputB.Length)
if (string.IsNullOrEmpty(inputA) || string.IsNullOrEmpty(inputB) ||
inputA.Length != inputB.Length)
{
return false;
}
Expand Down
23 changes: 23 additions & 0 deletions src/WebJobs.Script.WebHost/FunctionSecrets.cs
Original file line number Diff line number Diff line change
@@ -1,13 +1,36 @@
// Copyright (c) .NET Foundation. All rights reserved.
// Licensed under the MIT License. See License.txt in the project root for license information.

using System.Collections.Generic;
using Newtonsoft.Json;

namespace WebJobs.Script.WebHost
{
public class FunctionSecrets
{
/// <summary>
/// Gets or sets the function specific key value. These keys only allow invocation of
/// the single function they apply to.
/// Can contain either a single key value, or multiple comma separated values.
/// </summary>
[JsonProperty(PropertyName = "key")]
public string Key { get; set; }

[JsonProperty(PropertyName = "keys")]
public Dictionary<string, string> Keys { get; set; }

public string GetKeyValue(string keyId)
{
string key = null;
if (keyId != null && Keys != null &&
Keys.TryGetValue(keyId, out key))
{
return key;
}
else
{
return Key;
}
}
}
}
1 change: 1 addition & 0 deletions src/WebJobs.Script.WebHost/GlobalSuppressions.cs
Original file line number Diff line number Diff line change
Expand Up @@ -21,3 +21,4 @@
[assembly: System.Diagnostics.CodeAnalysis.SuppressMessage("Microsoft.Reliability", "CA2000:Dispose objects before losing scope", Scope = "member", Target = "WebJobs.Script.WebHost.App_Start.AutofacBootstrap.#Initialize(Autofac.ContainerBuilder,WebJobs.Script.WebHost.WebHostSettings)")]
[assembly: System.Diagnostics.CodeAnalysis.SuppressMessage("Microsoft.Usage", "CA2227:CollectionPropertiesShouldBeReadOnly", Scope = "member", Target = "WebJobs.Script.WebHost.Models.HostStatus.#Errors")]
[assembly: System.Diagnostics.CodeAnalysis.SuppressMessage("Microsoft.Performance", "CA1810:InitializeReferenceTypeStaticFieldsInline", Scope = "member", Target = "WebJobs.Script.WebHost.Diagnostics.MetricsEventManager.#.cctor()")]
[assembly: System.Diagnostics.CodeAnalysis.SuppressMessage("Microsoft.Usage", "CA2227:CollectionPropertiesShouldBeReadOnly", Scope = "member", Target = "WebJobs.Script.WebHost.FunctionSecrets.#Keys")]
9 changes: 9 additions & 0 deletions src/WebJobs.Script.WebHost/HostSecrets.cs
Original file line number Diff line number Diff line change
Expand Up @@ -7,8 +7,17 @@ namespace WebJobs.Script.WebHost
{
public class HostSecrets
{
/// <summary>
/// Gets or sets the host master (admin) key value. This key allows invocation of
/// any function, and also permit access to additional admin operations.
/// </summary>
[JsonProperty(PropertyName = "masterKey")]
public string MasterKey { get; set; }

/// <summary>
/// Gets or sets the host level function key value. This key allows invocation of
/// any function.
/// </summary>
[JsonProperty(PropertyName = "functionKey")]
public string FunctionKey { get; set; }
}
Expand Down
75 changes: 75 additions & 0 deletions src/WebJobs.Script.WebHost/KeyValuesJsonConverter.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,75 @@
// 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.Globalization;
using Newtonsoft.Json;
using Newtonsoft.Json.Linq;

namespace WebJobs.Script.WebHost
{
public class KeyValuesJsonConverter : JsonConverter
{
public override void WriteJson(JsonWriter writer, object value, JsonSerializer serializer)
{
throw new NotSupportedException();
//base.WriteJson(writer, value, serializer);
//WriteValue(writer, value);
}

public override object ReadJson(JsonReader reader, Type objectType, object existingValue, JsonSerializer serializer)
{
return ReadValue(reader);
}

private object ReadValue(JsonReader reader)
{
while (reader.TokenType == JsonToken.Comment)
{
if (!reader.Read())
{
throw new JsonSerializationException("Unexpected Token when converting IDictionary<string, object>");
}
}

switch (reader.TokenType)
{
case JsonToken.StartArray:
return ReadArray(reader);
case JsonToken.String:
return reader.Value;
default:
string msg = string.Format(CultureInfo.InvariantCulture, "Unexpected token when converting IDictionary<string, object>: {0}", reader.TokenType);
throw new JsonSerializationException(msg);
}
}

private object ReadArray(JsonReader reader)
{
IList<object> list = new List<object>();

while (reader.Read())
{
switch (reader.TokenType)
{
case JsonToken.Comment:
break;
default:
var value = ReadValue(reader);
list.Add(value);
break;
case JsonToken.EndArray:
return list;
}
}

throw new JsonSerializationException("Unexpected end when reading IDictionary<string, object>");
}

public override bool CanConvert(Type objectType)
{
return typeof(IList<string>).IsAssignableFrom(objectType);
}
}
}
1 change: 1 addition & 0 deletions src/WebJobs.Script.WebHost/SecretManager.cs
Original file line number Diff line number Diff line change
Expand Up @@ -89,6 +89,7 @@ public virtual FunctionSecrets GetFunctionSecrets(string functionName)
else
{
// initialize with new secrets and save it
// TODO: generate in the new file format?
secrets = new FunctionSecrets
{
Key = GenerateSecretString()
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -17,13 +17,23 @@ public DynamicWebHookReceiverConfig(SecretManager secretManager)

public Task<string> GetReceiverConfigAsync(string name, string id)
{
string functionName = id;
string keyId = null;
int idx = id.IndexOf(':');
if (idx > 0)
{
functionName = id.Substring(0, idx);
keyId = id.Substring(idx + 1);
}

// "id" will be the function name
// we ignore the "name" parameter since we only allow a function
// to be mapped to a single receiver
FunctionSecrets secrets = _secretManager.GetFunctionSecrets(id);
FunctionSecrets secrets = _secretManager.GetFunctionSecrets(functionName);
if (secrets != null)
{
return Task.FromResult(secrets.Key);
string key = secrets.GetKeyValue(keyId);
return Task.FromResult(key);
}

return null;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -79,6 +79,14 @@ public async Task<HttpResponseMessage> HandleRequestAsync(FunctionDescriptor fun
await request.Content.LoadIntoBufferAsync();

string receiverId = function.Name.ToLowerInvariant();

Dictionary<string, string> queryParams = request.GetQueryNameValuePairs().ToDictionary(p => p.Key, p => p.Value, StringComparer.OrdinalIgnoreCase);
string keyId = null;
if (queryParams.TryGetValue("id", out keyId))
{
receiverId += ":" + keyId;
}

return await receiver.ReceiveAsync(receiverId, context, request);
}

Expand Down
2 changes: 1 addition & 1 deletion src/WebJobs.Script.WebHost/WebJobs.Script.WebHost.csproj
Original file line number Diff line number Diff line change
Expand Up @@ -321,7 +321,7 @@
</Content>
<None Include="App_Data\secrets\host.json" />
<None Include="App_Data\secrets\HttpTrigger.json" />
<Content Include="App_Data\secrets\webhook-azure-csharp.json" />
<Content Include="App_Data\secrets\WebHook-Azure-CSharp.json" />
<None Include="App_Data\secrets\WebHook-Generic-CSharp.json" />
<None Include="App_Data\secrets\WebHook-Generic.json" />
<None Include="Properties\PublishProfiles\FileSystem.pubxml" />
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -308,8 +308,9 @@ private Dictionary<string, object> CreateScriptExecutionContext(object input, IB
HttpRequestMessage request = (HttpRequestMessage)input;
string rawBody = null;
var requestObject = CreateRequestObject(request, out rawBody);
requestObject["rawBody"] = rawBody;
input = requestObject;

if (rawBody != null)
{
bindDataInput = rawBody;
Expand Down
20 changes: 13 additions & 7 deletions test/WebJobs.Script.Tests/NodeEndToEndTests.cs
Original file line number Diff line number Diff line change
Expand Up @@ -161,8 +161,10 @@ public async Task HttpTrigger_Get()

string body = await response.Content.ReadAsStringAsync();
JObject resultObject = JObject.Parse(body);
Assert.Equal((string)resultObject["reqBodyType"], "undefined");
Assert.Equal("undefined", (string)resultObject["reqBodyType"]);
Assert.Null((string)resultObject["reqBody"]);
Assert.Equal("undefined", (string)resultObject["reqRawBodyType"]);
Assert.Null((string)resultObject["reqRawBody"]);

// validate input headers
JObject reqHeaders = (JObject)resultObject["reqHeaders"];
Expand Down Expand Up @@ -191,8 +193,9 @@ public async Task HttpTrigger_Post_PlainText()

string body = await response.Content.ReadAsStringAsync();
JObject resultObject = JObject.Parse(body);
Assert.Equal((string)resultObject["reqBodyType"], "string");
Assert.Equal((string)resultObject["reqBody"], testData);
Assert.Equal("string", (string)resultObject["reqBodyType"]);
Assert.Equal(testData, (string)resultObject["reqBody"]);
Assert.Equal(testData, (string)resultObject["reqRawBody"]);
}

[Fact]
Expand All @@ -203,11 +206,12 @@ public async Task HttpTrigger_Post_Json()
{
{ "testData", testData }
};
string rawBody = testObject.ToString();
HttpRequestMessage request = new HttpRequestMessage
{
RequestUri = new Uri(string.Format("http://localhost/api/httptrigger")),
Method = HttpMethod.Post,
Content = new StringContent(testObject.ToString())
Content = new StringContent(rawBody)
};
request.Content.Headers.ContentType = new MediaTypeHeaderValue("application/json");

Expand All @@ -222,9 +226,11 @@ public async Task HttpTrigger_Post_Json()

string body = await response.Content.ReadAsStringAsync();
JObject resultObject = JObject.Parse(body);
Assert.Equal((string)resultObject["reqBodyType"], "object");
Assert.Equal((string)resultObject["reqBody"]["testData"], testData);
Assert.Equal((string)resultObject["bindingData"]["testData"], testData);
Assert.Equal("string", (string)resultObject["reqRawBodyType"]);
Assert.Equal(rawBody, (string)resultObject["reqRawBody"]);
Assert.Equal("object", (string)resultObject["reqBodyType"]);
Assert.Equal(testData, (string)resultObject["reqBody"]["testData"]);
Assert.Equal(testData, (string)resultObject["bindingData"]["testData"]);
}

[Fact]
Expand Down
Loading