twins-upstream/config.go
Ivan Vilata-i-Balaguer 066fe276eb Fix listening on literal IPv6 address
Those contain colons, so splitting by colons would break the address in the
middle.  Instead, use a regular expression to split host and port at the last
colon, if followed by port digits and the end of the string.

Also, document the syntax for listening on literal IPv4 and IPv6 addresses.
2023-05-03 14:22:19 +02:00

293 lines
6.5 KiB
Go

package main
import (
"crypto/tls"
"errors"
"io/ioutil"
"log"
"net/url"
"os"
"regexp"
"strconv"
"strings"
"code.rocketnine.space/tslocum/gmitohtml/pkg/gmitohtml"
"github.com/kballard/go-shellquote"
"github.com/yookoala/gofast"
"gopkg.in/yaml.v2"
)
type pathConfig struct {
// Path to match
Path string
// Resource to serve
Root string
Command string
Proxy string
Redirect string
// Cache duration
Cache string
// FastCGI server address
FastCGI string
// Serve hidden files and directories
Hidden bool
// Request input
Input string
// Language
Lang string
// List directory
List bool
// Log file
Log string
// Request sensitive input
SensitiveInput string
// Follow symbolic links
SymLinks bool
// Content type
Type string
r *regexp.Regexp
cmd []string
cache int64
}
type hostConfig struct {
Cert string
Key string
Paths []*pathConfig
// Custom CSS styles. If specified, it will be used for all paths in that host/domain.
StyleSheet string
cert *tls.Certificate
css []byte
}
type serverConfig struct {
Listen string
Types map[string]string
ShowImages bool
Hosts map[string]*hostConfig
DisableHTTPS bool
DisableSize bool
SaneEOL bool
hostname string
port int
fcgiPools map[string]gofast.ConnFactory
}
const cacheUnset = -1965
var config *serverConfig
func readconfig(configPath string) error {
if configPath == "" {
return errors.New("file unspecified")
}
if _, err := os.Stat(configPath); os.IsNotExist(err) {
return errors.New("file not found")
}
configData, err := ioutil.ReadFile(configPath)
if err != nil {
return err
}
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")
}
if config.Types == nil {
config.Types = make(map[string]string)
}
if config.SaneEOL {
newLine = "\n"
} else {
newLine = "\r\n"
}
listenRe := regexp.MustCompile("(.*):([0-9]+)$")
if !listenRe.MatchString(config.Listen) {
config.hostname = config.Listen
config.Listen += ":1965"
} else {
config.hostname = listenRe.ReplaceAllString(config.Listen, "$1")
config.port, err = strconv.Atoi(listenRe.ReplaceAllString(config.Listen, "$2"))
if err != nil {
log.Fatalf("invalid port specified: %s", err)
}
}
// Default content types
if config.Types[".htm"] == "" {
config.Types[".htm"] = htmlType
}
if config.Types[".html"] == "" {
config.Types[".html"] = htmlType
}
if config.Types[".gmi"] == "" {
config.Types[".gmi"] = geminiType
}
if config.Types[".gemini"] == "" {
config.Types[".gemini"] = geminiType
}
gmitohtml.Config.ConvertImages = config.ShowImages
defaultHost := config.Hosts["default"]
delete(config.Hosts, "default")
config.fcgiPools = make(map[string]gofast.ConnFactory)
for hostname, host := range config.Hosts {
hostname = strings.ToLower(hostname)
if defaultHost != nil {
if host.Cert == "" {
host.Cert = defaultHost.Cert
}
if host.Key == "" {
host.Key = defaultHost.Key
}
if len(defaultHost.Paths) == 1 {
defaultPath := defaultHost.Paths[0]
for _, serve := range host.Paths {
// Resources
if defaultPath.Root != "" && serve.Root == "" {
serve.Root = defaultPath.Root
} else if defaultPath.Command != "" && serve.Command == "" {
serve.Command = defaultPath.Command
} else if defaultPath.Proxy != "" && serve.Proxy == "" {
serve.Proxy = defaultPath.Proxy
}
// Attributes
if defaultPath.Cache != "" && serve.Cache == "" {
serve.Cache = defaultPath.Cache
}
if defaultPath.FastCGI != "" && serve.FastCGI == "" {
serve.FastCGI = defaultPath.FastCGI
}
if defaultPath.Hidden {
serve.Hidden = defaultPath.Hidden
}
if defaultPath.Lang != "" && serve.Lang == "" {
serve.Lang = defaultPath.Lang
}
if defaultPath.List {
serve.List = defaultPath.List
}
if defaultPath.Log != "" && serve.Log == "" {
serve.Log = defaultPath.Log
}
if defaultPath.SymLinks {
serve.SymLinks = defaultPath.SymLinks
}
}
} else if len(defaultHost.Paths) > 1 {
log.Fatal("only one path may be defined for the default host")
}
}
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
// Custom CSS stylesheets are precached in customCSS and used on HTTPS requests.
if host.StyleSheet != "" {
_, err := os.Stat(host.StyleSheet)
if os.IsNotExist(err) {
log.Printf("error: stylesheet '%s' not found", host.StyleSheet)
} else {
host.css, err = ioutil.ReadFile(host.StyleSheet)
if err != nil {
host.css = nil
log.Printf("error: failed to read stylesheet %s: %s", host.StyleSheet, err)
}
}
}
for _, serve := range host.Paths {
if serve.Path == "" {
log.Fatal("a path must be specified in each serve entry")
}
if serve.Path[0] == '^' {
serve.r = regexp.MustCompile(serve.Path)
}
var resources int
if serve.Root != "" {
resources++
}
if serve.Command != "" {
resources++
}
if serve.Proxy != "" {
resources++
}
if serve.Redirect != "" {
resources++
}
if resources == 0 {
log.Fatalf("a resource must specified for path %s%s", hostname, serve.Path)
} else if resources > 1 {
log.Fatalf("only one resource (root, command, proxy or redirect) may specified for path %s%s", hostname, serve.Path)
}
serve.cache = cacheUnset
if serve.Cache != "" {
serve.cache, err = strconv.ParseInt(serve.Cache, 10, 64)
if err != nil {
log.Fatalf("failed to parse cache duration for path %s: %s", serve.Path, err)
}
}
if serve.Command != "" {
serve.cmd, err = shellquote.Split(serve.Command)
if err != nil {
log.Fatalf("failed to parse command %s: %s", serve.cmd, err)
}
} else if serve.FastCGI != "" {
if serve.Root == "" {
log.Fatalf("root must be specified to use fastcgi resource %s of path %s%s", serve.FastCGI, hostname, serve.Path)
}
if config.fcgiPools[serve.FastCGI] == nil {
f, err := url.Parse(serve.FastCGI)
if err != nil {
log.Fatalf("failed to parse fastcgi resource %s: %s", serve.FastCGI, err)
}
config.fcgiPools[serve.FastCGI] = gofast.SimpleConnFactory(f.Scheme, f.Host+f.Path)
}
}
}
}
return nil
}