Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Binary file modified .DS_Store
Binary file not shown.
3 changes: 2 additions & 1 deletion .gitignore
Original file line number Diff line number Diff line change
@@ -1 +1,2 @@
/idea
/idea
.DS_Store
114 changes: 106 additions & 8 deletions README.md
Original file line number Diff line number Diff line change
@@ -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
- **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
<html>...</html> ← 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 ./...
```
59 changes: 59 additions & 0 deletions cmd/httpserver/main.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,59 @@
package main

import (
"io"
"log"
"os"
"os/signal"
"syscall"

"RAWHTTP/internal/request"
"RAWHTTP/pkg/server"
)

const port = 8080

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: "Internal Server Error: failed to write response",
}
}
return nil
}

func handler2(w io.Writer, req *request.Request) *server.HandlerError {
// Write a simple response
message := "Hello World2!\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() {
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)
}
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")
}
48 changes: 48 additions & 0 deletions internal/response/response.go
Original file line number Diff line number Diff line change
@@ -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
}
Loading