From 77d570ac1e6d64a479470efd0af4db2740e1a236 Mon Sep 17 00:00:00 2001 From: MrMelon54 Date: Wed, 19 Apr 2023 01:30:38 +0100 Subject: [PATCH] Add base routing system and add redirects --- .idea/vcs.xml | 6 ++ go.mod | 3 +- go.sum | 6 +- router/router.go | 102 ++++++++++++++++++++++++++++++ router/router_test.go | 140 ++++++++++++++++++++++++++++++++++++++++++ target/redirect.go | 35 ++++++++++- target/route.go | 9 +++ target/service.go | 7 --- 8 files changed, 292 insertions(+), 16 deletions(-) create mode 100644 .idea/vcs.xml create mode 100644 router/router.go create mode 100644 router/router_test.go create mode 100644 target/route.go delete mode 100644 target/service.go diff --git a/.idea/vcs.xml b/.idea/vcs.xml new file mode 100644 index 0000000..94a25f7 --- /dev/null +++ b/.idea/vcs.xml @@ -0,0 +1,6 @@ + + + + + + \ No newline at end of file diff --git a/go.mod b/go.mod index 75ed3dc..dad864c 100644 --- a/go.mod +++ b/go.mod @@ -3,9 +3,8 @@ module github.com/MrMelon54/violet go 1.20 require ( - github.com/julienschmidt/httprouter v1.3.0 + github.com/MrMelon54/trie v0.0.2 github.com/stretchr/testify v1.8.2 - github.com/yousuf64/shift v0.4.0 golang.org/x/net v0.9.0 ) diff --git a/go.sum b/go.sum index 499b05b..c8c4a50 100644 --- a/go.sum +++ b/go.sum @@ -1,8 +1,8 @@ +github.com/MrMelon54/trie v0.0.2 h1:ZXWcX5ij62O9K4I/anuHmVg8L3tF0UGdlPceAASwKEY= +github.com/MrMelon54/trie v0.0.2/go.mod h1:sGCGOcqb+DxSxvHgSOpbpkmA7mFZR47YDExy9OCbVZI= github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= -github.com/julienschmidt/httprouter v1.3.0 h1:U0609e9tgbseu3rBINet9P48AI/D3oJs4dN7jwJOQ1U= -github.com/julienschmidt/httprouter v1.3.0/go.mod h1:JR6WtHb+2LUe8TCKY3cZOxFyyO8IZAc4RVcycCCAKdM= github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= @@ -12,8 +12,6 @@ github.com/stretchr/testify v1.7.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/ github.com/stretchr/testify v1.8.0/go.mod h1:yNjHg4UonilssWZ8iaSj1OCr/vHnekPRkoO+kdMU+MU= github.com/stretchr/testify v1.8.2 h1:+h33VjcLVPDHtOdpUCuF+7gSuG3yGIftsP1YvFihtJ8= github.com/stretchr/testify v1.8.2/go.mod h1:w2LPCIKwWwSfY2zedu0+kehJoqGctiVI29o6fzry7u4= -github.com/yousuf64/shift v0.4.0 h1:jiuYPa9KGyTShM08GEcSRSaZeO2jPEIpdqaplsRtk8g= -github.com/yousuf64/shift v0.4.0/go.mod h1:D9b+mj37s3goL48EGX2oCrYHW4rg4c3Il2w6fukjxas= golang.org/x/net v0.9.0 h1:aWJ/m6xSmxWBx+V0XRHTlrYrPG56jKsLdTFmsSsCzOM= golang.org/x/net v0.9.0/go.mod h1:d48xBJpPfHeWQsugry2m+kC02ZBRGRgulfHnEXEuWns= gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405 h1:yhCVgyC4o1eVCa2tZl7eS0r+SDo693bJlVdllGtEeKM= diff --git a/router/router.go b/router/router.go new file mode 100644 index 0000000..f5df533 --- /dev/null +++ b/router/router.go @@ -0,0 +1,102 @@ +package router + +import ( + "fmt" + "github.com/MrMelon54/trie" + "github.com/MrMelon54/violet/target" + "net/http" + "net/http/httputil" + "strings" +) + +type Router struct { + route map[string]*trie.Trie[target.Route] + redirect map[string]*trie.Trie[target.Redirect] + notFound http.Handler + proxy *httputil.ReverseProxy +} + +func New() *Router { + return &Router{ + route: make(map[string]*trie.Trie[target.Route]), + redirect: make(map[string]*trie.Trie[target.Redirect]), + notFound: http.HandlerFunc(func(rw http.ResponseWriter, req *http.Request) { + _, _ = fmt.Fprintf(rw, "%d %s\n", http.StatusNotFound, http.StatusText(http.StatusNotFound)) + }), + } +} + +func (r *Router) hostRoute(host string) *trie.Trie[target.Route] { + h := r.route[host] + if h == nil { + h = &trie.Trie[target.Route]{} + r.route[host] = h + } + return h +} + +func (r *Router) hostRedirect(host string) *trie.Trie[target.Redirect] { + h := r.redirect[host] + if h == nil { + h = &trie.Trie[target.Redirect]{} + r.redirect[host] = h + } + return h +} + +func (r *Router) AddService(host string, t target.Route) { + r.AddRoute(host, "", t) +} + +func (r *Router) AddRoute(host string, path string, t target.Route) { + r.hostRoute(host).PutString(path, t) +} + +func (r *Router) AddRedirect(host, path string, t target.Redirect) { + r.hostRedirect(host).PutString(path, t) +} + +func (r *Router) ServeHTTP(rw http.ResponseWriter, req *http.Request) { + host := req.Host + if r.serveRedirectHTTP(rw, req, host) { + return + } + if r.serveRouteHTTP(rw, req, host) { + return + } + + parentHostDot := strings.IndexByte(host, '.') + if parentHostDot == -1 { + r.notFound.ServeHTTP(rw, req) + return + } + + wildcardHost := "*." + host[parentHostDot:] + + if r.serveRedirectHTTP(rw, req, wildcardHost) { + return + } + if r.serveRouteHTTP(rw, req, wildcardHost) { + return + } +} + +func (r *Router) serveRouteHTTP(rw http.ResponseWriter, req *http.Request, host string) bool { + //fmt.Printf("Router::serveRouteHTTP(%#v, %#v, %s)\n", rw, req, host) + return false +} + +func (r *Router) serveRedirectHTTP(rw http.ResponseWriter, req *http.Request, host string) bool { + h := r.redirect[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/router/router_test.go b/router/router_test.go new file mode 100644 index 0000000..c4dabf0 --- /dev/null +++ b/router/router_test.go @@ -0,0 +1,140 @@ +package router + +import ( + "github.com/MrMelon54/violet/target" + "net/http" + "net/http/httptest" + "net/url" + "testing" +) + +type redirectTestBase struct { + path string + dst target.Redirect + tests map[string]string +} + +type mss map[string]string + +var redirectTests = []redirectTestBase{ + {"/", target.Redirect{}, mss{ + "/": "/", + "/hello": "", + }}, + {"/", target.Redirect{Path: "world"}, mss{ + "/": "/world", + "/hello": "", + }}, + {"/", target.Redirect{Abs: true}, mss{ + "/": "/", + "/hello": "", + }}, + {"/", target.Redirect{Abs: true, Path: "world"}, mss{ + "/": "/world", + "/hello": "", + }}, + {"/", target.Redirect{Pre: true}, mss{ + "/": "/", + "/hello": "/hello", + }}, + {"/", target.Redirect{Pre: true, Path: "world"}, mss{ + "/": "/world", + "/hello": "/world/hello", + }}, + {"/", target.Redirect{Pre: true, Abs: true}, mss{ + "/": "/", + "/hello": "/", + }}, + {"/", target.Redirect{Pre: true, Abs: true, Path: "world"}, mss{ + "/": "/world", + "/hello": "/world", + }}, + {"/hello", target.Redirect{}, mss{ + "/": "", + "/hello": "/", + "/hello/hi": "", + }}, + {"/hello", target.Redirect{Path: "world"}, mss{ + "/": "", + "/hello": "/world", + "/hello/hi": "", + }}, + {"/hello", target.Redirect{Abs: true}, mss{ + "/": "", + "/hello": "/", + "/hello/hi": "", + }}, + {"/hello", target.Redirect{Abs: true, Path: "world"}, mss{ + "/": "", + "/hello": "/world", + "/hello/hi": "", + }}, + {"/hello", target.Redirect{Pre: true}, mss{ + "/": "", + "/hello": "/", + "/hello/hi": "/hi", + }}, + {"/hello", target.Redirect{Pre: true, Path: "world"}, mss{ + "/": "", + "/hello": "/world", + "/hello/hi": "/world/hi", + }}, + {"/hello", target.Redirect{Pre: true, Abs: true}, mss{ + "/": "", + "/hello": "/", + "/hello/hi": "/", + }}, + {"/hello", target.Redirect{Pre: true, Abs: true, Path: "world"}, mss{ + "/": "", + "/hello": "/world", + "/hello/hi": "/world", + }}, +} + +func assertHttpRedirect(t *testing.T, r *Router, code int, target, method, start string) { + res := httptest.NewRecorder() + req := httptest.NewRequest(method, start, nil) + r.ServeHTTP(res, req) + l := res.Header().Get("Location") + if target == "" { + if code == res.Code || "" != l { + t.Logf("Test URL: %#v\n", req.URL) + t.Log(r.redirect["www.example.com"].String()) + t.Fatalf("%s => %s\n", start, target) + } + } else { + if code != res.Code || target != l { + t.Logf("Test URL: %#v\n", req.URL) + t.Log(r.redirect["www.example.com"].String()) + t.Fatalf("\nexpected %s => %s\n got %s => %s\n", start, target, start, l) + } + } +} + +func TestRouter_AddRedirect(t *testing.T) { + for _, i := range redirectTests { + r := New() + dst := i.dst + dst.Host = "example.com" + dst.Code = http.StatusFound + t.Logf("Running tests for %#v\n", dst) + r.AddRedirect("www.example.com", i.path, dst) + for k, v := range i.tests { + u1 := &url.URL{Scheme: "https", Host: "example.com", Path: v} + if v == "" { + u1 = nil + } else if v == "/" { + u1.Path = "" + } + u2 := &url.URL{Scheme: "https", Host: "www.example.com", Path: k} + assertHttpRedirect(t, r, http.StatusFound, outputUrl(u1), http.MethodGet, outputUrl(u2)) + } + } +} + +func outputUrl(u *url.URL) string { + if u == nil { + return "" + } + return u.String() +} diff --git a/target/redirect.go b/target/redirect.go index 10c4c88..2770fc2 100644 --- a/target/redirect.go +++ b/target/redirect.go @@ -1,15 +1,44 @@ package target import ( + "fmt" "net/http" "net/url" + "path" ) type Redirect struct { - url.URL + Pre bool + Host string + Port int + Path string + Abs bool Code int } -func (r Redirect) Handler() http.Handler { - return http.RedirectHandler(r.URL.String(), r.Code) +func (r Redirect) FullHost() string { + if r.Port == 0 { + return r.Host + } + return fmt.Sprintf("%s:%d", r.Host, r.Port) +} + +func (r Redirect) ServeHTTP(rw http.ResponseWriter, req *http.Request) { + p := r.Path + if !r.Abs { + p = path.Join(r.Path, req.URL.Path) + } + u := url.URL{ + Scheme: req.URL.Scheme, + Host: r.FullHost(), + Path: p, + } + if u.Path == "/" { + u.Path = "" + } + http.Redirect(rw, req, u.String(), r.Code) +} + +func (r Redirect) String() string { + return fmt.Sprintf("%#v", r) } diff --git a/target/route.go b/target/route.go new file mode 100644 index 0000000..fa74ecf --- /dev/null +++ b/target/route.go @@ -0,0 +1,9 @@ +package target + +type Route struct { + Pre bool + Host string + Port int + Path string + Abs bool +} diff --git a/target/service.go b/target/service.go deleted file mode 100644 index c9dea4d..0000000 --- a/target/service.go +++ /dev/null @@ -1,7 +0,0 @@ -package target - -type Service struct { - Host string - Port int - Absolute bool -}