Reorganized package tree and added doc comments
This commit is contained in:
34
router/config/config.go
Normal file
34
router/config/config.go
Normal file
@@ -0,0 +1,34 @@
|
||||
// Config provides a configuration system for routers.
|
||||
package config
|
||||
|
||||
import "crypto/tls"
|
||||
|
||||
// Config is an interface that configuration objects must fulfill.
|
||||
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)
|
||||
}
|
||||
|
||||
// User represents a Hnakra user.
|
||||
type User interface {
|
||||
Validate (key []byte) bool
|
||||
RconAllow () bool
|
||||
|
||||
OverPatterns (func (pattern string) bool)
|
||||
CanMountOn (scheme, host, path string) bool
|
||||
}
|
||||
491
router/config/file.go
Normal file
491
router/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
router/config/tls.go
Normal file
27
router/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() },
|
||||
}
|
||||
}
|
||||
@@ -3,10 +3,12 @@ package mux
|
||||
import "net/url"
|
||||
import "net/http"
|
||||
|
||||
// HTTP is an HTTP request multiplexer.
|
||||
type HTTP struct {
|
||||
Mux[http.Handler]
|
||||
}
|
||||
|
||||
// NewHTTP creates a new HTTP request multiplexer.
|
||||
func NewHTTP (resolver Resolver) *HTTP {
|
||||
mux := &HTTP { }
|
||||
mux.Mux.Redirect = mux.newRedirect
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
// Package mux provides request multiplexers for all protocols Hnakra supports.
|
||||
package mux
|
||||
|
||||
import "net"
|
||||
@@ -8,6 +9,8 @@ import "errors"
|
||||
import "strings"
|
||||
import "net/url"
|
||||
|
||||
// Resolver represents an object capable of transforming a hosname alias into
|
||||
// another hostname.
|
||||
type Resolver interface {
|
||||
ResolveAlias (alias string) string
|
||||
}
|
||||
@@ -68,6 +71,7 @@ func stripHostPort (h string) string {
|
||||
return host
|
||||
}
|
||||
|
||||
// Handler returns the handler for a particular URL.
|
||||
func (mux *Mux[HANDLER]) Handler (where *url.URL) (h HANDLER, pattern string) {
|
||||
// All other requests have any port stripped and path cleaned
|
||||
// before passing to mux.handler.
|
||||
@@ -144,6 +148,10 @@ func (mux *Mux[HANDLER]) match (path string, original *url.URL) (h HANDLER, patt
|
||||
return mux.NotFound(original), ""
|
||||
}
|
||||
|
||||
// Handle registers a handler on the specified pattern. If a pattern ends in
|
||||
// '/', all requests for URLS under the pattern will be directed to the handler,
|
||||
// as well as the pattern itself. Additionally, requests for the pattern without
|
||||
// the trailing slash will be redirected to the pattern with the trailing slash.
|
||||
func (mux *Mux[HANDLER]) Handle (pattern string, handler HANDLER) error {
|
||||
mux.mutex.Lock()
|
||||
defer mux.mutex.Unlock()
|
||||
@@ -176,6 +184,7 @@ func (mux *Mux[HANDLER]) Handle (pattern string, handler HANDLER) error {
|
||||
return nil
|
||||
}
|
||||
|
||||
// Unhandler removes the handler that was registered on the specified pattern.
|
||||
func (mux *Mux[HANDLER]) Unhandle (pattern string) error {
|
||||
mux.mutex.Lock()
|
||||
defer mux.mutex.Unlock()
|
||||
@@ -199,6 +208,7 @@ func (mux *Mux[HANDLER]) Unhandle (pattern string) error {
|
||||
return nil
|
||||
}
|
||||
|
||||
// OverHandlers calls a function for each registered handler.
|
||||
func (mux *Mux[HANDLER]) OverHandlers (callback func (pattern string, handler HANDLER) bool) {
|
||||
overSorted (mux.exactEntries, func (pattern string, entry muxEntry[HANDLER]) bool {
|
||||
return callback(pattern, entry.handler)
|
||||
|
||||
353
router/rcon/rcon.go
Normal file
353
router/rcon/rcon.go
Normal file
@@ -0,0 +1,353 @@
|
||||
// Package rcon provices a password-protected remote web interface for managing
|
||||
// a Hnakra router. For a user to be able to access the control panel, the user
|
||||
// entry in the configuration file must have the special string (rcon) listed as
|
||||
// an allowed pattern.
|
||||
package rcon
|
||||
|
||||
import "io"
|
||||
import "fmt"
|
||||
import "net/url"
|
||||
import "net/http"
|
||||
import "hnakra/router"
|
||||
import "hnakra/rotate"
|
||||
import "hnakra/protocol"
|
||||
import "hnakra/router/config"
|
||||
|
||||
const css = `
|
||||
body { background-color: #161815; color: #aea894; margin: 3em; }
|
||||
h1 { font-size: 1.5em; }
|
||||
h2 { font-size: 1.2em; }
|
||||
a { color: #69905f; }
|
||||
table { border-collapse: collapse; }
|
||||
th, td {
|
||||
border: 1px solid #272d24;
|
||||
padding: 0.5em;
|
||||
text-align: left;
|
||||
vertical-align: top;
|
||||
}`
|
||||
|
||||
type Rcon struct {
|
||||
root string
|
||||
config config.Config
|
||||
router *router.Router
|
||||
mux *http.ServeMux
|
||||
lines []string
|
||||
lineWriter io.Writer
|
||||
}
|
||||
|
||||
func New (root string) *Rcon {
|
||||
if len(root) > 0 && root[len(root) - 1] != '/' {
|
||||
root += "/"
|
||||
}
|
||||
rcon := &Rcon {
|
||||
root: root,
|
||||
mux: http.NewServeMux(),
|
||||
}
|
||||
rcon.lineWriter = rotate.LineWriter(rcon.pushLine)
|
||||
rcon.mux.HandleFunc(root + "", rcon.serveMainMenu)
|
||||
rcon.mux.HandleFunc(root + "log", rcon.serveLog)
|
||||
rcon.mux.HandleFunc(root + "services", rcon.serveServices)
|
||||
rcon.mux.HandleFunc(root + "config", rcon.serveConfig)
|
||||
rcon.mux.HandleFunc(root + "details", rcon.serveServiceDetails)
|
||||
return rcon
|
||||
}
|
||||
|
||||
func (rcon *Rcon) SetConfig (config config.Config) {
|
||||
rcon.config = config
|
||||
}
|
||||
|
||||
func (rcon *Rcon) SetRouter (router *router.Router) {
|
||||
rcon.router = router
|
||||
}
|
||||
|
||||
func (rcon *Rcon) pushLine (buffer []byte) (n int, err error) {
|
||||
const LEN_CAP = 128
|
||||
|
||||
n = len(buffer)
|
||||
line := string(buffer)
|
||||
rcon.lines = append(rcon.lines, line)
|
||||
if len(rcon.lines) > LEN_CAP {
|
||||
rcon.lines = rcon.lines[len(rcon.lines) - LEN_CAP:]
|
||||
}
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
func (rcon *Rcon) Write (buffer []byte) (int, error) {
|
||||
return rcon.lineWriter.Write(buffer)
|
||||
}
|
||||
|
||||
func (rcon *Rcon) ServeHTTP (res http.ResponseWriter, req *http.Request) {
|
||||
// disallow accessing rcon over an insecure connection
|
||||
if req.TLS == nil {
|
||||
res.Header().Add("content-type", "text/html")
|
||||
res.WriteHeader(http.StatusTeapot)
|
||||
res.Write([]byte (
|
||||
`<!DOCTYPE html>
|
||||
<html>
|
||||
<head>
|
||||
<title>HTTP Not Allowed</title>
|
||||
<style>` + css + `</style>
|
||||
</head>
|
||||
<body>
|
||||
<h1>HTTP Not Allowed</h1>
|
||||
<p>RCON is not available over HTTP for security reasons.
|
||||
Please try again with HTTPS.</p>
|
||||
</body>
|
||||
</html>`))
|
||||
return
|
||||
}
|
||||
|
||||
fail := func () {
|
||||
res.Header().Add("WWW-Authenticate", "Basic realm=rcon")
|
||||
res.WriteHeader(http.StatusUnauthorized)
|
||||
}
|
||||
|
||||
// need authentication for accessing rcon menu
|
||||
name, key, ok := req.BasicAuth()
|
||||
if ok {
|
||||
user := rcon.config.User(name)
|
||||
if !user.Validate([]byte(key)) { fail(); return }
|
||||
} else {
|
||||
fail(); return
|
||||
}
|
||||
|
||||
// if the user is authorized, serve page as normal
|
||||
rcon.mux.ServeHTTP(res, req)
|
||||
}
|
||||
|
||||
func (rcon *Rcon) serveMainMenu (res http.ResponseWriter, req *http.Request) {
|
||||
if req.URL.Path != rcon.root {
|
||||
http.NotFoundHandler().ServeHTTP(res, req)
|
||||
return
|
||||
}
|
||||
|
||||
res.Header().Add("content-type", "text/html")
|
||||
res.WriteHeader(http.StatusFound)
|
||||
res.Write([]byte (
|
||||
`<!DOCTYPE html>
|
||||
<html>
|
||||
<head>
|
||||
<title>RCON Menu</title>
|
||||
<style>` + css + `</style>
|
||||
</head>
|
||||
<body>
|
||||
<h1>RCON Menu</h1>
|
||||
<ul>
|
||||
<li><a href=log>log</a></li>
|
||||
<li><a href=services>services</a></li>
|
||||
<li><a href=config>config</a></li>
|
||||
</ul>
|
||||
</body>
|
||||
</html>`))
|
||||
}
|
||||
|
||||
func (rcon *Rcon) serveLog (res http.ResponseWriter, req *http.Request) {
|
||||
res.Header().Add("content-type", "text/html")
|
||||
res.WriteHeader(http.StatusFound)
|
||||
res.Write([]byte (
|
||||
`<!DOCTYPE html>
|
||||
<html>
|
||||
<head>
|
||||
<title>Router Log</title>
|
||||
<style>` + css + `</style>
|
||||
</head>
|
||||
<body>
|
||||
<h1>Router Log</h1><pre>`))
|
||||
|
||||
for _, line := range rcon.lines {
|
||||
res.Write([]byte(line))
|
||||
}
|
||||
|
||||
res.Write([]byte (
|
||||
`</pre></body>
|
||||
</html>`))
|
||||
}
|
||||
|
||||
func (rcon *Rcon) serveServices (res http.ResponseWriter, req *http.Request) {
|
||||
if rcon.router == nil {
|
||||
rcon.serveRouterOffline(res, req)
|
||||
return
|
||||
}
|
||||
|
||||
res.Header().Add("content-type", "text/html")
|
||||
res.WriteHeader(http.StatusFound)
|
||||
res.Write([]byte (
|
||||
`<!DOCTYPE html>
|
||||
<html>
|
||||
<head>
|
||||
<title>Connected Services</title>
|
||||
<style>` + css + `</style>
|
||||
</head>
|
||||
<body>
|
||||
<h1>Connected Services</h1>
|
||||
<table>
|
||||
<thead>
|
||||
<tr>
|
||||
<th>user</th>
|
||||
<th>name</th>
|
||||
<th>description</th>
|
||||
<th>pattern</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
`))
|
||||
|
||||
rcon.router.OverServices(func (service *router.Service) bool {
|
||||
res.Write([]byte(fmt.Sprintf (
|
||||
`<tr><td>%s</td><td><a href="details?service=%s">%s</a></td><td>%s</td><td>%s</td></tr>`,
|
||||
service.User(),
|
||||
service.Name(),
|
||||
service.Name(),
|
||||
service.Description(),
|
||||
router.FormatPattern(service.Pattern()))))
|
||||
return true
|
||||
})
|
||||
|
||||
res.Write([]byte (
|
||||
`</tbody></table></body>
|
||||
</html>`))
|
||||
}
|
||||
|
||||
func (rcon *Rcon) serveConfig (res http.ResponseWriter, req *http.Request) {
|
||||
res.Header().Add("content-type", "text/html")
|
||||
res.WriteHeader(http.StatusFound)
|
||||
res.Write([]byte (
|
||||
`<!DOCTYPE html>
|
||||
<html>
|
||||
<head>
|
||||
<title>Router Config</title>
|
||||
<style>` + css + `</style>
|
||||
</head>
|
||||
<body>
|
||||
<h1>Router Config</h1>
|
||||
<h2>Users</h2>
|
||||
<table>
|
||||
<thead>
|
||||
<tr>
|
||||
<th>name</th>
|
||||
<th>rcon</th>
|
||||
<th>patterns</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>`))
|
||||
|
||||
rcon.config.OverUsers(func (name string, user config.User) bool {
|
||||
patterns := ""
|
||||
user.OverPatterns (func (pattern string) bool {
|
||||
if patterns != "" {
|
||||
patterns += " "
|
||||
}
|
||||
patterns += pattern
|
||||
return true
|
||||
})
|
||||
res.Write([]byte(fmt.Sprintf (
|
||||
"<tr><td>%s</td><td>%v</td><td>%s</td></tr>",
|
||||
name, user.RconAllow(), patterns)))
|
||||
return true
|
||||
})
|
||||
|
||||
res.Write([]byte(`</tbody></table><h2>Aliases</h2>`))
|
||||
fallback := rcon.config.AliasFallback()
|
||||
if fallback != "" {
|
||||
res.Write([]byte(`<p>Fallback: ` + fallback + `</p>`))
|
||||
}
|
||||
res.Write([]byte(`
|
||||
<table>
|
||||
<thead>
|
||||
<tr>
|
||||
<th>alias</th>
|
||||
<th>target</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>`))
|
||||
rcon.config.OverAliases(func (alias, target string) bool {
|
||||
res.Write([]byte(fmt.Sprintf (
|
||||
"<tr><td>%s</td><td>%s</td></tr>",
|
||||
alias, target)))
|
||||
return true
|
||||
})
|
||||
|
||||
res.Write([]byte (
|
||||
`</tbody></table>
|
||||
<h2>Protocols</h2>
|
||||
<table>
|
||||
<thead>
|
||||
<tr>
|
||||
<th>scheme</th>
|
||||
<th>port</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>`))
|
||||
listProtocol := func (protocol string, port int) {
|
||||
res.Write([]byte(fmt.Sprintf (
|
||||
"<tr><td>%s</td><td>%d</td></tr>",
|
||||
protocol, port)))
|
||||
}
|
||||
if rcon.config.HTTPSEnable() {
|
||||
listProtocol("https", rcon.config.HTTPSPort())
|
||||
}
|
||||
if rcon.config.HTTPEnable() {
|
||||
listProtocol("http", rcon.config.HTTPPort())
|
||||
}
|
||||
if rcon.config.GeminiEnable() {
|
||||
listProtocol("gemini", rcon.config.GeminiPort())
|
||||
}
|
||||
res.Write([]byte(`</tbody></table></body></html>`))
|
||||
}
|
||||
|
||||
func (rcon *Rcon) serveServiceDetails (res http.ResponseWriter, req *http.Request) {
|
||||
name := req.URL.Query().Get("service")
|
||||
service := rcon.router.Service(name)
|
||||
if service == nil {
|
||||
http.NotFoundHandler().ServeHTTP(res, req)
|
||||
return
|
||||
}
|
||||
|
||||
res.Header().Add("content-type", "text/html")
|
||||
res.WriteHeader(http.StatusFound)
|
||||
res.Write([]byte (
|
||||
`<!DOCTYPE html>
|
||||
<html>
|
||||
<head>
|
||||
<title>Details of ` + service.Name() + `</title>
|
||||
<style>` + css + `</style>
|
||||
</head>
|
||||
<body>
|
||||
<h1>Details of ` + service.Name() + `</h1>
|
||||
<h2>Requests</h2>
|
||||
<table>
|
||||
<thead>
|
||||
<tr>
|
||||
<th>id</th>
|
||||
<th>url</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>`))
|
||||
|
||||
service.OverRequests(func (id protocol.ID, url *url.URL) bool {
|
||||
res.Write([]byte(fmt.Sprintf (
|
||||
"<tr><td>%v</td><td>%v</td></tr>",
|
||||
id, url)))
|
||||
return true
|
||||
})
|
||||
|
||||
res.Write([]byte(`</tbody></table></body></html>`))
|
||||
}
|
||||
|
||||
func (rcon *Rcon) serveRouterOffline (res http.ResponseWriter, req *http.Request) {
|
||||
res.Header().Add("content-type", "text/html")
|
||||
res.WriteHeader(http.StatusNotFound)
|
||||
res.Write([]byte (
|
||||
`<!DOCTYPE html>
|
||||
<html>
|
||||
<head>
|
||||
<title>Router Offline</title>
|
||||
<style>` + css + `</style>
|
||||
</head>
|
||||
<body>
|
||||
<h1>Router Offline</h1>
|
||||
<p>The router has not been started yet.</p>
|
||||
</body>
|
||||
</html>`))
|
||||
}
|
||||
@@ -1,32 +1,13 @@
|
||||
// Package router provides a reusable router implementation.
|
||||
package router
|
||||
|
||||
import "log"
|
||||
import "net"
|
||||
// import "errors"
|
||||
import "net/http"
|
||||
import "hnakra/config"
|
||||
import "hnakra/protocol"
|
||||
import "hnakra/router/mux"
|
||||
|
||||
type Pattern struct {
|
||||
Scheme string
|
||||
Host string
|
||||
Path string
|
||||
}
|
||||
|
||||
func (pattern Pattern) MuxPattern () string {
|
||||
return pattern.Host + pattern.Path
|
||||
}
|
||||
|
||||
func (pattern Pattern) String () string {
|
||||
return pattern.Scheme + "://" + pattern.Host + pattern.Path
|
||||
}
|
||||
|
||||
func (pattern *Pattern) FillDefaults () {
|
||||
if pattern.Scheme == "" { pattern.Scheme = "https" }
|
||||
if pattern.Host == "" { pattern.Host = "@" }
|
||||
if pattern.Path == "" { pattern.Path = "/" }
|
||||
}
|
||||
import "hnakra/router/config"
|
||||
|
||||
type Router struct {
|
||||
config config.Config
|
||||
@@ -81,13 +62,17 @@ func (router *Router) unlist (service *Service) {
|
||||
delete(router.services, service)
|
||||
}
|
||||
|
||||
func (router *Router) Validate (name string, key []byte, pattern Pattern) protocol.Status {
|
||||
func (router *Router) Validate (name string, key []byte, scheme, host, path string) protocol.Status {
|
||||
user := router.config.User(name)
|
||||
if user == nil || !user.Validate(key) {
|
||||
return protocol.StatusBadCredentials
|
||||
}
|
||||
if !user.CanMountOn(pattern.Scheme, pattern.Host, pattern.Path) {
|
||||
if !user.CanMountOn(scheme, host, path) {
|
||||
return protocol.StatusBadMount
|
||||
}
|
||||
return protocol.StatusOk
|
||||
}
|
||||
|
||||
func FormatPattern (scheme, host, path string) string {
|
||||
return scheme + "://" + host + path
|
||||
}
|
||||
|
||||
@@ -38,8 +38,7 @@ func (request *activeRequest) ensureHeader () {
|
||||
|
||||
type Service struct {
|
||||
router *Router
|
||||
validate func (user string, key []byte, pattern Pattern) protocol.Status
|
||||
pattern Pattern
|
||||
scheme, host, path string
|
||||
|
||||
user, name, description string
|
||||
|
||||
@@ -57,7 +56,6 @@ func (router *Router) newService (conn net.Conn) (service *Service) {
|
||||
service = &Service {
|
||||
idFactory: protocol.NewRouterIDFactory(),
|
||||
router: router,
|
||||
validate: router.Validate,
|
||||
conn: conn,
|
||||
requests: make(map[protocol.ID] *activeRequest),
|
||||
connReadWriter: bufio.NewReadWriter (
|
||||
@@ -88,8 +86,8 @@ func (service *Service) Description () string {
|
||||
return service.description
|
||||
}
|
||||
|
||||
func (service *Service) Pattern () Pattern {
|
||||
return service.pattern
|
||||
func (service *Service) Pattern () (user, name, description string) {
|
||||
return service.user, service.name, service.description
|
||||
}
|
||||
|
||||
func (service *Service) Close () error {
|
||||
@@ -104,9 +102,9 @@ func (service *Service) Shutdown () error {
|
||||
func (service *Service) ServeHTTP (res http.ResponseWriter, req *http.Request) {
|
||||
// if we are only accepting https requests and we recieve an http one,
|
||||
// redirect to the https version
|
||||
if req.TLS == nil && service.pattern.Scheme == "https" {
|
||||
if req.TLS == nil && service.scheme == "https" {
|
||||
newURL := req.URL
|
||||
newURL.Scheme = service.pattern.Scheme
|
||||
newURL.Scheme = service.scheme
|
||||
http.Redirect(res, req, newURL.String(), http.StatusPermanentRedirect)
|
||||
return
|
||||
}
|
||||
@@ -326,15 +324,17 @@ func (service *Service) authenticate () (func(), bool) {
|
||||
}
|
||||
|
||||
// create pattern
|
||||
service.pattern = Pattern {
|
||||
Scheme: login.Scheme,
|
||||
Host: login.Host,
|
||||
Path: login.Path,
|
||||
}
|
||||
service.pattern.FillDefaults()
|
||||
service.scheme = login.Scheme
|
||||
service.host = login.Host
|
||||
service.path = login.Path
|
||||
if service.scheme == "" { service.scheme = "https://" }
|
||||
if service.host == "" { service.host = "@" }
|
||||
if service.path == "" { service.path = "/" }
|
||||
|
||||
// validate credentials
|
||||
status := service.validate(login.User, login.Key, service.pattern)
|
||||
status := service.router.Validate (
|
||||
login.User, login.Key,
|
||||
service.scheme, service.host, service.path)
|
||||
service.send(protocol.MessageStatus { Status: status })
|
||||
if status == protocol.StatusOk {
|
||||
service.user = login.User
|
||||
@@ -343,19 +343,21 @@ func (service *Service) authenticate () (func(), bool) {
|
||||
|
||||
log.Println (
|
||||
"-->", service.conn.RemoteAddr(),
|
||||
"logged in as", login.User, "on", service.pattern)
|
||||
"logged in as", login.User, "on",
|
||||
FormatPattern(service.scheme, service.host, service.path))
|
||||
} else {
|
||||
log.Println (
|
||||
"ERR", service.conn.RemoteAddr(),
|
||||
"failed login as", login.User, "on", service.pattern)
|
||||
"failed login as", login.User, "on",
|
||||
FormatPattern(service.scheme, service.host, service.path))
|
||||
service.conn.Close()
|
||||
return nil, false
|
||||
}
|
||||
|
||||
// mount service on the mux.
|
||||
var unhandle func ()
|
||||
muxPattern := service.pattern.MuxPattern()
|
||||
switch service.pattern.Scheme {
|
||||
muxPattern := service.host + service.path
|
||||
switch service.scheme {
|
||||
case "http", "https":
|
||||
err = service.router.HTTPMux().Handle(muxPattern, service)
|
||||
unhandle = func () {
|
||||
@@ -367,7 +369,8 @@ func (service *Service) authenticate () (func(), bool) {
|
||||
if err != nil {
|
||||
log.Println (
|
||||
"ERR", service.name,
|
||||
"sent bad mount pattern:", service.pattern)
|
||||
"sent bad mount pattern:",
|
||||
FormatPattern(service.scheme, service.host, service.path))
|
||||
service.send(protocol.MessageStatus {
|
||||
Status: protocol.StatusBadMount,
|
||||
})
|
||||
|
||||
Reference in New Issue
Block a user