Skip to content

Commit

Permalink
restructure
Browse files Browse the repository at this point in the history
  • Loading branch information
stuckinforloop committed Feb 16, 2024
1 parent b1209d5 commit 3e4eef7
Show file tree
Hide file tree
Showing 9 changed files with 303 additions and 131 deletions.
18 changes: 0 additions & 18 deletions app/server.go

This file was deleted.

34 changes: 34 additions & 0 deletions internal/server/file.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,34 @@
package server

import (
"fmt"
"os"
)

func readFile(filePath string) ([]byte, error) {
_, err := os.Stat(filePath)
if err != nil {
return nil, err
}

body, err := os.ReadFile(filePath)
if err != nil {
return nil, fmt.Errorf("read file: %w", err)
}

return body, nil
}

func createFile(filePath string, body []byte) error {
f, err := os.Create(filePath)
if err != nil {
return fmt.Errorf("create file: %w", err)
}
defer f.Close()

if _, err := f.Write(body); err != nil {
return fmt.Errorf("write to file: %w", err)
}

return nil
}
172 changes: 62 additions & 110 deletions internal/server/handle.go
Original file line number Diff line number Diff line change
@@ -1,143 +1,95 @@
package server

import (
"bytes"
"errors"
"fmt"
"log"
"net"
"os"
"path/filepath"
"strconv"
"strings"
)

func handle(conn net.Conn, dir string) error {
defer conn.Close()

b := make([]byte, 1024)
if _, err := conn.Read(b); err != nil {
return fmt.Errorf("read from connection: %w", err)
}
lines := strings.Split(string(b), "\r\n")
if len(lines) < 3 {
return fmt.Errorf("invalid request")
}

path, ok := getPath(lines[0])
if !ok {
if path == "" {
return fmt.Errorf("invalid path: %s", path)
}

msg := "HTTP/1.1 404 Not Found\r\n\r\n"
if _, err := conn.Write([]byte(msg)); err != nil {
return fmt.Errorf("write to connection: %w", err)
}

return nil
req, err := ParseRequest(b)
if err != nil {
log.Printf("parse request: %v", err)
return fmt.Errorf("parse request: %w", err)
}

var msg string
switch path {
case "/":
msg = "HTTP/1.1 200 OK\r\n\r\n"
case "/user-agent":
var userAgent string
for _, l := range lines {
if strings.HasPrefix(l, "User-Agent") {
userAgent = strings.TrimPrefix(l, "User-Agent: ")
resp := Response{}
resp.Body = make([]byte, 1024)
resp.Headers = make(map[string]string)
switch {
case req.Target == "/":
resp.StatusCode = 200
resp.Version = req.Version

case strings.HasPrefix(req.Target, "/echo"):
body := strings.TrimPrefix(req.Target, "/echo/")

resp.StatusCode = 200
resp.Version = req.Version
resp.Headers["Content-Type"] = "text/plain"
resp.Headers["Content-Length"] = strconv.Itoa(len(body))
resp.Body = []byte(body)

case strings.HasPrefix(req.Target, "/files"):
filename := strings.TrimPrefix(req.Target, "/files/")
filePath := filepath.Join(dir, filename)

// if method post, then create a file
if req.Method == POST {
body := bytes.Trim(req.Body, "\x00")
if err := createFile(filePath, body); err != nil {
return fmt.Errorf("create file: %w", err)
}
}

msg = genMessage(userAgent)
default:
if strings.HasPrefix(path, "/files") {
filename := strings.TrimPrefix(path, "/files/")

method := strings.Split(lines[0], " ")[0]
if method == "POST" {
body := strings.Split(string(b), "\r\n\r\n")[1]
body = strings.Trim(body, "\x00")
if err := createFile(dir, filename, body); err != nil {
return fmt.Errorf("create file: %w", err)
resp.StatusCode = 201
resp.Version = req.Version
} else if req.Method == GET {
body, err := readFile(filePath)
if err != nil {
if errors.Is(err, os.ErrNotExist) {
resp.StatusCode = 404
resp.Version = req.Version
break
}
msg = "HTTP/1.1 201 OK\r\n\r\n"
} else {
msg = genFileMessage(conn, dir, filename)
return fmt.Errorf("read file: %w", err)
}
} else {
msg = genMessage(path)
}
}

if _, err := conn.Write([]byte(msg)); err != nil {
return fmt.Errorf("write to connection: %w", err)
}

return nil
}

func getPath(requestLine string) (string, bool) {
parts := strings.Split(requestLine, " ")
if len(parts) != 3 {
return "", false
}

// get the second part of the request line
path := parts[1]

if strings.HasPrefix(path, "/files") {
return path, true
}

// check if path starts with /echo/
str := strings.TrimPrefix(path, "/echo/")
if str != path {
return str, true
}

if path == "/" || path == "/user-agent" {
return path, true
}

return path, false
}

func genMessage(body string) string {
msg := "HTTP/1.1 200 OK\r\n"
msg += "Content-Type: text/plain\r\n"
msg += fmt.Sprintf("Content-Length: %d\r\n\r\n", len(body))
msg += body

return msg
}

func genFileMessage(conn net.Conn, dir, filename string) string {
filePath := filepath.Join(dir, filename)
_, err := os.Stat(filePath)
if err != nil && errors.Is(err, os.ErrNotExist) {
return "HTTP/1.1 404 Not Found\r\n\r\n"
}

body, _ := os.ReadFile(filePath)
resp.StatusCode = 200
resp.Version = req.Version
resp.Headers["Content-Type"] = "application/octet-stream"
resp.Headers["Content-Length"] = strconv.Itoa(len(body))
resp.Body = body
}

msg := "HTTP/1.1 200 OK\r\n"
msg += "Content-Type: application/octet-stream\r\n"
msg += fmt.Sprintf("Content-Length: %d\r\n\r\n", len(body))
msg += string(body)
case req.Target == "/user-agent":
body := req.Headers["User-Agent"]

return msg
}
resp.StatusCode = 200
resp.Version = req.Version
resp.Headers["Content-Type"] = "text/plain"
resp.Headers["Content-Length"] = strconv.Itoa(len(body))
resp.Body = []byte(body)

func createFile(dir, filename, body string) error {
filePath := filepath.Join(dir, filename)
default:
resp.StatusCode = 404
resp.Version = req.Version

f, err := os.Create(filePath)
if err != nil {
return fmt.Errorf("create file: %w", err)
}
defer f.Close()

if _, err := f.Write([]byte(body)); err != nil {
return fmt.Errorf("write to file: %w", err)
if err := WriteResponse(conn, resp); err != nil {
log.Printf("write response: %v", err)
return fmt.Errorf("write response: %w", err)
}

return nil
Expand Down
88 changes: 88 additions & 0 deletions internal/server/request.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,88 @@
package server

import (
"bufio"
"bytes"
"fmt"
"io"
"strconv"
"strings"
)

type Method string

const (
GET Method = "GET"
POST Method = "POST"
)

type Request struct {
Body []byte
Headers map[string]string
Method Method
Target string
Version string
}

// ParseRequest parses an HTTP request
func ParseRequest(b []byte) (*Request, error) {
// parse request line
reader := bufio.NewReader(bytes.NewReader(b))
requestLine, err := reader.ReadString('\n')
if err != nil {
return nil, fmt.Errorf("read request line: %w", err)
}

fields := strings.Fields(requestLine)
if len(fields) < 3 {
return nil, fmt.Errorf("invalid request line: %s", requestLine)
}
method := fields[0]
target := fields[1]
version := fields[2]

// parse headers
headers := make(map[string]string)
for {
line, err := reader.ReadString('\n')
if err != nil {
return nil, fmt.Errorf("read request headers: %w", err)
}
if line == "\r\n" {
break
}

splits := strings.SplitN(line, ":", 2)
headers[splits[0]] = formatHeader(splits[1])
}

// parse body
var contentLength int
contentLengthHeader := headers["Content-Length"]
if contentLengthHeader != "" {
contentLength, err = strconv.Atoi(headers["Content-Length"])
if err != nil {
return nil, fmt.Errorf("parse content length: %w", err)
}
}
body := make([]byte, contentLength)
if _, err := io.ReadFull(reader, body); err != nil && err != io.EOF {
return nil, fmt.Errorf("read request body: %w", err)
}

req := &Request{
Body: body,
Headers: headers,
Method: Method(method),
Target: target,
Version: version,
}

return req, nil
}

func formatHeader(val string) string {
val = strings.TrimSpace(val)
val = strings.Trim(val, "\r\n")
return val
}
Loading

0 comments on commit 3e4eef7

Please sign in to comment.