changeset 12:ba8f94212c6e

Added WebSocket support
author Ivo Smits <Ivo@UCIS.nl>
date Sun, 17 Feb 2013 17:11:58 +0100
parents 2e0ff842aa4a
children 92e09198e7e7
files Net/HTTP.cs Net/WebSocketPacketStream.cs UCIS.csproj
diffstat 3 files changed, 324 insertions(+), 0 deletions(-) [+]
line wrap: on
line diff
--- a/Net/HTTP.cs	Mon Feb 04 22:47:01 2013 +0100
+++ b/Net/HTTP.cs	Sun Feb 17 17:11:58 2013 +0100
@@ -281,6 +281,16 @@
 	public interface IHTTPContentProvider {
 		void ServeRequest(HTTPContext context);
 	}
+	public delegate void HTTPContentProviderDelegate(HTTPContext context);
+	public class HTTPContentProviderFunction : IHTTPContentProvider {
+		public HTTPContentProviderDelegate Handler { get; private set; }
+		public HTTPContentProviderFunction(HTTPContentProviderDelegate handler) {
+			this.Handler = handler;
+		}
+		public void ServeRequest(HTTPContext context) {
+			Handler(context);
+		}
+	}
 	public class HTTPPathSelector : IHTTPContentProvider {
 		private List<KeyValuePair<String, IHTTPContentProvider>> Prefixes;
 		private StringComparison PrefixComparison;
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/Net/WebSocketPacketStream.cs	Sun Feb 17 17:11:58 2013 +0100
@@ -0,0 +1,313 @@
+using System;
+using System.IO;
+using System.Security.Cryptography;
+using System.Text;
+using System.Threading;
+using UCIS.Util;
+
+namespace UCIS.Net.HTTP {
+	public class WebSocketPacketStream : PacketStream {
+		Stream baseStream;
+		Boolean negotiationDone = false;
+		Boolean closed = false;
+		ManualResetEvent negotiationEvent = new ManualResetEvent(false);
+		Boolean binaryProtocol = false;
+		int wsProtocol = -1;
+
+		public WebSocketPacketStream(HTTPContext context) {
+			try {
+				String ConnectionHeader = context.GetRequestHeader("Connection"); //can be comma-separated list
+				Boolean ConnectionUpgrade = ConnectionHeader != null && ConnectionHeader.Contains("Upgrade");
+				Boolean UpgradeWebsocket = "WebSocket".Equals(context.GetRequestHeader("Upgrade"), StringComparison.OrdinalIgnoreCase);
+				String SecWebSocketKey = context.GetRequestHeader("Sec-WebSocket-Key");
+				String SecWebSocketKey1 = context.GetRequestHeader("Sec-WebSocket-Key1");
+				String SecWebSocketKey2 = context.GetRequestHeader("Sec-WebSocket-Key2");
+				String SecWebSocketProtocol = context.GetRequestHeader("Sec-WebSocket-Protocol");
+				String[] SecWebSocketProtocols = SecWebSocketProtocol == null ? null : SecWebSocketProtocol.Split(new String[] { ", " }, StringSplitOptions.None);
+				if (!ConnectionUpgrade || !UpgradeWebsocket || SecWebSocketProtocols == null || SecWebSocketProtocols.Length == 0) goto Failure;
+				binaryProtocol = SecWebSocketProtocol != null && Array.IndexOf(SecWebSocketProtocols, "binary") != -1;
+				if (SecWebSocketKey != null) {
+					wsProtocol = 13;
+					String hashedKey;
+					using (SHA1 sha1 = new SHA1Managed()) {
+						Byte[] hashable = Encoding.ASCII.GetBytes(SecWebSocketKey + "258EAFA5-E914-47DA-95CA-C5AB0DC85B11");
+						Byte[] hash = sha1.ComputeHash(hashable);
+						hashedKey = Convert.ToBase64String(hash, Base64FormattingOptions.None);
+					}
+					context.SuppressStandardHeaders = true;
+					context.SendStatus(101);
+					context.SendHeader("Connection", "Upgrade");
+					context.SendHeader("Upgrade", "websocket");
+					context.SendHeader("Sec-WebSocket-Accept", hashedKey);
+					context.SendHeader("Sec-WebSocket-Protocol", binaryProtocol ? "binary" : "base64");
+					baseStream = context.GetResponseStream();
+				} else if (SecWebSocketKey1 != null && SecWebSocketKey2 != null) {
+					wsProtocol = 100;
+					Byte[] key = new Byte[4 + 4 + 8];
+					CalculateHybi00MagicNumber(SecWebSocketKey1, key, 0);
+					CalculateHybi00MagicNumber(SecWebSocketKey2, key, 4);
+					context.SuppressStandardHeaders = true;
+					context.SendStatus(101);
+					context.SendHeader("Connection", "Upgrade");
+					context.SendHeader("Upgrade", "websocket");
+					context.SendHeader("Sec-WebSocket-Protocol", binaryProtocol ? "binary" : "base64");
+					context.SendHeader("Sec-WebSocket-Origin", context.GetRequestHeader("Origin"));
+					context.SendHeader("Sec-WebSocket-Location", "ws://" + context.GetRequestHeader("Host") + context.RequestPath);
+					baseStream = context.GetResponseStream();
+					ReadAllBytes(key, 8, 8);
+					using (MD5 md5 = new MD5CryptoServiceProvider()) key = md5.ComputeHash(key);
+					baseStream.Write(key, 0, key.Length);
+				} else {
+					goto Failure;
+				}
+				if (closed) baseStream.Close();
+			} catch (Exception ex) {
+				closed = true;
+				context.Close();
+			} finally {
+				negotiationDone = true;
+				negotiationEvent.Set();
+			}
+			return;
+Failure:
+			closed = true;
+			context.SendErrorAndClose(400);
+			return;
+		}
+
+		private void CalculateHybi00MagicNumber(String s, Byte[] obuf, int opos) {
+			long number = 0;
+			long spaces = 0;
+			foreach (Char c in s) {
+				if (c == ' ') {
+					spaces++;
+				} else if (c >= '0' && c <= '9') {
+					number = number * 10 + (c - '0');
+				}
+			}
+			number /= spaces;
+			obuf[opos++] = (Byte)(number >> 24);
+			obuf[opos++] = (Byte)(number >> 16);
+			obuf[opos++] = (Byte)(number >> 8);
+			obuf[opos++] = (Byte)(number >> 0);
+		}
+
+		public override bool CanRead { get { return negotiationDone && !closed; } }
+		public override bool CanSeek { get { return false; } }
+		public override bool CanWrite { get { return negotiationDone && !closed; } }
+		public override void Flush() { }
+
+		public override void Close() {
+			closed = true;
+			base.Close();
+			if (baseStream != null) baseStream.Close();
+		}
+		public override bool CanTimeout { get { return baseStream.CanTimeout; } }
+		public override int ReadTimeout {
+			get { return baseStream.ReadTimeout; }
+			set { baseStream.ReadTimeout = value; }
+		}
+		public override int WriteTimeout {
+			get { return baseStream.WriteTimeout; }
+			set { baseStream.WriteTimeout = value; }
+		}
+
+		public override long Length { get { throw new NotSupportedException(); } }
+		public override long Seek(long offset, SeekOrigin origin) { throw new NotSupportedException(); }
+		public override void SetLength(long value) { throw new NotSupportedException(); }
+		public override long Position {
+			get { throw new NotSupportedException(); }
+			set { throw new NotSupportedException(); }
+		}
+
+		private void ReadAllBytes(Byte[] buffer, int offset, int count) {
+			while (count > 0) {
+				int l = baseStream.Read(buffer, offset, count);
+				if (l <= 0) throw new EndOfStreamException();
+				offset += l;
+				count -= l;
+			}
+		}
+
+		Byte[] leftOver = null;
+		public override int Read(byte[] buffer, int offset, int count) {
+			Byte[] packet = leftOver;
+			leftOver = null;
+			if (packet == null) packet = ReadPacket();
+			if (packet == null) return 0;
+			if (count > packet.Length) count = packet.Length;
+			Buffer.BlockCopy(packet, 0, buffer, offset, count);
+			if (packet.Length > count) leftOver = ArrayUtil.Slice(packet, count);
+			return count;
+		}
+		public override byte[] ReadPacket() {
+			if (leftOver != null) throw new InvalidOperationException("There is remaining data from a partial read");
+			negotiationEvent.WaitOne();
+			if (closed) throw new ObjectDisposedException("WebSocketPacketStream");
+			try {
+				if (wsProtocol == 13) {
+					Byte[] multipartbuffer = null;
+					int multipartopcode = -1;
+					while (true) {
+						int flags = baseStream.ReadByte();
+						if (flags == -1) throw new EndOfStreamException();
+						UInt64 pllen = (byte)baseStream.ReadByte();
+						Boolean masked = (pllen & 128) != 0;
+						pllen &= 127;
+						if (pllen == 126) {
+							pllen = (uint)baseStream.ReadByte() << 8;
+							pllen |= (uint)baseStream.ReadByte();
+						} else if (pllen == 127) {
+							pllen = (ulong)baseStream.ReadByte() << 56;
+							pllen |= (ulong)baseStream.ReadByte() << 48;
+							pllen |= (ulong)baseStream.ReadByte() << 40;
+							pllen |= (ulong)baseStream.ReadByte() << 32;
+							pllen |= (uint)baseStream.ReadByte() << 24;
+							pllen |= (uint)baseStream.ReadByte() << 16;
+							pllen |= (uint)baseStream.ReadByte() << 8;
+							pllen |= (uint)baseStream.ReadByte();
+						}
+						Byte[] mask = new Byte[4];
+						if (masked) ReadAllBytes(mask, 0, mask.Length);
+						//Console.WriteLine("Read flags={0} masked={1} mask={2} len={3}", flags, masked, mask, pllen);
+						Byte[] payload = new Byte[pllen]; // + (4 - (pllen % 4))];
+						ReadAllBytes(payload, 0, (int)pllen);
+						if (masked) for (int i = 0; i < (int)pllen; i++) payload[i] ^= mask[i % 4];
+						int opcode = flags & 0x0f;
+						Boolean fin = (flags & 0x80) != 0;
+						if (opcode == 0) {
+							//Console.WriteLine("WebSocket received continuation frame type {0}!", multipartopcode);
+							Array.Resize(ref multipartbuffer, multipartbuffer.Length + payload.Length);
+							payload.CopyTo(multipartbuffer, multipartbuffer.Length - payload.Length);
+							opcode = -1;
+							if (fin) {
+								payload = multipartbuffer;
+								opcode = multipartopcode;
+								multipartbuffer = null;
+							}
+						} else if (!fin) {
+							//Console.WriteLine("WebSocket received non-fin frame type {0}!", opcode);
+							multipartbuffer = payload;
+							multipartopcode = opcode;
+							opcode = -1;
+						}
+						if (opcode == -1) {
+						} else if (opcode == 0) {
+							throw new NotSupportedException("WebSocket opcode 0 is not supported");
+						} else if (opcode == 1) {
+							String text = Encoding.UTF8.GetString(payload); //, 0, pllen);
+							return Convert.FromBase64String(text);
+						} else if (opcode == 2) {
+							return payload; // ArrayUtil.Slice(payload, 0, pllen);
+						} else if (opcode == 8) {
+							return null;
+						} else if (opcode == 9) {
+							//Console.WriteLine("WebSocket PING");
+							WriteFrame(10, payload, 0, (int)pllen);
+						} else if (opcode == 10) { //PONG
+						} else {
+							//Console.WriteLine("WebSocket UNKNOWN OPCODE {0}", opcode);
+						}
+					}
+				} else if (wsProtocol == 100) {
+					int frameType = baseStream.ReadByte();
+					if (frameType == -1) throw new EndOfStreamException();
+					if ((frameType & 0x80) != 0) {
+						int length = 0;
+						while (true) {
+							int b = baseStream.ReadByte();
+							if (b == -1) throw new EndOfStreamException();
+							length = (length << 7) | (b & 0x7f);
+							if ((b & 0x80) == 0) break;
+						}
+						Byte[] buffer = new Byte[length];
+						ReadAllBytes(buffer, 0, length);
+						if (frameType == 0xff && length == 0) {
+							return null;
+						} else {
+							throw new InvalidOperationException();
+						}
+					} else {
+						using (MemoryStream ms = new MemoryStream()) {
+							while (true) {
+								int b = baseStream.ReadByte();
+								if (b == -1) throw new EndOfStreamException();
+								if (b == 0xff) break;
+								ms.WriteByte((Byte)b);
+							}
+							if (frameType == 0x00) {
+								ms.Seek(0, SeekOrigin.Begin);
+								StreamReader reader = new StreamReader(ms, Encoding.UTF8, false);
+								return Convert.FromBase64String(reader.ReadToEnd());
+							} else {
+								throw new InvalidOperationException();
+							}
+						}
+					}
+				} else {
+					throw new InvalidOperationException();
+				}
+			} catch (Exception ex) {
+				Console.WriteLine(ex);
+				throw;
+			}
+		}
+		private delegate Byte[] ReadPacketDelegate();
+		ReadPacketDelegate readPacketDelegate;
+		public override IAsyncResult BeginReadPacket(AsyncCallback callback, object state) {
+			if (readPacketDelegate == null) readPacketDelegate = ReadPacket;
+			return readPacketDelegate.BeginInvoke(callback, state);
+		}
+		public override byte[] EndReadPacket(IAsyncResult asyncResult) {
+			return readPacketDelegate.EndInvoke(asyncResult);
+		}
+		public override void Write(byte[] buffer, int offset, int count) {
+			negotiationEvent.WaitOne();
+			if (closed) throw new ObjectDisposedException("WebSocketPacketStream");
+			if (!binaryProtocol) {
+				String encoded = Convert.ToBase64String(buffer, offset, count, Base64FormattingOptions.None);
+				buffer = Encoding.ASCII.GetBytes(encoded);
+				offset = 0;
+				count = buffer.Length;
+			}
+			if (wsProtocol == 13) {
+				WriteFrame(binaryProtocol ? (Byte)0x2 : (Byte)0x1, buffer, offset, count);
+			} else if (wsProtocol == 100) {
+				Byte[] bytes = new Byte[2 + count];
+				bytes[0] = 0x00;
+				Buffer.BlockCopy(buffer, offset, bytes, 1, count);
+				bytes[1 + count] = 0xff;
+				baseStream.Write(bytes, 0, bytes.Length);
+			} else {
+				throw new InvalidOperationException();
+			}
+		}
+		private void WriteFrame(Byte opcode, Byte[] buffer, int offset, int count) {
+			int pllen = count;
+			int hlen = 2;
+			if (pllen > 0xffff) hlen += 8;
+			else if (pllen > 125) hlen += 2;
+			Byte[] wbuf = new Byte[count + hlen];
+			wbuf[0] = (Byte)(opcode | 0x80);
+			if (pllen > 0xffff) {
+				wbuf[1] = 127;
+				wbuf[2] = 0;
+				wbuf[3] = 0;
+				wbuf[4] = 0;
+				wbuf[5] = 0;
+				wbuf[6] = (Byte)(pllen >> 24);
+				wbuf[7] = (Byte)(pllen >> 16);
+				wbuf[8] = (Byte)(pllen >> 8);
+				wbuf[9] = (Byte)(pllen >> 0);
+			} else if (pllen > 125) {
+				wbuf[1] = 126;
+				wbuf[2] = (Byte)(pllen >> 8);
+				wbuf[3] = (Byte)(pllen >> 0);
+			} else {
+				wbuf[1] = (Byte)pllen;
+			}
+			Buffer.BlockCopy(buffer, offset, wbuf, hlen, count);
+			baseStream.Write(wbuf, 0, wbuf.Length);
+		}
+	}
+}
\ No newline at end of file
--- a/UCIS.csproj	Mon Feb 04 22:47:01 2013 +0100
+++ b/UCIS.csproj	Sun Feb 17 17:11:58 2013 +0100
@@ -48,6 +48,7 @@
     <Compile Include="Net\INetworkConnection.cs" />
     <Compile Include="Net\TCPServer.cs" />
     <Compile Include="Net\TCPStream.cs" />
+    <Compile Include="Net\WebSocketPacketStream.cs" />
     <Compile Include="Pml\Channels\ActivePmlChannel.cs" />
     <Compile Include="Pml\Channels\PassivePmlChannel.cs" />
     <Compile Include="Pml\Channels\PmlChannel.cs" />