├── 5-http-implementation ├── README.md ├── main.go └── http │ ├── server_test.go │ └── server.go ├── 4-http-outline ├── README.md └── http │ ├── server.go │ └── server_test.go ├── README.md ├── 1-tcp-logger ├── main.go └── README.md ├── 2-tcp-two-way ├── README.md └── main.go ├── LICENSE └── 3-http-mvp ├── main.go └── README.md /5-http-implementation/README.md: -------------------------------------------------------------------------------- 1 | # 5. Implementing a HTTP package 2 | 3 | In this step we will build on the outline we created previously and implement a fairly functional HTTP server. The goal is not to implement the entire HTTP spec but rather to get something somewhat reasonable working. 4 | 5 | -------------------------------------------------------------------------------- /4-http-outline/README.md: -------------------------------------------------------------------------------- 1 | # 4. Outlining a HTTP Package 2 | 3 | So far, we have implemented everything in main. In this step we will outline the bare bones of a `Server` struct which will abstract away the details of implementing the HTTP protocol. 4 | 5 | NOTE: Tests will timeout because the Server is not implemented. 6 | 7 | -------------------------------------------------------------------------------- /5-http-implementation/main.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "io/ioutil" 5 | "log" 6 | "net" 7 | 8 | "github.com/nstogner/learning-http/5-http-implementation/http" 9 | ) 10 | 11 | func main() { 12 | s := http.Server{ 13 | Handler: handler{}, 14 | } 15 | 16 | l, err := net.Listen("tcp", ":7000") 17 | if err != nil { 18 | log.Fatalln("unable to listen:", err) 19 | } 20 | if err := s.Serve(l); err != nil { 21 | log.Fatalln("unable to serve:", err) 22 | } 23 | } 24 | 25 | // handler implements the http.Handler interface. 26 | type handler struct{} 27 | 28 | func (h handler) ServeHTTP(w *http.Response, r *http.Request) { 29 | log.Println("serving http") 30 | 31 | btys, _ := ioutil.ReadAll(r.Body) 32 | log.Printf("read request body: %q\n", string(btys)) 33 | 34 | w.Write([]byte("howdy")) 35 | } 36 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Learning HTTP 2 | 3 | Understanding one step below the abstraction level where you operate is extremely useful. For most programmers who spend a lot of their time developing REST APIs, this level would be HTTP. What better way to learn about how the HyperText Transfer Protocol works than to build a server on top of TCP? 4 | 5 | 1. TCP Logger - Listen for TCP connections and log what is received 6 | 2. TCP Two-Way Communication - Create a protocol for talking back-in-forth over TCP 7 | 3. Minimal HTTP Server - The bare bones of responding to a HTTP request 8 | 4. HTTP Server Outline - Scaffolding out our own HTTP package 9 | 5. HTTP Server Implementation - A "working" HTTP server 10 | 11 | ## Additional Topics 12 | 13 | ### A. Pipelining 14 | 15 | ### B. Chunked Transfer Encoding 16 | 17 | ### C. HTTP 2.0 18 | 19 | -------------------------------------------------------------------------------- /1-tcp-logger/main.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "log" 5 | "net" 6 | ) 7 | 8 | func main() { 9 | l, err := net.Listen("tcp", ":7000") 10 | if err != nil { 11 | log.Fatalln("unable to listen:", err) 12 | } 13 | defer l.Close() 14 | 15 | log.Print("listening") 16 | 17 | for { 18 | // Accept blocks until there is an incoming connection. 19 | conn, err := l.Accept() 20 | if err != nil { 21 | log.Println("unable to accept:", err) 22 | break 23 | } 24 | 25 | serve(conn) 26 | } 27 | } 28 | 29 | // serve manages reading from a connection. 30 | func serve(c net.Conn) { 31 | defer c.Close() 32 | 33 | // Create a buffer of length = 1. 34 | // Try experimenting with different lengths. 35 | buf := make([]byte, 1) 36 | 37 | for { 38 | if _, err := c.Read(buf); err != nil { 39 | log.Println("unable to read from conn:", err) 40 | return 41 | } 42 | 43 | log.Printf("buffer bits: %08b: %q", buf, buf) 44 | } 45 | } 46 | -------------------------------------------------------------------------------- /2-tcp-two-way/README.md: -------------------------------------------------------------------------------- 1 | # 2. Writing to TCP Connections 2 | 3 | ## Bidirectional 4 | 5 | - A TCP connection is bidirectional 6 | - In Go, this translates to our `net.Conn` interface having both `Read` and `Write` methods. (`io.Reader` / `io.Writer` interfaces) 7 | 8 | ## Code 9 | 10 | In this example, we define a simple protocol for talking to our server. 11 | 12 | The client can issue a command by sending a command string plus a CRLF ("\r\n") over the connection. By default we have implemented a single command: "BEEP". Ideally this will cause our computer to beep if all goes well. 13 | 14 | ```sh 15 | go run ./2-tcp-two-way/main.go 16 | ``` 17 | 18 | ```sh 19 | # In another shell 20 | telnet localhost 7000 21 | BEEP 22 | ``` 23 | 24 | The server will ether respond with "REJECTED" or "ACCEPTED" depending on whether it understands the command. 25 | 26 | ## Bonus 27 | 28 | - Add a "CLOSE" command that will close the connection to the client 29 | 30 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2017 Nicholas Stogner 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /3-http-mvp/main.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "bufio" 5 | "log" 6 | "net" 7 | ) 8 | 9 | func main() { 10 | l, err := net.Listen("tcp", ":7000") 11 | if err != nil { 12 | log.Fatalln("unable to listen:", err) 13 | } 14 | defer l.Close() 15 | 16 | log.Print("listening") 17 | 18 | for { 19 | conn, err := l.Accept() 20 | if err != nil { 21 | log.Println("unable to accept", err) 22 | break 23 | } 24 | 25 | serve(conn) 26 | } 27 | } 28 | 29 | // serve manages reading and writing to a connection. 30 | func serve(c net.Conn) { 31 | defer c.Close() 32 | 33 | buf := bufio.NewReader(c) 34 | 35 | // Read request line 36 | // e.g. "GET /abc HTTP/1.1" 37 | ln0, err := buf.ReadString('\n') 38 | if err != nil { 39 | return 40 | } 41 | log.Printf("read request line: %q", ln0) 42 | 43 | // Read headers 44 | // e.g. "Content-Type: application/json" 45 | for { 46 | ln, err := buf.ReadString('\n') 47 | if err != nil { 48 | break 49 | } 50 | 51 | // An empty line with crlf marks the end of the headers 52 | if ln == "\r\n" { 53 | break 54 | } 55 | 56 | // Must be a header if we have reached this point 57 | log.Printf("read request header: %q", ln) 58 | } 59 | 60 | // Ignore the request body for now 61 | 62 | // Write response 63 | c.Write([]byte("HTTP/1.1 200 OK\r\nContent-Length: 0\r\n\r\n")) 64 | } 65 | -------------------------------------------------------------------------------- /2-tcp-two-way/main.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "bufio" 5 | "io" 6 | "log" 7 | "net" 8 | "strings" 9 | ) 10 | 11 | func main() { 12 | l, err := net.Listen("tcp", ":7000") 13 | if err != nil { 14 | log.Fatalln("unable to listen:", err) 15 | } 16 | defer l.Close() 17 | 18 | log.Print("listening") 19 | 20 | for { 21 | // Accept blocks until there is an incoming connection 22 | conn, err := l.Accept() 23 | if err != nil { 24 | log.Println("unable to accept:", err) 25 | break 26 | } 27 | 28 | serve(conn) 29 | } 30 | } 31 | 32 | // serve manages reading and writing to a connection. 33 | func serve(c net.Conn) { 34 | defer c.Close() 35 | 36 | // The bufio Reader provides some nice convenience functions for reading 37 | // up until a particular character is found 38 | buf := bufio.NewReader(c) 39 | 40 | for { 41 | // Read up to and including the next newline character 42 | // (the second byte of a crlf) 43 | ln, err := buf.ReadString('\n') 44 | if err != nil { 45 | log.Println("unable to read from conn:", err) 46 | return 47 | } 48 | 49 | // NOTE: Telnet will send a crlf when you hit enter so we will strip 50 | // that off here 51 | cmd := strings.TrimSuffix(ln, "\r\n") 52 | handle(c, cmd) 53 | } 54 | } 55 | 56 | // handle accepts a Writer so that it can respond to a given parsed command 57 | // string. 58 | func handle(w io.Writer, cmd string) { 59 | switch cmd { 60 | case "BEEP": 61 | log.Print("beep! beep!") 62 | w.Write([]byte("ACCEPTED\r\n")) 63 | default: 64 | log.Printf("invalid command: %q", cmd) 65 | w.Write([]byte("REJECTED\r\n")) 66 | } 67 | } 68 | -------------------------------------------------------------------------------- /4-http-outline/http/server.go: -------------------------------------------------------------------------------- 1 | package http 2 | 3 | import ( 4 | "bufio" 5 | "io" 6 | "net" 7 | ) 8 | 9 | // Handler responds to a HTTP request. 10 | type Handler interface { 11 | // ServeHTTP takes a Response struct rather than a ResponseWriter interface 12 | // like the standard library to keep things simple. 13 | ServeHTTP(*Response, *Request) 14 | } 15 | 16 | // Response is used to construct a HTTP response. 17 | type Response struct { 18 | Status int 19 | Headers map[string]string 20 | 21 | // We know we need a buffer because we need to know what the Content-Length 22 | // header should be before we can write the body to the connection buf bytes.Buffer 23 | } 24 | 25 | func (res *Response) Write(b []byte) (int, error) { 26 | // TODO 27 | return 0, nil 28 | } 29 | 30 | // Request represents a HTTP request sent to a server. 31 | type Request struct { 32 | // Parsed out header fields 33 | Method string 34 | URI string 35 | Proto string 36 | Headers map[string]string 37 | 38 | // A way to read bytes from the body 39 | Body io.Reader 40 | 41 | // Should the connection be terminated after the response is sent? 42 | keepalive bool 43 | } 44 | 45 | // httpConn handles persistent HTTP connections. 46 | type httpConn struct { 47 | netConn net.Conn 48 | handler Handler 49 | } 50 | 51 | // serve reads and responds to one or many HTTP requests off of a single 52 | // connection. 53 | func (hc *httpConn) serve() { 54 | defer hc.netConn.Close() 55 | 56 | buf := bufio.NewReader(hc.netConn) 57 | 58 | for { 59 | req, err := readRequest(buf) 60 | if err != nil { 61 | // TODO: Send bad request 62 | return 63 | } 64 | 65 | res := &Response{} 66 | 67 | hc.handler.ServeHTTP(res, req) 68 | 69 | // TODO: Send response back 70 | 71 | if !req.keepalive { 72 | return 73 | } 74 | } 75 | } 76 | 77 | // Server wraps a Handler and manages a network listener. 78 | type Server struct { 79 | Handler Handler 80 | } 81 | 82 | // Serve accepts incoming HTTP connections and handles them in a new goroutine. 83 | func (s *Server) Serve(l net.Listener) error { 84 | defer l.Close() 85 | 86 | for { 87 | nc, err := l.Accept() 88 | if err != nil { 89 | return err 90 | } 91 | 92 | hc := httpConn{nc, s.Handler} 93 | // Spawn off a goroutine so we can accept other connections 94 | go hc.serve() 95 | } 96 | return nil 97 | } 98 | 99 | func readRequest(r io.Reader) (*Request, error) { 100 | var req Request 101 | 102 | // TODO: Parse request line 103 | 104 | for { 105 | // TODO: Parse headers 106 | } 107 | 108 | // TODO: Assign body reader 109 | 110 | return &req, nil 111 | } 112 | -------------------------------------------------------------------------------- /4-http-outline/http/server_test.go: -------------------------------------------------------------------------------- 1 | package http_test 2 | 3 | import ( 4 | "bytes" 5 | "io/ioutil" 6 | "net" 7 | stdhttp "net/http" 8 | "strconv" 9 | "testing" 10 | 11 | "github.com/nstogner/learning-http/5-http-implementation/http" 12 | ) 13 | 14 | func TestServe(t *testing.T) { 15 | const ( 16 | reqBody = `{"abc":123}` 17 | resBody = `{"xyz":456}` 18 | ) 19 | 20 | // Start a listener on any free port. 21 | l, err := net.Listen("tcp", "") 22 | if err != nil { 23 | t.Fatal("unable to listen:", err) 24 | } 25 | 26 | var th testHandler 27 | th.response.status = 201 28 | th.response.body = []byte(resBody) 29 | server := http.Server{ 30 | Handler: &th, 31 | } 32 | go func() { 33 | if err := server.Serve(l); err != nil { 34 | t.Fatal("unable to serve:", err) 35 | } 36 | }() 37 | 38 | _, port, err := net.SplitHostPort(l.Addr().String()) 39 | if err != nil { 40 | t.Fatal("unable to split host and port:", err) 41 | } 42 | resp, err := stdhttp.Post("http://localhost:"+port, "application/json", bytes.NewReader([]byte(reqBody))) 43 | if err != nil { 44 | t.Fatal("post failed:", err) 45 | } 46 | 47 | // Check that the response set by the testHandler match the response that 48 | // was received. 49 | if th.response.status != resp.StatusCode { 50 | t.Fatalf("expected status code %s, got: %s", th.response.status, resp.StatusCode) 51 | } 52 | 53 | // Check that expected response headers are set. 54 | if len(resp.Header.Get("Date")) == 0 { 55 | t.Fatal("expected header 'Date' to be set") 56 | } 57 | if cl, err := strconv.Atoi(resp.Header.Get("Content-Length")); err == nil { 58 | if exp := len(resBody); cl != exp { 59 | t.Fatalf("expected header 'Content-Length' = %v, got: %v", exp, cl) 60 | } 61 | } else { 62 | t.Fatal("unable to parse 'Content-Length' header:", err) 63 | } 64 | 65 | // Check that the fields that were parsed by the Server match what was 66 | // requested. 67 | if exp := "POST"; th.request.method != exp { 68 | t.Fatalf("expected method %s, got: %s", exp, th.request.method) 69 | } 70 | if string(th.request.body) != reqBody { 71 | t.Fatalf("expected body '%s', got: '%s'", reqBody, string(th.request.body)) 72 | } 73 | } 74 | 75 | // testHandler records parsed request fields and sets predefined response 76 | // fields. 77 | type testHandler struct { 78 | request struct { 79 | method string 80 | body []byte 81 | } 82 | response struct { 83 | status int 84 | body []byte 85 | } 86 | } 87 | 88 | // ServeHTTP satisfies the http.Handler interface. 89 | func (th *testHandler) ServeHTTP(res *http.Response, req *http.Request) { 90 | th.request.method = req.Method 91 | 92 | // Ignore error because if there is an error it will be caught by body 93 | // comparison in test. 94 | th.request.body, _ = ioutil.ReadAll(req.Body) 95 | 96 | res.Status = th.response.status 97 | res.Write(th.response.body) 98 | } 99 | -------------------------------------------------------------------------------- /5-http-implementation/http/server_test.go: -------------------------------------------------------------------------------- 1 | package http_test 2 | 3 | import ( 4 | "bytes" 5 | "io/ioutil" 6 | "net" 7 | stdhttp "net/http" 8 | "strconv" 9 | "testing" 10 | 11 | "github.com/nstogner/learning-http/5-http-implementation/http" 12 | ) 13 | 14 | func TestServe(t *testing.T) { 15 | const ( 16 | reqBody = `{"abc":123}` 17 | resBody = `{"xyz":456}` 18 | ) 19 | 20 | // Start a listener on any free port. 21 | l, err := net.Listen("tcp", "") 22 | if err != nil { 23 | t.Fatal("unable to listen:", err) 24 | } 25 | 26 | var th testHandler 27 | th.response.status = 201 28 | th.response.body = []byte(resBody) 29 | server := http.Server{ 30 | Handler: &th, 31 | } 32 | go func() { 33 | if err := server.Serve(l); err != nil { 34 | t.Fatal("unable to serve:", err) 35 | } 36 | }() 37 | 38 | _, port, err := net.SplitHostPort(l.Addr().String()) 39 | if err != nil { 40 | t.Fatal("unable to split host and port:", err) 41 | } 42 | resp, err := stdhttp.Post("http://localhost:"+port, "application/json", bytes.NewReader([]byte(reqBody))) 43 | if err != nil { 44 | t.Fatal("post failed:", err) 45 | } 46 | 47 | // Check that the response set by the testHandler match the response that 48 | // was received. 49 | if th.response.status != resp.StatusCode { 50 | t.Fatalf("expected status code %s, got: %s", th.response.status, resp.StatusCode) 51 | } 52 | 53 | // Check that expected response headers are set. 54 | if len(resp.Header.Get("Date")) == 0 { 55 | t.Fatal("expected header 'Date' to be set") 56 | } 57 | if cl, err := strconv.Atoi(resp.Header.Get("Content-Length")); err == nil { 58 | if exp := len(resBody); cl != exp { 59 | t.Fatalf("expected header 'Content-Length' = %v, got: %v", exp, cl) 60 | } 61 | } else { 62 | t.Fatal("unable to parse 'Content-Length' header:", err) 63 | } 64 | 65 | // Check that the fields that were parsed by the Server match what was 66 | // requested. 67 | if exp := "POST"; th.request.method != exp { 68 | t.Fatalf("expected method %s, got: %s", exp, th.request.method) 69 | } 70 | if string(th.request.body) != reqBody { 71 | t.Fatalf("expected body '%s', got: '%s'", reqBody, string(th.request.body)) 72 | } 73 | } 74 | 75 | // testHandler records parsed request fields and sets predefined response 76 | // fields. 77 | type testHandler struct { 78 | request struct { 79 | method string 80 | body []byte 81 | } 82 | response struct { 83 | status int 84 | body []byte 85 | } 86 | } 87 | 88 | // ServeHTTP satisfies the http.Handler interface. 89 | func (th *testHandler) ServeHTTP(res *http.Response, req *http.Request) { 90 | th.request.method = req.Method 91 | 92 | // Ignore error because if there is an error it will be caught by body 93 | // comparison in test. 94 | th.request.body, _ = ioutil.ReadAll(req.Body) 95 | 96 | res.Status = th.response.status 97 | res.Write(th.response.body) 98 | } 99 | -------------------------------------------------------------------------------- /1-tcp-logger/README.md: -------------------------------------------------------------------------------- 1 | # 1. Reading from a TCP connection 2 | 3 | ## Networking Layers 4 | 5 | - OSI Model (7-layer) (Open Systems Interconnection) 6 | - TCP/IP Model (4-layer) 7 | - Also called: Internet protocol suite 8 | - Also called: DoD (Department of Defense) Model (Development was funded by US government: DARPA) 9 | 10 | ``` 11 | | OSI Model | TCP/IP Model | Protocol | 12 | |-----------------------|-------------------------|----------| 13 | | 7. Application Layer | | HTTP | 14 | | 6. Presentation Layer | 4. Application Layer | | 15 | | 5. Session Layer | | | 16 | |-----------------------|-------------------------|----------| 17 | | 4. Transport Layer | 3. Transport Layer | TCP | 18 | |-----------------------|-------------------------|----------| 19 | | 3. Network Layer | 2. Internet Layer | | 20 | |-----------------------|-------------------------|----------| 21 | | 2. Data Link Layer | | | 22 | | 1. Physical Layer | 1. Network Access Layer | | 23 | |-----------------------|-------------------------|----------| 24 | ``` 25 | 26 | NOTES: 27 | 28 | - Usually when people refer to a layer by number, they are using the OSI model 29 | - Usually TCP/IP layers are referred to by name 30 | - Some protocols dont neatly fit into a given layer (i.e. TCP in layer 4, but it also deals with layer 5 "Session Layer") 31 | 32 | **We are going to build out a library that implements the HTTP protocol using the abstractions provided by TCP.** 33 | 34 | ## What is TCP? 35 | 36 | - Stands for Transmission Control Protocol 37 | - Has the concept of a connection between a client and a server 38 | - Handles control flow (it tries not to send more data than the receiver can handle) 39 | - Handles congestion control (using a slow-start algorithm) 40 | - Allows for the reliable transfer of data (bytes of information) across the network 41 | - It protects against data corruption by using checksums of the data that gets sent 42 | 43 | Side Note: How does it relate to UDP? 44 | 45 | - UDP stands for User Datagram Protocol 46 | - UDP also checksums data to provide data integrity 47 | - UDP does not redeliver dropped packets 48 | 49 | ## Tools 50 | 51 | What is `telnet`? 52 | 53 | - When referring to `telnet` here, we are talking about the CLI tool 54 | - There is a `telnet` text-based protocol 55 | - We will use `telnet` to make TCP connections and send text over them 56 | - When we press `telnet` will send what we typed followed by two bytes: `\r\n` 57 | 58 | ## Code 59 | 60 | This simple server listens for TCP connections and logs anything that is sent to it before closing the connection to the client. 61 | 62 | ```sh 63 | go run ./1-tcp-logger/main.go 64 | ``` 65 | 66 | ```sh 67 | # In another shell 68 | telnet localhost 7000 69 | Hey there server! 70 | ``` 71 | 72 | Note what is logged in the server. The `telnet` tool sends a carriage return `\r` and a newline feed `\n` (usually referred to as CRLF online). 73 | -------------------------------------------------------------------------------- /3-http-mvp/README.md: -------------------------------------------------------------------------------- 1 | # 3. Minimal HTTP Server 2 | 3 | ## What is a protocol anyways? 4 | 5 | - In terms of networking, a protocol is just a set of rules that endpoints use to communicate 6 | - These rules specify the manner in which data is sent and formatted 7 | 8 | ## HTTP Protocol 9 | 10 | - Is a request-response protocol 11 | 12 | ### Versions 13 | 14 | - There are multiple versions of the HTTP protocol 15 | - HTTP 1.0 & 1.1 are text based (ASCII) 16 | - HTTP 2.0 is a binary format (We will not touch on this here) 17 | - We will focus on 1.X 18 | - The structure of an HTTP 1.0 request and response is defined by [RFC 1945](https://tools.ietf.org/html/rfc1945) 19 | - [Wikipedia](https://en.wikipedia.org/wiki/Hypertext_Transfer_Protocol) has a good example 20 | 21 | ### Request Structure 22 | 23 | - A request consists of multiple lines of text 24 | - Each line is seperated by a Carraige Return byte (CR = `\r`) followed by a Line Feed byte (LF = `\n`) 25 | - CR & LF bytes are called "control characters" (They are not intended to be printed) 26 | 27 | | Line | Example | Format | 28 | |-----------------|-------------------------|------------------------------| 29 | | Request Line | `GET /users HTTP/1.1` | ` ` | 30 | | Header(s) | `Host: www.example.com` | `: ` | 31 | | Empty Line | `` | N/A | 32 | | Body (Optional) | `Hello from client!` | N/A | 33 | 34 | #### Request Data 35 | 36 | - Method: Can be thought of as a verb that indicates what action should be taked on a resource 37 | - Path: Identified a resource within the server which to act on 38 | - Protocol: Used to specifying the version `HTTP/1.0`, `HTTP/1.1` 39 | - Headers: Key-value pairs which specify metadata about the request 40 | 41 | #### Parsing Logic 42 | 43 | With a line determined by: everything read up to the CRLF (`\r\n`)... 44 | 45 | 1. Split the first line of the request on spaces: 46 | 47 | ``` 48 | split[0] = Method 49 | split[1] = Path 50 | split[2] = Protocol 51 | ``` 52 | 53 | 2. For each line following (that isnt empty), split on the first `:` (max length of 2): 54 | 55 | ``` 56 | split[0] = Header key 57 | split[1] = Header value 58 | ``` 59 | 60 | Note: HTTP allows for duplicates of the same header keys. This is why Go stores headers as: `map[string][]string` rather than `map[string]string`. 61 | 62 | 3. When a empty line is encountered, we know we have reached the end of the headers. 63 | 64 | 4. Body: TODO 65 | 66 | ### Response Structure 67 | 68 | | Line | Example | Format | 69 | |-----------------|---------------------------------------|------------------------------------------| 70 | | Response Line | `HTTP/1.1 200 OK` | ` ` | 71 | | Header(s) | `Date: Mon, 23 May 2005 22:38:34 GMT` | `: ` | 72 | | Empty Line | `` | N/A | 73 | | Body (Optional) | `Hello from server!` | N/A | 74 | 75 | ## Code 76 | 77 | We will define a "minimal HTTP server" as a server that can successfully respond to a simple curl GET request. We will start out by observing what happens when we issue this request to our TCP logger server: 78 | 79 | ```sh 80 | go run ./1-tcp-logger/main.go 81 | ``` 82 | 83 | ```sh 84 | # In another shell 85 | curl localhost:7000/abc -v 86 | ``` 87 | 88 | We can observe that request header lines are seperated by crlf's and the header ends in two back-to-back crlf's. Hint: response headers are the same, with the exception of the first line. An example response header line: `HTTP/1.1 200 OK`. 89 | 90 | ## Bonus 91 | 92 | - Parse the request line (the first line) to extract the HTTP method, URI, and protocol version. 93 | 94 | -------------------------------------------------------------------------------- /5-http-implementation/http/server.go: -------------------------------------------------------------------------------- 1 | package http 2 | 3 | import ( 4 | "bufio" 5 | "bytes" 6 | "fmt" 7 | "io" 8 | "net" 9 | "strconv" 10 | "strings" 11 | "time" 12 | ) 13 | 14 | const ( 15 | http10 = "HTTP/1.0" 16 | http11 = "HTTP/1.1" 17 | ) 18 | 19 | // statusTitles map HTTP status codes to their titles. This is handy for 20 | // sending the response header. 21 | var statusTitles = map[int]string{ 22 | 200: "OK", 23 | 201: "Created", 24 | 202: "Accepted", 25 | 203: "Non-Authoritative Information", 26 | 204: "No Content", 27 | // TODO: More status codes 28 | } 29 | 30 | // Handler responds to a HTTP request. 31 | type Handler interface { 32 | // ServeHTTP takes a Response struct rather than a ResponseWriter interface 33 | // like the standard library to keep things simple. 34 | ServeHTTP(*Response, *Request) 35 | } 36 | 37 | // Response is used to construct a HTTP response. 38 | type Response struct { 39 | Status int 40 | Headers map[string]string 41 | 42 | proto string 43 | buf bytes.Buffer 44 | } 45 | 46 | // Write writes data to a buffer which is later flushed to the network 47 | // connection. 48 | func (res *Response) Write(b []byte) (int, error) { 49 | return res.buf.Write(b) 50 | } 51 | 52 | // writeTo writes an HTTP response with headers and buffered body to a writer. 53 | func (res *Response) writeTo(w io.Writer) error { 54 | if err := res.writeHeadersTo(w); err != nil { 55 | return err 56 | } 57 | 58 | if _, err := res.buf.WriteTo(w); err != nil { 59 | return err 60 | } 61 | 62 | return nil 63 | } 64 | 65 | // writeHeadersTo writes HTTP headers to a writer. 66 | func (res *Response) writeHeadersTo(w io.Writer) error { 67 | statusText, ok := statusTitles[res.Status] 68 | if !ok { 69 | return fmt.Errorf("unsupported status code: %v", res.Status) 70 | } 71 | 72 | res.Headers["Date"] = time.Now().UTC().Format("Mon, 02 Jan 2006 15:04:05 GMT") 73 | res.Headers["Content-Length"] = strconv.Itoa(res.buf.Len()) 74 | 75 | // https://www.w3.org/Protocols/rfc2616/rfc2616-sec6.html 76 | headers := fmt.Sprintf("%s %v %s\r\n", res.proto, res.Status, statusText) 77 | for k, v := range res.Headers { 78 | headers += fmt.Sprintf("%s: %s\r\n", k, v) 79 | } 80 | headers += "\r\n" 81 | 82 | if _, err := w.Write([]byte(headers)); err != nil { 83 | return err 84 | } 85 | 86 | return nil 87 | } 88 | 89 | // Request represents a HTTP request sent to a server. 90 | type Request struct { 91 | Method string 92 | URI string 93 | Proto string 94 | Headers map[string]string 95 | 96 | Body io.Reader 97 | } 98 | 99 | // parseConnection determines whether a connection should be kept alive and 100 | // whether the connection header should be echoed in the response. 101 | func (req *Request) parseConnection() (bool, bool) { 102 | conn := strings.ToLower(req.Headers["connection"]) 103 | 104 | switch req.Proto { 105 | case http10: 106 | if conn == "keep-alive" { 107 | return true, true 108 | } 109 | case http11: 110 | if conn == "close" { 111 | return false, true 112 | } 113 | } 114 | 115 | return false, false 116 | } 117 | 118 | // httpConn handles persistent HTTP connections. 119 | type httpConn struct { 120 | netConn net.Conn 121 | handler Handler 122 | } 123 | 124 | // serve reads and responds to one or many HTTP requests off of a single 125 | // connection. 126 | func (hc *httpConn) serve() { 127 | defer hc.netConn.Close() 128 | 129 | buf := bufio.NewReader(hc.netConn) 130 | 131 | for { 132 | req, err := readRequest(buf) 133 | if err != nil { 134 | const bad = "HTTP/1.1 400 Bad Request\r\nConnection: close\r\n\r\n" 135 | hc.netConn.Write([]byte(bad)) 136 | return 137 | } 138 | 139 | res := Response{ 140 | Status: 200, 141 | Headers: make(map[string]string), 142 | proto: req.Proto, 143 | } 144 | 145 | // Determine if connection should be closed after request. 146 | keepalive, echo := req.parseConnection() 147 | if echo { 148 | res.Headers["Connection"] = req.Headers["connection"] 149 | } 150 | 151 | hc.handler.ServeHTTP(&res, req) 152 | 153 | if err := res.writeTo(hc.netConn); err != nil { 154 | return 155 | } 156 | 157 | if !keepalive { 158 | return 159 | } 160 | } 161 | } 162 | 163 | // Server wraps a Handler and manages a network listener. 164 | type Server struct { 165 | Handler Handler 166 | } 167 | 168 | // Serve accepts incoming HTTP connections and handles them in a new goroutine. 169 | func (s *Server) Serve(l net.Listener) error { 170 | defer l.Close() 171 | 172 | for { 173 | nc, err := l.Accept() 174 | if err != nil { 175 | return err 176 | } 177 | 178 | hc := httpConn{nc, s.Handler} 179 | 180 | // Spawn off a goroutine so we can accept other connections. 181 | go hc.serve() 182 | } 183 | } 184 | 185 | // readRequest generates a Request object by parsing text from a bufio.Reader. 186 | func readRequest(buf *bufio.Reader) (*Request, error) { 187 | req := Request{ 188 | Headers: make(map[string]string), 189 | } 190 | 191 | // Read the HTTP request line (first line). 192 | if ln0, err := readHTTPLine(buf); err == nil { 193 | var ok bool 194 | if req.Method, req.URI, req.Proto, ok = parseRequestLine(ln0); !ok { 195 | return nil, fmt.Errorf("malformed request line: %q", ln0) 196 | } 197 | } 198 | 199 | // Read each subsequent header. 200 | for { 201 | ln, err := readHTTPLine(buf) 202 | if err != nil { 203 | return nil, err 204 | } 205 | 206 | if len(ln) == 0 { 207 | break 208 | } 209 | 210 | if key, val, ok := parseHeaderLine(ln); ok { 211 | req.Headers[key] = val 212 | } 213 | } 214 | 215 | // Limit the body to the number of bytes specified by Content-Length. 216 | var cl int64 217 | if str, ok := req.Headers["content-length"]; ok { 218 | var err error 219 | if cl, err = strconv.ParseInt(str, 10, 64); err != nil { 220 | return nil, err 221 | } 222 | } 223 | req.Body = &io.LimitedReader{R: buf, N: cl} 224 | 225 | return &req, nil 226 | } 227 | 228 | // parseRequestLine attempts to parse the initial line of an HTTP request. 229 | func parseRequestLine(ln string) (method, uri, proto string, ok bool) { 230 | s := strings.Split(ln, " ") 231 | if len(s) != 3 { 232 | return 233 | } 234 | 235 | return s[0], s[1], s[2], true 236 | } 237 | 238 | // parseHeaderLine attempts to parse a standard HTTP header, e.g. 239 | // "Content-Type: application/json". 240 | func parseHeaderLine(ln string) (key, val string, ok bool) { 241 | s := strings.SplitN(ln, ":", 2) 242 | if len(s) != 2 { 243 | return 244 | } 245 | 246 | return strings.ToLower(s[0]), strings.TrimSpace(s[1]), true 247 | } 248 | 249 | // readHTTPLine reads up to a newline feed and strips off the trailing crlf. 250 | func readHTTPLine(buf *bufio.Reader) (string, error) { 251 | ln, err := buf.ReadString('\n') 252 | if err != nil { 253 | return "", err 254 | } 255 | 256 | return strings.TrimSuffix(ln, "\r\n"), nil 257 | } 258 | --------------------------------------------------------------------------------