gopher/gopher.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)
}