quorate/packages/grafana-rcon/backend/main.go

140 lines
3.9 KiB
Go
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

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)
}
}