IGS/WebServer/SimpleHttpServer.cs
using System;
using System.Collections;
using System.Collections.Specialized;
using System.IO;
using System.Net;
using System.Net.Sockets;
using System.Threading;
using System.Web;
using System.Diagnostics;
// offered to the public domain for any use with no restriction
// and also with no warranty of any kind, please enjoy. - David Jeske.
// simple HTTP explanation
// http://www.jmarshall.com/easy/http/
namespace PointAndControl.WebServer
{
/// <summary>
/// This class contains the Arguments of a HTTP event /// </summary>
public class HttpEventArgs : EventArgs
{
/// <summary>
/// Constructor for the event
/// <param name="clientip">The IP of the User, who is responsible for the triggering</param>
/// <param name="dev">The ID of the Device which should be controlled</param>
/// <param name="cmd">The ID of the command which should be executed.</param>
/// <param name="val">The Value belonging to the command.</param>
/// <param name="p">The HTTpProcessor belonging to the command.</param>
/// </summary>
public HttpEventArgs(String clientip, String dev, String cmd, String val,String language, HttpProcessor p)
{
ClientIp = clientip;
Dev = dev;
Cmd = cmd;
Val = val;
Language = language;
P = p;
}
public HttpEventArgs(String clientip, String PostString, HttpProcessor p)
{
POSTString = PostString;
P = p;
}
public String POSTString { get; set; }
/// <summary>
/// The IP of the client.\n
/// With the "set"-method the ip can be set.\n
/// With the "get"-method the ip can be returned.
/// <returns>Returns the IP of the client</returns>
/// </summary>
public String ClientIp { get; set; }
/// <summary>
/// The ID of the device which will be controlled. \n
/// With the "set"-method the DeviceID can be set.\n
/// With the "get"-method the DeviceID can be returned.
/// <returns>Returns the DeviceID</returns>
/// </summary>
public String Dev { get; set; }
public String Language { get; set; }
/// <summary>
/// The Command ID of the command which should be executed.\n
/// With the "set"-method the CommandID can be set.\n
/// With the "get"-method the CommandID can be returned.
/// <returns>Returns the CommandID</returns>
/// </summary>
public String Cmd { get; set; }
/// <summary>
/// The passed Value of the command, which should be executed.\n
/// With the "set"-method the value can be set.\n
/// With the "get"-method the value can be returned.
/// <returns>Returns the value of the command</returns>
/// </summary>
public String Val { get; set; }
/// <summary>
/// The HTTPProcessor of the client.\n
/// With the "set"-method the HTTPProcessor can be set.\n
/// With the "get"-method the HTTPProcessor can be returned.
/// <returns>Returns the HTTPProcessor of the client.</returns>
/// </summary>
public HttpProcessor P { get; set; }
}
/// <summary>
/// delegate for a HTTP event
/// <param name="sender">the sender</param>
/// <param name="e">the eventArgs</param>
/// </summary>
public delegate void HttpEventHandler(object sender, HttpEventArgs e);
/// <summary>
/// Is responsible for the management of a client.
/// The class HttpProcessor runs in a seperate thread which will be created at arrival of a new client.
/// The class receives the requests of the client and passes them on to the class HttpServer.
/// The request will be interpreted there and the action for the corresponding request will be executed
/// @author Christopher Baumgärtner(edited)
/// </summary>
public class HttpProcessor
{
/// <summary>
/// The header
/// </summary>
private Hashtable _httpHeaders = new Hashtable();
private static int MAX_POST_SIZE = 10 * 1024 * 1024; // 10MB
/// <summary>
/// constructor for the HTTPProcessor
/// </summary>
/// <param name="s">The TCP client</param>
/// <param name="srv">The HTTP server</param>
public HttpProcessor(TcpClient s, HttpServer srv)
{
Socket = s;
Srv = srv;
}
/// <summary>
/// The socket of the connection.
/// With the "set"-method the HTTPProcessor can be set.\n
/// With the "get"-method the HTTPProcessor can be returned.
/// <returns>Returns the socket of the connection</returns>
/// </summary>
public TcpClient Socket { get; set; }
/// <summary>
/// The HTTP server.
/// With the "set"-method the HTTP server can be set.\n
/// With the "get"-method the HTTP server can be returned.
/// <returns>Returns the HTTP server</returns>
/// </summary>
public HttpServer Srv { get; set; }
/// <summary>
/// The InputStream
/// With the "set"-method the InputStream can be set.\n
/// With the "get"-method the InputStream can be returned.
/// <returns>returns the InputStream</returns>
/// </summary>
public Stream InputStream { get; set; }
/// <summary>
/// The OutputStream
/// With the "set"-method the OutputStream can be set.\n
/// With the "get"-method the OutputStream can be returned.
/// <returns>Returns the OutputStream</returns>
/// </summary>
public StreamWriter OutputStream { get; set; }
/// <summary>
/// The HTTP-method.
/// With the "set"-method the HTTP-method can be set.\n
/// With the "get"-method the HTTP-method can be returned.
/// <returns>Returns the HTTP-method</returns>
/// </summary>
public String HttpMethod { get; set; }
/// <summary>
/// The HTTP-URL.
/// With the "set"-method the HTTP-URL can be set.\n
/// With the "get"-method the HTTP-URL can be returned.
/// <returns>Returns the HTTP-URL</returns>
/// </summary>
public String HttpUrl { get; set; }
/// <summary>
/// The versionstring of the protokol.
/// With the "set"-method the versionstring of the protokolr can be set.\n
/// With the "get"-method the versionstring of the protokol can be returned.
/// <returns>Gibt den Versionsstring des Protokols zurück</returns>
/// </summary>
public String HttpProtocolVersionstring { get; set; }
/// <summary>
/// The HTTP-Header.
/// With the "set"-method the HTTP-Header can be set.\n
/// With the "get"-method the HTTP-Header can be returned.
/// <returns>Returns the HTTP-Header</returns>
/// </summary>
public Hashtable HttpHeaders
{
get { return _httpHeaders; }
set { _httpHeaders = value; }
}
private String StreamReadLine(Stream inputStream)
{
String data = "";
while (true)
{
int nextChar = inputStream.ReadByte();
if (nextChar == '\n')
{
break;
}
if (nextChar == '\r')
{
continue;
}
if (nextChar == -1)
{
Thread.Sleep(1);
continue;
}
data += Convert.ToChar(nextChar);
}
return data;
}
/// <summary>
/// Receivs the request, reads the header and passes the request on to handleGETRequest() or handlePOSTRequest().
/// </summary>
public void Process()
{
// we can't use a StreamReader for input, because it buffers up extra data on us inside it's
// "processed" view of the world, and we want the data raw after the headers
InputStream = new BufferedStream(Socket.GetStream());
// we probably shouldn't be using a streamwriter for all output from handlers either
OutputStream = new StreamWriter(new BufferedStream(Socket.GetStream()));
try
{
ParseRequest();
ReadHeaders();
if (HttpMethod.Equals("GET"))
{
HandleGetRequest();
}
else if (HttpMethod.Equals("POST"))
{
HandlePostRequest();
}
OutputStream.Flush();
}
catch (Exception e)
{
Console.WriteLine(e.StackTrace);
WriteFailure();
}
InputStream = null;
OutputStream = null;
Socket.Close();
}
/// <summary>
/// Receivs the http request and splits it in its parts (http_method, http_url, http_protocol_versionstring).
/// </summary>
private void ParseRequest()
{
String request = StreamReadLine(InputStream);
String[] tokens = request.Split(' ');
if (tokens.Length != 3)
{
throw new Exception("invalid http request line");
}
HttpMethod = tokens[0].ToUpper();
HttpUrl = tokens[1].Trim('/');
Debug.WriteLine("HttpUrl: {0}", HttpUrl);
HttpProtocolVersionstring = tokens[2];
Debug.WriteLine("starting: " + request);
}
/// <summary>
/// Receivs and processes the Http header.
/// For every header the name and the value will be read and saved in httpHeader[].
/// </summary>
private void ReadHeaders()
{
Debug.WriteLine("readHeaders()");
String line;
while ((line = StreamReadLine(InputStream)) != null)
{
if (line.Equals(""))
{
Debug.WriteLine("got headers");
return;
}
int separator = line.IndexOf(':');
if (separator == -1)
{
throw new Exception("invalid http header line: " + line);
}
String name = line.Substring(0, separator);
int pos = separator + 1;
while ((pos < line.Length) && (line[pos] == ' '))
{
pos++; // strip any spaces
}
String value = line.Substring(pos, line.Length - pos);
Debug.WriteLine("header: {0}:{1}", name, value);
HttpHeaders[name] = value;
}
}
/// <summary>
/// Passes the GET-request on to the server (HttpServer.handleGetRequest).
/// </summary>
public void HandleGetRequest()
{
Srv.HandleGetRequest(this);
}
private const int BUF_SIZE = 4096;
public void HandlePostRequest()
{
// this post data processing just reads everything into a memory stream.
// this is fine for smallish things, but for large stuff we should really
// hand an input stream to the request processor. However, the input stream
// we hand him needs to let him see the "end of the stream" at this content
// length, because otherwise he won't know when he's seen it all!
int content_len = 0;
MemoryStream ms = new MemoryStream();
if (HttpHeaders.ContainsKey("Content-Length"))
{
content_len = Convert.ToInt32(this.HttpHeaders["Content-Length"]);
if (content_len > MAX_POST_SIZE)
{
throw new Exception(
String.Format("POST Content-Length({0}) too big for this simple server",
content_len));
}
byte[] buf = new byte[BUF_SIZE];
int to_read = content_len;
while (to_read > 0)
{
Console.WriteLine("starting Read, to_read={0}", to_read);
int numread = this.InputStream.Read(buf, 0, Math.Min(BUF_SIZE, to_read));
Console.WriteLine("read finished, numread={0}", numread);
if (numread == 0)
{
if (to_read == 0)
{
break;
}
else
{
throw new Exception("client disconnected during post");
}
}
to_read -= numread;
ms.Write(buf, 0, numread);
}
ms.Seek(0, SeekOrigin.Begin);
}
Srv.HandlePostRequest(this, new StreamReader(ms));
}
/// <summary>
/// Responses that the reading was successful(200).
/// </summary>
public void WriteSuccess(String content_type)
{
OutputStream.WriteLine("HTTP/1.0 200 OK");
OutputStream.WriteLine("Content-Type: " + content_type);
OutputStream.WriteLine("Connection: close");
OutputStream.WriteLine("");
OutputStream.Flush();
}
/// <summary>
/// Responses that the reading was erroneus(404).
/// </summary>
public void WriteFailure()
{
OutputStream.WriteLine("HTTP/1.0 404 File not found");
OutputStream.WriteLine("Connection: close");
OutputStream.WriteLine("");
}
/// <summary>
/// Responses to redirect to another URL.
/// Error Number 301 (Moved Permanently) - Cache - UA MUST NOT Autmatically Redirect
/// Error Number 302 (Found) - Only Cachable if indicated by Cache-Control or Expires header fiel. UA See 301
/// Error Number 303 (See Other) - MUST NOT be Cached - Should be answered with a GET method.
/// Default: 301
/// </summary>
public void WriteRedirect(String new_location, int errorNumber)
{
String error = "";
switch (errorNumber)
{
case 301:
error = "Moved Permanently";
break;
case 302:
error = "Found";
break;
case 303:
error = "See Other";
break;
case 307:
error = "Moved Temporary";
break;
default:
error = "Found";
errorNumber = 302;
break;
}
OutputStream.WriteLine("HTTP/1.1 " + errorNumber + " " + error);
OutputStream.WriteLine("Location: " + new_location);
OutputStream.WriteLine("Cache - Control: no - cache, no - store");
OutputStream.WriteLine("Pragma: no-cache");
OutputStream.WriteLine("Expires: 0");
OutputStream.WriteLine("");
OutputStream.Flush();
}
}
/// <summary>
/// Part of the design pattern: subject(HttpEvent)
/// the abstract class of a HttpServer.
/// The classes implementing HTTPServer are called by a HttpProcessor and interpretes the passed request.
/// The methood hadnleGETRequest and handlePOSTRequest are responsible for the follwing procession of the request.
/// @author Christopher Baumgärtner(edited)
/// </summary>
public abstract class HttpServer
{
/// <summary>
/// The local IP
/// </summary>
protected IPAddress LocalIp;
private const bool is_active = true;
private TcpListener _listener;
/// <summary>
/// The port the server uses.
/// </summary>
protected int Port;
/// <summary>
/// Constructor for the HttpServer.
/// <param name="port">the port which is used by the server</param>
/// <param name="localIp">the IP-adress of the server</param>
/// </summary>
protected HttpServer(int port, IPAddress localIp)
{
Port = port;
LocalIP = localIp;
}
/// <summary>
/// The IP-adress of the server.
/// With the "set"-method the OutputStream can be set.\n
/// With the "get"-method the OutputStream can be returned.
/// <returns>Returns the Ip-adress of the server</returns>
/// </summary>
public IPAddress LocalIP { get; set; }
/// <summary>
/// Part of the design pattern: observer(HttpEvent)
/// </summary>
public virtual event HttpEventHandler Request;
public virtual event HttpEventHandler postRequest;
/// <summary>
/// Waits for arriving clients and starts a new thread with HttpProcessor and the TCP Client at arrival.
/// </summary>
public void Listen()
{
Console.WriteLine(String.Format(Properties.Resources.StartingHTTPServer, LocalIP.ToString(),Port));
_listener = new TcpListener(LocalIP, Port);
try
{
_listener.Start();
} catch (SocketException)
{
Console.WriteLine("Port already in use, please type another one");
Console.Write("Port: ");
Port = int.Parse(Console.ReadLine());
_listener = new TcpListener(LocalIP, Port);
_listener.Start();
}
while (is_active)
{
TcpClient s = _listener.AcceptTcpClient();
HttpProcessor processor = new HttpProcessor(s, this);
Thread thread = new Thread(new ThreadStart(processor.Process));
thread.Start();
Thread.Sleep(1);
}
}
/// <summary>
/// Will be called when an event occurs
/// Part of the design pattern: observerTeil des Entwurfmusters: Beobachter(HttpEvent)
/// Inhabits the function of the notify method in the observer design pattern.
/// </summary>
/// <param name="e">the eventArgs</param>
public abstract void OnRequest(HttpEventArgs e);
public abstract void OnPOSTRequest(HttpEventArgs e);
/// <summary>
/// processes a GET-request.
/// <param name="p">der HttpProcessor</param>
/// </summary>
public abstract void HandleGetRequest(HttpProcessor p);
public abstract void HandlePostRequest(HttpProcessor p, StreamReader inputdata);
/// <summary>
/// Sends a response to the client.
/// </summary>
/// <param name="p">the HttpProcessor processing the connection with the client.</param>
/// <param name="msg">the response</param>
public abstract void SendResponse(HttpProcessor p, String msg);
//public abstract void SendDataDirect(HttpProcessor p, String msg);
}
/// <summary>
/// Part of the design pattern: subject(HttpEvent)
/// The class MyHttpServer implements a http server.
/// The class MyHttpServer is called by a HttpProcesser and implements the provided request.
/// The method handleGETRequest and handlePOSTRequest is responsible for the following processing of the request.
/// @author Christopher Baumgärtner(edited)
/// </summary>
public class MyHttpServer : HttpServer
{
/// <summary>
/// Constructor for MyHttpServer.
/// <param name="port">the port belonging to the server</param>
/// <param name="localIp">the ip-adress of the server</param>
/// </summary>
public MyHttpServer(int port, IPAddress localIp)
: base(port, localIp)
{
}
/// <summary>
/// Part of the design pattern: observer(HttpEvent)
/// </summary>
public override event HttpEventHandler Request;
public override event HttpEventHandler postRequest;
/// <summary>
/// Is called when an event occurs
/// Part of the design pattern: observer(HttpEvent)
/// Takes the place of the notify-method in the observer deisgn pattern.
/// </summary>
/// <param name="e">die EventArgs</param>
public override void OnRequest(HttpEventArgs e)
{
if (Request != null) Request(this, e);
}
public override void OnPOSTRequest(HttpEventArgs e)
{
if (postRequest != null) postRequest(this, e);
}
/// <summary>
/// Processes the GET-request.
/// Either the requested file will be sent to the client or the provided parameters will be processed.
/// <param name="p">the HttpProcessor</param>
/// </summary>
public override void HandleGetRequest(HttpProcessor p)
{
string querystring = null;
string pathstring = null;
// check query part of string
int iqs = p.HttpUrl.IndexOf('?');
// If query string variables exist, put them in a string.
//if(p.HttpUrl == "")
//{
// p.WriteRedirect("Http://" + LocalIP + ":" + Port + "/index.html",302);
//}
if (iqs >= 0)
{
querystring = (iqs < p.HttpUrl.Length - 1) ? p.HttpUrl.Substring(iqs + 1) : String.Empty;
pathstring = p.HttpUrl.Substring(0, iqs);
}
else
{
pathstring = p.HttpUrl;
}
if (isFileEnding(pathstring))
{
sendData(p);
}
else if (querystring != null && querystring.Length > 0) //TODO: currently we either serve files or process query parameters
{
NameValueCollection col = HttpUtility.ParseQueryString(querystring);
String device = col["dev"];
String command = col["cmd"];
String language = p.HttpHeaders["Accept-Language"].ToString();
language = language.Split(',')[0];
if (device != null && command != null)
{
String value = (col["val"] != null)? col["val"] : "";
String clientIp = ((IPEndPoint)p.Socket.Client.RemoteEndPoint).Address.ToString();
OnRequest(new HttpEventArgs(clientIp, device, command, value, language, p));
} else {
p.WriteFailure();
}
}
else
{
p.WriteRedirect(String.Format("Http://{0}:{1}/index.html", LocalIP, Port), 302);
}
}
public override void HandlePostRequest(HttpProcessor p, StreamReader inputData)
{
Console.WriteLine("POST request: {0}", p.HttpUrl);
string data = inputData.ReadToEnd();
p.WriteSuccess("text/html");
p.OutputStream.WriteLine("<html><body><h1>test server</h1>");
p.OutputStream.WriteLine("<a href=/test>return</a><p>");
p.OutputStream.WriteLine("postbody: <pre>{0}</pre>", data);
String clientIp = ((IPEndPoint)p.Socket.Client.RemoteEndPoint).Address.ToString();
OnPOSTRequest(new HttpEventArgs(clientIp, data, p));
}
/// <summary>
/// Sends the requested file to the client
/// <param name="p">the HttpProcessor</param>
/// </summary>
private void sendData(HttpProcessor p)
{
//TODO: Write Flexible Header usage
String pathstring = null;
int iqs = p.HttpUrl.IndexOf("?");
if (iqs == -1)
{
pathstring = p.HttpUrl;
}
else if (iqs > 0)
{
pathstring = p.HttpUrl.Substring(0, iqs);
}
Debug.WriteLine("send Data: " + pathstring);
bool continueSending = responseToFileRequest(p, pathstring);
if (!continueSending)
{
return;
}
if (File.Exists(AppDomain.CurrentDomain.BaseDirectory + "\\HttpRoot\\" + pathstring))
{
using (
Stream fs = File.Open(AppDomain.CurrentDomain.BaseDirectory + "\\HttpRoot\\" + pathstring,
FileMode.Open)
)
{
fs.CopyTo(p.OutputStream.BaseStream);
p.OutputStream.BaseStream.Flush();
}
}
else
{
p.WriteFailure();
}
}
/// <summary>
/// Sends a response to the client.
/// </summary>
/// <param name="p">the HttpProcessor processing the connection with the client.</param>
/// <param name="msg">the response</param>
public override void SendResponse(HttpProcessor p, String msg)
{
p.OutputStream.Write(msg);
}
public bool isFileEnding(string pathstring)
{
string ending = getFileEnding(pathstring);
switch (ending)
{
case ".html":
return true;
case ".css":
return true;
case ".js":
return true;
case ".json":
return true;
case ".map":
return true;
case ".jpg":
return true;
case ".gif":
return true;
case ".png":
return true;
case ".ico":
return true;
case ".svg":
return true;
default:
return false;
}
}
private string getFileEnding(string s)
{
string[] pathDotSplit = s.Split('.');
return "." + pathDotSplit[pathDotSplit.Length - 1];
}
private bool responseToFileRequest(HttpProcessor p, string pathString)
{
string fileEnding = getFileEnding(pathString);
switch (fileEnding)
{
case ".html":
p.WriteSuccess("text/html");
return true;
case ".css":
p.WriteSuccess("text/css");
return true;
case ".js":
if (pathString.Equals("js/globalize/globalize.js"))
{
string gloablizeRerout = getLocalizationPath(p, pathString);
p.WriteRedirect(gloablizeRerout, 302);
return false;
}
p.WriteSuccess("text/javascript");
return true;
case ".json":
p.WriteSuccess("application/json");
return true;
case ".map":
p.WriteSuccess("application/octet-stream");
return true;
case ".jpg":
p.WriteSuccess("image/jpg");
return true;
case ".gif":
p.WriteSuccess("image/gif");
return true;
case ".png":
p.WriteSuccess("image/png");
return true;
case ".ico":
p.WriteSuccess("image/x-icon");
return true;
case ".svg":
p.WriteSuccess("image/svg+xml");
return true;
default:
p.WriteFailure();
return false;
}
}
public string getLocalizationPath(HttpProcessor p, string pathString)
{
string language = p.HttpHeaders["Accept-Language"].ToString();
language = language.Split(',')[0];
if(pathString == "js/globalize/globalize.js")
{
string baseServerGlobalize = "http://" + LocalIP + ":" + Port + "/js/globalize/";
string globalizeServerPath = AppDomain.CurrentDomain.BaseDirectory + "\\HttpRoot\\js\\globalize\\" + language + "\\globalize.js";
if (File.Exists(globalizeServerPath))
{
return baseServerGlobalize + language + "/globalize.js";
} else
{
return baseServerGlobalize + "default" + "/globalize.js";
}
} else
{
return "";
}
}
}
}