Added endpoint routing support (#7608)
- Basic endpoint routing support to for SignalR hubs, ConnectionHandler and IConnectionBuilder endpoints - Updated all functional tests and samples to use it - Added all attributes as metadata from Hubs and ConnectionHandlers - Added a test to verify client is rejected if auth is ineffective
This commit is contained in:
parent
8daca5ec3a
commit
5ef51822de
|
|
@ -899,6 +899,33 @@ namespace Microsoft.AspNetCore.SignalR.Client.FunctionalTests
|
|||
}
|
||||
}
|
||||
|
||||
[Theory]
|
||||
[MemberData(nameof(TransportTypesWithAuth))]
|
||||
public async Task ClientWillFailAuthEndPointIfNotAuthorized(HttpTransportType transportType, string hubPath)
|
||||
{
|
||||
bool ExpectedErrors(WriteContext writeContext)
|
||||
{
|
||||
return writeContext.Exception is HttpRequestException;
|
||||
}
|
||||
|
||||
using (StartServer<Startup>(out var server, ExpectedErrors))
|
||||
{
|
||||
var hubConnection = new HubConnectionBuilder()
|
||||
.WithLoggerFactory(LoggerFactory)
|
||||
.WithUrl(server.Url + hubPath, transportType)
|
||||
.Build();
|
||||
try
|
||||
{
|
||||
var ex = await Assert.ThrowsAnyAsync<HttpRequestException>(() => hubConnection.StartAsync().OrTimeout());
|
||||
Assert.Equal("Response status code does not indicate success: 401 (Unauthorized).", ex.Message);
|
||||
}
|
||||
finally
|
||||
{
|
||||
await hubConnection.DisposeAsync().OrTimeout();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
[Theory]
|
||||
[MemberData(nameof(TransportTypes))]
|
||||
public async Task ClientCanUseJwtBearerTokenForAuthenticationWhenRedirected(HttpTransportType transportType)
|
||||
|
|
@ -1172,6 +1199,17 @@ namespace Microsoft.AspNetCore.SignalR.Client.FunctionalTests
|
|||
}
|
||||
}
|
||||
|
||||
public static IEnumerable<object[]> TransportTypesWithAuth()
|
||||
{
|
||||
foreach (var transport in TransportTypes().SelectMany(t => t).Cast<HttpTransportType>())
|
||||
{
|
||||
foreach (var path in new[] { "/authorizedhub", "/authorizedhub2" })
|
||||
{
|
||||
yield return new object[] { transport, path };
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// This list excludes "special" hub paths like "default-nowebsockets" which exist for specific tests.
|
||||
public static string[] HubPaths = new[] { "/default", "/dynamic", "/hubT" };
|
||||
|
||||
|
|
|
|||
|
|
@ -221,4 +221,10 @@ namespace Microsoft.AspNetCore.SignalR.Client.FunctionalTests
|
|||
{
|
||||
public string Echo(string message) => TestHubMethodsImpl.Echo(message);
|
||||
}
|
||||
|
||||
// Authorization is added via endpoint routing in Startup
|
||||
public class HubWithAuthorization2 : Hub
|
||||
{
|
||||
public string Echo(string message) => TestHubMethodsImpl.Echo(message);
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -5,9 +5,11 @@ using System;
|
|||
using System.IdentityModel.Tokens.Jwt;
|
||||
using System.Security.Claims;
|
||||
using Microsoft.AspNetCore.Authentication.JwtBearer;
|
||||
using Microsoft.AspNetCore.Authorization;
|
||||
using Microsoft.AspNetCore.Builder;
|
||||
using Microsoft.AspNetCore.Http;
|
||||
using Microsoft.AspNetCore.Http.Connections;
|
||||
using Microsoft.AspNetCore.Routing;
|
||||
using Microsoft.Extensions.DependencyInjection;
|
||||
using Microsoft.IdentityModel.Tokens;
|
||||
using Newtonsoft.Json;
|
||||
|
|
@ -21,11 +23,9 @@ namespace Microsoft.AspNetCore.SignalR.Client.FunctionalTests
|
|||
|
||||
public void ConfigureServices(IServiceCollection services)
|
||||
{
|
||||
services.AddSignalR(options =>
|
||||
{
|
||||
options.EnableDetailedErrors = true;
|
||||
})
|
||||
.AddMessagePackProtocol();
|
||||
services.AddSignalR(options => options.EnableDetailedErrors = true)
|
||||
.AddMessagePackProtocol();
|
||||
|
||||
services.AddSingleton<IUserIdProvider, HeaderUserIdProvider>();
|
||||
services.AddAuthorization(options =>
|
||||
{
|
||||
|
|
@ -35,50 +35,53 @@ namespace Microsoft.AspNetCore.SignalR.Client.FunctionalTests
|
|||
policy.RequireClaim(ClaimTypes.NameIdentifier);
|
||||
});
|
||||
});
|
||||
|
||||
services.AddAuthentication(JwtBearerDefaults.AuthenticationScheme)
|
||||
.AddJwtBearer(options =>
|
||||
{
|
||||
options.TokenValidationParameters =
|
||||
new TokenValidationParameters
|
||||
.AddJwtBearer(options =>
|
||||
{
|
||||
ValidateAudience = false,
|
||||
ValidateIssuer = false,
|
||||
ValidateActor = false,
|
||||
ValidateLifetime = true,
|
||||
IssuerSigningKey = SecurityKey
|
||||
};
|
||||
});
|
||||
options.TokenValidationParameters =
|
||||
new TokenValidationParameters
|
||||
{
|
||||
ValidateAudience = false,
|
||||
ValidateIssuer = false,
|
||||
ValidateActor = false,
|
||||
ValidateLifetime = true,
|
||||
IssuerSigningKey = SecurityKey
|
||||
};
|
||||
});
|
||||
}
|
||||
|
||||
public void Configure(IApplicationBuilder app)
|
||||
{
|
||||
app.UseAuthentication();
|
||||
|
||||
app.UseSignalR(routes =>
|
||||
app.UseRouting(routes =>
|
||||
{
|
||||
routes.MapHub<TestHub>("/default");
|
||||
routes.MapHub<DynamicTestHub>("/dynamic");
|
||||
routes.MapHub<TestHubT>("/hubT");
|
||||
routes.MapHub<HubWithAuthorization>("/authorizedhub");
|
||||
routes.MapHub<TestHub>("/default-nowebsockets", options => options.Transports = HttpTransportType.LongPolling | HttpTransportType.ServerSentEvents);
|
||||
});
|
||||
routes.MapHub<HubWithAuthorization2>("/authorizedhub2")
|
||||
.RequireAuthorization(new AuthorizeAttribute(JwtBearerDefaults.AuthenticationScheme));
|
||||
|
||||
app.Run(async (context) =>
|
||||
{
|
||||
if (context.Request.Path.StartsWithSegments("/generateJwtToken"))
|
||||
routes.MapHub<TestHub>("/default-nowebsockets", options => options.Transports = HttpTransportType.LongPolling | HttpTransportType.ServerSentEvents);
|
||||
|
||||
routes.MapGet("/generateJwtToken", context =>
|
||||
{
|
||||
await context.Response.WriteAsync(GenerateJwtToken());
|
||||
return;
|
||||
}
|
||||
else if (context.Request.Path.StartsWithSegments("/redirect"))
|
||||
return context.Response.WriteAsync(GenerateJwtToken());
|
||||
});
|
||||
|
||||
routes.Map("/redirect/{*anything}", context =>
|
||||
{
|
||||
await context.Response.WriteAsync(JsonConvert.SerializeObject(new
|
||||
return context.Response.WriteAsync(JsonConvert.SerializeObject(new
|
||||
{
|
||||
url = $"{context.Request.Scheme}://{context.Request.Host}/authorizedHub",
|
||||
accessToken = GenerateJwtToken()
|
||||
}));
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
app.UseAuthorization();
|
||||
}
|
||||
|
||||
private string GenerateJwtToken()
|
||||
|
|
|
|||
|
|
@ -12,6 +12,7 @@ using Microsoft.AspNetCore.Builder;
|
|||
using Microsoft.AspNetCore.Hosting;
|
||||
using Microsoft.AspNetCore.Http;
|
||||
using Microsoft.AspNetCore.Http.Connections;
|
||||
using Microsoft.AspNetCore.Routing;
|
||||
using Microsoft.Extensions.DependencyInjection;
|
||||
using Microsoft.Extensions.Primitives;
|
||||
using Microsoft.IdentityModel.Tokens;
|
||||
|
|
@ -122,39 +123,21 @@ namespace FunctionalTests
|
|||
return next.Invoke();
|
||||
});
|
||||
|
||||
app.UseConnections(routes =>
|
||||
{
|
||||
routes.MapConnectionHandler<EchoConnectionHandler>("/echo");
|
||||
});
|
||||
|
||||
app.Use(async (context, next) =>
|
||||
{
|
||||
if (context.Request.Path.Value.Contains("/negotiate"))
|
||||
{
|
||||
context.Response.Cookies.Append("testCookie", "testValue");
|
||||
context.Response.Cookies.Append("testCookie2", "testValue2");
|
||||
context.Response.Cookies.Append("expiredCookie", "doesntmatter", new CookieOptions() { Expires = DateTimeOffset.Now.AddHours(-1) });
|
||||
}
|
||||
await next.Invoke();
|
||||
});
|
||||
|
||||
app.UseSignalR(routes =>
|
||||
app.UseRouting(routes =>
|
||||
{
|
||||
routes.MapHub<TestHub>("/testhub");
|
||||
routes.MapHub<TestHub>("/testhub-nowebsockets", options => options.Transports = HttpTransportType.ServerSentEvents | HttpTransportType.LongPolling);
|
||||
routes.MapHub<UncreatableHub>("/uncreatable");
|
||||
routes.MapHub<HubWithAuthorization>("/authorizedhub");
|
||||
});
|
||||
|
||||
app.Use(next => async (context) =>
|
||||
{
|
||||
if (context.Request.Path.StartsWithSegments("/generateJwtToken"))
|
||||
routes.MapConnectionHandler<EchoConnectionHandler>("/echo");
|
||||
|
||||
routes.MapGet("/generateJwtToken", context =>
|
||||
{
|
||||
await context.Response.WriteAsync(GenerateJwtToken());
|
||||
return;
|
||||
}
|
||||
return context.Response.WriteAsync(GenerateJwtToken());
|
||||
});
|
||||
|
||||
if (context.Request.Path.StartsWithSegments("/deployment"))
|
||||
routes.MapGet("/deployment", context =>
|
||||
{
|
||||
var attributes = Assembly.GetAssembly(typeof(Startup)).GetCustomAttributes<AssemblyMetadataAttribute>();
|
||||
|
||||
|
|
@ -182,7 +165,20 @@ namespace FunctionalTests
|
|||
|
||||
json.WriteTo(writer);
|
||||
}
|
||||
|
||||
return Task.CompletedTask;
|
||||
});
|
||||
});
|
||||
|
||||
app.Use(async (context, next) =>
|
||||
{
|
||||
if (context.Request.Path.Value.Contains("/negotiate"))
|
||||
{
|
||||
context.Response.Cookies.Append("testCookie", "testValue");
|
||||
context.Response.Cookies.Append("testCookie2", "testValue2");
|
||||
context.Response.Cookies.Append("expiredCookie", "doesntmatter", new CookieOptions() { Expires = DateTimeOffset.Now.AddHours(-1) });
|
||||
}
|
||||
await next.Invoke();
|
||||
});
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -0,0 +1,151 @@
|
|||
// Copyright (c) .NET Foundation. All rights reserved.
|
||||
// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
|
||||
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using Microsoft.AspNetCore.Authorization;
|
||||
using Microsoft.AspNetCore.Builder;
|
||||
using Microsoft.AspNetCore.Connections;
|
||||
using Microsoft.AspNetCore.Http.Connections;
|
||||
using Microsoft.AspNetCore.Http.Connections.Internal;
|
||||
using Microsoft.Extensions.DependencyInjection;
|
||||
|
||||
namespace Microsoft.AspNetCore.Routing
|
||||
{
|
||||
public static class ConnectionEndpointRouteBuilderExtensions
|
||||
{
|
||||
/// <summary>
|
||||
/// Maps incoming requests with the specified path to the provided connection pipeline.
|
||||
/// </summary>
|
||||
/// <param name="builder">The <see cref="IEndpointRouteBuilder"/> to add the route to.</param>
|
||||
/// <param name="pattern">The route pattern.</param>
|
||||
/// <param name="configure">A callback to configure the connection.</param>
|
||||
/// <returns>An <see cref="IEndpointConventionBuilder"/> for endpoints associated with the connections.</returns>
|
||||
public static IEndpointConventionBuilder MapConnections(this IEndpointRouteBuilder builder, string pattern, Action<IConnectionBuilder> configure) =>
|
||||
builder.MapConnections(pattern, new HttpConnectionDispatcherOptions(), configure);
|
||||
|
||||
/// <summary>
|
||||
/// Maps incoming requests with the specified path to the provided connection pipeline.
|
||||
/// </summary>
|
||||
/// <typeparam name="TConnectionHandler">The <see cref="ConnectionHandler"/> type.</typeparam>
|
||||
/// <param name="builder">The <see cref="IEndpointRouteBuilder"/> to add the route to.</param>
|
||||
/// <param name="pattern">The route pattern.</param>
|
||||
/// <returns>An <see cref="IEndpointConventionBuilder"/> for endpoints associated with the connections.</returns>
|
||||
public static IEndpointConventionBuilder MapConnectionHandler<TConnectionHandler>(this IEndpointRouteBuilder builder, string pattern) where TConnectionHandler : ConnectionHandler
|
||||
{
|
||||
return builder.MapConnectionHandler<TConnectionHandler>(pattern, configureOptions: null);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Maps incoming requests with the specified path to the provided connection pipeline.
|
||||
/// </summary>
|
||||
/// <typeparam name="TConnectionHandler">The <see cref="ConnectionHandler"/> type.</typeparam>
|
||||
/// <param name="builder">The <see cref="IEndpointRouteBuilder"/> to add the route to.</param>
|
||||
/// <param name="pattern">The route pattern.</param>
|
||||
/// <param name="configureOptions">A callback to configure dispatcher options.</param>
|
||||
/// <returns>An <see cref="IEndpointConventionBuilder"/> for endpoints associated with the connections.</returns>
|
||||
public static IEndpointConventionBuilder MapConnectionHandler<TConnectionHandler>(this IEndpointRouteBuilder builder, string pattern, Action<HttpConnectionDispatcherOptions> configureOptions) where TConnectionHandler : ConnectionHandler
|
||||
{
|
||||
var options = new HttpConnectionDispatcherOptions();
|
||||
// REVIEW: WE should consider removing this and instead just relying on the
|
||||
// AuthorizationMiddleware
|
||||
var attributes = typeof(TConnectionHandler).GetCustomAttributes(inherit: true);
|
||||
foreach (var attribute in attributes.OfType<AuthorizeAttribute>())
|
||||
{
|
||||
options.AuthorizationData.Add(attribute);
|
||||
}
|
||||
configureOptions?.Invoke(options);
|
||||
|
||||
var conventionBuilder = builder.MapConnections(pattern, options, b =>
|
||||
{
|
||||
b.UseConnectionHandler<TConnectionHandler>();
|
||||
});
|
||||
|
||||
conventionBuilder.Add(e =>
|
||||
{
|
||||
// Add all attributes on the ConnectionHandler has metadata (this will allow for things like)
|
||||
// auth attributes and cors attributes to work seamlessly
|
||||
foreach (var item in attributes)
|
||||
{
|
||||
e.Metadata.Add(item);
|
||||
}
|
||||
});
|
||||
|
||||
return conventionBuilder;
|
||||
}
|
||||
|
||||
|
||||
/// <summary>
|
||||
/// Maps incoming requests with the specified path to the provided connection pipeline.
|
||||
/// </summary>
|
||||
/// <param name="builder">The <see cref="IEndpointRouteBuilder"/> to add the route to.</param>
|
||||
/// <param name="pattern">The route pattern.</param>
|
||||
/// <param name="options">Options used to configure the connection.</param>
|
||||
/// <param name="configure">A callback to configure the connection.</param>
|
||||
/// <returns>An <see cref="IEndpointConventionBuilder"/> for endpoints associated with the connections.</returns>
|
||||
public static IEndpointConventionBuilder MapConnections(this IEndpointRouteBuilder builder, string pattern, HttpConnectionDispatcherOptions options, Action<IConnectionBuilder> configure)
|
||||
{
|
||||
var dispatcher = builder.ServiceProvider.GetRequiredService<HttpConnectionDispatcher>();
|
||||
|
||||
var connectionBuilder = new ConnectionBuilder(builder.ServiceProvider);
|
||||
configure(connectionBuilder);
|
||||
var connectionDelegate = connectionBuilder.Build();
|
||||
|
||||
// REVIEW: Consider expanding the internals of the dispatcher as endpoint routes instead of
|
||||
// using if statemants we can let the matcher handle
|
||||
|
||||
var conventionBuilders = new List<IEndpointConventionBuilder>();
|
||||
|
||||
// Build the negotiate application
|
||||
var app = builder.CreateApplicationBuilder();
|
||||
app.UseWebSockets();
|
||||
app.Run(c => dispatcher.ExecuteNegotiateAsync(c, options));
|
||||
var negotiateHandler = app.Build();
|
||||
|
||||
var negotiateBuilder = builder.Map(pattern + "/negotiate", negotiateHandler);
|
||||
conventionBuilders.Add(negotiateBuilder);
|
||||
|
||||
// build the execute handler part of the protocol
|
||||
app = builder.CreateApplicationBuilder();
|
||||
app.UseWebSockets();
|
||||
app.Run(c => dispatcher.ExecuteAsync(c, options, connectionDelegate));
|
||||
var executehandler = app.Build();
|
||||
|
||||
var executeBuilder = builder.Map(pattern, executehandler);
|
||||
conventionBuilders.Add(executeBuilder);
|
||||
|
||||
var compositeConventionBuilder = new CompositeEndpointConventionBuilder(conventionBuilders);
|
||||
|
||||
// Add metadata to all of Endpoints
|
||||
compositeConventionBuilder.Add(e =>
|
||||
{
|
||||
// Add the authorization data as metadata
|
||||
foreach (var data in options.AuthorizationData)
|
||||
{
|
||||
e.Metadata.Add(data);
|
||||
}
|
||||
});
|
||||
|
||||
return compositeConventionBuilder;
|
||||
}
|
||||
|
||||
private class CompositeEndpointConventionBuilder : IEndpointConventionBuilder
|
||||
{
|
||||
private readonly List<IEndpointConventionBuilder> _endpointConventionBuilders;
|
||||
|
||||
public CompositeEndpointConventionBuilder(List<IEndpointConventionBuilder> endpointConventionBuilders)
|
||||
{
|
||||
_endpointConventionBuilders = endpointConventionBuilders;
|
||||
}
|
||||
|
||||
public void Add(Action<EndpointBuilder> convention)
|
||||
{
|
||||
foreach (var endpointConventionBuilder in _endpointConventionBuilders)
|
||||
{
|
||||
endpointConventionBuilder.Add(convention);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -66,11 +66,15 @@ namespace JwtSample
|
|||
public void Configure(IApplicationBuilder app, IHostingEnvironment env)
|
||||
{
|
||||
app.UseFileServer();
|
||||
app.UseSignalR(options => options.MapHub<Broadcaster>("/broadcast"));
|
||||
|
||||
var routeBuilder = new RouteBuilder(app);
|
||||
routeBuilder.MapGet("generatetoken", c => c.Response.WriteAsync(GenerateToken(c)));
|
||||
app.UseRouter(routeBuilder.Build());
|
||||
app.UseRouting(routes =>
|
||||
{
|
||||
routes.MapHub<Broadcaster>("/broadcast");
|
||||
routes.MapGet("/generatetoken", context =>
|
||||
{
|
||||
return context.Response.WriteAsync(GenerateToken(context));
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
private string GenerateToken(HttpContext httpContext)
|
||||
|
|
|
|||
|
|
@ -4,9 +4,11 @@
|
|||
using System;
|
||||
using System.IO;
|
||||
using System.Reflection;
|
||||
using System.Threading;
|
||||
using System.Threading.Tasks;
|
||||
using Microsoft.AspNetCore.Builder;
|
||||
using Microsoft.AspNetCore.Hosting;
|
||||
using Microsoft.AspNetCore.Routing;
|
||||
using Microsoft.Extensions.DependencyInjection;
|
||||
using Newtonsoft.Json;
|
||||
using Newtonsoft.Json.Linq;
|
||||
|
|
@ -55,23 +57,17 @@ namespace SignalRSamples
|
|||
|
||||
app.UseCors("Everything");
|
||||
|
||||
app.UseSignalR(routes =>
|
||||
app.UseRouting(routes =>
|
||||
{
|
||||
routes.MapHub<DynamicChat>("/dynamic");
|
||||
routes.MapHub<Chat>("/default");
|
||||
routes.MapHub<Streaming>("/streaming");
|
||||
routes.MapHub<UploadHub>("/uploading");
|
||||
routes.MapHub<HubTChat>("/hubT");
|
||||
});
|
||||
|
||||
app.UseConnections(routes =>
|
||||
{
|
||||
routes.MapConnectionHandler<MessagesConnectionHandler>("/chat");
|
||||
});
|
||||
|
||||
app.Use(next => (context) =>
|
||||
{
|
||||
if (context.Request.Path.StartsWithSegments("/deployment"))
|
||||
routes.MapGet("/deployment", context =>
|
||||
{
|
||||
var attributes = Assembly.GetAssembly(typeof(Startup)).GetCustomAttributes<AssemblyMetadataAttribute>();
|
||||
|
||||
|
|
@ -99,9 +95,12 @@ namespace SignalRSamples
|
|||
|
||||
json.WriteTo(writer);
|
||||
}
|
||||
}
|
||||
return Task.CompletedTask;
|
||||
|
||||
return Task.CompletedTask;
|
||||
});
|
||||
});
|
||||
|
||||
app.UseAuthorization();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -3,6 +3,7 @@
|
|||
|
||||
using Microsoft.AspNetCore.Builder;
|
||||
using Microsoft.AspNetCore.Hosting;
|
||||
using Microsoft.AspNetCore.Routing;
|
||||
using Microsoft.Extensions.DependencyInjection;
|
||||
using Microsoft.Extensions.Logging;
|
||||
using SocialWeather.Json;
|
||||
|
|
@ -30,9 +31,13 @@ namespace SocialWeather
|
|||
app.UseDeveloperExceptionPage();
|
||||
}
|
||||
|
||||
app.UseConnections(o => o.MapConnectionHandler<SocialWeatherConnectionHandler>("/weather"));
|
||||
app.UseFileServer();
|
||||
|
||||
app.UseRouting(routes =>
|
||||
{
|
||||
routes.MapConnectionHandler<SocialWeatherConnectionHandler>("/weather");
|
||||
});
|
||||
|
||||
var formatterResolver = app.ApplicationServices.GetRequiredService<FormatterResolver>();
|
||||
formatterResolver.AddFormatter<WeatherReport, JsonStreamFormatter<WeatherReport>>("json");
|
||||
formatterResolver.AddFormatter<WeatherReport, ProtobufWeatherStreamFormatter>("protobuf");
|
||||
|
|
|
|||
|
|
@ -0,0 +1,75 @@
|
|||
// Copyright (c) .NET Foundation. All rights reserved.
|
||||
// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
|
||||
|
||||
using System;
|
||||
using System.Linq;
|
||||
using Microsoft.AspNetCore.Authorization;
|
||||
using Microsoft.AspNetCore.Builder;
|
||||
using Microsoft.AspNetCore.Http.Connections;
|
||||
using Microsoft.AspNetCore.SignalR;
|
||||
using Microsoft.Extensions.DependencyInjection;
|
||||
|
||||
namespace Microsoft.AspNetCore.Routing
|
||||
{
|
||||
public static class HubEndpointRouteBuilderExtensions
|
||||
{
|
||||
/// <summary>
|
||||
/// Maps incoming requests with the specified path to the specified <see cref="Hub"/> type.
|
||||
/// </summary>
|
||||
/// <typeparam name="THub">The <see cref="Hub"/> type to map requests to.</typeparam>
|
||||
/// <param name="builder">The <see cref="IEndpointRouteBuilder"/> to add the route to.</param>
|
||||
/// <param name="pattern">The route pattern.</param>
|
||||
/// <returns>An <see cref="IEndpointConventionBuilder"/> for endpoints associated with the connections.</returns>
|
||||
public static IEndpointConventionBuilder MapHub<THub>(this IEndpointRouteBuilder builder, string pattern) where THub : Hub
|
||||
{
|
||||
return builder.MapHub<THub>(pattern, configureOptions: null);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Maps incoming requests with the specified path to the specified <see cref="Hub"/> type.
|
||||
/// </summary>
|
||||
/// <typeparam name="THub">The <see cref="Hub"/> type to map requests to.</typeparam>
|
||||
/// <param name="builder">The <see cref="IEndpointRouteBuilder"/> to add the route to.</param>
|
||||
/// <param name="pattern">The route pattern.</param>
|
||||
/// <param name="configureOptions">A callback to configure dispatcher options.</param>
|
||||
/// <returns>An <see cref="IEndpointConventionBuilder"/> for endpoints associated with the connections.</returns>
|
||||
public static IEndpointConventionBuilder MapHub<THub>(this IEndpointRouteBuilder builder, string pattern, Action<HttpConnectionDispatcherOptions> configureOptions) where THub : Hub
|
||||
{
|
||||
var marker = builder.ServiceProvider.GetService<SignalRMarkerService>();
|
||||
|
||||
if (marker == null)
|
||||
{
|
||||
throw new InvalidOperationException("Unable to find the required services. Please add all the required services by calling " +
|
||||
"'IServiceCollection.AddSignalR' inside the call to 'ConfigureServices(...)' in the application startup code.");
|
||||
}
|
||||
|
||||
var options = new HttpConnectionDispatcherOptions();
|
||||
// REVIEW: WE should consider removing this and instead just relying on the
|
||||
// AuthorizationMiddleware
|
||||
var attributes = typeof(THub).GetCustomAttributes(inherit: true);
|
||||
foreach (var attribute in attributes.OfType<AuthorizeAttribute>())
|
||||
{
|
||||
options.AuthorizationData.Add(attribute);
|
||||
}
|
||||
|
||||
configureOptions?.Invoke(options);
|
||||
|
||||
var conventionBuilder = builder.MapConnections(pattern, options, b =>
|
||||
{
|
||||
b.UseHub<THub>();
|
||||
});
|
||||
|
||||
conventionBuilder.Add(e =>
|
||||
{
|
||||
// Add all attributes on the Hub has metadata (this will allow for things like)
|
||||
// auth attributes and cors attributes to work seamlessly
|
||||
foreach (var item in attributes)
|
||||
{
|
||||
e.Metadata.Add(item);
|
||||
}
|
||||
});
|
||||
|
||||
return conventionBuilder;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -1,8 +1,11 @@
|
|||
using System;
|
||||
using System;
|
||||
using System.Collections;
|
||||
using System.Collections.Generic;
|
||||
using System.Threading.Tasks;
|
||||
using Microsoft.AspNetCore.Authorization;
|
||||
using Microsoft.AspNetCore.Builder;
|
||||
using Microsoft.AspNetCore.Hosting;
|
||||
using Microsoft.AspNetCore.Routing;
|
||||
using Microsoft.Extensions.DependencyInjection;
|
||||
using Xunit;
|
||||
|
||||
|
|
@ -36,7 +39,8 @@ namespace Microsoft.AspNetCore.SignalR.Tests
|
|||
{
|
||||
executedConfigure = true;
|
||||
|
||||
var ex = Assert.Throws<InvalidOperationException>(() => {
|
||||
var ex = Assert.Throws<InvalidOperationException>(() =>
|
||||
{
|
||||
app.UseSignalR(routes =>
|
||||
{
|
||||
routes.MapHub<AuthHub>("/overloads");
|
||||
|
|
@ -56,6 +60,43 @@ namespace Microsoft.AspNetCore.SignalR.Tests
|
|||
Assert.True(executedConfigure);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void NotAddingSignalRServiceThrowsWhenUsingEndpointRouting()
|
||||
{
|
||||
var executedConfigure = false;
|
||||
var builder = new WebHostBuilder();
|
||||
|
||||
builder
|
||||
.UseKestrel()
|
||||
.ConfigureServices(services =>
|
||||
{
|
||||
services.AddRouting();
|
||||
})
|
||||
.Configure(app =>
|
||||
{
|
||||
executedConfigure = true;
|
||||
|
||||
var ex = Assert.Throws<InvalidOperationException>(() =>
|
||||
{
|
||||
app.UseRouting(routes =>
|
||||
{
|
||||
routes.MapHub<AuthHub>("/overloads");
|
||||
});
|
||||
});
|
||||
|
||||
Assert.Equal("Unable to find the required services. Please add all the required services by calling " +
|
||||
"'IServiceCollection.AddSignalR' inside the call to 'ConfigureServices(...)' in the application startup code.", ex.Message);
|
||||
})
|
||||
.UseUrls("http://127.0.0.1:0");
|
||||
|
||||
using (var host = builder.Build())
|
||||
{
|
||||
host.Start();
|
||||
}
|
||||
|
||||
Assert.True(executedConfigure);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void MapHubFindsAuthAttributeOnHub()
|
||||
{
|
||||
|
|
@ -101,6 +142,49 @@ namespace Microsoft.AspNetCore.SignalR.Tests
|
|||
Assert.Equal(2, authCount);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void MapHubEndPointRoutingFindsAttributesOnHub()
|
||||
{
|
||||
var authCount = 0;
|
||||
using (var host = BuildWebHostWithEndPointRouting(routes => routes.MapHub<AuthHub>("/path", options =>
|
||||
{
|
||||
authCount += options.AuthorizationData.Count;
|
||||
})))
|
||||
{
|
||||
host.Start();
|
||||
|
||||
var dataSource = host.Services.GetRequiredService<EndpointDataSource>();
|
||||
// We register 2 endpoints (/negotiate and /)
|
||||
Assert.Equal(2, dataSource.Endpoints.Count);
|
||||
Assert.NotNull(dataSource.Endpoints[0].Metadata.GetMetadata<IAuthorizeData>());
|
||||
Assert.NotNull(dataSource.Endpoints[1].Metadata.GetMetadata<IAuthorizeData>());
|
||||
}
|
||||
|
||||
Assert.Equal(1, authCount);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void MapHubEndPointRoutingAppliesAttributesBeforeConventions()
|
||||
{
|
||||
void ConfigureRoutes(IEndpointRouteBuilder routes)
|
||||
{
|
||||
// This "Foo" policy should override the default auth attribute
|
||||
routes.MapHub<AuthHub>("/path")
|
||||
.RequireAuthorization(new AuthorizeAttribute("Foo"));
|
||||
}
|
||||
|
||||
using (var host = BuildWebHostWithEndPointRouting(ConfigureRoutes))
|
||||
{
|
||||
host.Start();
|
||||
|
||||
var dataSource = host.Services.GetRequiredService<EndpointDataSource>();
|
||||
// We register 2 endpoints (/negotiate and /)
|
||||
Assert.Equal(2, dataSource.Endpoints.Count);
|
||||
Assert.Equal("Foo", dataSource.Endpoints[0].Metadata.GetMetadata<IAuthorizeData>()?.Policy);
|
||||
Assert.Equal("Foo", dataSource.Endpoints[1].Metadata.GetMetadata<IAuthorizeData>()?.Policy);
|
||||
}
|
||||
}
|
||||
|
||||
private class InvalidHub : Hub
|
||||
{
|
||||
public void OverloadedMethod(int num)
|
||||
|
|
@ -126,6 +210,22 @@ namespace Microsoft.AspNetCore.SignalR.Tests
|
|||
{
|
||||
}
|
||||
|
||||
private IWebHost BuildWebHostWithEndPointRouting(Action<IEndpointRouteBuilder> configure)
|
||||
{
|
||||
return new WebHostBuilder()
|
||||
.UseKestrel()
|
||||
.ConfigureServices(services =>
|
||||
{
|
||||
services.AddSignalR();
|
||||
})
|
||||
.Configure(app =>
|
||||
{
|
||||
app.UseRouting(routes => configure(routes));
|
||||
})
|
||||
.UseUrls("http://127.0.0.1:0")
|
||||
.Build();
|
||||
}
|
||||
|
||||
private IWebHost BuildWebHost(Action<HubRouteBuilder> configure)
|
||||
{
|
||||
return new WebHostBuilder()
|
||||
|
|
|
|||
|
|
@ -5,6 +5,7 @@ using System.Security.Claims;
|
|||
using Microsoft.AspNetCore.Authentication.Cookies;
|
||||
using Microsoft.AspNetCore.Builder;
|
||||
using Microsoft.AspNetCore.Hosting;
|
||||
using Microsoft.AspNetCore.Routing;
|
||||
using Microsoft.Extensions.DependencyInjection;
|
||||
|
||||
namespace Microsoft.AspNetCore.SignalR.Tests
|
||||
|
|
@ -28,18 +29,15 @@ namespace Microsoft.AspNetCore.SignalR.Tests
|
|||
|
||||
public void Configure(IApplicationBuilder app, IHostingEnvironment env)
|
||||
{
|
||||
app.UseConnections(routes =>
|
||||
app.UseRouting(routes =>
|
||||
{
|
||||
routes.MapHub<UncreatableHub>("/uncreatable");
|
||||
|
||||
routes.MapConnectionHandler<EchoConnectionHandler>("/echo");
|
||||
routes.MapConnectionHandler<WriteThenCloseConnectionHandler>("/echoAndClose");
|
||||
routes.MapConnectionHandler<HttpHeaderConnectionHandler>("/httpheader");
|
||||
routes.MapConnectionHandler<AuthConnectionHandler>("/auth");
|
||||
});
|
||||
|
||||
app.UseSignalR(routes =>
|
||||
{
|
||||
routes.MapHub<UncreatableHub>("/uncreatable");
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -4,6 +4,7 @@
|
|||
using System;
|
||||
using Microsoft.AspNetCore.Builder;
|
||||
using Microsoft.AspNetCore.Hosting;
|
||||
using Microsoft.AspNetCore.Routing;
|
||||
using Microsoft.Extensions.DependencyInjection;
|
||||
using Microsoft.Extensions.Primitives;
|
||||
|
||||
|
|
@ -28,7 +29,10 @@ namespace Microsoft.AspNetCore.SignalR.StackExchangeRedis.Tests
|
|||
|
||||
public void Configure(IApplicationBuilder app, IHostingEnvironment env)
|
||||
{
|
||||
app.UseSignalR(options => options.MapHub<EchoHub>("/echo"));
|
||||
app.UseRouting(routes =>
|
||||
{
|
||||
routes.MapHub<EchoHub>("/echo");
|
||||
});
|
||||
}
|
||||
|
||||
private class UserNameIdProvider : IUserIdProvider
|
||||
|
|
|
|||
Loading…
Reference in New Issue