Skip to content

Instantly share code, notes, and snippets.

@shovon
Last active August 28, 2019 00:42
Show Gist options
  • Save shovon/dc51584edcacc07c68366a4cdb60ec7c to your computer and use it in GitHub Desktop.
Save shovon/dc51584edcacc07c68366a4cdb60ec7c to your computer and use it in GitHub Desktop.
A dumb little HTTP server that works on top of TCP.
package main
import (
"bufio"
"errors"
"fmt"
"io"
"log"
"net"
"net/http"
"strconv"
"strings"
)
func trimSpaces(s string) string {
return strings.Trim(s, " \t\r\n")
}
func writeWriter(w io.Writer, b []byte) error {
count, err := w.Write(b)
if err != nil {
return err
}
if count < len(b) {
return errors.New("Failed to write a single byte")
}
return nil
}
func respond(conn net.Conn, status int, header map[string]string, body []byte) error {
conn.Write([]byte(fmt.Sprintf("HTTP/1.1 %d %s\r\n", status, http.StatusText(status))))
for key, val := range header {
conn.Write([]byte(fmt.Sprintf("%s: %s\r\n", key, val)))
}
err := writeWriter(conn, []byte("\r\n"))
if err != nil {
return err
}
err = writeWriter(conn, body)
if err != nil {
return err
}
return conn.Close()
}
func readLine(r *bufio.Reader) (string, error) {
var line string
for {
l, isPrefixed, err := r.ReadLine()
if err != nil {
if err != io.EOF {
return "", err
}
break
}
line += string(l)
if !isPrefixed {
break
}
}
return line, nil
}
// Request the request object.
type Request struct {
Method string
Path string
Headers map[string]string
Body []byte
}
// ResponseWriter for writing purposes
type ResponseWriter func(status int, header map[string]string, body []byte)
// This internally creates a TCP socket, and handles the TCP connections that
// come in.
func listen(addr string, handler func(Request, ResponseWriter)) error {
ln, err := net.Listen("tcp", addr)
if err != nil {
return err
}
// Listener loop that spawns off new threads per connection.
for {
// Synchronous listener for new connection requests.
conn, err := ln.Accept()
if err != nil {
log.Println(err)
}
// Spawn off a new thread to handle the HTTP request.
go func() {
headers := make(map[string]string)
// Scan the headers.
r := bufio.NewReader(conn)
initial, err := readLine(r)
if err != nil {
log.Println(err)
return
}
components := strings.Split(initial, " ")
method, path := components[0], components[1]
for {
line, err := readLine(r)
if err != nil {
conn.Close()
return
}
// This means that the header portion has concluded.
if len(line) <= 0 {
break
}
// Get the header
header := strings.Split(line, ": ")
if len(header) <= 1 {
log.Println("Bad header")
continue
}
key := strings.ToLower(trimSpaces(header[0]))
headers[key] = trimSpaces(header[1])
}
contentLengthStr, ok := headers["content-length"]
if !ok {
respond(conn, http.StatusLengthRequired, map[string]string{}, []byte("Length expected"))
return
}
contentLength, err := strconv.Atoi(contentLengthStr)
bytesRead := 0
body := make([]byte, 0, 256)
for {
buffer := make([]byte, 256)
count, err := r.Read(buffer)
body = append(body, buffer...)
bytesRead += count
if err != nil {
if err != io.EOF {
log.Println("Failed")
}
break
}
if count >= contentLength {
break
}
log.Println(string(buffer[:count]))
}
handler(Request{
Method: method,
Path: path,
Headers: headers,
Body: body,
}, func(status int, header map[string]string, responseBody []byte) {
respond(conn, status, header, responseBody)
})
}()
}
}
func main() {
// Opens a server socket.
log.Println("Server listening on port 8080")
// This is where we handle the request.
panic(listen(":8080", func(r Request, w ResponseWriter) {
w(200, map[string]string{}, r.Body)
}))
}
@shovon
Copy link
Author

shovon commented Aug 28, 2019

There are obviously a bunch of issues with this implementation.

Off the top of my head:

  • Request body is entirely buffered, rather than let the handler decide what to do with the incoming connection stream. I'll probably need to write a special io.Writer for this
  • It does not properly handle the distinction between HTTP/1.0 and HTTP/1.1
    • the implication here is that Content-Length (or Transfer-Encoding) is not required in HTTP/1.0, but it is required in HTTP/1.1. Whether you're using HTTP/1.1 or HTTP/1.0, it does not matter with this server. It will handle both the same
  • the response writer interface does not allow writing directly to the write stream. Instead, it just expects a buffer
  • a bunch of edge-cases are just ignored

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment