├── LICENSE ├── README.md ├── go.mod ├── go.sum ├── hostrouter.go └── hostrouter_test.go /LICENSE: -------------------------------------------------------------------------------- 1 | Copyright (c) 2016-Present https://github.com/go-chi authors 2 | 3 | MIT License 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy of 6 | this software and associated documentation files (the "Software"), to deal in 7 | the Software without restriction, including without limitation the rights to 8 | use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of 9 | the Software, and to permit persons to whom the Software is furnished to do so, 10 | subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS 17 | FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR 18 | COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER 19 | IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN 20 | CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 21 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # HostRouter 2 | 3 | `hostrouter` is a small Go pkg to let you route traffic to different http handlers or routers 4 | based on the request host. This is useful to mount multiple routers on a single server. 5 | 6 | ## Basic usage example 7 | 8 | ```go 9 | //... 10 | func main() { 11 | r := chi.NewRouter() 12 | 13 | r.Use(middleware.RequestID) 14 | r.Use(middleware.RealIP) 15 | r.Use(middleware.Logger) 16 | r.Use(middleware.Recoverer) 17 | 18 | hr := hostrouter.New() 19 | 20 | // Requests to api.domain.com 21 | hr.Map("", apiRouter()) // default 22 | hr.Map("api.domain.com", apiRouter()) 23 | 24 | // Requests to doma.in 25 | hr.Map("doma.in", shortUrlRouter()) 26 | 27 | // Requests to *.doma.in 28 | hr.Map("*.doma.in", shortUrlRouter()) 29 | 30 | // Requests to host that isn't defined above 31 | hr.Map("*", everythingElseRouter()) 32 | 33 | // Mount the host router 34 | r.Mount("/", hr) 35 | 36 | http.ListenAndServe(":3333", r) 37 | } 38 | 39 | // Router for the API service 40 | func apiRouter() chi.Router { 41 | r := chi.NewRouter() 42 | r.Get("/", apiIndexHandler) 43 | // ... 44 | return r 45 | } 46 | 47 | // Router for the Short URL service 48 | func shortUrlRouter() chi.Router { 49 | r := chi.NewRouter() 50 | r.Get("/", shortIndexHandler) 51 | // ... 52 | return r 53 | } 54 | ``` 55 | -------------------------------------------------------------------------------- /go.mod: -------------------------------------------------------------------------------- 1 | module github.com/go-chi/hostrouter 2 | 3 | go 1.16 4 | 5 | require github.com/go-chi/chi/v5 v5.2.0 6 | -------------------------------------------------------------------------------- /go.sum: -------------------------------------------------------------------------------- 1 | github.com/go-chi/chi/v5 v5.2.0 h1:Aj1EtB0qR2Rdo2dG4O94RIU35w2lvQSj6BRA4+qwFL0= 2 | github.com/go-chi/chi/v5 v5.2.0/go.mod h1:DslCQbL2OYiznFReuXYUmQ2hGd1aDpCnlMNITLSKoi8= 3 | -------------------------------------------------------------------------------- /hostrouter.go: -------------------------------------------------------------------------------- 1 | package hostrouter 2 | 3 | import ( 4 | "net/http" 5 | "strings" 6 | 7 | "github.com/go-chi/chi/v5" 8 | ) 9 | 10 | type Routes map[string]chi.Router 11 | 12 | var _ chi.Routes = Routes{} 13 | 14 | func New() Routes { 15 | return Routes{} 16 | } 17 | 18 | func (hr Routes) Match(rctx *chi.Context, method, path string) bool { 19 | return true 20 | } 21 | 22 | func (hr Routes) Find(rctx *chi.Context, method, path string) string { 23 | return "" 24 | } 25 | 26 | func (hr Routes) Map(host string, h chi.Router) { 27 | hr[strings.ToLower(host)] = h 28 | } 29 | 30 | func (hr Routes) Unmap(host string) { 31 | delete(hr, strings.ToLower(host)) 32 | } 33 | 34 | func (hr Routes) ServeHTTP(w http.ResponseWriter, r *http.Request) { 35 | host := requestHost(r) 36 | if router, ok := hr[strings.ToLower(host)]; ok { 37 | router.ServeHTTP(w, r) 38 | return 39 | } 40 | if router, ok := hr[strings.ToLower(getWildcardHost(host))]; ok { 41 | router.ServeHTTP(w, r) 42 | return 43 | } 44 | if router, ok := hr["*"]; ok { 45 | router.ServeHTTP(w, r) 46 | return 47 | } 48 | http.Error(w, http.StatusText(404), 404) 49 | } 50 | 51 | func (hr Routes) Routes() []chi.Route { 52 | return hr[""].Routes() 53 | } 54 | 55 | func (hr Routes) Middlewares() chi.Middlewares { 56 | return chi.Middlewares{} 57 | } 58 | 59 | func requestHost(r *http.Request) (host string) { 60 | // not standard, but most popular 61 | host = r.Header.Get("X-Forwarded-Host") 62 | if host != "" { 63 | return 64 | } 65 | 66 | // RFC 7239 67 | host = r.Header.Get("Forwarded") 68 | _, _, host = parseForwarded(host) 69 | if host != "" { 70 | return 71 | } 72 | 73 | // if all else fails fall back to request host 74 | host = r.Host 75 | return 76 | } 77 | 78 | func parseForwarded(forwarded string) (addr, proto, host string) { 79 | if forwarded == "" { 80 | return 81 | } 82 | for _, forwardedPair := range strings.Split(forwarded, ";") { 83 | if tv := strings.SplitN(forwardedPair, "=", 2); len(tv) == 2 { 84 | token, value := tv[0], tv[1] 85 | token = strings.TrimSpace(token) 86 | value = strings.TrimSpace(strings.Trim(value, `"`)) 87 | switch strings.ToLower(token) { 88 | case "for": 89 | addr = value 90 | case "proto": 91 | proto = value 92 | case "host": 93 | host = value 94 | } 95 | 96 | } 97 | } 98 | return 99 | } 100 | 101 | func getWildcardHost(host string) string { 102 | parts := strings.Split(host, ".") 103 | if len(parts) > 1 { 104 | wildcard := append([]string{"*"}, parts[1:]...) 105 | return strings.Join(wildcard, ".") 106 | } 107 | return strings.Join(parts, ".") 108 | } 109 | -------------------------------------------------------------------------------- /hostrouter_test.go: -------------------------------------------------------------------------------- 1 | package hostrouter 2 | 3 | import "testing" 4 | 5 | func Test_getWildcardHost(t *testing.T) { 6 | type args struct { 7 | host string 8 | } 9 | tests := []struct { 10 | name string 11 | args args 12 | want string 13 | }{ 14 | {"no wildcard in 1-part host", args{"com"}, "com"}, 15 | {"wildcard in 2-part", args{"dot.com"}, "*.com"}, 16 | {"wildcard in 3-part", args{"amazing.dot.com"}, "*.dot.com"}, 17 | } 18 | for _, tt := range tests { 19 | t.Run(tt.name, func(t *testing.T) { 20 | if got := getWildcardHost(tt.args.host); got != tt.want { 21 | t.Errorf("getWildcardHost() = %v, want %v", got, tt.want) 22 | } 23 | }) 24 | } 25 | } 26 | --------------------------------------------------------------------------------