From 9c7ee8bb109d50083d1af3d8070961b9b36aab0b Mon Sep 17 00:00:00 2001 From: Trevor Slocum Date: Sat, 31 Oct 2020 14:59:44 -0700 Subject: [PATCH] Support reloading configuration file Resolves #2. --- CONFIGURATION.md | 85 +++++++++++++++++++++++++----------------------- README.md | 1 + config.go | 41 +++++++++++++++-------- main.go | 35 +++++++++----------- server.go | 19 +++++++++-- 5 files changed, 105 insertions(+), 76 deletions(-) diff --git a/CONFIGURATION.md b/CONFIGURATION.md index 980afa6..7593863 100644 --- a/CONFIGURATION.md +++ b/CONFIGURATION.md @@ -16,12 +16,23 @@ Address to listen for connections on in the format of `interface:port`. `:1965` -## Certificates +## Hosts -At least one certificate and private key must be specified, as Gemini requires -TLS. +Hosts are defined by their hostname followed by one or more paths to serve. -### localhost certificate +Paths may be defined as fixed strings or regular expressions (starting with `^`). + +Paths are matched in the order they are defined. + +Fixed string paths will match with and without a trailing slash. + +When accessing a directory the file `index.gemini` or `index.gmi` is served. + +### Certificates + +A certificate and private key must be specified. + +#### localhost certificate Use `openssl` generate a certificate for localhost. @@ -32,7 +43,7 @@ openssl req -x509 -out localhost.crt -keyout localhost.key \ printf "[dn]\nCN=localhost\n[req]\ndistinguished_name = dn\n[EXT]\nsubjectAltName=DNS:localhost\nkeyUsage=digitalSignature\nextendedKeyUsage=serverAuth") ``` -### Domain certificate +#### Domain certificate Use [certbot](https://certbot.eff.org) to get a certificate from [Let's Encrypt](https://letsencrypt.org) for a domain. @@ -49,18 +60,6 @@ certbot certonly --config-dir /home/www/certs \ Provide the path to the certificate file at `certs/live/$DOMAIN/fullchain.pem` and the private key file at `certs/live/$DOMAIN/privkey.pem` to twins. -## Hosts - -Hosts are defined by their hostname followed by one or more paths to serve. - -Paths may be defined as fixed strings or regular expressions (starting with `^`). - -Paths are matched in the order they are defined. - -Fixed string paths will match with and without a trailing slash. - -When accessing a directory the file `index.gemini` or `index.gmi` is served. - ### Path #### Resources @@ -110,33 +109,37 @@ listen: :1965 # TLS certificates certificates: - - cert: /home/gemini.rocks/data/cert.crt - key: /home/gemini.rocks/data/cert.key # Hosts and paths to serve hosts: gemini.rocks: - - - path: /sites - root: /home/gemini.rocks/data/sites - listdirectory: true - - - path: ^/(help|info)$ - root: /home/gemini.rocks/data/help - - - path: ^/proxy-example$ - proxy: gemini://localhost:1966 - - - path: ^/cmd-example$ - command: uname -a - - - path: / - root: /home/gemini.rocks/data/home + cert: /srv/gemini.rocks/data/cert.crt + key: /srv/gemini.rocks/data/cert.key + paths: + - + path: /sites + root: /home/geminirocks/data/sites + listdirectory: true + - + path: ^/(help|info)$ + root: /home/geminirocks/data/help + - + path: ^/proxy-example$ + proxy: gemini://localhost:1966 + - + path: ^/cmd-example$ + command: uname -a + - + path: / + root: /home/geminirocks/data/home twins.rocketnine.space: - - - path: /sites - root: /home/twins/data/sites - - - path: / - root: /home/twins/data/home + cert: /srv/twins.rocketnine.space/data/cert.crt + key: /srv/twins.rocketnine.space/data/cert.key + paths: + - + path: /sites + root: /home/twins/data/sites + - + path: / + root: /home/twins/data/home ``` diff --git a/README.md b/README.md index a30ac4e..2e7ab2e 100644 --- a/README.md +++ b/README.md @@ -13,6 +13,7 @@ Breaking changes may be made. - Directory listing (when enabled) - Serve the output of system commands - Reverse proxy requests +- Reload configuration on `SIGHUP` ## Download diff --git a/config.go b/config.go index a09b676..a31600c 100644 --- a/config.go +++ b/config.go @@ -1,6 +1,7 @@ package main import ( + "crypto/tls" "errors" "io/ioutil" "log" @@ -35,26 +36,24 @@ type pathConfig struct { cmd []string } -type certConfig struct { - Cert string - Key string +type hostConfig struct { + Cert string + Key string + Paths []*pathConfig + + cert *tls.Certificate } type serverConfig struct { Listen string - Certificates []*certConfig - - Hosts map[string][]*pathConfig + Hosts map[string]*hostConfig hostname string port int } -var config = &serverConfig{ - hostname: "localhost", - port: 1965, -} +var config *serverConfig func readconfig(configPath string) error { if configPath == "" { @@ -70,10 +69,16 @@ func readconfig(configPath string) error { return err } - err = yaml.Unmarshal(configData, &config) + var newConfig *serverConfig + err = yaml.Unmarshal(configData, &newConfig) if err != nil { return err } + config = newConfig + + if config.Listen == "" { + log.Fatal("listen address must be specified") + } split := strings.Split(config.Listen, ":") if len(split) != 2 { @@ -87,8 +92,18 @@ func readconfig(configPath string) error { } } - for _, paths := range config.Hosts { - for _, serve := range paths { + for _, host := range config.Hosts { + if host.Cert == "" || host.Key == "" { + log.Fatal("a certificate must be specified for each domain (gemini requires TLS for all connections)") + } + + cert, err := tls.LoadX509KeyPair(host.Cert, host.Key) + if err != nil { + log.Fatalf("failed to load certificate: %s", err) + } + host.cert = &cert + + for _, serve := range host.Paths { if serve.Path == "" { log.Fatal("path must be specified in serve entry") } else if (serve.Root != "" && (serve.Proxy != "" || serve.Command != "")) || diff --git a/main.go b/main.go index 2262454..09d9f3d 100644 --- a/main.go +++ b/main.go @@ -1,11 +1,12 @@ package main import ( - "crypto/tls" "flag" "log" "os" + "os/signal" "path" + "syscall" ) func init() { @@ -26,29 +27,25 @@ func main() { } } + sig := make(chan os.Signal, 1) + signal.Notify(sig, syscall.SIGHUP) + go func() { + for { + <-sig + err := readconfig(*configFile) + if err != nil { + log.Fatalf("failed to reload configuration file at %s: %v", *configFile, err) + } + log.Println("configuration reloaded successfully") + } + }() + err := readconfig(*configFile) if err != nil { log.Fatalf("failed to read configuration file at %s: %v\nSee CONFIGURATION.md for information on configuring twins", *configFile, err) } - if config.hostname == "" || config.port <= 0 { - log.Fatal("hostname and port must be specified") - } - - if len(config.Certificates) == 0 { - log.Fatal("at least one certificate must be specified (gemini requires TLS for all connections)") - } - - var certificates []tls.Certificate - for _, cert := range config.Certificates { - cert, err := tls.LoadX509KeyPair(cert.Cert, cert.Key) - if err != nil { - log.Fatalf("failed to load certificate: %s", err) - } - certificates = append(certificates, cert) - } - log.Printf("twins running on %s:%d", config.hostname, config.port) - listen(config.Listen, certificates) + listen(config.Listen) } diff --git a/server.go b/server.go index 0c70564..4055b7f 100644 --- a/server.go +++ b/server.go @@ -382,7 +382,7 @@ func handleConn(c net.Conn) { } matchedHost = true - for _, serve := range config.Hosts[hostname] { + for _, serve := range config.Hosts[hostname].Paths { matchedRegexp := serve.r != nil && serve.r.Match(pathBytes) matchedPrefix := serve.r == nil && strings.HasPrefix(request.Path, serve.Path) if !matchedRegexp && !matchedPrefix { @@ -440,6 +440,7 @@ func handleConn(c net.Conn) { return } } + break } if matchedHost { @@ -449,6 +450,17 @@ func handleConn(c net.Conn) { } } +func getCertificate(info *tls.ClientHelloInfo) (*tls.Certificate, error) { + host := config.Hosts[info.ServerName] + if host != nil { + return host.cert, nil + } + for _, host := range config.Hosts { + return host.cert, nil + } + return nil, nil +} + func handleListener(l net.Listener) { for { conn, err := l.Accept() @@ -460,9 +472,10 @@ func handleListener(l net.Listener) { } } -func listen(address string, certificates []tls.Certificate) { +func listen(address string) { tlsConfig := &tls.Config{ - Certificates: certificates, + ClientAuth: tls.RequestClientCert, + GetCertificate: getCertificate, } listener, err := tls.Listen("tcp", address, tlsConfig)