Initial commit

This commit is contained in:
Sasha Koshka
2023-05-25 18:08:56 -04:00
commit c300567c0c
51 changed files with 42251 additions and 0 deletions

31
config/config.go Normal file
View 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
View 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
View 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() },
}
}