mirror of
https://code.rocketnine.space/tslocum/twins.git
synced 2024-11-27 11:48:13 +01:00
066fe276eb
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.
293 lines
6.5 KiB
Go
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
|
|
}
|