gopher/gopher.go

1358 lines
32 KiB
Go

// Package 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"
"crypto/rand"
"crypto/tls"
"encoding/json"
"errors"
"fmt"
"io"
"log"
"net"
"net/http"
"net/url"
"os"
"path"
"path/filepath"
"sort"
"strconv"
"strings"
"sync"
"golang.org/x/net/context"
)
// 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
PNG = ItemType('p') // Item is a PNG Image
DOC = ItemType('d') // Item is a Document
)
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 "TXT"
case DIRECTORY:
return "DIR"
case PHONEBOOK:
return "PHO"
case ERROR:
return "ERR"
case BINHEX:
return "HEX"
case DOSARCHIVE:
return "ARC"
case UUENCODED:
return "UUE"
case INDEXSEARCH:
return "QRY"
case TELNET:
return "TEL"
case BINARY:
return "BIN"
case REDUNDANT:
return "DUP"
case TN3270:
return "TN3"
case GIF:
return "GIF"
case IMAGE:
return "IMG"
case INFO:
return "NFO"
case HTML:
return "HTM"
case AUDIO:
return "SND"
case PNG:
return "PNG"
case DOC:
return "DOC"
default:
return "???"
}
}
// 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 string) error {
parts := strings.Split(line, "\t")
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:])
if len(parts) > 1 {
i.Selector = string(parts[1])
} else {
i.Selector = ""
}
if len(parts) > 2 {
i.Host = string(parts[2])
} else {
i.Host = "null.host"
}
if len(parts) > 3 {
port, err := strconv.Atoi(string(parts[3]))
if err != nil {
// Ignore parsing errors for bad servers for INFO types
if i.Type != INFO {
return err
}
i.Port = 0
}
i.Port = port
} else {
i.Port = 0
}
if len(parts) >= 4 {
for _, v := range parts[4:] {
i.Extras = append(i.Extras, string(v))
}
}
return nil
}
func (i *Item) isDirectoryLike() bool {
switch i.Type {
case DIRECTORY:
return true
case INDEXSEARCH:
return true
default:
return false
}
}
// 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.isDirectoryLike() {
d, err := i.FetchDirectory()
if err != nil {
return nil, err
}
res.Dir = d
} else {
reader, err := i.FetchFile()
if err != nil {
return nil, err
}
res.Body = reader
}
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")
}
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.isDirectoryLike() {
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
}
var d Directory
reader := bufio.NewReader(conn)
scanner := bufio.NewScanner(reader)
scanner.Split(bufio.ScanLines)
for scanner.Scan() {
line := scanner.Text()
if len(line) == 0 {
continue
}
if len(line) == 1 && line[0] == END {
break
}
var i Item
err := i.parse(line)
if err != nil {
log.Printf("Error parsing %q: %q", line, err)
continue
}
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
Hostname string // FQDN Hostname to reach this server on
// 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)
}
// ListenAndServeTLS listens on the TCP network address srv.Addr and
// then calls Serve to handle requests on incoming TLS connections.
// Accepted connections are configured to enable TCP keep-alives.
//
// Filenames containing a certificate and matching private key for the
// server must be provided if neither the Server's TLSConfig.Certificates
// nor TLSConfig.GetCertificate are populated. If the certificate is
// signed by a certificate authority, the certFile should be the
// concatenation of the server's certificate, any intermediates, and
// the CA's certificate.
//
// If srv.Addr is blank, ":gophers" is used (port 73).
//
// ListenAndServeTLS always returns a non-nil error.
func (s *Server) ListenAndServeTLS(certFile, keyFile string) error {
addr := s.Addr
if addr == "" {
addr = ":73"
}
cert, err := tls.LoadX509KeyPair(certFile, keyFile)
if err != nil {
log.Fatalf("server: loadkeys: %s", err)
}
config := tls.Config{Certificates: []tls.Certificate{cert}}
config.Rand = rand.Reader
ln, err := tls.Listen("tcp", addr, &config)
if err != nil {
log.Fatalf("server: listen: %s", 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 {
fmt.Errorf("error acceptig new client: %s", err)
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 {
c := &conn{
server: s,
rwc: rwc,
}
return c
}
func (c *conn) serve(ctx context.Context) {
c.remoteAddr = c.rwc.RemoteAddr().String()
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
}
server := ctx.Value(ServerContextKey).(*Server)
if server.Hostname == "" {
req.LocalHost = host
req.LocalPort = int(n)
} else {
req.LocalHost = server.Hostname
// TODO: Parse this from -bind option
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()
}
// ListenAndServeTLS acts identically to ListenAndServe, except that it
// expects TLS connections. Additionally, files containing a certificate and
// matching private key for the server must be provided. If the certificate
// is signed by a certificate authority, the certFile should be the
// concatenation of the server's certificate, any intermediates,
// and the CA's certificate.
//
// A trivial example server is:
//
// import (
// "log"
//
// "github.com/prologic/go-gopher",
// )
//
// func HelloServer(w gopher.ResponseWriter, req *gopher.Request) {
// w.WriteInfo("hello, world!")
// }
//
// func main() {
// gopher.HandleFunc("/", handler)
// log.Printf("About to listen on 73. Go to gophers://127.0.0.1:73/")
// err := gopher.ListenAndServeTLS(":73", "cert.pem", "key.pem", nil)
// log.Fatal(err)
// }
//
// One can use generate_cert.go in crypto/tls to generate cert.pem and key.pem.
//
// ListenAndServeTLS always returns a non-nil error.
func ListenAndServeTLS(addr, certFile, keyFile string, handler Handler) error {
server := &Server{Addr: addr, Handler: handler}
return server.ListenAndServeTLS(certFile, keyFile)
}
// 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
for k, v := range mux.m {
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)
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) {
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 {
if w.rt == 0 {
w.rt = 2
}
if w.rt != 2 {
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()
if err != nil {
return err
}
_, err = w.w.Write(b)
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) {
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) {
root := fs.Name()
fullpath := f.(*os.File).Name()
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))
for _, file := range files {
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) {
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
}
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)
}