Support reloading configuration file

Resolves #2.
This commit is contained in:
Trevor Slocum 2020-10-31 14:59:44 -07:00
parent b4876f8756
commit 9c7ee8bb10
5 changed files with 105 additions and 76 deletions

View file

@ -16,12 +16,23 @@ Address to listen for connections on in the format of `interface:port`.
`:1965` `:1965`
## Certificates ## Hosts
At least one certificate and private key must be specified, as Gemini requires Hosts are defined by their hostname followed by one or more paths to serve.
TLS.
### 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. 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") 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. 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` 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. 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 ### Path
#### Resources #### Resources
@ -110,19 +109,20 @@ listen: :1965
# TLS certificates # TLS certificates
certificates: certificates:
- -
cert: /home/gemini.rocks/data/cert.crt
key: /home/gemini.rocks/data/cert.key
# Hosts and paths to serve # Hosts and paths to serve
hosts: hosts:
gemini.rocks: gemini.rocks:
cert: /srv/gemini.rocks/data/cert.crt
key: /srv/gemini.rocks/data/cert.key
paths:
- -
path: /sites path: /sites
root: /home/gemini.rocks/data/sites root: /home/geminirocks/data/sites
listdirectory: true listdirectory: true
- -
path: ^/(help|info)$ path: ^/(help|info)$
root: /home/gemini.rocks/data/help root: /home/geminirocks/data/help
- -
path: ^/proxy-example$ path: ^/proxy-example$
proxy: gemini://localhost:1966 proxy: gemini://localhost:1966
@ -131,8 +131,11 @@ hosts:
command: uname -a command: uname -a
- -
path: / path: /
root: /home/gemini.rocks/data/home root: /home/geminirocks/data/home
twins.rocketnine.space: twins.rocketnine.space:
cert: /srv/twins.rocketnine.space/data/cert.crt
key: /srv/twins.rocketnine.space/data/cert.key
paths:
- -
path: /sites path: /sites
root: /home/twins/data/sites root: /home/twins/data/sites

View file

@ -13,6 +13,7 @@ Breaking changes may be made.
- Directory listing (when enabled) - Directory listing (when enabled)
- Serve the output of system commands - Serve the output of system commands
- Reverse proxy requests - Reverse proxy requests
- Reload configuration on `SIGHUP`
## Download ## Download

View file

@ -1,6 +1,7 @@
package main package main
import ( import (
"crypto/tls"
"errors" "errors"
"io/ioutil" "io/ioutil"
"log" "log"
@ -35,26 +36,24 @@ type pathConfig struct {
cmd []string cmd []string
} }
type certConfig struct { type hostConfig struct {
Cert string Cert string
Key string Key string
Paths []*pathConfig
cert *tls.Certificate
} }
type serverConfig struct { type serverConfig struct {
Listen string Listen string
Certificates []*certConfig Hosts map[string]*hostConfig
Hosts map[string][]*pathConfig
hostname string hostname string
port int port int
} }
var config = &serverConfig{ var config *serverConfig
hostname: "localhost",
port: 1965,
}
func readconfig(configPath string) error { func readconfig(configPath string) error {
if configPath == "" { if configPath == "" {
@ -70,10 +69,16 @@ func readconfig(configPath string) error {
return err return err
} }
err = yaml.Unmarshal(configData, &config) var newConfig *serverConfig
err = yaml.Unmarshal(configData, &newConfig)
if err != nil { if err != nil {
return err return err
} }
config = newConfig
if config.Listen == "" {
log.Fatal("listen address must be specified")
}
split := strings.Split(config.Listen, ":") split := strings.Split(config.Listen, ":")
if len(split) != 2 { if len(split) != 2 {
@ -87,8 +92,18 @@ func readconfig(configPath string) error {
} }
} }
for _, paths := range config.Hosts { for _, host := range config.Hosts {
for _, serve := range paths { 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 == "" { if serve.Path == "" {
log.Fatal("path must be specified in serve entry") log.Fatal("path must be specified in serve entry")
} else if (serve.Root != "" && (serve.Proxy != "" || serve.Command != "")) || } else if (serve.Root != "" && (serve.Proxy != "" || serve.Command != "")) ||

35
main.go
View file

@ -1,11 +1,12 @@
package main package main
import ( import (
"crypto/tls"
"flag" "flag"
"log" "log"
"os" "os"
"os/signal"
"path" "path"
"syscall"
) )
func init() { 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) err := readconfig(*configFile)
if err != nil { if err != nil {
log.Fatalf("failed to read configuration file at %s: %v\nSee CONFIGURATION.md for information on configuring twins", *configFile, err) 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) log.Printf("twins running on %s:%d", config.hostname, config.port)
listen(config.Listen, certificates) listen(config.Listen)
} }

View file

@ -382,7 +382,7 @@ func handleConn(c net.Conn) {
} }
matchedHost = true matchedHost = true
for _, serve := range config.Hosts[hostname] { for _, serve := range config.Hosts[hostname].Paths {
matchedRegexp := serve.r != nil && serve.r.Match(pathBytes) matchedRegexp := serve.r != nil && serve.r.Match(pathBytes)
matchedPrefix := serve.r == nil && strings.HasPrefix(request.Path, serve.Path) matchedPrefix := serve.r == nil && strings.HasPrefix(request.Path, serve.Path)
if !matchedRegexp && !matchedPrefix { if !matchedRegexp && !matchedPrefix {
@ -440,6 +440,7 @@ func handleConn(c net.Conn) {
return return
} }
} }
break
} }
if matchedHost { 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) { func handleListener(l net.Listener) {
for { for {
conn, err := l.Accept() 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{ tlsConfig := &tls.Config{
Certificates: certificates, ClientAuth: tls.RequestClientCert,
GetCertificate: getCertificate,
} }
listener, err := tls.Listen("tcp", address, tlsConfig) listener, err := tls.Listen("tcp", address, tlsConfig)