package main import ( "errors" "github.com/BurntSushi/toml" "log" "os" "path/filepath" "strings" ) type SysConfig struct { Port int Hostname string CertPath string KeyPath string AccessLog string ErrorLog string DocBase string HomeDocBase string CGIPaths []string SCGIPaths map[string]string ReadMollyFiles bool AllowTLS12 bool RateLimitEnable bool RateLimitAverage int RateLimitSoft int RateLimitHard int } type UserConfig struct { GeminiExt string DefaultLang string DefaultEncoding string TempRedirects map[string]string PermRedirects map[string]string MimeOverrides map[string]string CertificateZones map[string][]string DirectoryListing bool DirectorySort string DirectorySubdirsFirst bool DirectoryReverse bool DirectoryTitles bool } func getConfig(filename string) (SysConfig, UserConfig, error) { var sysConfig SysConfig var userConfig UserConfig // Defaults sysConfig.Port = 1965 sysConfig.Hostname = "localhost" sysConfig.CertPath = "cert.pem" sysConfig.KeyPath = "key.pem" sysConfig.AccessLog = "access.log" sysConfig.ErrorLog = "" sysConfig.DocBase = "/var/gemini/" sysConfig.HomeDocBase = "users" sysConfig.CGIPaths = make([]string, 0) sysConfig.SCGIPaths = make(map[string]string) sysConfig.ReadMollyFiles = false sysConfig.AllowTLS12 = true sysConfig.RateLimitEnable = false sysConfig.RateLimitAverage = 1 sysConfig.RateLimitSoft = 10 sysConfig.RateLimitHard = 50 userConfig.GeminiExt = "gmi" userConfig.DefaultLang = "" userConfig.DefaultEncoding = "" userConfig.TempRedirects = make(map[string]string) userConfig.PermRedirects = make(map[string]string) userConfig.DirectoryListing = true userConfig.DirectorySort = "Name" userConfig.DirectorySubdirsFirst = false // Return defaults if no filename given if filename == "" { return sysConfig, userConfig, nil } // Attempt to overwrite defaults from file sysConfig, err := readSysConfig(filename, sysConfig) if err != nil { return sysConfig, userConfig, err } userConfig, err = readUserConfig(filename, userConfig, true) if err != nil { return sysConfig, userConfig, err } return sysConfig, userConfig, nil } func readSysConfig(filename string, config SysConfig) (SysConfig, error) { _, err := toml.DecodeFile(filename, &config) if err != nil { return config, err } // Force hostname to lowercase config.Hostname = strings.ToLower(config.Hostname) // Absolutise paths config.DocBase, err = filepath.Abs(config.DocBase) if err != nil { return config, err } config.CertPath, err = filepath.Abs(config.CertPath) if err != nil { return config, err } config.KeyPath, err = filepath.Abs(config.KeyPath) if err != nil { return config, err } if config.AccessLog != "" && config.AccessLog != "-" { config.AccessLog, err = filepath.Abs(config.AccessLog) if err != nil { return config, err } } if config.ErrorLog != "" { config.ErrorLog, err = filepath.Abs(config.ErrorLog) if err != nil { return config, err } } // Absolutise CGI paths for index, cgiPath := range config.CGIPaths { if !filepath.IsAbs(cgiPath) { config.CGIPaths[index] = filepath.Join(config.DocBase, cgiPath) } } // Expand CGI paths var cgiPaths []string for _, cgiPath := range config.CGIPaths { expandedPaths, err := filepath.Glob(cgiPath) if err != nil { return config, errors.New("Error expanding CGI path glob " + cgiPath + ": " + err.Error()) } cgiPaths = append(cgiPaths, expandedPaths...) } config.CGIPaths = cgiPaths // Absolutise SCGI paths for index, scgiPath := range config.SCGIPaths { config.SCGIPaths[index], err = filepath.Abs( scgiPath) if err != nil { return config, err } } return config, nil } func readUserConfig(filename string, config UserConfig, requireValid bool) (UserConfig, error) { _, err := toml.DecodeFile(filename, &config) if err != nil { return config, err } // Validate pseudo-enums if requireValid { switch config.DirectorySort { case "Name", "Size", "Time": default: return config, errors.New("Invalid DirectorySort value.") } } // Validate redirects for key, value := range config.TempRedirects { if strings.Contains(value, "://") && !strings.HasPrefix(value, "gemini://") { if requireValid { return config, errors.New("Invalid cross-protocol redirect to " + value) } else { log.Println("Ignoring cross-protocol redirect to " + value + " in .molly file " + filename) delete(config.TempRedirects, key) } } } for key, value := range config.PermRedirects { if strings.Contains(value, "://") && !strings.HasPrefix(value, "gemini://") { if requireValid { return config, errors.New("Invalid cross-protocol redirect to " + value) } else { log.Println("Ignoring cross-protocol redirect to " + value + " in .molly file " + filename) delete(config.PermRedirects, key) } } } return config, nil } func parseMollyFiles(path string, docBase string, config UserConfig) UserConfig { // Replace config variables which use pointers with new ones, // so that changes made here aren't reflected everywhere. config.TempRedirects = make(map[string]string) config.PermRedirects = make(map[string]string) config.MimeOverrides = make(map[string]string) config.CertificateZones = make(map[string][]string) // Build list of directories to check var dirs []string dirs = append(dirs, path) for { if path == filepath.Clean(docBase) { break } subpath := filepath.Dir(path) dirs = append(dirs, subpath) path = subpath } // Parse files in reverse order for i := len(dirs) - 1; i >= 0; i-- { dir := dirs[i] // Break out of the loop if a directory doesn't exist _, err := os.Stat(dir) if os.IsNotExist(err) { break } // Construct path for a .molly file in this dir mollyPath := filepath.Join(dir, ".molly") _, err = os.Stat(mollyPath) if err != nil { continue } // If the file exists and we can read it, try to parse it config, err = readUserConfig(mollyPath, config, false) if err != nil { log.Println("Error parsing .molly file " + mollyPath + ": " + err.Error()) continue } } return config }