Initial commit

This commit is contained in:
2026-04-04 21:27:36 +02:00
commit f21c9cf919
13 changed files with 920 additions and 0 deletions

117
internal/headers/headers.go Normal file
View 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
}

View 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
View 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
}

View 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)
}

View 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
View 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
}