492 lines
11 KiB
Go
492 lines
11 KiB
Go
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 <alias> -> <target>")
|
|
}
|
|
|
|
if len(alias) < 1 || len(target) < 1 {
|
|
return errors.New("syntax: alias must be of the form: alias <alias> -> <target>")
|
|
}
|
|
|
|
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
|
|
}
|