2020-10-29 21:35:48 +01:00
|
|
|
package main
|
|
|
|
|
|
|
|
import (
|
2020-10-31 22:59:44 +01:00
|
|
|
"crypto/tls"
|
2020-10-29 21:35:48 +01:00
|
|
|
"errors"
|
|
|
|
"io/ioutil"
|
2020-10-30 01:17:23 +01:00
|
|
|
"log"
|
2020-11-04 21:48:55 +01:00
|
|
|
"net/url"
|
2020-10-29 21:35:48 +01:00
|
|
|
"os"
|
|
|
|
"regexp"
|
2020-10-30 21:30:09 +01:00
|
|
|
"strconv"
|
|
|
|
"strings"
|
2020-10-29 21:35:48 +01:00
|
|
|
|
2020-10-30 19:19:16 +01:00
|
|
|
"github.com/kballard/go-shellquote"
|
2020-11-04 21:48:55 +01:00
|
|
|
"github.com/yookoala/gofast"
|
2020-10-29 21:35:48 +01:00
|
|
|
"gopkg.in/yaml.v3"
|
|
|
|
)
|
|
|
|
|
2020-10-30 21:30:09 +01:00
|
|
|
type pathConfig struct {
|
2020-10-30 19:19:16 +01:00
|
|
|
// Path to match
|
2020-10-29 22:58:12 +01:00
|
|
|
Path string
|
2020-10-30 01:17:23 +01:00
|
|
|
|
2020-10-30 19:19:16 +01:00
|
|
|
// Resource to serve
|
2020-11-19 18:24:12 +01:00
|
|
|
Root string
|
|
|
|
Command string
|
|
|
|
Proxy string
|
|
|
|
Redirect string
|
2020-10-29 21:35:48 +01:00
|
|
|
|
2020-11-10 05:10:53 +01:00
|
|
|
// Cache duration
|
2020-11-10 06:12:22 +01:00
|
|
|
Cache string
|
2020-11-10 05:10:53 +01:00
|
|
|
|
2020-11-04 21:48:55 +01:00
|
|
|
// FastCGI server address
|
|
|
|
FastCGI string
|
|
|
|
|
2020-12-03 20:12:13 +01:00
|
|
|
// Serve hidden files and directories
|
|
|
|
Hidden bool
|
|
|
|
|
|
|
|
// Request input
|
|
|
|
Input string
|
|
|
|
|
2020-12-02 19:29:23 +01:00
|
|
|
// Language
|
|
|
|
Lang string
|
|
|
|
|
2020-12-03 20:12:13 +01:00
|
|
|
// List directory
|
|
|
|
List bool
|
|
|
|
|
2020-11-12 18:56:59 +01:00
|
|
|
// Log file
|
|
|
|
Log string
|
|
|
|
|
2020-12-03 20:12:13 +01:00
|
|
|
// Request sensitive input
|
|
|
|
SensitiveInput string
|
|
|
|
|
|
|
|
// Follow symbolic links
|
|
|
|
SymLinks bool
|
|
|
|
|
|
|
|
// Content type
|
|
|
|
Type string
|
|
|
|
|
2020-11-10 06:12:22 +01:00
|
|
|
r *regexp.Regexp
|
|
|
|
cmd []string
|
|
|
|
cache int64
|
2020-10-29 21:35:48 +01:00
|
|
|
}
|
|
|
|
|
2020-10-31 22:59:44 +01:00
|
|
|
type hostConfig struct {
|
|
|
|
Cert string
|
|
|
|
Key string
|
|
|
|
Paths []*pathConfig
|
|
|
|
|
|
|
|
cert *tls.Certificate
|
2020-10-30 21:30:09 +01:00
|
|
|
}
|
|
|
|
|
2020-10-29 21:35:48 +01:00
|
|
|
type serverConfig struct {
|
2020-12-03 20:12:13 +01:00
|
|
|
Listen string
|
|
|
|
Types map[string]string
|
|
|
|
Hosts map[string]*hostConfig
|
|
|
|
DisableHTTPS bool
|
|
|
|
DisableSize bool
|
|
|
|
SaneEOL bool
|
2020-11-05 21:57:28 +01:00
|
|
|
|
2020-11-04 21:48:55 +01:00
|
|
|
hostname string
|
|
|
|
port int
|
2020-11-04 22:07:06 +01:00
|
|
|
fcgiPools map[string]gofast.ConnFactory
|
2020-10-29 21:35:48 +01:00
|
|
|
}
|
|
|
|
|
2020-11-10 06:12:22 +01:00
|
|
|
const cacheUnset = -1965
|
|
|
|
|
2020-10-31 22:59:44 +01:00
|
|
|
var config *serverConfig
|
2020-10-29 21:35:48 +01:00
|
|
|
|
|
|
|
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
|
|
|
|
}
|
|
|
|
|
2020-10-31 22:59:44 +01:00
|
|
|
var newConfig *serverConfig
|
|
|
|
err = yaml.Unmarshal(configData, &newConfig)
|
2020-10-29 21:35:48 +01:00
|
|
|
if err != nil {
|
|
|
|
return err
|
|
|
|
}
|
2020-10-31 22:59:44 +01:00
|
|
|
config = newConfig
|
|
|
|
|
|
|
|
if config.Listen == "" {
|
|
|
|
log.Fatal("listen address must be specified")
|
|
|
|
}
|
2020-10-29 21:35:48 +01:00
|
|
|
|
2020-11-17 20:31:22 +01:00
|
|
|
if config.Types == nil {
|
|
|
|
config.Types = make(map[string]string)
|
|
|
|
}
|
|
|
|
|
2020-11-05 21:57:28 +01:00
|
|
|
if config.SaneEOL {
|
|
|
|
newLine = "\n"
|
|
|
|
} else {
|
|
|
|
newLine = "\r\n"
|
|
|
|
}
|
|
|
|
|
2020-10-30 21:30:09 +01:00
|
|
|
split := strings.Split(config.Listen, ":")
|
|
|
|
if len(split) != 2 {
|
|
|
|
config.hostname = config.Listen
|
|
|
|
config.Listen += ":1965"
|
|
|
|
} else {
|
|
|
|
config.hostname = split[0]
|
|
|
|
config.port, err = strconv.Atoi(split[1])
|
|
|
|
if err != nil {
|
|
|
|
log.Fatalf("invalid port specified: %s", err)
|
2020-10-29 21:35:48 +01:00
|
|
|
}
|
2020-10-30 21:30:09 +01:00
|
|
|
}
|
2020-10-29 22:58:12 +01:00
|
|
|
|
2020-11-17 20:31:22 +01:00
|
|
|
// 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
|
|
|
|
}
|
|
|
|
|
2020-11-10 21:05:42 +01:00
|
|
|
defaultHost := config.Hosts["default"]
|
|
|
|
delete(config.Hosts, "default")
|
|
|
|
|
2020-11-04 22:07:06 +01:00
|
|
|
config.fcgiPools = make(map[string]gofast.ConnFactory)
|
2020-11-04 21:48:55 +01:00
|
|
|
for hostname, host := range config.Hosts {
|
2020-11-26 06:43:50 +01:00
|
|
|
hostname = strings.ToLower(hostname)
|
|
|
|
|
2020-11-10 21:05:42 +01:00
|
|
|
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 {
|
2020-12-03 20:12:13 +01:00
|
|
|
// Resources
|
2020-11-10 21:05:42 +01:00
|
|
|
if defaultPath.Root != "" && serve.Root == "" {
|
|
|
|
serve.Root = defaultPath.Root
|
2020-12-03 20:12:13 +01:00
|
|
|
} else if defaultPath.Command != "" && serve.Command == "" {
|
2020-11-10 21:05:42 +01:00
|
|
|
serve.Command = defaultPath.Command
|
2020-12-03 20:12:13 +01:00
|
|
|
} else if defaultPath.Proxy != "" && serve.Proxy == "" {
|
2020-11-19 18:24:12 +01:00
|
|
|
serve.Proxy = defaultPath.Proxy
|
|
|
|
}
|
2020-12-03 20:12:13 +01:00
|
|
|
// Attributes
|
2020-11-10 21:05:42 +01:00
|
|
|
if defaultPath.Cache != "" && serve.Cache == "" {
|
|
|
|
serve.Cache = defaultPath.Cache
|
|
|
|
}
|
|
|
|
if defaultPath.FastCGI != "" && serve.FastCGI == "" {
|
|
|
|
serve.FastCGI = defaultPath.FastCGI
|
|
|
|
}
|
2020-12-03 20:12:13 +01:00
|
|
|
if defaultPath.Hidden {
|
|
|
|
serve.Hidden = defaultPath.Hidden
|
|
|
|
}
|
2020-12-02 19:29:23 +01:00
|
|
|
if defaultPath.Lang != "" && serve.Lang == "" {
|
|
|
|
serve.Lang = defaultPath.Lang
|
|
|
|
}
|
2020-12-03 20:12:13 +01:00
|
|
|
if defaultPath.List {
|
|
|
|
serve.List = defaultPath.List
|
|
|
|
}
|
2020-11-12 18:56:59 +01:00
|
|
|
if defaultPath.Log != "" && serve.Log == "" {
|
|
|
|
serve.Log = defaultPath.Log
|
|
|
|
}
|
2020-12-03 20:12:13 +01:00
|
|
|
if defaultPath.SymLinks {
|
|
|
|
serve.SymLinks = defaultPath.SymLinks
|
|
|
|
}
|
2020-11-10 21:05:42 +01:00
|
|
|
}
|
|
|
|
} else if len(defaultHost.Paths) > 1 {
|
|
|
|
log.Fatal("only one path may be defined for the default host")
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2020-10-31 22:59:44 +01:00
|
|
|
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 {
|
2020-10-30 21:30:09 +01:00
|
|
|
if serve.Path == "" {
|
2020-11-19 18:24:12 +01:00
|
|
|
log.Fatal("a path must be specified in each serve entry")
|
2020-10-30 21:30:09 +01:00
|
|
|
}
|
|
|
|
if serve.Path[0] == '^' {
|
|
|
|
serve.r = regexp.MustCompile(serve.Path)
|
|
|
|
}
|
2020-10-30 19:19:16 +01:00
|
|
|
|
2020-11-19 18:24:12 +01:00
|
|
|
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)
|
|
|
|
}
|
|
|
|
|
2020-11-10 06:12:22 +01:00
|
|
|
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)
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2020-11-19 18:24:12 +01:00
|
|
|
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 != "" {
|
2020-11-04 21:48:55 +01:00
|
|
|
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)
|
|
|
|
}
|
|
|
|
|
2020-11-04 22:07:06 +01:00
|
|
|
config.fcgiPools[serve.FastCGI] = gofast.SimpleConnFactory(f.Scheme, f.Host+f.Path)
|
2020-11-04 21:48:55 +01:00
|
|
|
}
|
2020-10-30 19:19:16 +01:00
|
|
|
}
|
|
|
|
}
|
2020-10-29 21:35:48 +01:00
|
|
|
}
|
|
|
|
|
|
|
|
return nil
|
|
|
|
}
|