2023-05-25 16:08:56 -06:00
|
|
|
// 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"
|
2023-05-25 23:18:16 -06:00
|
|
|
import "hnakra/router/config"
|
2023-05-25 16:08:56 -06:00
|
|
|
|
|
|
|
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(),
|
2023-05-25 23:18:16 -06:00
|
|
|
router.FormatPattern(service.Pattern()))))
|
2023-05-25 16:08:56 -06:00
|
|
|
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>`))
|
|
|
|
}
|