WebSocket per-message compression in ASP.NET Core 6
Back in 2017, I've written about WebSocket per-message compression in ASP.NET Core. Back then the built-in support was deep in backlog with no foreseeable delivery, so I've created my own hackey implementation. Luckily it's 2021 and the built-in support is about to come with ASP.NET Core 6. I was very eager to archive my repository so I've decided to quickly take a look at what changes are needed to benefit from this new capability.
Enabling and Configuring Compression Extension Negotiation
In order to allow compression extension negotiation between client and server, you need to use AcceptWebSocketAsync
overload which takes WebSocketAcceptContext
as parameter and set DangerousEnableCompression
to true.
Let's say you have a WebSocket implementation which supports subprotocol negotiation.
WebSocket webSocket = await context.WebSockets.AcceptWebSocketAsync(textSubProtocol?.SubProtocol);
The version with compression negotiation enabled would look like below.
WebSocket webSocket = await context.WebSockets.AcceptWebSocketAsync(new WebSocketAcceptContext
{
SubProtocol = textSubProtocol?.SubProtocol,
DangerousEnableCompression = true
});
You might be wondering why such a "scary" property name. It's not because things may fail. If the client doesn't support compressions (or doesn't support compression with specific parameters), the negotiated connection will have compression disabled. It's about security. Similarly to HTTPS, encrypted WebSocket connections are subject to CRIME/BREACH attacks. If you are using encryption, you should be very cautious and not compress sensitive messages.
Regarding parameters, you can set no context takeover and max window bits through DisableServerContextTakeover
and ServerMaxWindowBits
properties of WebSocketAcceptContext
. The documentation states that they are advanced options that control how the compression works. I did high level description of them in my previous post and going deeper into details of sliding-window compression is beyond the scope of this post. So, without going to deep, I'm going to say that they allow for tuning between memory overhead and compression ratio.
Per-Message Compression
As we already know, using encryption and compression together is dangerous from security perspective. But what if only some specific messages are of sensitive nature? There is an option to disable compression for a specific message.
Most common used overload of SendAsync
is the one which takes bool for endOfMessage
parameter.
await _webSocket.SendAsync(messageSegment, messageType, true, cancellationToken);
In order to be able to disable compression on per-message basis, you need to use overload which accepts WebSocketMessageFlags
instead.
await _webSocket.SendAsync(messageSegment, messageType, WebSocketMessageFlags.EndOfMessage, cancellationToken);
Now you can disable compression for specific message by adding the DisableCompression
flag.
await _webSocket.SendAsync(messageSegment, messageType, WebSocketMessageFlags.EndOfMessage | WebSocketMessageFlags.DisableCompression, cancellationToken);
Of course this approach can be used anytime when you want to disable compression per-message, not only in security related scenarios.
Summary
WebSocket per-message compression in ASP.NET Core 6 is feature complete and I cannot be more happy to stop using my own implementation. If you want to play with it, I've updated my WebSocket demo to use it.