Click here to Skip to main content
15,867,488 members
Articles / Programming Languages / C#

Embedded .NET HTTP Server

Rate me:
Please Sign up or sign in to vote.
4.72/5 (33 votes)
12 Jul 2012CPOL10 min read 190.7K   5.2K   144   64
A simple HTTP server that can be embedded in any .NET application.

Note: this download requires you to have the source or binary from my Sockets library.

Introduction 

HTTP is everywhere these days. If you want to find out something, chances are you'll look for the answer on the Internet, through your browser and over HTTP. It is increasingly becoming a good idea if a lights-out server application can be monitored, and possibly administered, via HTTP. The .NET framework has good client-side support for HTTP through the System.Web namespace, and supports various clever ways of transferring data from one place to another (object remoting, Web Services), much of which is done by HTTP under the covers. But there is no simple and easy to use HTTP server available under the normal Framework; Microsoft thinks in terms of enterprises and IIS, not individual applications running a web server.

In order to provide a simple sign-up, user management, and monitoring system for my online gaming framework, which I spoke about here, I have put together a simple HTTP server that can be embedded in any .NET application, and which can be used to view the state of an application or submit material to it from within a browser.

The server supports session management (through cookies), switching folders based on the host requested (running multiple domains on the same IP), keep-alive connections, and, through the request handler which is provided, serving files from disk with substitution of pseudo-tags for dynamic content.

Use

To start an HTTP server that simply serves files is simple:

C#
HttpServer http = new HttpServer(new Server(80));
http.Handlers.Add(new SubstitutingFileReader(this));

However, the chances are you will want to perform some dynamic processing of certain URLs, support postback, or substitute particular <%pseudotags> for elements of dynamic content (for example, a navigation bar, a countern or a panel displaying the current user's private information such as private messages). In this case, you will need to inherit from SubstitutingFileReader and specify how to replace certain tags:

C#
public class MyHandler : SubstitutingFileReader {
 public override string GetValue(HttpRequest req, string tag){
  if(tag == "navbar") return "<!-- Navbar Begin -->" +
                "<div class=navbar>" +
                "<div class=logopanel>Test Web Application</div>" +
                "<div class=navlinks>" +
                "<a href=index.html>Home</a>" +
                "<a href=register.html>Register</a>" +
                "<a href=tos.html>Terms of Service</a>" +
                "</div>    </div>";
  else return base.GetValue(req, tag);
 }
}

This trivial example will replace <%navbar> with a fixed navigation bar as specified in your code.

Sessions

The previous example raises another point: to perform useful substitutions, in most cases, you will need a session. Because HTTP is inherently stateless, you cannot save information with a connection the way you can with a typical client-server system where a connection stays alive for long periods (such as the game server). However, it is common practice in dynamic servers to circumvent this problem with a session object, which is identified by a token that can be passed back and forth between the browser and the server.

There are three common ways in which a session is kept alive over multiple requests:

  • By passing the session token in the URL. If you have seen websites where the address bar contains '?sessid=5b3426AF42' or similar, that is the session identifier being passed back and forth each time you move to a new page.
  • By setting a cookie (a piece of data stored by the browser) which the browser will send with future requests.
  • In certain circumstances involving form submissions, the session ID can be sent through a hidden field which will be submitted when the other fields are.

As is probably most common at present, my server uses cookies to manage the session. In terms of the application code, you need to request a session in your handler's Process method:

C#
public override bool Process(HttpServer server, HttpRequest request, HttpResponse response){
  server.RequestSession(request);
  request.Session["lastpage"] = request.Page;
  return base.Process(server, request, response);
}

You can place any object in the Session object, and it will be available until that session is no longer valid. A typical use of a session is to manage authentication and login, allowing some pages to only be visible to a logged-in user. I will move on to this in the next section.

Postback

As well as serving up information, an HTTP server commonly receives information, either as a POST request (typically from a form) or via a query string appended to the URL. This information is available to your application through the Query field of HttpRequest, and you will typically want to enable posting back to a limited number of URLs. As with session management, you will need to override Process and insert code there to manage postback. Here is an example of processing a user login via postback and the HTML file that is used to generate the requests (login.html):

C#
public class PostbackHandler : SubstitutingFileHandler {
    public override bool Process(HttpServer server, 
                    HttpRequest request, HttpResponse response){
        if((request.Page.Length > 8) && 
           (request.Page.Substring(request.Page.Length - 8) == "postback")){
            // Postback. Action depends on the page parameter
            server.RequestSession(request);
            string target = request.Page.Substring(0, request.Page.Length - 8) + 
                            request.Query["page"] as string;
            if(target == "/login") Login(request, response);
            else {
                response.Content = "Unknown postback target "+target;
                response.ReturnCode = 404;
            }
            return true;
        }
        // Session management, special processing of GET requests etc
        base.Process(server, request, response);
    }

    void Login(HttpRequest req, HttpResponse resp){
        // Authenticate
        if( (((string)req.Query["f1"]) != "test") ||
            (((string)req.Query["f2"]) != "password") ){
            resp.MakeRedirect("/login.html?error=1&redirect="+req.Query["redirect"]);
            return;
        }
        // Add to session and redirect
        req.Session["user"] = new string[]{"test", 
                                            "password", "A Test User"};
        resp.MakeRedirect((string)req.Query["redirect"]);
    }

    public override string GetValue(HttpRequest req, string tag){
        if(tag == "navbar") return "<i>insert navbar</i>";
        else if(tag == "loginerror")
            return ((string)req.Query["error"] == "1") ?
              "<p class=error>The user name or password " + 
              "you provided was incorrect.</p>" : "";
        else if(tag == "redirect") return "" + req.Query["redirect"];
        else return base.GetValue(req, tag);
    }
}

Here is the HTML file that is used to send the postback:

HTML
<!-- login.html -->
<html>
<head>
<title>Test App: Log In</title>
<link rel=stylesheet href=my.css>
</head>

<body>

<%navbar>

<!-- Navbar End -->
<div id=content>
<h1>Log In</h1>
<p>The page you were trying to view requires you to be logged in. 
   Please enter your details below to be redirected.</p>

<%loginerror>

<div class=loginpanel>
<form action="postback?page=login&redirect=<%redirect>" method=POST>
<table class=login>
<tr><td align=right>Username:</td><td><input name="f1" value=""></td></tr>
<tr><td align=right>Password:</td><td><input type="password" name="f2" value=""></td></tr>
<tr><td colspan=2 align=center><input type=submit value="   Log in!   "></td></tr>
</table>
</form>
</div>

</div></body></html>

Note that the HTML file includes three pseudotags (navbar, loginerror, and redirect), which we have defined in the GetValue method of our handler. Obviously, you would include some form of authentication from a list of users loaded from file, or users loaded into your application already, but this example shows the basic mechanism of postback. The fields from the form end up in the request.Query hashtable.

Notice also that the example uses response.MakeRedirect() to redirect the user back to the login page in the case of a failure to log in correctly, and also to redirect the user to the target page when the login is successful. (You need to navigate to login.html?redirect=otherpage.html for this to work correctly.) MakeRedirect sends an HTTP 303, which causes the browser to request the page you pass to MakeRedirect. This allows you to separate your postback handling completely from your file reading, which could be considered a good thing; however, it is a purely stylistic choice, and you can set content in response to a POST in the ordinary way if you wish.

More on Authentication and Members' Areas

In the postback example above, the login function places an array into the Session, containing information on the user who is currently logged in. I recommend this technique, placing either an array, a Hashtable, or a custom UserInfo class in the session. Session["user"] then contains everything you need to know about the currently logged in user everywhere you might need it. For example, a simple technique to have a protected folder that you need to be logged in for (once again, place this code in your Process method):

C#
if((request.Page.Length > 9) && (request.Page.Substring(0, 9) == "/members/")){
    server.RequestSession(request);
    if(request.Session["user"] == null){
        response.MakeRedirect("/login.html?redirect="+request.Page);
        return true;
    }
}

Now, any attempt to access a URL under /members will redirect the user to a login page if they are not already logged in. (You must call RequestSession before accessing request.Session; for many applications, you will want to call RequestSession in the first line of Process so it is always available.) Of course, if you use the login code and the HTML file from earlier in this article, when you log in successfully, you are redirected back to the page you initially tried to access.

Multiple Handlers

In most cases, one handler is sufficient. However, if you prefer, it is possible to add multiple handlers (instances of IHttpHandler) to the HttpServer's Handlers list; for example, you could have a separate handler to cope with postback or protected folders, instead of adding branches to the flow of the Process method in a single handler. The last handler added will take precedence, and for each handler (working back) that returns false from Process, the previous handler will be asked to process it.

How it Works

If all you are interested in is making use of an HTTP server in your application, you can skip back to the top and click the Download link at this point. However, CodeProject being what it is, most people will be interested in some of the internals. This implementation uses my own sockets library, but similar code will be behind an HTTP server running directly from a .NET socket, or indeed a socket in another language.

The HTTP Header

Searching the Internet will quickly turn up the HTTP standard, including definitions for all the valid header fields, and a lot more detail than you will probably want to see. (This does not claim to be a complete HTTP 1.1 implementation; just enough to work.) However, an HTTP header is generally of the form:

GET /path/page.html?query=value HTTP/1.1
Host: www.test.com
Header-Field: value

... and is terminated by a blank line ("\r\n\r\n"). My sockets library allows for messages terminated by a text delimiter, so in the connection handler, we can set an event handler that can parse the header:

C#
bool ClientConnect(Server s, ClientInfo ci){
    ci.Delimiter = "\r\n\r\n";
    ci.Data = new ClientData(ci);
    ci.OnRead += new ConnectionRead(ClientRead);
    ci.OnReadBytes += new ConnectionReadBytes(ClientReadBytes);
    return true;
}

We need a Read, which reads text messages terminated by the delimiter, for the header, but we also need a ReadBytes handler to receive content, which is not terminated by a fixed delimiter and may contain any characters. The Read handler performs the relatively easy task of parsing and validating the header. Firstly, it checks that it should handle the current message:

C#
ClientData data = (ClientData)ci.Data;
if(data.state != ClientState.Header) return;
// already done; must be some text in content, which will be handled elsewhere

... as it is possible to receive a blank line within POST content. Then, it replaces the two character "\r\n" line-end with a one-character one, and splits the header into lines. The first line contains a lot of the most important information, so that is parsed and validated first:

C#
// First line: METHOD /path/url HTTP/version
string[] firstline = lines[0].Split(' ');
if(firstline.Length != 3){
  SendResponse(ci, data.req, new HttpResponse(400, 
     "Incorrect first header line "+lines[0]), true); return;
}
if(firstline[2].Substring(0, 4) != "HTTP"){
  SendResponse(ci, data.req, new HttpResponse(400, 
     "Unknown protocol "+firstline[2]), true); return;
}
data.req.Method = firstline[0];
data.req.Url = firstline[1];
data.req.HttpVersion = firstline[2].Substring(5);

The URL is scanned for a question mark, and if one is found, it is split into a Page and a QueryString. Assuming the first line is valid, the remaining lines are assumed to be header fields, and are split at the colon and placed into the Header hashtable. There are three special header fields that the server looks at: Host, which is placed in request.Host and must exist; Cookie, which is parsed and placed in the Cookie hashtable; and Content-Length, which specifies how much content will follow.

C#
data.req.Host = (string)data.req.Header["Host"];
if(null == data.req.Host){
  SendResponse(ci, data.req, new HttpResponse(400, "No Host specified"), true);
  return; 
}

if(null != data.req.Header["Cookie"]){
    string[] cookies = ((string)data.req.Header["Cookie"]).Split(';');
    foreach(string cookie in cookies){
        p = cookie.IndexOf('=');
        if(p > 0){
            data.req.Cookies[cookie.Substring(0, p).Trim()] = cookie.Substring(p+1);
        } else {
            data.req.Cookies[cookie.Trim()] = "";
        }
    }
}

if(null == data.req.Header["Content-Length"]) data.req.ContentLength = 0;
else data.req.ContentLength = Int32.Parse((string)data.req.Header["Content-Length"]);

Finally, the state of the connection is changed to indicate it is ready to receive content and how many bytes of header to skip, in preparation for reading the content (if any). Even if there is no content, because of the structure of my sockets library, the ClientReadBytes handler will be called and will effectively process a zero byte message.

Content

The content of the message has no fixed end delimiter, so we must hook to the binary stream of the socket, which is exposed through the ClientReadBytes event, which is called whenever data is received. This includes the header, even though we have processed that above, so we must skip over any data which was part of the header. After removing the header, what we read is simply appended to the content of the request, and if the message is complete, the query string (from the URL and from any POST content) is parsed and the request is processed.

C#
data.req.Content += Encoding.Default.GetString(bytes, ofs, len-ofs);
data.req.BytesRead += len - ofs;
data.headerskip += len - ofs;
if(data.req.BytesRead >= data.req.ContentLength){
    if(data.req.Method == "POST"){
        if(data.req.QueryString == "")data.req.QueryString = data.req.Content;
        else data.req.QueryString += "&" + data.req.Content;
    }
    ParseQuery(data.req);
    DoProcess(ci);
}

data.headerskip is used for the next request on this connection to ensure that the content of this message is not misinterpreted as part of the header of the next, as a connection is kept alive if there is no error.

Processing

Once a request has been parsed, it is passed to a response handler to produce the result. Except in the case of an invalid (i.e., the header could not be parsed correctly) request, the server class does not process requests, although there is a default handler which is attached by default (it simply echoes information about the request back to the browser). This has two steps: first, the query string is parsed, if there is one (simply splitting on & and then on =); secondly, the request is passed to each handler in turn, starting from the latest added, until one handles it:

C#
void DoProcess(ClientInfo ci){
    ClientData data = (ClientData)ci.Data;
    string sessid = (string)data.req.Cookies["_sessid"];
    if(sessid != null) data.req.Session = (Session)sessions[sessid];
    bool closed = Process(ci, data.req);
    data.state = closed ? ClientState.Closed : ClientState.Header;
    data.read = 0;
    HttpRequest oldreq = data.req;
    // Once processed, the connection will be used for a new request
    data.req = new HttpRequest();
    data.req.Session = oldreq.Session; // ... but session is persisted
    data.req.From = ((IPEndPoint)ci.Socket.RemoteEndPoint).Address;
}

protected virtual bool Process(ClientInfo ci, HttpRequest req){
    HttpResponse resp = new HttpResponse();
    resp.Url = req.Url;
    for(int i = handlers.Count - 1; i >= 0; i--){
        IHttpHandler handler = (IHttpHandler)handlers[i];
        if(handler.Process(this, req, resp)){
            SendResponse(ci, req, resp, resp.ReturnCode != 200);
            return resp.ReturnCode != 200;
        }
    }
    return true;
}

DoProcess performs a certain amount of administrative work before and after calling Process: first, it loads the session for this request, if there is one; and after the request is processed, it creates a new HttpRequest object for the next request to be made over this connection. Process itself works through the handlers, and when one responds to the request, it calls SendResponse (see below), keeping the connection alive if there was no error.

Responding

The final stage is sending a response once the application has determined what the content should be. This involves adding a valid HTTP header to the message and then sending the content, which may either be binary or text (which is sent as UTF-8).

C#
void SendResponse(ClientInfo ci, HttpRequest req, HttpResponse resp, bool close){
    ci.Send("HTTP/1.1 " + resp.ReturnCode + Responses[resp.ReturnCode] +
            "\r\nDate: "+DateTime.Now.ToString("R")+
            "\r\nServer: RedCoronaEmbedded/1.0"+
            "\r\nConnection: "+(close ? "close" : "Keep-Alive"));
    if(resp.RawContent == null )
        ci.Send("\r\nContent-Encoding: utf-8"+
            "\r\nContent-Length: "+resp.Content.Length);
    else
        ci.Send("\r\nContent-Length: "+resp.RawContent.Length);
    if(req.Session != null) ci.Send("\r\nSet-Cookie: _sessid="+req.Session.ID+"; path=/");
    foreach(DictionaryEntry de in resp.Header) ci.Send("\r\n" + de.Key + ": " + de.Value);
    ci.Send("\r\n\r\n"); // End of header
    if(resp.RawContent != null) ci.Send(resp.RawContent);
    else ci.Send(resp.Content);
    //Console.WriteLine("** SENDING\n"+Encoding.Default.GetString(resp.Content));
    if(close) ci.Close();
}

Session Management

One useful feature of this server is session management. Most of the code to manage sessions has in fact already been shown; in DoProcess, the cookie _sessid is checked for and a session is loaded if it exists, and in SendResponse, the cookie is set if there is a session. There are two other methods related to sessions: RequestSession, the public method used to obtain a valid session; and CleanUpSessions, which is called whenever any request is processed, and which removes any sessions that have expired.

C#
public Session RequestSession(HttpRequest req){
    if(req.Session != null){
        if(sessions[req.Session.ID] == req.Session) return req.Session;
    }
    req.Session = new Session(req.From);
    sessions[req.Session.ID] = req.Session;
    return req.Session;
}

void CleanUpSessions(){
    ICollection keys = sessions.Keys;
    ArrayList toRemove = new ArrayList();
    foreach(string k in keys){
        Session s = (Session)sessions[k];
        int time = (int)((DateTime.Now - s.LastTouched).TotalSeconds);
        if(time > sessionTimeout){
            toRemove.Add(k);
            Console.WriteLine("Removed session "+k);
        }
    }
    foreach(object k in toRemove) sessions.Remove(k);
}

History

  • April 2012: Updated the source to use generics in the request, and to add a URLDecode method in the server which is called on query contents.

License

This article, along with any associated source code and files, is licensed under The Code Project Open License (CPOL)


Written By
United Kingdom United Kingdom
I'm a recent graduate (MSci) from the University of Cambridge, no longer studying Geology. Programming is a hobby so I get to write all the cool things and not all the boring things Smile | :) . However I now have a job in which I have to do a bit of work with a computer too.

Comments and Discussions

 
GeneralDocumentation [Info] Pin
Peter Mortensen29-Jul-15 12:06
Peter Mortensen29-Jul-15 12:06 
QuestionLine "http.Handlers.Add(new SubstitutingFileReader(this));" does not compile [with solution] [with suggestion to the author] Pin
Peter Mortensen28-Jul-15 11:48
Peter Mortensen28-Jul-15 11:48 
QuestionThe last 256 bytes are not returned when serving static files Pin
Peter Mortensen28-Jul-15 10:00
Peter Mortensen28-Jul-15 10:00 
AnswerRe: The last 256 bytes are not returned when serving static files Pin
Peter Mortensen28-Jul-15 11:49
Peter Mortensen28-Jul-15 11:49 
QuestionSlow serving [with solution] Pin
Peter Mortensen28-Jul-15 8:54
Peter Mortensen28-Jul-15 8:54 
GeneralWhere are files served from? From sub folder "webhome"! Pin
Peter Mortensen28-Jul-15 8:38
Peter Mortensen28-Jul-15 8:38 
GeneralCorrupted ZIP file Pin
Peter Mortensen27-Jul-15 8:56
Peter Mortensen27-Jul-15 8:56 
GeneralRe: Corrupted ZIP file Pin
Peter Mortensen27-Jul-15 9:04
Peter Mortensen27-Jul-15 9:04 
GeneralI like it! Pin
Lucas Ontivero17-Feb-15 12:49
professionalLucas Ontivero17-Feb-15 12:49 
SuggestionA complete Example would be useful Pin
DrJaymz24-Dec-13 3:42
DrJaymz24-Dec-13 3:42 
GeneralRe: A complete Example would be useful Pin
BobJanova24-Dec-13 4:00
BobJanova24-Dec-13 4:00 
GeneralRe: A complete Example would be useful Pin
DrJaymz24-Dec-13 4:05
DrJaymz24-Dec-13 4:05 
GeneralRe: A complete Example would be useful Pin
DrJaymz1-Jan-14 21:46
DrJaymz1-Jan-14 21:46 
GeneralMy vote of 5 Pin
Southmountain28-May-13 7:29
Southmountain28-May-13 7:29 
GeneralRe: My vote of 5 Pin
BobJanova28-May-13 7:58
BobJanova28-May-13 7:58 
As it says in the introduction, I use it in my game server to provide a remote status/admin capability. You could use it anywhere where you want to provide HTTP (i.e. browser) access to your application without having to make a plugin for an existing web server.
QuestionGreat work! file uploading? Pin
sean 9127-Jul-12 6:42
sean 9127-Jul-12 6:42 
AnswerRe: Great work! file uploading? Pin
BobJanova27-Jul-12 10:35
BobJanova27-Jul-12 10:35 
GeneralRe: Great work! file uploading? Pin
sean 9128-Jul-12 1:57
sean 9128-Jul-12 1:57 
QuestionSystem.net httplistener Pin
TheFisch16-Jul-12 10:16
TheFisch16-Jul-12 10:16 
AnswerRe: System.net httplistener Pin
BobJanova16-Jul-12 10:21
BobJanova16-Jul-12 10:21 
GeneralMy vote of 5 Pin
devvvy12-Jul-12 16:19
devvvy12-Jul-12 16:19 
QuestionBob this is cool. no god, you are cool! Pin
devvvy12-Jul-12 16:16
devvvy12-Jul-12 16:16 
QuestionWow, seems like a lot of work. Pin
John9703010-Jul-12 7:36
John9703010-Jul-12 7:36 
GeneralMy vote of 4 Pin
Aamer Alduais9-Jul-12 21:13
Aamer Alduais9-Jul-12 21:13 
GeneralMy vote of 5 Pin
Casey Sheridan9-Jul-12 16:58
professionalCasey Sheridan9-Jul-12 16:58 

General General    News News    Suggestion Suggestion    Question Question    Bug Bug    Answer Answer    Joke Joke    Praise Praise    Rant Rant    Admin Admin   

Use Ctrl+Left/Right to switch messages, Ctrl+Up/Down to switch threads, Ctrl+Shift+Left/Right to switch pages.