ce7cb702218693d08ea6d83464d2457c9250027c
This commit is contained in:
commit
34c5f8963c
1 changed files with 145336 additions and 0 deletions
140
packages/grafana-rcon/backend/main.go
Normal file
140
packages/grafana-rcon/backend/main.go
Normal file
|
|
@ -0,0 +1,140 @@
|
|||
package main
|
||||
|
||||
import (
|
||||
"context"
|
||||
"encoding/json"
|
||||
"errors"
|
||||
"net/http"
|
||||
"os"
|
||||
"strings"
|
||||
|
||||
"github.com/gorcon/rcon"
|
||||
"github.com/grafana/grafana-plugin-sdk-go/backend"
|
||||
"github.com/grafana/grafana-plugin-sdk-go/backend/app"
|
||||
"github.com/grafana/grafana-plugin-sdk-go/backend/instancemgmt"
|
||||
"github.com/grafana/grafana-plugin-sdk-go/backend/resource/httpadapter"
|
||||
)
|
||||
|
||||
const pluginID = "quorate-rcon-app"
|
||||
|
||||
type serverConfig struct {
|
||||
APIURL string `json:"apiUrl"`
|
||||
Address string `json:"address"`
|
||||
PasswordPath string `json:"passwordPath"`
|
||||
}
|
||||
|
||||
type command struct {
|
||||
Command string `json:"command"`
|
||||
}
|
||||
|
||||
type console struct {
|
||||
backend.CallResourceHandler
|
||||
allowed func(*http.Request) (bool, error)
|
||||
execute func(context.Context, string) (string, error)
|
||||
check func(context.Context) error
|
||||
}
|
||||
|
||||
func newConsole(ctx context.Context, _ backend.AppInstanceSettings) (instancemgmt.Instance, error) {
|
||||
data, err := os.ReadFile("/etc/grafana/rcon/config.json")
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
var cfg serverConfig
|
||||
if err := json.Unmarshal(data, &cfg); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if cfg.Address == "" || cfg.PasswordPath == "" || cfg.APIURL == "" {
|
||||
return nil, errors.New("Grafana API URL, RCON address and password file are required")
|
||||
}
|
||||
auth := newAuthorizer(cfg.APIURL)
|
||||
c := &console{allowed: auth.canSubmit, execute: func(ctx context.Context, cmd string) (string, error) {
|
||||
password, err := cfg.password()
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
return execute(ctx, cfg.Address, password, cmd)
|
||||
}, check: func(ctx context.Context) error {
|
||||
password, err := cfg.password()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
return checkRcon(ctx, cfg.Address, password)
|
||||
}}
|
||||
c.CallResourceHandler = httpadapter.New(c.routes())
|
||||
return c, nil
|
||||
}
|
||||
|
||||
func (cfg serverConfig) password() (string, error) {
|
||||
data, err := os.ReadFile(cfg.PasswordPath)
|
||||
return strings.TrimSuffix(string(data), "\n"), err
|
||||
}
|
||||
|
||||
func (c *console) routes() http.Handler {
|
||||
mux := http.NewServeMux()
|
||||
mux.HandleFunc("GET /access", func(w http.ResponseWriter, req *http.Request) {
|
||||
allowed, err := c.allowed(req)
|
||||
if err != nil {
|
||||
http.Error(w, "authorization unavailable", http.StatusServiceUnavailable)
|
||||
return
|
||||
}
|
||||
respond(w, map[string]bool{"submit": allowed})
|
||||
})
|
||||
mux.HandleFunc("POST /command", c.submit)
|
||||
mux.HandleFunc("GET /health", func(w http.ResponseWriter, req *http.Request) {
|
||||
if !c.authorize(w, req) {
|
||||
return
|
||||
}
|
||||
if err := c.check(req.Context()); err != nil {
|
||||
http.Error(w, "RCON connection or authentication failed", http.StatusBadGateway)
|
||||
return
|
||||
}
|
||||
respond(w, map[string]string{"status": "ok"})
|
||||
})
|
||||
return mux
|
||||
}
|
||||
|
||||
func respond(w http.ResponseWriter, data any) {
|
||||
w.Header().Set("Content-Type", "application/json")
|
||||
w.Header().Set("Cache-Control", "no-store")
|
||||
_ = json.NewEncoder(w).Encode(data)
|
||||
}
|
||||
|
||||
func (c *console) authorize(w http.ResponseWriter, req *http.Request) bool {
|
||||
allowed, err := c.allowed(req)
|
||||
if err != nil {
|
||||
http.Error(w, "authorization unavailable", http.StatusServiceUnavailable)
|
||||
return false
|
||||
}
|
||||
if !allowed {
|
||||
http.Error(w, "permission denied", http.StatusForbidden)
|
||||
return false
|
||||
}
|
||||
return true
|
||||
}
|
||||
|
||||
func (c *console) submit(w http.ResponseWriter, req *http.Request) {
|
||||
if !c.authorize(w, req) {
|
||||
return
|
||||
}
|
||||
var input command
|
||||
if err := json.NewDecoder(req.Body).Decode(&input); err != nil {
|
||||
http.Error(w, "invalid command", http.StatusBadRequest)
|
||||
return
|
||||
}
|
||||
if strings.TrimSpace(input.Command) == "" || len(input.Command) > rcon.MaxCommandLen || strings.ContainsAny(input.Command, "\x00\r\n") {
|
||||
http.Error(w, "enter one command of 1–1000 bytes", http.StatusBadRequest)
|
||||
return
|
||||
}
|
||||
output, err := c.execute(req.Context(), input.Command)
|
||||
if err != nil {
|
||||
http.Error(w, "RCON request failed; execution may have occurred. Check server state before resubmitting.", http.StatusBadGateway)
|
||||
return
|
||||
}
|
||||
respond(w, map[string]string{"output": output})
|
||||
}
|
||||
|
||||
func main() {
|
||||
if err := app.Manage(pluginID, newConsole, app.ManageOpts{}); err != nil {
|
||||
os.Exit(1)
|
||||
}
|
||||
}
|
||||
Loading…
Reference in a new issue