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" } 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) } } // 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 }