Initial commit
This commit is contained in:
353
rcon/rcon.go
Normal file
353
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/config"
|
||||
import "hnakra/router"
|
||||
import "hnakra/rotate"
|
||||
import "hnakra/protocol"
|
||||
|
||||
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(),
|
||||
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>`))
|
||||
}
|
||||
Reference in New Issue
Block a user