diff --git a/benchmarks/go.mod b/benchmarks/go.mod new file mode 100644 index 0000000..f711658 --- /dev/null +++ b/benchmarks/go.mod @@ -0,0 +1,10 @@ +module benchmarks + +go 1.20 + +require ( + github.com/MrMelon54/violet v0.0.0-20230419182034-77d570ac1e6d + github.com/gorilla/mux v1.8.0 +) + +require github.com/MrMelon54/trie v0.0.2 // indirect diff --git a/benchmarks/go.sum b/benchmarks/go.sum new file mode 100644 index 0000000..667450b --- /dev/null +++ b/benchmarks/go.sum @@ -0,0 +1,6 @@ +github.com/MrMelon54/trie v0.0.2 h1:ZXWcX5ij62O9K4I/anuHmVg8L3tF0UGdlPceAASwKEY= +github.com/MrMelon54/trie v0.0.2/go.mod h1:sGCGOcqb+DxSxvHgSOpbpkmA7mFZR47YDExy9OCbVZI= +github.com/MrMelon54/violet v0.0.0-20230419182034-77d570ac1e6d h1:TstTVV4XN/4UKau4h+f9KsATSoEMW/OZp2eawCXHUq8= +github.com/MrMelon54/violet v0.0.0-20230419182034-77d570ac1e6d/go.mod h1:Opth240Nv/NHNJ3yrSxFAcw6zG7CqxJsh6rcxe3j3/E= +github.com/gorilla/mux v1.8.0 h1:i40aqfkR1h2SlN9hojwV5ZA91wcXFOvkdNIeFDP5koI= +github.com/gorilla/mux v1.8.0/go.mod h1:DVbg23sWSpFRCP0SfiEN6jmj59UnW/n46BH5rLB71So= diff --git a/benchmarks/router_test.go b/benchmarks/router_test.go new file mode 100644 index 0000000..b8d089f --- /dev/null +++ b/benchmarks/router_test.go @@ -0,0 +1,42 @@ +package benchmarks + +import ( + "github.com/MrMelon54/violet/router" + "github.com/MrMelon54/violet/target" + gorillaRouter "github.com/gorilla/mux" + "net/http" + "net/http/httptest" + "testing" +) + +func benchRequest(b *testing.B, router http.Handler, r *http.Request) { + w := httptest.NewRecorder() + b.ReportAllocs() + b.ResetTimer() + for i := 0; i < b.N; i++ { + router.ServeHTTP(w, r) + } + if w.Header().Get("Location") != "https://example.com" { + b.Fatal("Location: ", w.Header().Get("Location"), " != https://example.com") + } +} + +func BenchmarkVioletRouter(b *testing.B) { + r := router.New() + r.AddRedirect("*.example.com", "", target.Redirect{ + Pre: true, + Host: "example.com", + Code: http.StatusPermanentRedirect, + }) + benchRequest(b, r, httptest.NewRequest(http.MethodGet, "https://www.example.com", nil)) +} + +func BenchmarkGorillaMux(b *testing.B) { + r := gorillaRouter.NewRouter() + r.Host("{subdomain}.example.com").Handler(target.Redirect{ + Pre: true, + Host: "example.com", + Code: http.StatusPermanentRedirect, + }) + benchRequest(b, r, httptest.NewRequest(http.MethodGet, "https://www.example.com/", nil)) +} diff --git a/router/router.go b/router/router.go index f5df533..165433e 100644 --- a/router/router.go +++ b/router/router.go @@ -71,7 +71,7 @@ func (r *Router) ServeHTTP(rw http.ResponseWriter, req *http.Request) { return } - wildcardHost := "*." + host[parentHostDot:] + wildcardHost := "*" + host[parentHostDot:] if r.serveRedirectHTTP(rw, req, wildcardHost) { return @@ -82,7 +82,17 @@ func (r *Router) ServeHTTP(rw http.ResponseWriter, req *http.Request) { } func (r *Router) serveRouteHTTP(rw http.ResponseWriter, req *http.Request, host string) bool { - //fmt.Printf("Router::serveRouteHTTP(%#v, %#v, %s)\n", rw, req, host) + h := r.route[host] + if h != nil { + pairs := h.GetAllKeyValues([]byte(req.URL.Path)) + for i := len(pairs) - 1; i >= 0; i-- { + if pairs[i].Value.Pre || pairs[i].Key == req.URL.Path { + req.URL.Path = strings.TrimPrefix(req.URL.Path, pairs[i].Key) + pairs[i].Value.ServeHTTP(rw, req) + return true + } + } + } return false } diff --git a/target/redirect.go b/target/redirect.go index 2770fc2..5ed9fde 100644 --- a/target/redirect.go +++ b/target/redirect.go @@ -2,6 +2,7 @@ package target import ( "fmt" + "github.com/MrMelon54/violet/utils" "net/http" "net/url" "path" @@ -28,7 +29,7 @@ func (r Redirect) ServeHTTP(rw http.ResponseWriter, req *http.Request) { if !r.Abs { p = path.Join(r.Path, req.URL.Path) } - u := url.URL{ + u := &url.URL{ Scheme: req.URL.Scheme, Host: r.FullHost(), Path: p, @@ -36,7 +37,7 @@ func (r Redirect) ServeHTTP(rw http.ResponseWriter, req *http.Request) { if u.Path == "/" { u.Path = "" } - http.Redirect(rw, req, u.String(), r.Code) + utils.FastRedirect(rw, req, u.String(), r.Code) } func (r Redirect) String() string { diff --git a/target/redirect_test.go b/target/redirect_test.go new file mode 100644 index 0000000..f686526 --- /dev/null +++ b/target/redirect_test.go @@ -0,0 +1,30 @@ +package target + +import ( + "github.com/stretchr/testify/assert" + "net/http" + "net/http/httptest" + "testing" +) + +func TestRedirect_FullHost(t *testing.T) { + assert.Equal(t, "localhost", Redirect{Host: "localhost"}.FullHost()) + assert.Equal(t, "localhost:22", Redirect{Host: "localhost", Port: 22}.FullHost()) +} + +func TestRedirect_ServeHTTP(t *testing.T) { + a := []struct { + Redirect + target string + }{ + {Redirect{Host: "example.com", Path: "/bye", Abs: true, Code: http.StatusFound}, "https://example.com/bye"}, + {Redirect{Host: "example.com", Path: "/bye", Code: http.StatusFound}, "https://example.com/bye/hello/world"}, + } + for _, i := range a { + res := httptest.NewRecorder() + req := httptest.NewRequest(http.MethodGet, "https://www.example.com/hello/world", nil) + i.ServeHTTP(res, req) + assert.Equal(t, i.Code, res.Code) + assert.Equal(t, i.target, res.Header().Get("Location")) + } +} diff --git a/target/route.go b/target/route.go index fa74ecf..b0f7e75 100644 --- a/target/route.go +++ b/target/route.go @@ -1,5 +1,9 @@ package target +import ( + "net/http" +) + type Route struct { Pre bool Host string @@ -7,3 +11,7 @@ type Route struct { Path string Abs bool } + +func (r Route) ServeHTTP(rw http.ResponseWriter, req *http.Request) { + // pass +} diff --git a/utils/fast-redirect.go b/utils/fast-redirect.go new file mode 100644 index 0000000..e82784b --- /dev/null +++ b/utils/fast-redirect.go @@ -0,0 +1,19 @@ +package utils + +import ( + "net/http" +) + +var ( + a1 = []byte("") + a3 = []byte(".\n") +) + +func FastRedirect(rw http.ResponseWriter, req *http.Request, url string, code int) { + rw.Header().Add("Location", url) + rw.WriteHeader(code) + if req.Method == http.MethodGet { + _, _ = rw.Write([]byte(http.StatusText(code))) + } +} diff --git a/utils/fast-redirect_test.go b/utils/fast-redirect_test.go new file mode 100644 index 0000000..f1bfb91 --- /dev/null +++ b/utils/fast-redirect_test.go @@ -0,0 +1,33 @@ +package utils + +import ( + "net/http" + "net/http/httptest" + "testing" +) + +type fakeResponseWriter struct{ h http.Header } + +func (f fakeResponseWriter) Header() http.Header { return f.h } +func (f fakeResponseWriter) Write(bytes []byte) (int, error) { return len(bytes), nil } +func (f fakeResponseWriter) WriteHeader(statusCode int) {} + +func BenchmarkRedirect(b *testing.B) { + b.ReportAllocs() + b.ResetTimer() + res := &fakeResponseWriter{h: make(http.Header, 10)} + req := httptest.NewRequest(http.MethodGet, "https://www.example.com", nil) + for i := 0; i < b.N; i++ { + http.Redirect(res, req, "https://example.com", http.StatusPermanentRedirect) + } +} + +func BenchmarkFastRedirect(b *testing.B) { + b.ReportAllocs() + b.ResetTimer() + res := &fakeResponseWriter{h: make(http.Header, 10)} + req := httptest.NewRequest(http.MethodGet, "https://www.example.com", nil) + for i := 0; i < b.N; i++ { + FastRedirect(res, req, "https://example.com", http.StatusPermanentRedirect) + } +}