Last active
August 28, 2019 00:42
-
-
Save shovon/dc51584edcacc07c68366a4cdb60ec7c to your computer and use it in GitHub Desktop.
A dumb little HTTP server that works on top of TCP.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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) | |
})) | |
} |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
There are obviously a bunch of issues with this implementation.
Off the top of my head:
io.Writer
for thisHTTP/1.0
andHTTP/1.1
Content-Length
(orTransfer-Encoding
) is not required in HTTP/1.0, but it is required inHTTP/1.1
. Whether you're usingHTTP/1.1
orHTTP/1.0
, it does not matter with this server. It will handle both the same