1
0
mirror of https://git.sr.ht/~adnano/go-gemini synced 2024-11-26 22:18:24 +01:00

Implement file server

This commit is contained in:
adnano 2020-09-26 16:38:26 -04:00
parent 749b765aaa
commit 1288e1bc31
4 changed files with 103 additions and 23 deletions

@ -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 {

@ -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])

@ -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,

@ -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
}