aboutsummaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authoradnano <[email protected]>2020-10-11 18:57:04 -0400
committeradnano <[email protected]>2020-10-11 18:57:04 -0400
commit7fdc28d5be3a1913d1dbe21b0416209fdf529469 (patch)
tree7542f9e793c90a75402ce7594e8e2f120b3f84c9
parentImplement matching of hostnames and schemes (diff)
downloadgo-gemini-7fdc28d5be3a1913d1dbe21b0416209fdf529469.tar.xz
go-gemini-7fdc28d5be3a1913d1dbe21b0416209fdf529469.zip
Move filesystem code to its own file
-rw-r--r--client.go13
-rw-r--r--fs.go81
-rw-r--r--server.go121
-rw-r--r--vendor.go37
4 files changed, 105 insertions, 147 deletions
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 ":<port>".
-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
-}