1275 lines
30 KiB
Go
1275 lines
30 KiB
Go
// gopher provides an implementation of the Gopher protocol (RFC 1436)
|
|
//
|
|
// Much of the API is similar in design to the net/http package of the
|
|
// standard library. To build custom Gopher servers implement handler
|
|
// functions or the `Handler{}` interface. Implementing a client is as
|
|
// simple as calling `gopher.Get(uri)` and passing in a `uri` such as
|
|
// `"gopher://gopher.floodgap.com/"`.
|
|
package gopher
|
|
|
|
import (
|
|
"bufio"
|
|
"bytes"
|
|
"context"
|
|
"crypto/tls"
|
|
"encoding/json"
|
|
"errors"
|
|
"fmt"
|
|
"io"
|
|
"io/ioutil"
|
|
"log"
|
|
"net"
|
|
"net/http"
|
|
"net/url"
|
|
"os"
|
|
"path"
|
|
"path/filepath"
|
|
"sort"
|
|
"strconv"
|
|
"strings"
|
|
"sync"
|
|
)
|
|
|
|
// Item Types
|
|
const (
|
|
FILE = ItemType('0') // Item is a file
|
|
DIRECTORY = ItemType('1') // Item is a directory
|
|
PHONEBOOK = ItemType('2') // Item is a CSO phone-book server
|
|
ERROR = ItemType('3') // Error
|
|
BINHEX = ItemType('4') // Item is a BinHexed Macintosh file.
|
|
DOSARCHIVE = ItemType('5') // Item is DOS binary archive of some sort. (*)
|
|
UUENCODED = ItemType('6') // Item is a UNIX uuencoded file.
|
|
INDEXSEARCH = ItemType('7') // Item is an Index-Search server.
|
|
TELNET = ItemType('8') // Item points to a text-based telnet session.
|
|
BINARY = ItemType('9') // Item is a binary file! (*)
|
|
|
|
// (*) Client must read until the TCP connection is closed.
|
|
|
|
REDUNDANT = ItemType('+') // Item is a redundant server
|
|
TN3270 = ItemType('T') // Item points to a text-based tn3270 session.
|
|
GIF = ItemType('g') // Item is a GIF format graphics file.
|
|
IMAGE = ItemType('I') // Item is some kind of image file.
|
|
|
|
// non-standard
|
|
INFO = ItemType('i') // Item is an informational message
|
|
HTML = ItemType('h') // Item is a HTML document
|
|
AUDIO = ItemType('s') // Item is an Audio file
|
|
)
|
|
|
|
const (
|
|
// END represents the terminator used in directory responses
|
|
END = byte('.')
|
|
|
|
// TAB is the delimiter used to separate item response parts
|
|
TAB = byte('\t')
|
|
|
|
// CRLF is the delimiter used per line of response item
|
|
CRLF = "\r\n"
|
|
|
|
// DEFAULT is the default item type
|
|
DEFAULT = BINARY
|
|
)
|
|
|
|
// contextKey is a value for use with context.WithValue. It's used as
|
|
// a pointer so it fits in an interface{} without allocation.
|
|
type contextKey struct {
|
|
name string
|
|
}
|
|
|
|
func (k *contextKey) String() string {
|
|
return "gopher context value " + k.name
|
|
}
|
|
|
|
var (
|
|
// ServerContextKey is a context key. It can be used in Gopher
|
|
// handlers with context.WithValue to access the server that
|
|
// started the handler. The associated value will be of type *Server.
|
|
ServerContextKey = &contextKey{"gopher-server"}
|
|
|
|
// LocalAddrContextKey is a context key. It can be used in
|
|
// Gopher handlers with context.WithValue to access the address
|
|
// the local address the connection arrived on.
|
|
// The associated value will be of type net.Addr.
|
|
LocalAddrContextKey = &contextKey{"local-addr"}
|
|
)
|
|
|
|
// ItemType represents the type of an item
|
|
type ItemType byte
|
|
|
|
// MarshalJSON returns a JSON mashaled byte array of an ItemType
|
|
func (it ItemType) MarshalJSON() ([]byte, error) {
|
|
return []byte(fmt.Sprintf("%q", string(byte(it)))), nil
|
|
}
|
|
|
|
// Return a human friendly represation of an ItemType
|
|
func (it ItemType) String() string {
|
|
switch it {
|
|
case FILE:
|
|
return "FILE"
|
|
case DIRECTORY:
|
|
return "DIRECTORY"
|
|
case PHONEBOOK:
|
|
return "PHONEBOOK"
|
|
case ERROR:
|
|
return "ERROR"
|
|
case BINHEX:
|
|
return "BINHEX"
|
|
case DOSARCHIVE:
|
|
return "DOSARCHIVE"
|
|
case UUENCODED:
|
|
return "UUENCODED"
|
|
case INDEXSEARCH:
|
|
return "INDEXSEARCH"
|
|
case TELNET:
|
|
return "TELNET"
|
|
case BINARY:
|
|
return "BINARY"
|
|
case REDUNDANT:
|
|
return "REDUNDANT"
|
|
case TN3270:
|
|
return "TN3270"
|
|
case GIF:
|
|
return "GIF"
|
|
case IMAGE:
|
|
return "IMAGE"
|
|
case INFO:
|
|
return "INFO"
|
|
case HTML:
|
|
return "HTML"
|
|
case AUDIO:
|
|
return "AUDIO"
|
|
default:
|
|
return "UNKNOWN"
|
|
}
|
|
}
|
|
|
|
// Item describes an entry in a directory listing.
|
|
type Item struct {
|
|
Type ItemType
|
|
Description string
|
|
Selector string
|
|
Host string
|
|
Port int
|
|
|
|
// non-standard extensions (ignored by standard clients)
|
|
Extras []string
|
|
}
|
|
|
|
// MarshalText serializes an Item into an array of bytes
|
|
func (i Item) MarshalText() ([]byte, error) {
|
|
b := []byte{}
|
|
b = append(b, byte(i.Type))
|
|
b = append(b, []byte(i.Description)...)
|
|
b = append(b, TAB)
|
|
b = append(b, []byte(i.Selector)...)
|
|
b = append(b, TAB)
|
|
b = append(b, []byte(i.Host)...)
|
|
b = append(b, TAB)
|
|
b = append(b, []byte(strconv.Itoa(i.Port))...)
|
|
|
|
for _, s := range i.Extras {
|
|
b = append(b, TAB)
|
|
b = append(b, []byte(s)...)
|
|
}
|
|
|
|
b = append(b, []byte(CRLF)...)
|
|
|
|
return b, nil
|
|
}
|
|
|
|
func (i *Item) parse(line []byte) error {
|
|
parts := bytes.Split(bytes.Trim(line, "\r\n"), []byte{'\t'})
|
|
if len(parts) < 4 {
|
|
return errors.New("truncated item line: " + string(line))
|
|
}
|
|
|
|
if len(parts[0]) < 1 {
|
|
return errors.New("no item type: " + string(line))
|
|
}
|
|
|
|
i.Type = ItemType(parts[0][0])
|
|
i.Description = string(parts[0][1:])
|
|
i.Selector = string(parts[1])
|
|
i.Host = string(parts[2])
|
|
|
|
port, err := strconv.Atoi(string(parts[3]))
|
|
if err != nil {
|
|
return err
|
|
}
|
|
i.Port = port
|
|
|
|
if len(parts) >= 4 {
|
|
for _, v := range parts[4:] {
|
|
i.Extras = append(i.Extras, string(v))
|
|
}
|
|
}
|
|
|
|
return nil
|
|
}
|
|
|
|
// Directory representes a Gopher Menu of Items
|
|
type Directory []Item
|
|
|
|
// ToJSON returns the Directory as JSON bytes
|
|
func (d *Directory) ToJSON() ([]byte, error) {
|
|
jsonBytes, err := json.Marshal(d)
|
|
return jsonBytes, err
|
|
}
|
|
|
|
// ToText returns the Directory as UTF-8 encoded bytes
|
|
func (d *Directory) ToText() ([]byte, error) {
|
|
var buffer bytes.Buffer
|
|
for _, i := range *d {
|
|
val, err := i.MarshalText()
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
buffer.Write(val)
|
|
}
|
|
return buffer.Bytes(), nil
|
|
}
|
|
|
|
// Response represents a Gopher resource that
|
|
// Items contains a non-empty array of Item(s)
|
|
// for directory types, otherwise the Body
|
|
// contains the fetched resource (file, image, etc).
|
|
type Response struct {
|
|
Type ItemType
|
|
Dir Directory
|
|
Body io.Reader
|
|
}
|
|
|
|
// Get fetches a Gopher resource by URI
|
|
func Get(uri string) (*Response, error) {
|
|
u, err := url.Parse(uri)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
if u.Scheme != "gopher" {
|
|
return nil, errors.New("invalid scheme for uri")
|
|
}
|
|
|
|
var (
|
|
host string
|
|
port int
|
|
)
|
|
|
|
hostport := strings.Split(u.Host, ":")
|
|
if len(hostport) == 2 {
|
|
host = hostport[0]
|
|
n, err := strconv.ParseInt(hostport[1], 10, 32)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
port = int(n)
|
|
} else {
|
|
host, port = hostport[0], 70
|
|
}
|
|
|
|
var (
|
|
Type ItemType
|
|
Selector string
|
|
)
|
|
|
|
path := strings.TrimPrefix(u.Path, "/")
|
|
if len(path) > 2 {
|
|
Type = ItemType(path[0])
|
|
Selector = path[1:]
|
|
} else if len(path) == 1 {
|
|
Type = ItemType(path[0])
|
|
Selector = ""
|
|
} else {
|
|
Type = ItemType(DIRECTORY)
|
|
Selector = ""
|
|
}
|
|
|
|
i := Item{Type: Type, Selector: Selector, Host: host, Port: port}
|
|
res := Response{Type: i.Type}
|
|
|
|
if i.Type == ItemType(DIRECTORY) {
|
|
d, err := i.FetchDirectory()
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
res.Dir = d
|
|
} else if i.Type == ItemType(FILE) {
|
|
reader, err := i.FetchFile()
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
res.Body = reader
|
|
} else {
|
|
return nil, fmt.Errorf("unsupported type: %s", i.Type)
|
|
}
|
|
|
|
return &res, nil
|
|
}
|
|
|
|
// FetchFile fetches data, not directory information.
|
|
// Calling this on a DIRECTORY Item type
|
|
// or unsupported type will return an error.
|
|
func (i *Item) FetchFile() (io.Reader, error) {
|
|
if i.Type == DIRECTORY {
|
|
return nil, errors.New("cannot fetch a directory as a file")
|
|
}
|
|
|
|
if i.Type != FILE {
|
|
return nil, errors.New("non-plaintext encodings not supported")
|
|
}
|
|
|
|
conn, err := net.Dial("tcp", i.Host+":"+strconv.Itoa(i.Port))
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
_, err = conn.Write([]byte(i.Selector + CRLF))
|
|
if err != nil {
|
|
conn.Close()
|
|
return nil, err
|
|
}
|
|
|
|
return conn, nil
|
|
}
|
|
|
|
// FetchDirectory fetches directory information, not data.
|
|
// Calling this on an Item whose type is not DIRECTORY will return an error.
|
|
func (i *Item) FetchDirectory() (Directory, error) {
|
|
if i.Type != DIRECTORY {
|
|
return nil, errors.New("cannot fetch a file as a directory")
|
|
}
|
|
|
|
conn, err := net.Dial("tcp", i.Host+":"+strconv.Itoa(i.Port))
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
_, err = conn.Write([]byte(i.Selector + CRLF))
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
data, err := ioutil.ReadAll(conn)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
lines := bytes.Split(data, []byte(CRLF))
|
|
|
|
var d Directory
|
|
for _, line := range lines {
|
|
if len(line) == 1 && line[0] == END {
|
|
break
|
|
}
|
|
var i Item
|
|
err := i.parse(line)
|
|
if err != nil {
|
|
return d, err
|
|
}
|
|
d = append(d, i)
|
|
}
|
|
|
|
return d, nil
|
|
}
|
|
|
|
// Request repsesnts an inbound request to a listening server.
|
|
// LocalHost and LocalPort may be used by the Handler for local links.
|
|
// These are specified in the call to ListenAndServe.
|
|
type Request struct {
|
|
conn net.Conn
|
|
Selector string
|
|
LocalHost string
|
|
LocalPort int
|
|
}
|
|
|
|
// A Handler responds to a Gopher request.
|
|
//
|
|
// ServeGopher should write data or items to the ResponseWriter
|
|
// and then return. Returning signals that the request is finished; it
|
|
// is not valid to use the ResponseWriter concurrently with the completion
|
|
// of the ServeGopher call.
|
|
//
|
|
// Handlers should not modify the provided request.
|
|
//
|
|
// If ServeGopher panics, the server (the caller of ServeGopher) assumes
|
|
// that the effect of the panic was isolated to the active request.
|
|
// It recovers the panic, logs a stack trace to the server error log,
|
|
// and hangs up the connection.
|
|
type Handler interface {
|
|
ServeGopher(ResponseWriter, *Request)
|
|
}
|
|
|
|
// FileExtensions defines a mapping of known file extensions to gopher types
|
|
var FileExtensions = map[string]ItemType{
|
|
".txt": FILE,
|
|
".gif": GIF,
|
|
".jpg": IMAGE,
|
|
".jpeg": IMAGE,
|
|
".png": IMAGE,
|
|
".html": HTML,
|
|
".ogg": AUDIO,
|
|
".mp3": AUDIO,
|
|
".wav": AUDIO,
|
|
".mod": AUDIO,
|
|
".it": AUDIO,
|
|
".xm": AUDIO,
|
|
".mid": AUDIO,
|
|
".vgm": AUDIO,
|
|
".s": FILE,
|
|
".c": FILE,
|
|
".py": FILE,
|
|
".h": FILE,
|
|
".md": FILE,
|
|
".go": FILE,
|
|
".fs": FILE,
|
|
}
|
|
|
|
// MimeTypes defines a mapping of known mimetypes to gopher types
|
|
var MimeTypes = map[string]ItemType{
|
|
"text/html": HTML,
|
|
"text/*": FILE,
|
|
|
|
"image/gif": GIF,
|
|
"image/*": IMAGE,
|
|
|
|
"audio/*": AUDIO,
|
|
|
|
"application/x-tar": DOSARCHIVE,
|
|
"application/x-gtar": DOSARCHIVE,
|
|
|
|
"application/x-xz": DOSARCHIVE,
|
|
"application/x-zip": DOSARCHIVE,
|
|
"application/x-gzip": DOSARCHIVE,
|
|
"application/x-bzip2": DOSARCHIVE,
|
|
}
|
|
|
|
func matchExtension(f os.FileInfo) ItemType {
|
|
extension := strings.ToLower(filepath.Ext(f.Name()))
|
|
k, ok := FileExtensions[extension]
|
|
if !ok {
|
|
return DEFAULT
|
|
}
|
|
return k
|
|
}
|
|
|
|
func matchMimeType(mimeType string) ItemType {
|
|
for k, v := range MimeTypes {
|
|
matched, err := filepath.Match(k, mimeType)
|
|
if !matched || (err != nil) {
|
|
continue
|
|
}
|
|
return v
|
|
}
|
|
return DEFAULT
|
|
}
|
|
|
|
// GetItemType returns the Gopher Type of the given path
|
|
func GetItemType(p string) ItemType {
|
|
fi, err := os.Stat(p)
|
|
if err != nil {
|
|
return DEFAULT
|
|
}
|
|
|
|
if fi.IsDir() {
|
|
return DIRECTORY
|
|
}
|
|
|
|
f, err := os.Open(p)
|
|
if err != nil {
|
|
return matchExtension(fi)
|
|
}
|
|
|
|
b := make([]byte, 512)
|
|
n, err := io.ReadAtLeast(f, b, 512)
|
|
if (err != nil) || (n != 512) {
|
|
return matchExtension(fi)
|
|
}
|
|
|
|
mimeType := http.DetectContentType(b)
|
|
mimeParts := strings.Split(mimeType, ";")
|
|
return matchMimeType(mimeParts[0])
|
|
}
|
|
|
|
// Server defines parameters for running a Gopher server.
|
|
// A zero value for Server is valid configuration.
|
|
type Server struct {
|
|
Addr string // TCP address to listen on, ":gopher" if empty
|
|
Handler Handler // handler to invoke, gopher.DefaultServeMux if nil
|
|
|
|
// ErrorLog specifies an optional logger for errors accepting
|
|
// connections and unexpected behavior from handlers.
|
|
// If nil, logging goes to os.Stderr via the log package's
|
|
// standard logger.
|
|
ErrorLog *log.Logger
|
|
}
|
|
|
|
// serverHandler delegates to either the server's Handler or
|
|
// DefaultServeMux and also handles "OPTIONS *" requests.
|
|
type serverHandler struct {
|
|
s *Server
|
|
}
|
|
|
|
func (sh serverHandler) ServeGopher(rw ResponseWriter, req *Request) {
|
|
handler := sh.s.Handler
|
|
if handler == nil {
|
|
handler = DefaultServeMux
|
|
}
|
|
handler.ServeGopher(rw, req)
|
|
}
|
|
|
|
// ListenAndServe starts serving gopher requests using the given Handler.
|
|
// The address passed to ListenAndServe should be an internet-accessable
|
|
// domain name, optionally followed by a colon and the port number.
|
|
//
|
|
// If the address is not a FQDN, LocalHost as passed to the Handler
|
|
// may not be accessible to clients, so links may not work.
|
|
func (s Server) ListenAndServe() error {
|
|
addr := s.Addr
|
|
if addr == "" {
|
|
addr = ":70"
|
|
}
|
|
|
|
ln, err := net.Listen("tcp", addr)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
return s.Serve(ln)
|
|
}
|
|
|
|
// Serve ...
|
|
func (s Server) Serve(l net.Listener) error {
|
|
defer l.Close()
|
|
|
|
ctx := context.Background()
|
|
ctx = context.WithValue(ctx, ServerContextKey, s)
|
|
ctx = context.WithValue(ctx, LocalAddrContextKey, l.Addr())
|
|
|
|
for {
|
|
rw, err := l.Accept()
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
c := s.newConn(rw)
|
|
go c.serve(ctx)
|
|
}
|
|
}
|
|
|
|
// A conn represents the server side of a Gopher connection.
|
|
type conn struct {
|
|
// server is the server on which the connection arrived.
|
|
// Immutable; never nil.
|
|
server *Server
|
|
|
|
// rwc is the underlying network connection.
|
|
// This is never wrapped by other types and is the value given out
|
|
// to CloseNotifier callers. It is usually of type *net.TCPConn or
|
|
// *tls.Conn.
|
|
rwc net.Conn
|
|
|
|
// remoteAddr is rwc.RemoteAddr().String(). It is not populated synchronously
|
|
// inside the Listener's Accept goroutine, as some implementations block.
|
|
// It is populated immediately inside the (*conn).serve goroutine.
|
|
// This is the value of a Handler's (*Request).RemoteAddr.
|
|
remoteAddr string
|
|
|
|
// tlsState is the TLS connection state when using TLS.
|
|
// nil means not TLS.
|
|
tlsState *tls.ConnectionState
|
|
|
|
// mu guards hijackedv, use of bufr, (*response).closeNotifyCh.
|
|
mu sync.Mutex
|
|
}
|
|
|
|
// Create new connection from rwc.
|
|
func (s *Server) newConn(rwc net.Conn) *conn {
|
|
s.logf("accepted new connection")
|
|
c := &conn{
|
|
server: s,
|
|
rwc: rwc,
|
|
}
|
|
return c
|
|
}
|
|
|
|
func (c *conn) serve(ctx context.Context) {
|
|
c.remoteAddr = c.rwc.RemoteAddr().String()
|
|
c.server.logf("serving %s", c.remoteAddr)
|
|
|
|
w, err := c.readRequest(ctx)
|
|
|
|
if err != nil {
|
|
if err == io.EOF {
|
|
return // don't reply
|
|
}
|
|
if neterr, ok := err.(net.Error); ok && neterr.Timeout() {
|
|
return // don't reply
|
|
}
|
|
io.WriteString(c.rwc, "3\tbad request\terror.host\t0")
|
|
return
|
|
}
|
|
|
|
serverHandler{c.server}.ServeGopher(w, w.req)
|
|
w.End()
|
|
}
|
|
|
|
func readRequest(rwc net.Conn) (req *Request, err error) {
|
|
reader := bufio.NewReader(rwc)
|
|
scanner := bufio.NewScanner(reader)
|
|
scanner.Split(bufio.ScanLines)
|
|
scanner.Scan()
|
|
|
|
req = &Request{
|
|
Selector: scanner.Text(),
|
|
}
|
|
|
|
// If empty selector, assume /
|
|
if req.Selector == "" {
|
|
req.Selector = "/"
|
|
}
|
|
|
|
// If no leading / prefix, add one
|
|
if !strings.HasPrefix(req.Selector, "/") {
|
|
req.Selector = "/" + req.Selector
|
|
}
|
|
|
|
return req, nil
|
|
}
|
|
|
|
func (c *conn) close() (err error) {
|
|
c.mu.Lock() // while using bufr
|
|
err = c.rwc.Close()
|
|
c.mu.Unlock()
|
|
return
|
|
}
|
|
|
|
func (c *conn) readRequest(ctx context.Context) (w *response, err error) {
|
|
c.mu.Lock() // while using bufr
|
|
req, err := readRequest(c.rwc)
|
|
c.mu.Unlock()
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
localaddr := ctx.Value(LocalAddrContextKey).(*net.TCPAddr)
|
|
host, port, err := net.SplitHostPort(localaddr.String())
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
n, err := strconv.ParseInt(port, 10, 32)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
req.LocalHost = host
|
|
req.LocalPort = int(n)
|
|
|
|
w = &response{
|
|
conn: c,
|
|
req: req,
|
|
}
|
|
w.w = bufio.NewWriter(c.rwc)
|
|
|
|
return w, nil
|
|
}
|
|
|
|
func (s *Server) logf(format string, args ...interface{}) {
|
|
if s.ErrorLog != nil {
|
|
s.ErrorLog.Printf(format, args...)
|
|
} else {
|
|
log.Printf(format, args...)
|
|
}
|
|
}
|
|
|
|
// ListenAndServe listens on the TCP network address addr
|
|
// and then calls Serve with handler to handle requests
|
|
// on incoming connections.
|
|
//
|
|
// A trivial example server is:
|
|
//
|
|
// package main
|
|
//
|
|
// import (
|
|
// "io"
|
|
// "log"
|
|
//
|
|
// "github.com/prologic/go-gopher"
|
|
// )
|
|
//
|
|
// // hello world, the gopher server
|
|
// func HelloServer(w gopher.ResponseWriter, req *gopher.Request) {
|
|
// w.WriteInfo("hello, world!")
|
|
// }
|
|
//
|
|
// func main() {
|
|
// gopher.HandleFunc("/hello", HelloServer)
|
|
// log.Fatal(gopher.ListenAndServe(":7000", nil))
|
|
// }
|
|
//
|
|
// ListenAndServe always returns a non-nil error.
|
|
func ListenAndServe(addr string, handler Handler) error {
|
|
server := &Server{Addr: addr, Handler: handler}
|
|
return server.ListenAndServe()
|
|
}
|
|
|
|
// ServeMux is a Gopher request multiplexer.
|
|
// It matches the URL of each incoming request against a list of registered
|
|
// patterns and calls the handler for the pattern that
|
|
// most closely matches the URL.
|
|
//
|
|
// Patterns name fixed, rooted paths, like "/favicon.ico",
|
|
// or rooted subtrees, like "/images/" (note the trailing slash).
|
|
// Longer patterns take precedence over shorter ones, so that
|
|
// if there are handlers registered for both "/images/"
|
|
// and "/images/thumbnails/", the latter handler will be
|
|
// called for paths beginning "/images/thumbnails/" and the
|
|
// former will receive requests for any other paths in the
|
|
// "/images/" subtree.
|
|
//
|
|
// Note that since a pattern ending in a slash names a rooted subtree,
|
|
// the pattern "/" matches all paths not matched by other registered
|
|
// patterns, not just the URL with Path == "/".
|
|
//
|
|
// If a subtree has been registered and a request is received naming the
|
|
// subtree root without its trailing slash, ServeMux redirects that
|
|
// request to the subtree root (adding the trailing slash). This behavior can
|
|
// be overridden with a separate registration for the path without
|
|
// the trailing slash. For example, registering "/images/" causes ServeMux
|
|
// to redirect a request for "/images" to "/images/", unless "/images" has
|
|
// been registered separately.
|
|
//
|
|
// ServeMux also takes care of sanitizing the URL request path,
|
|
// redirecting any request containing . or .. elements or repeated slashes
|
|
// to an equivalent, cleaner URL.
|
|
type ServeMux struct {
|
|
mu sync.RWMutex
|
|
m map[string]muxEntry
|
|
}
|
|
|
|
type muxEntry struct {
|
|
explicit bool
|
|
h Handler
|
|
pattern string
|
|
}
|
|
|
|
// NewServeMux allocates and returns a new ServeMux.
|
|
func NewServeMux() *ServeMux { return new(ServeMux) }
|
|
|
|
// DefaultServeMux is the default ServeMux used by Serve.
|
|
var DefaultServeMux = &defaultServeMux
|
|
|
|
var defaultServeMux ServeMux
|
|
|
|
// Does selector match pattern?
|
|
func selectorMatch(pattern, selector string) bool {
|
|
if len(pattern) == 0 {
|
|
// should not happen
|
|
return false
|
|
}
|
|
n := len(pattern)
|
|
if pattern[n-1] != '/' {
|
|
return pattern == selector
|
|
}
|
|
return len(selector) >= n && selector[0:n] == pattern
|
|
}
|
|
|
|
// Return the canonical path for p, eliminating . and .. elements.
|
|
func cleanPath(p string) string {
|
|
if p == "" {
|
|
return "/"
|
|
}
|
|
if p[0] != '/' {
|
|
p = "/" + p
|
|
}
|
|
np := path.Clean(p)
|
|
// path.Clean removes trailing slash except for root;
|
|
// put the trailing slash back if necessary.
|
|
if p[len(p)-1] == '/' && np != "/" {
|
|
np += "/"
|
|
}
|
|
return np
|
|
}
|
|
|
|
// Find a handler on a handler map given a path string
|
|
// Most-specific (longest) pattern wins
|
|
func (mux *ServeMux) match(selector string) (h Handler, pattern string) {
|
|
var n = 0
|
|
log.Printf("selector: %q", selector)
|
|
for k, v := range mux.m {
|
|
log.Printf("k: %q v: %q", k, v)
|
|
if !selectorMatch(k, selector) {
|
|
continue
|
|
}
|
|
if h == nil || len(k) > n {
|
|
n = len(k)
|
|
h = v.h
|
|
pattern = v.pattern
|
|
}
|
|
}
|
|
return
|
|
}
|
|
|
|
// Handler returns the handler to use for the given request,
|
|
// consulting r.Selector. It always returns
|
|
// a non-nil handler.
|
|
//
|
|
// Handler also returns the registered pattern that matches the request.
|
|
//
|
|
// If there is no registered handler that applies to the request,
|
|
// Handler returns a ``resource not found'' handler and an empty pattern.
|
|
func (mux *ServeMux) Handler(r *Request) (h Handler, pattern string) {
|
|
return mux.handler(r.Selector)
|
|
}
|
|
|
|
// handler is the main implementation of Handler.
|
|
func (mux *ServeMux) handler(selector string) (h Handler, pattern string) {
|
|
mux.mu.RLock()
|
|
defer mux.mu.RUnlock()
|
|
|
|
h, pattern = mux.match(selector)
|
|
log.Printf("found handler %q with pattern %q", h, pattern)
|
|
if h == nil {
|
|
h, pattern = NotFoundHandler(), ""
|
|
}
|
|
|
|
return
|
|
}
|
|
|
|
// ServeGopher dispatches the request to the handler whose
|
|
// pattern most closely matches the request URL.
|
|
func (mux *ServeMux) ServeGopher(w ResponseWriter, r *Request) {
|
|
w.Server().logf("calling ServeGopher:")
|
|
h, _ := mux.Handler(r)
|
|
h.ServeGopher(w, r)
|
|
}
|
|
|
|
// Handle registers the handler for the given pattern.
|
|
// If a handler already exists for pattern, Handle panics.
|
|
func (mux *ServeMux) Handle(pattern string, handler Handler) {
|
|
mux.mu.Lock()
|
|
defer mux.mu.Unlock()
|
|
|
|
if pattern == "" {
|
|
panic("gopher: invalid pattern " + pattern)
|
|
}
|
|
if handler == nil {
|
|
panic("gopher: nil handler")
|
|
}
|
|
if mux.m[pattern].explicit {
|
|
panic("gopher: multiple registrations for " + pattern)
|
|
}
|
|
|
|
if mux.m == nil {
|
|
mux.m = make(map[string]muxEntry)
|
|
}
|
|
mux.m[pattern] = muxEntry{explicit: true, h: handler, pattern: pattern}
|
|
}
|
|
|
|
// HandleFunc registers the handler function for the given pattern.
|
|
func (mux *ServeMux) HandleFunc(pattern string, handler func(ResponseWriter, *Request)) {
|
|
mux.Handle(pattern, HandlerFunc(handler))
|
|
}
|
|
|
|
// The HandlerFunc type is an adapter to allow the use of
|
|
// ordinary functions as Gopher handlers. If f is a function
|
|
// with the appropriate signature, HandlerFunc(f) is a
|
|
// Handler that calls f.
|
|
type HandlerFunc func(ResponseWriter, *Request)
|
|
|
|
// ServeGopher calls f(w, r).
|
|
func (f HandlerFunc) ServeGopher(w ResponseWriter, r *Request) {
|
|
f(w, r)
|
|
}
|
|
|
|
// A ResponseWriter interface is used by a Gopher handler to
|
|
// construct an Gopher response.
|
|
//
|
|
// A ResponseWriter may not be used after the Handler.ServeGopher method
|
|
// has returned.
|
|
type ResponseWriter interface {
|
|
// Server returns the connection's server instance
|
|
Server() *Server
|
|
|
|
// End ends the document by writing the terminating period and crlf
|
|
End() error
|
|
|
|
// Write writes the data to the connection as part of a Gopher reply.
|
|
//
|
|
Write([]byte) (int, error)
|
|
|
|
// WriteError writes an error item
|
|
WriteError(err string) error
|
|
|
|
// WriteInfo writes an informational item
|
|
WriteInfo(msg string) error
|
|
|
|
// WriteItem writes an item
|
|
WriteItem(i Item) error
|
|
}
|
|
|
|
// A response represents the server side of a Gopher response.
|
|
type response struct {
|
|
conn *conn
|
|
|
|
req *Request // request for this response
|
|
|
|
w *bufio.Writer // buffers output
|
|
|
|
rt int
|
|
}
|
|
|
|
func (w *response) Server() *Server {
|
|
return w.conn.server
|
|
}
|
|
|
|
func (w *response) Write(b []byte) (int, error) {
|
|
if w.rt == 0 {
|
|
w.rt = 1
|
|
}
|
|
|
|
if w.rt != 1 {
|
|
return 0, errors.New("cannot write document data to a directory")
|
|
}
|
|
|
|
return w.w.Write(b)
|
|
}
|
|
|
|
func (w *response) WriteError(err string) error {
|
|
if w.rt == 0 {
|
|
w.rt = 2
|
|
}
|
|
|
|
if w.rt != 2 {
|
|
_, e := w.w.Write([]byte(err))
|
|
return e
|
|
}
|
|
|
|
i := Item{
|
|
Type: ERROR,
|
|
Description: err,
|
|
Host: "error.host",
|
|
Port: 1,
|
|
}
|
|
|
|
return w.WriteItem(i)
|
|
}
|
|
|
|
func (w *response) WriteInfo(msg string) error {
|
|
if w.rt == 0 {
|
|
w.rt = 2
|
|
}
|
|
|
|
if w.rt != 2 {
|
|
_, e := w.w.Write([]byte(msg))
|
|
return e
|
|
}
|
|
|
|
i := Item{
|
|
Type: INFO,
|
|
Description: msg,
|
|
Host: "error.host",
|
|
Port: 1,
|
|
}
|
|
|
|
return w.WriteItem(i)
|
|
}
|
|
|
|
func (w *response) WriteItem(i Item) error {
|
|
log.Printf("WriteItem: %q", i)
|
|
if w.rt == 0 {
|
|
w.rt = 2
|
|
}
|
|
|
|
if w.rt != 2 {
|
|
log.Printf("Ooops!")
|
|
return errors.New("cannot write directory data to a document")
|
|
}
|
|
|
|
if i.Host == "" && i.Port == 0 {
|
|
i.Host = w.req.LocalHost
|
|
i.Port = w.req.LocalPort
|
|
}
|
|
|
|
b, err := i.MarshalText()
|
|
log.Printf("b: %q err: %q", b, err)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
n, err := w.w.Write(b)
|
|
log.Printf("n: %q err: %q", n, err)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
return nil
|
|
}
|
|
|
|
func (w *response) End() (err error) {
|
|
if w.rt == 2 {
|
|
_, err = w.w.Write(append([]byte{END}, CRLF...))
|
|
if err != nil {
|
|
return
|
|
}
|
|
}
|
|
|
|
err = w.w.Flush()
|
|
if err != nil {
|
|
return
|
|
}
|
|
|
|
err = w.conn.close()
|
|
if err != nil {
|
|
return
|
|
}
|
|
|
|
return
|
|
}
|
|
|
|
// Helper handlers
|
|
|
|
// Error replies to the request with the specified error message.
|
|
// It does not otherwise end the request; the caller should ensure no further
|
|
// writes are done to w.
|
|
// The error message should be plain text.
|
|
func Error(w ResponseWriter, error string) {
|
|
w.WriteError(error)
|
|
}
|
|
|
|
// NotFound replies to the request with an resouce not found error item.
|
|
func NotFound(w ResponseWriter, r *Request) {
|
|
Error(w, "resource not found")
|
|
}
|
|
|
|
// NotFoundHandler returns a simple request handler
|
|
// that replies to each request with a ``resource page not found'' reply.
|
|
func NotFoundHandler() Handler { return HandlerFunc(NotFound) }
|
|
|
|
type fileHandler struct {
|
|
root FileSystem
|
|
}
|
|
|
|
// FileServer returns a handler that serves Gopher requests
|
|
// with the contents of the file system rooted at root.
|
|
//
|
|
// To use the operating system's file system implementation,
|
|
// use gopher.Dir:
|
|
//
|
|
// gopher.Handle("/", gopher.FileServer(gopher.Dir("/tmp")))
|
|
func FileServer(root FileSystem) Handler {
|
|
return &fileHandler{root}
|
|
}
|
|
|
|
func (f *fileHandler) ServeGopher(w ResponseWriter, r *Request) {
|
|
w.Server().logf("fileHandler.ServeGopher: %s", r.Selector)
|
|
upath := r.Selector
|
|
if !strings.HasPrefix(upath, "/") {
|
|
upath = "/" + upath
|
|
r.Selector = upath
|
|
}
|
|
serveFile(w, r, f.root, path.Clean(upath))
|
|
}
|
|
|
|
// A Dir implements FileSystem using the native file system restricted to a
|
|
// specific directory tree.
|
|
//
|
|
// While the FileSystem.Open method takes '/'-separated paths, a Dir's string
|
|
// value is a filename on the native file system, not a URL, so it is separated
|
|
// by filepath.Separator, which isn't necessarily '/'.
|
|
//
|
|
// An empty Dir is treated as ".".
|
|
type Dir string
|
|
|
|
// Name returns the directory
|
|
func (d Dir) Name() string {
|
|
return string(d)
|
|
}
|
|
|
|
// Open opens the directory
|
|
func (d Dir) Open(name string) (File, error) {
|
|
if filepath.Separator != '/' &&
|
|
strings.ContainsRune(name, filepath.Separator) ||
|
|
strings.Contains(name, "\x00") {
|
|
return nil, errors.New("gopher: invalid character in file path")
|
|
}
|
|
dir := string(d)
|
|
if dir == "" {
|
|
dir = "."
|
|
}
|
|
f, err := os.Open(
|
|
filepath.Join(dir, filepath.FromSlash(path.Clean("/"+name))),
|
|
)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
return f, nil
|
|
}
|
|
|
|
// A FileSystem implements access to a collection of named files.
|
|
// The elements in a file path are separated by slash ('/', U+002F)
|
|
// characters, regardless of host operating system convention.
|
|
type FileSystem interface {
|
|
Name() string
|
|
Open(name string) (File, error)
|
|
}
|
|
|
|
// A File is returned by a FileSystem's Open method and can be
|
|
// served by the FileServer implementation.
|
|
//
|
|
// The methods should behave the same as those on an *os.File.
|
|
type File interface {
|
|
io.Closer
|
|
io.Reader
|
|
io.Seeker
|
|
Readdir(count int) ([]os.FileInfo, error)
|
|
Stat() (os.FileInfo, error)
|
|
}
|
|
|
|
func dirList(w ResponseWriter, r *Request, f File, fs FileSystem) {
|
|
log.Printf("dirList: %s", f)
|
|
root := fs.Name()
|
|
log.Printf("root: %q", root)
|
|
|
|
/*
|
|
fileinfo, err := f.Stat()
|
|
if err != nil {
|
|
// TODO: log err.Error() to the Server.ErrorLog, once it's possible
|
|
// for a handler to get at its Server via the ResponseWriter.
|
|
Error(w, "Error reading directory")
|
|
return
|
|
}
|
|
*/
|
|
|
|
fullpath := f.(*os.File).Name()
|
|
log.Printf("fullpath: %s", fullpath)
|
|
|
|
files, err := f.Readdir(-1)
|
|
if err != nil {
|
|
// TODO: log err.Error() to the Server.ErrorLog, once it's possible
|
|
// for a handler to get at its Server via the ResponseWriter.
|
|
Error(w, "Error reading directory")
|
|
return
|
|
}
|
|
sort.Sort(byName(files))
|
|
|
|
log.Printf("reading %d files", len(files))
|
|
|
|
for _, file := range files {
|
|
log.Printf("file: %q", file.Name())
|
|
if file.Name()[0] == '.' {
|
|
continue
|
|
}
|
|
if file.Mode()&os.ModeDir != 0 {
|
|
pathname, err := filepath.Rel(
|
|
root,
|
|
path.Join(fullpath, file.Name()),
|
|
)
|
|
if err != nil {
|
|
Error(w, "Error reading directory")
|
|
return
|
|
}
|
|
w.WriteItem(
|
|
Item{
|
|
Type: DIRECTORY,
|
|
Description: file.Name(),
|
|
Selector: pathname,
|
|
Host: r.LocalHost,
|
|
Port: r.LocalPort,
|
|
},
|
|
)
|
|
} else if file.Mode()&os.ModeType == 0 {
|
|
pathname, err := filepath.Rel(
|
|
root,
|
|
path.Join(fullpath, file.Name()),
|
|
)
|
|
if err != nil {
|
|
Error(w, "Error reading directory")
|
|
return
|
|
}
|
|
|
|
itemtype := GetItemType(path.Join(fullpath, file.Name()))
|
|
|
|
w.WriteItem(
|
|
Item{
|
|
Type: itemtype,
|
|
Description: file.Name(),
|
|
Selector: pathname,
|
|
Host: r.LocalHost,
|
|
Port: r.LocalPort,
|
|
},
|
|
)
|
|
}
|
|
}
|
|
}
|
|
|
|
type byName []os.FileInfo
|
|
|
|
func (s byName) Len() int { return len(s) }
|
|
func (s byName) Less(i, j int) bool { return s[i].Name() < s[j].Name() }
|
|
func (s byName) Swap(i, j int) { s[i], s[j] = s[j], s[i] }
|
|
|
|
// name is '/'-separated, not filepath.Separator.
|
|
func serveFile(w ResponseWriter, r *Request, fs FileSystem, name string) {
|
|
log.Printf("serveFile: ...")
|
|
const gophermapFile = "/gophermap"
|
|
|
|
f, err := fs.Open(name)
|
|
if err != nil {
|
|
Error(w, err.Error())
|
|
return
|
|
}
|
|
defer f.Close()
|
|
|
|
d, err := f.Stat()
|
|
if err != nil {
|
|
Error(w, err.Error())
|
|
return
|
|
}
|
|
|
|
// use contents of gophermap for directory, if present
|
|
if d.IsDir() {
|
|
gophermap := strings.TrimSuffix(name, "/") + gophermapFile
|
|
ff, err := fs.Open(gophermap)
|
|
if err == nil {
|
|
defer ff.Close()
|
|
dd, err := ff.Stat()
|
|
if err == nil {
|
|
name = gophermap
|
|
d = dd
|
|
f = ff
|
|
}
|
|
}
|
|
}
|
|
|
|
// Still a directory? (we didn't find a gophermap file)
|
|
if d.IsDir() {
|
|
dirList(w, r, f, fs)
|
|
return
|
|
}
|
|
|
|
log.Printf("serving file: %s", d.Name())
|
|
serveContent(w, r, f)
|
|
}
|
|
|
|
// content must be seeked to the beginning of the file.
|
|
func serveContent(w ResponseWriter, r *Request, content io.ReadSeeker) {
|
|
io.Copy(w, content)
|
|
}
|
|
|
|
// Handle registers the handler for the given pattern
|
|
// in the DefaultServeMux.
|
|
// The documentation for ServeMux explains how patterns are matched.
|
|
func Handle(pattern string, handler Handler) {
|
|
DefaultServeMux.Handle(pattern, handler)
|
|
}
|
|
|
|
// HandleFunc registers the handler function for the given pattern
|
|
// in the DefaultServeMux.
|
|
// The documentation for ServeMux explains how patterns are matched.
|
|
func HandleFunc(pattern string, handler func(ResponseWriter, *Request)) {
|
|
DefaultServeMux.HandleFunc(pattern, handler)
|
|
}
|