Reorganized package tree and added doc comments

This commit is contained in:
Sasha Koshka
2023-05-26 01:18:16 -04:00
parent c300567c0c
commit 60e5a1c729
11 changed files with 48 additions and 44 deletions

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

View File

@@ -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

View File

@@ -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
View 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>`))
}

View File

@@ -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
}

View File

@@ -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,
})