diff --git a/client.go b/client.go index 5d28b84..ff968e4 100644 --- a/client.go +++ b/client.go @@ -9,6 +9,7 @@ import ( "net" "net/url" "strconv" + "strings" "time" ) @@ -48,9 +49,13 @@ type Request struct { } // Hostname returns the request host without the port. +// It assumes that r.Host contains a valid host:port. func (r *Request) Hostname() string { - host, _ := splitHostPort(r.Host) - return host + colon := strings.LastIndexByte(r.Host, ':') + if colon != -1 { + return r.Host[:colon] + } + return r.Host } // NewRequest returns a new request. The host is inferred from the provided URL. @@ -60,9 +65,8 @@ func NewRequest(rawurl string) (*Request, error) { return nil, err } - host := u.Host - // If there is no port, use the default port of 1965 + host := u.Host if u.Port() == "" { host += ":1965" } @@ -287,7 +291,6 @@ func (c *Client) Send(req *Request) (*Response, error) { } } } - return resp, nil } diff --git a/fs.go b/fs.go new file mode 100644 index 0000000..01e2618 --- /dev/null +++ b/fs.go @@ -0,0 +1,81 @@ +package gmi + +import ( + "errors" + "io" + "os" + "path" +) + +// FileServer errors. +var ( + ErrNotAFile = errors.New("gemini: not a file") +) + +// FileServer takes a filesystem and returns a Handler which uses that filesystem. +// The returned Handler sanitizes paths before handling them. +func FileServer(fsys FS) Handler { + return fsHandler{fsys} +} + +type fsHandler struct { + FS +} + +func (fsh fsHandler) Serve(rw *ResponseWriter, req *Request) { + path := path.Clean(req.URL.Path) + f, err := fsh.Open(path) + if err != nil { + NotFound(rw, req) + return + } + // TODO: detect mimetype + rw.SetMimetype("text/gemini") + // Copy file to response writer + io.Copy(rw, f) +} + +// TODO: replace with io/fs.FS when available +type FS interface { + Open(name string) (File, error) +} + +// TODO: replace with io/fs.File when available +type File interface { + Stat() (os.FileInfo, error) + Read([]byte) (int, error) + Close() error +} + +// Dir implements FS using the native filesystem restricted to a specific directory. +type Dir string + +// Open tries to open the file with the given name. +// If the file is a directory, it tries to open the index file in that directory. +func (d Dir) Open(name string) (File, error) { + p := path.Join(string(d), name) + f, err := os.OpenFile(p, os.O_RDONLY, 0644) + if err != nil { + return nil, err + } + + if stat, err := f.Stat(); err == nil { + if stat.IsDir() { + f, err := os.Open(path.Join(p, "index.gmi")) + if err != nil { + return nil, err + } + stat, err := f.Stat() + if err != nil { + return nil, err + } + if stat.Mode().IsRegular() { + return f, nil + } + return nil, ErrNotAFile + } else if !stat.Mode().IsRegular() { + return nil, ErrNotAFile + } + } + return f, nil +} diff --git a/server.go b/server.go index 1f2701b..d88cb60 100644 --- a/server.go +++ b/server.go @@ -5,11 +5,9 @@ import ( "crypto/tls" "crypto/x509" "errors" - "io" "log" "net" "net/url" - "os" "path" "sort" "strconv" @@ -21,7 +19,6 @@ import ( // Server errors. var ( ErrBodyNotAllowed = errors.New("gemini: response status code does not allow for body") - ErrNotAFile = errors.New("gemini: not a file") ) // Server is a Gemini server. @@ -31,7 +28,7 @@ type Server struct { Addr string // Certificate provides a TLS certificate for use by the server. - // Using a self-signed certificate is recommended. + // A self-signed certificate is recommended. Certificate tls.Certificate // registered handlers @@ -40,16 +37,23 @@ type Server struct { // Handle registers a handler for the given host. // A default scheme of gemini:// is assumed. -func (s *Server) Handle(host string, h Handler) { - s.HandleScheme("gemini", host, h) +func (s *Server) Handle(host string, handler Handler) { + if host == "" { + panic("gmi: invalid host") + } + if handler == nil { + panic("gmi: nil handler") + } + + s.HandleScheme("gemini", host, handler) } // HandleScheme registers a handler for the given scheme and host. -func (s *Server) HandleScheme(scheme string, host string, h Handler) { +func (s *Server) HandleScheme(scheme string, host string, handler Handler) { s.handlers = append(s.handlers, handlerEntry{ scheme, host, - h, + handler, }) } @@ -372,85 +376,8 @@ func (f HandlerFunc) Serve(rw *ResponseWriter, req *Request) { f(rw, req) } -// FileServer takes a filesystem and returns a handler which uses that filesystem. -// The returned Handler rejects requests containing '..' in them. -func FileServer(fsys FS) Handler { - return fsHandler{ - fsys, - } -} - -type fsHandler struct { - FS -} - -func (fsys fsHandler) Serve(rw *ResponseWriter, req *Request) { - // Reject requests with '..' in them - if containsDotDot(req.URL.Path) { - NotFound(rw, req) - return - } - f, err := fsys.Open(req.URL.Path) - if err != nil { - NotFound(rw, req) - return - } - // TODO: detect mimetype - rw.SetMimetype("text/gemini") - // Copy file to response writer - io.Copy(rw, f) -} - -// TODO: replace with fs.FS when available -type FS interface { - Open(name string) (File, error) -} - -// TODO: replace with fs.File when available -type File interface { - Stat() (os.FileInfo, error) - Read([]byte) (int, error) - Close() error -} - -// Dir implements FS using the native filesystem restricted to a specific directory. -type Dir string - -func (d Dir) Open(name string) (File, error) { - p := path.Join(string(d), name) - f, err := os.OpenFile(p, os.O_RDONLY, 0644) - if err != nil { - return nil, err - } - - if stat, err := f.Stat(); err == nil { - if !stat.Mode().IsRegular() { - return nil, ErrNotAFile - } - } - 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 @@ -477,9 +404,9 @@ func isSlashRune(r rune) bool { return r == '/' || r == '\\' } // to redirect a request for "/images" to "/images/", unless "/images" has // been registered separately. // -// 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. +// ServeMux also takes care of sanitizing the URL request path 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 @@ -491,9 +418,6 @@ type muxEntry struct { pattern string } -// 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 == "" { @@ -516,19 +440,6 @@ func cleanPath(p string) string { 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(path string) (h Handler, pattern string) { @@ -671,7 +582,7 @@ func appendSorted(es []muxEntry, e muxEntry) []muxEntry { } // 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 + copy(es[i+1:], es[i:]) // move shorter entries down es[i] = e return es } diff --git a/vendor.go b/vendor.go index 4871baf..8a0fd5a 100644 --- a/vendor.go +++ b/vendor.go @@ -1,8 +1,6 @@ // Hostname verification code from the crypto/x509 package. // Modified to allow Common Names in the short term, until new certificates // can be issued with SANs. -// -// Also includes the splitHostPort function from the net/url package. // Copyright 2011 The Go Authors. All rights reserved. // Use of this source code is governed by a BSD-style @@ -227,38 +225,3 @@ func verifyHostname(c *x509.Certificate, h string) error { return x509.HostnameError{c, h} } - -// validOptionalPort reports whether port is either an empty string -// or matches /^:\d*$/ -func validOptionalPort(port string) bool { - if port == "" { - return true - } - if port[0] != ':' { - return false - } - for _, b := range port[1:] { - if b < '0' || b > '9' { - return false - } - } - return true -} - -// splitHostPort separates host and port. If the port is not valid, it returns -// the entire input as host, and it doesn't check the validity of the host. -// Unlike net.SplitHostPort, but per RFC 3986, it requires ports to be numeric. -func splitHostPort(hostport string) (host, port string) { - host = hostport - - colon := strings.LastIndexByte(host, ':') - if colon != -1 && validOptionalPort(host[colon:]) { - host, port = host[:colon], host[colon+1:] - } - - if strings.HasPrefix(host, "[") && strings.HasSuffix(host, "]") { - host = host[1 : len(host)-1] - } - - return -}