465 lines
16 KiB
C#
465 lines
16 KiB
C#
using System;
|
|
using Tesses.WebServer;
|
|
using System.Threading.Tasks;
|
|
using Tesses.WebServer.WebSocket;
|
|
using System.Threading;
|
|
using System.Text;
|
|
using Newtonsoft.Json;
|
|
using System.Security.Cryptography;
|
|
using System.Collections.Generic;
|
|
using System.Linq;
|
|
using System.Collections;
|
|
using System.IO;
|
|
|
|
namespace Tesses.WebServer
|
|
{
|
|
|
|
public abstract class EasyWebSocketServer
|
|
{
|
|
System.Timers.Timer timer;
|
|
Func<WebSocketMessage,Task> wsm;
|
|
Func<byte[],Task> ping;
|
|
bool canEnable;
|
|
public bool Enabled {
|
|
get{
|
|
if(!canEnable) return false;
|
|
return timer.Enabled;
|
|
}
|
|
|
|
set{
|
|
if(canEnable) timer.Enabled = value;
|
|
}}
|
|
internal async Task Opened(Func<WebSocketMessage,Task> sendWsm,Func<byte[],Task> ping,CancellationToken token)
|
|
{
|
|
this.wsm=sendWsm;
|
|
this.ping =ping;
|
|
timer=new System.Timers.Timer();
|
|
timer.Elapsed += async(sender,e)=>{
|
|
try{
|
|
await Ping();
|
|
}catch(Exception ex)
|
|
{
|
|
_=ex;
|
|
}
|
|
};
|
|
timer.Interval = 10000;
|
|
canEnable=true;
|
|
await OnConnectionStarted(token);
|
|
}
|
|
public abstract Task OnConnectionStarted(CancellationToken token);
|
|
public async Task Ping()
|
|
{
|
|
await Ping(new byte[]{ (byte)'P', (byte)'i', (byte)'n', (byte)'g' });
|
|
}
|
|
public async Task Ping(byte[] data)
|
|
{
|
|
await ping(data);
|
|
}
|
|
public abstract Task OnReceiveMessage(WebSocketMessage msg);
|
|
public async Task SendMessage(WebSocketMessage msg)
|
|
{
|
|
await wsm(msg);
|
|
}
|
|
public void Close(bool clean)
|
|
{
|
|
canEnable=false;
|
|
timer.Enabled=false;
|
|
timer.Dispose();
|
|
|
|
OnConnectionEnded(clean);
|
|
}
|
|
protected virtual void OnConnectionEnded(bool clean)
|
|
{
|
|
|
|
}
|
|
|
|
}
|
|
public static class WebSocketExtensions
|
|
{
|
|
|
|
internal static bool FirstEquals<T1,T2>(this Dictionary<T1,List<T2>> dict,T1 t,T2 t2)
|
|
{
|
|
T2 firstVal;
|
|
return dict.TryGetFirst(t,out firstVal) && firstVal.Equals(t2);
|
|
}
|
|
internal static bool AnyEquals<T1,T2>(this Dictionary<T1,List<T2>> dict,T1 t,T2 t2)
|
|
{
|
|
List<T2> items;
|
|
if(dict.TryGetValue(t,out items))
|
|
{
|
|
foreach(var item in items)
|
|
{
|
|
if(item.Equals(t2)) return true;
|
|
}
|
|
}
|
|
return false;
|
|
|
|
}
|
|
public static void StartWebSocketConnection(this ServerContext ctx,Action<Action<WebSocketMessage>,Action<byte[]>,CancellationToken> opened,Action<WebSocketMessage> arrived,Action<bool> closed)
|
|
{
|
|
var t=ctx.StartWebSocketConnectionAsync(async(s,p,c)=>await Task.Run(()=>opened(
|
|
(mm)=>{
|
|
Task.Run(async()=>await s(mm)).Wait();
|
|
},(data)=>{
|
|
Task.Run(async()=>await p(data)).Wait();
|
|
},c)),async(m)=>await Task.Run(()=>arrived(m)),closed);
|
|
Task.Run(()=>t).Wait();
|
|
}
|
|
public static async Task StartEasyWebSocketConnectionAsync(this ServerContext ctx,EasyWebSocketServer wss)
|
|
{
|
|
await ctx.StartWebSocketConnectionAsync(wss.Opened,wss.OnReceiveMessage,wss.Close);
|
|
}
|
|
public static async Task StartWebSocketConnectionAsync(this ServerContext ctx,Func<Func<WebSocketMessage,Task>,Func<byte[],Task>,CancellationToken,Task> opened,Func<WebSocketMessage,Task> arrived,Action<bool> closed)
|
|
{
|
|
WebSocketServer server=new WebSocketServer(ctx);
|
|
server.MessageArrived+=async(sender,e)=>{
|
|
try{
|
|
await arrived(e.Message);
|
|
}catch(Exception ex)
|
|
{
|
|
_=ex;
|
|
}
|
|
};
|
|
server.WebSocketClosed+=(sender,e)=>{
|
|
|
|
closed(e.Clean);
|
|
|
|
};
|
|
|
|
using(var cts=new CancellationTokenSource()){
|
|
Thread t=new Thread(async()=>{
|
|
try{
|
|
await opened(server.SendMessageAsync,server.Ping,cts.Token);
|
|
}catch(Exception ex)
|
|
{
|
|
_=ex;
|
|
}
|
|
});
|
|
t.Start();
|
|
await server.StartAsync();
|
|
cts.Cancel();
|
|
t.Join();
|
|
}
|
|
}
|
|
}
|
|
}
|
|
namespace Tesses.WebServer.WebSocket
|
|
{
|
|
public class WebSocketMessage
|
|
{
|
|
public static WebSocketMessage Create(string text)
|
|
{
|
|
WebSocketMessage msg=new WebSocketMessage();
|
|
msg.Text = text;
|
|
return msg;
|
|
}
|
|
public static WebSocketMessage Create(byte[] data)
|
|
{
|
|
WebSocketMessage msg=new WebSocketMessage();
|
|
msg.Data=data;
|
|
return msg;
|
|
}
|
|
public static WebSocketMessage Create(object data)
|
|
{
|
|
WebSocketMessage msg=new WebSocketMessage();
|
|
msg.EncodeJson(data);
|
|
return msg;
|
|
}
|
|
private WebSocketMessage()
|
|
{
|
|
Data=new byte[0];
|
|
|
|
}
|
|
internal WebSocketMessage(byte[] message,bool binary)
|
|
{
|
|
data=message;
|
|
Binary=binary;
|
|
}
|
|
private byte[] data;
|
|
public bool Binary {get;private set;}
|
|
public byte[] Data {get{return data;} private set{data=value; Binary=true;}}
|
|
|
|
public T DecodeJson<T>()
|
|
{
|
|
return JsonConvert.DeserializeObject<T>(Text);
|
|
}
|
|
private void EncodeJson(object data)
|
|
{
|
|
Text=JsonConvert.SerializeObject(data);
|
|
}
|
|
|
|
|
|
public string Text {get{return Encoding.UTF8.GetString(Data);} private set{data=Encoding.UTF8.GetBytes(value); Binary=false;}}
|
|
}
|
|
public class WebSocketMessageEventArgs : EventArgs
|
|
{
|
|
public WebSocketMessageEventArgs(WebSocketMessage message)
|
|
{
|
|
Message=message;
|
|
}
|
|
public WebSocketMessage Message {get;private set;}
|
|
}
|
|
public class WebSocketClosedEventArgs : EventArgs
|
|
{
|
|
public WebSocketClosedEventArgs(bool clean)
|
|
{
|
|
Clean=clean;
|
|
}
|
|
public bool Clean {get;private set;}
|
|
}
|
|
|
|
public class WebSocketServer
|
|
{
|
|
Mutex mtx=new Mutex();
|
|
bool hasInit=false;
|
|
ServerContext context;
|
|
public WebSocketServer(ServerContext ctx)
|
|
{
|
|
context=ctx;
|
|
}
|
|
public EventHandler<WebSocketMessageEventArgs> MessageArrived;
|
|
|
|
public EventHandler<WebSocketClosedEventArgs> WebSocketClosed;
|
|
|
|
private byte[] glenBytes(long len)
|
|
{
|
|
if(len < 126)
|
|
{
|
|
return new byte[]{(byte)len};
|
|
}else if(len <= ushort.MaxValue)
|
|
{
|
|
byte[] num = BitConverter.GetBytes((ushort)len);
|
|
if(BitConverter.IsLittleEndian)
|
|
{
|
|
Array.Reverse(num);
|
|
}
|
|
return new byte[]{126,num[0],num[1]};
|
|
}else{
|
|
byte[] num = BitConverter.GetBytes(len);
|
|
if(BitConverter.IsLittleEndian)
|
|
{
|
|
Array.Reverse(num);
|
|
}
|
|
return new byte[]{127,num[0],num[1],num[2],num[3],num[4],num[5],num[6],num[7]};
|
|
}
|
|
}
|
|
|
|
public async Task SendMessageAsync(WebSocketMessage msg)
|
|
{
|
|
|
|
while(!hasInit) ;
|
|
mtx.WaitOne();
|
|
int opCode = msg.Binary ? 0x2 : 0x1;
|
|
|
|
|
|
int dataBytes = msg.Data.Length;
|
|
|
|
int lengthlastByte = dataBytes % 4096;
|
|
|
|
|
|
|
|
|
|
int noPackets = (int)Math.Ceiling(dataBytes / 4096.0);
|
|
|
|
for(int i = 0;i<noPackets;i++)
|
|
{
|
|
bool fin = i == noPackets-1;
|
|
int finField = fin ? 0b10000000 : 0;
|
|
int opCode2 = i==0 ? opCode : 0;
|
|
|
|
byte firstByte= (byte)(finField | (opCode2 & 0xF));
|
|
int r=(i==noPackets-1?lengthlastByte : 4096);
|
|
var b=glenBytes(r);
|
|
byte[] message = new byte[1+b.Length + (fin?lengthlastByte : 4096)];
|
|
message[0]=firstByte;
|
|
Array.Copy(b,0,message,1,b.Length);
|
|
Array.Copy(msg.Data,i*4096,message,1+b.Length,r);
|
|
await context.NetworkStream.WriteAsync(message,0,message.Length);
|
|
}
|
|
mtx.ReleaseMutex();
|
|
}
|
|
private async Task PongSend(byte[] msg,long len)
|
|
{
|
|
|
|
mtx.WaitOne();
|
|
int finField = 0b10000000 ;
|
|
|
|
byte firstByte= (byte)(finField | 0xA);
|
|
var b=glenBytes(len);
|
|
byte[] message = new byte[1+b.Length + len];
|
|
message[0]=firstByte;
|
|
Array.Copy(b,0,message,1,b.Length);
|
|
Array.Copy(msg,0,message,1+b.Length,len);
|
|
await context.NetworkStream.WriteAsync(message,0,message.Length);
|
|
mtx.ReleaseMutex();
|
|
}
|
|
private string get_Sec_WebSocketAccept(string headerVal)
|
|
{
|
|
if(string.IsNullOrWhiteSpace(headerVal))
|
|
{
|
|
return "";
|
|
}
|
|
string headerVal2 = $"{headerVal.Trim()}258EAFA5-E914-47DA-95CA-C5AB0DC85B11";
|
|
var res=SHA1.Create().ComputeHash(Encoding.UTF8.GetBytes(headerVal2));
|
|
return Convert.ToBase64String(res);
|
|
}
|
|
private void MaskMessage(byte[] key,byte[] message)
|
|
{
|
|
if(key.Length < 4)
|
|
{
|
|
return;
|
|
|
|
}
|
|
for(int i = 0;i<message.Length;i++)
|
|
{
|
|
message[i] ^= key[i % 4];
|
|
}
|
|
}
|
|
private async Task<long> get_long()
|
|
{
|
|
byte[] data = new byte[8];
|
|
await context.NetworkStream.ReadAsync(data,0,data.Length);
|
|
if(BitConverter.IsLittleEndian)
|
|
{
|
|
Array.Reverse(data);
|
|
}
|
|
return BitConverter.ToInt64(data,0);
|
|
}
|
|
|
|
public async Task Ping(byte[] ping)
|
|
{
|
|
mtx.WaitOne();
|
|
int finField = 0b10000000 ;
|
|
|
|
byte firstByte= (byte)(finField | 0x9);
|
|
var b=glenBytes(ping.Length);
|
|
byte[] message = new byte[1+b.Length + ping.Length];
|
|
message[0]=firstByte;
|
|
Array.Copy(b,0,message,1,b.Length);
|
|
Array.Copy(ping,0,message,1+b.Length,ping.Length);
|
|
await context.NetworkStream.WriteAsync(message,0,message.Length);
|
|
mtx.ReleaseMutex();
|
|
}
|
|
private async Task<short> get_short()
|
|
{
|
|
byte[] data = new byte[2];
|
|
await context.NetworkStream.ReadAsync(data,0,data.Length);
|
|
if(BitConverter.IsLittleEndian)
|
|
{
|
|
Array.Reverse(data);
|
|
}
|
|
return BitConverter.ToInt16(data,0);
|
|
}
|
|
private async Task<(byte[] data,long len)> read_packet_async(byte len)
|
|
{
|
|
|
|
int realLen=len & 127;
|
|
bool masked=(len & 0b10000000) > 0;
|
|
long realLen2 = realLen >= 126 ? realLen > 126 ? await get_long() : await get_short() : realLen;
|
|
byte[] maskingKey = new byte[4];
|
|
if(masked)
|
|
{
|
|
await context.NetworkStream.ReadAsync(maskingKey,0,maskingKey.Length);
|
|
}
|
|
byte[] data = new byte[realLen2];
|
|
await context.NetworkStream.ReadAsync(data,0,data.Length);
|
|
if(masked)
|
|
{
|
|
MaskMessage(maskingKey,data);
|
|
}
|
|
return (data,realLen2);
|
|
}
|
|
public async Task StartAsync()
|
|
{
|
|
/*
|
|
GET /chatUrl HTTP/1.1
|
|
Host: server.example.com
|
|
Upgrade: websocket
|
|
|
|
*/
|
|
string sec_websocket_accept="";
|
|
|
|
|
|
if(context.RequestHeaders.TryGetFirst("Sec-WebSocket-Key",out sec_websocket_accept))
|
|
{
|
|
sec_websocket_accept=get_Sec_WebSocketAccept(sec_websocket_accept);
|
|
}else{
|
|
return;
|
|
}
|
|
if(!context.RequestHeaders.AnyEquals("Upgrade","websocket"))
|
|
{
|
|
//Console.WriteLine("Doesn't contain Upgrade: websocket");
|
|
return;
|
|
}
|
|
|
|
if(!context.RequestHeaders.AnyEquals("Sec-WebSocket-Version", "13"))
|
|
{
|
|
//Console.WriteLine("Doesn't contain version 13");
|
|
return;
|
|
}
|
|
context.StatusCode = 101;
|
|
context.ResponseHeaders.Add("Upgrade","websocket");
|
|
if(context.ResponseHeaders.ContainsKey("Connection"))
|
|
{
|
|
context.ResponseHeaders["Connection"].Clear();
|
|
}
|
|
context.ResponseHeaders.Add("Connection","Upgrade");
|
|
context.ResponseHeaders.Add("Sec-WebSocket-Accept",sec_websocket_accept);
|
|
|
|
await context.WriteHeadersAsync();
|
|
//await context.NetworkStream.FlushAsync();
|
|
await context.NetworkStream.FlushAsync();
|
|
hasInit=true;
|
|
bool isBinary=false;
|
|
MemoryStream strm=new MemoryStream();
|
|
while(context.Connected)
|
|
{
|
|
|
|
byte[] frame_start=new byte[2];
|
|
await context.NetworkStream.ReadAsync(frame_start,0,2);
|
|
byte first= frame_start[0];
|
|
bool hasMessage =false;
|
|
int opcode = first & 0xF;
|
|
bool fin = (first | 0b10000000) > 0;
|
|
switch(opcode)
|
|
{
|
|
case 0x0:
|
|
if(!hasMessage) break;
|
|
var (data,len)= await read_packet_async(frame_start[1]);
|
|
strm.Write(data,0,(int)len);
|
|
break;
|
|
case 0x1:
|
|
case 0x2:
|
|
hasMessage=true;
|
|
strm.Dispose();
|
|
strm=new MemoryStream();
|
|
isBinary = opcode == 0x2;
|
|
|
|
var (data2,len2)= await read_packet_async(frame_start[1]);
|
|
strm.Write(data2,0,(int)len2);
|
|
break;
|
|
case 0x8:
|
|
WebSocketClosed?.Invoke(this,new WebSocketClosedEventArgs(true));
|
|
return;
|
|
case 0x9:
|
|
var (data3,len3) =await read_packet_async(frame_start[1]);
|
|
await PongSend(data3,len3);
|
|
break;
|
|
case 0xA:
|
|
var (data4,len4) =await read_packet_async(frame_start[1]);
|
|
break;
|
|
|
|
|
|
}
|
|
if(fin && hasMessage)
|
|
{
|
|
hasMessage=false;
|
|
WebSocketMessage msg=new WebSocketMessage(strm.ToArray(),isBinary);
|
|
MessageArrived?.Invoke(this,new WebSocketMessageEventArgs(msg));
|
|
}
|
|
}
|
|
WebSocketClosed?.Invoke(this,new WebSocketClosedEventArgs(false));
|
|
}
|
|
}
|
|
}
|