mirror of
https://git.sr.ht/~adnano/go-gemini
synced 2024-11-23 16:52:06 +01:00
Move filesystem code to its own file
This commit is contained in:
parent
80828c126c
commit
037a707863
13
client.go
13
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
|
||||
}
|
||||
|
||||
|
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/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
|
||||
}
|
||||
|
37
vendor.go
37
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
|
||||
}
|
||||
|
Loading…
Reference in New Issue
Block a user