Skip to content

Commit e8566e0

Browse files
committed
update
1 parent c52fd43 commit e8566e0

File tree

6 files changed

+211
-250
lines changed

6 files changed

+211
-250
lines changed

src/Middleware/WebSockets/src/ExtendedWebSocketAcceptContext.cs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -34,7 +34,7 @@ public class ExtendedWebSocketAcceptContext : WebSocketAcceptContext
3434
/// <summary>
3535
///
3636
/// </summary>
37-
public bool ServerContextTakeover { get; set; } = true;
37+
public bool DisableServerContextTakeover { get; set; } = false;
3838

3939
/// <summary>
4040
///

src/Middleware/WebSockets/src/HandshakeHelpers.cs

Lines changed: 133 additions & 83 deletions
Original file line numberDiff line numberDiff line change
@@ -77,11 +77,19 @@ public static string CreateResponseKey(string requestKey)
7777
}
7878

7979
// https://datatracker.ietf.org/doc/html/rfc7692#section-7.1
80-
public static bool ParseDeflateOptions(ReadOnlySpan<char> extension, WebSocketDeflateOptions options, [NotNullWhen(true)] out string? response)
80+
public static bool ParseDeflateOptions(ReadOnlySpan<char> extension, bool serverContextTakeover,
81+
int serverMaxWindowBits, out WebSocketDeflateOptions parsedOptions, [NotNullWhen(true)] out string? response)
8182
{
8283
bool hasServerMaxWindowBits = false;
8384
bool hasClientMaxWindowBits = false;
85+
bool hasClientNoContext = false;
86+
bool hasServerNoContext = false;
8487
response = null;
88+
parsedOptions = new WebSocketDeflateOptions()
89+
{
90+
ServerContextTakeover = serverContextTakeover,
91+
ServerMaxWindowBits = serverMaxWindowBits
92+
};
8593
var builder = new StringBuilder(WebSocketDeflateConstants.MaxExtensionLength);
8694
builder.Append(WebSocketDeflateConstants.Extension);
8795

@@ -90,83 +98,139 @@ public static bool ParseDeflateOptions(ReadOnlySpan<char> extension, WebSocketDe
9098
int end = extension.IndexOf(';');
9199
ReadOnlySpan<char> value = (end >= 0 ? extension[..end] : extension).Trim();
92100

93-
if (value.Length > 0)
101+
if (value.Length == 0)
102+
{
103+
break;
104+
}
105+
106+
if (value.SequenceEqual(WebSocketDeflateConstants.ClientNoContextTakeover))
107+
{
108+
// https://datatracker.ietf.org/doc/html/rfc7692#section-7
109+
// MUST decline if:
110+
// The negotiation offer contains multiple extension parameters with
111+
// the same name.
112+
if (hasClientNoContext)
113+
{
114+
return false;
115+
}
116+
117+
hasClientNoContext = true;
118+
parsedOptions.ClientContextTakeover = false;
119+
builder.Append("; ").Append(WebSocketDeflateConstants.ClientNoContextTakeover);
120+
}
121+
else if (value.SequenceEqual(WebSocketDeflateConstants.ServerNoContextTakeover))
122+
{
123+
// https://datatracker.ietf.org/doc/html/rfc7692#section-7
124+
// MUST decline if:
125+
// The negotiation offer contains multiple extension parameters with
126+
// the same name.
127+
if (hasServerNoContext)
128+
{
129+
return false;
130+
}
131+
132+
hasServerNoContext = true;
133+
parsedOptions.ServerContextTakeover = false;
134+
}
135+
else if (value.StartsWith(WebSocketDeflateConstants.ClientMaxWindowBits))
136+
{
137+
// https://datatracker.ietf.org/doc/html/rfc7692#section-7
138+
// MUST decline if:
139+
// The negotiation offer contains multiple extension parameters with
140+
// the same name.
141+
if (hasClientMaxWindowBits)
142+
{
143+
return false;
144+
}
145+
146+
hasClientMaxWindowBits = true;
147+
if (!ParseWindowBits(value, WebSocketDeflateConstants.ClientMaxWindowBits, out var clientMaxWindowBits))
148+
{
149+
return false;
150+
}
151+
152+
// 8 is a valid value according to the spec, but our zlib implementation does not support it
153+
if (clientMaxWindowBits == 8)
154+
{
155+
return false;
156+
}
157+
158+
// https://tools.ietf.org/html/rfc7692#section-7.1.2.2
159+
// the server may either ignore this
160+
// value or use this value to avoid allocating an unnecessarily big LZ77
161+
// sliding window by including the "client_max_window_bits" extension
162+
// parameter in the corresponding extension negotiation response to the
163+
// offer with a value equal to or smaller than the received value.
164+
parsedOptions.ClientMaxWindowBits = clientMaxWindowBits ?? 15;
165+
166+
// If a received extension negotiation offer doesn't have the
167+
// "client_max_window_bits" extension parameter, the corresponding
168+
// extension negotiation response to the offer MUST NOT include the
169+
// "client_max_window_bits" extension parameter.
170+
builder.Append("; ").Append(WebSocketDeflateConstants.ClientMaxWindowBits).Append('=')
171+
.Append(parsedOptions.ClientMaxWindowBits.ToString(CultureInfo.InvariantCulture));
172+
}
173+
else if (value.StartsWith(WebSocketDeflateConstants.ServerMaxWindowBits))
94174
{
95-
if (value.SequenceEqual(WebSocketDeflateConstants.ClientNoContextTakeover))
175+
// https://datatracker.ietf.org/doc/html/rfc7692#section-7
176+
// MUST decline if:
177+
// The negotiation offer contains multiple extension parameters with
178+
// the same name.
179+
if (hasServerMaxWindowBits)
96180
{
97-
options.ClientContextTakeover = false;
98-
builder.Append("; ").Append(WebSocketDeflateConstants.ClientNoContextTakeover);
181+
return false;
99182
}
100-
else if (value.SequenceEqual(WebSocketDeflateConstants.ServerNoContextTakeover))
183+
184+
hasServerMaxWindowBits = true;
185+
if (!ParseWindowBits(value, WebSocketDeflateConstants.ServerMaxWindowBits, out var parsedServerMaxWindowBits))
101186
{
102-
// REVIEW: Do we want to reject it?
103-
// Client requests no context takeover but options passed in specified context takeover, so reject the negotiate offer
104-
if (options.ServerContextTakeover)
105-
{
106-
return false;
107-
}
187+
return false;
108188
}
109-
else if (value.StartsWith(WebSocketDeflateConstants.ClientMaxWindowBits))
189+
190+
// 8 is a valid value according to the spec, but our zlib implementation does not support it
191+
if (parsedServerMaxWindowBits == 8)
110192
{
111-
var clientMaxWindowBits = ParseWindowBits(value, WebSocketDeflateConstants.ClientMaxWindowBits);
112-
// 8 is a valid value according to the spec, but our zlib implementation does not support it
113-
if (clientMaxWindowBits == 8)
114-
{
115-
return false;
116-
}
117-
118-
// https://tools.ietf.org/html/rfc7692#section-7.1.2.2
119-
// the server may either ignore this
120-
// value or use this value to avoid allocating an unnecessarily big LZ77
121-
// sliding window by including the "client_max_window_bits" extension
122-
// parameter in the corresponding extension negotiation response to the
123-
// offer with a value equal to or smaller than the received value.
124-
options.ClientMaxWindowBits = Math.Min(clientMaxWindowBits ?? 15, options.ClientMaxWindowBits);
125-
126-
// If a received extension negotiation offer doesn't have the
127-
// "client_max_window_bits" extension parameter, the corresponding
128-
// extension negotiation response to the offer MUST NOT include the
129-
// "client_max_window_bits" extension parameter.
130-
builder.Append("; ").Append(WebSocketDeflateConstants.ClientMaxWindowBits).Append('=')
131-
.Append(options.ClientMaxWindowBits.ToString(CultureInfo.InvariantCulture));
193+
return false;
132194
}
133-
else if (value.StartsWith(WebSocketDeflateConstants.ServerMaxWindowBits))
195+
196+
// https://tools.ietf.org/html/rfc7692#section-7.1.2.1
197+
// A server accepts an extension negotiation offer with this parameter
198+
// by including the "server_max_window_bits" extension parameter in the
199+
// extension negotiation response to send back to the client with the
200+
// same or smaller value as the offer.
201+
parsedOptions.ServerMaxWindowBits = Math.Min(parsedServerMaxWindowBits ?? 15, serverMaxWindowBits);
202+
}
203+
204+
static bool ParseWindowBits(ReadOnlySpan<char> value, string propertyName, out int? parsedValue)
205+
{
206+
var startIndex = value.IndexOf('=');
207+
208+
// parameters can be sent without a value by the client, we'll use the values set by the app developer or the default of 15
209+
if (startIndex < 0)
210+
{
211+
parsedValue = null;
212+
return true;
213+
}
214+
215+
value = value[(startIndex + 1)..].TrimEnd();
216+
217+
// https://datatracker.ietf.org/doc/html/rfc7692#section-5.2
218+
// check for value in quotes and pull the value out without the quotes
219+
if (value[0] == '"' && value.EndsWith("\"".AsSpan()))
134220
{
135-
hasServerMaxWindowBits = true;
136-
var serverMaxWindowBits = ParseWindowBits(value, WebSocketDeflateConstants.ServerMaxWindowBits);
137-
// 8 is a valid value according to the spec, but our zlib implementation does not support it
138-
if (serverMaxWindowBits == 8)
139-
{
140-
return false;
141-
}
142-
143-
// https://tools.ietf.org/html/rfc7692#section-7.1.2.1
144-
// A server accepts an extension negotiation offer with this parameter
145-
// by including the "server_max_window_bits" extension parameter in the
146-
// extension negotiation response to send back to the client with the
147-
// same or smaller value as the offer.
148-
options.ServerMaxWindowBits = Math.Min(serverMaxWindowBits ?? 15, options.ServerMaxWindowBits);
221+
value = value[1..^1];
149222
}
150223

151-
static int? ParseWindowBits(ReadOnlySpan<char> value, string propertyName)
224+
if (!int.TryParse(value, NumberStyles.Integer, CultureInfo.InvariantCulture, out int windowBits) ||
225+
windowBits < 8 ||
226+
windowBits > 15)
152227
{
153-
var startIndex = value.IndexOf('=');
154-
155-
// parameters can be sent without a value by the client, we'll use the values set by the app developer or the default of 15
156-
if (startIndex < 0)
157-
{
158-
return null;
159-
}
160-
161-
if (!int.TryParse(value[(startIndex + 1)..], NumberStyles.Integer, CultureInfo.InvariantCulture, out int windowBits) ||
162-
windowBits < 8 ||
163-
windowBits > 15)
164-
{
165-
throw new WebSocketException(WebSocketError.HeaderError, $"invalid {propertyName} used: {value[(startIndex + 1)..].ToString()}");
166-
}
167-
168-
return windowBits;
228+
parsedValue = null;
229+
return false;
169230
}
231+
232+
parsedValue = windowBits;
233+
return true;
170234
}
171235

172236
if (end < 0)
@@ -176,30 +240,16 @@ public static bool ParseDeflateOptions(ReadOnlySpan<char> extension, WebSocketDe
176240
extension = extension[(end + 1)..];
177241
}
178242

179-
if (!options.ServerContextTakeover)
243+
if (!parsedOptions.ServerContextTakeover)
180244
{
181245
builder.Append("; ").Append(WebSocketDeflateConstants.ServerNoContextTakeover);
182246
}
183247

184-
if (hasServerMaxWindowBits || options.ServerMaxWindowBits != 15)
248+
if (hasServerMaxWindowBits || parsedOptions.ServerMaxWindowBits != 15)
185249
{
186250
builder.Append("; ")
187251
.Append(WebSocketDeflateConstants.ServerMaxWindowBits).Append('=')
188-
.Append(options.ServerMaxWindowBits.ToString(CultureInfo.InvariantCulture));
189-
}
190-
191-
// https://tools.ietf.org/html/rfc7692#section-7.1.2.2
192-
// If a received extension negotiation offer doesn't have the
193-
// "client_max_window_bits" extension parameter, the corresponding
194-
// extension negotiation response to the offer MUST NOT include the
195-
// "client_max_window_bits" extension parameter.
196-
//
197-
// Absence of this extension parameter in an extension negotiation
198-
// response indicates that the server can receive messages compressed
199-
// using an LZ77 sliding window of up to 32,768 bytes.
200-
if (!hasClientMaxWindowBits)
201-
{
202-
options.ClientMaxWindowBits = 15;
252+
.Append(parsedOptions.ServerMaxWindowBits.ToString(CultureInfo.InvariantCulture));
203253
}
204254

205255
response = builder.ToString();

src/Middleware/WebSockets/src/PublicAPI.Unshipped.txt

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -2,8 +2,8 @@
22
Microsoft.AspNetCore.Builder.WebSocketOptions.AllowedOrigins.get -> System.Collections.Generic.IList<string!>!
33
Microsoft.AspNetCore.WebSockets.ExtendedWebSocketAcceptContext.DangerousEnableCompression.get -> bool
44
Microsoft.AspNetCore.WebSockets.ExtendedWebSocketAcceptContext.DangerousEnableCompression.set -> void
5-
Microsoft.AspNetCore.WebSockets.ExtendedWebSocketAcceptContext.ServerContextTakeover.get -> bool
6-
Microsoft.AspNetCore.WebSockets.ExtendedWebSocketAcceptContext.ServerContextTakeover.set -> void
5+
Microsoft.AspNetCore.WebSockets.ExtendedWebSocketAcceptContext.DisableServerContextTakeover.get -> bool
6+
Microsoft.AspNetCore.WebSockets.ExtendedWebSocketAcceptContext.DisableServerContextTakeover.set -> void
77
Microsoft.AspNetCore.WebSockets.ExtendedWebSocketAcceptContext.ServerMaxWindowBits.get -> int
88
Microsoft.AspNetCore.WebSockets.ExtendedWebSocketAcceptContext.ServerMaxWindowBits.set -> void
99
Microsoft.AspNetCore.WebSockets.WebSocketMiddleware.Invoke(Microsoft.AspNetCore.Http.HttpContext! context) -> System.Threading.Tasks.Task!

src/Middleware/WebSockets/src/WebSocketMiddleware.cs

Lines changed: 5 additions & 25 deletions
Original file line numberDiff line numberDiff line change
@@ -154,7 +154,7 @@ public async Task<WebSocket> AcceptAsync(WebSocketAcceptContext acceptContext)
154154
keepAliveInterval = advancedAcceptContext.KeepAliveInterval.Value;
155155
}
156156
enableCompression = advancedAcceptContext.DangerousEnableCompression;
157-
serverContextTakeover = advancedAcceptContext.ServerContextTakeover;
157+
serverContextTakeover = !advancedAcceptContext.DisableServerContextTakeover;
158158
serverMaxWindowBits = advancedAcceptContext.ServerMaxWindowBits;
159159
}
160160

@@ -168,18 +168,14 @@ public async Task<WebSocket> AcceptAsync(WebSocketAcceptContext acceptContext)
168168
var ext = _context.Request.Headers.SecWebSocketExtensions;
169169
if (ext.Count != 0)
170170
{
171-
// loop over each extension offer, extensions can have multiple offers we can accept any
171+
// loop over each extension offer, extensions can have multiple offers, we can accept any
172172
foreach (var extension in _context.Request.Headers.GetCommaSeparatedValues(HeaderNames.SecWebSocketExtensions))
173173
{
174-
if (extension.TrimStart().StartsWith("permessage-deflate", StringComparison.Ordinal))
174+
if (extension.AsSpan().TrimStart().StartsWith("permessage-deflate", StringComparison.Ordinal))
175175
{
176-
deflateOptions = new WebSocketDeflateOptions()
177-
{
178-
ServerContextTakeover = serverContextTakeover,
179-
ServerMaxWindowBits = serverMaxWindowBits
180-
};
181-
if (HandshakeHelpers.ParseDeflateOptions(extension, deflateOptions, out var response))
176+
if (HandshakeHelpers.ParseDeflateOptions(extension.AsSpan().TrimStart(), serverContextTakeover, serverMaxWindowBits, out var parsedOptions, out var response))
182177
{
178+
deflateOptions = parsedOptions;
183179
// If more extension types are added, this would need to be a header append
184180
// and we wouldn't want to break out of the loop
185181
_context.Response.Headers.SecWebSocketExtensions = response;
@@ -201,22 +197,6 @@ public async Task<WebSocket> AcceptAsync(WebSocketAcceptContext acceptContext)
201197
});
202198
}
203199

204-
private static WebSocketDeflateOptions? CloneWebSocketDeflateOptions([NotNullIfNotNull("options")] WebSocketDeflateOptions? options)
205-
{
206-
if (options is null)
207-
{
208-
return null;
209-
}
210-
211-
return new WebSocketDeflateOptions()
212-
{
213-
ClientContextTakeover = options.ClientContextTakeover,
214-
ClientMaxWindowBits = options.ClientMaxWindowBits,
215-
ServerContextTakeover = options.ServerContextTakeover,
216-
ServerMaxWindowBits = options.ServerMaxWindowBits
217-
};
218-
}
219-
220200
public static bool CheckSupportedWebSocketRequest(string method, IHeaderDictionary requestHeaders)
221201
{
222202
if (!HttpMethods.IsGet(method))

0 commit comments

Comments
 (0)