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.