diff --git a/README.md b/README.md index 29fe813..70b4b53 100644 --- a/README.md +++ b/README.md @@ -71,7 +71,7 @@ client.TrustCertificate = func(cert *x509.Certificate, knownHosts *gemini.KnownH Advanced clients can prompt the user for what to do when encountering an unknown certificate: ```go -client.TrustCertificate: func(cert *x509.Certificate, knownHosts *gemini.KnownHosts) error { +client.TrustCertificate = func(cert *x509.Certificate, knownHosts *gemini.KnownHosts) error { err := knownHosts.Lookup(cert) if err != nil { switch err { diff --git a/examples/client/client.go b/examples/client/client.go index 687068a..5ac68ed 100644 --- a/examples/client/client.go +++ b/examples/client/client.go @@ -5,6 +5,7 @@ package main import ( "bufio" "crypto/tls" + "crypto/x509" "fmt" "log" "os" @@ -28,6 +29,29 @@ func init() { KnownHosts: knownHosts, } + client.TrustCertificate = func(cert *x509.Certificate, knownHosts *gemini.KnownHosts) error { + err := knownHosts.Lookup(cert) + if err != nil { + switch err { + case gemini.ErrCertificateNotTrusted: + // Alert the user that the certificate is not trusted + fmt.Println("error: certificate is not trusted!") + fmt.Println("This could indicate a Man-in-the-Middle attack.") + case gemini.ErrCertificateUnknown: + // Prompt the user to trust the certificate + if userTrustsCertificateTemporarily() { + // Temporarily trust the certificate + return nil + } else if userTrustsCertificatePermanently() { + // Add the certificate to the known hosts file + knownHosts.Add(cert) + return nil + } + } + } + return err + } + // Configure a client side certificate. // To generate a certificate, run: // @@ -81,6 +105,20 @@ func makeRequest(url string) { } } +func userTrustsCertificateTemporarily() bool { + fmt.Println("Do you want to trust the certificate temporarily? (y/n)") + scanner := bufio.NewScanner(os.Stdin) + scanner.Scan() + return scanner.Text() == "y" +} + +func userTrustsCertificatePermanently() bool { + fmt.Println("How about permanently? (y/n)") + scanner := bufio.NewScanner(os.Stdin) + scanner.Scan() + return scanner.Text() == "y" +} + func main() { if len(os.Args) < 2 { log.Fatalf("usage: %s gemini://...", os.Args[0]) diff --git a/examples/server/server.go b/examples/server/server.go index f99c6cd..03dfc2b 100644 --- a/examples/server/server.go +++ b/examples/server/server.go @@ -23,11 +23,7 @@ func main() { } mux := &gemini.ServeMux{} - mux.HandleFunc("/", func(rw *gemini.ResponseWriter, req *gemini.Request) { - rw.WriteHeader(gemini.StatusSuccess, "text/gemini") - rw.Write([]byte("You requested " + req.URL.String())) - log.Printf("Request from %s for %s", req.RemoteAddr.String(), req.URL) - }) + mux.Handle("/", gemini.FileServer(gemini.Dir("/var/www"))) server := gemini.Server{ Handler: mux, diff --git a/server.go b/server.go index 60af4a7..480eb48 100644 --- a/server.go +++ b/server.go @@ -4,9 +4,12 @@ import ( "bufio" "crypto/tls" "errors" + "io" "log" "net" "net/url" + "os" + "path/filepath" "sort" "strconv" "strings" @@ -16,6 +19,7 @@ import ( // Server errors. var ( ErrBodyNotAllowed = errors.New("gemini: response status code does not allow for body") + ErrNotAFile = errors.New("gemini: not a file") ) // Server is a Gemini server. @@ -233,23 +237,6 @@ func appendSorted(es []muxEntry, e muxEntry) []muxEntry { // - Entries with a scheme take preference over entries without. // - Entries with a host take preference over entries without. // - Longer paths take preference over shorter paths. - // - // Long version: - // if es[i].scheme != "" { - // if e.scheme == "" { - // return false - // } - // return len(es[i].scheme) < len(e.scheme) - // } - // if es[i].host != "" { - // if e.host == "" { - // return false - // } - // return len(es[i].host) < len(e.host) - // } - // return len(es[i].path) < len(e.path) - - // Condensed version: return (es[i].u.Scheme == "" || (e.u.Scheme != "" && len(es[i].u.Scheme) < len(e.u.Scheme))) && (es[i].u.Host == "" || (e.u.Host != "" && len(es[i].u.Host) < len(e.u.Host))) && len(es[i].u.Path) < len(e.u.Path) @@ -270,3 +257,62 @@ type HandlerFunc func(*ResponseWriter, *Request) func (f HandlerFunc) Serve(rw *ResponseWriter, req *Request) { f(rw, req) } + +// ServeDir serves files from a directory. +type ServeDir struct { + path string // path to the directory +} + +// FileServer takes a filesystem and returns a handler which uses that filesystem. +func FileServer(fsys FS) Handler { + return fsHandler{ + fsys, + } +} + +type fsHandler struct { + FS +} + +func (fsys fsHandler) Serve(rw *ResponseWriter, req *Request) { + // FIXME: Don't serve paths with .. in them + f, err := fsys.Open(req.URL.Path) + if err != nil { + rw.WriteHeader(StatusNotFound, "Not found") + return + } + // TODO: detect mimetype + mime := "text/gemini" + rw.WriteHeader(StatusSuccess, mime) + // Copy file to response writer + io.Copy(rw, f) +} + +// TODO: replace with fs.FS when available +type FS interface { + Open(name string) (File, error) +} + +// TODO: replace with fs.File when available +type File interface { + Stat() (os.FileInfo, error) + Read([]byte) (int, error) + Close() error +} + +// Dir implements FS using the native filesystem restricted to a specific directory. +type Dir string + +func (d Dir) Open(name string) (File, error) { + path := filepath.Join(string(d), name) + f, err := os.OpenFile(path, os.O_RDONLY, 0644) + if err != nil { + return nil, err + } + if stat, err := f.Stat(); err == nil { + if !stat.Mode().IsRegular() { + return nil, ErrNotAFile + } + } + return f, nil +}