You can not select more than 25 topics
Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
210 lines
8.0 KiB
210 lines
8.0 KiB
#if !NET20
|
|
|
|
using System;
|
|
using System.Collections.Generic;
|
|
using System.IO;
|
|
using System.Linq;
|
|
using System.Security.Cryptography;
|
|
using System.Text;
|
|
|
|
namespace Apewer.WebSocket
|
|
{
|
|
internal static class Hybi13Handler
|
|
{
|
|
public static ComposableHandler Create(HttpRequest request, Action<string> onMessage, Action onClose, Action<byte[]> onBytes, Action<byte[]> onPing, Action<byte[]> onPong)
|
|
{
|
|
var readState = new ReadState();
|
|
return new ComposableHandler
|
|
{
|
|
Handshake = sub => Hybi13Handler.BuildHandshake(request, sub),
|
|
TextFrame = s => Hybi13Handler.FrameData(Encoding.UTF8.GetBytes(s), FrameType.Text),
|
|
BytesFrame = s => Hybi13Handler.FrameData(s, FrameType.Bytes),
|
|
PingFrame = s => Hybi13Handler.FrameData(s, FrameType.Ping),
|
|
PongFrame = s => Hybi13Handler.FrameData(s, FrameType.Pong),
|
|
CloseFrame = i => Hybi13Handler.FrameData(i.ToBigEndianBytes<ushort>(), FrameType.Close),
|
|
ReceiveData = d => Hybi13Handler.ReceiveData(d, readState, (op, data) => Hybi13Handler.ProcessFrame(op, data, onMessage, onClose, onBytes, onPing, onPong))
|
|
};
|
|
}
|
|
|
|
public static byte[] FrameData(byte[] payload, FrameType frameType)
|
|
{
|
|
var memoryStream = new MemoryStream();
|
|
byte op = (byte)((byte)frameType + 128);
|
|
|
|
memoryStream.WriteByte(op);
|
|
|
|
if (payload.Length > UInt16.MaxValue) {
|
|
memoryStream.WriteByte(127);
|
|
var lengthBytes = payload.Length.ToBigEndianBytes<ulong>();
|
|
memoryStream.Write(lengthBytes, 0, lengthBytes.Length);
|
|
} else if (payload.Length > 125) {
|
|
memoryStream.WriteByte(126);
|
|
var lengthBytes = payload.Length.ToBigEndianBytes<ushort>();
|
|
memoryStream.Write(lengthBytes, 0, lengthBytes.Length);
|
|
} else {
|
|
memoryStream.WriteByte((byte)payload.Length);
|
|
}
|
|
|
|
memoryStream.Write(payload, 0, payload.Length);
|
|
|
|
return memoryStream.ToArray();
|
|
}
|
|
|
|
public static void ReceiveData(List<byte> data, ReadState readState, Action<FrameType, byte[]> processFrame)
|
|
{
|
|
|
|
while (data.Count >= 2)
|
|
{
|
|
var isFinal = (data[0] & 128) != 0;
|
|
var reservedBits = (data[0] & 112);
|
|
var frameType = (FrameType)(data[0] & 15);
|
|
var isMasked = (data[1] & 128) != 0;
|
|
var length = (data[1] & 127);
|
|
|
|
|
|
if (!isMasked
|
|
|| !Enum.IsDefined(typeof(FrameType), frameType)
|
|
|| reservedBits != 0 //Must be zero per spec 5.2
|
|
|| (frameType == FrameType.Continuation && !readState.FrameType.HasValue))
|
|
throw new WebSocketException(StatusCodes.ProtocolError);
|
|
|
|
var index = 2;
|
|
int payloadLength;
|
|
|
|
if (length == 127)
|
|
{
|
|
if (data.Count < index + 8)
|
|
return; //Not complete
|
|
payloadLength = data.Skip(index).Take(8).ToArray().ToLittleEndianInt();
|
|
index += 8;
|
|
}
|
|
else if (length == 126)
|
|
{
|
|
if (data.Count < index + 2)
|
|
return; //Not complete
|
|
payloadLength = data.Skip(index).Take(2).ToArray().ToLittleEndianInt();
|
|
index += 2;
|
|
}
|
|
else
|
|
{
|
|
payloadLength = length;
|
|
}
|
|
|
|
if (data.Count < index + 4)
|
|
return; //Not complete
|
|
|
|
var maskBytes = data.Skip(index).Take(4).ToArray();
|
|
|
|
index += 4;
|
|
|
|
|
|
if (data.Count < index + payloadLength)
|
|
return; //Not complete
|
|
|
|
byte[] payloadData = new byte[payloadLength];
|
|
for (int i = 0; i < payloadLength; i++)
|
|
payloadData[i] = (byte)(data[index+i] ^ maskBytes[i % 4]);
|
|
|
|
readState.Data.AddRange(payloadData);
|
|
data.RemoveRange(0, index + payloadLength);
|
|
|
|
if (frameType != FrameType.Continuation)
|
|
readState.FrameType = frameType;
|
|
|
|
if (isFinal && readState.FrameType.HasValue)
|
|
{
|
|
var stateData = readState.Data.ToArray();
|
|
var stateFrameType = readState.FrameType;
|
|
readState.Clear();
|
|
|
|
processFrame(stateFrameType.Value, stateData);
|
|
}
|
|
}
|
|
}
|
|
|
|
public static void ProcessFrame(FrameType frameType, byte[] data, Action<string> onMessage, Action onClose, Action<byte[]> onBytes, Action<byte[]> onPing, Action<byte[]> onPong)
|
|
{
|
|
switch (frameType)
|
|
{
|
|
case FrameType.Close:
|
|
if (data.Length == 1 || data.Length>125)
|
|
throw new WebSocketException(StatusCodes.ProtocolError);
|
|
|
|
if (data.Length >= 2)
|
|
{
|
|
var closeCode = (ushort)data.Take(2).ToArray().ToLittleEndianInt();
|
|
if (!StatusCodes.ValidCloseCodes.Contains(closeCode) && (closeCode < 3000 || closeCode > 4999))
|
|
throw new WebSocketException(StatusCodes.ProtocolError);
|
|
}
|
|
|
|
if (data.Length > 2)
|
|
ReadUTF8PayloadData(data.Skip(2).ToArray());
|
|
|
|
onClose();
|
|
break;
|
|
case FrameType.Bytes:
|
|
onBytes(data);
|
|
break;
|
|
case FrameType.Ping:
|
|
onPing(data);
|
|
break;
|
|
case FrameType.Pong:
|
|
onPong(data);
|
|
break;
|
|
case FrameType.Text:
|
|
onMessage(ReadUTF8PayloadData(data));
|
|
break;
|
|
default:
|
|
WebSocketLog.Debug("Received unhandled " + frameType);
|
|
break;
|
|
}
|
|
}
|
|
|
|
|
|
public static byte[] BuildHandshake(HttpRequest request, string subProtocol)
|
|
{
|
|
WebSocketLog.Debug("Building Hybi-14 Response");
|
|
|
|
var builder = new StringBuilder();
|
|
|
|
builder.Append("HTTP/1.1 101 Switching Protocols\r\n");
|
|
builder.Append("Upgrade: websocket\r\n");
|
|
builder.Append("Connection: Upgrade\r\n");
|
|
if (subProtocol != null)
|
|
builder.AppendFormat("Sec-WebSocket-Protocol: {0}\r\n", subProtocol);
|
|
|
|
var responseKey = CreateResponseKey(request["Sec-WebSocket-Key"]);
|
|
builder.AppendFormat("Sec-WebSocket-Accept: {0}\r\n", responseKey);
|
|
builder.Append("\r\n");
|
|
|
|
return Encoding.ASCII.GetBytes(builder.ToString());
|
|
}
|
|
|
|
private const string WebSocketResponseGuid = "258EAFA5-E914-47DA-95CA-C5AB0DC85B11";
|
|
|
|
public static string CreateResponseKey(string requestKey)
|
|
{
|
|
var combined = requestKey + WebSocketResponseGuid;
|
|
|
|
var bytes = SHA1.Create().ComputeHash(Encoding.ASCII.GetBytes(combined));
|
|
|
|
return Convert.ToBase64String(bytes);
|
|
}
|
|
|
|
private static string ReadUTF8PayloadData(byte[] bytes)
|
|
{
|
|
var encoding = new UTF8Encoding(false, true);
|
|
try
|
|
{
|
|
return encoding.GetString(bytes);
|
|
}
|
|
catch(ArgumentException)
|
|
{
|
|
throw new WebSocketException(StatusCodes.InvalidFramePayloadData);
|
|
}
|
|
}
|
|
}
|
|
|
|
}
|
|
|
|
#endif
|