-
Notifications
You must be signed in to change notification settings - Fork 4.8k
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Add compression support in WebSocket #31088
Comments
Something rubs me the wrong way about being able to mutate the options on the object after it's started on a per message basis. What's the rationale for that? |
@davidfowl No particular reason. The options would actually be used only once in the WebSocket (in the ctor), no reference to them would be kept. In that regard they could very well be a struct, although it would have to be nullable in WebSocketClientOptions. I find it more readable to be able to write new WebSocketCompressionOptions
{
ServerNoContextTakeover = true
} than to write new WebSocketCompressionOptions( serverNoContextTakeover: true ); With that in mind, this is only an opinion. If you think immutable class or struct is better for the case, I will change it. |
@dotnet/ncl |
Would this still be usable if it were less configurable? What do we get by exposing the LZ77 window size? |
@scalablecory The websocket would be very usable without it, but servers with high number of concurrent connections or clients with low memory requirements will have problems with any defaults we come up with. The websocket will have to keep 2 separate native memory buffers for inflate and deflate. I've collected benchmark traces for both with the different options to illustrate the point. Here is how the allocations look for inflate:
Here is how the allocations look for deflate. Here however we have another option to consider that I've not exposed - the memory and compression level and of the deflater (i.e. fast vs best). The benchmark bellow is running in the default memory level 8 (max is 9) used by DeflateStream for best compression.
The benchmark bellow is running in the default memory level 2.
As you can see the memory footprint for a single WebSocket can range anywhere between 18KB and 400KB. I cannot see how we can come up with any defaults that would be considered universally good enough for any web server or client. |
@danmosemsft any idea why it was added to the Project ".NET Core impacting internal partners"? |
@karelz that's where it most recently came up. |
@karelz I see that you've changed the milestone to Future. Does it mean you will not consider this proposal for the 5.0 release? |
@davidfowl Definitely. |
Milestone reflects our intent and priority from our side. It can change if more info appears. Nothing prevents anyone to get Future issues done in specific release, incl. 5.0. We take any high quality contributions. API design is tricky though in general and requires non-trivial investment from our side first. My understanding was that the original API proposal here seemed to be too complicated to the person driving our API reviews -- @scalablecory can provide more feedback / impressions he has from the API. @davidfowl what did you mean by "that's where it most recently came up."? Dan sent me offline info and it seems that it came from ASP.NET - maybe we can connect those dots also publicly here? |
👍
Sure and it has to be costed. That's basically what @zlatanov is looking for, design feedback following our typical API review process. We should spend a little time discussing it to see if it is complicated and make a decision. I don't think we have enough information to make a call yet (on the API).
It didn't, it came from another team internally at Microsoft. This issue has come up in the past filed by customers on the ASP.NET repository and we opened an issue here https://github.com/dotnet/corefx/issues/15430 a long time ago. |
@zlatanov Are you still going to take this on? |
@davidfowl Yup. |
@davidfowl @karelz Now that the main branch is 6.0, will this be considered? No feedback has been given since the API was proposed, almost 1 year ago (even just to say why it wasn't considered for 5.0 or at all). I'm sorry to say but it feels very off-putting. |
@zlatanov yes, we plan to get back to this in 6.0. Sorry if it felt off-putting, but sadly, we have only limited bandwidth and even though this was important, it was less important than other features/APIs and we just didn't have anyone to do the research and provide valuable input. Given our current priorities (finishing off 5.0, YARP, QUIC), I think we should be able to get to this API in couple of months -- with plenty of time to finish it off in 6.0. |
@zlatanov I've been watching this for some time - look forward to you having the chance to get this out there 👍 |
Okay, all the API details proposed make sense now that I've skimmed the RFC. I am not a web socket expert, so here are some thoughts -- @zlatanov @davidfowl does this make sense?:
And general API review thoughts:
So the final API would look something like: // new
public sealed class WebSocketCreationOptions
{
// taken from existing WebSocket.CreateFromStream params.
public bool IsServer { get; set; }
public string? SubProtocol { get; set; }
public TimeSpan KeepAliveInterval { get; set; }
// new
public bool UseDeflateCompression { get; set; } = false;
public int? ClientMaxDeflateWindowBits { get; set; } = null;
public bool ClientDeflateContextTakeover { get; set; } = true; // or ClientPersistDeflateContext etc.
public int? ServerMaxDeflateWindowBits { get; set; } = null;
public bool ServerDeflateContextTakeover { get; set; } = true;
}
// existing
public sealed class ClientWebSocketOptions
{
// new
public bool UseDeflateCompression { get; set; } = false;
public int? ClientMaxDeflateWindowBits { get; set; } = null;
public bool ClientDeflateContextTakeover { get; set; } = true; // or ClientPersistDeflateContext etc.
public int? ServerMaxDeflateWindowBits { get; set; } = null;
public bool ServerDeflateContextTakeover { get; set; } = true;
}
// existing
public abstract class WebSocket
{
// existing
public static WebSocket CreateFromStream(Stream stream, bool isServer, string? subProtocol, TimeSpan keepAliveInterval);
// new
public static WebSocket CreateFromStream(Stream stream, WebSocketCreationOptions options);
} This appears straightforward to implement. We can probably use the existing deflate code from System.IO.Compression. I think we'll want to add calls to |
@scalablecory thanks for the feedback. Here are my thoughts: I completely agree that renaming some of the properties towards making more sense for consumers who are not (and probably should not) familiar with the RFC. The implementation will be the same regardless server / client, but I agree that having this omitted from the client is a good thing, and basically matches what we have in the browsers right now where these options are handled by the browser itself. Personally I am not fond of changing the API for sending messages. My reasoning is that consumers down the line don't need to be rebuild to support compression, one only need to change the setup of the websocket server / client. One other important fact about having a I suggested |
Do you have some specific examples of where you'd want to disable compression? I agree it'd be easy to just leave it off here, but if there's a compelling use case we should keep it. If we do want to keep it, we can invert the feature -- instead of |
@zlatanov yes, it would be great if you could create a draft PR with your changes at that moment. You may additionally mark it as WIP if you wish. |
Hey @zlatanov I understand that you might have been busy, but have you got any chance to proceed with the implementation? Is there anything I can help you with? |
@CarnaViire I am working on the implementation, as it turned out little trickier than I thought because there are lots of assumptions inside the current implementation. Do you have somewhere where we could chat and figure out how we can collaborate? |
@zlatanov sure, just drop me an email (it should be visible on GH for now) and we'll figure it out from there. |
We've discussed that compression may cause security implications if turned on blindly. (You may see a part of that discussion on the PR here #49304 (comment)). We want to make sure that users understand that this is dangerous and encourage them to read the docs and weigh the risks. We also think it is important at this point to be able to turn off the compression for specific messages (if e.g. they contain a secret). That's why we want to add "Dangerous" prefix to the API and also add an API for per-message compression opt-out. I've highlighted the changes we'd like to add to the proposal. namespace System.Net.WebSockets
{
public sealed class WebSocketDeflateOptions
{
public int ClientMaxWindowBits { get; set; } = 15;
public bool ClientContextTakeover { get; set; } = true;
public int ServerMaxWindowBits { get; set; } = 15;
public bool ServerContextTakeover { get; set; } = true;
}
public sealed class WebSocketCreationOptions
{
public bool IsServer { get; set; }
public string? SubProtocol { get; set; }
public TimeSpan KeepAliveInterval { get; set; }
- public WebSocketDeflateOptions? DeflateOptions { get; set; } = null;
+ public WebSocketDeflateOptions? DangerousDeflateOptions { get; set; } = null;
}
public partial class ClientWebSocketOptions
{
- public WebSocketDeflateOptions? DeflateOptions { get; set; } = null;
+ public WebSocketDeflateOptions? DangerousDeflateOptions { get; set; } = null;
}
+ [Flags]
+ public enum WebSocketMessageFlags
+ {
+ // taken from existing WebSocket.SendAsync params
+ EndOfMessage = 1,
+
+ // new
+ DisableCompression = 2
+ }
public partial class WebSocket
{
public virtual ValueTask SendAsync(ReadOnlyMemory<byte> buffer, WebSocketMessageType messageType, bool endOfMessage, CancellationToken cancellationToken);
+ public virtual ValueTask SendAsync(ReadOnlyMemory<byte> buffer, WebSocketMessageType messageType, WebSocketMessageFlags messageFlags, CancellationToken cancellationToken = default);
}
} |
Would |
@danmoseley our reason for naming it |
I like the added proposal changes. I don't think if you invert the flag to |
I defer to whatever API review says ☺ |
namespace System.Net.WebSockets
{
public sealed class WebSocketCreationOptions
{
public bool IsServer { get; set; }
public string? SubProtocol { get; set; }
public TimeSpan KeepAliveInterval { get; set; }
- public WebSocketDeflateOptions? DeflateOptions { get; set; } = null;
+ public WebSocketDeflateOptions? DangerousDeflateOptions { get; set; } = null;
}
public partial class ClientWebSocketOptions
{
- public WebSocketDeflateOptions? DeflateOptions { get; set; } = null;
+ public WebSocketDeflateOptions? DangerousDeflateOptions { get; set; } = null;
}
+ [Flags]
+ public enum WebSocketMessageFlags
+ {
+ None = 0,
+
+ // taken from existing WebSocket.SendAsync params
+ EndOfMessage = 1,
+
+ // new
+ DisableCompression = 2
+ }
public partial class WebSocket
{
+ public virtual ValueTask SendAsync(ReadOnlyMemory<byte> buffer, WebSocketMessageType messageType, WebSocketMessageFlags messageFlags, CancellationToken cancellationToken = default);
}
} |
We've discussed the things left for consideration. 1) Whether the "Dangerous" prefix might be overused so it will lose its purpose 2) Defaulting If anyone sees any issues with our decisions, please let us know. |
Is it possible that there might in future be a solid mitigation to this dangerousness? Eg., a form of compression that pads to a fixed length. In that case, might we have a WebSocketDeflateOptions member that wasn't dangerous, and yet was accessed with DangerousDeflateOptions ? |
I doubt it. For compression to be useful, it has to compress to something smaller than the original. If it does, then it seems like it's still subject to a CRIME attack. |
To be clear, the general mitigation is to ensure you don't use the same compression context to compress data from different sources, at least one of which is untrusted. Unfortunately it's not easy to figure out how to enforce that generally. |
Adds support for RFC 7692 "compression extensions for websocket". Closes #31088
Adds support for RFC 7692 "compression extensions for websocket".
API Proposal
Original Request
AB#1118550
See discussion here #20004.
At the moment the WebSocket doesn't support per-message deflate (see https://tools.ietf.org/html/rfc7692#section-7). Adding support for it in the BCL will mean that people (myself including) will no longer resort to implementing custom WebSockets in order to use it.
Proposed API
Rationale and Usage
The main drive behind the API is that we should not introduce any breaking changes. WebSockets already built will work as is. This is why I suggest we add new CreateFromStream method with
WebSocketCompressionOptions
parameter instead of adding it to the existing one.There are a few options built in the WebSocket compression protocol that are considered advance use - controlling the size of the LZ77 sliding window, context takeover. We could easily hide them and choose reasonable defaults, but I think there are good use cases for them and as such we should expose them. See this blog post for good example of their usage: https://www.igvita.com/2013/11/27/configuring-and-optimizing-websocket-compression/.
The usage of the WebSocket would not change at all. By default a WebSocket created with compression options would compress all messages if the connection on the other end supports it. We introduce
OutputCompression
property to allow explicit opt out. The property has no effect if compression is not supported for the current connection.Here is example of how we would disable compression for specific messages:
Here is example for WebSocketClient:
Additional work will be required in AspNetCore repository and more specifically WebSocketMiddleware to light up the compression feature.
The text was updated successfully, but these errors were encountered: