Just An Application

June 27, 2013

Programming With Rust — Part Seventeen: The Rest Of httpd v0.5

1.0 Handling A Connection

The handleConnection function now looks like this

    fn handleConnection(socket: TcpSocket)
    {	    
        let     socketBuf = tcp::socket_buf(socket);
        let mut buffer    = RequestBuffer::new(socketBuf);
        let mut writer    = ResponseWriter::new(socketBuf);

        let     request   = Request::read(&mut buffer);
        let     response  = handleRequest(&request);
        
        response.write(&mut writer);
    }

This is both sub-optimal and incorrect and results in the connection being closed after a single Request but it will do for now.

2.0 Handling A Request

The handleRequest function currently does nothing more than print out the Request and return a ‘not found’ Response.

    fn handleRequest(request: &Request) -> Response
    {
        io::println(fmt!("request == %?", request));
    
        ResponseBuilder::notFound()
    }

3.0 Testing

Connecting with a web browser is OK as a basic ‘smoke test’ but we are now at the stage where we need some actual tests.

While it is tempting to write the tests in Rust as well by extending the server side code to support HTTP clients, when writing
anything involving protocols it is always a good idea to test against a completely independent implementation if possible.

3.1 The Test Code

Below is some very simple test code written in Ruby.

    require 'net/http'

    class MethodTests

        def initialize(host, port)
    
            @host = host
            @port = port
        
        end
        
        def run()
    
            method_tests.each { |method| self.method(method).call() }

        end
    
        def connect()

            begin
         
                Net::HTTP.start(
                              @host, @port + 2, 
                              @host, @port, 
                              'sjL', 'sjL', { :use_ssl => true } ) do |http|
            
                    http.get('/connect_test')

                end
         
            rescue Net::HTTPServerException => hse
         
                puts 'connect: ' + hse.to_s

            end

        end

        def delete

            http_method('delete')

        end
    
        def get

            http_method('get')

        end
    
        def get_proxy
    
            begin
         
                Net::HTTP.start(@host, @port + 2, @host, @port) do |http|
            
                    res = http.get('/absolute_uri_test')

                    puts 'get_proxy: ' + res.code

                end
         
            rescue Net::HTTPServerException => hse
         
                puts 'get_proxy: ' + hse.to_s

            end
    
        end
    
        def head

            http_method('head')
        end
    
        def options

            http_method('options', '*')

        end

        def post
        
            http_method('post', '/post_test', '')

        end

        def put

            http_method('put', '/put_test', '')

        end


        def trace
    
            http_method('trace')
        
        end
    
        private

            def method_tests
        
                ['connect', 'delete', 'get', 'get_proxy', 'head', 'options', 'post', 'put', 'trace']

            end
        
            def http_method(name, *args)
        
                res = Net::HTTP.start(@host, @port) do |http|
    
                    case args.length
                
                        when 1
                    
                            http.method(name).call(args[0])
                       
                        when 2
                    
                            http.method(name).call(args[0], args[1])
                       
                        else
                      
                            http.method(name).call('/' + name + '_test')
                        
                    end

                end
                puts name + ': ' + res.code

            end
        
    end


    MethodTests.new('127.0.0.1', 3534).run()


3.2 The Tests

Between them the tests test all the supported methods and request URI formats.

The only test methods that are possibly not self-explanatory are connect and get_proxy

3.2.1 connect

The connect test method creates an HTTP client that will attampt to use the actual HTTP server at @host:@port as a proxy to access another non-existent HTTP server at @host:(@port + 2) using a secure connection.

The call to the HTTP client’s get method causes it to attempt to create an SSL connection which tunnels through the proxy using the HTTP CONNECT method, which is what we want.

3.2.2 get_proxy

Like the connect test method, the get_proxy test method creates an HTTP client that will attampt to use the actual HTTP server at @host:@port as a proxy to access another non-existent HTTP server at @host:(@port + 2) but in this case using an ordinary non-secure connection.

The call to the HTTP client’s get method causes it to send an HTTP Request with an Absolute URI as the Request URI.

4.0 Running The Tests

4.1 Test Output

When run the test code outputs the following

connect: 404 "Not Found"
delete: 404
get: 404
get_proxy: 404
head: 404
options: 404
post: 404
put: 404
trace: 404

4.2 Server Output

When the test code is run httpd outputs the following (heavily edited for readability)

./httpd
on_establish_callback({x: {data: (0x100709600 as *())}})
new_connection_callback(NewTcpConn((0x101810400 as *())), {x: {data: (0x100709600 as *())}})
accept succeeded
request == &{method: CONNECT, \
             uri: Authority(~"127.0.0.1:3536"), \
             headers: {headers: ~[{key: ~"Host", value: ~"127.0.0.1:3536"}, \
                                  {key: ~"Proxy-Authorization", value: ~"Basic c2pMOnNqTA=="}]}}
new_connection_callback(NewTcpConn((0x101810400 as *())), {x: {data: (0x100709600 as *())}})
accept succeeded
request == &{method: DELETE, \
             uri: AbsolutePath(~"/delete_test"), \
             headers: {headers: ~[{key: ~"Depth", value: ~"Infinity"}, {key: ~"Accept", value: ~"*/*"}, \
                                  {key: ~"User-Agent", value: ~"Ruby"}, {key: ~"Host", value: ~"127.0.0.1:3534"}]}}
new_connection_callback(NewTcpConn((0x101810400 as *())), {x: {data: (0x100709600 as *())}})
accept succeeded
request == &{method: GET, \
             uri: AbsolutePath(~"/get_test"), \
             headers: {headers: ~[{key: ~"Accept-Encoding", value: ~"gzip;q=1.0,deflate;q=0.6,identity;q=0.3"}, \
                                  {key: ~"Accept", value: ~"*/*"}, {key: ~"User-Agent", value: ~"Ruby"}, \
                                  {key: ~"Host", value: ~"127.0.0.1:3534"}]}}
new_connection_callback(NewTcpConn((0x101810400 as *())), {x: {data: (0x100709600 as *())}})
accept succeeded
request == &{method: GET, \
             uri: AbsoluteURI({scheme: ~"http", user: None, host: ~"127.0.0.1", port: Some(~"3536"), \
             path: ~"/absolute_uri_test", query: ~[], fragment: None}), \
             headers: {headers: ~[{key: ~"Accept-Encoding", value: ~"gzip;q=1.0,deflate;q=0.6,identity;q=0.3"}, \
                                  {key: ~"Accept", value: ~"*/*"}, {key: ~"User-Agent", value: ~"Ruby"},\
                                  {key: ~"Host", value: ~"127.0.0.1:3536"}]}}
new_connection_callback(NewTcpConn((0x101810400 as *())), {x: {data: (0x100709600 as *())}})
accept succeeded
request == &{method: HEAD, \
             uri: AbsolutePath(~"/head_test"), \
             headers: {headers: ~[{key: ~"Accept", value: ~"*/*"}, {key: ~"User-Agent", value: ~"Ruby"}, \
                                  {key: ~"Host", value: ~"127.0.0.1:3534"}]}}
new_connection_callback(NewTcpConn((0x101810400 as *())), {x: {data: (0x100709600 as *())}})
accept succeeded
request == &{method: OPTIONS, \
             uri: Wildcard, \
             headers: {headers: ~[{key: ~"Accept", value: ~"*/*"}, {key: ~"User-Agent", value: ~"Ruby"}, \
                                  {key: ~"Host", value: ~"127.0.0.1:3534"}]}}
new_connection_callback(NewTcpConn((0x101810400 as *())), {x: {data: (0x100709600 as *())}})
accept succeeded
request == &{method: POST, \
             uri: AbsolutePath(~"/post_test"), \
             headers: {headers: ~[{key: ~"Accept", value: ~"*/*"}, {key: ~"User-Agent", value: ~"Ruby"}, \
                                  {key: ~"Host", value: ~"127.0.0.1:3534"}, {key: ~"Content-Length", value: ~"0"}, \
                                  {key: ~"Content-Type", value: ~"application/x-www-form-urlencoded"}]}}
new_connection_callback(NewTcpConn((0x101810400 as *())), {x: {data: (0x100709600 as *())}})
accept succeeded
request == &{method: PUT, 
             uri: AbsolutePath(~"/put_test"), \
             headers: {headers: ~[{key: ~"Accept", value: ~"*/*"}, {key: ~"User-Agent", value: ~"Ruby"}, \
                                  {key: ~"Host", value: ~"127.0.0.1:3534"}, {key: ~"Content-Length", value: ~"0"}, \
                                  {key: ~"Content-Type", value: ~"application/x-www-form-urlencoded"}]}}
new_connection_callback(NewTcpConn((0x101810400 as *())), {x: {data: (0x100709600 as *())}})
accept succeeded
request == &{method: TRACE, \
             uri: AbsolutePath(~"/trace_test"), \
             headers: {headers: ~[{key: ~"Accept", value: ~"*/*"}, {key: ~"User-Agent", value: ~"Ruby"},\
                                  {key: ~"Host", value: ~"127.0.0.1:3534"}]}}

5.0 Source Files

5.1 httpd.rc

// httpd.rc

// v0.5

extern mod std;

mod HTTP;
mod buffer;
mod headers;
mod request;
mod response;
mod server;
mod writer;

static PORT: uint = 3534;

static IPV4_LOOPBACK: &'static str = "127.0.0.1";

fn main()
{	
    server::run(IPV4_LOOPBACK, PORT);
}

5.2 server.rs

// server.rs

// part of httpd v0.5

use core::comm::SharedChan;

use core::option::Option;

use core::task;

use std::net::ip;
use std::net::tcp;
use std::net::tcp::TcpErrData;
use std::net::tcp::TcpNewConnection;
use std::net::tcp::TcpSocket;

use std::sync::Mutex;

use std::uv_iotask;


use buffer::RequestBuffer;

use request::Request;

use response::Response;
use response::ResponseBuilder;

use writer::ResponseWriter;

//

static BACKLOG: uint = 5;


fn on_establish_callback(chan: SharedChan<Option>)
{
    io::println(fmt!("on_establish_callback(%?)", chan));
}

fn new_connection_callback(newConn :TcpNewConnection, chan: SharedChan<Option>)
{
    io::println(fmt!("new_connection_callback(%?, %?)", newConn, chan));
	
    let mx = Mutex();
	
    do mx.lock_cond
        |cv|
        {
            let mxc = ~mx.clone();

            do task::spawn 
            {
                match tcp::accept(newConn)
                {
                    Ok(socket) => 
                    {
                        io::println("accept succeeded");
                        do mxc.lock_cond
                            |cv|
                            {
                                cv.signal();
                            }
                        handleConnection(socket);
				                
                    },
                    Err(error) => 
                    {
                        io::println(fmt!("accept failed: %?", error));
                        do mxc.lock_cond
                            |cv|
                            {
                                cv.signal();
                            }
                    }
                }
            }
            cv.wait();
        }
}

fn handleConnection(socket: TcpSocket)
{	    
    let     socketBuf = tcp::socket_buf(socket);
    let mut buffer    = RequestBuffer::new(socketBuf);
    let mut writer    = ResponseWriter::new(socketBuf);
    
    let     request   = Request::read(&mut buffer);
    let     response  = handleRequest(&request);
    
    response.write(&mut writer);
}

//


fn handleRequest(request: &Request) -> Response
{
    io::println(fmt!("request == %?", request));
    
    ResponseBuilder::notFound()
}


//

pub fn run(address: &str, port: uint)
{	
    tcp::listen(
        ip::v4::parse_addr(address),
        port,
        BACKLOG,
        &uv_iotask::spawn_iotask(task::task()),
        on_establish_callback,
        new_connection_callback);
}

Copyright (c) 2013 By Simon Lewis. All Rights Reserved.

Unauthorized use and/or duplication of this material without express and written permission from this blog’s author and owner Simon Lewis is strictly prohibited.

Excerpts and links may be used, provided that full and clear credit is given to Simon Lewis and justanapplication.wordpress.com with appropriate and specific direction to the original content.

Create a free website or blog at WordPress.com.

%d bloggers like this: