Add base routing system and add redirects

This commit is contained in:
Melon 2023-04-19 01:30:38 +01:00
parent 68bbd67c01
commit 77d570ac1e
Signed by: melon
GPG Key ID: 6C9D970C50D26A25
8 changed files with 292 additions and 16 deletions

6
.idea/vcs.xml Normal file
View File

@ -0,0 +1,6 @@
<?xml version="1.0" encoding="UTF-8"?>
<project version="4">
<component name="VcsDirectoryMappings">
<mapping directory="$PROJECT_DIR$" vcs="Git" />
</component>
</project>

3
go.mod
View File

@ -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
)

6
go.sum
View File

@ -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=

102
router/router.go Normal file
View File

@ -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
}

140
router/router_test.go Normal file
View File

@ -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()
}

View File

@ -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)
}

9
target/route.go Normal file
View File

@ -0,0 +1,9 @@
package target
type Route struct {
Pre bool
Host string
Port int
Path string
Abs bool
}

View File

@ -1,7 +0,0 @@
package target
type Service struct {
Host string
Port int
Absolute bool
}