view Net/HTTP.cs @ 54:ba4e2cb031e0

Added general purpose tar archive reader class
author Ivo Smits <Ivo@UCIS.nl>
date Wed, 02 Oct 2013 21:17:30 +0200
parents 8df7f4dc5615
children 50d4aed66c67
line wrap: on
line source

???using System;
using System.Collections.Generic;
using System.IO;
using System.Net;
using System.Net.Security;
using System.Net.Sockets;
using System.Security.Cryptography.X509Certificates;
using System.Text;
using UCIS.Net;
using UCIS.Util;
using HTTPHeader = System.Collections.Generic.KeyValuePair<string, string>;

namespace UCIS.Net.HTTP {
	public class HTTPServer : TCPServer.IModule, IDisposable {
		public IHTTPContentProvider ContentProvider { get; set; }
		public Boolean ServeFlashPolicyFile { get; set; }
		public X509Certificate SSLCertificate { get; set; }
		private Socket Listener = null;

		public HTTPServer() { }

		public void Listen(int port) {
			Listen(new IPEndPoint(IPAddress.Any, port));
		}

		public void Listen(EndPoint localep) {
			if (Listener != null) throw new InvalidOperationException("A listener exists");
			Listener = new Socket(localep.AddressFamily, SocketType.Stream, ProtocolType.Tcp);
			Listener.Bind(localep);
			Listener.Listen(5);
			Listener.BeginAccept(AcceptCallback, null);
		}

		private void AcceptCallback(IAsyncResult ar) {
			try {
				Socket socket = Listener.EndAccept(ar);
				if (SSLCertificate != null) {
					SslStream ssl = new SslStream(new NetworkStream(socket, true));
					ssl.BeginAuthenticateAsServer(SSLCertificate, SslAuthenticationCallback, new Object[] { socket, ssl });
				} else {
					new HTTPContext(this, socket);
				}
			} catch (Exception) { }
			try {
				Listener.BeginAccept(AcceptCallback, null);
			} catch (Exception) { }
		}

		private void SslAuthenticationCallback(IAsyncResult ar) {
			Object[] args = (Object[])ar.AsyncState;
			Socket socket = (Socket)args[0];
			SslStream ssl = (SslStream)args[1];
			try {
				ssl.EndAuthenticateAsServer(ar);
				new HTTPContext(this, ssl, socket);
			} catch (Exception) { }
		}

		public void Dispose() {
			if (Listener != null) Listener.Close();
		}

		public void HandleClient(Socket client) {
			new HTTPContext(this, client);
		}

		bool TCPServer.IModule.Accept(TCPStream stream) {
			new HTTPContext(this, stream);
			return false;
		}
	}

	public class HTTPContext {
		public HTTPServer Server { get; private set; }
		public EndPoint LocalEndPoint { get; private set; }
		public EndPoint RemoteEndPoint { get; private set; }

		public String RequestMethod { get; private set; }
		public String RequestPath { get; private set; }
		public String RequestQuery { get; private set; }
		public int HTTPVersion { get; set; }

		public Socket Socket { get; private set; }
		public Boolean SuppressStandardHeaders { get; set; }
		public TCPStream TCPStream { get; private set; }

		private StreamWriter Writer;
		private PrebufferingStream Reader;
		private List<HTTPHeader> RequestHeaders;
		private HTTPConnectionState State = HTTPConnectionState.Starting;


		private enum HTTPConnectionState {
			Starting = 0,
			ReceivingRequest = 1,
			ProcessingRequest = 2,
			SendingHeaders = 3,
			SendingContent = 4,
			Closed = 5,
		}

		public HTTPContext(HTTPServer server, TCPStream stream) : this(server, stream, stream.Socket) { }
		public HTTPContext(HTTPServer server, Socket socket) : this(server, null, socket) { }
		public HTTPContext(HTTPServer server, Stream stream, Socket socket) {
			this.Server = server;
			this.Socket = socket;
			if (socket != null) {
				this.LocalEndPoint = socket.LocalEndPoint;
				this.RemoteEndPoint = socket.RemoteEndPoint;
				if (socket.ProtocolType == ProtocolType.Tcp) socket.SetSocketOption(SocketOptionLevel.Socket, SocketOptionName.KeepAlive, true);
				if (stream == null) stream = new NetworkStream(socket, true);
			}
			Init(stream);
		}

		private void Init(Stream Stream) {
			Writer = new StreamWriter(Stream, Encoding.ASCII);
			Writer.NewLine = "\r\n";
			Writer.AutoFlush = true;
			Reader = new PrebufferingStream(Stream);
			Reader.BeginPrebuffering(PrebufferCallback, null);
		}

		private String ReadLine() {
			StringBuilder s = new StringBuilder();
			while (true) {
				int b = Reader.ReadByte();
				if (b == -1) {
					if (s.Length == 0) return null;
					break;
				} else if (b == 13) {
				} else if (b == 10 || b == 0) {
					break;
				} else {
					s.Append((Char)b);
				}
			}
			return s.ToString();
		}

		private void PrebufferCallback(IAsyncResult ar) {
			State = HTTPConnectionState.ReceivingRequest;
			try {
				Reader.EndPrebuffering(ar);
				String line = ReadLine();
				if (line == null) {
					Close();
					return;
				}
				if (Server.ServeFlashPolicyFile && line[0] == '<') { //<policy-file-request/>
					Writer.WriteLine("<cross-domain-policy><allow-access-from domain=\"*\" to-ports=\"*\" /></cross-domain-policy>");
					Reader.WriteByte(0);
					Close();
					return;
				}
				String[] request = line.Split(' ');
				if (request.Length != 3) goto SendError400AndClose;
				RequestMethod = request[0];
				String RequestAddress = request[1];
				switch (request[2]) {
					case "HTTP/1.0": HTTPVersion = 10; break;
					case "HTTP/1.1": HTTPVersion = 11; break;
					default: goto SendError400AndClose;
				}
				request = RequestAddress.Split(new Char[] { '?' });
				RequestPath = Uri.UnescapeDataString(request[0]);
				RequestQuery = request.Length > 1 ? request[1] : null;
				RequestHeaders = new List<HTTPHeader>();
				while (true) {
					line = ReadLine();
					if (line == null) goto SendError400AndClose;
					if (line.Length == 0) break;
					request = line.Split(new String[] { ": " }, 2, StringSplitOptions.None);
					if (request.Length != 2) goto SendError400AndClose;
					RequestHeaders.Add(new HTTPHeader(request[0], request[1]));
				}
				IHTTPContentProvider content = Server.ContentProvider;
				if (content == null) goto SendError500AndClose;
				State = HTTPConnectionState.ProcessingRequest;
				content.ServeRequest(this);
			} catch (Exception ex) {
				Console.Error.WriteLine(ex);
				switch (State) {
					case HTTPConnectionState.ProcessingRequest: goto SendError500AndClose;
					default:
						Close();
						break;
				}
			}
			return;

		SendError400AndClose:
			State = HTTPConnectionState.ProcessingRequest;
			SendErrorAndClose(400);
			return;
		SendError500AndClose:
			State = HTTPConnectionState.ProcessingRequest;
			SendErrorAndClose(500);
			return;
		}

		public String GetRequestHeader(String name) {
			if (State != HTTPConnectionState.ProcessingRequest && State != HTTPConnectionState.SendingHeaders && State != HTTPConnectionState.SendingContent) throw new InvalidOperationException();
			foreach (HTTPHeader h in RequestHeaders) {
				if (name.Equals(h.Key, StringComparison.OrdinalIgnoreCase)) return h.Value;
			}
			return null;
		}
		public String[] GetRequestHeaders(String name) {
			if (State != HTTPConnectionState.ProcessingRequest && State != HTTPConnectionState.SendingHeaders && State != HTTPConnectionState.SendingContent) throw new InvalidOperationException();
			String[] items = new String[0];
			foreach (HTTPHeader h in RequestHeaders) {
				if (name.Equals(h.Key, StringComparison.OrdinalIgnoreCase)) ArrayUtil.Add(ref items, h.Value);
			}
			return items;
		}

		public void SendErrorAndClose(int state) {
			try {
				SendStatus(state);
				GetResponseStream();
			} catch (Exception ex) {
				Console.Error.WriteLine(ex);
			}
			Close();
		}

		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;
			}
			SendStatus(code, message);
		}
		public void SendStatus(int code, String message) {
			if (State != HTTPConnectionState.ProcessingRequest) throw new InvalidOperationException();
			StringBuilder sb = new StringBuilder();
			sb.Append("HTTP/");
			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");
			}
			sb.Append(" ");
			sb.Append(code);
			sb.Append(" ");
			sb.Append(message);
			Writer.WriteLine(sb.ToString());
			State = HTTPConnectionState.SendingHeaders;
			if (!SuppressStandardHeaders) {
				SendHeader("Expires", "Expires: Sun, 1 Jan 2000 00:00:00 GMT");
				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("Connection", "Close");
			}
		}
		public void SendHeader(String name, String value) {
			if (State == HTTPConnectionState.ProcessingRequest) SendStatus(200);
			if (State != HTTPConnectionState.SendingHeaders) throw new InvalidOperationException();
			Writer.WriteLine(name + ": " + value);
		}
		public Stream GetResponseStream() {
			if (State == HTTPConnectionState.ProcessingRequest) SendStatus(200);
			if (State == HTTPConnectionState.SendingHeaders) {
				Writer.WriteLine();
				State = HTTPConnectionState.SendingContent;
			}
			if (State != HTTPConnectionState.SendingContent) throw new InvalidOperationException();
			return Reader;
		}

		public void Close() {
			if (State == HTTPConnectionState.Closed) return;
			Reader.Close();
			State = HTTPConnectionState.Closed;
		}
	}

	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;
		public HTTPPathSelector() : this(false) { }
		public HTTPPathSelector(Boolean caseSensitive) {
			Prefixes = new List<KeyValuePair<string, IHTTPContentProvider>>();
			PrefixComparison = caseSensitive ? StringComparison.Ordinal : StringComparison.OrdinalIgnoreCase;
		}
		public void AddPrefix(String prefix, IHTTPContentProvider contentProvider) {
			Prefixes.Add(new KeyValuePair<string, IHTTPContentProvider>(prefix, contentProvider));
		}
		public void DeletePrefix(String prefix) {
			Prefixes.RemoveAll(delegate(KeyValuePair<string, IHTTPContentProvider> item) { return prefix.Equals(item.Key, PrefixComparison); });
		}
		public void ServeRequest(HTTPContext context) {
			KeyValuePair<string, IHTTPContentProvider> c = Prefixes.Find(delegate(KeyValuePair<string, IHTTPContentProvider> item) { return context.RequestPath.StartsWith(item.Key, PrefixComparison); });
			if (c.Value != null) {
				c.Value.ServeRequest(context);
			} else {
				context.SendErrorAndClose(404);
			}
		}
	}
	public class HTTPStaticContent : IHTTPContentProvider {
		public ArraySegment<Byte> ContentBuffer { get; set; }
		public String ContentType { get; set; }
		public HTTPStaticContent() : this(new ArraySegment<Byte>()) { }
		public HTTPStaticContent(ArraySegment<Byte> content) : this(content, "application/octet-stream") { }
		public HTTPStaticContent(String content, String contentType) : this(Encoding.UTF8.GetBytes(content), contentType) { }
		public HTTPStaticContent(String contentType) : this(new ArraySegment<Byte>(), contentType) { }
		public HTTPStaticContent(Byte[] content, String contentType) : this(new ArraySegment<Byte>(content), contentType) { }
		public HTTPStaticContent(ArraySegment<Byte> content, String contentType) {
			this.ContentBuffer = content;
			this.ContentType = contentType;
		}
		public void SetContent(Byte[] bytes) { ContentBuffer = new ArraySegment<byte>(bytes); }
		public void SetContent(Byte[] bytes, int offset, int count) { ContentBuffer = new ArraySegment<byte>(bytes, offset, count); }
		public void SetContent(String content, Encoding encoding) { SetContent(encoding.GetBytes(content)); }
		public void SetContent(String content) { SetContent(content, Encoding.UTF8); }
		public void ServeRequest(HTTPContext context) {
			ArraySegment<Byte> content = ContentBuffer;
			if (content.Array == null) {
				context.SendErrorAndClose(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();
		}
	}
	public class HTTPFileProvider : IHTTPContentProvider {
		public String FileName { get; private set; }
		public String ContentType { get; private set; }
		public HTTPFileProvider(String fileName) : this(fileName, "application/octet-stream") { }
		public HTTPFileProvider(String fileName, String contentType) {
			this.FileName = fileName;
			this.ContentType = contentType;
		}
		public void ServeRequest(HTTPContext context) {
			if (File.Exists(FileName)) {
				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();
					byte[] buffer = new byte[1024 * 10];
					while (fs.CanRead) {
						int len = fs.Read(buffer, 0, buffer.Length);
						if (len <= 0) break;
						left -= len;
						response.Write(buffer, 0, len);
					}
					response.Close();
				}
			} else {
				context.SendErrorAndClose(404);
			}
		}
	}
	public class HTTPUnTarchiveProvider : IHTTPContentProvider {
		public String TarFileName { get; private set; }
		public HTTPUnTarchiveProvider(String tarFile) {
			this.TarFileName = tarFile;
		}
		public void ServeRequest(HTTPContext context) {
			if (!File.Exists(TarFileName)) {
				context.SendErrorAndClose(404);
				return;
			}
			String reqname1 = context.RequestPath;
			if (reqname1.StartsWith("/")) reqname1 = reqname1.Substring(1);
			String reqname2 = reqname1;
			if (reqname2.Length > 0 && !reqname2.EndsWith("/")) reqname2 += "/";
			reqname2 += "index.htm";
			foreach (TarchiveEntry file in new TarchiveReader(TarFileName)) {
				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;
					case ".htm":
					case ".html": ctype = "text/html"; break;
					case ".css": ctype = "text/css"; break;
					case ".js": ctype = "application/x-javascript"; break;
					case ".png": ctype = "image/png"; break;
					case ".jpg":
					case ".jpeg": ctype = "image/jpeg"; break;
					case ".gif": ctype = "image/gif"; break;
					case ".ico": ctype = "image/x-icon"; break;
				}
				if (ctype != null) context.SendHeader("Content-Type", ctype);
				using (Stream response = context.GetResponseStream(), source = file.GetStream()) {
					byte[] buffer = new byte[Math.Min(source.Length, 1024 * 10)];
					while (source.CanRead) {
						int len = source.Read(buffer, 0, buffer.Length);
						if (len <= 0) break;
						response.Write(buffer, 0, len);
					}
				}
				return;
			}
			context.SendErrorAndClose(404);
		}
	}
}