Skip to content

Commit 866f12b

Browse files
omkarmore83safihamid
authored andcommitted
using Function Executor to call local functions
Refactoring code to reuse methods in FunctionController Refactoring to use scriptconstants Fixing unit tests Refactoring based on CR Comments Code rebase and fix some of the PR comments.
1 parent 4d981ef commit 866f12b

File tree

8 files changed

+241
-90
lines changed

8 files changed

+241
-90
lines changed

src/WebJobs.Script.WebHost/Controllers/FunctionsController.cs

Lines changed: 12 additions & 88 deletions
Original file line numberDiff line numberDiff line change
@@ -11,8 +11,10 @@
1111
using System.Web.Http;
1212
using System.Web.Http.Controllers;
1313
using System.Web.Http.Dependencies;
14+
using Microsoft.Azure.AppService.Proxy.Client.Contract;
1415
using Microsoft.Azure.WebJobs.Extensions.Http;
1516
using Microsoft.Azure.WebJobs.Script.Description;
17+
using Microsoft.Azure.WebJobs.Script.Host;
1618
using Microsoft.Azure.WebJobs.Script.WebHost.Filters;
1719
using Microsoft.Azure.WebJobs.Script.WebHost.Properties;
1820
using Microsoft.Azure.WebJobs.Script.WebHost.WebHooks;
@@ -33,110 +35,32 @@ public FunctionsController(WebScriptHostManager scriptHostManager, WebHookReceiv
3335
_webHookReceiverManager = webHookReceiverManager;
3436
}
3537

36-
private static bool IsHomepageDisabled
37-
{
38-
get
39-
{
40-
return string.Equals(Environment.GetEnvironmentVariable(EnvironmentSettingNames.AzureWebJobsDisableHomepage),
41-
bool.TrueString, StringComparison.OrdinalIgnoreCase);
42-
}
43-
}
44-
4538
public override async Task<HttpResponseMessage> ExecuteAsync(HttpControllerContext controllerContext, CancellationToken cancellationToken)
4639
{
4740
var request = controllerContext.Request;
4841
var function = _scriptHostManager.GetHttpFunctionOrNull(request);
49-
if (function == null)
50-
{
51-
if (request.RequestUri.AbsolutePath == "/")
52-
{
53-
// if the request is to the root and we can't find any matching FunctionDescriptors which might have been setup by proxies
54-
// then homepage logic will be applied.
55-
return (IsHomepageDisabled || request.IsAntaresInternalRequest())
56-
? new HttpResponseMessage(HttpStatusCode.NoContent)
57-
: new HttpResponseMessage(HttpStatusCode.OK)
58-
{
59-
Content = new StringContent(Resources.Homepage, Encoding.UTF8, "text/html")
60-
};
61-
}
6242

63-
// request does not map to an HTTP function
64-
return new HttpResponseMessage(HttpStatusCode.NotFound);
65-
}
66-
67-
request.SetProperty(ScriptConstants.AzureFunctionsHttpFunctionKey, function);
43+
var secretManager = controllerContext.Configuration.DependencyResolver.GetService<ISecretManager>();
44+
var functionRequestInvoker = new FunctionRequestInvoker(function, secretManager);
45+
var response = await functionRequestInvoker.PreprocessRequestAsync(request);
6846

69-
var authorizationLevel = await DetermineAuthorizationLevelAsync(request, function, controllerContext.Configuration.DependencyResolver);
70-
if (function.Metadata.IsExcluded ||
71-
(function.Metadata.IsDisabled && !(request.IsAuthDisabled() || authorizationLevel == AuthorizationLevel.Admin)))
47+
if (response != null)
7248
{
73-
// disabled functions are not publicly addressable w/o Admin level auth,
74-
// and excluded functions are also ignored here (though the check above will
75-
// already exclude them)
76-
return new HttpResponseMessage(HttpStatusCode.NotFound);
49+
return response;
7750
}
7851

7952
Func<HttpRequestMessage, CancellationToken, Task<HttpResponseMessage>> processRequestHandler = async (req, ct) =>
8053
{
81-
return await ProcessRequestAsync(req, function, ct);
54+
return await functionRequestInvoker.ProcessRequestAsync(req, ct, _scriptHostManager, _webHookReceiverManager);
8255
};
83-
return await _scriptHostManager.HttpRequestManager.ProcessRequestAsync(request, processRequestHandler, cancellationToken);
84-
}
8556

86-
public static async Task<AuthorizationLevel> DetermineAuthorizationLevelAsync(HttpRequestMessage request, FunctionDescriptor function, IDependencyResolver resolver)
87-
{
88-
var secretManager = resolver.GetService<ISecretManager>();
89-
var authorizationResult = await AuthorizationLevelAttribute.GetAuthorizationResultAsync(request, secretManager, functionName: function.Name);
90-
var authorizationLevel = authorizationResult.AuthorizationLevel;
91-
request.SetAuthorizationLevel(authorizationLevel);
92-
request.SetProperty(ScriptConstants.AzureFunctionsHttpRequestKeyNameKey, authorizationResult.KeyName);
93-
94-
return authorizationLevel;
95-
}
96-
97-
private async Task<HttpResponseMessage> ProcessRequestAsync(HttpRequestMessage request, FunctionDescriptor function, CancellationToken cancellationToken)
98-
{
99-
var httpTrigger = function.GetTriggerAttributeOrNull<HttpTriggerAttribute>();
100-
bool isWebHook = !string.IsNullOrEmpty(httpTrigger.WebHookType);
101-
var authorizationLevel = request.GetAuthorizationLevel();
102-
HttpResponseMessage response = null;
103-
104-
if (isWebHook)
57+
if (function.Metadata.IsProxy)
10558
{
106-
if (request.HasAuthorizationLevel(AuthorizationLevel.Admin))
107-
{
108-
// Admin level requests bypass the WebHook auth pipeline
109-
response = await _scriptHostManager.HandleRequestAsync(function, request, cancellationToken);
110-
}
111-
else
112-
{
113-
// This is a WebHook request so define a delegate for the user function.
114-
// The WebHook Receiver pipeline will first validate the request fully
115-
// then invoke this callback.
116-
Func<HttpRequestMessage, Task<HttpResponseMessage>> invokeFunction = async (req) =>
117-
{
118-
// Reset the content stream before passing the request down to the function
119-
Stream stream = await req.Content.ReadAsStreamAsync();
120-
stream.Seek(0, SeekOrigin.Begin);
121-
122-
return await _scriptHostManager.HandleRequestAsync(function, req, cancellationToken);
123-
};
124-
response = await _webHookReceiverManager.HandleRequestAsync(function, request, invokeFunction);
125-
}
59+
IFuncExecutor proxyFunctionExecutor = new ProxyFunctionExecutor(this._scriptHostManager, _webHookReceiverManager, secretManager);
60+
request.Properties.Add(ScriptConstants.AzureProxyFunctionExecutorKey, proxyFunctionExecutor);
12661
}
127-
else
128-
{
129-
// Authorize
130-
if (!request.HasAuthorizationLevel(httpTrigger.AuthLevel))
131-
{
132-
return new HttpResponseMessage(HttpStatusCode.Unauthorized);
133-
}
13462

135-
// Not a WebHook request so dispatch directly
136-
response = await _scriptHostManager.HandleRequestAsync(function, request, cancellationToken);
137-
}
138-
139-
return response;
63+
return await _scriptHostManager.HttpRequestManager.ProcessRequestAsync(request, processRequestHandler, cancellationToken);
14064
}
14165
}
14266
}
Lines changed: 129 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,129 @@
1+
// Copyright (c) .NET Foundation. All rights reserved.
2+
// Licensed under the MIT License. See License.txt in the project root for license information.
3+
4+
using System;
5+
using System.IO;
6+
using System.Net;
7+
using System.Net.Http;
8+
using System.Text;
9+
using System.Threading;
10+
using System.Threading.Tasks;
11+
using System.Web.Http.Dependencies;
12+
using Microsoft.Azure.WebJobs.Extensions.Http;
13+
using Microsoft.Azure.WebJobs.Script.Description;
14+
using Microsoft.Azure.WebJobs.Script.WebHost;
15+
using Microsoft.Azure.WebJobs.Script.WebHost.Filters;
16+
using Microsoft.Azure.WebJobs.Script.WebHost.Properties;
17+
using Microsoft.Azure.WebJobs.Script.WebHost.WebHooks;
18+
19+
namespace Microsoft.Azure.WebJobs.Script.Host
20+
{
21+
public class FunctionRequestInvoker
22+
{
23+
private readonly ISecretManager _secretManager;
24+
private readonly FunctionDescriptor _function;
25+
26+
public FunctionRequestInvoker(FunctionDescriptor function, ISecretManager secretManager)
27+
{
28+
_secretManager = secretManager;
29+
_function = function;
30+
}
31+
32+
private static bool IsHomepageDisabled
33+
{
34+
get
35+
{
36+
return string.Equals(Environment.GetEnvironmentVariable(EnvironmentSettingNames.AzureWebJobsDisableHomepage),
37+
bool.TrueString, StringComparison.OrdinalIgnoreCase);
38+
}
39+
}
40+
41+
public async Task<HttpResponseMessage> ProcessRequestAsync(HttpRequestMessage request, CancellationToken cancellationToken, WebScriptHostManager scriptHostManager, WebHookReceiverManager webHookReceiverManager)
42+
{
43+
var httpTrigger = _function.GetTriggerAttributeOrNull<HttpTriggerAttribute>();
44+
bool isWebHook = !string.IsNullOrEmpty(httpTrigger.WebHookType);
45+
var authorizationLevel = request.GetAuthorizationLevel();
46+
HttpResponseMessage response = null;
47+
48+
if (isWebHook)
49+
{
50+
if (request.HasAuthorizationLevel(AuthorizationLevel.Admin))
51+
{
52+
// Admin level requests bypass the WebHook auth pipeline
53+
response = await scriptHostManager.HandleRequestAsync(_function, request, cancellationToken);
54+
}
55+
else
56+
{
57+
// This is a WebHook request so define a delegate for the user function.
58+
// The WebHook Receiver pipeline will first validate the request fully
59+
// then invoke this callback.
60+
Func<HttpRequestMessage, Task<HttpResponseMessage>> invokeFunction = async (req) =>
61+
{
62+
// Reset the content stream before passing the request down to the function
63+
Stream stream = await req.Content.ReadAsStreamAsync();
64+
stream.Seek(0, SeekOrigin.Begin);
65+
66+
return await scriptHostManager.HandleRequestAsync(_function, req, cancellationToken);
67+
};
68+
response = await webHookReceiverManager.HandleRequestAsync(_function, request, invokeFunction);
69+
}
70+
}
71+
else
72+
{
73+
// Authorize
74+
if (!request.HasAuthorizationLevel(httpTrigger.AuthLevel))
75+
{
76+
return new HttpResponseMessage(HttpStatusCode.Unauthorized);
77+
}
78+
79+
// Not a WebHook request so dispatch directly
80+
response = await scriptHostManager.HandleRequestAsync(_function, request, cancellationToken);
81+
}
82+
83+
return response;
84+
}
85+
86+
public async Task<HttpResponseMessage> PreprocessRequestAsync(HttpRequestMessage request)
87+
{
88+
if (_function == null)
89+
{
90+
if (request.RequestUri.AbsolutePath == "/")
91+
{
92+
// if the request is to the root and we can't find any matching FunctionDescriptors which might have been setup by proxies
93+
// then homepage logic will be applied.
94+
return (IsHomepageDisabled || request.IsAntaresInternalRequest())
95+
? new HttpResponseMessage(HttpStatusCode.NoContent)
96+
: new HttpResponseMessage(HttpStatusCode.OK)
97+
{
98+
Content = new StringContent(Resources.Homepage, Encoding.UTF8, "text/html")
99+
};
100+
}
101+
102+
// request does not map to an HTTP function
103+
return new HttpResponseMessage(HttpStatusCode.NotFound);
104+
}
105+
106+
request.SetProperty(ScriptConstants.AzureFunctionsHttpFunctionKey, _function);
107+
108+
var authorizationLevel = await DetermineAuthorizationLevelAsync(request);
109+
if (_function.Metadata.IsDisabled && !(request.IsAuthDisabled() || authorizationLevel == AuthorizationLevel.Admin))
110+
{
111+
// disabled functions are not publicly addressable w/o Admin level auth,
112+
// and excluded functions are also ignored here
113+
return new HttpResponseMessage(HttpStatusCode.NotFound);
114+
}
115+
116+
return null;
117+
}
118+
119+
private async Task<AuthorizationLevel> DetermineAuthorizationLevelAsync(HttpRequestMessage request)
120+
{
121+
var authorizationResult = await AuthorizationLevelAttribute.GetAuthorizationResultAsync(request, _secretManager, functionName: _function.Name);
122+
var authorizationLevel = authorizationResult.AuthorizationLevel;
123+
request.SetAuthorizationLevel(authorizationLevel);
124+
request.SetProperty(ScriptConstants.AzureFunctionsHttpRequestKeyNameKey, authorizationResult.KeyName);
125+
126+
return authorizationLevel;
127+
}
128+
}
129+
}
Lines changed: 58 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,58 @@
1+
// Copyright (c) .NET Foundation. All rights reserved.
2+
// Licensed under the MIT License. See License.txt in the project root for license information.
3+
4+
using System;
5+
using System.Collections.Generic;
6+
using System.IO;
7+
using System.Net;
8+
using System.Net.Http;
9+
using System.Threading;
10+
using System.Threading.Tasks;
11+
using System.Web.Http.Dependencies;
12+
using Microsoft.Azure.AppService.Proxy.Client.Contract;
13+
using Microsoft.Azure.WebJobs.Extensions.Http;
14+
using Microsoft.Azure.WebJobs.Script.Description;
15+
using Microsoft.Azure.WebJobs.Script.WebHost;
16+
using Microsoft.Azure.WebJobs.Script.WebHost.Controllers;
17+
using Microsoft.Azure.WebJobs.Script.WebHost.WebHooks;
18+
19+
namespace Microsoft.Azure.WebJobs.Script.Host
20+
{
21+
public class ProxyFunctionExecutor : IFuncExecutor
22+
{
23+
private readonly WebScriptHostManager _scriptHostManager;
24+
private readonly ISecretManager _secretManager;
25+
private WebHookReceiverManager _webHookReceiverManager;
26+
27+
internal ProxyFunctionExecutor(WebScriptHostManager scriptHostManager, WebHookReceiverManager webHookReceiverManager, ISecretManager secretManager)
28+
{
29+
_scriptHostManager = scriptHostManager;
30+
_webHookReceiverManager = webHookReceiverManager;
31+
_secretManager = secretManager;
32+
}
33+
34+
public async Task ExecuteFuncAsync(string funcName, Dictionary<string, object> arguments, CancellationToken cancellationToken)
35+
{
36+
HttpRequestMessage request = arguments[ScriptConstants.AzureFunctionsHttpRequestKey] as HttpRequestMessage;
37+
var function = _scriptHostManager.GetHttpFunctionOrNull(request);
38+
39+
var functionRequestInvoker = new FunctionRequestInvoker(function, _secretManager);
40+
var response = await functionRequestInvoker.PreprocessRequestAsync(request);
41+
42+
if (response != null)
43+
{
44+
request.Properties[ScriptConstants.AzureFunctionsHttpResponseKey] = response;
45+
return;
46+
}
47+
48+
Func<HttpRequestMessage, CancellationToken, Task<HttpResponseMessage>> processRequestHandler = async (req, ct) =>
49+
{
50+
return await functionRequestInvoker.ProcessRequestAsync(req, ct, _scriptHostManager, _webHookReceiverManager);
51+
};
52+
53+
var resp = await _scriptHostManager.HttpRequestManager.ProcessRequestAsync(request, processRequestHandler, cancellationToken);
54+
request.Properties[ScriptConstants.AzureFunctionsHttpResponseKey] = resp;
55+
return;
56+
}
57+
}
58+
}

src/WebJobs.Script.WebHost/WebJobs.Script.WebHost.csproj

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -504,6 +504,8 @@
504504
<DesignTime>True</DesignTime>
505505
<DependentUpon>Resources.resx</DependentUpon>
506506
</Compile>
507+
<Compile Include="FunctionRequestInvoker.cs" />
508+
<Compile Include="ProxyFunctionExecutor.cs" />
507509
<Compile Include="Security\BlobStorageSecretsRepository.cs" />
508510
<Compile Include="Security\DefaultSecretManagerFactory.cs" />
509511
<Compile Include="Security\DefaultSecretsRepositoryFactory.cs" />

src/WebJobs.Script.WebHost/WebScriptHostManager.cs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -316,7 +316,7 @@ internal static void AddRouteDataToRequest(IHttpRouteData routeData, HttpRequest
316316
routeDataValues.Add(pair.Key, value);
317317
}
318318

319-
request.Properties.Add(HttpExtensionConstants.AzureWebJobsHttpRouteDataKey, routeDataValues);
319+
request.Properties[HttpExtensionConstants.AzureWebJobsHttpRouteDataKey] = routeDataValues;
320320
}
321321
}
322322

src/WebJobs.Script/Description/Proxies/ProxyClientExecutor.cs

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -24,7 +24,9 @@ public ProxyData GetProxyData()
2424

2525
public async Task Execute(HttpRequestMessage request, ILogger logger)
2626
{
27-
await _proxyClient.CallAsync(new object[] { request }, null, logger);
27+
IFuncExecutor proxyFunctionExecutor = null;
28+
request.Properties.TryGetValue(ScriptConstants.AzureProxyFunctionExecutorKey, out proxyFunctionExecutor);
29+
await _proxyClient.CallAsync(new object[] { request }, proxyFunctionExecutor, logger);
2830
}
2931
}
3032
}

src/WebJobs.Script/ScriptConstants.cs

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,8 @@ public static class ScriptConstants
1414
public const string AzureFunctionsHttpRequestAuthorizationDisabledKey = "MS_AzureFunctionsAuthorizationDisabled";
1515
public const string AzureFunctionsHttpFunctionKey = "MS_AzureFunctionsHttpFunction";
1616
public const string AzureFunctionsRequestIdKey = "MS_AzureFunctionsRequestID";
17+
public const string AzureFunctionsHttpRequestKey = "MS_AzureFunctionsHttpRequest";
18+
public const string AzureProxyFunctionExecutorKey = "MS_AzureProxyFunctionExecutor";
1719

1820
public const string TracePropertyPrimaryHostKey = "MS_PrimaryHost";
1921
public const string TracePropertyFunctionNameKey = "MS_FunctionName";

0 commit comments

Comments
 (0)