Move filesystem code to its own file

This commit is contained in:
adnano 2020-10-11 18:57:04 -04:00
parent 2d11edaa4c
commit 7fdc28d5be
4 changed files with 105 additions and 147 deletions

View File

@ -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
}

81
fs.go Normal file
View File

@ -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
}

121
server.go
View File

@ -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
}

View File

@ -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
}