hnakra/config/file.go
2023-05-25 18:08:56 -04:00

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
}