package config import "os" import "io" import "bufio" import "errors" import "unicode" import "strconv" import "strings" import "crypto/tls" import "golang.org/x/crypto/bcrypt" type matchMode int; const ( // exactly match matchModeExact matchMode = iota // match all starting with/ending with (depending on context) matchModeGlob // match exactly, as well as anything underneath (hierarchically) matchModeExtend ) type pattern struct { matchAll bool scheme string host string path string hostMode matchMode pathMode matchMode } func (pattern pattern) String () (output string) { if pattern.matchAll { return "*" } output += pattern.scheme + "://" switch pattern.hostMode { case matchModeExtend: output += "." case matchModeGlob: output += "*" } output += pattern.host output += pattern.path switch pattern.pathMode { case matchModeExtend: output += "/" case matchModeGlob: output += "*" } return } func (pattern pattern) Match (scheme, host, path string) bool { if pattern.matchAll { return true } if scheme != pattern.scheme && pattern.scheme != "*" { return false } switch pattern.hostMode { case matchModeExact: if host != pattern.host { return false } case matchModeGlob: if !strings.HasSuffix(host, pattern.host) { return false } case matchModeExtend: match := host == pattern.host || strings.HasSuffix(host, "." + pattern.host) if !match { return false } default: return false } switch pattern.pathMode { case matchModeExact: if path != pattern.path { return false } case matchModeGlob: if !strings.HasPrefix(path, pattern.path) { return false } case matchModeExtend: match := path == pattern.path || strings.HasSuffix(path, "." + pattern.path) if !match { return false } default: return false } return false } // user is a real user parsed from a configuration file. type user struct { name string rconAllow bool noSecurity bool hash []byte patterns []pattern } func (user user) String () (output string) { output += user.name + " ->" first := true if user.rconAllow { output += " (rcon)" first = false } for _, pattern := range user.patterns { if !first { output += "," } first = false output += " " output += pattern.String() } return } func (user user) RconAllow () bool { return user.rconAllow } func (user user) OverPatterns (callback func (pattern string) bool) { for _, pattern := range user.patterns { if !callback(pattern.String()) { break } } } func (user user) Validate (key []byte) bool { if user.noSecurity == true { return true } return bcrypt.CompareHashAndPassword(user.hash, key) == nil } func (user user) CanMountOn (scheme, host, path string) bool { if user.noSecurity == true { return true } for _, pattern := range user.patterns { if pattern.Match(scheme, host, path) { return true } } return false } // dummy is a dummy user that validates everything to true. type dummy struct { } func (dummy) Validate ([]byte) bool { return true } func (dummy) CanMountOn (string, string, string) bool { return true } // config is a config read from a file type config struct { noSecurity bool users map[string] user rconEnable bool routerPort int httpsEnable bool httpsPort int httpEnable bool httpPort int geminiEnable bool geminiPort int fallback string aliases map[string]string keyPath string certPath string cert tls.Certificate } // File reads configuration values in order of precedence from: // - The file specified by path // - Environment variables // - Default values // If the given file is inaccessible, this function will not return an error. // However, if the file is accessible but it could not be parsed, it will return // an error. func File (path string) (Config, error) { conf := new(config) conf.populateDefault() file, err := os.Open(path) if err == nil { err = conf.readFrom(file) if err != nil { return nil, err } } return conf, nil } // Default returns configuration values from environment variables. Unset values // are populated with default values. func Default () Config { conf := new(config) conf.populateDefault() return conf } // ReadFrom reads a config file from a reader. It always closes the reader when // it is done. func ReadFrom (reader io.ReadCloser) (Config, error) { config := new(config) err := config.readFrom(reader) if err != nil { return nil, err } return config, nil } func (config *config) readFrom (reader io.ReadCloser) error { buferred := bufio.NewReader(reader) defer reader.Close() config.aliases = map[string]string { "localhost": "@", "127.0.0.1": "@", "::ffff:127.0.0.1": "@", "::1": "@", } config.users = make(map[string] user) var key string var val string var state int for { ch, _, err := buferred.ReadRune() if err != nil { if err == io.EOF { break } return err } switch state { case 0: // wait for key or comment if ch == '#' { state = 3 } else if !unicode.IsSpace(ch) { state = 1 buferred.UnreadRune() } break case 1: // ignore whitespace until value (or EOL) if ch == '\n' { err = config.handleKeyVal(key, "") if err != nil { return err } key = "" val = "" state = 0 } else if unicode.IsSpace(ch) { state = 2 } else { key += string(ch) } break case 2: // get key until EOL if ch == '\n' { err = config.handleKeyVal(key, strings.TrimSpace(val)) if err != nil { return err } key = "" val = "" state = 0 } else { val += string(ch) } break case 3: // ignore comment until EOL if ch == '\n' { state = 0 } break } } cert, err := tls.LoadX509KeyPair(config.certPath, config.keyPath) if err != nil { return err } config.cert = cert return nil } func (config *config) populateDefault () { defaultRouterPortStr := os.Getenv("HNAKRA_ROUTER_PORT") defaultHTTPSPortStr := os.Getenv("HNAKRA_HTTPS_PORT") defaultHTTPPortStr := os.Getenv("HNAKRA_HTTP_PORT") defaultGeminiPortStr := os.Getenv("HNAKRA_GEMINI_PORT") defaultRouterPort, _ := strconv.Atoi(defaultRouterPortStr) defaultHTTPSPort, _ := strconv.Atoi(defaultHTTPSPortStr) defaultHTTPPort, _ := strconv.Atoi(defaultHTTPPortStr) defaultGeminiPort, _ := strconv.Atoi(defaultGeminiPortStr) if defaultRouterPort == 0 { defaultRouterPort = 2048 } if defaultHTTPSPort == 0 { defaultHTTPSPort = 443 } if defaultHTTPPort == 0 { defaultHTTPPort = 80 } if defaultGeminiPort == 0 { defaultGeminiPort = 1965 } config.routerPort = defaultRouterPort config.httpsPort = defaultHTTPSPort config.httpPort = defaultHTTPPort config.geminiPort = defaultGeminiPort } func (config *config) handleKeyVal (key, val string) error { valn, _ := strconv.Atoi(val) switch key { case "alias": return config.parseAlias(key, val) case "unalias": delete(config.aliases, val) case "keyPath": config.keyPath = val case "certPath": config.certPath = val case "rcon": config.rconEnable = val == "true" case "router": config.routerPort = valn case "https": config.httpsEnable = true if valn > 0 { config.httpsPort = valn } case "http": config.httpEnable = true if valn > 0 { config.httpPort = valn } case "gemini": config.geminiEnable = true if valn > 0 { config.geminiPort = valn } case "noSecurity": config.noSecurity = val == "I AM NOT USING THIS IN PRODUCTION." case "user": return config.parseUser(key, val) } return nil } func (config *config) parseAlias (key, val string) error { alias, target, found := strings.Cut(val, "->") alias = strings.TrimSpace(alias) target = strings.TrimSpace(target) if !found { return errors.New("syntax: alias must be of the form: alias -> ") } if len(alias) < 1 || len(target) < 1 { return errors.New("syntax: alias must be of the form: alias -> ") } if alias == "(fallback)" { config.fallback = target } else { config.aliases[alias] = target } return nil } func (config *config) parseUser (key, val string) error { user := user { } name, keyAndPatterns, _ := strings.Cut(val, ":") name = strings.TrimSpace(name) user.name = name key, patternsStr, _ := strings.Cut(keyAndPatterns, "->") key = strings.TrimSpace(key) user.hash = []byte(key) patternStrs := strings.Split(patternsStr, ",") for _, patternStr := range patternStrs { patternStr = strings.TrimSpace(patternStr) if patternStr == "(rcon)" { user.rconAllow = true } else if patternStr != "" { pattern, err := parsePattern(patternStr) if err != nil { return err } user.patterns = append(user.patterns, pattern) } } config.users[name] = user return nil } func parsePattern (val string) (pattern, error) { pattern := pattern { } if val == "*" { pattern.matchAll = true return pattern, nil } scheme, hostAndPath, found := strings.Cut(val, "://") if !found { return pattern, errors.New("pattern must include scheme") } pattern.scheme = scheme host, path, found := strings.Cut(hostAndPath, "/") if !found { return pattern, errors.New("pattern must include path") } path = "/" + path if strings.HasPrefix(host, "*") { pattern.hostMode = matchModeGlob host = strings.TrimPrefix(host, "*") } else if strings.HasPrefix(host, ".") { pattern.hostMode = matchModeExtend host = strings.TrimPrefix(host, ".") } if strings.HasSuffix(path, "*") { pattern.pathMode = matchModeGlob path = strings.TrimSuffix(path, "*") } else if strings.HasSuffix(path, "/") { pattern.pathMode = matchModeExtend path = strings.TrimSuffix(path, "/") } pattern.host = host pattern.path = path return pattern, nil } func (config *config) OverUsers (callback func (name string, user User) bool) { for name, user := range config.users { if !callback(name, user) { break } } } func (config *config) User (name string) User { user := config.users[name] user.noSecurity = config.noSecurity return user } func (config *config) RconEnable () bool { return config.rconEnable } func (config *config) RouterPort () int { return config.routerPort } func (config *config) HTTPSEnable () bool { return config.httpsEnable } func (config *config) HTTPSPort () int { return config.httpsPort } func (config *config) HTTPEnable () bool { return config.httpEnable } func (config *config) HTTPPort () int { return config.httpPort } func (config *config) GeminiEnable () bool { return config.geminiEnable } func (config *config) GeminiPort () int { return config.geminiPort } func (config *config) ResolveAlias (alias string) string { // try to match an alias for key, value := range config.aliases { if alias == key { return value } } // if a fallback is set, and no aliases were found, use fallback if config.fallback != "" { return config.fallback } // if we don't have anything to resolve, return input as is return alias } func (config *config) AliasFallback () string { return config.fallback } func (config *config) OverAliases (callback func (alias, target string) bool) { for alias, target := range config.aliases { if !callback(alias, target) { break } } } func (config *config) Certificate () tls.Certificate { return config.cert }