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

`)) rcon.router.OverServices(func (service *router.Service) bool { res.Write([]byte(fmt.Sprintf ( ``, service.User(), service.Name(), service.Name(), service.Description(), router.FormatPattern(service.Pattern())))) return true }) res.Write([]byte ( `
user name description pattern
%s%s%s%s
`)) } 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

`)) 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 ( "", name, user.RconAllow(), patterns))) return true }) res.Write([]byte(`
name rcon patterns
%s%v%s

Aliases

`)) fallback := rcon.config.AliasFallback() if fallback != "" { res.Write([]byte(`

Fallback: ` + fallback + `

`)) } res.Write([]byte(` `)) rcon.config.OverAliases(func (alias, target string) bool { res.Write([]byte(fmt.Sprintf ( "", alias, target))) return true }) res.Write([]byte ( `
alias target
%s%s

Protocols

`)) listProtocol := func (protocol string, port int) { res.Write([]byte(fmt.Sprintf ( "", 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(`
scheme port
%s%d
`)) } 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

`)) service.OverRequests(func (id protocol.ID, url *url.URL) bool { res.Write([]byte(fmt.Sprintf ( "", id, url))) return true }) res.Write([]byte(`
id url
%v%v
`)) } 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.

`)) }