From 10760a256f471a2550e8d1457b96cd64e218a829 Mon Sep 17 00:00:00 2001 From: adnano Date: Mon, 28 Sep 2020 03:15:19 -0400 Subject: [PATCH] Use net/http implementation of ServeMux --- client.go | 1 - server.go | 378 +++++++++++++++++++++++++++++++++++++++++------------- 2 files changed, 288 insertions(+), 91 deletions(-) diff --git a/client.go b/client.go index a8f97ec..6cd7baf 100644 --- a/client.go +++ b/client.go @@ -9,7 +9,6 @@ import ( "net" "net/url" "strconv" - "strings" "time" ) diff --git a/server.go b/server.go index ae0d8e9..9a2198c 100644 --- a/server.go +++ b/server.go @@ -10,10 +10,12 @@ import ( "net" "net/url" "os" + "path" "path/filepath" "sort" "strconv" "strings" + "sync" "time" ) @@ -331,82 +333,6 @@ func CertificateHandler(f func(*x509.Certificate)) Handler { }) } -// ServeMux is a Gemini request multiplexer. -// It matches the URL of each incoming request against a list of registered -// patterns and calls the handler for the pattern that most closesly matches -// the URL. -type ServeMux struct { - entries []muxEntry -} - -type muxEntry struct { - u *url.URL - handler Handler -} - -func (m *ServeMux) match(url *url.URL) Handler { - for _, e := range m.entries { - if (e.u.Scheme == "" || url.Scheme == e.u.Scheme) && - (e.u.Host == "" || url.Host == e.u.Host) && - strings.HasPrefix(url.Path, e.u.Path) { - return e.handler - } - } - return nil -} - -// Handle registers a Handler for the given pattern. -func (m *ServeMux) Handle(pattern string, handler Handler) { - url, err := url.Parse(pattern) - if err != nil { - panic(err) - } - e := muxEntry{ - url, - handler, - } - m.entries = appendSorted(m.entries, e) -} - -// HandleFunc registers a HandlerFunc for the given pattern. -func (m *ServeMux) HandleFunc(pattern string, handlerFunc func(*ResponseWriter, *Request)) { - handler := HandlerFunc(handlerFunc) - m.Handle(pattern, handler) -} - -// Serve responds to the request with the appropriate handler. -func (m *ServeMux) Serve(rw *ResponseWriter, req *Request) { - h := m.match(req.URL) - if h == nil { - NotFound(rw, req) - return - } - h.Serve(rw, req) -} - -// appendSorted appends the entry e in the proper place in entries. -func appendSorted(es []muxEntry, e muxEntry) []muxEntry { - n := len(es) - // sort by length - i := sort.Search(n, func(i int) bool { - // Sort entries by length. - // - Entries with a scheme take preference over entries without. - // - Entries with a host take preference over entries without. - // - Longer paths take preference over shorter paths. - return (es[i].u.Scheme == "" || (e.u.Scheme != "" && len(es[i].u.Scheme) < len(e.u.Scheme))) && - (es[i].u.Host == "" || (e.u.Host != "" && len(es[i].u.Host) < len(e.u.Host))) && - len(es[i].u.Path) < len(e.u.Path) - }) - if i == n { - return append(es, e) - } - // we now know that i points at where we want to insert - es = append(es, muxEntry{}) // try to grow the slice in place, any entry works. - copy(es[i+1:], es[i:]) // Move shorter entries down - es[i] = e - return es -} - // A wrapper around a bare function that implements Handler. type HandlerFunc func(*ResponseWriter, *Request) @@ -443,20 +369,6 @@ func (fsys fsHandler) Serve(rw *ResponseWriter, req *Request) { io.Copy(rw, f) } -func containsDotDot(v string) bool { - if !strings.Contains(v, "..") { - return false - } - for _, ent := range strings.FieldsFunc(v, isSlashRune) { - if ent == ".." { - return true - } - } - return false -} - -func isSlashRune(r rune) bool { return r == '/' || r == '\\' } - // TODO: replace with fs.FS when available type FS interface { Open(name string) (File, error) @@ -485,3 +397,289 @@ func (d Dir) Open(name string) (File, error) { } return f, nil } + +// The following code is modified from the net/http package. + +// Copyright 2009 The Go Authors. All rights reserved. +// Use of this source code is governed by a BSD-style +// license that can be found in the LICENSE file. + +func containsDotDot(v string) bool { + if !strings.Contains(v, "..") { + return false + } + for _, ent := range strings.FieldsFunc(v, isSlashRune) { + if ent == ".." { + return true + } + } + return false +} + +func isSlashRune(r rune) bool { return r == '/' || r == '\\' } + +// ServeMux is a Gemini request multiplexer. +// It matches the URL of each incoming request against a list of registered +// patterns and calls the handler for the pattern that +// most closely matches the URL. +// +// Patterns name fixed, rooted paths, like "/favicon.ico", +// or rooted subtrees, like "/images/" (note the trailing slash). +// Longer patterns take precedence over shorter ones, so that +// if there are handlers registered for both "/images/" +// and "/images/thumbnails/", the latter handler will be +// called for paths beginning "/images/thumbnails/" and the +// former will receive requests for any other paths in the +// "/images/" subtree. +// +// Note that since a pattern ending in a slash names a rooted subtree, +// the pattern "/" matches all paths not matched by other registered +// patterns, not just the URL with Path == "/". +// +// If a subtree has been registered and a request is received naming the +// subtree root without its trailing slash, ServeMux redirects that +// request to the subtree root (adding the trailing slash). This behavior can +// be overridden with a separate registration for the path without +// the trailing slash. For example, registering "/images/" causes ServeMux +// to redirect a request for "/images" to "/images/", unless "/images" has +// been registered separately. +// +// 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 +// "/codesearch" and "codesearch.google.com/" without also taking over +// requests for "http://www.google.com/". +// +// ServeMux also takes care of sanitizing the URL request path and the Host +// header, stripping the port number and redirecting any request containing . or +// .. elements or repeated slashes to an equivalent, cleaner URL. +type ServeMux struct { + mu sync.RWMutex + m map[string]muxEntry + es []muxEntry // slice of entries sorted from longest to shortest. + hosts bool // whether any patterns contain hostnames +} + +type muxEntry struct { + h Handler + pattern string + u *url.URL +} + +// NewServeMux allocates and returns a new ServeMux. +func NewServeMux() *ServeMux { return new(ServeMux) } + +// cleanPath returns the canonical path for p, eliminating . and .. elements. +func cleanPath(p string) string { + if p == "" { + return "/" + } + if p[0] != '/' { + p = "/" + p + } + np := path.Clean(p) + // path.Clean removes trailing slash except for root; + // put the trailing slash back if necessary. + if p[len(p)-1] == '/' && np != "/" { + // Fast path for common case of p being the string we want: + if len(p) == len(np)+1 && strings.HasPrefix(p, np) { + np = p + } else { + np += "/" + } + } + return np +} + +// stripHostPort returns h without any trailing ":". +func stripHostPort(h string) string { + // If no port on host, return unchanged + if strings.IndexByte(h, ':') == -1 { + return h + } + host, _, err := net.SplitHostPort(h) + if err != nil { + return h // on error, return unchanged + } + return host +} + +// Find a handler on a handler map given a path string. +// Most-specific (longest) pattern wins. +func (mux *ServeMux) match(url *url.URL) (h Handler, pattern string) { + // Check for exact match first. + v, ok := mux.m[url.String()] + if ok { + return v.h, v.pattern + } + + // 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.u.Scheme == "" || url.Scheme == e.u.Scheme) && + (e.u.Host == "" || url.Host == e.u.Host) && + strings.HasPrefix(url.Path, e.u.Path) { + return e.h, e.pattern + } + } + return nil, "" +} + +// redirectToPathSlash determines if the given path needs appending "/" to it. +// 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 *ServeMux) redirectToPathSlash(host, path string, u *url.URL) (*url.URL, bool) { + mux.mu.RLock() + shouldRedirect := mux.shouldRedirectRLocked(host, path) + mux.mu.RUnlock() + if !shouldRedirect { + return u, false + } + path = path + "/" + u = &url.URL{Path: path, RawQuery: u.RawQuery} + return u, 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 ServeMux. +func (mux *ServeMux) shouldRedirectRLocked(host, path string) bool { + p := []string{path, host + path} + + for _, c := range p { + if _, exist := mux.m[c]; exist { + return false + } + } + + n := len(path) + if n == 0 { + return false + } + for _, c := range p { + if _, exist := mux.m[c+"/"]; exist { + return path[n-1] != '/' + } + } + + return false +} + +// Handler returns the handler to use for the given request, +// consulting r.Method, r.Host, and r.URL.Path. It always returns +// a non-nil handler. If the path is not in its canonical form, the +// handler will be an internally-generated handler that redirects +// to the canonical path. If the host contains a port, it is ignored +// when matching handlers. +// +// Handler also returns the registered pattern that matches the +// request or, in the case of internally-generated redirects, +// the pattern that will match after following the redirect. +// +// If there is no registered handler that applies to the request, +// Handler returns a ``page not found'' handler and an empty pattern. +func (mux *ServeMux) Handler(r *Request) (h Handler, pattern string) { + // All requests have any port stripped and path cleaned + // before passing to mux.handler. + url := *r.URL + url.Host = stripHostPort(r.Host) + url.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(url.Host, url.Path, r.URL); ok { + return PermanentRedirectHandler(u.String()), u.Path + } + + if url.Path != r.URL.Path { + _, pattern = mux.handler(&url) + red := *r.URL + red.Path = url.Path + return PermanentRedirectHandler(red.String()), pattern + } + + return mux.handler(&url) +} + +// handler is the main implementation of Handler. +func (mux *ServeMux) handler(url *url.URL) (h Handler, pattern string) { + mux.mu.RLock() + defer mux.mu.RUnlock() + + h, pattern = mux.match(url) + if h == nil { + h, pattern = NotFoundHandler(), "" + } + return +} + +// Serve dispatches the request to the handler whose +// pattern most closely matches the request URL. +func (mux *ServeMux) Serve(w *ResponseWriter, r *Request) { + h, _ := mux.Handler(r) + h.Serve(w, r) +} + +// Handle registers the handler for the given pattern. +// If a handler already exists for pattern, Handle panics. +func (mux *ServeMux) Handle(pattern string, handler Handler) { + mux.mu.Lock() + defer mux.mu.Unlock() + + if pattern == "" { + panic("gmi: invalid pattern") + } + if handler == nil { + panic("gmi: nil handler") + } + if _, exist := mux.m[pattern]; exist { + panic("gmi: multiple registrations for " + pattern) + } + + if mux.m == nil { + mux.m = make(map[string]muxEntry) + } + url, err := url.Parse(pattern) + if err != nil { + panic("gmi: invalid pattern") + } + e := muxEntry{h: handler, pattern: pattern, u: url} + mux.m[pattern] = e + if pattern[len(pattern)-1] == '/' { + mux.es = appendSorted(mux.es, e) + } + + if pattern[0] != '/' { + mux.hosts = true + } +} + +func appendSorted(es []muxEntry, e muxEntry) []muxEntry { + n := len(es) + i := sort.Search(n, func(i int) bool { + // Sort entries by length. + // - Entries with a scheme take preference over entries without. + // - Entries with a host take preference over entries without. + // - Longer paths take preference over shorter paths. + return (es[i].u.Scheme == "" || (e.u.Scheme != "" && len(es[i].u.Scheme) < len(e.u.Scheme))) && + (es[i].u.Host == "" || (e.u.Host != "" && len(es[i].u.Host) < len(e.u.Host))) && + len(es[i].u.Path) < len(e.u.Path) + }) + if i == n { + return append(es, e) + } + // we now know that i points at where we want to insert + es = append(es, muxEntry{}) // try to grow the slice in place, any entry works. + copy(es[i+1:], es[i:]) // Move shorter entries down + es[i] = e + return es +} + +// HandleFunc registers the handler function for the given pattern. +func (mux *ServeMux) HandleFunc(pattern string, handler func(*ResponseWriter, *Request)) { + if handler == nil { + panic("gmi: nil handler") + } + mux.Handle(pattern, HandlerFunc(handler)) +}