mirror of
https://code.rocketnine.space/tslocum/twins.git
synced 2024-11-27 12:38:14 +01:00
parent
b4876f8756
commit
9c7ee8bb10
5 changed files with 105 additions and 76 deletions
|
@ -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,33 +109,37 @@ 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
|
||||||
path: /sites
|
key: /srv/gemini.rocks/data/cert.key
|
||||||
root: /home/gemini.rocks/data/sites
|
paths:
|
||||||
listdirectory: true
|
-
|
||||||
-
|
path: /sites
|
||||||
path: ^/(help|info)$
|
root: /home/geminirocks/data/sites
|
||||||
root: /home/gemini.rocks/data/help
|
listdirectory: true
|
||||||
-
|
-
|
||||||
path: ^/proxy-example$
|
path: ^/(help|info)$
|
||||||
proxy: gemini://localhost:1966
|
root: /home/geminirocks/data/help
|
||||||
-
|
-
|
||||||
path: ^/cmd-example$
|
path: ^/proxy-example$
|
||||||
command: uname -a
|
proxy: gemini://localhost:1966
|
||||||
-
|
-
|
||||||
path: /
|
path: ^/cmd-example$
|
||||||
root: /home/gemini.rocks/data/home
|
command: uname -a
|
||||||
|
-
|
||||||
|
path: /
|
||||||
|
root: /home/geminirocks/data/home
|
||||||
twins.rocketnine.space:
|
twins.rocketnine.space:
|
||||||
-
|
cert: /srv/twins.rocketnine.space/data/cert.crt
|
||||||
path: /sites
|
key: /srv/twins.rocketnine.space/data/cert.key
|
||||||
root: /home/twins/data/sites
|
paths:
|
||||||
-
|
-
|
||||||
path: /
|
path: /sites
|
||||||
root: /home/twins/data/home
|
root: /home/twins/data/sites
|
||||||
|
-
|
||||||
|
path: /
|
||||||
|
root: /home/twins/data/home
|
||||||
```
|
```
|
||||||
|
|
|
@ -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
|
||||||
|
|
||||||
|
|
41
config.go
41
config.go
|
@ -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
35
main.go
|
@ -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)
|
||||||
}
|
}
|
||||||
|
|
19
server.go
19
server.go
|
@ -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)
|
||||||
|
|
Loading…
Reference in a new issue