173 lines
7.1 KiB
C#
173 lines
7.1 KiB
C#
// Copyright (c) .NET Foundation. All rights reserved.
|
|
// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
|
|
|
|
using System;
|
|
using System.Collections.Generic;
|
|
using System.Globalization;
|
|
using System.IO;
|
|
using System.Linq;
|
|
using System.Net.WebSockets;
|
|
using System.Security.Cryptography;
|
|
using System.Text;
|
|
using System.Threading;
|
|
|
|
namespace Microsoft.Net.Http.Server
|
|
{
|
|
internal static class WebSocketHelpers
|
|
{
|
|
internal static string SupportedProtocolVersion = "13";
|
|
|
|
internal const string SecWebSocketKeyGuid = "258EAFA5-E914-47DA-95CA-C5AB0DC85B11";
|
|
internal const string WebSocketUpgradeToken = "websocket";
|
|
internal const int DefaultReceiveBufferSize = 16 * 1024;
|
|
internal const int DefaultClientSendBufferSize = 16 * 1024;
|
|
internal const int MaxControlFramePayloadLength = 123;
|
|
internal static readonly TimeSpan DefaultKeepAliveInterval = TimeSpan.FromMinutes(2);
|
|
|
|
// RFC 6455 requests WebSocket clients to let the server initiate the TCP close to avoid that client sockets
|
|
// end up in TIME_WAIT-state
|
|
//
|
|
// After both sending and receiving a Close message, an endpoint considers the WebSocket connection closed and
|
|
// MUST close the underlying TCP connection. The server MUST close the underlying TCP connection immediately;
|
|
// the client SHOULD wait for the server to close the connection but MAY close the connection at any time after
|
|
// sending and receiving a Close message, e.g., if it has not received a TCP Close from the server in a
|
|
// reasonable time period.
|
|
internal const int ClientTcpCloseTimeout = 1000; // 1s
|
|
|
|
private const int CloseStatusCodeAbort = 1006;
|
|
private const int CloseStatusCodeFailedTLSHandshake = 1015;
|
|
private const int InvalidCloseStatusCodesFrom = 0;
|
|
private const int InvalidCloseStatusCodesTo = 999;
|
|
private const string Separators = "()<>@,;:\\\"/[]?={} ";
|
|
|
|
internal static readonly ArraySegment<byte> EmptyPayload = new ArraySegment<byte>(new byte[] { }, 0, 0);
|
|
private static readonly Random KeyGenerator = new Random();
|
|
|
|
internal static bool AreWebSocketsSupported
|
|
{
|
|
get
|
|
{
|
|
return ComNetOS.IsWin8orLater;
|
|
}
|
|
}
|
|
|
|
internal static bool IsValidWebSocketKey(string key)
|
|
{
|
|
if (string.IsNullOrWhiteSpace(key))
|
|
{
|
|
return false;
|
|
}
|
|
// TODO:
|
|
// throw new NotImplementedException();
|
|
return true;
|
|
}
|
|
|
|
internal static string GetSecWebSocketAcceptString(string secWebSocketKey)
|
|
{
|
|
string retVal;
|
|
// SHA1 used only for hashing purposes, not for crypto. Check here for FIPS compat.
|
|
using (SHA1 sha1 = SHA1.Create())
|
|
{
|
|
string acceptString = string.Concat(secWebSocketKey, WebSocketHelpers.SecWebSocketKeyGuid);
|
|
byte[] toHash = Encoding.UTF8.GetBytes(acceptString);
|
|
retVal = Convert.ToBase64String(sha1.ComputeHash(toHash));
|
|
}
|
|
return retVal;
|
|
}
|
|
|
|
internal static WebSocket CreateServerWebSocket(Stream opaqueStream, string subProtocol, int receiveBufferSize, TimeSpan keepAliveInterval)
|
|
{
|
|
return ManagedWebSocket.CreateFromConnectedStream(opaqueStream, isServer: true, subprotocol: subProtocol,
|
|
keepAliveIntervalSeconds: (int)keepAliveInterval.TotalSeconds, receiveBufferSize: receiveBufferSize);
|
|
}
|
|
|
|
// return value here signifies if a Sec-WebSocket-Protocol header should be returned by the server.
|
|
internal static bool ProcessWebSocketProtocolHeader(IEnumerable<string> clientSecWebSocketProtocols, string subProtocol)
|
|
{
|
|
if (clientSecWebSocketProtocols == null || !clientSecWebSocketProtocols.Any())
|
|
{
|
|
// client hasn't specified any Sec-WebSocket-Protocol header
|
|
if (!string.IsNullOrEmpty(subProtocol))
|
|
{
|
|
// If the server specified _anything_ this isn't valid.
|
|
throw new WebSocketException(WebSocketError.UnsupportedProtocol,
|
|
"The client did not specify a Sec-WebSocket-Protocol header. SubProtocol: " + subProtocol);
|
|
}
|
|
// Treat empty and null from the server as the same thing here, server should not send headers.
|
|
return false;
|
|
}
|
|
|
|
// here, we know the client specified something and it's non-empty.
|
|
|
|
if (string.IsNullOrEmpty(subProtocol))
|
|
{
|
|
// client specified some protocols, server specified 'null'. So server should send headers.
|
|
return false;
|
|
}
|
|
|
|
// here, we know that the client has specified something, it's not empty
|
|
// and the server has specified exactly one protocol
|
|
|
|
// client specified protocols, serverOptions has exactly 1 non-empty entry. Check that
|
|
// this exists in the list the client specified.
|
|
foreach (var currentRequestProtocol in clientSecWebSocketProtocols)
|
|
{
|
|
if (string.Compare(subProtocol, currentRequestProtocol, StringComparison.OrdinalIgnoreCase) == 0)
|
|
{
|
|
return true;
|
|
}
|
|
}
|
|
|
|
throw new WebSocketException(WebSocketError.UnsupportedProtocol,
|
|
$"Unsupported protocol: {subProtocol}; Client supported protocols: {string.Join(", ", clientSecWebSocketProtocols)}");
|
|
}
|
|
|
|
internal static void ValidateSubprotocol(string subProtocol)
|
|
{
|
|
if (string.IsNullOrEmpty(subProtocol))
|
|
{
|
|
return;
|
|
}
|
|
|
|
char[] chars = subProtocol.ToCharArray();
|
|
string invalidChar = null;
|
|
int i = 0;
|
|
while (i < chars.Length)
|
|
{
|
|
char ch = chars[i];
|
|
if (ch < 0x21 || ch > 0x7e)
|
|
{
|
|
invalidChar = string.Format(CultureInfo.InvariantCulture, "[{0}]", (int)ch);
|
|
break;
|
|
}
|
|
|
|
if (!char.IsLetterOrDigit(ch) &&
|
|
Separators.IndexOf(ch) >= 0)
|
|
{
|
|
invalidChar = ch.ToString();
|
|
break;
|
|
}
|
|
|
|
i++;
|
|
}
|
|
|
|
if (invalidChar != null)
|
|
{
|
|
throw new ArgumentException($"Invalid character '{invalidChar}' in the subProtocol '{subProtocol}'", nameof(subProtocol));
|
|
}
|
|
}
|
|
|
|
internal static void ValidateOptions(string subProtocol, TimeSpan keepAliveInterval)
|
|
{
|
|
ValidateSubprotocol(subProtocol);
|
|
|
|
// -1
|
|
if (keepAliveInterval < Timeout.InfiniteTimeSpan)
|
|
{
|
|
throw new ArgumentOutOfRangeException(nameof(keepAliveInterval), keepAliveInterval,
|
|
"The value must be greater than or equal too 0 seconds, or -1 second to disable.");
|
|
}
|
|
}
|
|
}
|
|
}
|