// 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 != 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) }