Skip to content

Commit 3d923c5

Browse files
authored
[WebPubSub][Middleware] Integrate with REST client to improve using experience. (Azure#25428)
Sample ```cs public class WebPubSubSample { #region Snippet:WebPubSubDependencyInjection public void ConfigureServices(IServiceCollection services) { services.AddWebPubSub(o => { o.ServiceEndpoint = new("<connection-string>"); }).AddWebPubSubServiceClient<SampleHub>(); } #endregion #region Snippet:WebPubSubMapHub public void Configure(IApplicationBuilder app) { app.UseEndpoints(endpoint => { endpoint.MapWebPubSubHub<SampleHub>("/eventhandler"); }); } #endregion private sealed class SampleHub : WebPubSubHub { internal WebPubSubServiceClient<SampleHub> _serviceClient; // Need to ensure is injected by call `AddWebPubSubServiceClient<SampleHub>` in ConfigureServices. public SampleHub(WebPubSubServiceClient<SampleHub> serviceClient) { _serviceClient = serviceClient; } #region Snippet:WebPubSubConnectMethods public override ValueTask<ConnectEventResponse> OnConnectAsync(ConnectEventRequest request, CancellationToken cancellationToken) { _serviceClient.SendToAll($"Hello from {request.ConnectionContext.UserId}"); var response = new ConnectEventResponse { UserId = request.ConnectionContext.UserId }; return new ValueTask<ConnectEventResponse>(response); } #endregion #region Snippet:WebPubSubDefaultMethods public override ValueTask<UserEventResponse> OnMessageReceivedAsync(UserEventRequest request, CancellationToken cancellationToken) { _serviceClient.SendToAll(request.Data.ToString()); return base.OnMessageReceivedAsync(request, cancellationToken); } #endregion } } ```
1 parent 482a28b commit 3d923c5

20 files changed

+542
-276
lines changed

sdk/webpubsub/Microsoft.Azure.WebPubSub.AspNetCore/Microsoft.Azure.WebPubSub.AspNetCore.sln

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,8 @@ Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Microsoft.Azure.WebPubSub.C
99
EndProject
1010
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Microsoft.Azure.WebPubSub.AspNetCore.Tests", "tests\Microsoft.Azure.WebPubSub.AspNetCore.Tests.csproj", "{7ECB787F-ED29-4750-AAE9-A4E7F66E61CB}"
1111
EndProject
12+
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Azure.Messaging.WebPubSub", "..\Azure.Messaging.WebPubSub\src\Azure.Messaging.WebPubSub.csproj", "{9A3153A7-3D2E-46BE-9EC4-EDC79802CC6B}"
13+
EndProject
1214
Global
1315
GlobalSection(SolutionConfigurationPlatforms) = preSolution
1416
Debug|Any CPU = Debug|Any CPU
@@ -27,6 +29,10 @@ Global
2729
{7ECB787F-ED29-4750-AAE9-A4E7F66E61CB}.Debug|Any CPU.Build.0 = Debug|Any CPU
2830
{7ECB787F-ED29-4750-AAE9-A4E7F66E61CB}.Release|Any CPU.ActiveCfg = Release|Any CPU
2931
{7ECB787F-ED29-4750-AAE9-A4E7F66E61CB}.Release|Any CPU.Build.0 = Release|Any CPU
32+
{9A3153A7-3D2E-46BE-9EC4-EDC79802CC6B}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
33+
{9A3153A7-3D2E-46BE-9EC4-EDC79802CC6B}.Debug|Any CPU.Build.0 = Debug|Any CPU
34+
{9A3153A7-3D2E-46BE-9EC4-EDC79802CC6B}.Release|Any CPU.ActiveCfg = Release|Any CPU
35+
{9A3153A7-3D2E-46BE-9EC4-EDC79802CC6B}.Release|Any CPU.Build.0 = Release|Any CPU
3036
EndGlobalSection
3137
GlobalSection(SolutionProperties) = preSolution
3238
HideSolutionNode = FALSE

sdk/webpubsub/Microsoft.Azure.WebPubSub.AspNetCore/api/Microsoft.Azure.WebPubSub.AspNetCore.netcoreapp3.1.cs

Lines changed: 18 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,18 @@ public static partial class WebPubSubEndpointRouteBuilderExtensions
77
}
88
namespace Microsoft.Azure.WebPubSub.AspNetCore
99
{
10+
public partial interface IWebPubSubServerBuilder
11+
{
12+
Microsoft.Extensions.DependencyInjection.IServiceCollection Services { get; }
13+
}
14+
public partial class ServiceEndpoint
15+
{
16+
public ServiceEndpoint(string connectionString, Azure.Messaging.WebPubSub.WebPubSubServiceClientOptions clientOptions = null) { }
17+
public ServiceEndpoint(System.Uri endpoint, Azure.AzureKeyCredential credential, Azure.Messaging.WebPubSub.WebPubSubServiceClientOptions clientOptions = null) { }
18+
public ServiceEndpoint(System.Uri endpoint, Azure.Core.TokenCredential credential, Azure.Messaging.WebPubSub.WebPubSubServiceClientOptions clientOptions = null) { }
19+
public Azure.Messaging.WebPubSub.WebPubSubServiceClientOptions ClientOptions { get { throw null; } }
20+
public System.Uri Endpoint { get { throw null; } }
21+
}
1022
public abstract partial class WebPubSubHub
1123
{
1224
protected WebPubSubHub() { }
@@ -18,20 +30,19 @@ protected WebPubSubHub() { }
1830
public partial class WebPubSubOptions
1931
{
2032
public WebPubSubOptions() { }
21-
public Microsoft.Azure.WebPubSub.AspNetCore.WebPubSubValidationOptions ValidationOptions { get { throw null; } set { } }
33+
public Microsoft.Azure.WebPubSub.AspNetCore.ServiceEndpoint ServiceEndpoint { get { throw null; } set { } }
2234
}
23-
public partial class WebPubSubValidationOptions
35+
public partial class WebPubSubServiceClient<THub> : Azure.Messaging.WebPubSub.WebPubSubServiceClient where THub : Microsoft.Azure.WebPubSub.AspNetCore.WebPubSubHub
2436
{
25-
public WebPubSubValidationOptions(System.Collections.Generic.IEnumerable<string> connectionStrings) { }
26-
public WebPubSubValidationOptions(params string[] connectionStrings) { }
27-
public void Add(string connectionString) { }
37+
protected WebPubSubServiceClient() { }
2838
}
2939
}
3040
namespace Microsoft.Extensions.DependencyInjection
3141
{
3242
public static partial class WebPubSubDependencyInjectionExtensions
3343
{
34-
public static Microsoft.Extensions.DependencyInjection.IServiceCollection AddWebPubSub(this Microsoft.Extensions.DependencyInjection.IServiceCollection services) { throw null; }
35-
public static Microsoft.Extensions.DependencyInjection.IServiceCollection AddWebPubSub(this Microsoft.Extensions.DependencyInjection.IServiceCollection services, System.Action<Microsoft.Azure.WebPubSub.AspNetCore.WebPubSubOptions> configure) { throw null; }
44+
public static Microsoft.Azure.WebPubSub.AspNetCore.IWebPubSubServerBuilder AddWebPubSub(this Microsoft.Extensions.DependencyInjection.IServiceCollection services) { throw null; }
45+
public static Microsoft.Azure.WebPubSub.AspNetCore.IWebPubSubServerBuilder AddWebPubSub(this Microsoft.Extensions.DependencyInjection.IServiceCollection services, System.Action<Microsoft.Azure.WebPubSub.AspNetCore.WebPubSubOptions> configure) { throw null; }
46+
public static Microsoft.Azure.WebPubSub.AspNetCore.IWebPubSubServerBuilder AddWebPubSubServiceClient<THub>(this Microsoft.Azure.WebPubSub.AspNetCore.IWebPubSubServerBuilder builder) where THub : Microsoft.Azure.WebPubSub.AspNetCore.WebPubSubHub { throw null; }
3647
}
3748
}

sdk/webpubsub/Microsoft.Azure.WebPubSub.AspNetCore/src/Extensions/WebPubSubDependencyInjectionExtensions.cs

Lines changed: 36 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
// Copyright (c) Microsoft Corporation. All rights reserved.
22
// Licensed under the MIT License.
3+
34
using System;
45
using Microsoft.Azure.WebPubSub.AspNetCore;
56

@@ -15,8 +16,8 @@ public static class WebPubSubDependencyInjectionExtensions
1516
/// </summary>
1617
/// <param name="services">The <see cref="IServiceCollection"/>.</param>
1718
/// <param name="configure">A callback to configure the <see cref="WebPubSubOptions"/>.</param>
18-
/// <returns>The same instance of the <see cref="IServiceCollection"/>.</returns>
19-
public static IServiceCollection AddWebPubSub(this IServiceCollection services, Action<WebPubSubOptions> configure)
19+
/// <returns>The same instance of the <see cref="IWebPubSubServerBuilder"/>.</returns>
20+
public static IWebPubSubServerBuilder AddWebPubSub(this IServiceCollection services, Action<WebPubSubOptions> configure)
2021
{
2122
if (services == null)
2223
{
@@ -30,25 +31,51 @@ public static IServiceCollection AddWebPubSub(this IServiceCollection services,
3031

3132
services.Configure(configure);
3233

33-
services.AddWebPubSub();
34-
35-
return services;
34+
return services.AddWebPubSubCore();
3635
}
3736

3837
/// <summary>
3938
/// Adds the minimum essential Azure Web PubSub services to the <see cref="IServiceCollection"/>.
4039
/// </summary>
4140
/// <param name="services">The <see cref="IServiceCollection"/>.</param>
42-
/// <returns>The same instance of the <see cref="IServiceCollection"/>.</returns>
43-
public static IServiceCollection AddWebPubSub(this IServiceCollection services)
41+
/// <returns>The same instance of the <see cref="IWebPubSubServerBuilder"/>.</returns>
42+
public static IWebPubSubServerBuilder AddWebPubSub(this IServiceCollection services)
4443
{
4544
if (services == null)
4645
{
4746
throw new ArgumentNullException(nameof(services));
4847
}
4948

50-
return services.AddSingleton<ServiceRequestHandlerAdapter>()
51-
.AddSingleton<WebPubSubMarkerService>();
49+
// Add a default option to avoid null, inbound traffic validation will always succeed.
50+
// And customer will not be able to `AddWebPubSubServiceClient` in this case.
51+
return services.AddWebPubSub(o => o = new());
52+
}
53+
54+
/// <summary>
55+
/// Adds the Web PubSub service clients to be able to inject in <see cref="WebPubSubHub"/> and invoke service.
56+
/// </summary>
57+
/// <typeparam name="THub">User implemented <see cref="WebPubSubHub"/>.</typeparam>
58+
/// <param name="builder">The <see cref="IWebPubSubServerBuilder"/>.</param>
59+
/// <returns>The same instance of the <see cref="IWebPubSubServerBuilder"/>.</returns>
60+
public static IWebPubSubServerBuilder AddWebPubSubServiceClient<THub>(this IWebPubSubServerBuilder builder) where THub : WebPubSubHub
61+
{
62+
builder.Services.AddSingleton(typeof(WebPubSubServiceClient<THub>), sp =>
63+
{
64+
var factory = sp.GetRequiredService<WebPubSubServiceClientFactory>();
65+
return factory.Create<THub>();
66+
});
67+
return builder;
68+
}
69+
70+
private static IWebPubSubServerBuilder AddWebPubSubCore(this IServiceCollection services)
71+
{
72+
services.AddSingleton<ServiceRequestHandlerAdapter>()
73+
.AddSingleton<WebPubSubMarkerService>()
74+
.AddSingleton<WebPubSubServiceClientFactory>()
75+
.AddSingleton<RequestValidator>();
76+
77+
var builder = new WebPubSubServerBuilder(services);
78+
return builder;
5279
}
5380
}
5481
}

sdk/webpubsub/Microsoft.Azure.WebPubSub.AspNetCore/src/Extensions/WebPubSubRequestExtensions.cs

Lines changed: 12 additions & 66 deletions
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,6 @@
66
using System.IO;
77
using System.Linq;
88
using System.Net.Http.Headers;
9-
using System.Security.Cryptography;
109
using System.Text;
1110
using System.Text.Json;
1211
using System.Threading;
@@ -30,40 +29,30 @@ internal static class WebPubSubRequestExtensions
3029
/// <param name="request">Upstream HttpRequest.</param>
3130
/// <param name="options"></param>
3231
/// <param name="cancellationToken"></param>
33-
/// <returns>Deserialize <see cref="WebPubSubEventRequest"/></returns>
34-
internal static async Task<WebPubSubEventRequest> ReadWebPubSubEventAsync(this HttpRequest request, WebPubSubValidationOptions options = null, CancellationToken cancellationToken = default)
32+
/// <returns>Deserialize <see cref="WebPubSubEventRequest"/>.</returns>
33+
internal static async Task<WebPubSubEventRequest> ReadWebPubSubEventAsync(this HttpRequest request, RequestValidator options, CancellationToken cancellationToken = default)
3534
{
3635
if (request == null)
3736
{
3837
throw new ArgumentNullException(nameof(request));
3938
}
39+
if (options == null)
40+
{
41+
throw new ArgumentNullException(nameof(options));
42+
}
4043

4144
// validation request.
42-
if (request.IsPreflightRequest(out var requestHosts))
45+
if (request.IsPreflightRequest(out var requestOrigins))
4346
{
44-
if (options == null || !options.ContainsHost())
45-
{
46-
return new PreflightRequest(true);
47-
}
48-
else
49-
{
50-
foreach (var item in requestHosts)
51-
{
52-
if (options.ContainsHost(item))
53-
{
54-
return new PreflightRequest(true);
55-
}
56-
}
57-
}
58-
return new PreflightRequest(false);
47+
return new PreflightRequest(options.IsValidOrigin(requestOrigins));
5948
}
6049

6150
if (!request.TryParseCloudEvents(out var context))
6251
{
6352
throw new ArgumentException("Invalid Web PubSub upstream request missing required fields in header.");
6453
}
6554

66-
if (!context.IsValidSignature(options))
55+
if (!options.IsValidSignature(context))
6756
{
6857
throw new UnauthorizedAccessException("Signature validation failed.");
6958
}
@@ -104,51 +93,18 @@ internal static async Task<WebPubSubEventRequest> ReadWebPubSubEventAsync(this H
10493
}
10594
}
10695

107-
internal static bool IsPreflightRequest(this HttpRequest request, out List<string> requestHosts)
96+
internal static bool IsPreflightRequest(this HttpRequest request, out IReadOnlyList<string> requestOrigins)
10897
{
10998
if (HttpMethods.IsOptions(request.Method))
11099
{
111100
request.Headers.TryGetValue(Constants.Headers.WebHookRequestOrigin, out StringValues requestOrigin);
112101
if (requestOrigin.Count > 0)
113102
{
114-
requestHosts = requestOrigin.ToList();
115-
return true;
116-
}
117-
}
118-
requestHosts = null;
119-
return false;
120-
}
121-
122-
internal static bool IsValidSignature(this WebPubSubConnectionContext connectionContext, WebPubSubValidationOptions options)
123-
{
124-
// no options skip validation.
125-
if (options == null || !options.ContainsHost())
126-
{
127-
return true;
128-
}
129-
130-
// TODO: considering add cache to improve.
131-
if (options.TryGetKey(connectionContext.Origin, out var accessKey))
132-
{
133-
// server side disable signature checks.
134-
if (string.IsNullOrEmpty(accessKey))
135-
{
136-
return true;
137-
}
138-
139-
var signatures = connectionContext.Signature.ToHeaderList();
140-
if (signatures == null)
141-
{
142-
return false;
143-
}
144-
using var hmac = new HMACSHA256(Encoding.UTF8.GetBytes(accessKey));
145-
var hashBytes = hmac.ComputeHash(Encoding.UTF8.GetBytes(connectionContext.ConnectionId));
146-
var hash = "sha256=" + BitConverter.ToString(hashBytes).Replace("-", "");
147-
if (signatures.Contains(hash, StringComparer.OrdinalIgnoreCase))
148-
{
103+
requestOrigins = requestOrigin;
149104
return true;
150105
}
151106
}
107+
requestOrigins = null;
152108
return false;
153109
}
154110

@@ -274,16 +230,6 @@ private static bool IsValidMediaType(this string mediaType, out WebPubSubDataTyp
274230
}
275231
}
276232

277-
private static IReadOnlyList<string> ToHeaderList(this string signatures)
278-
{
279-
if (string.IsNullOrEmpty(signatures))
280-
{
281-
return default;
282-
}
283-
284-
return signatures.Split(Constants.HeaderSeparator, StringSplitOptions.RemoveEmptyEntries);
285-
}
286-
287233
private static WebPubSubEventType GetEventType(this string ceType)
288234
{
289235
return ceType.StartsWith(Constants.Headers.CloudEvents.TypeSystemPrefix, StringComparison.OrdinalIgnoreCase) ?
Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,18 @@
1+
// Copyright (c) Microsoft Corporation. All rights reserved.
2+
// Licensed under the MIT License.
3+
4+
using Microsoft.Extensions.DependencyInjection;
5+
6+
namespace Microsoft.Azure.WebPubSub.AspNetCore
7+
{
8+
/// <summary>
9+
/// A builder abstraction for configuring Web PubSub server.
10+
/// </summary>
11+
public interface IWebPubSubServerBuilder
12+
{
13+
/// <summary>
14+
/// Gets the builder service collection.
15+
/// </summary>
16+
IServiceCollection Services { get; }
17+
}
18+
}
Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,13 @@
1+
// Copyright (c) Microsoft Corporation. All rights reserved.
2+
// Licensed under the MIT License.
3+
4+
namespace Microsoft.Azure.WebPubSub.AspNetCore
5+
{
6+
internal enum CredentialKind
7+
{
8+
None,
9+
ConnectionString,
10+
AzureKeyCredential,
11+
TokenCredential,
12+
}
13+
}

0 commit comments

Comments
 (0)