Skip to content

Commit

Permalink
fix ws handshake nitpicks
Browse files Browse the repository at this point in the history
  • Loading branch information
raftario committed Feb 27, 2022
1 parent b59812b commit 9b80fbc
Show file tree
Hide file tree
Showing 7 changed files with 72 additions and 24 deletions.
2 changes: 1 addition & 1 deletion Hydra.Example/Chat.cs
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@ namespace Hydra.Example
{
public static partial class Handlers
{
public static Task<HttpResponse> Chat(HttpRequest request) => Task.FromResult(WebSocket.Response(Example.Chat.Handler));
public static Task<HttpResponse> Chat(HttpRequest request) => Task.FromResult(WebSocket.Response(request, Example.Chat.Handler));
}

internal static class Chat
Expand Down
29 changes: 29 additions & 0 deletions Hydra/Exceptions.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
using Hydra.Http11;
using System;

namespace Hydra
{
public class HttpMethodNotAllowedException : Exception { }
public class HttpUpgradeRequiredException : Exception
{
public string Upgrade { get; }

public HttpUpgradeRequiredException(string upgrade) : base()
{
Upgrade = upgrade;
}
}

public class InvalidWebSocketKeyException : HttpBadRequestException { }
public class UnsupportedVersionException : HttpBadRequestException { }

public class NonGetWebSocketRequestException : HttpMethodNotAllowedException { }

public class HttpWebSocketUpgradeRequiredException : HttpUpgradeRequiredException
{
public HttpWebSocketUpgradeRequiredException() : base("websocket") { }
}

public class InvalidWebSocketUpgradeException : HttpWebSocketUpgradeRequiredException { }
public class InvalidWebSocketVersionException : HttpWebSocketUpgradeRequiredException { }
}
2 changes: 1 addition & 1 deletion Hydra/Http/HttpRequest.cs
Original file line number Diff line number Diff line change
Expand Up @@ -123,7 +123,7 @@ public async Task ReadHeaders(CancellationToken cancellationToken = default)
private void Validate()
{
// HTTP/1.1 requests must contain a Host header
if (Version == Http11.HttpVersion.Http10 && (!Headers.TryGetValue("Host", out var host) || host.Count > 1)) throw new InvalidHostException();
if (Version == Http11.HttpVersion.Http11 && (!Headers.TryGetValue("Host", out var host) || host.Count > 1)) throw new InvalidHostException();

// requests containing both Transfer-Encoding and Content-Length headers are invalid and should be rejected
if (Headers.ContainsKey("Transfer-Encoding") && Headers.ContainsKey("Content-Length")) throw new TransferEncodingAndContentLengthException();
Expand Down
2 changes: 1 addition & 1 deletion Hydra/Http/HttpResponse.cs
Original file line number Diff line number Diff line change
Expand Up @@ -106,7 +106,7 @@ public static async Task<bool> WriteResponse(this HttpWriter writer, HttpRespons
// if the client is HTTP/1.0 or indicates it wants the connection to close we need to close the connection once the body is set
bool needsClose = request.Version == HttpVersion.Http10
|| (request.Headers.TryGetValue("Connection", out var conn) && conn.ToString().Equals("close", StringComparison.OrdinalIgnoreCase));
// if the response has transfer encodings and the last one isn't chunked the client can't know the length and need to close the connection once the body is sent
// if the response has transfer encodings and the last one isn't chunked the client can't know the length and we need to close the connection once the body is sent
needsClose = needsClose || (!noBody
&& response.Headers.TryGetValue("Transfer-Encoding", out var te)
&& !te.ToString().TrimEnd().EndsWith("chunked", StringComparison.OrdinalIgnoreCase));
Expand Down
18 changes: 15 additions & 3 deletions Hydra/Server.cs
Original file line number Diff line number Diff line change
Expand Up @@ -188,19 +188,31 @@ private async Task HttpClient(Socket socket, CancellationToken cancellationToken
await httpWriter.Send(Stream.Null, cancellationToken);
return;
}
catch (HttpMethodNotAllowedException)
{
HttpWriteErrorResponse(httpWriter, 405, "Method Not Allowed");
await httpWriter.Send(Stream.Null, cancellationToken);
return;
}
catch (HttpNotImplementedException)
{
HttpWriteErrorResponse(httpWriter, 501, "Not Implemented");
await httpWriter.Send(Stream.Null, cancellationToken);
return;
}
catch (HttpUpgradeRequiredException ex)
{
HttpWriteErrorResponse(httpWriter, 426, "Upgrade Required");
httpWriter.WriteHeader("Upgrade", ex.Upgrade);
if (ex is HttpWebSocketUpgradeRequiredException) httpWriter.WriteHeader("Sec-WebSocket-Version", "13");
await httpWriter.Send(Stream.Null, cancellationToken);
return;
}

try
{

// returns true if we need to close
if (await httpWriter.WriteResponse(response, request, cancellationToken)
&& response is not WebSocketResponse) return;
if (await httpWriter.WriteResponse(response, request, cancellationToken)) return;

// need to make sure the whole request has been read before parsing the next one
await request.Drain();
Expand Down
16 changes: 1 addition & 15 deletions Hydra/WebSocket/WebSocket.cs
Original file line number Diff line number Diff line change
Expand Up @@ -72,21 +72,7 @@ internal WebSocket(Socket socket, PipeReader reader, PipeWriter writer, int back
/// is complete</param>
/// <returns>An HTTP response for the upgrade, or alternatively a code 400 response any of the necessary
/// WSS headers are missing or incorrect</returns>
public static HttpResponse Response(HttpRequest request, Server.WebSocketHandler handler)
{
// Verify all of the WebSocket headers
if (!request.Headers.TryGetValue("Sec-WebSocket-Key", out var clientKey)
|| (request.Headers.TryGetValue("Connection", out var conn)
&& conn != "Upgrade")
|| (request.Headers.TryGetValue("Upgrade", out var upgrade)
&& upgrade != "websocket")
|| (request.Headers.TryGetValue("Sec-WebSocket-Version", out var webSocketVersion)
&& webSocketVersion != "13")) // v13 is the only supported version by Hydra
return new HttpResponse(400);


return new WebSocketResponse(clientKey, handler);
}
public static HttpResponse Response(HttpRequest request, Server.WebSocketHandler handler) => new WebSocketResponse(request, handler);

[MethodImpl(MethodImplOptions.AggressiveOptimization)]
public async Task<bool> Send(WebSocketMessage message, CancellationToken cancellationToken = default)
Expand Down
27 changes: 24 additions & 3 deletions Hydra/WebSocketResponse.cs
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
using System;
using System.Linq;
using System.Security.Cryptography;
using System.Text;

Expand All @@ -10,15 +11,35 @@ public class WebSocketResponse : HttpResponse

internal Server.WebSocketHandler handler;

public WebSocketResponse(string clientKey, Server.WebSocketHandler handler) : base(101)
public WebSocketResponse(HttpRequest request, Server.WebSocketHandler handler) : base(101)
{
// HTTP/1.0 doesn't support upgrades
if (request.Version == Http11.HttpVersion.Http10)
throw new UnsupportedVersionException();
// client handshake must be a GET request
if (request.Method != "GET")
throw new NonGetWebSocketRequestException();
// client handshake `Upgrade` header must be `websocket`
if (!request.Headers.TryGetValue("Upgrade", out var upgrade) || !upgrade.ToString().Equals("websocket", StringComparison.OrdinalIgnoreCase))
throw new InvalidWebSocketUpgradeException();
// client handshake `Upgrade` header must be `websocket`
if (!request.Headers.TryGetValue("Connection", out var connection) || !connection.ToString().Contains("Upgrade", StringComparison.OrdinalIgnoreCase))
throw new InvalidWebSocketUpgradeException();
// client handhsake websocket version must be `13`
if (!request.Headers.Contains(new("Sec-WebSocket-Version", "13")))
throw new InvalidWebSocketVersionException();
// client handshake must contain a 16 bytes base64 key (will be 24 bytes when encoded)
if (!request.Headers.TryGetValue("Sec-WebSocket-Key", out var key) || key.ToString().Length != 24)
throw new InvalidWebSocketKeyException();

this.handler = handler;

byte[] acceptKeyBytes = SHA1.HashData(Encoding.UTF8.GetBytes(clientKey + WebSocketMagic));
byte[] hash = SHA1.HashData(Encoding.ASCII.GetBytes(key + WebSocketMagic));
string accept = Convert.ToBase64String(hash);

Headers["Upgrade"] = "websocket";
Headers["Connection"] = "Upgrade";
Headers["Sec-WebSocket-Accept"] = Convert.ToBase64String(acceptKeyBytes);
Headers["Sec-WebSocket-Accept"] = accept;
}
}
}

0 comments on commit 9b80fbc

Please sign in to comment.