From 3f303d8e099862812eab22fb9f2fc67c917a4a50 Mon Sep 17 00:00:00 2001 From: Eduard Urbach Date: Tue, 2 Apr 2024 15:23:44 +0200 Subject: [PATCH 01/10] Renamed content to send --- content/content.go => send/send.go | 2 +- content/content_test.go => send/send_test.go | 18 +++++++++--------- 2 files changed, 10 insertions(+), 10 deletions(-) rename content/content.go => send/send.go (98%) rename content/content_test.go => send/send_test.go (82%) diff --git a/content/content.go b/send/send.go similarity index 98% rename from content/content.go rename to send/send.go index 2eca461..79e2cf8 100644 --- a/content/content.go +++ b/send/send.go @@ -1,4 +1,4 @@ -package content +package send import ( "encoding/json" diff --git a/content/content_test.go b/send/send_test.go similarity index 82% rename from content/content_test.go rename to send/send_test.go index 5d4d2ca..4822a4e 100644 --- a/content/content_test.go +++ b/send/send_test.go @@ -1,42 +1,42 @@ -package content_test +package send_test import ( "testing" "git.akyoto.dev/go/assert" "git.akyoto.dev/go/web" - "git.akyoto.dev/go/web/content" + "git.akyoto.dev/go/web/send" ) func TestContentTypes(t *testing.T) { s := web.NewServer() s.Get("/css", func(ctx web.Context) error { - return content.CSS(ctx, "body{}") + return send.CSS(ctx, "body{}") }) s.Get("/csv", func(ctx web.Context) error { - return content.CSV(ctx, "ID;Name\n") + return send.CSV(ctx, "ID;Name\n") }) s.Get("/html", func(ctx web.Context) error { - return content.HTML(ctx, "") + return send.HTML(ctx, "") }) s.Get("/js", func(ctx web.Context) error { - return content.JS(ctx, "console.log(42)") + return send.JS(ctx, "console.log(42)") }) s.Get("/json", func(ctx web.Context) error { - return content.JSON(ctx, struct{ Name string }{Name: "User 1"}) + return send.JSON(ctx, struct{ Name string }{Name: "User 1"}) }) s.Get("/text", func(ctx web.Context) error { - return content.Text(ctx, "Hello") + return send.Text(ctx, "Hello") }) s.Get("/xml", func(ctx web.Context) error { - return content.XML(ctx, "") + return send.XML(ctx, "") }) tests := []struct { From 1f62562d7092edfd19b889a3e3301d5830a5752a Mon Sep 17 00:00:00 2001 From: Eduard Urbach Date: Tue, 2 Apr 2024 16:17:43 +0200 Subject: [PATCH 02/10] Added Redirect function --- Context.go | 9 +++++++++ Context_test.go | 12 ++++++++++++ README.md | 1 + 3 files changed, 22 insertions(+) diff --git a/Context.go b/Context.go index 46b3b8c..7afb303 100644 --- a/Context.go +++ b/Context.go @@ -9,6 +9,7 @@ type Context interface { Bytes([]byte) error Error(...any) error Next() error + Redirect(int, string) error Request() Request Response() Response Status(int) Context @@ -51,6 +52,14 @@ func (ctx *context) Next() error { return ctx.server.handlers[ctx.handlerCount](ctx) } +// Redirect redirects the client to a different location +// with the specified status code. +func (ctx *context) Redirect(status int, location string) error { + ctx.response.SetStatus(status) + ctx.response.SetHeader("Location", location) + return nil +} + // Request returns the HTTP request. func (ctx *context) Request() Request { return &ctx.request diff --git a/Context_test.go b/Context_test.go index 05bc24f..5a25cab 100644 --- a/Context_test.go +++ b/Context_test.go @@ -55,3 +55,15 @@ func TestErrorMultiple(t *testing.T) { assert.Equal(t, response.Status(), 401) assert.Equal(t, string(response.Body()), "") } + +func TestRedirect(t *testing.T) { + s := web.NewServer() + + s.Get("/", func(ctx web.Context) error { + return ctx.Redirect(301, "/target") + }) + + response := s.Request("GET", "/", nil, nil) + assert.Equal(t, response.Status(), 301) + assert.Equal(t, response.Header("Location"), "/target") +} diff --git a/README.md b/README.md index 9b912f2..67e9259 100644 --- a/README.md +++ b/README.md @@ -44,6 +44,7 @@ PASS: TestBytes PASS: TestString PASS: TestError PASS: TestErrorMultiple +PASS: TestRedirect PASS: TestRequest PASS: TestRequestHeader PASS: TestRequestParam From b6fec17b494cd3d83bee40bef99c44e21985ae82 Mon Sep 17 00:00:00 2001 From: Eduard Urbach Date: Wed, 19 Jun 2024 10:50:41 +0200 Subject: [PATCH 03/10] Updated usage examples --- README.md | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/README.md b/README.md index 67e9259..157ad13 100644 --- a/README.md +++ b/README.md @@ -26,12 +26,12 @@ s.Get("/", func(ctx web.Context) error { // Parameter route s.Get("/blog/:post", func(ctx web.Context) error { - return ctx.String(ctx.Get("post")) + return ctx.String(ctx.Request().Param("post")) }) // Wildcard route s.Get("/images/*file", func(ctx web.Context) error { - return ctx.String(ctx.Get("file")) + return ctx.String(ctx.Request().Param("file")) }) s.Run(":8080") From b3271e03b7dc2a7d47e16ee1492e56d50cd6a36a Mon Sep 17 00:00:00 2001 From: Eduard Urbach Date: Sat, 22 Jun 2024 13:41:43 +0200 Subject: [PATCH 04/10] Added middleware documentation --- README.md | 11 +++++++++++ 1 file changed, 11 insertions(+) diff --git a/README.md b/README.md index 157ad13..b6930cd 100644 --- a/README.md +++ b/README.md @@ -34,6 +34,17 @@ s.Get("/images/*file", func(ctx web.Context) error { return ctx.String(ctx.Request().Param("file")) }) +// Middleware +s.Use(func(ctx web.Context) error { + start := time.Now() + + defer func() { + fmt.Println(ctx.Request().Path(), time.Since(start)) + }() + + return ctx.Next() +}) + s.Run(":8080") ``` From d58f68f3fee848cae077323cd51af4f20cceade0 Mon Sep 17 00:00:00 2001 From: Eduard Urbach Date: Thu, 13 Feb 2025 22:38:38 +0100 Subject: [PATCH 05/10] Implemented connection close request --- Server.go | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/Server.go b/Server.go index f987c51..c810681 100644 --- a/Server.go +++ b/Server.go @@ -121,6 +121,7 @@ func (s *server) handleConnection(conn net.Conn) { ctx = s.contextPool.Get().(*context) method string url string + close bool ) ctx.reader.Reset(conn) @@ -128,7 +129,7 @@ func (s *server) handleConnection(conn net.Conn) { defer conn.Close() defer s.contextPool.Put(ctx) - for { + for !close { // Read the HTTP request line message, err := ctx.reader.ReadString('\n') @@ -183,6 +184,10 @@ func (s *server) handleConnection(conn net.Conn) { Key: key, Value: value, }) + + if value == "close" && strings.EqualFold(key, "connection") { + close = true + } } // Handle the request From bb5d849cd9240808ca79037a8421d5a1fadd9767 Mon Sep 17 00:00:00 2001 From: Eduard Urbach Date: Fri, 14 Feb 2025 22:28:29 +0100 Subject: [PATCH 06/10] Added a check for malformed headers --- README.md | 1 + Server.go | 4 ++++ Server_test.go | 20 ++++++++++++++++++++ 3 files changed, 25 insertions(+) diff --git a/README.md b/README.md index b6930cd..ddb0e47 100644 --- a/README.md +++ b/README.md @@ -70,6 +70,7 @@ PASS: TestBadRequest PASS: TestBadRequestHeader PASS: TestBadRequestMethod PASS: TestBadRequestProtocol +PASS: TestConnectionClose PASS: TestEarlyClose PASS: TestUnavailablePort coverage: 100.0% of statements diff --git a/Server.go b/Server.go index c810681..dcaea9a 100644 --- a/Server.go +++ b/Server.go @@ -177,6 +177,10 @@ func (s *server) handleConnection(conn net.Conn) { continue } + if colon > len(message)-4 { + continue + } + key := message[:colon] value := message[colon+2 : len(message)-2] diff --git a/Server_test.go b/Server_test.go index 9793aef..7172d54 100644 --- a/Server_test.go +++ b/Server_test.go @@ -136,6 +136,26 @@ func TestBadRequestProtocol(t *testing.T) { s.Run(":8080") } +func TestConnectionClose(t *testing.T) { + s := web.NewServer() + + go func() { + defer syscall.Kill(syscall.Getpid(), syscall.SIGTERM) + + conn, err := net.Dial("tcp", ":8080") + assert.Nil(t, err) + defer conn.Close() + + _, err = io.WriteString(conn, "GET / HTTP/1.1\r\nConnection: close\r\n\r\n") + assert.Nil(t, err) + + _, err = io.ReadAll(conn) + assert.Nil(t, err) + }() + + s.Run(":8080") +} + func TestEarlyClose(t *testing.T) { s := web.NewServer() From e68e5ca911eb326c189b612b3915e0ea4ef6821e Mon Sep 17 00:00:00 2001 From: Eduard Urbach Date: Tue, 25 Feb 2025 16:48:09 +0100 Subject: [PATCH 07/10] Updated module path --- Context_test.go | 4 ++-- README.md | 6 +++--- Request.go | 2 +- Request_test.go | 4 ++-- Response_test.go | 4 ++-- Server.go | 2 +- Server_test.go | 6 +++--- examples/hello/main.go | 2 +- go.mod | 8 ++++---- go.sum | 8 ++++---- send/send.go | 2 +- send/send_test.go | 6 +++--- 12 files changed, 27 insertions(+), 27 deletions(-) diff --git a/Context_test.go b/Context_test.go index 5a25cab..b3a6005 100644 --- a/Context_test.go +++ b/Context_test.go @@ -4,8 +4,8 @@ import ( "errors" "testing" - "git.akyoto.dev/go/assert" - "git.akyoto.dev/go/web" + "git.urbach.dev/go/assert" + "git.urbach.dev/go/web" ) func TestBytes(t *testing.T) { diff --git a/README.md b/README.md index ddb0e47..fcfe006 100644 --- a/README.md +++ b/README.md @@ -1,6 +1,6 @@ # web -A fast HTTP/1.1 web server that can sit behind a reverse proxy like `caddy` or `nginx` for HTTP 1/2/3 support. +A minimal HTTP/1.1 web server that sits behind a reverse proxy like `caddy` or `nginx` for HTTP 1/2/3 support. ## Features @@ -11,7 +11,7 @@ A fast HTTP/1.1 web server that can sit behind a reverse proxy like `caddy` or ` ## Installation ```shell -go get git.akyoto.dev/go/web +go get git.urbach.dev/go/web ``` ## Usage @@ -82,7 +82,7 @@ coverage: 100.0% of statements ## License -Please see the [license documentation](https://akyoto.dev/license). +Please see the [license documentation](https://urbach.dev/license). ## Copyright diff --git a/Request.go b/Request.go index 9ec8ef9..6c7ef3c 100644 --- a/Request.go +++ b/Request.go @@ -3,7 +3,7 @@ package web import ( "bufio" - "git.akyoto.dev/go/router" + "git.urbach.dev/go/router" ) // Request is an interface for HTTP requests. diff --git a/Request_test.go b/Request_test.go index 6a08fd0..1e079a8 100644 --- a/Request_test.go +++ b/Request_test.go @@ -4,8 +4,8 @@ import ( "fmt" "testing" - "git.akyoto.dev/go/assert" - "git.akyoto.dev/go/web" + "git.urbach.dev/go/assert" + "git.urbach.dev/go/web" ) func TestRequest(t *testing.T) { diff --git a/Response_test.go b/Response_test.go index dc62965..9028acf 100644 --- a/Response_test.go +++ b/Response_test.go @@ -6,8 +6,8 @@ import ( "io" "testing" - "git.akyoto.dev/go/assert" - "git.akyoto.dev/go/web" + "git.urbach.dev/go/assert" + "git.urbach.dev/go/web" ) func TestWrite(t *testing.T) { diff --git a/Server.go b/Server.go index dcaea9a..ece9024 100644 --- a/Server.go +++ b/Server.go @@ -13,7 +13,7 @@ import ( "sync" "syscall" - "git.akyoto.dev/go/router" + "git.urbach.dev/go/router" ) // Server is the interface for an HTTP server. diff --git a/Server_test.go b/Server_test.go index 7172d54..013ca9b 100644 --- a/Server_test.go +++ b/Server_test.go @@ -7,8 +7,8 @@ import ( "syscall" "testing" - "git.akyoto.dev/go/assert" - "git.akyoto.dev/go/web" + "git.urbach.dev/go/assert" + "git.urbach.dev/go/web" ) func TestPanic(t *testing.T) { @@ -77,7 +77,7 @@ func TestBadRequestHeader(t *testing.T) { assert.Nil(t, err) defer conn.Close() - _, err = io.WriteString(conn, "GET / HTTP/1.1\r\nBadHeader\r\nGood: Header\r\n\r\n") + _, err = io.WriteString(conn, "GET / HTTP/1.1\r\nBad\r\nBad:\r\nGood: Header\r\n\r\n") assert.Nil(t, err) buffer := make([]byte, len("HTTP/1.1 200")) diff --git a/examples/hello/main.go b/examples/hello/main.go index c9853da..b42db84 100644 --- a/examples/hello/main.go +++ b/examples/hello/main.go @@ -1,7 +1,7 @@ package main import ( - "git.akyoto.dev/go/web" + "git.urbach.dev/go/web" ) func main() { diff --git a/go.mod b/go.mod index a4d4a7c..1fc6254 100644 --- a/go.mod +++ b/go.mod @@ -1,8 +1,8 @@ -module git.akyoto.dev/go/web +module git.urbach.dev/go/web -go 1.22 +go 1.24 require ( - git.akyoto.dev/go/assert v0.1.3 - git.akyoto.dev/go/router v0.1.4 + git.urbach.dev/go/assert v0.0.0-20250225153414-fc1f84f19edf + git.urbach.dev/go/router v0.0.0-20250225153839-20945fb42429 ) diff --git a/go.sum b/go.sum index a12e1ec..b693c95 100644 --- a/go.sum +++ b/go.sum @@ -1,4 +1,4 @@ -git.akyoto.dev/go/assert v0.1.3 h1:QwCUbmG4aZYsNk/OuRBz1zWVKmGlDUHhOnnDBfn8Qw8= -git.akyoto.dev/go/assert v0.1.3/go.mod h1:0GzMaM0eURuDwtGkJJkCsI7r2aUKr+5GmWNTFPgDocM= -git.akyoto.dev/go/router v0.1.4 h1:ZL5HPl4aNn4QKihf3VVs0Mm9R6ZGn2StAHGRQxjEbws= -git.akyoto.dev/go/router v0.1.4/go.mod h1:rbHbkLJlQOafuOuvBalO3O8E0JtMFPT3zzTKX3h9T08= +git.urbach.dev/go/assert v0.0.0-20250225153414-fc1f84f19edf h1:BQWa5GKNUsA5CSUa/+UlFWYCEVe3IDDKRbVqBLK0mAE= +git.urbach.dev/go/assert v0.0.0-20250225153414-fc1f84f19edf/go.mod h1:y9jGII9JFiF1HNIju0u87OyPCt82xKCtqnAFyEreCDo= +git.urbach.dev/go/router v0.0.0-20250225153839-20945fb42429 h1:VaFZujqHv30JGFZkfDEVE8kEqr3BaBSm+zy8TfMB6vw= +git.urbach.dev/go/router v0.0.0-20250225153839-20945fb42429/go.mod h1:jIdUMhSrZ9FepXzVWdMvYvhE4FfYhKOWJyw8lw5KOPk= diff --git a/send/send.go b/send/send.go index 79e2cf8..9c4c2ca 100644 --- a/send/send.go +++ b/send/send.go @@ -3,7 +3,7 @@ package send import ( "encoding/json" - "git.akyoto.dev/go/web" + "git.urbach.dev/go/web" ) // CSS sends the body with the content type set to `text/css`. diff --git a/send/send_test.go b/send/send_test.go index 4822a4e..fec22c3 100644 --- a/send/send_test.go +++ b/send/send_test.go @@ -3,9 +3,9 @@ package send_test import ( "testing" - "git.akyoto.dev/go/assert" - "git.akyoto.dev/go/web" - "git.akyoto.dev/go/web/send" + "git.urbach.dev/go/assert" + "git.urbach.dev/go/web" + "git.urbach.dev/go/web/send" ) func TestContentTypes(t *testing.T) { From 934e8e720676faa6bbf4100e3d40302a6afe8cae Mon Sep 17 00:00:00 2001 From: Eduard Urbach Date: Sat, 1 Mar 2025 14:29:56 +0100 Subject: [PATCH 08/10] Updated dependencies --- go.mod | 2 +- go.sum | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/go.mod b/go.mod index 1fc6254..b2fbb30 100644 --- a/go.mod +++ b/go.mod @@ -4,5 +4,5 @@ go 1.24 require ( git.urbach.dev/go/assert v0.0.0-20250225153414-fc1f84f19edf - git.urbach.dev/go/router v0.0.0-20250225153839-20945fb42429 + git.urbach.dev/go/router v0.0.0-20250301132726-63655425afec ) diff --git a/go.sum b/go.sum index b693c95..f3c6e5e 100644 --- a/go.sum +++ b/go.sum @@ -1,4 +1,4 @@ git.urbach.dev/go/assert v0.0.0-20250225153414-fc1f84f19edf h1:BQWa5GKNUsA5CSUa/+UlFWYCEVe3IDDKRbVqBLK0mAE= git.urbach.dev/go/assert v0.0.0-20250225153414-fc1f84f19edf/go.mod h1:y9jGII9JFiF1HNIju0u87OyPCt82xKCtqnAFyEreCDo= -git.urbach.dev/go/router v0.0.0-20250225153839-20945fb42429 h1:VaFZujqHv30JGFZkfDEVE8kEqr3BaBSm+zy8TfMB6vw= -git.urbach.dev/go/router v0.0.0-20250225153839-20945fb42429/go.mod h1:jIdUMhSrZ9FepXzVWdMvYvhE4FfYhKOWJyw8lw5KOPk= +git.urbach.dev/go/router v0.0.0-20250301132726-63655425afec h1:9HKukG9bQSAxsNhCUstZUsZgmptuHa6iHZuQVFamhh8= +git.urbach.dev/go/router v0.0.0-20250301132726-63655425afec/go.mod h1:jIdUMhSrZ9FepXzVWdMvYvhE4FfYhKOWJyw8lw5KOPk= From cdad24e2f7819cab39a597bfdc03192d202a00d7 Mon Sep 17 00:00:00 2001 From: Eduard Urbach Date: Tue, 18 Mar 2025 22:12:46 +0100 Subject: [PATCH 09/10] Added a check for malformed request paths --- README.md | 1 + Server.go | 9 ++++++++- Server_test.go | 21 +++++++++++++++++++++ 3 files changed, 30 insertions(+), 1 deletion(-) diff --git a/README.md b/README.md index fcfe006..149eb93 100644 --- a/README.md +++ b/README.md @@ -69,6 +69,7 @@ PASS: TestRun PASS: TestBadRequest PASS: TestBadRequestHeader PASS: TestBadRequestMethod +PASS: TestBadRequestPath PASS: TestBadRequestProtocol PASS: TestConnectionClose PASS: TestEarlyClose diff --git a/Server.go b/Server.go index ece9024..ba163a0 100644 --- a/Server.go +++ b/Server.go @@ -157,7 +157,14 @@ func (s *server) handleConnection(conn net.Conn) { lastSpace = len(message) - len("\r\n") } - url = message[space+1 : lastSpace] + space += 1 + + if space > lastSpace { + io.WriteString(conn, "HTTP/1.1 400 Bad Request\r\n\r\n") + return + } + + url = message[space:lastSpace] // Add headers until we meet an empty line for { diff --git a/Server_test.go b/Server_test.go index 013ca9b..f2c142b 100644 --- a/Server_test.go +++ b/Server_test.go @@ -110,6 +110,27 @@ func TestBadRequestMethod(t *testing.T) { s.Run(":8080") } +func TestBadRequestPath(t *testing.T) { + s := web.NewServer() + + go func() { + defer syscall.Kill(syscall.Getpid(), syscall.SIGTERM) + + conn, err := net.Dial("tcp", ":8080") + assert.Nil(t, err) + defer conn.Close() + + _, err = io.WriteString(conn, "GET \n") + assert.Nil(t, err) + + response, err := io.ReadAll(conn) + assert.Nil(t, err) + assert.Equal(t, string(response), "HTTP/1.1 400 Bad Request\r\n\r\n") + }() + + s.Run(":8080") +} + func TestBadRequestProtocol(t *testing.T) { s := web.NewServer() From c5ad01daee1a5d959091b96751937421679c39d8 Mon Sep 17 00:00:00 2001 From: Eduard Urbach Date: Sun, 1 Jun 2025 18:24:11 +0200 Subject: [PATCH 10/10] Updated dependencies --- go.mod | 2 +- go.sum | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/go.mod b/go.mod index b2fbb30..f2bf46e 100644 --- a/go.mod +++ b/go.mod @@ -4,5 +4,5 @@ go 1.24 require ( git.urbach.dev/go/assert v0.0.0-20250225153414-fc1f84f19edf - git.urbach.dev/go/router v0.0.0-20250301132726-63655425afec + git.urbach.dev/go/router v0.0.0-20250601162231-e35d5715d1a5 ) diff --git a/go.sum b/go.sum index f3c6e5e..4c3db0c 100644 --- a/go.sum +++ b/go.sum @@ -1,4 +1,4 @@ git.urbach.dev/go/assert v0.0.0-20250225153414-fc1f84f19edf h1:BQWa5GKNUsA5CSUa/+UlFWYCEVe3IDDKRbVqBLK0mAE= git.urbach.dev/go/assert v0.0.0-20250225153414-fc1f84f19edf/go.mod h1:y9jGII9JFiF1HNIju0u87OyPCt82xKCtqnAFyEreCDo= -git.urbach.dev/go/router v0.0.0-20250301132726-63655425afec h1:9HKukG9bQSAxsNhCUstZUsZgmptuHa6iHZuQVFamhh8= -git.urbach.dev/go/router v0.0.0-20250301132726-63655425afec/go.mod h1:jIdUMhSrZ9FepXzVWdMvYvhE4FfYhKOWJyw8lw5KOPk= +git.urbach.dev/go/router v0.0.0-20250601162231-e35d5715d1a5 h1:3qlZjgWbrHw4LWM4uBzOTldbpqCLJdeSgvcYK6f3xpc= +git.urbach.dev/go/router v0.0.0-20250601162231-e35d5715d1a5/go.mod h1:O+doTe0DZdT2XMsTY5pe6qUMqEEIAbwtX6Xp6EqHw34=