aboutsummaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authoradnano <[email protected]>2020-09-26 16:38:26 -0400
committeradnano <[email protected]>2020-09-26 16:38:26 -0400
commit92a1dbbc0c6af06f8e463fce53adb9de7f6b0aea (patch)
treed9bef41efef59fb6377a7af92d6ef0b92eea3782
parentAdd preliminary CertificateStore API (diff)
downloadgo-gemini-92a1dbbc0c6af06f8e463fce53adb9de7f6b0aea.tar.xz
go-gemini-92a1dbbc0c6af06f8e463fce53adb9de7f6b0aea.zip
Implement file server
-rw-r--r--README.md2
-rw-r--r--examples/client/client.go38
-rw-r--r--examples/server/server.go6
-rw-r--r--server.go80
4 files changed, 103 insertions, 23 deletions
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
+}