2012-03-16

While hacking on MuttonChop, It was necessary to create an HTTP server to handle requests from the Web client. Since the server could be quite useful for various other projects, I though it would be a good idea to take out the MuttonChop specific code and share the Server as a building block for future projects.

Originally, the server was going to use libsoup, an HTTP client/server library, for all of my HTTP serving needs but I couldn't get libsoup to create Server Sent Events so I instead opted to use GIO for low level socket communication and I had to roll my own server.

Enter the Vala

/* A Little Vala Server
 * copyright 2012 Jezra Lickter
 * Licensed GPLv3
 
 * save as "valaserver.vala" and compile with
 * valac --pkg gio-2.0 valaserver.vala
 
 */

using GLib;
//what port are we serving on?
const uint16 PORT 8787;

namespace StatusCode {
  const string FILE_NOT_FOUND "HTTP/1.1 404 Not Found\n"
  const string OK "HTTP/1.1 200 OK\n"
  const string ERROR "HTTP/1.1 500 Internal Server Error\n"
}

struct Request {
  string full_request;
  string path;
  string query;
  HashTable<stringstringargs;
  string object;
  string action;
  string val;
}

struct Response {
  string status_code;
  string content_type;
  string text;
  uint8[] data;
}

public class WebServer {
  
  private ThreadedSocketService tss;
  private string public_dir;
  private Regex ext_reg;
  
  public WebServer() {
    public_dir="public";
    try {
      ext_reg new Regex("\\.(?<ext>[a-z]{2,4})$");
    catch(Error e) {
      stderr.printf(e.message+"\n");
    }
    //make the threaded socket service with hella possible threads
    tss new ThreadedSocketService(150);
    //create an IPV4 InetAddress bound to no specific IP address
    InetAddress ia new InetAddress.any(SocketFamily.IPV4);
    //create a socket address based on the netadress and set the port
    InetSocketAddress isa new InetSocketAddress(iaPORT);
    //try to add the address to the ThreadedSocketService
    try {
      tss.add_address(isaSocketType.STREAMSocketProtocol.TCPnullnull);
    catch(Error e) {
      stderr.printf(e.message+"\n");
      return;
    }
    /* connect the 'run' signal that is emitted when 
     * there is a connection to our connection handler
     */
    tss.run.connectconnection_handler );
  }
  
  public void run() {
    //we need a gobject main loop
    MainLoop ml new MainLoop();
    //start listening 
    tss.start();
    stdout.printf(@"Serving on port $PORT\n");
    //run the main loop
    ml.run();
  }
  //when a request is made, handle the socket connection
  private bool connection_handler(SocketConnection connection) {
    string first_line ="";
    size_t size 0;
    Request request Request();
    //get data input and output streams for the connection
    DataInputStream dis new DataInputStream(connection.input_stream);
    DataOutputStream dos new DataOutputStream(connection.output_stream);  
    //read the first line from the input stream
    try {
      first_line dis.read_lineout size );
      request get_requestfirst_line );
      
    catch (Error e) {
      stderr.printf(e.message+"\n");
    }
    //build a response based on the request
    Response response Response();
    response get_file_response(request);
    serve_responseresponsedos );
    return false;
  }
  
  private void serve_response(Response responseDataOutputStream dos) {
    try {
      var data response.data ?? response.text.data;
      dos.put_string(response.status_code);
      dos.put_string("Server: ValaSocket\n");
      dos.put_string("Content-Type: %s\n".printf(response.content_type));
      dos.put_string("Content-Length: %d\n".printf(data.length));
      dos.put_string("\n");//this is the end of the return headers
      /* For long string writes, a loop should be used,
       * because sometimes not all data can be written in one run 
       *  see http://live.gnome.org/Vala/GIOSamples#Writing_Data
       */ 
      long written 0;
      while (written data.length) { 
          // sum of the bytes of 'text' that already have been written to the stream
          written += dos.write (data[written:data.length]);
      }
    catchError e ) {
      stderr.printf(e.message+"\n");
    }
  }
  
  private Response get_file_response(Request request) {
    //default request.path = index.htm
    string request_path = (request.path=="/") ? "index.htm" request.path;
    string filepathPath.build_filename(public_dirrequest_path);
    Response response Response();
    response.content_type "text/plain";
    response.status_code StatusCode.ERROR;
    //does the file exist?
    if (FileUtils.test(filepathGLib.FileTest.IS_REGULAR) ) {
      //serve the file
      bool read_failed true;
      uint8[] data = {};
       try {
        FileUtils.get_data(filepathout data);
        response.data data;
        response.content_type get_content_typefilepath );
        response.status_code StatusCode.OK;
        read_failed false;
      catch (Error err) {
        response.text err.message;
        response.status_code StatusCode.ERROR;
        response.content_type="text/plain";
      }    
    else {
      //file not found
      response.status_code StatusCode.FILE_NOT_FOUND;
      response.content_type "text/plain";
      response.text "File Not Found";
    }
    return response;
  }
  
  private string get_content_type(string file) {
    //get the extension
    MatchInfo mi;
    ext_reg.matchfile0out mi );
    var ext mi.fetch_named("ext");
    string content_type "text/plain";
    if (ext!=null) {
      string lower_ext ext.down();
      switch(lower_ext) {
        case "htm":
        case "html":
          content_type="text/html";
          break;
        case "xml":
          content_type="text/xml";
          break;
        case "js":
        case "json":
          content_type="text/javascript";
          break;
        case "css":
          content_type="text/css";
          break;
        case "ico":
          content_type="image/icon";
          break;
        case "png":
          content_type="image/png";
          break;
        case "jpg":
          content_type="image/jpeg";
          break;
        case "gif":
          content_type="image/gif";
          break;
      }
    }
    return content_type;
  }
  
  // return a Request based on a portion of th line
  private Request get_request(string line) {
    Request Request();
    r.args new HashTable<stringstring>(str_hashstr_equal);
    //get the parts from the line
    string[] parts line.split(" ");
    //how many parts are there?
    if (parts.length == 1) {
      return r;
    }
    //add the path to the Request
    r.full_request parts[1];
    parts r.full_request.split("?");
    r.path parts[0];
    r.query parts[1] ?? "";
    //get the object and action
    parts r.path.split("/");
    if (parts.length 1) {
      r.object parts[1] ?? "";
    }
    if (parts.length 2) {
      r.action parts[2] ?? "";
    }
    if (parts.length 3) {
      r.val Uri.unescape_string(parts[3]) ?? "";
    }
    //split the query if it exists
    if (r.query != "") {
      string[] query_parts={};
      parts r.query.split("&");
      foreachstring part in parts ) {
        query_parts part.split("=");
        if (query_parts.length == 2){
          r.args[query_parts[0]] = Uri.unescape_string(query_parts[1]);
        }
      }
    }
    return r;
  
}

public static void main() {
  WebServer ws new WebServer();
  ws.run();
}

The code is also available at http://hoof.jezra.net/snip/nV. save as "valaserver.vala" and compile with

valac --pkg gio-2.0 valaserver.vala

In the same directory where the code was compiled, create a directory named "public" and fill the directory with static HTML files. Point your browser to http://localhost:8787 to see the server in actions.

note: The default file needs to be named "index.htm"

When an HTTP request is parsed, the Request struct splits the request into controller,action,val variables based on the location of text in the requested URL. The format is host:port/controller/action/val.

In MuttonChop these variables are processed with a series of "switch" statements in order to determine what MuttonChop is supposed to do. For example to to set the player volume to 77, the controller is 'player', the action is 'volume' and the val is '77', thus the URL would be host:port/player/volume/77.

Sweet Sauce!

Comments
2012-10-19 Adrian:
very nice, thank you.

I am desperatly trying to write a webserver whith SSL Support, but i have no idea how to solve that.

I woul be _VERY_ grateful for any hints or samples.
2012-10-19 jezra:
Hi Adrian, unfortunately, I have no experience writing an SSL webserver, so you are going to have to slog through some documentation to find your solution.

Ewww, that sound far to much like I just told you to RTFM. sorry buddy.
2012-10-22 Adrian:
alright, but thx anyway!

:-)
Name:
not required
Email:
not required (will not be displayed)
Website:
not required (will link your name to your site)
Comment:
required
Please do not post HTML code or bbcode unless you want it to show up as code in your post. (or if you are a blog spammer, in which case, you probably aren't reading this anyway).
Prove you are human by solving a math problem! I'm sorry, but due to an increase of blog spam, I've had to implement a CAPTCHA.
Problem:
7 minus 4
Answer:
required
subscribe
 
2019
2016
2015
2014
2013
2012
2011
2010
December
November
October
September
August
July
June
May
April
March
February
January
2009
December
November
October
September
August
July
June
May
April
March
February
January
2008