From fb0d4d24bd27aee5579b8f7ec417775c4b7e74d7 Mon Sep 17 00:00:00 2001 From: Adnan Maolood Date: Sat, 26 Jun 2021 18:50:05 -0400 Subject: [PATCH] mux: Remove support for handling schemes Also fix redirection to subtree roots for wildcard patterns and patterns without a host name. --- mux.go | 148 ++++++++++++++++++----------------------- mux_test.go | 187 ++-------------------------------------------------- 2 files changed, 69 insertions(+), 266 deletions(-) diff --git a/mux.go b/mux.go index fc3663d..3929ab8 100644 --- a/mux.go +++ b/mux.go @@ -32,31 +32,15 @@ import ( // the pattern "/" matches all paths not matched by other registered // patterns, not just the URL with Path == "/". // -// Patterns may also contain schemes and hostnames. -// Wildcard patterns can be used to match multiple hostnames (e.g. "*.example.com"). +// Patterns may optionally begin with a host name, restricting matches to +// URLs on that host only. Host-specific patterns take precedence over +// general patterns, so that a handler might register for the two patterns +// "/search" and "search.example.com/" without also taking over requests +// for "gemini://example.com/". // -// The following are examples of valid patterns, along with the scheme, -// hostname, and path that they match. -// -// Pattern │ Scheme │ Hostname │ Path -// ─────────────────────────────┼────────┼──────────┼───────────── -// /file │ gemini │ * │ /file -// /directory/ │ gemini │ * │ /directory/* -// hostname/file │ gemini │ hostname │ /file -// hostname/directory/ │ gemini │ hostname │ /directory/* -// scheme://hostname/file │ scheme │ hostname │ /file -// scheme://hostname/directory/ │ scheme │ hostname │ /directory/* -// //hostname/file │ * │ hostname │ /file -// //hostname/directory/ │ * │ hostname │ /directory/* -// scheme:///file │ scheme │ * │ /file -// scheme:///directory/ │ scheme │ * │ /directory/* -// ///file │ * │ * │ /file -// ///directory/ │ * │ * │ /directory/* -// -// A pattern without a hostname will match any hostname. -// If a pattern begins with "//", it will match any scheme. -// Otherwise, a pattern with no scheme is treated as though it has a -// scheme of "gemini". +// Wildcard patterns can be used to match multiple hostnames, so that the +// pattern "*.example.com" will match requests for "blog.example.com" and +// "gemini.example.com", but not "example.org". // // If a subtree has been registered and a request is received naming the // subtree root without its trailing slash, Mux redirects that @@ -71,19 +55,19 @@ import ( // to an equivalent, cleaner URL. type Mux struct { mu sync.RWMutex - m map[muxKey]Handler + m map[hostpath]Handler es []muxEntry // slice of entries sorted from longest to shortest } -type muxKey struct { - scheme string - host string - path string +type hostpath struct { + host string + path string } type muxEntry struct { handler Handler - key muxKey + host string + path string } // cleanPath returns the canonical path for p, eliminating . and .. elements. @@ -110,24 +94,17 @@ func cleanPath(p string) string { // Find a handler on a handler map given a path string. // Most-specific (longest) pattern wins. -func (mux *Mux) match(key muxKey) Handler { +func (mux *Mux) match(host, path string) Handler { // Check for exact match first. - if r, ok := mux.m[key]; ok { - return r - } else if r, ok := mux.m[muxKey{"", key.host, key.path}]; ok { - return r - } else if r, ok := mux.m[muxKey{key.scheme, "", key.path}]; ok { - return r - } else if r, ok := mux.m[muxKey{"", "", key.path}]; ok { - return r + if h, ok := mux.m[hostpath{host, path}]; ok { + return h } // Check for longest valid match. mux.es contains all patterns // that end in / sorted from longest to shortest. for _, e := range mux.es { - if (e.key.scheme == "" || key.scheme == e.key.scheme) && - (e.key.host == "" || key.host == e.key.host) && - strings.HasPrefix(key.path, e.key.path) { + if len(e.host) == len(host) && e.host == host && + strings.HasPrefix(path, e.path) { return e.handler } } @@ -138,31 +115,32 @@ func (mux *Mux) match(key muxKey) Handler { // This occurs when a handler for path + "/" was already registered, but // not for path itself. If the path needs appending to, it creates a new // URL, setting the path to u.Path + "/" and returning true to indicate so. -func (mux *Mux) redirectToPathSlash(key muxKey, u *url.URL) (*url.URL, bool) { +func (mux *Mux) redirectToPathSlash(host, path string, u *url.URL) (*url.URL, bool) { mux.mu.RLock() - shouldRedirect := mux.shouldRedirectRLocked(key) + shouldRedirect := mux.shouldRedirectRLocked(host, path) mux.mu.RUnlock() if !shouldRedirect { return u, false } - return u.ResolveReference(&url.URL{Path: key.path + "/"}), true + return u.ResolveReference(&url.URL{Path: path + "/"}), true } // shouldRedirectRLocked reports whether the given path and host should be redirected to // path+"/". This should happen if a handler is registered for path+"/" but // not path -- see comments at Mux. -func (mux *Mux) shouldRedirectRLocked(key muxKey) bool { - if _, exist := mux.m[key]; exist { +func (mux *Mux) shouldRedirectRLocked(host, path string) bool { + if _, exist := mux.m[hostpath{host, path}]; exist { return false } - n := len(key.path) + n := len(path) if n == 0 { return false } - if _, exist := mux.m[muxKey{key.scheme, key.host, key.path + "/"}]; exist { - return key.path[n-1] != '/' + if _, exist := mux.m[hostpath{host, path + "/"}]; exist { + return path[n-1] != '/' } + return false } @@ -182,13 +160,17 @@ func getWildcard(hostname string) (string, bool) { // internally-generated handler that redirects to the canonical path. If the // host contains a port, it is ignored when matching handlers. func (mux *Mux) Handler(r *Request) Handler { - scheme := r.URL.Scheme + // Disallow non-Gemini schemes + if r.URL.Scheme != "gemini" { + return NotFoundHandler() + } + host := r.URL.Hostname() path := cleanPath(r.URL.Path) // If the given path is /tree and its handler is not registered, // redirect for /tree/. - if u, ok := mux.redirectToPathSlash(muxKey{scheme, host, path}, r.URL); ok { + if u, ok := mux.redirectToPathSlash(host, path, r.URL); ok { return StatusHandler(StatusPermanentRedirect, u.String()) } @@ -201,16 +183,30 @@ func (mux *Mux) Handler(r *Request) Handler { mux.mu.RLock() defer mux.mu.RUnlock() - h := mux.match(muxKey{scheme, host, path}) + h := mux.match(host, path) + if h == nil { // Try wildcard if wildcard, ok := getWildcard(host); ok { - h = mux.match(muxKey{scheme, wildcard, path}) + if u, ok := mux.redirectToPathSlash(wildcard, path, r.URL); ok { + return StatusHandler(StatusPermanentRedirect, u.String()) + } + h = mux.match(wildcard, path) } } + + if h == nil { + // Try empty host + if u, ok := mux.redirectToPathSlash("", path, r.URL); ok { + return StatusHandler(StatusPermanentRedirect, u.String()) + } + h = mux.match("", path) + } + if h == nil { h = NotFoundHandler() } + return h } @@ -234,48 +230,32 @@ func (mux *Mux) Handle(pattern string, handler Handler) { mux.mu.Lock() defer mux.mu.Unlock() - var key muxKey - if strings.HasPrefix(pattern, "//") { - // match any scheme - key.scheme = "" - pattern = pattern[2:] - } else { - // extract scheme - cut := strings.Index(pattern, "://") - if cut == -1 { - // default scheme of gemini - key.scheme = "gemini" - } else { - key.scheme = pattern[:cut] - pattern = pattern[cut+3:] - } - } - + var host, path string // extract hostname and path cut := strings.Index(pattern, "/") if cut == -1 { - key.host = pattern - key.path = "/" + host = pattern + path = "/" } else { - key.host = pattern[:cut] - key.path = pattern[cut:] + host = pattern[:cut] + path = pattern[cut:] } // strip port from hostname - if hostname, _, err := net.SplitHostPort(key.host); err == nil { - key.host = hostname + if hostname, _, err := net.SplitHostPort(host); err == nil { + host = hostname } - if _, exist := mux.m[key]; exist { + if _, exist := mux.m[hostpath{host, path}]; exist { panic("gemini: multiple registrations for " + pattern) } if mux.m == nil { - mux.m = make(map[muxKey]Handler) + mux.m = make(map[hostpath]Handler) } - mux.m[key] = handler - e := muxEntry{handler, key} - if key.path[len(key.path)-1] == '/' { + mux.m[hostpath{host, path}] = handler + e := muxEntry{handler, host, path} + if path[len(path)-1] == '/' { mux.es = appendSorted(mux.es, e) } } @@ -283,9 +263,7 @@ func (mux *Mux) Handle(pattern string, handler Handler) { func appendSorted(es []muxEntry, e muxEntry) []muxEntry { n := len(es) i := sort.Search(n, func(i int) bool { - return len(es[i].key.scheme) < len(e.key.scheme) || - len(es[i].key.host) < len(es[i].key.host) || - len(es[i].key.path) < len(e.key.path) + return len(es[i].path) < len(e.path) }) if i == n { return append(es, e) diff --git a/mux_test.go b/mux_test.go index 47e1ca7..2a2101f 100644 --- a/mux_test.go +++ b/mux_test.go @@ -21,7 +21,7 @@ func TestMuxMatch(t *testing.T) { Matches []Match }{ { - // scheme: gemini, hostname: *, path: /* + // hostname: *, path: /* Pattern: "/", Matches: []Match{ {"gemini://example.com/path", true}, @@ -34,7 +34,7 @@ func TestMuxMatch(t *testing.T) { }, }, { - // scheme: gemini, hostname: *, path: /path + // hostname: *, path: /path Pattern: "/path", Matches: []Match{ {"gemini://example.com/path", true}, @@ -47,7 +47,7 @@ func TestMuxMatch(t *testing.T) { }, }, { - // scheme: gemini, hostname: *, path: /subtree/* + // hostname: *, path: /subtree/* Pattern: "/subtree/", Matches: []Match{ {"gemini://example.com/subtree/", true}, @@ -62,7 +62,7 @@ func TestMuxMatch(t *testing.T) { }, }, { - // scheme: gemini, hostname: example.com, path: /* + // hostname: example.com, path: /* Pattern: "example.com", Matches: []Match{ {"gemini://example.com/path", true}, @@ -75,7 +75,7 @@ func TestMuxMatch(t *testing.T) { }, }, { - // scheme: gemini, hostname: example.com, path: /path + // hostname: example.com, path: /path Pattern: "example.com/path", Matches: []Match{ {"gemini://example.com/path", true}, @@ -88,7 +88,7 @@ func TestMuxMatch(t *testing.T) { }, }, { - // scheme: gemini, hostname: example.com, path: /subtree/* + // hostname: example.com, path: /subtree/* Pattern: "example.com/subtree/", Matches: []Match{ {"gemini://example.com/subtree/", true}, @@ -102,170 +102,6 @@ func TestMuxMatch(t *testing.T) { {"http://example.com/subtree/", false}, }, }, - { - // scheme: http, hostname: example.com, path: /* - Pattern: "http://example.com", - Matches: []Match{ - {"http://example.com/path", true}, - {"http://example.com/", true}, - {"http://example.com/path.gmi", true}, - {"http://example.com/path/", true}, - {"http://example.org/path", false}, - {"gemini://example.com/path", false}, - {"gemini://example.org/path", false}, - }, - }, - { - // scheme: http, hostname: example.com, path: /path - Pattern: "http://example.com/path", - Matches: []Match{ - {"http://example.com/path", true}, - {"http://example.com/", false}, - {"http://example.com/path.gmi", false}, - {"http://example.com/path/", false}, - {"http://example.org/path", false}, - {"gemini://example.com/path", false}, - {"gemini://example.org/path", false}, - }, - }, - { - // scheme: http, hostname: example.com, path: /subtree/* - Pattern: "http://example.com/subtree/", - Matches: []Match{ - {"http://example.com/subtree/", true}, - {"http://example.com/subtree/nested/", true}, - {"http://example.com/subtree/nested/file", true}, - {"http://example.org/subtree/", false}, - {"http://example.org/subtree/nested/", false}, - {"http://example.org/subtree/nested/file", false}, - {"http://example.com/subtree", false}, - {"http://www.example.com/subtree/", false}, - {"gemini://example.com/subtree/", false}, - }, - }, - { - // scheme: *, hostname: example.com, path: /* - Pattern: "//example.com", - Matches: []Match{ - {"gemini://example.com/path", true}, - {"gemini://example.com/", true}, - {"gemini://example.com/path.gmi", true}, - {"gemini://example.com/path/", true}, - {"gemini://example.org/path", false}, - {"http://example.com/path", true}, - {"http://example.org/path", false}, - }, - }, - { - // scheme: *, hostname: example.com, path: /path - Pattern: "//example.com/path", - Matches: []Match{ - {"gemini://example.com/path", true}, - {"gemini://example.com/", false}, - {"gemini://example.com/path.gmi", false}, - {"gemini://example.com/path/", false}, - {"gemini://example.org/path", false}, - {"http://example.com/path", true}, - {"http://example.org/path", false}, - }, - }, - { - // scheme: *, hostname: example.com, path: /subtree/* - Pattern: "//example.com/subtree/", - Matches: []Match{ - {"gemini://example.com/subtree/", true}, - {"gemini://example.com/subtree/nested/", true}, - {"gemini://example.com/subtree/nested/file", true}, - {"gemini://example.org/subtree/", false}, - {"gemini://example.org/subtree/nested/", false}, - {"gemini://example.org/subtree/nested/file", false}, - {"gemini://example.com/subtree", false}, - {"gemini://www.example.com/subtree/", false}, - {"http://example.com/subtree/", true}, - }, - }, - { - // scheme: http, hostname: *, path: /* - Pattern: "http://", - Matches: []Match{ - {"http://example.com/path", true}, - {"http://example.com/", true}, - {"http://example.com/path.gmi", true}, - {"http://example.com/path/", true}, - {"http://example.org/path", true}, - {"gemini://example.com/path", false}, - {"gemini://example.org/path", false}, - }, - }, - { - // scheme: http, hostname: *, path: /path - Pattern: "http:///path", - Matches: []Match{ - {"http://example.com/path", true}, - {"http://example.com/", false}, - {"http://example.com/path.gmi", false}, - {"http://example.com/path/", false}, - {"http://example.org/path", true}, - {"gemini://example.com/path", false}, - {"gemini://example.org/path", false}, - }, - }, - { - // scheme: http, hostname: *, path: /subtree/* - Pattern: "http:///subtree/", - Matches: []Match{ - {"http://example.com/subtree/", true}, - {"http://example.com/subtree/nested/", true}, - {"http://example.com/subtree/nested/file", true}, - {"http://example.org/subtree/", true}, - {"http://example.org/subtree/nested/", true}, - {"http://example.org/subtree/nested/file", true}, - {"http://example.com/subtree", false}, - {"http://www.example.com/subtree/", true}, - {"gemini://example.com/subtree/", false}, - }, - }, - { - // scheme: *, hostname: *, path: /* - Pattern: "//", - Matches: []Match{ - {"gemini://example.com/path", true}, - {"gemini://example.com/", true}, - {"gemini://example.com/path.gmi", true}, - {"gemini://example.com/path/", true}, - {"gemini://example.org/path", true}, - {"http://example.com/path", true}, - {"http://example.org/path", true}, - }, - }, - { - // scheme: *, hostname: *, path: /path - Pattern: "///path", - Matches: []Match{ - {"gemini://example.com/path", true}, - {"gemini://example.com/", false}, - {"gemini://example.com/path.gmi", false}, - {"gemini://example.com/path/", false}, - {"gemini://example.org/path", true}, - {"http://example.com/path", true}, - {"http://example.org/path", true}, - }, - }, - { - // scheme: *, hostname: *, path: /subtree/* - Pattern: "///subtree/", - Matches: []Match{ - {"gemini://example.com/subtree/", true}, - {"gemini://example.com/subtree/nested/", true}, - {"gemini://example.com/subtree/nested/file", true}, - {"gemini://example.org/subtree/", true}, - {"gemini://example.org/subtree/nested/", true}, - {"gemini://example.org/subtree/nested/file", true}, - {"gemini://example.com/subtree", false}, - {"gemini://www.example.com/subtree/", true}, - {"http://example.com/subtree/", true}, - }, - }, { // scheme: gemini, hostname: *.example.com, path: /* Pattern: "*.example.com", @@ -277,17 +113,6 @@ func TestMuxMatch(t *testing.T) { {"http://www.example.com/", false}, }, }, - { - // scheme: http, hostname: *.example.com, path: /* - Pattern: "http://*.example.com", - Matches: []Match{ - {"http://mail.example.com/", true}, - {"http://www.example.com/index.gmi", true}, - {"http://example.com/", false}, - {"http://a.b.example.com/", false}, - {"gemini://www.example.com/", false}, - }, - }, } for i, test := range tests {