HTTP 1.1 Server

Gavin Whitson, 6/9/2024

Background

this project came around purely from the drive to learn about multiple things. programming servers and networking protocols is something that i have been interested due to my job being involved in servers. i had watched an excellent youtube video by computerphile in which laurence tratt programmed a web server in 25 lines using rust . i was inspired to create my own simple http server and as it was seemingly a simple project, i decided to use this to learn a new programming language, go . i do slightly regret diving head first into this programming language. it would have been hugely beneficial to complete the wonderful tour of go , in which the go dev team provides a run down of the main features of go. this would have helped greatly as i made a couple of pretty basic mistakes throughout this project that slowed me down. otherwise, this was a fantastic project and taught me a large amount about http. it was a great deal of fun working on a project that i could see the result through any normal web browser.

Implementation

once i was able to get the basic 'hello world' program going, working on this project was quite a breeze. go provides a fantastic standard package in 'net' . although this package does come with a http module built in, what fun is that? using this package, it is quite simple to bind to a port at which point you can listen for connections and print anything sent to the screen. starting with this, you can then startup the program and attempt to navigate to your server on your preferred web browser.

package main import ( "fmt" "net" ) func main() { listener, err := net.listen("tcp","localhost:8080") if err != nil { panic(err) } defer listener.close() fmt.println("listening on port 8080") for { conn, err := listener.accept() if err != nil { panic(err) } defer conn.close() go handleconnection(conn) } } func handleconnection(conn net.conn) { buffer := make([]byte, 1024) conn.read(buffer) fmt.printf("%s", buffer) }

the above result is the http 1.1 get request that was generated by your firefox as it attempts to connect to the server. this is an incredible resource as it shows us the very query that we need to respond to. there is one important line for this basic implementation--the very first line. headers are another important part of this request, however as it accepts any header ('*/*'), we dont need to worry about this at the moment.

to handle this request properly, we first need to parse it. to get started, we can take only the first line and break it into the information it carries. it is space delimited so comes with three parts, the request type: 'get', the resource requested: '/', and the protocol with which this request was sent. if the resource requested is '/' as in our example, we can treat it as 'index.html'. we don't actually care which protocol its sent with, we can assume that if the first two parts are formatted correctly, we can respond in kind. if you want to be picky, check that part of the string against whatever protocol you please, and return early if it does not fit your type.

so how do we respond properly? a properly formatted http response follows another simple format. the first line contains the protocol version and status information. the following lines can contain various information depending on what we need, in our simple case we are just putting the correct content-type header value depending on the resource requested. i only care about html and css at the moment, so i am going to assume it is html and switch it to css if the file extension shows it to be a css file.

conttype := "text/html" if res[reslen - 4 : reslen] == ".css" { conttype = "text/css" }

finally we add the line htdocs followed by the contents of the file requested. with everything done properly, you should be able to start a static webpage hosted by your very own server

Future Work

i would love to continue working on this project and adding additional functionality to it. i want to implement the other types of requests and since i have learned quite a lot about go since writing this write-up, there are a large amount of things i would change. i have actually written a project in which i interact with a rest api through go's 'net/http' module using more structured typing for formatting the information sent through requests. i want to implement those ideas into a better type which allows you to add headers and set content for requests and responses through methods. go has a lot of fantastic features for this and revisiting this project i know that i could make it into a more sophisticated solution.

package main import ( "bytes" "fmt" "net" "os" "errors" ) func main() { listener, err := net.listen("tcp","localhost:8080") check(err) defer listener.close() fmt.println("listening on port 8080") for { conn, err := listener.accept() check(err) defer conn.close() go handleconnection(conn) } } func handleconnection(conn net.conn) { buffer := make([]byte, 1024) conn.read(buffer) printbytes(buffer) if string(bytes.trim(buffer,"\x00")) != "" { t, r, _, _ := getrequest(buffer) t, r = bytes.trim(t, "\x00"), bytes.trim(r, "\x00") rtype := string(t) res := string(r) if res[len(res) - 1] == '/' { res += "index.html" } if res[0] == '/' { res = "." + res } reslen := len(res) conttype := "text/html" if res[reslen - 4 : reslen] == ".css" { conttype = "text/css" } fmt.print(res) switch rtype { case "get": fmt.println("serving request") dat, err := os.readfile("site/" + res) check(err) res := "http/1.1 200 ok\r\ncontent-type:" + conttype + "\r\nhtdocs\r\n" res += string(dat) conn.write([]byte(res)) default: conn.write([]byte("http/1.1 200 ok\r\n\r\n")) } } defer conn.close() } func printbytes(buffer []byte) { fmt.printf("%s", buffer[:]) } func getrequest(buffer []byte) ([]byte, []byte, []byte, error) { rtype := make([]byte, 16) res := make([]byte, 32) rhttp := make([]byte, 16) empty := make([]byte, 1024) ind := 0 i := buffer[ind] if string(buffer) == string(empty) { fmt.print("dont reach here") return rtype, res, rhttp, errors.new("empty buffer") } // get type of request for i != byte(' ') { rtype = append(rtype[:], i) ind = ind + 1 i = buffer[ind] } // get resource requested ind = ind + 1 i = buffer[ind] for i != byte(' ') { res = append(res[:], i) ind = ind + 1 i = buffer[ind] } // get http protocol ind = ind + 1 i = buffer[ind] for i != byte('\n') { rhttp = append(rhttp[:], i) ind = ind + 1 i = buffer[ind] } return rtype, res, rhttp, nil } func check(e error) { if e != nil { fmt.println(e) } }