all repos — flounder @ e806ee97af47b96ed240b2c4ff0e20ee99eb4640

A small site builder for the Gemini protocol

TLS setup
alex wennerberg alex@alexwennerberg.com
Sun, 25 Oct 2020 10:47:24 -0700
commit

e806ee97af47b96ed240b2c4ff0e20ee99eb4640

parent

e8c4efc8ff0efd2f3f607384d88a92bf18e364c3

M .gitignore.gitignore

@@ -1,2 +1,3 @@

files/ -tmpcerts/ +*.crt +*.key
M README.mdREADME.md

@@ -9,8 +9,10 @@ ## Hosting

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.
M admin.goadmin.go

@@ -3,3 +3,8 @@

// Commands for administering your instance // reset user password -> generate link // delete user + +// Run some scripts to setup your instance + +func initialize() { +}
M config.goconfig.go

@@ -7,7 +7,8 @@

type Config struct { FilesDirectory string TemplatesDirectory string - RootDomain string + Hostname string + Host string SiteTitle string Debug bool SecretKey string

@@ -16,6 +17,8 @@ PasswdFile string // TODO remove

CookieStoreKey string OkExtensions []string MaxFileSize int + TLSCertFile string + TLSKeyFile string } func getConfig(filename string) (Config, error) {
M flounder.tomlflounder.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"]
M gemini.gogemini.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 @@ SiteTitle string

Files []*File Users []string }{ - Domain: c.RootDomain, + Domain: c.Hostname, SiteTitle: c.SiteTitle, Files: files, Users: users,

@@ -65,91 +60,15 @@ }

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 -}
M http.gohttp.go

@@ -57,12 +57,12 @@ renderError(w, InternalServerErrorMsg, 500)

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

// 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 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 @@ errors = append(errors, "Username or email is already used")

} 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 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 @@ log.Fatal(err)

} 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 @@ srv := &http.Server{

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)) }
M main.gomain.go

@@ -97,6 +97,16 @@ c, err = getConfig(*configPath)

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 {
M schema.sqlschema.sql

@@ -7,3 +7,6 @@ approved boolean NOT NULL DEFAULT false,

created_at INTEGER DEFAULT (strftime('%s', 'now')) ); +CREATE TABLE cookie_key ( + value TEXT NOT NULL; +);
M templates/index.gmitemplates/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!
M templates/index.htmltemplates/index.html

@@ -1,4 +1,4 @@

-{{$domain := .Domain}} +{{$domain := .Host}} {{template "header" .}} <h1>{{.PageTitle}}!</h1> {{template "nav.html" .}}
M templates/message.htmltemplates/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" .}}
M templates/my_site.htmltemplates/my_site.html

@@ -1,4 +1,4 @@

-{{$domain := .Domain}} +{{$domain := .Host}} {{$authUser := .AuthUser}} {{template "header" .}} <h1>Managing
M templates/register.htmltemplates/register.html

@@ -9,7 +9,7 @@ name="username"

size="32" type="text" value="" - />.{{.Domain}} + />.{{.Host}} </div> <div> <label for="email">Email</label>
M templates/user_page.htmltemplates/user_page.html

@@ -1,4 +1,4 @@

-{{$domain := .Domain}} +{{$domain := .Host}} {{template "header" .}} <h1>{{.PageTitle}}!</h1> {{template "nav.html" .}}