mirror of
https://code.rocketnine.space/tslocum/twins.git
synced 2024-11-27 15:28:13 +01:00
parent
ae6e0b6b7b
commit
8aec55c459
5 changed files with 163 additions and 89 deletions
|
@ -6,35 +6,49 @@ Serve entries have either a `root` path or `proxy` URL. When a `root` path is
|
||||||
provided static files and directories are served from that location. When a
|
provided static files and directories are served from that location. When a
|
||||||
`proxy` URL is provided requests are forwarded to the Gemini server at that URL.
|
`proxy` URL is provided requests are forwarded to the Gemini server at that URL.
|
||||||
|
|
||||||
|
Paths are matched in the order they are provided.
|
||||||
|
|
||||||
When accessing a directory `index.gemini` or `index.gmi` is served.
|
When accessing a directory `index.gemini` or `index.gmi` is served.
|
||||||
|
|
||||||
# config.yaml
|
# config.yaml
|
||||||
|
|
||||||
```yaml
|
```yaml
|
||||||
# Hostname and port to listen for connections on
|
# Address to listen on
|
||||||
hostname: gemini.rocks
|
listen: 0.0.0.0:1965
|
||||||
port: 1965
|
|
||||||
|
|
||||||
# Path to certificate and private key
|
# TLS certificates
|
||||||
cert: /home/twins/data/certfile.crt
|
certificates:
|
||||||
key: /home/twins/data/keyfile.key
|
-
|
||||||
|
cert: /home/gemini.rocks/data/cert.crt
|
||||||
|
key: /home/gemini.rocks/data/cert.key
|
||||||
|
|
||||||
|
# Hosts and paths to serve
|
||||||
# Server paths
|
hosts:
|
||||||
serve:
|
|
||||||
-
|
-
|
||||||
path: /sites
|
name: gemini.rocks
|
||||||
root: /home/twins/data/sites
|
paths:
|
||||||
|
-
|
||||||
|
path: /sites
|
||||||
|
root: /home/gemini.rocks/data/sites
|
||||||
|
-
|
||||||
|
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
|
||||||
-
|
-
|
||||||
path: ^/(help|info)$
|
name: twins.rocketnine.space
|
||||||
root: /home/twins/data/help
|
paths:
|
||||||
-
|
-
|
||||||
path: ^/proxy-example$
|
path: /sites
|
||||||
proxy: gemini://localhost:1966
|
root: /home/twins/data/sites
|
||||||
-
|
-
|
||||||
path: ^/cmd-example$
|
path: /
|
||||||
command: uname -a
|
root: /home/twins/data/home
|
||||||
-
|
|
||||||
path: /
|
|
||||||
root: /home/twins/data/home
|
|
||||||
```
|
```
|
|
@ -7,7 +7,7 @@
|
||||||
## Features
|
## Features
|
||||||
|
|
||||||
- Serve static files
|
- Serve static files
|
||||||
- Serve output of system commands
|
- Serve the output of system commands
|
||||||
- Reverse proxy requests
|
- Reverse proxy requests
|
||||||
|
|
||||||
## Download
|
## Download
|
||||||
|
|
77
config.go
77
config.go
|
@ -6,12 +6,14 @@ import (
|
||||||
"log"
|
"log"
|
||||||
"os"
|
"os"
|
||||||
"regexp"
|
"regexp"
|
||||||
|
"strconv"
|
||||||
|
"strings"
|
||||||
|
|
||||||
"github.com/kballard/go-shellquote"
|
"github.com/kballard/go-shellquote"
|
||||||
"gopkg.in/yaml.v3"
|
"gopkg.in/yaml.v3"
|
||||||
)
|
)
|
||||||
|
|
||||||
type serveConfig struct {
|
type pathConfig struct {
|
||||||
// Path to match
|
// Path to match
|
||||||
Path string
|
Path string
|
||||||
|
|
||||||
|
@ -24,17 +26,30 @@ type serveConfig struct {
|
||||||
cmd []string
|
cmd []string
|
||||||
}
|
}
|
||||||
|
|
||||||
|
type hostConfig struct {
|
||||||
|
Name string
|
||||||
|
Paths []*pathConfig
|
||||||
|
}
|
||||||
|
|
||||||
|
type certConfig struct {
|
||||||
|
Cert string
|
||||||
|
Key string
|
||||||
|
}
|
||||||
|
|
||||||
type serverConfig struct {
|
type serverConfig struct {
|
||||||
Cert string
|
Listen string
|
||||||
Key string
|
|
||||||
Hostname string
|
Certificates []*certConfig
|
||||||
Port int
|
|
||||||
Serve []*serveConfig
|
Hosts []*hostConfig
|
||||||
|
|
||||||
|
hostname string
|
||||||
|
port int
|
||||||
}
|
}
|
||||||
|
|
||||||
var config = &serverConfig{
|
var config = &serverConfig{
|
||||||
Hostname: "localhost",
|
hostname: "localhost",
|
||||||
Port: 1965,
|
port: 1965,
|
||||||
}
|
}
|
||||||
|
|
||||||
func readconfig(configPath string) error {
|
func readconfig(configPath string) error {
|
||||||
|
@ -56,25 +71,39 @@ func readconfig(configPath string) error {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
for _, serve := range config.Serve {
|
split := strings.Split(config.Listen, ":")
|
||||||
if serve.Path == "" {
|
if len(split) != 2 {
|
||||||
log.Fatal("path must be specified in serve entry")
|
config.hostname = config.Listen
|
||||||
} else if (serve.Root != "" && (serve.Proxy != "" || serve.Command != "")) ||
|
config.Listen += ":1965"
|
||||||
(serve.Proxy != "" && (serve.Root != "" || serve.Command != "")) ||
|
} else {
|
||||||
(serve.Command != "" && (serve.Root != "" || serve.Proxy != "")) {
|
config.hostname = split[0]
|
||||||
log.Fatal("only one root, reverse proxy or command may specified in a serve entry")
|
config.port, err = strconv.Atoi(split[1])
|
||||||
|
if err != nil {
|
||||||
|
log.Fatalf("invalid port specified: %s", err)
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
if serve.Path[0] == '^' {
|
for _, host := range config.Hosts {
|
||||||
serve.r = regexp.MustCompile(serve.Path)
|
for _, serve := range host.Paths {
|
||||||
} else if serve.Path[len(serve.Path)-1] == '/' {
|
if serve.Path == "" {
|
||||||
serve.Path = serve.Path[:len(serve.Path)-1]
|
log.Fatal("path must be specified in serve entry")
|
||||||
}
|
} else if (serve.Root != "" && (serve.Proxy != "" || serve.Command != "")) ||
|
||||||
|
(serve.Proxy != "" && (serve.Root != "" || serve.Command != "")) ||
|
||||||
|
(serve.Command != "" && (serve.Root != "" || serve.Proxy != "")) {
|
||||||
|
log.Fatal("only one root, reverse proxy or command may specified in a serve entry")
|
||||||
|
}
|
||||||
|
|
||||||
if serve.Command != "" {
|
if serve.Path[0] == '^' {
|
||||||
serve.cmd, err = shellquote.Split(serve.Command)
|
serve.r = regexp.MustCompile(serve.Path)
|
||||||
if err != nil {
|
} else if serve.Path[len(serve.Path)-1] == '/' {
|
||||||
log.Fatalf("failed to parse command %s: %s", serve.cmd, err)
|
serve.Path = serve.Path[:len(serve.Path)-1]
|
||||||
|
}
|
||||||
|
|
||||||
|
if serve.Command != "" {
|
||||||
|
serve.cmd, err = shellquote.Split(serve.Command)
|
||||||
|
if err != nil {
|
||||||
|
log.Fatalf("failed to parse command %s: %s", serve.cmd, err)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
20
main.go
20
main.go
|
@ -1,6 +1,7 @@
|
||||||
package main
|
package main
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"crypto/tls"
|
||||||
"flag"
|
"flag"
|
||||||
"log"
|
"log"
|
||||||
"os"
|
"os"
|
||||||
|
@ -30,15 +31,24 @@ func main() {
|
||||||
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 {
|
if config.hostname == "" || config.port <= 0 {
|
||||||
log.Fatal("hostname and port must be specified")
|
log.Fatal("hostname and port must be specified")
|
||||||
}
|
}
|
||||||
|
|
||||||
if config.Cert == "" || config.Key == "" {
|
if len(config.Certificates) == 0 {
|
||||||
log.Fatal("certificate file and private key must be specified (gemini requires TLS for all connections)")
|
log.Fatal("at least one certificate must be specified (gemini requires TLS for all connections)")
|
||||||
}
|
}
|
||||||
|
|
||||||
log.Printf("twins running on port %d", config.Port)
|
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)
|
||||||
|
}
|
||||||
|
|
||||||
listen(config.Hostname, config.Port, config.Cert, config.Key)
|
log.Printf("twins running on %s:%d", config.hostname, config.port)
|
||||||
|
|
||||||
|
listen(config.Listen, certificates)
|
||||||
}
|
}
|
||||||
|
|
93
server.go
93
server.go
|
@ -48,7 +48,7 @@ func writeStatus(c net.Conn, code int) {
|
||||||
writeHeader(c, code, meta)
|
writeHeader(c, code, meta)
|
||||||
}
|
}
|
||||||
|
|
||||||
func serveFile(c net.Conn, filePath string) {
|
func serveFile(c net.Conn, requestData, filePath string) {
|
||||||
fi, err := os.Stat(filePath)
|
fi, err := os.Stat(filePath)
|
||||||
if os.IsNotExist(err) {
|
if os.IsNotExist(err) {
|
||||||
writeStatus(c, gemini.StatusNotFound)
|
writeStatus(c, gemini.StatusNotFound)
|
||||||
|
@ -59,6 +59,13 @@ func serveFile(c net.Conn, filePath string) {
|
||||||
}
|
}
|
||||||
|
|
||||||
if mode := fi.Mode(); mode.IsDir() {
|
if mode := fi.Mode(); mode.IsDir() {
|
||||||
|
if requestData[len(requestData)-1] != '/' {
|
||||||
|
// Add trailing slash
|
||||||
|
log.Println(requestData)
|
||||||
|
writeHeader(c, gemini.StatusRedirectPermanent, requestData+"/")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
_, err := os.Stat(path.Join(filePath, "index.gemini"))
|
_, err := os.Stat(path.Join(filePath, "index.gemini"))
|
||||||
if err == nil {
|
if err == nil {
|
||||||
filePath = path.Join(filePath, "index.gemini")
|
filePath = path.Join(filePath, "index.gemini")
|
||||||
|
@ -200,6 +207,7 @@ func handleConn(c net.Conn) {
|
||||||
requestData = scanner.Text()
|
requestData = scanner.Text()
|
||||||
}
|
}
|
||||||
if err := scanner.Err(); err != nil {
|
if err := scanner.Err(); err != nil {
|
||||||
|
log.Println(scanner.Text(), "FAILED")
|
||||||
writeStatus(c, gemini.StatusBadRequest)
|
writeStatus(c, gemini.StatusBadRequest)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
@ -214,7 +222,13 @@ func handleConn(c net.Conn) {
|
||||||
}
|
}
|
||||||
|
|
||||||
request, err := url.Parse(requestData)
|
request, err := url.Parse(requestData)
|
||||||
if err != nil || request.Hostname() == "" || strings.ContainsRune(request.Hostname(), ' ') {
|
if err != nil {
|
||||||
|
writeStatus(c, gemini.StatusBadRequest)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
requestHostname := request.Hostname()
|
||||||
|
if requestHostname == "" || strings.ContainsRune(requestHostname, ' ') {
|
||||||
writeStatus(c, gemini.StatusBadRequest)
|
writeStatus(c, gemini.StatusBadRequest)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
@ -230,7 +244,7 @@ func handleConn(c net.Conn) {
|
||||||
if request.Scheme == "" {
|
if request.Scheme == "" {
|
||||||
request.Scheme = "gemini"
|
request.Scheme = "gemini"
|
||||||
}
|
}
|
||||||
if request.Scheme != "gemini" || request.Hostname() != config.Hostname || (requestPort > 0 && requestPort != config.Port) {
|
if request.Scheme != "gemini" || (requestPort > 0 && requestPort != config.port) {
|
||||||
writeStatus(c, gemini.StatusProxyRequestRefused)
|
writeStatus(c, gemini.StatusProxyRequestRefused)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -246,35 +260,47 @@ func handleConn(c net.Conn) {
|
||||||
strippedPath = strippedPath[1:]
|
strippedPath = strippedPath[1:]
|
||||||
}
|
}
|
||||||
|
|
||||||
for _, serve := range config.Serve {
|
var matchedHost bool
|
||||||
if serve.Proxy != "" {
|
for _, host := range config.Hosts {
|
||||||
if serve.r != nil && serve.r.Match(pathBytes) {
|
if requestHostname != host.Name {
|
||||||
serveProxy(c, requestData, serve.Proxy)
|
continue
|
||||||
return
|
|
||||||
} else if serve.r == nil && strings.HasPrefix(request.Path, serve.Path) {
|
|
||||||
serveProxy(c, requestData, serve.Proxy)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
} else if serve.cmd != nil {
|
|
||||||
if serve.r != nil && serve.r.Match(pathBytes) {
|
|
||||||
serveCommand(c, serve.cmd)
|
|
||||||
return
|
|
||||||
} else if serve.r == nil && strings.HasPrefix(request.Path, serve.Path) {
|
|
||||||
serveCommand(c, serve.cmd)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
matchedHost = true
|
||||||
|
|
||||||
if serve.r != nil && serve.r.Match(pathBytes) {
|
for _, serve := range host.Paths {
|
||||||
serveFile(c, path.Join(serve.Root, strippedPath))
|
if serve.Proxy != "" {
|
||||||
return
|
if serve.r != nil && serve.r.Match(pathBytes) {
|
||||||
} else if serve.r == nil && strings.HasPrefix(request.Path, serve.Path) {
|
serveProxy(c, requestData, serve.Proxy)
|
||||||
serveFile(c, path.Join(serve.Root, strippedPath[len(serve.Path)-1:]))
|
return
|
||||||
return
|
} else if serve.r == nil && strings.HasPrefix(request.Path, serve.Path) {
|
||||||
|
serveProxy(c, requestData, serve.Proxy)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
} else if serve.cmd != nil {
|
||||||
|
if serve.r != nil && serve.r.Match(pathBytes) {
|
||||||
|
serveCommand(c, serve.cmd)
|
||||||
|
return
|
||||||
|
} else if serve.r == nil && strings.HasPrefix(request.Path, serve.Path) {
|
||||||
|
serveCommand(c, serve.cmd)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if serve.r != nil && serve.r.Match(pathBytes) {
|
||||||
|
serveFile(c, requestData, path.Join(serve.Root, strippedPath))
|
||||||
|
return
|
||||||
|
} else if serve.r == nil && strings.HasPrefix(request.Path, serve.Path) {
|
||||||
|
serveFile(c, requestData, path.Join(serve.Root, strippedPath[len(serve.Path)-1:]))
|
||||||
|
return
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
writeStatus(c, gemini.StatusNotFound)
|
if matchedHost {
|
||||||
|
writeStatus(c, gemini.StatusNotFound)
|
||||||
|
} else {
|
||||||
|
writeStatus(c, gemini.StatusProxyRequestRefused)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func handleListener(l net.Listener) {
|
func handleListener(l net.Listener) {
|
||||||
|
@ -288,19 +314,14 @@ func handleListener(l net.Listener) {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func listen(hostname string, port int, certFile, keyFile string) {
|
func listen(address string, certificates []tls.Certificate) {
|
||||||
cert, err := tls.LoadX509KeyPair(certFile, keyFile)
|
|
||||||
if err != nil {
|
|
||||||
log.Fatalf("failed to load certificate: %s", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
tlsConfig := &tls.Config{
|
tlsConfig := &tls.Config{
|
||||||
Certificates: []tls.Certificate{cert},
|
Certificates: certificates,
|
||||||
}
|
}
|
||||||
|
|
||||||
listener, err := tls.Listen("tcp", fmt.Sprintf("%s:%d", hostname, port), tlsConfig)
|
listener, err := tls.Listen("tcp", address, tlsConfig)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
log.Fatalf("failed to listen on %s:%d: %s", hostname, port, err)
|
log.Fatalf("failed to listen on %s: %s", address, err)
|
||||||
}
|
}
|
||||||
|
|
||||||
handleListener(listener)
|
handleListener(listener)
|
||||||
|
|
Loading…
Reference in a new issue