commit
ee67efd29e
@ -1,7 +1,7 @@
|
||||
<Project Sdk="Microsoft.NET.Sdk">
|
||||
|
||||
<PropertyGroup>
|
||||
<TargetFramework>net5.0</TargetFramework>
|
||||
<TargetFramework>net6.0</TargetFramework>
|
||||
</PropertyGroup>
|
||||
|
||||
</Project>
|
||||
|
@ -0,0 +1,5 @@
|
||||
namespace Hamakaze.WebSocket {
|
||||
public interface IHasBinaryData {
|
||||
byte[] Data { get; }
|
||||
}
|
||||
}
|
@ -0,0 +1,11 @@
|
||||
using System;
|
||||
|
||||
namespace Hamakaze.WebSocket {
|
||||
public class WsBinaryMessage : WsMessage, IHasBinaryData {
|
||||
public byte[] Data { get; }
|
||||
|
||||
public WsBinaryMessage(byte[] data) {
|
||||
Data = data ?? Array.Empty<byte>();
|
||||
}
|
||||
}
|
||||
}
|
@ -0,0 +1,36 @@
|
||||
using System;
|
||||
|
||||
namespace Hamakaze.WebSocket {
|
||||
public class WsBufferedSend : IDisposable {
|
||||
private WsConnection Connection { get; }
|
||||
|
||||
internal WsBufferedSend(WsConnection conn) {
|
||||
Connection = conn ?? throw new ArgumentNullException(nameof(conn));
|
||||
}
|
||||
|
||||
public void SendPart(ReadOnlySpan<byte> data)
|
||||
=> Connection.WriteFrame(WsOpcode.DataBinary, data, false);
|
||||
|
||||
public void SendFinalPart(ReadOnlySpan<byte> data)
|
||||
=> Connection.WriteFrame(WsOpcode.DataBinary, data, true);
|
||||
|
||||
private bool IsDisposed;
|
||||
|
||||
~WsBufferedSend() {
|
||||
DoDispose();
|
||||
}
|
||||
|
||||
public void Dispose() {
|
||||
DoDispose();
|
||||
GC.SuppressFinalize(this);
|
||||
}
|
||||
|
||||
private void DoDispose() {
|
||||
if(IsDisposed)
|
||||
return;
|
||||
IsDisposed = true;
|
||||
|
||||
Connection.EndBufferedSend();
|
||||
}
|
||||
}
|
||||
}
|
@ -0,0 +1,306 @@
|
||||
using System;
|
||||
using System.Threading;
|
||||
|
||||
// todo: sending errors as fake close messages
|
||||
|
||||
namespace Hamakaze.WebSocket {
|
||||
public class WsClient : IDisposable {
|
||||
public WsConnection Connection { get; }
|
||||
public bool IsRunning { get; private set; } = true;
|
||||
|
||||
private Thread ReadThread { get; }
|
||||
private Action<WsMessage> MessageHandler { get; }
|
||||
private Action<Exception> ExceptionHandler { get; }
|
||||
|
||||
private Mutex SendLock { get; }
|
||||
private const int TIMEOUT = 60000;
|
||||
|
||||
public WsClient(
|
||||
WsConnection connection,
|
||||
Action<WsMessage> messageHandler,
|
||||
Action<Exception> exceptionHandler
|
||||
) {
|
||||
Connection = connection ?? throw new ArgumentNullException(nameof(connection));
|
||||
MessageHandler = messageHandler ?? throw new ArgumentNullException(nameof(messageHandler));
|
||||
ExceptionHandler = exceptionHandler ?? throw new ArgumentNullException(nameof(exceptionHandler));
|
||||
|
||||
SendLock = new();
|
||||
|
||||
ReadThread = new(ReadThreadBody) { IsBackground = true };
|
||||
ReadThread.Start();
|
||||
}
|
||||
|
||||
private void ReadThreadBody() {
|
||||
try {
|
||||
while(IsRunning)
|
||||
MessageHandler(Connection.Receive());
|
||||
} catch(Exception ex) {
|
||||
IsRunning = false;
|
||||
ExceptionHandler(ex);
|
||||
}
|
||||
}
|
||||
|
||||
public void Send(string text) {
|
||||
try {
|
||||
if(!SendLock.WaitOne(TIMEOUT))
|
||||
throw new WsClientMutexFailedException();
|
||||
Connection.Send(text);
|
||||
} finally {
|
||||
SendLock.ReleaseMutex();
|
||||
}
|
||||
}
|
||||
|
||||
public void Send(object obj) {
|
||||
if(obj == null)
|
||||
throw new ArgumentNullException(nameof(obj));
|
||||
|
||||
try {
|
||||
if(!SendLock.WaitOne(TIMEOUT))
|
||||
throw new WsClientMutexFailedException();
|
||||
Connection.Send(obj.ToString());
|
||||
} finally {
|
||||
SendLock.ReleaseMutex();
|
||||
}
|
||||
}
|
||||
|
||||
public void Send(ReadOnlySpan<byte> data) {
|
||||
if(data == null)
|
||||
throw new ArgumentNullException(nameof(data));
|
||||
|
||||
try {
|
||||
if(!SendLock.WaitOne(TIMEOUT))
|
||||
throw new WsClientMutexFailedException();
|
||||
Connection.Send(data);
|
||||
} finally {
|
||||
SendLock.ReleaseMutex();
|
||||
}
|
||||
}
|
||||
|
||||
public void Send(byte[] buffer, int offset, int count) {
|
||||
if(buffer == null)
|
||||
throw new ArgumentNullException(nameof(buffer));
|
||||
|
||||
try {
|
||||
if(!SendLock.WaitOne(TIMEOUT))
|
||||
throw new WsClientMutexFailedException();
|
||||
Connection.Send(buffer.AsSpan(offset, count));
|
||||
} finally {
|
||||
SendLock.ReleaseMutex();
|
||||
}
|
||||
}
|
||||
|
||||
public void Send(Action<WsBufferedSend> handler) {
|
||||
if(handler == null)
|
||||
throw new ArgumentNullException(nameof(handler));
|
||||
|
||||
try {
|
||||
if(!SendLock.WaitOne(TIMEOUT))
|
||||
throw new WsClientMutexFailedException();
|
||||
using(WsBufferedSend bs = Connection.BeginBufferedSend())
|
||||
handler(bs);
|
||||
} finally {
|
||||
SendLock.ReleaseMutex();
|
||||
}
|
||||
}
|
||||
|
||||
public void Ping() {
|
||||
try {
|
||||
if(!SendLock.WaitOne(TIMEOUT))
|
||||
throw new WsClientMutexFailedException();
|
||||
Connection.Ping();
|
||||
} finally {
|
||||
SendLock.ReleaseMutex();
|
||||
}
|
||||
}
|
||||
|
||||
public void Ping(ReadOnlySpan<byte> data) {
|
||||
if(data == null)
|
||||
throw new ArgumentNullException(nameof(data));
|
||||
|
||||
try {
|
||||
if(!SendLock.WaitOne(TIMEOUT))
|
||||
throw new WsClientMutexFailedException();
|
||||
Connection.Ping(data);
|
||||
} finally {
|
||||
SendLock.ReleaseMutex();
|
||||
}
|
||||
}
|
||||
|
||||
public void Ping(byte[] buffer, int offset, int length) {
|
||||
if(buffer == null)
|
||||
throw new ArgumentNullException(nameof(buffer));
|
||||
|
||||
try {
|
||||
if(!SendLock.WaitOne(TIMEOUT))
|
||||
throw new WsClientMutexFailedException();
|
||||
Connection.Ping(buffer.AsSpan(offset, length));
|
||||
} finally {
|
||||
SendLock.ReleaseMutex();
|
||||
}
|
||||
}
|
||||
|
||||
public void Pong() {
|
||||
try {
|
||||
if(!SendLock.WaitOne(TIMEOUT))
|
||||
throw new WsClientMutexFailedException();
|
||||
Connection.Pong();
|
||||
} finally {
|
||||
SendLock.ReleaseMutex();
|
||||
}
|
||||
}
|
||||
|
||||
public void Pong(ReadOnlySpan<byte> data) {
|
||||
if(data == null)
|
||||
throw new ArgumentNullException(nameof(data));
|
||||
|
||||
try {
|
||||
if(!SendLock.WaitOne(TIMEOUT))
|
||||
throw new WsClientMutexFailedException();
|
||||
Connection.Pong(data);
|
||||
} finally {
|
||||
SendLock.ReleaseMutex();
|
||||
}
|
||||
}
|
||||
|
||||
public void Pong(byte[] buffer, int offset, int length) {
|
||||
if(buffer == null)
|
||||
throw new ArgumentNullException(nameof(buffer));
|
||||
|
||||
try {
|
||||
if(!SendLock.WaitOne(TIMEOUT))
|
||||
throw new WsClientMutexFailedException();
|
||||
Connection.Pong(buffer.AsSpan(offset, length));
|
||||
} finally {
|
||||
SendLock.ReleaseMutex();
|
||||
}
|
||||
}
|
||||
|
||||
public void Close() {
|
||||
try {
|
||||
if(!SendLock.WaitOne(TIMEOUT))
|
||||
throw new WsClientMutexFailedException();
|
||||
Connection.Close(WsCloseReason.NormalClosure);
|
||||
} finally {
|
||||
SendLock.ReleaseMutex();
|
||||
}
|
||||
}
|
||||
|
||||
public void CloseEmpty() {
|
||||
try {
|
||||
if(!SendLock.WaitOne(TIMEOUT))
|
||||
throw new WsClientMutexFailedException();
|
||||
Connection.CloseEmpty();
|
||||
} finally {
|
||||
SendLock.ReleaseMutex();
|
||||
}
|
||||
}
|
||||
|
||||
public void Close(WsCloseReason opcode) {
|
||||
try {
|
||||
if(!SendLock.WaitOne(TIMEOUT))
|
||||
throw new WsClientMutexFailedException();
|
||||
Connection.Close(opcode);
|
||||
} finally {
|
||||
SendLock.ReleaseMutex();
|
||||
}
|
||||
}
|
||||
|
||||
public void Close(string reason) {
|
||||
if(reason == null)
|
||||
throw new ArgumentNullException(nameof(reason));
|
||||
|
||||
try {
|
||||
if(!SendLock.WaitOne(TIMEOUT))
|
||||
throw new WsClientMutexFailedException();
|
||||
Connection.Close(WsCloseReason.NormalClosure, reason);
|
||||
} finally {
|
||||
SendLock.ReleaseMutex();
|
||||
}
|
||||
}
|
||||
|
||||
public void Close(WsCloseReason opcode, string reason) {
|
||||
if(reason == null)
|
||||
throw new ArgumentNullException(nameof(reason));
|
||||
|
||||
try {
|
||||
if(!SendLock.WaitOne(TIMEOUT))
|
||||
throw new WsClientMutexFailedException();
|
||||
Connection.Close(opcode, reason);
|
||||
} finally {
|
||||
SendLock.ReleaseMutex();
|
||||
}
|
||||
}
|
||||
|
||||
public void Close(ReadOnlySpan<byte> data) {
|
||||
if(data == null)
|
||||
throw new ArgumentNullException(nameof(data));
|
||||
|
||||
try {
|
||||
if(!SendLock.WaitOne(TIMEOUT))
|
||||
throw new WsClientMutexFailedException();
|
||||
Connection.Close(data);
|
||||
} finally {
|
||||
SendLock.ReleaseMutex();
|
||||
}
|
||||
}
|
||||
|
||||
public void Close(byte[] buffer, int offset, int length) {
|
||||
if(buffer == null)
|
||||
throw new ArgumentNullException(nameof(buffer));
|
||||
|
||||
try {
|
||||
if(!SendLock.WaitOne(TIMEOUT))
|
||||
throw new WsClientMutexFailedException();
|
||||
Connection.Close(buffer.AsSpan(offset, length));
|
||||
} finally {
|
||||
SendLock.ReleaseMutex();
|
||||
}
|
||||
}
|
||||
|
||||
public void Close(WsCloseReason opcode, ReadOnlySpan<byte> data) {
|
||||
if(data == null)
|
||||
throw new ArgumentNullException(nameof(data));
|
||||
|
||||
try {
|
||||
if(!SendLock.WaitOne(TIMEOUT))
|
||||
throw new WsClientMutexFailedException();
|
||||
Connection.Close(opcode, data);
|
||||
} finally {
|
||||
SendLock.ReleaseMutex();
|
||||
}
|
||||
}
|
||||
|
||||
public void Close(WsCloseReason code, byte[] buffer, int offset, int length) {
|
||||
if(buffer == null)
|
||||
throw new ArgumentNullException(nameof(buffer));
|
||||
|
||||
try {
|
||||
if(!SendLock.WaitOne(TIMEOUT))
|
||||
throw new WsClientMutexFailedException();
|
||||
Connection.Close(code, buffer.AsSpan(offset, length));
|
||||
} finally {
|
||||
SendLock.ReleaseMutex();
|
||||
}
|
||||
}
|
||||
|
||||
private bool IsDisposed;
|
||||
|
||||
~WsClient() {
|
||||
DoDispose();
|
||||
}
|
||||
|
||||
public void Dispose() {
|
||||
DoDispose();
|
||||
GC.SuppressFinalize(this);
|
||||
}
|
||||
|
||||
private void DoDispose() {
|
||||
if(IsDisposed)
|
||||
return;
|
||||
IsDisposed = true;
|
||||
|
||||
SendLock.Dispose();
|
||||
Connection.Dispose();
|
||||
}
|
||||
}
|
||||
}
|
@ -0,0 +1,36 @@
|
||||
using System;
|
||||
using System.Text;
|
||||
|
||||
namespace Hamakaze.WebSocket {
|
||||
public class WsCloseMessage : WsMessage, IHasBinaryData {
|
||||
public WsCloseReason Reason { get; }
|
||||
public string ReasonPhrase { get; }
|
||||
public byte[] Data { get; }
|
||||
|
||||
public WsCloseMessage(WsCloseReason reason) {
|
||||
Reason = reason;
|
||||
ReasonPhrase = string.Empty;
|
||||
Data = Array.Empty<byte>();
|
||||
}
|
||||
|
||||
public WsCloseMessage(byte[] data) {
|
||||
if(data == null) {
|
||||
Reason = WsCloseReason.NoStatus;
|
||||
ReasonPhrase = string.Empty;
|
||||
Data = Array.Empty<byte>();
|
||||
} else {
|
||||
Reason = (WsCloseReason)WsUtils.ToU16(data);
|
||||
Data = data;
|
||||
|
||||
if(data.Length > 2)
|
||||
try {
|
||||
ReasonPhrase = Encoding.UTF8.GetString(data, 2, data.Length - 2);
|
||||
} catch {
|
||||
ReasonPhrase = string.Empty;
|
||||
}
|
||||
else
|
||||
ReasonPhrase = string.Empty;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
@ -0,0 +1,16 @@
|
||||
namespace Hamakaze.WebSocket {
|
||||
public enum WsCloseReason : ushort {
|
||||
NormalClosure = 1000,
|
||||
GoingAway = 1001,
|
||||
ProtocolError = 1002,
|
||||
InvalidData = 1003,
|
||||
NoStatus = 1005, // virtual -> no data in close frame
|
||||
AbnormalClosure = 1006, // virtual -> connection dropped
|
||||
MalformedData = 1007,
|
||||
PolicyViolation = 1008,
|
||||
FrameTooLarge = 1009,
|
||||
MissingExtension = 1010,
|
||||
UnexpectedCondition = 1011,
|
||||
TlsHandshakeFailed = 1015, // virtual -> obvious
|
||||
}
|
||||
}
|
@ -0,0 +1,395 @@
|
||||
using System;
|
||||
using System.IO;
|
||||
using System.Net.Security;
|
||||
using System.Security.Cryptography;
|
||||
using System.Text;
|
||||
|
||||
namespace Hamakaze.WebSocket {
|
||||
public class WsConnection : IDisposable {
|
||||
public Stream Stream { get; }
|
||||
|
||||
public bool IsSecure { get; }
|
||||
public bool IsClosed { get; private set; }
|
||||
|
||||
private const byte MASK_FLAG = 0x80;
|
||||
private const int MASK_SIZE = 4;
|
||||
|
||||
private WsOpcode FragmentType = 0;
|
||||
private MemoryStream FragmentStream;
|
||||
|
||||
private WsBufferedSend BufferedSend;
|
||||
|
||||
public WsConnection(Stream stream) {
|
||||
Stream = stream ?? throw new ArgumentNullException(nameof(stream));
|
||||
IsSecure = stream is SslStream;
|
||||
}
|
||||
|
||||
private static byte[] GenerateMask() {
|
||||
return RandomNumberGenerator.GetBytes(MASK_SIZE);
|
||||
}
|
||||
|
||||
private void StrictRead(byte[] buffer, int offset, int length) {
|
||||
int read = Stream.Read(buffer, offset, length);
|
||||
if(read < length)
|
||||
throw new Exception(@"Was unable to read the requested amount of data.");
|
||||
}
|
||||
|
||||
private (WsOpcode opcode, int length, bool isFinal, byte[] mask) ReadFrameHeader() {
|
||||
byte[] buffer = new byte[8];
|
||||
StrictRead(buffer, 0, 2);
|
||||
|
||||
WsOpcode opcode = (WsOpcode)(buffer[0] & 0x0F);
|
||||
bool isFinal = (buffer[0] & (byte)WsOpcode.FlagFinal) > 0;
|
||||
|
||||
if(opcode >= WsOpcode.CtrlClose && !isFinal)
|
||||
throw new WsInvalidOpcodeException((WsOpcode)buffer[0]);
|
||||
|
||||
bool isControl = (opcode & WsOpcode.CtrlClose) > 0;
|
||||
|
||||
if(isControl && !isFinal)
|
||||
throw new WsInvalidControlFrameException(@"fragmented");
|
||||
|
||||
bool isMasked = (buffer[1] & MASK_FLAG) > 0;
|
||||
|
||||
// this may look stupid and you'd be correct but it's better than the stack of casts
|
||||
// i'd otherwise have to do otherwise because c# converts everything back to int32
|
||||
buffer[1] &= 0x7F;
|
||||
long length = buffer[1];
|
||||
|
||||
if(length == 126) {
|
||||
StrictRead(buffer, 0, 2);
|
||||
length = WsUtils.ToU16(buffer);
|
||||
} else if(length == 127) {
|
||||
StrictRead(buffer, 0, 8);
|
||||
length = WsUtils.ToI64(buffer);
|
||||
}
|
||||
|
||||
if(isControl && length > 125)
|
||||
throw new WsInvalidControlFrameException(@"too large");
|
||||
|
||||
// should there be a sanity check on the length of frames?
|
||||
// i seriously don't understand the rationale behind both
|
||||
// having a framing system but then also supporting frame lengths
|
||||
// of 2^63, feels like 2^16 per frame would be a fine max.
|
||||
// UPDATE: decided to put the max at 2^32-1
|
||||
// it's still more than you should ever need for a single frame
|
||||
// and it makes working with the number within a .NET context
|
||||
// less of a bother.
|
||||
if(length < 0 || length > int.MaxValue)
|
||||
throw new WsInvalidFrameSizeException(length);
|
||||
|
||||
byte[] mask = null;
|
||||
|
||||
if(isMasked) {
|
||||
StrictRead(buffer, 0, MASK_SIZE);
|
||||
mask = buffer;
|
||||
}
|
||||
|
||||
return (opcode, (int)length, isFinal, mask);
|
||||
}
|
||||
|
||||
private int ReadFrameBody(byte[] target, int length, byte[] mask, int offset = 0) {
|
||||
if(target == null)
|
||||
throw new ArgumentNullException(nameof(target));
|
||||
|
||||
bool isMasked = mask != null;
|
||||
|
||||
int read;
|
||||
const int bufferSize = 0x1000;
|
||||
int take = length > bufferSize ? bufferSize : (int)length;
|
||||
|
||||
while(length > 0) {
|
||||
read = Stream.Read(target, offset, take);
|
||||
|
||||
if(isMasked)
|
||||
for(int i = 0; i < read; ++i) {
|
||||
int o = offset + i;
|
||||
target[o] ^= mask[o % MASK_SIZE];
|
||||
}
|
||||
|
||||
length -= read;
|
||||
offset += read;
|
||||
|
||||
if(take > length)
|
||||
take = (int)length;
|
||||
}
|
||||
|
||||
return offset;
|
||||
}
|
||||
|
||||
private WsMessage ReadFrame() {
|
||||
(WsOpcode opcode, int length, bool isFinal, byte[] mask) = ReadFrameHeader();
|
||||
|
||||
if(opcode is not WsOpcode.DataContinue
|
||||
and not WsOpcode.DataBinary
|
||||
and not WsOpcode.DataText
|
||||
and not WsOpcode.CtrlClose
|
||||
and not WsOpcode.CtrlPing
|
||||
and not WsOpcode.CtrlPong)
|
||||
throw new WsUnsupportedOpcodeException(opcode);
|
||||
|
||||
bool hasBody = length > 0;
|
||||
bool isContinue = opcode == WsOpcode.DataContinue;
|
||||
bool canFragment = (opcode & WsOpcode.CtrlClose) == 0;
|
||||
|
||||
byte[] body = length < 1 ? null : new byte[length];
|
||||
|
||||
if(hasBody) {
|
||||
ReadFrameBody(body, length, mask);
|
||||
|
||||
if(canFragment) {
|
||||
if(isContinue) {
|
||||
if(FragmentType == 0)
|
||||
throw new WsUnexpectedContinueException();
|
||||
|
||||
opcode = FragmentType;
|
||||
|
||||
FragmentStream ??= new();
|
||||
FragmentStream.Write(body, 0, length);
|
||||
} else {
|
||||
if(FragmentType != 0)
|
||||
throw new WsUnexpectedDataException();
|
||||
|
||||
if(!isFinal) {
|
||||
FragmentType = opcode;
|
||||
FragmentStream = new();
|
||||
FragmentStream.Write(body, 0, length);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
WsMessage msg;
|
||||
|
||||
if(isFinal) {
|
||||
if(canFragment && isContinue) {
|
||||
FragmentType = 0;
|
||||
|
||||
body = FragmentStream.ToArray();
|
||||
FragmentStream.Dispose();
|
||||
FragmentStream = null;
|
||||
}
|
||||
|
||||
msg = opcode switch {
|
||||
WsOpcode.DataText => new WsTextMessage(body),
|
||||
WsOpcode.DataBinary => new WsBinaryMessage(body),
|
||||
|
||||
WsOpcode.CtrlClose => new WsCloseMessage(body),
|
||||
WsOpcode.CtrlPing => new WsPingMessage(body),
|
||||
WsOpcode.CtrlPong => new WsPongMessage(body),
|
||||
|
||||
// fallback, if we end up here something is very fucked
|
||||
_ => throw new WsUnsupportedOpcodeException(opcode),
|
||||
};
|
||||
} else msg = null;
|
||||
|
||||
return msg;
|
||||
}
|
||||
|
||||
public WsMessage Receive() {
|
||||
WsMessage msg;
|
||||
while((msg = ReadFrame()) == null);
|
||||
return msg;
|
||||
}
|
||||
|
||||
private void WriteFrameHeader(WsOpcode opcode, int length, bool isFinal, byte[] mask = null) {
|
||||
if(length < 0 || length > int.MaxValue)
|
||||
throw new WsInvalidFrameSizeException(length);
|
||||
|
||||
bool shouldMask = mask != null;
|
||||
|
||||
if(isFinal)
|
||||
opcode |= WsOpcode.FlagFinal;
|
||||
|
||||
Stream.WriteByte((byte)opcode);
|
||||
|
||||
byte bLen1 = 0;
|
||||
if(shouldMask)
|
||||
bLen1 |= MASK_FLAG;
|
||||
|
||||
byte[] bLenBuff = WsUtils.FromI64(length);
|
||||
if(length < 126) {
|
||||
Stream.WriteByte((byte)(bLen1 | bLenBuff[7]));
|
||||
} else if(length <= ushort.MaxValue) {
|
||||
Stream.WriteByte((byte)(bLen1 | 126));
|
||||
Stream.Write(bLenBuff, 6, 2);
|
||||
} else {
|
||||
Stream.WriteByte((byte)(bLen1 | 127));
|
||||
Stream.Write(bLenBuff, 0, 8);
|
||||
}
|
||||
|
||||
if(shouldMask)
|
||||
Stream.Write(mask, 0, MASK_SIZE);
|
||||
Stream.Flush();
|
||||
}
|
||||
|
||||
private int WriteFrameBody(ReadOnlySpan<byte> body, byte[] mask = null, int offset = 0) {
|
||||
if(body == null)
|
||||
throw new ArgumentNullException(nameof(body));
|
||||
|
||||
if(mask != null) {
|
||||
byte[] masked = new byte[body.Length];
|
||||
|
||||
for(int i = 0; i < body.Length; ++i)
|
||||
masked[i] = (byte)(body[i] ^ mask[offset++ % MASK_SIZE]);
|
||||
|
||||
body = masked;
|
||||
}
|
||||
|
||||
Stream.Write(body);
|
||||
Stream.Flush();
|
||||
|
||||
return offset;
|
||||
}
|
||||
|
||||
internal void WriteFrame(WsOpcode opcode, ReadOnlySpan<byte> body, bool isFinal) {
|
||||
if(body == null)
|
||||
throw new ArgumentNullException(nameof(body));
|
||||
|
||||
byte[] mask = GenerateMask();
|
||||
WriteFrameHeader(opcode, body.Length, isFinal, mask);
|
||||
if(body.Length > 0)
|
||||
WriteFrameBody(body, mask);
|
||||
}
|
||||
|
||||
private void WriteData(WsOpcode opcode, ReadOnlySpan<byte> body) {
|
||||
if(body == null)
|
||||
throw new ArgumentNullException(nameof(body));
|
||||
if(BufferedSend != null)
|
||||
throw new WsBufferedSendInSessionException();
|
||||
|
||||
if(body.Length > ushort.MaxValue) {
|
||||
WriteFrame(opcode, body.Slice(0, ushort.MaxValue), false);
|
||||
body = body.Slice(ushort.MaxValue);
|
||||
|
||||
while(body.Length > ushort.MaxValue) {
|
||||
WriteFrame(WsOpcode.DataContinue, body.Slice(0, ushort.MaxValue), false);
|
||||
body = body.Slice(ushort.MaxValue);
|
||||
}
|
||||
|
||||
WriteFrame(WsOpcode.DataContinue, body, true);
|
||||
} else
|
||||
WriteFrame(opcode, body, true);
|
||||
}
|
||||
|
||||
public void Send(string text)
|
||||
=> WriteData(WsOpcode.DataText, Encoding.UTF8.GetBytes(text));
|
||||
|
||||
public void Send(ReadOnlySpan<byte> buffer)
|
||||
=> WriteData(WsOpcode.DataBinary, buffer);
|
||||
|
||||
public WsBufferedSend BeginBufferedSend() {
|
||||
if(BufferedSend != null)
|
||||
throw new WsBufferedSendAlreadyActiveException();
|
||||
return BufferedSend = new(this);
|
||||
}
|
||||
|
||||
// this method should only be called from within WsBufferedSend.Dispose
|
||||
internal void EndBufferedSend() {
|
||||
BufferedSend = null;
|
||||
}
|
||||
|
||||
private void WriteControl(WsOpcode opcode)
|
||||
=> WriteFrameHeader(opcode, 0, true, GenerateMask());
|
||||
|
||||
private void WriteControl(WsOpcode opcode, ReadOnlySpan<byte> buffer) {
|
||||
if(buffer == null)
|
||||
throw new ArgumentNullException(nameof(buffer));
|
||||
if(buffer.Length > 125)
|
||||
throw new ArgumentException(@"Data may not be more than 125 bytes.", nameof(buffer));
|
||||
|
||||
byte[] mask = GenerateMask();
|
||||
WriteFrameHeader(opcode, buffer.Length, true, mask);
|
||||
WriteFrameBody(buffer, mask);
|
||||
}
|
||||
|
||||
public void Ping()
|
||||
=> WriteControl(WsOpcode.CtrlPing);
|
||||
|
||||
public void Ping(ReadOnlySpan<byte> buffer)
|
||||
=> WriteControl(WsOpcode.CtrlPing, buffer);
|
||||
|
||||
public void Pong()
|
||||
=> WriteControl(WsOpcode.CtrlPong);
|
||||
|
||||
public void Pong(ReadOnlySpan<byte> buffer)
|
||||
=> WriteControl(WsOpcode.CtrlPong, buffer);
|
||||
|
||||
public void CloseEmpty() {
|
||||
if(IsClosed)
|
||||
return;
|
||||
IsClosed = true;
|
||||
|
||||
WriteControl(WsOpcode.CtrlClose);
|
||||
}
|
||||
|
||||
public void Close(ReadOnlySpan<byte> buffer) {
|
||||
if(buffer == null)
|
||||
throw new ArgumentNullException(nameof(buffer));
|
||||
|
||||
if(IsClosed)
|
||||
return;
|
||||
IsClosed = true;
|
||||
|
||||
WriteControl(WsOpcode.CtrlClose, buffer);
|
||||
}
|
||||
|
||||
public void Close(WsCloseReason code)
|
||||
=> Close(WsUtils.FromU16((ushort)code));
|
||||
|
||||
public void Close(WsCloseReason code, ReadOnlySpan<byte> reason) {
|
||||
if(reason == null)
|
||||
throw new ArgumentNullException(nameof(reason));
|
||||
if(reason.Length > 123)
|
||||
throw new ArgumentException(@"Reason may not be more than 123 bytes.", nameof(reason));
|
||||
|
||||
if(IsClosed)
|
||||
return;
|
||||
IsClosed = true;
|
||||
|
||||
byte[] mask = GenerateMask();
|
||||
WriteFrameHeader(WsOpcode.CtrlClose, 2 + reason.Length, true, mask);
|
||||
WriteFrameBody(WsUtils.FromU16((ushort)code), mask);
|
||||
WriteFrameBody(reason, mask, 2);
|
||||
}
|
||||
|
||||
public void Close(WsCloseReason code, string reason) {
|
||||
if(reason == null)
|
||||
throw new ArgumentNullException(nameof(reason));
|
||||
|
||||
int length = Encoding.UTF8.GetByteCount(reason);
|
||||
if(length > 123)
|
||||
throw new ArgumentException(@"Reason string may not exceed 123 bytes in length.", nameof(reason));
|
||||
|
||||
if(IsClosed)
|
||||
return;
|
||||
IsClosed = true;
|
||||
|
||||
byte[] mask = GenerateMask();
|
||||
WriteFrameHeader(WsOpcode.CtrlClose, 2 + reason.Length, true, mask);
|
||||
WriteFrameBody(WsUtils.FromU16((ushort)code), mask);
|
||||
WriteFrameBody(Encoding.UTF8.GetBytes(reason), mask, 2);
|
||||
}
|
||||
|
||||