commit a4cfebfbdff8b6a9e940206186f6c788e29c8d35 Author: James Mills Date: Wed Sep 14 13:45:49 2016 +1000 Initial Commit diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..5de4e65 --- /dev/null +++ b/.gitignore @@ -0,0 +1,5 @@ +*~ +*.bak +examples/hello/hello +examples/client/client +examples/fileserver/fileserver diff --git a/README.md b/README.md new file mode 100644 index 0000000..fc3d369 --- /dev/null +++ b/README.md @@ -0,0 +1,60 @@ +# Gopher protocol library for Golang + +This is a standards compliant Gopher library for the Go programming language +implementing the RFC 1436 specification. The library includes both client and +server handling and examples of each. + +## Installation + + $ go get github.com/prologic/go-gopher + +## Usage + +```#!go +import "github.com/prologic/go-gopher" +``` + +## Example + +### Client + +```#!go +package main + +import ( + "fmt" + + "github.com/prologic/go-gopher" +) + +func main() { + res, _ := gopher.Get("gopher://gopher.floodgap.com/") + bytes, _ = res.Dir.ToText() + fmt.Println(string(bytes)) +} +``` + +### Server + +```#!go +package main + +import ( + "log" + + "github.com/prologic/go-gopher" +) + +func hello(w gopher.ResponseWriter, r *gopher.Request) { + w.WriteInfo("Hello World!") +} + +func main() { + gopher.HandleFunc("/hello", hello) + log.Fatal(gopher.ListenAndServe("localhost:70", nil)) +} +``` + +## License + +MIT diff --git a/examples/client/main.go b/examples/client/main.go new file mode 100644 index 0000000..5c82008 --- /dev/null +++ b/examples/client/main.go @@ -0,0 +1,56 @@ +package main + +import ( + "flag" + "fmt" + "io/ioutil" + "log" + + "github.com/prologic/go-gopher" +) + +var ( + json = flag.Bool("json", false, "display gopher directory as JSON") +) + +func main() { + var uri string + + flag.Parse() + + if len(flag.Args()) == 1 { + uri = flag.Arg(0) + } else { + uri = "gopher://gopher.floodgap.com/" + } + + res, err := gopher.Get(uri) + if err != nil { + log.Fatal(err) + } + + if res.Body != nil { + contents, err := ioutil.ReadAll(res.Body) + if err != nil { + log.Fatal(err) + } + + fmt.Print(contents) + } else { + var ( + bytes []byte + err error + ) + + if *json { + bytes, err = res.Dir.ToJSON() + } else { + bytes, err = res.Dir.ToText() + } + if err != nil { + log.Fatal(err) + } + + fmt.Println(string(bytes)) + } +} diff --git a/examples/fileserver/main.go b/examples/fileserver/main.go new file mode 100644 index 0000000..2e371dc --- /dev/null +++ b/examples/fileserver/main.go @@ -0,0 +1,29 @@ +package main + +import ( + "flag" + "log" + "os" + + "github.com/prologic/go-gopher" +) + +func cwd() string { + dir, err := os.Getwd() + if err != nil { + log.Fatal(err) + } + return dir +} + +func main() { + var ( + root = flag.String("root", cwd(), "root directory to serve") + ) + + flag.Parse() + + gopher.Handle("/", gopher.FileServer(gopher.Dir(*root))) + + log.Fatal(gopher.ListenAndServe("localhost:70", nil)) +} diff --git a/examples/hello/main.go b/examples/hello/main.go new file mode 100644 index 0000000..12d8209 --- /dev/null +++ b/examples/hello/main.go @@ -0,0 +1,27 @@ +package main + +import ( + "log" + + "github.com/prologic/go-gopher" +) + +func index(w gopher.ResponseWriter, r *gopher.Request) { + w.WriteItem( + gopher.Item{ + Type: gopher.FILE, + Selector: "/hello", + Description: "hello", + }, + ) +} + +func hello(w gopher.ResponseWriter, r *gopher.Request) { + w.WriteInfo("Hello World!") +} + +func main() { + gopher.HandleFunc("/", index) + gopher.HandleFunc("/hello", hello) + log.Fatal(gopher.ListenAndServe("localhost:70", nil)) +} diff --git a/gopher.go b/gopher.go new file mode 100644 index 0000000..a666596 --- /dev/null +++ b/gopher.go @@ -0,0 +1,1261 @@ +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) + c.server.logf("w: %q", w) + c.server.logf("err: %q", err) + + 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 != 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 != 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) +}