Initial commit
This commit is contained in:
BIN
assets/vim.mp4
Normal file
BIN
assets/vim.mp4
Normal file
Binary file not shown.
145
cmd/httpserver/main.go
Normal file
145
cmd/httpserver/main.go
Normal file
@@ -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(`
|
||||||
|
<html>
|
||||||
|
<head>
|
||||||
|
<title>200 OK</title>
|
||||||
|
</head>
|
||||||
|
<body>
|
||||||
|
<h1>Success!</h1>
|
||||||
|
<p>Your request was an absolute banger.</p>
|
||||||
|
</body>
|
||||||
|
</html>
|
||||||
|
`)
|
||||||
|
}
|
||||||
|
|
||||||
|
func respond400() []byte{
|
||||||
|
return []byte(`
|
||||||
|
<html>
|
||||||
|
<head>
|
||||||
|
<title>400 Bad Request</title>
|
||||||
|
</head>
|
||||||
|
<body>
|
||||||
|
<h1>Bad Request</h1>
|
||||||
|
<p>Your request honestly kinda sucked.</p>
|
||||||
|
</body>
|
||||||
|
</html>
|
||||||
|
`)
|
||||||
|
}
|
||||||
|
|
||||||
|
func respond500() []byte{
|
||||||
|
return []byte(`
|
||||||
|
<html>
|
||||||
|
<head>
|
||||||
|
<title>500 Internal Server Error</title>
|
||||||
|
</head>
|
||||||
|
<body>
|
||||||
|
<h1>Internal Server Error</h1>
|
||||||
|
<p>Okay, you know what? This one is on me.</p>
|
||||||
|
</body>
|
||||||
|
</html>
|
||||||
|
`)
|
||||||
|
}
|
||||||
|
|
||||||
|
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")
|
||||||
|
}
|
||||||
41
cmd/tcplistener/main.go
Normal file
41
cmd/tcplistener/main.go
Normal file
@@ -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)
|
||||||
|
}
|
||||||
|
}
|
||||||
63
cmd/udpsender/main.go
Normal file
63
cmd/udpsender/main.go
Normal file
@@ -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)
|
||||||
10
go.mod
Normal file
10
go.mod
Normal file
@@ -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
|
||||||
|
)
|
||||||
9
go.sum
Normal file
9
go.sum
Normal file
@@ -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=
|
||||||
117
internal/headers/headers.go
Normal file
117
internal/headers/headers.go
Normal file
@@ -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
|
||||||
|
}
|
||||||
47
internal/headers/headers_test.go
Normal file
47
internal/headers/headers_test.go
Normal file
@@ -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)
|
||||||
|
}
|
||||||
201
internal/request/request.go
Normal file
201
internal/request/request.go
Normal file
@@ -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
|
||||||
|
}
|
||||||
111
internal/request/request_test.go
Normal file
111
internal/request/request_test.go
Normal file
@@ -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)
|
||||||
|
}
|
||||||
100
internal/response/response.go
Normal file
100
internal/response/response.go
Normal file
@@ -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){
|
||||||
|
|
||||||
|
//}
|
||||||
71
internal/server/server.go
Normal file
71
internal/server/server.go
Normal file
@@ -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
|
||||||
|
}
|
||||||
5
messages.txt
Normal file
5
messages.txt
Normal file
@@ -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
|
||||||
Reference in New Issue
Block a user