changeset 75:50d4aed66c67

Improved HTTP classes
author Ivo Smits <Ivo@UCIS.nl>
date Mon, 03 Feb 2014 22:53:31 +0100
parents 6aca18ee4ec6
children b05350c24596
files Net/HTTP.cs Net/WebSocketPacketStream.cs
diffstat 2 files changed, 440 insertions(+), 43 deletions(-) [+]
line wrap: on
line diff
--- a/Net/HTTP.cs	Sat Nov 02 16:01:09 2013 +0100
+++ b/Net/HTTP.cs	Mon Feb 03 22:53:31 2014 +0100
@@ -70,6 +70,14 @@
 		}
 	}
 
+	public enum HTTPResponseStreamMode {
+		None = -1,
+		Direct = 0,
+		Buffered = 1,
+		Chunked = 2,
+		Hybrid = 3,
+	}
+
 	public class HTTPContext {
 		public HTTPServer Server { get; private set; }
 		public EndPoint LocalEndPoint { get; private set; }
@@ -88,7 +96,9 @@
 		private PrebufferingStream Reader;
 		private List<HTTPHeader> RequestHeaders;
 		private HTTPConnectionState State = HTTPConnectionState.Starting;
-
+		private KeyValuePair<String, String>[] QueryParameters = null, PostParameters = null;
+		private HTTPOutputStream ResponseStream = null;
+		private HTTPInputStream RequestStream = null;
 
 		private enum HTTPConnectionState {
 			Starting = 0,
@@ -96,7 +106,289 @@
 			ProcessingRequest = 2,
 			SendingHeaders = 3,
 			SendingContent = 4,
-			Closed = 5,
+			Completed = 5,
+			Closed = 6,
+		}
+
+		private class HTTPOutputStream : Stream {
+			public HTTPResponseStreamMode Mode { get; private set; }
+			public HTTPContext Context { get; private set; }
+			private Stream OutputStream = null;
+			private MemoryStream Buffer = null;
+			private long BytesWritten = 0;
+			private long MaxLength;
+
+			public HTTPOutputStream(HTTPContext context, HTTPResponseStreamMode mode) : this(context, mode, -1) { }
+			public HTTPOutputStream(HTTPContext context, HTTPResponseStreamMode mode, long length) {
+				this.Context = context;
+				this.Mode = mode;
+				this.MaxLength = length;
+				switch (Mode) {
+					case HTTPResponseStreamMode.Direct:
+						if (MaxLength != -1) Context.SendHeader("Content-Length", MaxLength.ToString());
+						OutputStream = Context.BeginResponseData();
+						break;
+					case HTTPResponseStreamMode.Chunked:
+						Context.SendHeader("Transfer-Encoding", "chunked");
+						OutputStream = Context.BeginResponseData();
+						break;
+					case HTTPResponseStreamMode.Buffered:
+					case HTTPResponseStreamMode.Hybrid:
+						if (Context.State != HTTPConnectionState.ProcessingRequest && Context.State != HTTPConnectionState.SendingHeaders) throw new InvalidOperationException("The response stream can not be created in the current state");
+						break;
+					default: throw new InvalidOperationException("Response stream mode is not supported");
+				}
+			}
+
+			private void WriteBuffered(byte[] buffer, int offset, int count) {
+				if (Buffer == null) Buffer = new MemoryStream();
+				Buffer.Write(buffer, offset, count);
+			}
+			private void WriteChunked(byte[] buffer, int offset, int count) {
+				Byte[] lb = Encoding.ASCII.GetBytes(count.ToString("X") + "\r\n");
+				OutputStream.Write(lb, 0, lb.Length);
+				if (count != 0) OutputStream.Write(buffer, offset, count);
+				OutputStream.Write(new Byte[] { (Byte)'\r', (Byte)'\n' }, 0, 2);
+			}
+			private void HybridSwitchToChunked() {
+				MemoryStream oldbuffer = Buffer;
+				Buffer = null;
+				Context.SendHeader("Transfer-Encoding", "chunked");
+				OutputStream = Context.BeginResponseData();
+				Mode = HTTPResponseStreamMode.Chunked;
+				oldbuffer.WriteTo(this);
+			}
+
+			public override void Write(byte[] buffer, int offset, int count) {
+				if (offset < 0 || count < 0 || offset + count > buffer.Length) throw new ArgumentOutOfRangeException("buffer", "Offset and count arguments exceed the buffer dimensions");
+				switch (Mode) {
+					case HTTPResponseStreamMode.Direct:
+						if (MaxLength != -1 && BytesWritten + count > MaxLength) throw new InvalidOperationException("The write operation exceeds the transfer length");
+						OutputStream.Write(buffer, offset, count);
+						BytesWritten += count;
+						break;
+					case HTTPResponseStreamMode.Buffered:
+						WriteBuffered(buffer, offset, count);
+						break;
+					case HTTPResponseStreamMode.Chunked:
+						if (count != 0) WriteChunked(buffer, offset, count);
+						BytesWritten += count;
+						break;
+					case HTTPResponseStreamMode.Hybrid:
+						if (count > 1024 || (Buffer != null && Buffer.Length + count > 1024)) {
+							HybridSwitchToChunked();
+							if (count != 0) WriteChunked(buffer, offset, count);
+						} else {
+							WriteBuffered(buffer, offset, count);
+						}
+						break;
+				}
+			}
+			class CompletedAsyncResult : AsyncResultBase {
+				public CompletedAsyncResult(AsyncCallback callback, Object state) : base(callback, state) {
+					SetCompleted(true, null);
+				}
+			}
+			public override IAsyncResult BeginWrite(byte[] buffer, int offset, int count, AsyncCallback callback, object state) {
+				if (offset < 0 || count < 0 || offset + count > buffer.Length) throw new ArgumentOutOfRangeException("buffer", "Offset and count arguments exceed the buffer dimensions");
+				switch (Mode) {
+					case HTTPResponseStreamMode.Direct:
+						if (MaxLength != -1 && BytesWritten + count > MaxLength) throw new InvalidOperationException("The write operation exceeds the transfer length");
+						BytesWritten += count;
+						return OutputStream.BeginWrite(buffer, offset, count, callback, state);
+					case HTTPResponseStreamMode.Buffered:
+					case HTTPResponseStreamMode.Chunked:
+					case HTTPResponseStreamMode.Hybrid:
+						Write(buffer, offset, count);
+						return new CompletedAsyncResult(callback, state);
+					default: return null;
+				}
+			}
+			public override void EndWrite(IAsyncResult asyncResult) {
+				switch (Mode) {
+					case HTTPResponseStreamMode.Direct:
+						OutputStream.EndWrite(asyncResult);
+						break;
+				}
+			}
+			public override void SetLength(long value) {
+				switch (Mode) {
+					case HTTPResponseStreamMode.Buffered:
+					case HTTPResponseStreamMode.Hybrid:
+						if (value != 0) throw new InvalidOperationException("The length can only be set to zero using this method");
+						Buffer = null;
+						break;
+					default: throw new InvalidOperationException("The operation is not supported in the current mode");
+				}
+			}
+			public override long Length {
+				get { return MaxLength == -1 ? Position : MaxLength; }
+			}
+			public override long Position {
+				get { return (Buffer == null) ? BytesWritten : BytesWritten + Buffer.Length; }
+				set { throw new NotSupportedException(); }
+			}
+			public override void Flush() {
+				switch (Mode) {
+					case HTTPResponseStreamMode.Hybrid:
+						HybridSwitchToChunked();
+						break;
+				}
+			}
+			public override bool CanWrite {
+				get { return Context.State != HTTPConnectionState.Completed && Context.State != HTTPConnectionState.Closed && (OutputStream == null || OutputStream.CanWrite); }
+			}
+			protected override void Dispose(bool disposing) {
+				base.Dispose(disposing);
+				if (disposing) {
+					switch (Mode) {
+						case HTTPResponseStreamMode.Direct:
+							/*if (MaxLength != -1 && MaxLength > BytesWritten) {
+								long left = MaxLength - BytesWritten;
+								Byte[] dummy = new Byte[Math.Min(left, 1024)];
+								for (; left > 0; left -= dummy.Length) OutputStream.Write(dummy, 0, (int)Math.Min(left, dummy.Length));
+							}*/
+							break;
+						case HTTPResponseStreamMode.Chunked:
+							WriteChunked(null, 0, 0);
+							Mode = HTTPResponseStreamMode.None;
+							break;
+						case HTTPResponseStreamMode.Buffered:
+						case HTTPResponseStreamMode.Hybrid:
+							long length = (Buffer == null) ? 0 : Buffer.Length;
+							Context.SendHeader("Content-Length", length.ToString());
+							OutputStream = Context.BeginResponseData();
+							if (Buffer != null) Buffer.WriteTo(OutputStream);
+							Buffer = null;
+							Mode = HTTPResponseStreamMode.None;
+							break;
+					}
+					Context.EndResponseData();
+					if (MaxLength != -1 && MaxLength > BytesWritten) Context.Close();
+				}
+			}
+			public override bool CanTimeout {
+				get { return OutputStream == null ? true : OutputStream.CanTimeout; }
+			}
+			public override int WriteTimeout {
+				get { return OutputStream == null ? 0 : OutputStream.WriteTimeout; }
+				set { if (OutputStream != null) OutputStream.WriteTimeout = value; }
+			}
+
+			public override bool CanRead { get { return false; } }
+			public override bool CanSeek { get { return false; } }
+
+			public override int Read(byte[] buffer, int offset, int count) { throw new NotSupportedException(); }
+			public override long Seek(long offset, SeekOrigin origin) { throw new NotSupportedException(); }
+		}
+		private class HTTPInputStream : Stream {
+			public HTTPResponseStreamMode Mode { get; private set; }
+			private Stream InputStream = null;
+			private long BytesRead = 0;
+			private long BytesLeft = 0;
+			public HTTPInputStream(HTTPContext context) {
+				String TransferEncoding = context.GetRequestHeader("Transfer-Encoding");
+				String ContentLength = context.GetRequestHeader("Content-Length");
+				InputStream = context.Reader;
+				if (TransferEncoding != null && TransferEncoding.StartsWith("chunked", StringComparison.InvariantCultureIgnoreCase)) {
+					Mode = HTTPResponseStreamMode.Chunked;
+				} else if (ContentLength != null) {
+					Mode = HTTPResponseStreamMode.Direct;
+					if (!long.TryParse(ContentLength, out BytesLeft)) BytesLeft = 0;
+				} else {
+					Mode = HTTPResponseStreamMode.None;
+				}
+			}
+			private int ReadDirect(Byte[] buffer, int offset, int count) {
+				if (count > BytesLeft) count = (int)BytesLeft;
+				if (count == 0) return 0;
+				int read = InputStream.Read(buffer, offset, count);
+				if (read >= 0) {
+					BytesRead += read;
+					BytesLeft -= read;
+				}
+				return read;
+			}
+			public override int Read(byte[] buffer, int offset, int count) {
+				if (offset < 0 || count < 0 || offset + count > buffer.Length) throw new ArgumentOutOfRangeException("buffer", "Offset and count arguments exceed the buffer dimensions");
+				switch (Mode) {
+					case HTTPResponseStreamMode.None: 
+						return 0;
+					case HTTPResponseStreamMode.Direct:
+						return ReadDirect(buffer, offset, count);
+					case HTTPResponseStreamMode.Chunked:
+						if (BytesLeft == 0) {
+							String length = ReadLine(InputStream);
+							if (!long.TryParse(length, out BytesLeft)) BytesLeft = 0;
+							if (BytesLeft == 0) {
+								while (true) {
+									String line = ReadLine(InputStream);
+									if (line == null || line.Length == 0) break;
+								}
+								Mode = HTTPResponseStreamMode.None;
+								return 0;
+							}
+						}
+						return ReadDirect(buffer, offset, count);
+					default:
+						return 0;
+				}
+			}
+			class CompletedAsyncResult : AsyncResultBase {
+				public int Count { get; private set; }
+				public CompletedAsyncResult(AsyncCallback callback, Object state, int count)
+					: base(callback, state) {
+					this.Count = count;
+					SetCompleted(true, null);
+				}
+			}
+			public override IAsyncResult BeginRead(byte[] buffer, int offset, int count, AsyncCallback callback, object state) {
+				if (BytesLeft > 0) {
+					if (count > BytesLeft) count = (int)BytesLeft;
+					if (count == 0) return new CompletedAsyncResult(callback, state, 0);
+					return InputStream.BeginRead(buffer, offset, count, callback, state);
+				} else {
+					int ret = Read(buffer, offset, count);
+					return new CompletedAsyncResult(callback, state, ret);
+				}
+			}
+			public override int EndRead(IAsyncResult asyncResult) {
+				CompletedAsyncResult car = asyncResult as CompletedAsyncResult;
+				if (car != null) {
+					return car.Count;
+				} else {
+					return InputStream.EndRead(asyncResult);
+				}
+			}
+			public override long Length {
+				get { return BytesRead + BytesLeft; }
+			}
+			public override long Position {
+				get { return BytesRead; }
+				set { throw new NotSupportedException(); }
+			}
+			public override bool CanRead {
+				get { return (BytesLeft > 0 || Mode == HTTPResponseStreamMode.Chunked) && InputStream.CanRead; }
+			}
+			public override bool CanTimeout {
+				get { return InputStream.CanTimeout; }
+			}
+			public override int ReadTimeout {
+				get { return InputStream.ReadTimeout; }
+				set { InputStream.ReadTimeout = value; }
+			}
+			protected override void Dispose(bool disposing) {
+				base.Dispose(disposing);
+				Byte[] dummy = new Byte[1024];
+				while (Read(dummy, 0, dummy.Length) != 0) ;
+			}
+
+			public override bool CanSeek { get { return false; } }
+			public override bool CanWrite { get { return false; } }
+			public override void Flush() { }
+			public override long Seek(long offset, SeekOrigin origin) { throw new NotSupportedException(); }
+			public override void SetLength(long value) { throw new NotSupportedException(); }
+			public override void Write(byte[] buffer, int offset, int count) { throw new NotSupportedException(); }
 		}
 
 		public HTTPContext(HTTPServer server, TCPStream stream) : this(server, stream, stream.Socket) { }
@@ -121,10 +413,10 @@
 			Reader.BeginPrebuffering(PrebufferCallback, null);
 		}
 
-		private String ReadLine() {
+		private static String ReadLine(Stream stream) {
 			StringBuilder s = new StringBuilder();
 			while (true) {
-				int b = Reader.ReadByte();
+				int b = stream.ReadByte();
 				if (b == -1) {
 					if (s.Length == 0) return null;
 					break;
@@ -137,6 +429,9 @@
 			}
 			return s.ToString();
 		}
+		private String ReadLine() {
+			return ReadLine(Reader);
+		}
 
 		private void PrebufferCallback(IAsyncResult ar) {
 			State = HTTPConnectionState.ReceivingRequest;
@@ -160,7 +455,7 @@
 				switch (request[2]) {
 					case "HTTP/1.0": HTTPVersion = 10; break;
 					case "HTTP/1.1": HTTPVersion = 11; break;
-					default: goto SendError400AndClose;
+					default: goto SendError505AndClose;
 				}
 				request = RequestAddress.Split(new Char[] { '?' });
 				RequestPath = Uri.UnescapeDataString(request[0]);
@@ -178,6 +473,7 @@
 				if (content == null) goto SendError500AndClose;
 				State = HTTPConnectionState.ProcessingRequest;
 				content.ServeRequest(this);
+				Close();
 			} catch (Exception ex) {
 				Console.Error.WriteLine(ex);
 				switch (State) {
@@ -197,6 +493,10 @@
 			State = HTTPConnectionState.ProcessingRequest;
 			SendErrorAndClose(500);
 			return;
+		SendError505AndClose:
+			State = HTTPConnectionState.ProcessingRequest;
+			SendErrorAndClose(400);
+			return;
 		}
 
 		public String GetRequestHeader(String name) {
@@ -215,26 +515,82 @@
 			return items;
 		}
 
-		public void SendErrorAndClose(int state) {
-			try {
-				SendStatus(state);
-				GetResponseStream();
-			} catch (Exception ex) {
-				Console.Error.WriteLine(ex);
+		private static String UnescapeUrlDataString(String text) {
+			return Uri.UnescapeDataString(text.Replace('+', ' '));
+		}
+		private static KeyValuePair<String, String>[] DecodeUrlEncodedFields(String data) {
+			List<KeyValuePair<string, string>> list = new List<KeyValuePair<string, string>>();
+			foreach (String arg in data.Split('&')) {
+				String[] parts = arg.Split(new Char[] { '=' }, 2);
+				String key = UnescapeUrlDataString(parts[0]);
+				String value = (parts.Length > 1) ? UnescapeUrlDataString(parts[1]) : String.Empty;
+				list.Add(new KeyValuePair<string, string>(key, value));
 			}
-			Close();
+			return list.ToArray();
+		}
+
+		public String GetQueryParameter(String name) {
+			foreach (KeyValuePair<String, String> kvp in GetQueryParameters()) if (kvp.Key == name) return kvp.Value;
+			return null;
+		}
+		public String[] GetQueryParameters(String name) {
+			List<String> list = new List<string>();
+			foreach (KeyValuePair<String, String> kvp in GetQueryParameters()) if (kvp.Key == name) list.Add(kvp.Value);
+			return list.ToArray();
+		}
+		public KeyValuePair<String, String>[] GetQueryParameters() {
+			if (QueryParameters == null) QueryParameters = DecodeUrlEncodedFields(RequestQuery);
+			return QueryParameters;
+		}
+
+		public String GetPostParameter(String name) {
+			foreach (KeyValuePair<String, String> kvp in GetPostParameters()) if (kvp.Key == name) return kvp.Value;
+			return null;
+		}
+		public String[] GetPostParameters(String name) {
+			List<String> list = new List<string>();
+			foreach (KeyValuePair<String, String> kvp in GetPostParameters()) if (kvp.Key == name) list.Add(kvp.Value);
+			return list.ToArray();
+		}
+		public KeyValuePair<String, String>[] GetPostParameters() {
+			if (PostParameters == null) {
+				if (RequestMethod == "POST" && GetRequestHeader("Content-Type") == "application/x-www-form-urlencoded") {
+					String data;
+					using (StreamReader reader = new StreamReader(OpenRequestStream(), Encoding.UTF8)) data = reader.ReadToEnd();
+					PostParameters = DecodeUrlEncodedFields(data);
+				} else {
+					PostParameters = new KeyValuePair<string, string>[0];
+				}
+			}
+			return PostParameters;
+		}
+
+		public Stream OpenRequestStream() {
+			if (RequestStream == null) RequestStream = new HTTPInputStream(this);
+			return RequestStream;
+		}
+
+		private static String GetMessageForStatus(int code) {
+			switch (code) {
+				case 101: return "Switching Protocols";
+				case 200: return "OK";
+				case 301: return "Moved Permanently";
+				case 302: return "Found";
+				case 303: return "See Other";
+				case 304: return "Not Modified";
+				case 307: return "Temporary Redirect";
+				case 400: return "Bad Request";
+				case 401: return "Access denied";
+				case 403: return "Forbidden";
+				case 404: return "Not Found";
+				case 500: return "Internal Server Error";
+				case 505: return "HTTP Version Not Supported";
+				default: return "Unknown Status";
+			}
 		}
 
 		public void SendStatus(int code) {
-			String message;
-			switch (code) {
-				case 101: message = "Switching Protocols"; break;
-				case 200: message = "OK"; break;
-				case 400: message = "Bad Request"; break;
-				case 404: message = "Not Found"; break;
-				case 500: message = "Internal Server Error"; break;
-				default: message = "Unknown Status"; break;
-			}
+			String message = GetMessageForStatus(code);
 			SendStatus(code, message);
 		}
 		public void SendStatus(int code, String message) {
@@ -244,7 +600,7 @@
 			switch (HTTPVersion) {
 				case 10: sb.Append("1.0"); break;
 				case 11: sb.Append("1.1"); break;
-				default: throw new ArgumentException("The HTTP version is not supported", "HTTPVersion");
+				default: sb.Append("1.0"); break;
 			}
 			sb.Append(" ");
 			sb.Append(code);
@@ -257,7 +613,7 @@
 				SendHeader("Cache-Control", "no-store, no-cache, must-revalidate");
 				SendHeader("Cache-Control", "post-check=0, pre-check=0");
 				SendHeader("Pragma", "no-cache");
-				SendHeader("Server", "UCIS Webserver");
+				SendHeader("Server", "UCIS Embedded Webserver");
 				SendHeader("Connection", "Close");
 			}
 		}
@@ -266,17 +622,63 @@
 			if (State != HTTPConnectionState.SendingHeaders) throw new InvalidOperationException();
 			Writer.WriteLine(name + ": " + value);
 		}
-		public Stream GetResponseStream() {
+		public void SendErrorResponse(int state) {
+			String message = GetMessageForStatus(state);
+			try {
+				SendStatus(state, message);
+				SendHeader("Content-Type", "text/plain");
+				WriteResponseData(Encoding.ASCII.GetBytes(String.Format("Error {0}: {1}", state, message)));
+			} catch (Exception ex) {
+				Console.Error.WriteLine(ex);
+			}
+		}
+		public Stream OpenResponseStream(HTTPResponseStreamMode mode) {
+			if (ResponseStream != null) throw new InvalidOperationException("The response stream has already been opened");
+			return ResponseStream = new HTTPOutputStream(this, mode);
+		}
+		public Stream OpenResponseStream(long length) {
+			if (ResponseStream != null) throw new InvalidOperationException("The response stream has already been opened");
+			if (length < 0) throw new ArgumentException("Response length can not be negative", "length");
+			return ResponseStream = new HTTPOutputStream(this, HTTPResponseStreamMode.Direct, length);
+		}
+		public void WriteResponseData(Byte[] buffer) {
+			WriteResponseData(buffer, 0, buffer.Length);
+		}
+		public void WriteResponseData(Byte[] buffer, int offset, int count) {
+			if (offset < 0 || count < 0 || offset + count > buffer.Length) throw new ArgumentOutOfRangeException("buffer", "Offset and count arguments exceed the buffer dimensions");
+			SendHeader("Content-Length", count.ToString());
+			Stream stream = BeginResponseData();
+			stream.Write(buffer, offset, count);
+			EndResponseData();
+		}
+		private Stream BeginResponseData() {
 			if (State == HTTPConnectionState.ProcessingRequest) SendStatus(200);
 			if (State == HTTPConnectionState.SendingHeaders) {
 				Writer.WriteLine();
 				State = HTTPConnectionState.SendingContent;
 			}
-			if (State != HTTPConnectionState.SendingContent) throw new InvalidOperationException();
+			if (State != HTTPConnectionState.SendingContent) throw new InvalidOperationException("The response stream can not be opened in the current state");
+			return Reader;
+		}
+		private void EndResponseData() {
+			if (State == HTTPConnectionState.Completed || State == HTTPConnectionState.Closed) return;
+			OpenRequestStream().Close();
+			if (State != HTTPConnectionState.SendingContent) WriteResponseData(new Byte[0]);
+			State = HTTPConnectionState.Completed;
+		}
+
+		public Stream GetDirectStream() {
+			if (State == HTTPConnectionState.Closed) throw new InvalidOperationException("The context has been closed");
+			BeginResponseData();
+			State = HTTPConnectionState.Closed;
 			return Reader;
 		}
 
-		public void Close() {
+		private void SendErrorAndClose(int code) {
+			SendErrorResponse(code);
+			Close();
+		}
+		private void Close() {
 			if (State == HTTPConnectionState.Closed) return;
 			Reader.Close();
 			State = HTTPConnectionState.Closed;
@@ -315,7 +717,7 @@
 			if (c.Value != null) {
 				c.Value.ServeRequest(context);
 			} else {
-				context.SendErrorAndClose(404);
+				context.SendErrorResponse(404);
 			}
 		}
 	}
@@ -338,16 +740,13 @@
 		public void ServeRequest(HTTPContext context) {
 			ArraySegment<Byte> content = ContentBuffer;
 			if (content.Array == null) {
-				context.SendErrorAndClose(404);
+				context.SendErrorResponse(404);
 				return;
 			}
 			String contentType = ContentType;
 			context.SendStatus(200);
 			if (contentType != null) context.SendHeader("Content-Type", contentType);
-			context.SendHeader("Content-Length", content.Count.ToString());
-			Stream response = context.GetResponseStream();
-			response.Write(content.Array, content.Offset, content.Count);
-			response.Close();
+			context.WriteResponseData(content.Array, content.Offset, content.Count);
 		}
 	}
 	public class HTTPFileProvider : IHTTPContentProvider {
@@ -363,9 +762,8 @@
 				using (FileStream fs = File.OpenRead(FileName)) {
 					context.SendStatus(200);
 					context.SendHeader("Content-Type", ContentType);
-					context.SendHeader("Content-Length", fs.Length.ToString());
 					long left = fs.Length;
-					Stream response = context.GetResponseStream();
+					Stream response = context.OpenResponseStream(fs.Length);
 					byte[] buffer = new byte[1024 * 10];
 					while (fs.CanRead) {
 						int len = fs.Read(buffer, 0, buffer.Length);
@@ -376,7 +774,7 @@
 					response.Close();
 				}
 			} else {
-				context.SendErrorAndClose(404);
+				context.SendErrorResponse(404);
 			}
 		}
 	}
@@ -387,7 +785,7 @@
 		}
 		public void ServeRequest(HTTPContext context) {
 			if (!File.Exists(TarFileName)) {
-				context.SendErrorAndClose(404);
+				context.SendErrorResponse(404);
 				return;
 			}
 			String reqname1 = context.RequestPath;
@@ -399,7 +797,6 @@
 				if (!file.IsFile) continue;
 				if (!reqname1.Equals(file.Name, StringComparison.OrdinalIgnoreCase) && !reqname2.Equals(file.Name, StringComparison.OrdinalIgnoreCase)) continue;
 				context.SendStatus(200);
-				context.SendHeader("Content-Length", file.Size.ToString());
 				String ctype = null;
 				switch (Path.GetExtension(file.Name).ToLowerInvariant()) {
 					case ".txt": ctype = "text/plain"; break;
@@ -414,7 +811,7 @@
 					case ".ico": ctype = "image/x-icon"; break;
 				}
 				if (ctype != null) context.SendHeader("Content-Type", ctype);
-				using (Stream response = context.GetResponseStream(), source = file.GetStream()) {
+				using (Stream response = context.OpenResponseStream(file.Size), source = file.GetStream()) {
 					byte[] buffer = new byte[Math.Min(source.Length, 1024 * 10)];
 					while (source.CanRead) {
 						int len = source.Read(buffer, 0, buffer.Length);
@@ -424,7 +821,7 @@
 				}
 				return;
 			}
-			context.SendErrorAndClose(404);
+			context.SendErrorResponse(404);
 		}
 	}
 }
--- a/Net/WebSocketPacketStream.cs	Sat Nov 02 16:01:09 2013 +0100
+++ b/Net/WebSocketPacketStream.cs	Mon Feb 03 22:53:31 2014 +0100
@@ -40,7 +40,7 @@
 					context.SendHeader("Upgrade", "websocket");
 					context.SendHeader("Sec-WebSocket-Accept", hashedKey);
 					context.SendHeader("Sec-WebSocket-Protocol", binaryProtocol ? "binary" : "base64");
-					baseStream = context.GetResponseStream();
+					baseStream = context.GetDirectStream();
 				} else if (SecWebSocketKey1 != null && SecWebSocketKey2 != null) {
 					wsProtocol = 100;
 					Byte[] key = new Byte[4 + 4 + 8];
@@ -53,7 +53,7 @@
 					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();
+					baseStream = context.GetDirectStream();
 					ReadAllBytes(key, 8, 8);
 					using (MD5 md5 = new MD5CryptoServiceProvider()) key = md5.ComputeHash(key);
 					baseStream.Write(key, 0, key.Length);
@@ -63,7 +63,7 @@
 				if (closed) baseStream.Close();
 			} catch (Exception) {
 				closed = true;
-				context.Close();
+				if (baseStream != null) baseStream.Close();
 			} finally {
 				negotiationDone = true;
 				negotiationEvent.Set();
@@ -71,7 +71,7 @@
 			return;
 Failure:
 			closed = true;
-			context.SendErrorAndClose(400);
+			context.SendErrorResponse(400);
 			return;
 		}