Polyhedric

Cristian Merighi's Multi-Faceted Blog

SignalR Core v1 First Impressions

SignalR Core v1 First Impressions

Thursday, April 26, 2018

Moving my domotics system to Asp.Net Core 2.1 and SignalR v1. What I've learned so far.


The Big Picture

Goal: manage local devices via a Web based solution.
Technologies involved:

  • IdentityServer4 as Identity Provider (Asp.Net Core v2.0)
  • Web Api as API resource (OAuth2) exposing SignalR Hubs (Asp.Net Core v2.1-preview2)
  • Web Client Application (Asp.Net Core v2.1-preview2)
  • Pacem JS as client side library (TypeScript)
  • bTicino home devices (Tcp/Ip + OpenWebNet protocol)
  • IP Cameras (MJpeg stream basically)

There's/was already a working prototype that I wrote in Asp.Net Mvc 4 and Angular2+, but I've been lately revisiting my tech chain aiming for AP-squared (As-Productive-As-Possible) ...-ness.
So my decision to:

  • Centralize the Authentication Layer exposing it as a service (thank you Brock and Dominick!)
  • Unpack the existing monolith into micro-services (thank you Damian & co.)
  • Stop following the JavaScript Framework delirium and start following the do-it-yourself approach (thank you Anders & co.)

...mostly a combination of DRY + DIY.
Having developed almost exclusively Asp.Net Core the last year/year-and-a-half and being fully aware of my Pacem JS strengths and weaknesses, the surprise factor completely stood on SignalR. Good surprises, indeed!
I'm gonna write here about a couple of them, contextualized in a real-world complexity (I won't get into the details, for brevity sake).

Integrated Authorization

Goosebumps, when I saw a new Context property (of type HubCallerContext) for the Hub class. It brings a ClaimsPrincipal user along that fits perfectly the eventual integration with the Identity Provider, under the OAuth2 (introspection) specs.
There's however a bit of work to do in order to make it run: a hint must be provided to the system to "find" the token while negotiating the websocket connection.

signalr-1

As you can see from the screenshot above, the access_token (I normally use reference tokens, not jwt) gets passed in querystring. Therefore, the need to implement a fallback resolution beside the usual Bearer authorization header.

// ConfigureServices: body extract (DI) string tokenRetriever(Microsoft.AspNetCore.Http.HttpRequest context) { if (context.Headers.TryGetValue("Authorization", out StringValues headers)) { string bearerPrefix = "Bearer "; var header = headers.Where(h => h?.StartsWith(bearerPrefix) == true) .FirstOrDefault(); if (header != null) return header.Substring(bearerPrefix.Length); } // querystring fallback if (context.Query.TryGetValue("access_token", out StringValues qsp)) { return qsp.FirstOrDefault(); } // not found return default(string); } services.AddAuthentication("Bearer") .AddOAuth2Introspection(options => { options.Authority = Configuration.GetValue<string>("api:authority"); options.ClientId = Configuration.GetValue<string>("api:id"); options.ClientSecret = Configuration.GetValue<string>("api:secret"); options.CacheDuration = TimeSpan.FromMinutes(5D); options.EnableCaching = true; options.TokenRetriever = tokenRetriever; options.NameClaimType = "name"; }); //... SignalR and Cors services.AddSignalR(); services.AddCors(options => { options.AddDefaultPolicy(cors => { cors.AllowAnyMethod(); cors.AllowAnyOrigin(); cors.AllowAnyHeader(); // signalr client passes credentials/cookies cors.AllowCredentials(); }); }); // Configure: body extract (pipeline) app.UseCors(); app.UseAuthentication(); app.UseSignalR(routes => { routes.MapHub<Hubs.PhousysHub>("/phousys"); });

Here's what happen at API level while trying to get a DLink IP camera stream whose access is granted by my "exclusive" family, name and some other claims (introspected by the Web API):

SignalR Hub Logging

DI/IoC and IHubContext<THub>

Differently from the "previous" SignalR, the Hub instance gets disposed after each request.
This is not a tiny change if you have a structured hub system. Mine, for instance, hides the typical - old-school - structured chat pattern that involves Lobbies and Rooms (...ah, old good times!). If you expect to easily roll timers or other persisting objects into the hub, you'll definitely fail with messages like "System.ObjectDisposedException: 'Cannot access a disposed object.'" right because the access to the hub resources is very volatile.
DI and IoC come to the rescue and save the day: the very IHubContext of your hub can be, in fact, injected into a singleton service that may persist "immortal" objects.
This is no edge case at all, the use of in-memory singletons is normal for real-time applications: if you wrap HTTP, ODBC, etc while websocket-ing, well ...goodbye real-time.

public class PhousysLobby : Lobby<PhousysHub> { private readonly IHubContext<PhousysHub> _phousys; private readonly ILogger<PhousysLobby> _logger; public PhousysLobby(IHubContext<PhousysHub> phousys, ILogger<PhousysLobby> logger) : base(phousys) { _phousys = phousys; _logger = logger; } }

It took me literally just a couple of minutes to refactor the whole structure and invert the Lobby+Rooms control! Great.
The core HTML+Razor markup needed to access the test DLink camera is - simplified - the following:

<pacem-data id="token" model="'@Model.AccessToken'"></pacem-data> <pacem-data id="alive" model="{{ Date.now() }}"></pacem-data> <pacem-img id="cam" style="background-color: #000;"></pacem-img> <pacem-hub-proxy accesstoken="{{ #token.model }}" id="hub" url="'@(Model.Endpoint)phousys'"> <pacem-hub-listener method="'newFrame'" on-receive="#cam.src = 'data:image/jpeg;base64,'+ $event.detail[1]"></pacem-hub-listener> <pacem-hub-listener method="'keepAlive'" on-receive="#alive.model = Date.now()"></pacem-hub-listener> </pacem-hub-proxy> <pacem-text text="{{ #hub.connected ? ('connected ' + pacem.date(#alive.model, 'full')) : 'disconnected' }}"></pacem-text>

...whoops! Ehm, Hi...

Output

Coming up next: Light toggling and dimming.

Recommend
Add Comment
Thank you for your feedback!
Your comment will be processed as soon as possible.