Move filesystem code to its own file
This commit is contained in:
parent
2d11edaa4c
commit
7fdc28d5be
13
client.go
13
client.go
|
@ -9,6 +9,7 @@ import (
|
||||||
"net"
|
"net"
|
||||||
"net/url"
|
"net/url"
|
||||||
"strconv"
|
"strconv"
|
||||||
|
"strings"
|
||||||
"time"
|
"time"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
@ -48,9 +49,13 @@ type Request struct {
|
||||||
}
|
}
|
||||||
|
|
||||||
// Hostname returns the request host without the port.
|
// Hostname returns the request host without the port.
|
||||||
|
// It assumes that r.Host contains a valid host:port.
|
||||||
func (r *Request) Hostname() string {
|
func (r *Request) Hostname() string {
|
||||||
host, _ := splitHostPort(r.Host)
|
colon := strings.LastIndexByte(r.Host, ':')
|
||||||
return host
|
if colon != -1 {
|
||||||
|
return r.Host[:colon]
|
||||||
|
}
|
||||||
|
return r.Host
|
||||||
}
|
}
|
||||||
|
|
||||||
// NewRequest returns a new request. The host is inferred from the provided URL.
|
// 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
|
return nil, err
|
||||||
}
|
}
|
||||||
|
|
||||||
host := u.Host
|
|
||||||
|
|
||||||
// If there is no port, use the default port of 1965
|
// If there is no port, use the default port of 1965
|
||||||
|
host := u.Host
|
||||||
if u.Port() == "" {
|
if u.Port() == "" {
|
||||||
host += ":1965"
|
host += ":1965"
|
||||||
}
|
}
|
||||||
|
@ -287,7 +291,6 @@ func (c *Client) Send(req *Request) (*Response, error) {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
return resp, nil
|
return resp, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
81
fs.go
Normal file
81
fs.go
Normal 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
121
server.go
|
@ -5,11 +5,9 @@ import (
|
||||||
"crypto/tls"
|
"crypto/tls"
|
||||||
"crypto/x509"
|
"crypto/x509"
|
||||||
"errors"
|
"errors"
|
||||||
"io"
|
|
||||||
"log"
|
"log"
|
||||||
"net"
|
"net"
|
||||||
"net/url"
|
"net/url"
|
||||||
"os"
|
|
||||||
"path"
|
"path"
|
||||||
"sort"
|
"sort"
|
||||||
"strconv"
|
"strconv"
|
||||||
|
@ -21,7 +19,6 @@ import (
|
||||||
// Server errors.
|
// Server errors.
|
||||||
var (
|
var (
|
||||||
ErrBodyNotAllowed = errors.New("gemini: response status code does not allow for body")
|
ErrBodyNotAllowed = errors.New("gemini: response status code does not allow for body")
|
||||||
ErrNotAFile = errors.New("gemini: not a file")
|
|
||||||
)
|
)
|
||||||
|
|
||||||
// Server is a Gemini server.
|
// Server is a Gemini server.
|
||||||
|
@ -31,7 +28,7 @@ type Server struct {
|
||||||
Addr string
|
Addr string
|
||||||
|
|
||||||
// Certificate provides a TLS certificate for use by the server.
|
// 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
|
Certificate tls.Certificate
|
||||||
|
|
||||||
// registered handlers
|
// registered handlers
|
||||||
|
@ -40,16 +37,23 @@ type Server struct {
|
||||||
|
|
||||||
// Handle registers a handler for the given host.
|
// Handle registers a handler for the given host.
|
||||||
// A default scheme of gemini:// is assumed.
|
// A default scheme of gemini:// is assumed.
|
||||||
func (s *Server) Handle(host string, h Handler) {
|
func (s *Server) Handle(host string, handler Handler) {
|
||||||
s.HandleScheme("gemini", host, h)
|
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.
|
// 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{
|
s.handlers = append(s.handlers, handlerEntry{
|
||||||
scheme,
|
scheme,
|
||||||
host,
|
host,
|
||||||
h,
|
handler,
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -372,85 +376,8 @@ func (f HandlerFunc) Serve(rw *ResponseWriter, req *Request) {
|
||||||
f(rw, req)
|
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.
|
// 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.
|
// ServeMux is a Gemini request multiplexer.
|
||||||
// It matches the URL of each incoming request against a list of registered
|
// It matches the URL of each incoming request against a list of registered
|
||||||
// patterns and calls the handler for the pattern that
|
// 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
|
// to redirect a request for "/images" to "/images/", unless "/images" has
|
||||||
// been registered separately.
|
// been registered separately.
|
||||||
//
|
//
|
||||||
// ServeMux also takes care of sanitizing the URL request path and the Host
|
// ServeMux also takes care of sanitizing the URL request path and
|
||||||
// header, stripping the port number and redirecting any request containing . or
|
// redirecting any request containing . or .. elements or repeated slashes
|
||||||
// .. elements or repeated slashes to an equivalent, cleaner URL.
|
// to an equivalent, cleaner URL.
|
||||||
type ServeMux struct {
|
type ServeMux struct {
|
||||||
mu sync.RWMutex
|
mu sync.RWMutex
|
||||||
m map[string]muxEntry
|
m map[string]muxEntry
|
||||||
|
@ -491,9 +418,6 @@ type muxEntry struct {
|
||||||
pattern string
|
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.
|
// cleanPath returns the canonical path for p, eliminating . and .. elements.
|
||||||
func cleanPath(p string) string {
|
func cleanPath(p string) string {
|
||||||
if p == "" {
|
if p == "" {
|
||||||
|
@ -516,19 +440,6 @@ func cleanPath(p string) string {
|
||||||
return np
|
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.
|
// Find a handler on a handler map given a path string.
|
||||||
// Most-specific (longest) pattern wins.
|
// Most-specific (longest) pattern wins.
|
||||||
func (mux *ServeMux) match(path string) (h Handler, pattern string) {
|
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
|
// 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.
|
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
|
es[i] = e
|
||||||
return es
|
return es
|
||||||
}
|
}
|
||||||
|
|
37
vendor.go
37
vendor.go
|
@ -1,8 +1,6 @@
|
||||||
// Hostname verification code from the crypto/x509 package.
|
// Hostname verification code from the crypto/x509 package.
|
||||||
// Modified to allow Common Names in the short term, until new certificates
|
// Modified to allow Common Names in the short term, until new certificates
|
||||||
// can be issued with SANs.
|
// can be issued with SANs.
|
||||||
//
|
|
||||||
// Also includes the splitHostPort function from the net/url package.
|
|
||||||
|
|
||||||
// Copyright 2011 The Go Authors. All rights reserved.
|
// Copyright 2011 The Go Authors. All rights reserved.
|
||||||
// Use of this source code is governed by a BSD-style
|
// 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}
|
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
|
|
||||||
}
|
|
||||||
|
|
Loading…
Reference in New Issue
Block a user