From eeabf01de73a310a1ad76f140111b4f7d405bd46 Mon Sep 17 00:00:00 2001 From: arkahood Date: Sun, 2 Nov 2025 16:51:24 +0530 Subject: [PATCH 1/3] initial server setup --- cmd/httpserver/main.go | 25 ++++++++++++++++ internal/server/server.go | 63 +++++++++++++++++++++++++++++++++++++++ 2 files changed, 88 insertions(+) create mode 100644 cmd/httpserver/main.go create mode 100644 internal/server/server.go diff --git a/cmd/httpserver/main.go b/cmd/httpserver/main.go new file mode 100644 index 0000000..200fd76 --- /dev/null +++ b/cmd/httpserver/main.go @@ -0,0 +1,25 @@ +package main + +import ( + "RAWHTTP/internal/server" + "log" + "os" + "os/signal" + "syscall" +) + +const port = 8080 + +func main() { + server, err := server.Serve(port) + if err != nil { + log.Fatalf("Error starting server: %v", err) + } + defer server.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/internal/server/server.go b/internal/server/server.go new file mode 100644 index 0000000..c4597df --- /dev/null +++ b/internal/server/server.go @@ -0,0 +1,63 @@ +package server + +import ( + "errors" + "fmt" + "net" + "sync/atomic" +) + +type Server struct { + listener net.Listener + serverIsClosed atomic.Bool +} + +func Serve(port int) (*Server, error) { + lstnr, err := net.Listen("tcp", fmt.Sprint(":", port)) + if err != nil { + return nil, errors.New("tcp error happened") + } + server := Server{ + listener: lstnr, + } + + go func(s *Server) { + s.listen() + }(&server) + + return &server, nil +} + +func (s *Server) Close() error { + s.serverIsClosed.Store(true) + return s.listener.Close() +} + +func (s *Server) listen() { + for { + conn, err := s.listener.Accept() + if err != nil { + if !s.serverIsClosed.Load() { + fmt.Println("Accept error:", err.Error()) + } + return + } + go s.handle(conn) + } +} + +func (s *Server) handle(conn net.Conn) { + defer conn.Close() + + response := "HTTP/1.1 200 OK\r\n" + + "Content-Type: text/plain\r\n" + + "Content-Length: 12\r\n" + + "Connection: close\r\n" + + "\r\n" + + "Hello World!" + + _, err := conn.Write([]byte(response)) + if err != nil { + fmt.Println("Write error:", err) + } +} From b63e412d894a286ff9de9134ab8b25613d0fd6e0 Mon Sep 17 00:00:00 2001 From: arkahood Date: Mon, 3 Nov 2025 18:12:22 +0530 Subject: [PATCH 2/3] httpserver implementation done --- cmd/httpserver/main.go | 44 +++++++++++++++- internal/response/response.go | 48 ++++++++++++++++++ internal/server/server.go | 96 ++++++++++++++++++++++++++++++----- 3 files changed, 174 insertions(+), 14 deletions(-) create mode 100644 internal/response/response.go diff --git a/cmd/httpserver/main.go b/cmd/httpserver/main.go index 200fd76..5231d9a 100644 --- a/cmd/httpserver/main.go +++ b/cmd/httpserver/main.go @@ -1,7 +1,9 @@ package main import ( + "RAWHTTP/internal/request" "RAWHTTP/internal/server" + "io" "log" "os" "os/signal" @@ -10,8 +12,48 @@ import ( const port = 8080 +func handler(w io.Writer, req *request.Request) *server.HandlerError { + // Check if the request target is /yourproblem + if req.RequestLine.RequestTarget == "/yourproblem" { + return &server.HandlerError{ + StatusCode: 400, + Message: "Your problem is not my problem\n", + } + } + + if req.RequestLine.RequestTarget == "/myproblem" { + return &server.HandlerError{ + StatusCode: 500, + Message: "My problem is not my problem\n", + } + } + + if req.RequestLine.RequestTarget == "/route1" { + message := "This is Route1\n" + _, err := w.Write([]byte(message)) + if err != nil { + return &server.HandlerError{ + StatusCode: 500, + Message: "Internal Server Error: failed to write response", + } + } + return nil + } + + // Write a simple response + message := "Hello World!\n" + _, err := w.Write([]byte(message)) + if err != nil { + return &server.HandlerError{ + StatusCode: 500, + Message: "Internal Server Error: failed to write response", + } + } + return nil +} + func main() { - server, err := server.Serve(port) + server, err := server.Serve(handler, port) if err != nil { log.Fatalf("Error starting server: %v", err) } diff --git a/internal/response/response.go b/internal/response/response.go new file mode 100644 index 0000000..9644c1c --- /dev/null +++ b/internal/response/response.go @@ -0,0 +1,48 @@ +package response + +import ( + "RAWHTTP/internal/headers" + "errors" + "io" + "strconv" +) + +type StatusCode int + +const ( + StatusCodeOK StatusCode = iota + StatusCodeClientErrors + StatusCodeInternalServerError +) + +func WriteStatusLine(w io.Writer, statusCode StatusCode) error { + var err error + switch statusCode { + case StatusCodeOK: + _, err = w.Write([]byte("HTTP/1.1 200 OK\r\n")) + case StatusCodeClientErrors: + _, err = w.Write([]byte("HTTP/1.1 400 Bad Request\r\n")) + case StatusCodeInternalServerError: + _, err = w.Write([]byte("HTTP/1.1 500 Internal Server Error\r\n")) + default: + return errors.New("unsupported response type") + } + return err +} +func GetDefaultHeaders(contentLen int) headers.Headers { + return headers.Headers{ + "content-length": strconv.Itoa(contentLen), + "content-type": "text/plain", + "connection": "close", + } +} + +func WriteHeaders(w io.Writer, headers headers.Headers) error { + var headerRes []byte + for key, val := range headers { + headerRes = append(headerRes, []byte(key+":"+val+"\r\n")...) + } + headerRes = append(headerRes, []byte("\r\n")...) + _, err := w.Write(headerRes) + return err +} diff --git a/internal/server/server.go b/internal/server/server.go index c4597df..789363e 100644 --- a/internal/server/server.go +++ b/internal/server/server.go @@ -1,8 +1,12 @@ package server import ( + "RAWHTTP/internal/request" + "RAWHTTP/internal/response" + "bytes" "errors" "fmt" + "io" "net" "sync/atomic" ) @@ -10,20 +14,61 @@ import ( type Server struct { listener net.Listener serverIsClosed atomic.Bool + handler Handler } -func Serve(port int) (*Server, error) { +type HandlerError struct { + StatusCode int + Message string +} + +type Handler func(w io.Writer, req *request.Request) *HandlerError + +// WriteHandlerError writes a HandlerError to an io.Writer with proper HTTP response format. +func (hErr *HandlerError) WriteHandlerError(w io.Writer) error { + // Determine the status code + var statusCode response.StatusCode + switch hErr.StatusCode { + case 400: + statusCode = response.StatusCodeClientErrors + case 500: + statusCode = response.StatusCodeInternalServerError + default: + statusCode = response.StatusCodeInternalServerError + } + + // Write status line + err := response.WriteStatusLine(w, statusCode) + if err != nil { + return err + } + + // Prepare error message as body + body := []byte(hErr.Message) + + // Write headers with content length + headers := response.GetDefaultHeaders(len(body)) + err = response.WriteHeaders(w, headers) + if err != nil { + return err + } + + // Write body + _, err = w.Write(body) + return err +} + +func Serve(h Handler, port int) (*Server, error) { lstnr, err := net.Listen("tcp", fmt.Sprint(":", port)) if err != nil { return nil, errors.New("tcp error happened") } server := Server{ listener: lstnr, + handler: h, } - go func(s *Server) { - s.listen() - }(&server) + go server.listen() return &server, nil } @@ -49,15 +94,40 @@ func (s *Server) listen() { func (s *Server) handle(conn net.Conn) { defer conn.Close() - response := "HTTP/1.1 200 OK\r\n" + - "Content-Type: text/plain\r\n" + - "Content-Length: 12\r\n" + - "Connection: close\r\n" + - "\r\n" + - "Hello World!" - - _, err := conn.Write([]byte(response)) + // Parse the request from the connection + req, err := request.RequestFromReader(conn) if err != nil { - fmt.Println("Write error:", err) + handlerErr := &HandlerError{ + StatusCode: 400, + Message: "Bad Request: " + err.Error(), + } + handlerErr.WriteHandlerError(conn) + return } + + // Create a new empty bytes.Buffer for the handler to write to + buf := bytes.NewBuffer([]byte{}) + + // Call the handler function + handlerErr := s.handler(buf, req) + + // If the handler errors, write the error to the connection + if handlerErr != nil { + handlerErr.WriteHandlerError(conn) + return + } + + // If the handler succeeds: + // Get the response body from the handler's buffer + body := buf.Bytes() + + // Create new default response headers + headers := response.GetDefaultHeaders(len(body)) + + // Write the status line + response.WriteStatusLine(conn, response.StatusCodeOK) + // Write the headers + response.WriteHeaders(conn, headers) + // write the body + conn.Write(body) } From a0286bcda39f820fcfb77606e3d0532dc25a69ba Mon Sep 17 00:00:00 2001 From: arkahood Date: Wed, 5 Nov 2025 04:12:50 +0530 Subject: [PATCH 3/3] LGTM --- .DS_Store | Bin 6148 -> 6148 bytes .gitignore | 3 +- README.md | 114 +++++++++++++++++++++++++++-- cmd/httpserver/main.go | 46 +++++------- {internal => pkg}/server/server.go | 37 ++++++++-- 5 files changed, 158 insertions(+), 42 deletions(-) rename {internal => pkg}/server/server.go (81%) diff --git a/.DS_Store b/.DS_Store index f4a12542cd78d693bd3512b12f4b0668a033d558..4cf57ad3fb64197233bd1c4d63c9cf8742fe4dd9 100644 GIT binary patch delta 68 zcmZoMXfc=|#>B)qu~2NHo+2aL#(>?7jBJ~ESaKLQ-(%He+SriBw3(fQp9837vmnQJ W=E?jbjvNd?z{tSBvN=Lz4Kn~-Z4c)F delta 156 zcmZoMXfc=|#>B!ku~2NHo+2a1#(>?7iw`g}F>-9?Vaj0)XJ^P|NMy)l$YaoBNN33R z%*jtq%E?b+U|h$Y_u diff --git a/.gitignore b/.gitignore index ac79464..6da317c 100644 --- a/.gitignore +++ b/.gitignore @@ -1 +1,2 @@ -/idea \ No newline at end of file +/idea +.DS_Store diff --git a/README.md b/README.md index f737733..6c28e43 100644 --- a/README.md +++ b/README.md @@ -1,21 +1,119 @@ # RawHTTP -HTTP 1.1 server from scratch +A from-scratch HTTP/1.1 server implementation in Go, built directly on TCP sockets. -## HTTP Message Structure +## Project Overview -According to RFC 7230, HTTP messages follow this structure: +RawHTTP is an HTTP/1.1 server implementation that demonstrates low-level network programming concepts by building a complete HTTP server from first principles. Unlike frameworks that abstract away protocol details, this implementation provides direct insight into: + +- Raw TCP socket handling +- HTTP message parsing and generation +- State machine-based request processing +- Buffer management and streaming + +explore how web servers work at the protocol level. + +## HTTP Protocol Deep Dive + +### HTTP Message Structure + +According to RFC 7230, HTTP messages follow this precise structure: ``` start-line CRLF *( field-line CRLF ) -*( field-line CRLF ) -... CRLF [ message-body ] ``` Where: - **start-line**: Request line (method, URI, version) or status line -- **field-line**: HTTP headers (key-value pairs) (The RFC uses the term) -- **CRLF**: Carriage return + line feed (`\r\n`) -- **message-body**: Optional request/response body \ No newline at end of file +- **field-line**: HTTP headers (key-value pairs) +- **CRLF**: Carriage return + line feed (`\r\n`) - critical for protocol compliance +- **message-body**: Optional request/response body + +### Request Message Anatomy + +``` +GET /api/users HTTP/1.1\r\n ← Request Line +Host: example.com\r\n ← Header Field +User-Agent: RawHTTP/1.0\r\n ← Header Field +Accept: application/json\r\n ← Header Field +\r\n ← Header/Body Separator +[optional message body] ← Body (for POST, PUT, etc.) +``` + +#### Request Line Components + +1. **HTTP Method**: Defines the action to be performed + - `GET`: Retrieve data + - `POST`: Submit data + - `PUT`: Update/create resource + - `DELETE`: Remove resource + +2. **Request Target**: Identifies the resource + - **origin-form**: `/path/to/resource?query=value` + - **absolute-form**: `http://example.com/path` + - **authority-form**: `example.com:80` (CONNECT only) + - **asterisk-form**: `*` (OPTIONS only) + +3. **HTTP Version**: Currently `HTTP/1.1` + +### Response Message Anatomy + +``` +HTTP/1.1 200 OK\r\n ← Status Line +Content-Type: text/html\r\n ← Header Field +Content-Length: 1234\r\n ← Header Field +Connection: close\r\n ← Header Field +\r\n ← Header/Body Separator +... ← Body +``` + + +### Header Field Theory + +#### Field Name Constraints +- **tchar**: `!#$%&'*+-.^_`|~` plus ALPHA and DIGIT +- Case-insensitive (normalized to lowercase in our implementation) +- No whitespace between field-name and colon + +#### Field Value Processing +- Leading/trailing whitespace is removed +- Multiple values can be comma-separated + +#### Critical Headers +- **Host**: Required in HTTP/1.1, enables virtual hosting +- **Content-Length**: **Byte count of message body** + +## Running the Server + +### Quick Start +```bash +# Clone the repository +git clone https://github.com/arkahood/RawHTTP.git +cd RawHTTP + +# Run the HTTP server +go run ./cmd/httpserver/main.go + +# Server starts on port 8080 +# Visit http://localhost:8080 in your browser +``` + +### Testing with curl +```bash +# Basic GET request +curl -v http://localhost:8080/ +``` + +### Development Commands +```bash +# Run tests +go test ./... + +# Build binary +go build -o httpserver cmd/httpserver/main.go + +# View test coverage +go test -cover ./... +``` \ No newline at end of file diff --git a/cmd/httpserver/main.go b/cmd/httpserver/main.go index 5231d9a..e72e3d9 100644 --- a/cmd/httpserver/main.go +++ b/cmd/httpserver/main.go @@ -1,47 +1,34 @@ package main import ( - "RAWHTTP/internal/request" - "RAWHTTP/internal/server" "io" "log" "os" "os/signal" "syscall" + + "RAWHTTP/internal/request" + "RAWHTTP/pkg/server" ) const port = 8080 -func handler(w io.Writer, req *request.Request) *server.HandlerError { - // Check if the request target is /yourproblem - if req.RequestLine.RequestTarget == "/yourproblem" { - return &server.HandlerError{ - StatusCode: 400, - Message: "Your problem is not my problem\n", - } - } - - if req.RequestLine.RequestTarget == "/myproblem" { +func handler1(w io.Writer, req *request.Request) *server.HandlerError { + // Write a simple response + message := "Hello World!\n" + _, err := w.Write([]byte(message)) + if err != nil { return &server.HandlerError{ StatusCode: 500, - Message: "My problem is not my problem\n", - } - } - - if req.RequestLine.RequestTarget == "/route1" { - message := "This is Route1\n" - _, err := w.Write([]byte(message)) - if err != nil { - return &server.HandlerError{ - StatusCode: 500, - Message: "Internal Server Error: failed to write response", - } + Message: "Internal Server Error: failed to write response", } - return nil } + return nil +} +func handler2(w io.Writer, req *request.Request) *server.HandlerError { // Write a simple response - message := "Hello World!\n" + message := "Hello World2!\n" _, err := w.Write([]byte(message)) if err != nil { return &server.HandlerError{ @@ -53,7 +40,12 @@ func handler(w io.Writer, req *request.Request) *server.HandlerError { } func main() { - server, err := server.Serve(handler, port) + r := server.NewRouter() + server, err := server.Serve(r, port) + + r.AddRoute("/", handler1) + r.AddRoute("/second-route", handler2) + if err != nil { log.Fatalf("Error starting server: %v", err) } diff --git a/internal/server/server.go b/pkg/server/server.go similarity index 81% rename from internal/server/server.go rename to pkg/server/server.go index 789363e..c22caa9 100644 --- a/internal/server/server.go +++ b/pkg/server/server.go @@ -1,20 +1,35 @@ package server import ( - "RAWHTTP/internal/request" - "RAWHTTP/internal/response" "bytes" "errors" "fmt" "io" "net" "sync/atomic" + + "RAWHTTP/internal/request" + "RAWHTTP/internal/response" ) type Server struct { listener net.Listener serverIsClosed atomic.Bool - handler Handler + handler *Router +} + +type Router struct { + routes map[string]Handler +} + +func (r *Router) AddRoute(route string, fn Handler) { + r.routes[route] = fn +} + +func NewRouter() *Router { + return &Router{ + routes: make(map[string]Handler), + } } type HandlerError struct { @@ -58,14 +73,14 @@ func (hErr *HandlerError) WriteHandlerError(w io.Writer) error { return err } -func Serve(h Handler, port int) (*Server, error) { +func Serve(r *Router, port int) (*Server, error) { lstnr, err := net.Listen("tcp", fmt.Sprint(":", port)) if err != nil { return nil, errors.New("tcp error happened") } server := Server{ listener: lstnr, - handler: h, + handler: r, } go server.listen() @@ -109,7 +124,17 @@ func (s *Server) handle(conn net.Conn) { buf := bytes.NewBuffer([]byte{}) // Call the handler function - handlerErr := s.handler(buf, req) + handler, exists := s.handler.routes[req.RequestLine.RequestTarget] + if !exists { + handlerErr := &HandlerError{ + StatusCode: 404, + Message: "Not Found: " + req.RequestLine.RequestTarget, + } + handlerErr.WriteHandlerError(conn) + return + } + + handlerErr := handler(buf, req) // If the handler errors, write the error to the connection if handlerErr != nil {