// 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 (
`
HTTP Not Allowed
HTTP Not Allowed
RCON is not available over HTTP for security reasons.
Please try again with HTTPS.
`))
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 (
`
RCON Menu
RCON Menu
`))
}
func (rcon *Rcon) serveLog (res http.ResponseWriter, req *http.Request) {
res.Header().Add("content-type", "text/html")
res.WriteHeader(http.StatusFound)
res.Write([]byte (
`
Router Log
Router Log
`))
for _, line := range rcon.lines {
res.Write([]byte(line))
}
res.Write([]byte (
`
`))
}
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 (
`
Connected Services
Connected Services
user |
name |
description |
pattern |
`))
rcon.router.OverServices(func (service *router.Service) bool {
res.Write([]byte(fmt.Sprintf (
`%s | %s | %s | %s |
`,
service.User(),
service.Name(),
service.Name(),
service.Description(),
router.FormatPattern(service.Pattern()))))
return true
})
res.Write([]byte (
`
`))
}
func (rcon *Rcon) serveConfig (res http.ResponseWriter, req *http.Request) {
res.Header().Add("content-type", "text/html")
res.WriteHeader(http.StatusFound)
res.Write([]byte (
`
Router Config
Router Config
Users
name |
rcon |
patterns |
`))
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 (
"%s | %v | %s |
",
name, user.RconAllow(), patterns)))
return true
})
res.Write([]byte(`
Aliases
`))
fallback := rcon.config.AliasFallback()
if fallback != "" {
res.Write([]byte(`Fallback: ` + fallback + `
`))
}
res.Write([]byte(`
alias |
target |
`))
rcon.config.OverAliases(func (alias, target string) bool {
res.Write([]byte(fmt.Sprintf (
"%s | %s |
",
alias, target)))
return true
})
res.Write([]byte (
`
Protocols
scheme |
port |
`))
listProtocol := func (protocol string, port int) {
res.Write([]byte(fmt.Sprintf (
"%s | %d |
",
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(`
`))
}
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 (
`
Details of ` + service.Name() + `
Details of ` + service.Name() + `
Requests
id |
url |
`))
service.OverRequests(func (id protocol.ID, url *url.URL) bool {
res.Write([]byte(fmt.Sprintf (
"%v | %v |
",
id, url)))
return true
})
res.Write([]byte(`
`))
}
func (rcon *Rcon) serveRouterOffline (res http.ResponseWriter, req *http.Request) {
res.Header().Add("content-type", "text/html")
res.WriteHeader(http.StatusNotFound)
res.Write([]byte (
`
Router Offline
Router Offline
The router has not been started yet.
`))
}