Initial commit
This commit is contained in:
31
config/config.go
Normal file
31
config/config.go
Normal file
@@ -0,0 +1,31 @@
|
||||
package config
|
||||
|
||||
import "crypto/tls"
|
||||
|
||||
type Config interface {
|
||||
User (name string) User
|
||||
OverUsers (func (name string, user User) bool)
|
||||
|
||||
RconEnable () bool
|
||||
|
||||
RouterPort () int
|
||||
HTTPSPort () int
|
||||
HTTPSEnable () bool
|
||||
HTTPPort () int
|
||||
HTTPEnable () bool
|
||||
GeminiPort () int
|
||||
GeminiEnable () bool
|
||||
Certificate () tls.Certificate
|
||||
|
||||
ResolveAlias (alias string) string
|
||||
AliasFallback () string
|
||||
OverAliases (func (alias, target string) bool)
|
||||
}
|
||||
|
||||
type User interface {
|
||||
Validate (key []byte) bool
|
||||
RconAllow () bool
|
||||
|
||||
OverPatterns (func (pattern string) bool)
|
||||
CanMountOn (scheme, host, path string) bool
|
||||
}
|
||||
491
config/file.go
Normal file
491
config/file.go
Normal file
@@ -0,0 +1,491 @@
|
||||
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
|
||||
}
|
||||
27
config/tls.go
Normal file
27
config/tls.go
Normal file
@@ -0,0 +1,27 @@
|
||||
package config
|
||||
|
||||
import "crypto/tls"
|
||||
|
||||
// TLSConfigFor returns a robus TLS config from a given Config object.
|
||||
func TLSConfigFor (conf Config) *tls.Config {
|
||||
// following:
|
||||
// https://blog.cloudflare.com/exposing-go-on-the-internet/
|
||||
return &tls.Config {
|
||||
PreferServerCipherSuites: true,
|
||||
CurvePreferences: []tls.CurveID {
|
||||
// https://safecurves.cr.yp.to/
|
||||
tls.X25519,
|
||||
tls.CurveP521,
|
||||
},
|
||||
MinVersion: tls.VersionTLS13,
|
||||
CipherSuites: []uint16 {
|
||||
tls.TLS_ECDHE_ECDSA_WITH_AES_256_GCM_SHA384,
|
||||
tls.TLS_ECDHE_RSA_WITH_AES_256_GCM_SHA384,
|
||||
tls.TLS_ECDHE_ECDSA_WITH_CHACHA20_POLY1305,
|
||||
tls.TLS_ECDHE_RSA_WITH_CHACHA20_POLY1305,
|
||||
tls.TLS_ECDHE_ECDSA_WITH_AES_128_GCM_SHA256,
|
||||
tls.TLS_ECDHE_RSA_WITH_AES_128_GCM_SHA256,
|
||||
},
|
||||
Certificates: []tls.Certificate { conf.Certificate() },
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user