flounder

A simple gemini site builder
Log | Files | Refs | README | LICENSE

commit e806ee97af47b96ed240b2c4ff0e20ee99eb4640
parent e8c4efc8ff0efd2f3f607384d88a92bf18e364c3
Author: alex wennerberg <alex@alexwennerberg.com>
Date:   Sun, 25 Oct 2020 10:47:24 -0700

TLS setup

Diffstat:
M.gitignore | 3++-
MREADME.md | 8+++++---
Madmin.go | 5+++++
Mconfig.go | 5++++-
Mflounder.toml | 19+++++++++++++++----
Mgemini.go | 91+++++--------------------------------------------------------------------------
Mhttp.go | 50+++++++++++++++++++++++++++++---------------------
Mmain.go | 10++++++++++
Mschema.sql | 3+++
Mtemplates/index.gmi | 2+-
Mtemplates/index.html | 2+-
Mtemplates/message.html | 2+-
Mtemplates/my_site.html | 2+-
Mtemplates/register.html | 2+-
Mtemplates/user_page.html | 2+-
15 files changed, 84 insertions(+), 122 deletions(-)

diff --git a/.gitignore b/.gitignore @@ -1,2 +1,3 @@ files/ -tmpcerts/ +*.crt +*.key diff --git a/README.md b/README.md @@ -9,8 +9,10 @@ Designed to help make the Gemini ecosystem more accessible. Flounder is designed to be very simple to host, and should be able to be relatively easily run by a single person. -Very simple to host -- a single binary with a gemini server, http server included. +Once you've installed Flounder, you'll want to set the configuration variables. The `flounder.toml` file in this directory provides some example configuration. -## Customizing +1. Install with `go get ...` +2. For local testing, flounder will generate a TLS cert for you. However, for production, you'll need to generate a cert that matches \*.your-domain signed by a Certificate Authority. +3. Set the cookie store key -You can relatively easily change the style and layout of your instance if you'd like. +Flounder uses the HTTP templates in the specified templates folder. If you want to modify the look and feel of your site, or host new files, you can modify these files. diff --git a/admin.go b/admin.go @@ -3,3 +3,8 @@ package main // Commands for administering your instance // reset user password -> generate link // delete user + +// Run some scripts to setup your instance + +func initialize() { +} diff --git a/config.go b/config.go @@ -7,7 +7,8 @@ import ( type Config struct { FilesDirectory string TemplatesDirectory string - RootDomain string + Hostname string + Host string SiteTitle string Debug bool SecretKey string @@ -16,6 +17,8 @@ type Config struct { CookieStoreKey string OkExtensions []string MaxFileSize int + TLSCertFile string + TLSKeyFile string } func getConfig(filename string) (Config, error) { diff --git a/flounder.toml b/flounder.toml @@ -1,13 +1,24 @@ # Used in HTML templates and titles SiteTitle="­čÉčflounder" -RootDomain="localhost" + +# Include port if != 443 +Host="localhost:8443" + +# Folder containing subfolders for each user's files FilesDirectory="./files" -# Generate a secure key + +# Secure key for Cookie Store TODO remove CookieStoreKey="12345678123456781234567812345678" -# handles templates and static files -# everything in the static subfolder will be served at root + +# A wildcard TLS cert +TLSCertFile="./server.crt" +TLSKeyFile="./server.key" + +# Templates and static files +# Everything in the static subfolder will be served at / TemplatesDirectory="./templates" DBFile="./flounder.db" + MaxFileSize=128000 # 128 KB OkExtensions=["", ".gmi", ".txt", ".jpg", ".jpeg", ".gif", ".png", ".svg", ".webp", ".midi", ".json", ".csv", ".gemini", ".mp3", ".css", ".ttf", ".otf", ".woff", ".woff2"] diff --git a/gemini.go b/gemini.go @@ -1,20 +1,15 @@ package main import ( - "bytes" "crypto/tls" - "crypto/x509" // todo move into cert file - "encoding/pem" "strings" // "fmt" "git.sr.ht/~adnano/gmi" "io/ioutil" "log" - "os" "path" "path/filepath" "text/template" - "time" ) func gmiIndex(w *gmi.ResponseWriter, r *gmi.Request) { @@ -30,7 +25,7 @@ func gmiIndex(w *gmi.ResponseWriter, r *gmi.Request) { Files []*File Users []string }{ - Domain: c.RootDomain, + Domain: c.Hostname, SiteTitle: c.SiteTitle, Files: files, Users: users, @@ -65,91 +60,15 @@ func runGeminiServer() { server.GetCertificate = func(hostname string, store *gmi.CertificateStore) *tls.Certificate { cert, err := store.Lookup(hostname) if err != nil { - switch err { - case gmi.ErrCertificateExpired: - // Generate a new certificate if the current one is expired. - log.Print("Old certificate expired, creating new one") - fallthrough - case gmi.ErrCertificateUnknown: - // Generate a certificate if one does not exist. - cert, err := gmi.NewCertificate(hostname, time.Minute) - if err != nil { - // Failed to generate new certificate, abort - return nil - } - // Store and return the new certificate - err = writeCertificate("./tmpcerts/"+hostname, cert) - if err != nil { - return nil - } - store.Add(hostname, cert) - return &cert - } + log.Fatal("Invalid TLS cert") } return cert } // replace with wildcard cert - server.HandleFunc(c.RootDomain, gmiIndex) - server.HandleFunc("*."+c.RootDomain, gmiPage) + hostname := strings.SplitN(c.Host, ":", 1)[0] + server.HandleFunc(hostname, gmiIndex) + server.HandleFunc("*."+hostname, gmiPage) server.ListenAndServe() } - -// TODO log request - -// writeCertificate writes the provided certificate and private key -// to path.crt and path.key respectively. -func writeCertificate(path string, cert tls.Certificate) error { - crt, err := marshalX509Certificate(cert.Leaf.Raw) - if err != nil { - return err - } - key, err := marshalPrivateKey(cert.PrivateKey) - if err != nil { - return err - } - - // Write the certificate - crtPath := path + ".crt" - crtOut, err := os.OpenFile(crtPath, os.O_WRONLY|os.O_CREATE|os.O_TRUNC, 0600) - if err != nil { - return err - } - if _, err := crtOut.Write(crt); err != nil { - return err - } - - // Write the private key - keyPath := path + ".key" - keyOut, err := os.OpenFile(keyPath, os.O_WRONLY|os.O_CREATE|os.O_TRUNC, 0600) - if err != nil { - return err - } - if _, err := keyOut.Write(key); err != nil { - return err - } - return nil -} - -// marshalX509Certificate returns a PEM-encoded version of the given raw certificate. -func marshalX509Certificate(cert []byte) ([]byte, error) { - var b bytes.Buffer - if err := pem.Encode(&b, &pem.Block{Type: "CERTIFICATE", Bytes: cert}); err != nil { - return nil, err - } - return b.Bytes(), nil -} - -// marshalPrivateKey returns PEM encoded versions of the given certificate and private key. -func marshalPrivateKey(priv interface{}) ([]byte, error) { - var b bytes.Buffer - privBytes, err := x509.MarshalPKCS8PrivateKey(priv) - if err != nil { - return nil, err - } - if err := pem.Encode(&b, &pem.Block{Type: "PRIVATE KEY", Bytes: privBytes}); err != nil { - return nil, err - } - return b.Bytes(), nil -} diff --git a/http.go b/http.go @@ -57,12 +57,12 @@ func rootHandler(w http.ResponseWriter, r *http.Request) { return } data := struct { - Domain string + Host string PageTitle string Files []*File Users []string LoggedIn bool - }{c.RootDomain, c.SiteTitle, indexFiles, allUsers, authd} + }{c.Host, c.SiteTitle, indexFiles, allUsers, authd} err = t.ExecuteTemplate(w, "index.html", data) if err != nil { log.Println(err) @@ -195,12 +195,12 @@ func mySiteHandler(w http.ResponseWriter, r *http.Request) { // check auth files, _ := getUserFiles(authUser) data := struct { - Domain string + Host string PageTitle string AuthUser string Files []*File LoggedIn bool - }{c.RootDomain, c.SiteTitle, authUser, files, authd} + }{c.Host, c.SiteTitle, authUser, files, authd} _ = t.ExecuteTemplate(w, "my_site.html", data) } @@ -279,10 +279,10 @@ func isOkUsername(s string) bool { func registerHandler(w http.ResponseWriter, r *http.Request) { if r.Method == "GET" { data := struct { - Domain string + Host string Errors []string PageTitle string - }{c.RootDomain, nil, "Register"} + }{c.Host, nil, "Register"} err := t.ExecuteTemplate(w, "register.html", data) if err != nil { log.Println(err) @@ -315,18 +315,18 @@ func registerHandler(w http.ResponseWriter, r *http.Request) { } if len(errors) > 0 { data := struct { - Domain string + Host string Errors []string PageTitle string - }{c.RootDomain, errors, "Register"} + }{c.Host, errors, "Register"} t.ExecuteTemplate(w, "register.html", data) } else { os.Mkdir(path.Join(c.FilesDirectory, username), os.ModePerm) data := struct { - Domain string + Host string Message string PageTitle string - }{c.RootDomain, "Registration complete! The server admin will approve your request before you can log in.", "Registration Complete"} + }{c.Host, "Registration complete! The server admin will approve your request before you can log in.", "Registration Complete"} t.ExecuteTemplate(w, "message.html", data) } } @@ -354,7 +354,7 @@ func userFile(w http.ResponseWriter, r *http.Request) { } func runHTTPServer() { - log.Println("Running http server") + log.Printf("Running http server on %s", c.Host) var err error t, err = template.ParseGlob(path.Join(c.TemplatesDirectory, "*.html")) if err != nil { @@ -362,14 +362,22 @@ func runHTTPServer() { } serveMux := http.NewServeMux() - serveMux.HandleFunc(c.RootDomain+"/", rootHandler) - serveMux.HandleFunc(c.RootDomain+"/my_site", mySiteHandler) - serveMux.HandleFunc(c.RootDomain+"/edit/", editFileHandler) - serveMux.HandleFunc(c.RootDomain+"/upload", uploadFilesHandler) - serveMux.HandleFunc(c.RootDomain+"/login", loginHandler) - serveMux.HandleFunc(c.RootDomain+"/logout", logoutHandler) - serveMux.HandleFunc(c.RootDomain+"/register", registerHandler) - serveMux.HandleFunc(c.RootDomain+"/delete/", deleteFileHandler) + s := strings.SplitN(c.Host, ":", 2) + hostname := s[0] + var port string + if len(s) > 1 { + port = s[1] + } else { + port = "443" + } + serveMux.HandleFunc(hostname+"/", rootHandler) + serveMux.HandleFunc(hostname+"/my_site", mySiteHandler) + serveMux.HandleFunc(hostname+"/edit/", editFileHandler) + serveMux.HandleFunc(hostname+"/upload", uploadFilesHandler) + serveMux.HandleFunc(hostname+"/login", loginHandler) + serveMux.HandleFunc(hostname+"/logout", logoutHandler) + serveMux.HandleFunc(hostname+"/register", registerHandler) + serveMux.HandleFunc(hostname+"/delete/", deleteFileHandler) // TODO rate limit login https://github.com/ulule/limiter @@ -382,9 +390,9 @@ func runHTTPServer() { ReadTimeout: 5 * time.Second, WriteTimeout: 10 * time.Second, IdleTimeout: 120 * time.Second, - Addr: ":8080", + Addr: ":" + port, // TLSConfig: tlsConfig, Handler: wrapped, } - log.Fatal(srv.ListenAndServe()) + log.Fatal(srv.ListenAndServeTLS(c.TLSCertFile, c.TLSKeyFile)) } diff --git a/main.go b/main.go @@ -97,6 +97,16 @@ func main() { if err != nil { log.Fatal(err) } + + // Generate self signed cert if does not exist. This is not suitable for production. + _, err1 := os.Stat(c.TLSCertFile) + _, err2 := os.Stat(c.TLSKeyFile) + if os.IsNotExist(err1) || os.IsNotExist(err2) { + log.Println("Keyfile or certfile does not exist.") + } + + // Generate session cookie key if does not exist + SessionStore = sessions.NewCookieStore([]byte(c.CookieStoreKey)) DB, err = sql.Open("sqlite3", c.DBFile) if err != nil { diff --git a/schema.sql b/schema.sql @@ -7,3 +7,6 @@ CREATE TABLE user ( created_at INTEGER DEFAULT (strftime('%s', 'now')) ); +CREATE TABLE cookie_key ( + value TEXT NOT NULL; +); diff --git a/templates/index.gmi b/templates/index.gmi @@ -1,4 +1,4 @@ -{{$domain := .Domain}} +{{$domain := .Host}} # {{.SiteTitle}}! Welcome to flounder, a home for Gemini sites. Flounder hosts small Gemini web pages over https and Gemini. Right now, the only way to make an account is via the https portal, but I'm working on adding alternatives. Feel free to make an account and join if you'd like! diff --git a/templates/index.html b/templates/index.html @@ -1,4 +1,4 @@ -{{$domain := .Domain}} +{{$domain := .Host}} {{template "header" .}} <h1>{{.PageTitle}}!</h1> {{template "nav.html" .}} diff --git a/templates/message.html b/templates/message.html @@ -1,5 +1,5 @@ {{template "header" .}} <h1>{{.PageTitle}}</h1> {{ .Message }} -<a href="https://{{.Domain}}">Go home</a> +<a href="https://{{.Host}}">Go home</a> {{template "footer" .}} diff --git a/templates/my_site.html b/templates/my_site.html @@ -1,4 +1,4 @@ -{{$domain := .Domain}} +{{$domain := .Host}} {{$authUser := .AuthUser}} {{template "header" .}} <h1>Managing diff --git a/templates/register.html b/templates/register.html @@ -9,7 +9,7 @@ size="32" type="text" value="" - />.{{.Domain}} + />.{{.Host}} </div> <div> <label for="email">Email</label> diff --git a/templates/user_page.html b/templates/user_page.html @@ -1,4 +1,4 @@ -{{$domain := .Domain}} +{{$domain := .Host}} {{template "header" .}} <h1>{{.PageTitle}}!</h1> {{template "nav.html" .}}