SignalR works on localhost but doesn't work when is deployed in Azure
Asp.net Core 1.0.0 (.Net Framework 4.6.1)
SignalR.Core 2.2.1
public static void UseSignalR2(this IApplicationBuilder app)
{
app.UseAppBuilder(appBuilder => {
appBuilder.MapSignalR(new HubConfiguration());
});
GlobalHost.HubPipeline.AddModule(new ErrorHandlingPipelineModule());
GlobalHost.HubPipeline.AddModule(new LoggingPipelineModule());
}
SignalR.js 2.2.1 with default settings
$.connection.hub.url = '/signalr';
Expected behavior
200 for url:
https://(name).azurewebsites.com/signalr/negotiate?clientProtocol=1.5&connectionData=%5B%7B%22name%22%3A%22productsimporthub%22%7D%5D&_=1472811629592
Actual behavior
/signalr/negotiate - on localhost returns 200 but for deployed app in azure returns 404
/signalr - works on both - Protocol error: Unknown transport.
/signalr/hubs - works on both - returns the SignalR js correctly
To find out the real cause of the issue you need to navigate to the negotiate url, and look for the response.
If the response tells you something about a 'CryptographicException: The data protection operation was unsuccessful...'. This is how to fix it.
1) Create a custom IDataProtectionProvider
2) Configure signalr
internal class MachineKeyProtectionProvider : IDataProtectionProvider
{
public IDataProtector Create(params string[] purposes)
{
return new MachineKeyDataProtector(purposes);
}
}
internal class MachineKeyDataProtector : IDataProtector
{
private readonly string[] _purposes;
public MachineKeyDataProtector(string[] purposes)
{
_purposes = purposes;
}
public byte[] Protect(byte[] userData)
{
//return MachineKey.Protect(userData, _purposes);
return userData;
}
public byte[] Unprotect(byte[] protectedData)
{
//return System.Web.Security.MachineKey.Unprotect(protectedData, _purposes);
return protectedData;
}
}
I use katana extension methods to bridge the IAppBuilder to IApplicationBuilder.
This allows your owin middleware to connect to asp.net core. It is important to use the RunSignalr method
app.UseAppBuilder(appBuilder =>
{
appBuilder.SetDataProtectionProvider(new MachineKeyProtectionProvider());
appBuilder.Map("/signalr", map =>
{
var hubConfiguration = new HubConfiguration
{
EnableDetailedErrors = true
};
map.RunSignalR(hubConfiguration);
});
});
Related
I have been struggling with this for few days so I am asking whether what I am trying to do is possible.
Basically I am experimenting things with signalR in Azure. I am using the latest version of SignalR NuGet.
I have a small Web App defining a simple signalR hub I want to access via a Azure SignalR Service.
I then have a couple of apps which communicate via hub in the signalR service.
I have various issues.
Getting the hub to load on the AZ SignalR Service. It does not work all the time. Complaining about Serverless/Default settings...
2023-02-16T14:05:25.034917571Z info: Microsoft.Azure.SignalR.Connections.Client.Internal.WebSocketsTransport[1]
2023-02-16T14:05:25.034960072Z Starting transport. Transfer mode: Binary. Url: 'wss://nmg-opus-common-inventory-dev-sigr.service.signalr.net/server/?hub=myhub&cid=9e64b380-b752-4175-8fd2-215a2b62139d'.
2023-02-16T14:05:25.057819457Z info: Microsoft.Azure.SignalR.Connections.Client.Internal.WebSocketsTransport[11]
2023-02-16T14:05:25.057859557Z WebSocket closed by the server. Close status NormalClosure.
2023-02-16T14:05:25.057865857Z info: Microsoft.Azure.SignalR.ServiceConnection[24]
2023-02-16T14:05:25.057870457Z Connection to service '(Primary)https://nmg-opus-common-inventory-dev-sigr.service.signalr.net(hub=MyHub)' handshake failed, probably caused by network instability or service restart. Will retry after the back off period. Error detail: Azure SignalR Service is in serverless mode, server connection is not allowed.. Id: 9e64b380-b752-4175-8fd2-215a2b62139d
2023-02-16T14:05:25.057875657Z info: Microsoft.Azure.SignalR.Connections.Client.Internal.WebSocketsTransport[6]
2023-02-16T14:05:25.057880257Z Transport is stopping.
2
On the client side, it seems that the connection succeeds if the service is set to Serverless mode.
Once connected, my call to join a group just hang and never returns. The connection happens after I click an element (div) on a blazor page.
So I am not sure why something so simple does not work.
here is my very simple hub :
public class MyHub : Hub
{
public async Task SendMessage(string groupName, string message)
{
await Group(groupName).SendAsync("ReceiveMessage", message);
}
public async Task JoinGroup(string groupName)
{
await Groups.AddToGroupAsync(Context.ConnectionId, groupName);
}
public async Task LeaveGroup(string groupName)
{
await Groups.RemoveFromGroupAsync(Context.ConnectionId, groupName);
}
public override async Task OnConnectedAsync()
{
await Clients.Client(Context.ConnectionId).SendAsync("Connected", Context.ConnectionId);
}
}
here is the classic startup method for the app defining hub
public class Startup
{
public Startup(IConfiguration configuration)
{
Configuration = configuration;
}
public IConfiguration Configuration { get; }
public void ConfigureServices(IServiceCollection services)
{
services.AddAzureAppConfiguration();
services.AddSignalR(e => { e.MaximumReceiveMessageSize = 10240000; });
string SignalRURLHub = Configuration.GetValue<string>("Resources:SignalR:Inventory:ConnectionString");
services.AddSignalR().AddAzureSignalR(SignalRURLHub);
}
public void Configure(IApplicationBuilder app, IWebHostEnvironment env)
{
app.UseHttpsRedirection();
app.UseStaticFiles();
app.UseAzureAppConfiguration();
app.UseRouting();
var SignalRHubName = $"/{Configuration.GetValue<string>("Resources:SignalR:HubName")}";
app.UseEndpoints(endpoints =>
{
endpoints.MapHub<MyHub>(SignalRHubName);
});
}
}
Code from the client applications Initializing the connection and starting it.
protected HubConnection _connection;
private readonly string groupName;
protected ClientHandler(string connectionString, string hubName, string groupName, string userId = null)
{
var serviceUtils = new ServiceUtils(connectionString);
var url = GetClientUrl(serviceUtils.Endpoint, hubName);
_connection = new HubConnectionBuilder()
.WithUrl(url
, option =>
{
option.AccessTokenProvider = () =>
{
return Task.FromResult(serviceUtils.GenerateAccessToken(url, userId));
};
})
.WithAutomaticReconnect(new AlwaysRetryPolicy())
.ConfigureLogging(logging =>
{
logging.SetMinimumLevel(LogLevel.Trace);
logging.AddConsole();
})
.Build();
this.groupName = groupName;
_connection.Reconnected += JoinGroup;
_connection.On("Connected", new[] { typeof(string) }, OnInitialConnection);
}
private string GetClientUrl(string endpoint, string hubName)
{
return $"{endpoint}/client/?hub={hubName}";
}
private async Task JoinGroup(string contextId)
{
try
{
await _connection.InvokeAsync("JoinGroup", groupName); //JoinGroup is C# method name
}
catch (Exception e)
{
}
}
// Called separately after the ClientHandler object is created.
public async Task StartAsyncWithRetry(CancellationToken cancellationToken = default)
{
while (!cancellationToken.IsCancellationRequested)
{
try
{
await _connection.StartAsync()
.ContinueWith(res =>
{
_connection.InvokeAsync("JoinGroup", groupName);
});
if (_connection.State == HubConnectionState.Disconnected)
{
await Task.Delay(GetRandomDelayMilliseconds());
continue;
}
return;
}
catch (Exception e)
{
//throw;
//cancellationToken.
await Task.Delay(GetRandomDelayMilliseconds());
}
}
}
I have tried tweaking various setting in Azure, in my apps, changed my code from synch to async...
I must be missing something ... I've found lot of post but many of them were out of date. Also tried my luck with ChatGPT ...
I am struggling with getting the package AspNetCoreRateLimit version 4.0.2 to work when hosting a Blazor WebAsssembly project in Azure.
The solution has been developed in Visual studio 2022 and is made up of 5 individual projects, where one of them is a standard API project. It is based on the net6.0 framework. I have a startup.cs configuration and a RateLimitingMiddleware class used to setup the rate limits and the configuration.
Startup.cs:
public void ConfigureServices(IServiceCollection services)
{
services.AddRateLimiting();
services.AddHsts(options =>
{
options.Preload = true;
options.IncludeSubDomains = true;
options.MaxAge = TimeSpan.FromDays(365);
});
services.AddDbContext<ApplicationDbContext>(options =>
{
options.UseSqlServer(Configuration.GetConnectionString("DefaultConnection"),
sqlServerOptionsAction: sqlOptions =>
{
sqlOptions.EnableRetryOnFailure(
maxRetryCount: 10,
maxRetryDelay: TimeSpan.FromSeconds(30),
errorNumbersToAdd: null);
});
}, ServiceLifetime.Transient, ServiceLifetime.Singleton);
services.AddAutoMapper(typeof(Startup));
....
}
public void Configure(IApplicationBuilder app, IWebHostEnvironment env)
{
app.UseRateLimiting();
if (env.IsDevelopment())
{
app.UseWebAssemblyDebugging();
app.UseSwagger();
}
else
{
app.UseExceptionHandler("/Error");
}
app.UseHsts();
...
}
RateLimitingMiddleware.cs
internal static class RateLimitingMiddleware
{
internal static IServiceCollection AddRateLimiting(this IServiceCollection services)
{
// Used to store rate limit counters and ip rules
services.AddOptions();
services.AddMemoryCache();
services.Configure<ClientRateLimitOptions>(options =>
{
options.EnableEndpointRateLimiting = true;
//options.RealIpHeader = "X-Real-IP";
options.ClientIdHeader = "authorization";
//options.EndpointWhitelist = new List<string> { "get:/_framework/*", "get:/_content/*", "*:/lib/*", "*:/css/*", "*:/js/", "*:/appsettings.json", "*:/images/" };
options.GeneralRules = new List<RateLimitRule>
{
new RateLimitRule
{
Endpoint="*:/api/*",
Period = "15m",
Limit=30
},
new RateLimitRule
{
Endpoint="*:/api/*",
Period = "12h",
Limit=20000
},
new RateLimitRule
{
Endpoint="*:/api/*",
Period = "7d",
Limit=1000000
}
};
});
services.AddInMemoryRateLimiting();
// Inject Counter and Store Rules
services.AddSingleton<IClientPolicyStore, MemoryCacheClientPolicyStore>();
services.AddSingleton<IRateLimitCounterStore, MemoryCacheRateLimitCounterStore>();
services.AddSingleton<IClientPolicyStore, DistributedCacheClientPolicyStore>();
services.AddSingleton<IRateLimitCounterStore, DistributedCacheRateLimitCounterStore>();
services.AddSingleton<IRateLimitConfiguration, RateLimitConfiguration>();
services.AddSingleton<IProcessingStrategy, AsyncKeyLockProcessingStrategy>();
//services.AddSingleton<IRateLimitConfiguration, CustomRateLimitConfiguration>();
//
// Return the services
return services;
}
internal static IApplicationBuilder UseRateLimiting(this IApplicationBuilder app)
{
var ipPolicyStore = app.ApplicationServices.GetRequiredService<IIpPolicyStore>();
ipPolicyStore.SeedAsync().GetAwaiter().GetResult();
var clientPolicyStore = app.ApplicationServices.GetRequiredService<IClientPolicyStore>();
clientPolicyStore.SeedAsync().GetAwaiter().GetResult();
app.UseClientRateLimiting();
app.UseIpRateLimiting();
return app;
}
}
With the above configuration the api calls are being limited when testing localhost and using Postman. However when the api is uploaded to our Azure environment the rules are not being implemented correctly.
As stated in the rule defined in the middelware I would like to use the authorization token to limit the number of count the number of requests but cannot get it to work.
I hope that someone has dealt with the same issue and can see where I am going wrong?
Thanks,
Problem solved. Wrong ratelimitheader. Should be CLIENT-IP.
When deployed to Azure SignalR doens't work works on localhost but doesn't work when deployed to Azure
It responds with a 500 internal server error response when doing the signalr/negotiate request.
Navigating manually to the negotiate url, I got a more detailed error explanation. 'CryptographicException: The data protection operation was unsuccessful...'
Everything works fine locally, using IISExpress.
How do I fix this?
I think this is the solution. This worked for me without making any code changes:
Azure WebApps is configured to not load user profile by default and this causes the exception. In Azure App Settings, create an Application Setting called WEBSITE_LOAD_USER_PROFILE and set it to 1. This will load the user profile.
https://www.magnetismsolutions.com/blog/jaredjohnson/2015/12/18/resolving-cryptography-issues-with-the-dynamics-crm-sdk-in-azure-web-apps
Got it working. I needed to use appBuilder.SetDataProtectionProvider
app.UseAppBuilder(appBuilder =>
{
appBuilder.SetDataProtectionProvider(new MachineKeyProtectionProvider());
appBuilder.Map("/signalr", map =>
{
var hubConfiguration = new HubConfiguration
{
EnableDetailedErrors = true
};
map.RunSignalR(hubConfiguration);
});
});
I use katana extension methods to bridge the IAppBuilder to IApplicationBuilder.
This allows your owin middleware to connect to asp.net core. It is important to use the RunSignalr method.
internal class MachineKeyProtectionProvider : IDataProtectionProvider
{
public IDataProtector Create(params string[] purposes)
{
return new MachineKeyDataProtector(purposes);
}
}
internal class MachineKeyDataProtector : IDataProtector
{
private readonly string[] _purposes;
public MachineKeyDataProtector(string[] purposes)
{
_purposes = purposes;
}
public byte[] Protect(byte[] userData)
{
//return MachineKey.Protect(userData, _purposes);
return userData;
}
public byte[] Unprotect(byte[] protectedData)
{
//return System.Web.Security.MachineKey.Unprotect(protectedData, _purposes);
return protectedData;
}
}
I'm using Ninject for dependency injection in my ASP.NET Web Api 2 project. Everything is working perfectly locally through Visual Studio and IIS Express, but when I deploy to IIS, the dependency's are not resolved. Below is my Startup.cs
public partial class Startup
{
public void Configuration(IAppBuilder app)
{
var webApiConfiguration = new HttpConfiguration();
webApiConfiguration.EnableCors();
webApiConfiguration.SuppressDefaultHostAuthentication();
webApiConfiguration.Filters.Add(new HostAuthenticationFilter(OAuthDefaults.AuthenticationType));
webApiConfiguration.MapHttpAttributeRoutes();
app.UseNinjectMiddleware(CreateKernel).UseNinjectWebApi(webApiConfiguration);
ConfigureAuth(app);
FilterConfig.RegisterGlobalFilters(GlobalFilters.Filters);
app.Run(async context =>
{
await context.Response.WriteAsync("Welcome to Web API");
});
}
private static IKernel CreateKernel()
{
var kernel = new StandardKernel();
kernel.Bind<Func<IKernel>>().ToMethod(ctx => () => new Bootstrapper().Kernel);
kernel.Bind<IHttpModule>().To<HttpApplicationInitializationHttpModule>();
kernel.Load(new CourseModule(), new DataPullModule(), new DegreeModule(), new ResponseModule(), new RestSharpModule());
return kernel;
}
}
The error I get is when trying to access one of my controllers is below:
An error occurred when trying to create a controller of type 'DegreeController'. Make sure that the controller has a parameterless public constructor.
Here is my constructor for the DegreeController:
public DegreeController(IDegreeMapper degreeMapper, IDegreeRepository degreeRepository)
{
_degreeMapper = degreeMapper;
_degreeRepository = degreeRepository;
}
And here is the DegreeModule where I bind interfaces to classes.
public class DegreeModule : NinjectModule
{
public override void Load()
{
Bind<IDegreeController>().To<DegreeController>().InRequestScope();
Bind<IDegreeMapper>().To<DegreeMapper>().InRequestScope();
Bind<IDegreeRepository>().To<DegreeRepository>().InRequestScope();
Bind<IDegreeRatingCalculator>().To<DegreeRatingCalculator>().InRequestScope();
}
}
var kernel = CreateKernel();
app.UseNinjectMiddleware(() => kernel).UseNinjectWebApi(configuration);
Here my server-side code
class Program
{
static void Main(string[] args)
{
string url = "http://localhost:8080";
using (WebApp.Start<Startup>(url))
{
Console.WriteLine("Server running on {0}", url);
}
}
}
public class Startup
{
public void Configuration(IAppBuilder app)
{
app.Map("/signalr", map =>
{
map.UseCors(CorsOptions.AllowAll);
var hubConfiguration = new HubConfiguration
{
};
map.RunSignalR(hubConfiguration);
});
}
}
When I run the console app and browse 'http://localhost:8080/signalr/hubs' on the server, it's show some jQuery code But when I try to browse 'http://[server-ip]/signalr/hubs', it's show error 400 on both server and clients
Is this mean than my server-side app not allow cross domain yet?
Here the solutions :
change
string url = "http://localhost:8080";
to
string url = "http://+:8080";