commit f21c9cf9198c0665a462798e121552acd2359530 Author: sander Date: Sat Apr 4 21:27:36 2026 +0200 Initial commit diff --git a/assets/vim.mp4 b/assets/vim.mp4 new file mode 100644 index 0000000..c53368e Binary files /dev/null and b/assets/vim.mp4 differ diff --git a/cmd/httpserver/main.go b/cmd/httpserver/main.go new file mode 100644 index 0000000..c3614a4 --- /dev/null +++ b/cmd/httpserver/main.go @@ -0,0 +1,145 @@ +package main + +import ( + "crypto/sha256" + "fmt" + "net/http" + "log" + "os" + "os/signal" + "strings" + "syscall" + + "httpfromtcp/internal/headers" + "httpfromtcp/internal/server" + "httpfromtcp/internal/request" + "httpfromtcp/internal/response" +) + +const port = 42069 + +func respond200() []byte{ + return []byte(` + + + 200 OK + + +

Success!

+

Your request was an absolute banger.

+ + + `) +} + +func respond400() []byte{ + return []byte(` + + + 400 Bad Request + + +

Bad Request

+

Your request honestly kinda sucked.

+ + + `) +} + +func respond500() []byte{ + return []byte(` + + + 500 Internal Server Error + + +

Internal Server Error

+

Okay, you know what? This one is on me.

+ + + `) +} + +func toStr(bytes []byte) string{ + out := "" + for _,b := range bytes { + out += fmt.Sprintf("%02x",b) + } + return out +} + +func main() { + s, err := server.Serve(port, func(w *response.Writer, req *request.Request){ + h := response.GetDefaultHeaders(0) + body := respond200() + status := response.StatusOK + + if req.RequestLine.RequestTarget == "/yourproblem"{ + body = respond400() + status = response.StatusBadRequest + }else if req.RequestLine.RequestTarget == "/myproblem"{ + body = respond500() + status = response.StatusInternalServerError + }else if req.RequestLine.RequestTarget == "/video"{ + f, _ := os.ReadFile("assets/vim.mp4") + h.Replace("content-type","video/mp4") + h.Replace("content-length", fmt.Sprintf("%d",len(f))) + w.WriteStatusLine(response.StatusOK) + w.WriteHeaders(*h) + w.WriteBody(f) + }else if strings.HasPrefix(req.RequestLine.RequestTarget, "/httpbin/"){ + target := req.RequestLine.RequestTarget + + res,err := http.Get("https://httpbin.org/" +target[len("/httpbin/"):]) + if err != nil{ + body = respond500() + status = response.StatusInternalServerError + }else { + w.WriteStatusLine(response.StatusOK) + h.Delete("Content-length") + h.Set("Transfer-Encoding","chunked") + h.Replace("Content-Type","text/plain") + h.Set("Trailer","X-Content-SHA256") + h.Set("Trailer","X-Content-Length") + w.WriteHeaders(*h) + + fullBody := []byte{} + for { + data := make([]byte,32) + n,err := res.Body.Read(data) + if err != nil { + break + } + fullBody = append(fullBody, data[:n]...) + w.WriteBody([]byte(fmt.Sprintf("%x\r\n",n))) + w.WriteBody(data[:n]) + w.WriteBody([]byte("\r\n")) + } + w.WriteBody([]byte("0\r\n")) + trailers := headers.NewHeaders() + out := sha256.Sum256(fullBody) + trailers.Set("X-Content-SHA256",toStr(out[:])) + trailers.Set("X-Content-Length",fmt.Sprintf("%d",len(fullBody))) + w.WriteHeaders(*trailers) + return + } + } + + h.Replace("Content-length",fmt.Sprintf("%d",len(body))) + h.Replace("Content-Type","text/html") + w.WriteStatusLine(status) + w.WriteHeaders(*h) + w.WriteBody(body) + }) + + if err != nil { + log.Fatalf("Error starting server: %v", err) + } + defer s.Close() + log.Println("Server started on port", port) + + sigChan := make(chan os.Signal, 1) + signal.Notify(sigChan, syscall.SIGINT, syscall.SIGTERM) + <-sigChan + log.Println("Server gracefully stopped") +} diff --git a/cmd/tcplistener/main.go b/cmd/tcplistener/main.go new file mode 100644 index 0000000..264e5e4 --- /dev/null +++ b/cmd/tcplistener/main.go @@ -0,0 +1,41 @@ +package main + +import ( + "fmt" + "log" + "net" + + "httpfromtcp/internal/request" +) + +func main() { + listener, err := net.Listen("tcp", ":42069") + if err != nil { + fmt.Println("Couldn't open messages.txt:", err) + return + } + + for { + conn, err := listener.Accept() + if err != nil { + log.Fatal("error", "error", err) + } + + r, err := request.RequestFromReader(conn) + if err != nil { + log.Fatal("error", "error", err) + } + + fmt.Printf("Request line: \n") + fmt.Printf("- Method: %s\n", r.RequestLine.Method) + fmt.Printf("- Target: %s\n", r.RequestLine.RequestTarget) + fmt.Printf("- Version: %s\n", r.RequestLine.HttpVersion) + + fmt.Printf("Headers:\n") + r.Headers.ForEach(func(n, v string) { + fmt.Printf("- %s: %s\n", n, v) + }) + fmt.Printf("Body:\n") + fmt.Printf("%s\n", r.Body) + } +} diff --git a/cmd/udpsender/main.go b/cmd/udpsender/main.go new file mode 100644 index 0000000..823ffa4 --- /dev/null +++ b/cmd/udpsender/main.go @@ -0,0 +1,63 @@ +package udpsender + +import ( + "bytes" + "fmt" + "io" +) + +func getLinesChannel(f io.ReadCloser) <-chan string { + out := make(chan string, 1) + + go func() { + defer f.Close() + defer close(out) + + str := "" + for { + data := make([]byte, 8) + n, err := f.Read(data) + if err == io.EOF { + break + } + + if err != nil { + fmt.Println("error reading file:", err) + return + } + data = data[:n] + if i := bytes.IndexByte(data, '\n'); i != -1 { + str += string(data[:i]) + data = data[i+1:] + out <- str + str = "" + } + str += string(data) + } + if len(str) != 0 { + out <- str + } + }() + return out +} + +//for line := range getLinesChannel(conn){ +// fmt.Printf("read: %s\n",line) +// } + + + +// var body []byte = nil +// var status response.StatusCode = response.StatusOK + +// if handlerError != nil{ +// status = handlerError.StatusCode +// body = []byte(handlerError.Message) +// }else{ +// body = writer.Bytes() +// } + +// headers.Replace("Content-Length", fmt.Sprintf("%d", len(body))) +// response.WriteStatusLine(conn,status) +// response.WriteHeaders(conn,headers) +// conn.Write(body) diff --git a/go.mod b/go.mod new file mode 100644 index 0000000..8d8cece --- /dev/null +++ b/go.mod @@ -0,0 +1,10 @@ +module httpfromtcp + +go 1.25.3 + +require ( + github.com/davecgh/go-spew v1.1.1 // indirect + github.com/pmezard/go-difflib v1.0.0 // indirect + github.com/stretchr/testify v1.11.1 // indirect + gopkg.in/yaml.v3 v3.0.1 // indirect +) diff --git a/go.sum b/go.sum new file mode 100644 index 0000000..cc8b3f4 --- /dev/null +++ b/go.sum @@ -0,0 +1,9 @@ +github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= +github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= +github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= +github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= +github.com/stretchr/testify v1.11.1 h1:7s2iGBzp5EwR7/aIZr8ao5+dra3wiQyKjjFuvgVKu7U= +github.com/stretchr/testify v1.11.1/go.mod h1:wZwfW3scLgRK+23gO65QZefKpKQRnfz6sD981Nm4B6U= +gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= +gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= +gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= diff --git a/internal/headers/headers.go b/internal/headers/headers.go new file mode 100644 index 0000000..3ab24ae --- /dev/null +++ b/internal/headers/headers.go @@ -0,0 +1,117 @@ +package headers + +import ( + "bytes" + "fmt" + "strings" +) + +type Headers struct { + headers map[string]string +} + +var rn = []byte("\r\n") + +func isToken(str []byte) bool { + for _, ch := range str { + found := false + + if ch >= 'A' && ch <= 'Z' || + ch >= 'a' && ch <= 'z' || + ch >= '0' && ch <= '9' { + found = true + } + + switch ch { + case '!', '#', '$', '%', '&', '\'', '*', '+', '-', '.', '^', '_', '`', '|', '~': + found = true + } + + if !found { + return false + } + } + + return true +} + +func NewHeaders() *Headers { + return &Headers{map[string]string{}} +} + +func parseHeader(fieldLine []byte) (string, string, error) { + parts := bytes.SplitN(fieldLine, []byte(":"), 2) + if len(parts) != 2 { + return "", "", fmt.Errorf("malformed header") + } + + name := parts[0] + value := bytes.TrimSpace(parts[1]) + + if bytes.HasSuffix(name, []byte(" ")) { + return "", "", fmt.Errorf("malformed header") + } + + return string(name), string(value), nil +} + +func (h *Headers) Get(name string) (string, bool) { + v, ok := h.headers[strings.ToLower(name)] + return v, ok +} + +func (h *Headers) Set(name, value string) { + name = strings.ToLower(name) + + if v, ok := h.headers[name]; ok { + h.headers[name] = fmt.Sprintf("%s,%s", v, value) + } else { + h.headers[name] = value + } +} +func (h *Headers) Replace(name, value string) { + name = strings.ToLower(name) + h.headers[name] = value +} + +func (h *Headers) Delete(name string) { + name = strings.ToLower(name) + delete(h.headers,name) +} + +func (h *Headers) ForEach(cb func(n, v string)) { + for n, v := range h.headers { + cb(n, v) + } +} + +func (h Headers) Parse(data []byte) (int, bool, error) { + read := 0 + done := false + + for { + idx := bytes.Index(data[read:], rn) + if idx == -1 { + break + } + + if idx == 0 { + done = true + break + } + + name, value, err := parseHeader(data[read : read+idx]) + if err != nil { + return 0, false, err + } + + if !isToken([]byte(name)) { + return 0, false, fmt.Errorf("malformed header") + } + + read += idx + len(rn) + h.Set(name, value) + } + + return read, done, nil +} diff --git a/internal/headers/headers_test.go b/internal/headers/headers_test.go new file mode 100644 index 0000000..c713765 --- /dev/null +++ b/internal/headers/headers_test.go @@ -0,0 +1,47 @@ +package headers + +import ( + "testing" + + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" +) + +func TestHeaders(t *testing.T) { + // Test: Valid single header + headers := NewHeaders() + data := []byte("Host: localhost:42069\r\n\r\n") + n, done, err := headers.Parse(data) + require.NoError(t, err) + require.NotNil(t, headers) + host, _ := headers.Get("HoST") + assert.Equal(t, "localhost:42069", host) + assert.Equal(t, 23, n) + assert.True(t, done) + + // Test: Invalid spacing header + headers = NewHeaders() + data = []byte(" Host : localhost:42069 \r\n\r\n") + n, done, err = headers.Parse(data) + require.Error(t, err) + assert.Equal(t, 0, n) + assert.False(t, done) + + // Test: Invalid spacing header + headers = NewHeaders() + data = []byte("H©st: localhost:42069\r\n\r\n") + n, done, err = headers.Parse(data) + require.Error(t, err) + assert.Equal(t, 0, n) + assert.False(t, done) + + // Test: Invalid spacing header + headers = NewHeaders() + data = []byte("Host: localhost:42069\r\nHost: localhost:42069\r\n") + n, done, err = headers.Parse(data) + require.NoError(t, err) + require.NotNil(t, headers) + host, _ = headers.Get("HOST") + assert.Equal(t, "localhost:42069,localhost:42069", host) + assert.False(t, done) +} diff --git a/internal/request/request.go b/internal/request/request.go new file mode 100644 index 0000000..2c8d740 --- /dev/null +++ b/internal/request/request.go @@ -0,0 +1,201 @@ +package request + +import ( + "bytes" + "fmt" + "io" + "strconv" + "strings" + + "httpfromtcp/internal/headers" +) + +type Request struct { + RequestLine RequestLine + Headers *headers.Headers + Body []byte + state parserState +} + +type RequestLine struct { + HttpVersion string + RequestTarget string + Method string +} + +var ERROR_BAD_START_LINE = fmt.Errorf("bad start line") +var SEPARATOR = []byte("\r\n") + +type parserState string + +const ( + StateInit parserState = "init" + StateDone parserState = "done" + StateHeaders parserState = "headers" + StateBody parserState = "body" +) + +func getInt(headers *headers.Headers, name string, defaultValue int) int { + valueStr, exists := headers.Get(name) + if !exists { + return defaultValue + } + + value, err := strconv.Atoi(strings.TrimSpace(valueStr)) + if err != nil { + return defaultValue + } + return value +} + +func newRequest() *Request { + return &Request{state: StateInit, Headers: headers.NewHeaders()} +} + +func (r *RequestLine) ValidHTTP() bool { + return r.HttpVersion == "1.1" +} + +func parseRequestLine(b []byte) (*RequestLine, int, error) { + idx := bytes.Index(b, SEPARATOR) + if idx == -1 { + return nil, 0, nil + } + + startLine := b[:idx] + read := idx + len(SEPARATOR) + + parts := bytes.Split(startLine, []byte(" ")) + if len(parts) != 3 { + return nil, 0, ERROR_BAD_START_LINE + } + + httpParts := bytes.Split(parts[2], []byte("/")) + if len(httpParts) != 2 || string(httpParts[0]) != "HTTP" || string(httpParts[1]) != "1.1" { + return nil, 0, ERROR_BAD_START_LINE + } + + rl := &RequestLine{ + Method: string(parts[0]), + RequestTarget: string(parts[1]), + HttpVersion: string(httpParts[1]), + } + + if !rl.ValidHTTP() { + return nil, 0, ERROR_BAD_START_LINE + } + + return rl, read, nil +} + +func (r *Request) parse(data []byte) (int, error) { + + read := 0 +outer: + for { + currentData := data[read:] + if len(currentData) == 0 { + break outer + } + + switch r.state { + case StateInit: + rl, n, err := parseRequestLine(currentData) + if err != nil { + fmt.Printf("error: %s", err) + return 0, err + } + + if n == 0 { + break outer + } + + r.RequestLine = *rl + read += n + r.state = StateHeaders + + case StateDone: + break outer + case StateHeaders: + n, done, err := r.Headers.Parse(currentData) + if err != nil { + fmt.Printf("error: %s", err) + return 0, err + + } + read += n + + if done { + remaining := currentData[n:] + if bytes.HasPrefix(remaining, SEPARATOR) { + read += len(SEPARATOR) + } + if getInt(r.Headers, "content-length", 0) == 0 { + r.state = StateDone + } else { + r.state = StateBody + } + } else if n == 0 { + break outer + } + + case StateBody: + length := getInt(r.Headers, "content-length", 0) + if length == 0 { + r.state = StateDone + break + } + + remaining := min(length-len(r.Body), len(currentData)) + r.Body = append(r.Body, currentData[:remaining]...) + read += remaining + fmt.Printf("data: %d,%s", read, r.Body) + + if len(r.Body) == length { + r.state = StateDone + } + + default: + panic("code bad") + } + } + return read, nil +} + +func (r *Request) done() bool { + return r.state == StateDone +} + +func RequestFromReader(reader io.Reader) (*Request, error) { + request := newRequest() + + buf := make([]byte, 1024) + bufLen := 0 + + for !request.done() { + n, err := reader.Read(buf[bufLen:]) + bufLen += n + + if n > 0 { + readN, err := request.parse(buf[:bufLen]) + if err != nil { + return nil, err + } + copy(buf, buf[readN:bufLen]) + bufLen -= readN + } + + if err == io.EOF { + break + } + + if err != nil { + return nil, err + } + } + if !request.done() { + return nil, fmt.Errorf("Incomplete request") + } + + return request, nil +} diff --git a/internal/request/request_test.go b/internal/request/request_test.go new file mode 100644 index 0000000..07cd61a --- /dev/null +++ b/internal/request/request_test.go @@ -0,0 +1,111 @@ +package request + +import ( + "io" + "testing" + + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" +) + +type chunkReader struct { + data string + numBytesPerRead int + pos int +} + +// Read reads up to len(p) or numBytesPerRead bytes from the string per call +// its useful for simulating reading a variable number of bytes per chunk from a network connection +func (cr *chunkReader) Read(p []byte) (n int, err error) { + if cr.pos >= len(cr.data) { + return 0, io.EOF + } + endIndex := cr.pos + cr.numBytesPerRead + if endIndex > len(cr.data) { + endIndex = len(cr.data) + } + n = copy(p, cr.data[cr.pos:endIndex]) + cr.pos += n + + return n, nil +} + +func TestRequestLineParse(t *testing.T) { + // Test: Good GET Request line + reader := &chunkReader{ + data: "GET / HTTP/1.1\r\nHost: localhost:42069\r\nUser-Agent: curl/7.81.0\r\nAccept: */*\r\n\r\n", + numBytesPerRead: 3, + } + r, err := RequestFromReader(reader) + require.NoError(t, err) + require.NotNil(t, r) + assert.Equal(t, "GET", r.RequestLine.Method) + assert.Equal(t, "/", r.RequestLine.RequestTarget) + assert.Equal(t, "1.1", r.RequestLine.HttpVersion) + + // Test: Good GET Request line with path + reader = &chunkReader{ + data: "GET /coffee HTTP/1.1\r\nHost: localhost:42069\r\nUser-Agent: curl/7.81.0\r\nAccept: */*\r\n\r\n", + numBytesPerRead: 1, + } + r, err = RequestFromReader(reader) + require.NoError(t, err) + require.NotNil(t, r) + assert.Equal(t, "GET", r.RequestLine.Method) + assert.Equal(t, "/coffee", r.RequestLine.RequestTarget) + assert.Equal(t, "1.1", r.RequestLine.HttpVersion) +} + +func TestParseHeaders(t *testing.T) { + // Test: Standard Headers + reader := &chunkReader{ + data: "GET / HTTP/1.1\r\nHost: localhost:42069\r\nUser-Agent: curl/7.81.0\r\nAccept: */*\r\n\r\n", + numBytesPerRead: 3, + } + r, err := RequestFromReader(reader) + require.NoError(t, err) + require.NotNil(t, r) + + host, _ := r.Headers.Get("host") + assert.Equal(t, "localhost:42069", host) + user, _ := r.Headers.Get("user-agent") + assert.Equal(t, "curl/7.81.0", user) + accept, _ := r.Headers.Get("accept") + assert.Equal(t, "*/*", accept) + + // Test: Malformed Header + reader = &chunkReader{ + data: "GET / HTTP/1.1\r\nHost localhost:42069\r\n\r\n", + numBytesPerRead: 3, + } + r, err = RequestFromReader(reader) + require.Error(t, err) +} + +func TestParseBody(t *testing.T) { + // Test: Standard Body + reader := &chunkReader{ + data: "POST /submit HTTP/1.1\r\n" + + "Host: localhost:42069\r\n" + + "Content-Length: 13\r\n" + + "\r\n" + + "hello world!\n", + numBytesPerRead: 3, + } + r, err := RequestFromReader(reader) + require.NoError(t, err) + require.NotNil(t, r) + assert.Equal(t, "hello world!\n", string(r.Body)) + + // Test: Body shorter than reported content length + reader = &chunkReader{ + data: "POST /submit HTTP/1.1\r\n" + + "Host: localhost:42069\r\n" + + "Content-Length: 20\r\n" + + "\r\n" + + "partial content", + numBytesPerRead: 3, + } + r, err = RequestFromReader(reader) + require.Error(t, err) +} diff --git a/internal/response/response.go b/internal/response/response.go new file mode 100644 index 0000000..14ef201 --- /dev/null +++ b/internal/response/response.go @@ -0,0 +1,100 @@ +package response + +import( + "fmt" + "io" + + "httpfromtcp/internal/headers" +) + +type Response struct { +} + +type Writer struct{ + writer io.Writer + +} + +func NewWriter(writer io.Writer)*Writer{ + return &Writer{writer:writer} +} + +func (w *Writer) WriteStatusLine(statusCode StatusCode) error{ + statusLine := []byte{} + + switch statusCode{ + case StatusOK: statusLine = []byte("HTTP/1.1 200 OK\r\n") + case StatusBadRequest: statusLine = []byte("HTTP/1.1 400 Bad Request\r\n") + case StatusInternalServerError: statusLine = []byte("HTTP/1.1 500 Internal Server Error\r\n") + default: + return fmt.Errorf("unrecognized error code") + } + + _,err := w.writer.Write(statusLine) + return err +} + +func (w *Writer) WriteHeaders(h headers.Headers) error{ + b := []byte{} + h.ForEach(func(n,v string){ + b = fmt.Appendf(b, "%s: %s\r\n",n,v) + }) + b = fmt.Appendf(b,"\r\n") + _,err := w.writer.Write(b) + return err +} + +func (w *Writer) WriteBody(p []byte) (int, error){ + return w.writer.Write(p) +} + + +type StatusCode int + +const ( + StatusOK StatusCode = 200 + StatusBadRequest StatusCode = 400 + StatusInternalServerError StatusCode = 500 +) + +func WriteHeaders(w io.Writer, h *headers.Headers) error{ + b := []byte{} + h.ForEach(func(n,v string){ + b = fmt.Appendf(b, "%s: %s\r\n",n,v) + }) + b = fmt.Appendf(b,"\r\n") + _,err := w.Write(b) + return err +} + +func GetDefaultHeaders(contentLen int) *headers.Headers{ + h := headers.NewHeaders() + h.Set("Content-Length", fmt.Sprintf("%d",contentLen)) + h.Set("Connection", "close") + h.Set("Content-Type","text/plain") + + return h +} + +func WriteStatusLine(w io.Writer, statusCode StatusCode) error{ + statusLine := []byte{} + + switch statusCode{ + case StatusOK: statusLine = []byte("HTTP/1.1 200 OK\r\n") + case StatusBadRequest: statusLine = []byte("HTTP/1.1 400 Bad Request\r\n") + case StatusInternalServerError: statusLine = []byte("HTTP/1.1 500 Internal Server Error\r\n") + default: + return fmt.Errorf("unrecognized error code") + } + + _,err := w.Write(statusLine) + return err +} + +//func (w *Writer) WriteChunkedBody(p []byte) (int,error){ + +//} + +//func (w *Writer) WriteChunkedBodyDone() (int,error){ + +//} diff --git a/internal/server/server.go b/internal/server/server.go new file mode 100644 index 0000000..d59d57c --- /dev/null +++ b/internal/server/server.go @@ -0,0 +1,71 @@ +package server + +import ( + "fmt" + "io" + "net" + + "httpfromtcp/internal/response" + "httpfromtcp/internal/request" +) + +type HandlerError struct{ + StatusCode response.StatusCode + Message string +} + +type Handler func(w *response.Writer, req *request.Request) + +type Server struct { + Closed bool + Handler Handler +} + +func runConnection(s *Server, conn io.ReadWriteCloser) { + defer conn.Close() + + responseWriter := response.NewWriter(conn) + headers := response.GetDefaultHeaders(0) + r,err:= request.RequestFromReader(conn) + if err != nil{ + responseWriter.WriteStatusLine(response.StatusBadRequest) + responseWriter.WriteHeaders(*headers) + return + } + + s.Handler(responseWriter,r) +} + +func runServer(s *Server, listener net.Listener) { + for { + conn, err := listener.Accept() + + if s.Closed { + return + } + + if err != nil { + return + } + go runConnection(s, conn) + } +} + +func Serve(port uint16, handler Handler) (*Server, error) { + listener, err := net.Listen("tcp", fmt.Sprintf(":%d", port)) + if err != nil { + return nil, err + } + server := &Server{ + Closed: false, + Handler: handler, + } + go runServer(server, listener) + + return server, nil +} + +func (s *Server) Close() error { + s.Closed = true + return nil +} diff --git a/messages.txt b/messages.txt new file mode 100644 index 0000000..5269008 --- /dev/null +++ b/messages.txt @@ -0,0 +1,5 @@ +Do you have what it takes to be an engineer at TheStartup™? +Are you willing to work 80 hours a week in hopes that your 0.001% equity is worth something? +Can you say "synergy" and "democratize" with a straight face? +Are you prepared to eat top ramen at your desk 3 meals a day? +end