ce7cb702218693d08ea6d83464d2457c9250027c
This commit is contained in:
commit
34c5f8963c
23 changed files with 145336 additions and 0 deletions
2
packages/grafana-rcon/.gitignore
vendored
Normal file
2
packages/grafana-rcon/.gitignore
vendored
Normal file
|
|
@ -0,0 +1,2 @@
|
|||
/dist/
|
||||
/node_modules/
|
||||
13
packages/grafana-rcon/Makefile
Normal file
13
packages/grafana-rcon/Makefile
Normal file
|
|
@ -0,0 +1,13 @@
|
|||
.PHONY: build test clean
|
||||
|
||||
build:
|
||||
npm run build
|
||||
CGO_ENABLED=0 GOOS=linux GOARCH=amd64 go build -trimpath -o dist/rcon_linux_amd64 ./backend
|
||||
CGO_ENABLED=0 GOOS=linux GOARCH=arm64 go build -trimpath -o dist/rcon_linux_arm64 ./backend
|
||||
|
||||
test:
|
||||
npm run check
|
||||
go test -race ./backend
|
||||
|
||||
clean:
|
||||
rm -rf dist
|
||||
66
packages/grafana-rcon/backend/auth.go
Normal file
66
packages/grafana-rcon/backend/auth.go
Normal file
|
|
@ -0,0 +1,66 @@
|
|||
package main
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"errors"
|
||||
"net/http"
|
||||
"net/url"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"github.com/grafana/authlib/authn"
|
||||
"github.com/grafana/grafana-plugin-sdk-go/config"
|
||||
)
|
||||
|
||||
type authorizer struct {
|
||||
url string
|
||||
client *http.Client
|
||||
verifier authn.Verifier[struct{}]
|
||||
}
|
||||
|
||||
func newAuthorizer(base string) *authorizer {
|
||||
base = strings.TrimRight(base, "/")
|
||||
client := &http.Client{Timeout: 10 * time.Second}
|
||||
keys := authn.NewKeyRetriever(authn.KeyRetrieverConfig{SigningKeysURL: base + "/api/signing-keys/keys"}, authn.WithHTTPClientKeyRetrieverOpt(client))
|
||||
return &authorizer{
|
||||
url: base,
|
||||
client: client,
|
||||
verifier: authn.NewVerifier[struct{}](authn.VerifierConfig{}, authn.TokenTypeID, keys),
|
||||
}
|
||||
}
|
||||
|
||||
func (a *authorizer) canSubmit(req *http.Request) (bool, error) {
|
||||
cfg := config.GrafanaConfigFromContext(req.Context())
|
||||
token, err := cfg.PluginAppClientSecret()
|
||||
if err != nil || token == "" {
|
||||
return false, errors.New("plugin service account is unavailable")
|
||||
}
|
||||
claims, err := a.verifier.Verify(req.Context(), req.Header.Get("X-Grafana-Id"))
|
||||
if err != nil {
|
||||
return false, err
|
||||
}
|
||||
id, ok := strings.CutPrefix(claims.Subject, "user:")
|
||||
if !ok || id == "" {
|
||||
return false, nil
|
||||
}
|
||||
request, err := http.NewRequestWithContext(req.Context(), "GET", a.url+"/api/users/"+url.PathEscape(id), nil)
|
||||
if err != nil {
|
||||
return false, err
|
||||
}
|
||||
request.Header.Set("Authorization", "Bearer "+token)
|
||||
response, err := a.client.Do(request)
|
||||
if err != nil {
|
||||
return false, err
|
||||
}
|
||||
defer response.Body.Close()
|
||||
if response.StatusCode != http.StatusOK {
|
||||
return false, errors.New("user lookup is unavailable")
|
||||
}
|
||||
var profile struct {
|
||||
Admin bool `json:"isGrafanaAdmin"`
|
||||
}
|
||||
if err := json.NewDecoder(response.Body).Decode(&profile); err != nil {
|
||||
return false, err
|
||||
}
|
||||
return profile.Admin, nil
|
||||
}
|
||||
158
packages/grafana-rcon/backend/auth_test.go
Normal file
158
packages/grafana-rcon/backend/auth_test.go
Normal file
|
|
@ -0,0 +1,158 @@
|
|||
package main
|
||||
|
||||
import (
|
||||
"context"
|
||||
"crypto/ecdsa"
|
||||
"crypto/elliptic"
|
||||
"crypto/rand"
|
||||
"encoding/json"
|
||||
"net/http"
|
||||
"net/http/httptest"
|
||||
"strings"
|
||||
"sync/atomic"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"github.com/go-jose/go-jose/v4"
|
||||
"github.com/go-jose/go-jose/v4/jwt"
|
||||
"github.com/grafana/grafana-plugin-sdk-go/config"
|
||||
)
|
||||
|
||||
type authFixture struct {
|
||||
key *ecdsa.PrivateKey
|
||||
server *httptest.Server
|
||||
admin atomic.Bool
|
||||
status atomic.Int32
|
||||
badJSON atomic.Bool
|
||||
keyCalls atomic.Int32
|
||||
userCalls atomic.Int32
|
||||
bearer atomic.Value
|
||||
}
|
||||
|
||||
func newAuthFixture(t *testing.T) *authFixture {
|
||||
t.Helper()
|
||||
key, err := ecdsa.GenerateKey(elliptic.P256(), rand.Reader)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
f := &authFixture{key: key}
|
||||
f.admin.Store(true)
|
||||
f.status.Store(http.StatusOK)
|
||||
f.server = httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, req *http.Request) {
|
||||
switch req.URL.Path {
|
||||
case "/api/signing-keys/keys":
|
||||
f.keyCalls.Add(1)
|
||||
_ = json.NewEncoder(w).Encode(jose.JSONWebKeySet{Keys: []jose.JSONWebKey{{Key: &key.PublicKey, KeyID: "synthetic"}}})
|
||||
case "/api/users/17":
|
||||
f.userCalls.Add(1)
|
||||
f.bearer.Store(req.Header.Get("Authorization"))
|
||||
w.WriteHeader(int(f.status.Load()))
|
||||
if f.badJSON.Load() {
|
||||
_, _ = w.Write([]byte("invalid JSON"))
|
||||
return
|
||||
}
|
||||
_ = json.NewEncoder(w).Encode(map[string]any{"isGrafanaAdmin": f.admin.Load(), "role": "Admin"})
|
||||
default:
|
||||
http.NotFound(w, req)
|
||||
}
|
||||
}))
|
||||
t.Cleanup(f.server.Close)
|
||||
return f
|
||||
}
|
||||
|
||||
func signedID(t *testing.T, key *ecdsa.PrivateKey, subject string, expires time.Time) string {
|
||||
t.Helper()
|
||||
signer, err := jose.NewSigner(jose.SigningKey{Algorithm: jose.ES256, Key: jose.JSONWebKey{Key: key, KeyID: "synthetic"}}, new(jose.SignerOptions).WithType("jwt"))
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
token, err := jwt.Signed(signer).Claims(jwt.Claims{Subject: subject, Expiry: jwt.NewNumericDate(expires)}).Serialize()
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
return token
|
||||
}
|
||||
|
||||
func authRequest(identity, secret string) *http.Request {
|
||||
ctx := config.WithGrafanaConfig(context.Background(), config.NewGrafanaCfg(map[string]string{config.AppClientSecret: secret}))
|
||||
req := httptest.NewRequest("POST", "/command", strings.NewReader(`{"command":"list"}`)).WithContext(ctx)
|
||||
req.Header.Set("X-Grafana-Id", identity)
|
||||
req.Header.Set("X-Grafana-User", "forged-admin")
|
||||
req.Header.Set("X-Grafana-Role", "Admin")
|
||||
return req
|
||||
}
|
||||
|
||||
func TestRoleRevocation(t *testing.T) {
|
||||
f := newAuthFixture(t)
|
||||
auth := newAuthorizer(f.server.URL)
|
||||
identity := signedID(t, f.key, "user:17", time.Now().Add(time.Minute))
|
||||
calls := 0
|
||||
c := &console{allowed: auth.canSubmit, execute: func(context.Context, string) (string, error) { calls++; return "ok", nil }}
|
||||
for _, step := range []struct {
|
||||
admin bool
|
||||
secret string
|
||||
status int
|
||||
}{
|
||||
{true, "synthetic-first", 200},
|
||||
{false, "synthetic-second", 403},
|
||||
{false, "synthetic-second", 403},
|
||||
{true, "synthetic-second", 200},
|
||||
} {
|
||||
f.admin.Store(step.admin)
|
||||
w := httptest.NewRecorder()
|
||||
c.routes().ServeHTTP(w, authRequest(identity, step.secret))
|
||||
if w.Code != step.status {
|
||||
t.Fatalf("status=%d want=%d", w.Code, step.status)
|
||||
}
|
||||
}
|
||||
if calls != 2 || f.keyCalls.Load() != 1 || f.userCalls.Load() != 4 || f.bearer.Load() != "Bearer synthetic-second" {
|
||||
t.Fatalf("commands=%d keys=%d users=%d bearer=%v", calls, f.keyCalls.Load(), f.userCalls.Load(), f.bearer.Load())
|
||||
}
|
||||
}
|
||||
|
||||
func TestIdentityFailures(t *testing.T) {
|
||||
for _, name := range []string{"missing ID", "forged ID", "expired ID", "wrong signature", "service account", "empty user", "missing secret", "user API failure", "invalid profile", "organization admin"} {
|
||||
t.Run(name, func(t *testing.T) {
|
||||
f := newAuthFixture(t)
|
||||
identity := signedID(t, f.key, "user:17", time.Now().Add(time.Minute))
|
||||
secret := "synthetic-service"
|
||||
status := http.StatusServiceUnavailable
|
||||
switch name {
|
||||
case "missing ID":
|
||||
identity = ""
|
||||
case "forged ID":
|
||||
identity = "admin"
|
||||
case "expired ID":
|
||||
identity = signedID(t, f.key, "user:17", time.Now().Add(-time.Hour))
|
||||
case "wrong signature":
|
||||
key, err := ecdsa.GenerateKey(elliptic.P256(), rand.Reader)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
identity = signedID(t, key, "user:17", time.Now().Add(time.Minute))
|
||||
case "service account":
|
||||
identity = signedID(t, f.key, "service-account:17", time.Now().Add(time.Minute))
|
||||
status = http.StatusForbidden
|
||||
case "empty user":
|
||||
identity = signedID(t, f.key, "user:", time.Now().Add(time.Minute))
|
||||
status = http.StatusForbidden
|
||||
case "missing secret":
|
||||
secret = ""
|
||||
case "user API failure":
|
||||
f.status.Store(http.StatusInternalServerError)
|
||||
case "invalid profile":
|
||||
f.badJSON.Store(true)
|
||||
case "organization admin":
|
||||
f.admin.Store(false)
|
||||
status = http.StatusForbidden
|
||||
}
|
||||
calls := 0
|
||||
c := &console{allowed: newAuthorizer(f.server.URL).canSubmit, execute: func(context.Context, string) (string, error) { calls++; return "ok", nil }}
|
||||
w := httptest.NewRecorder()
|
||||
c.routes().ServeHTTP(w, authRequest(identity, secret))
|
||||
if w.Code != status || calls != 0 {
|
||||
t.Fatalf("status=%d want=%d commands=%d", w.Code, status, calls)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
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)
|
||||
}
|
||||
}
|
||||
75
packages/grafana-rcon/backend/main_test.go
Normal file
75
packages/grafana-rcon/backend/main_test.go
Normal file
|
|
@ -0,0 +1,75 @@
|
|||
package main
|
||||
|
||||
import (
|
||||
"context"
|
||||
"errors"
|
||||
"net/http"
|
||||
"net/http/httptest"
|
||||
"strings"
|
||||
"testing"
|
||||
)
|
||||
|
||||
func TestCommandBoundary(t *testing.T) {
|
||||
for _, tc := range []struct {
|
||||
name, method, body string
|
||||
allow bool
|
||||
authErr error
|
||||
status, calls int
|
||||
}{
|
||||
{"viewer", "POST", `{"command":"stop"}`, false, nil, 403, 0},
|
||||
{"auth failure", "POST", `{"command":"stop"}`, false, errors.New("offline"), 503, 0},
|
||||
{"query refresh", "GET", `{"command":"stop"}`, true, nil, 405, 0},
|
||||
{"empty", "POST", `{"command":" "}`, true, nil, 400, 0},
|
||||
{"newline", "POST", `{"command":"list\nstop"}`, true, nil, 400, 0},
|
||||
{"nul", "POST", `{"command":"list\u0000stop"}`, true, nil, 400, 0},
|
||||
{"oversized", "POST", `{"command":"` + strings.Repeat("x", 1001) + `"}`, true, nil, 400, 0},
|
||||
{"admin", "POST", `{"command":"list"}`, true, nil, 200, 1},
|
||||
} {
|
||||
t.Run(tc.name, func(t *testing.T) {
|
||||
calls := 0
|
||||
c := &console{allowed: func(*http.Request) (bool, error) { return tc.allow, tc.authErr }, execute: func(context.Context, string) (string, error) { calls++; return "Nobody online", nil }}
|
||||
w := httptest.NewRecorder()
|
||||
c.routes().ServeHTTP(w, httptest.NewRequest(tc.method, "/command", strings.NewReader(tc.body)))
|
||||
if w.Code != tc.status || calls != tc.calls {
|
||||
t.Fatalf("status=%d calls=%d", w.Code, calls)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestNoRetry(t *testing.T) {
|
||||
calls := 0
|
||||
c := &console{allowed: func(*http.Request) (bool, error) { return true, nil }, execute: func(context.Context, string) (string, error) { calls++; return "", errors.New("lost response") }}
|
||||
w := httptest.NewRecorder()
|
||||
c.routes().ServeHTTP(w, httptest.NewRequest("POST", "/command", strings.NewReader(`{"command":"say once"}`)))
|
||||
if w.Code != 502 || calls != 1 {
|
||||
t.Fatalf("status=%d calls=%d", w.Code, calls)
|
||||
}
|
||||
}
|
||||
|
||||
func TestHealth(t *testing.T) {
|
||||
for _, tc := range []struct {
|
||||
name string
|
||||
allow bool
|
||||
err error
|
||||
status, calls int
|
||||
}{
|
||||
{"viewer", false, nil, 403, 0},
|
||||
{"admin", true, nil, 200, 1},
|
||||
{"unavailable RCON", true, errors.New("offline"), 502, 1},
|
||||
} {
|
||||
t.Run(tc.name, func(t *testing.T) {
|
||||
checks, commands := 0, 0
|
||||
c := &console{
|
||||
allowed: func(*http.Request) (bool, error) { return tc.allow, nil },
|
||||
check: func(context.Context) error { checks++; return tc.err },
|
||||
execute: func(context.Context, string) (string, error) { commands++; return "", nil },
|
||||
}
|
||||
w := httptest.NewRecorder()
|
||||
c.routes().ServeHTTP(w, httptest.NewRequest("GET", "/health", nil))
|
||||
if w.Code != tc.status || checks != tc.calls || commands != 0 {
|
||||
t.Fatalf("status=%d checks=%d commands=%d", w.Code, checks, commands)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
97
packages/grafana-rcon/backend/rcon.go
Normal file
97
packages/grafana-rcon/backend/rcon.go
Normal file
|
|
@ -0,0 +1,97 @@
|
|||
package main
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"net"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"github.com/gorcon/rcon"
|
||||
)
|
||||
|
||||
const (
|
||||
authID = 0
|
||||
cmdID = 1
|
||||
endID = 2
|
||||
)
|
||||
|
||||
func authenticate(conn net.Conn, password string) error {
|
||||
if _, err := rcon.NewPacket(rcon.SERVERDATA_AUTH, authID, password).WriteTo(conn); err != nil {
|
||||
return err
|
||||
}
|
||||
var packet rcon.Packet
|
||||
if _, err := packet.ReadFrom(conn); err != nil {
|
||||
return err
|
||||
}
|
||||
if packet.Type != rcon.SERVERDATA_AUTH_RESPONSE {
|
||||
return rcon.ErrInvalidAuthResponse
|
||||
}
|
||||
if packet.ID == -1 {
|
||||
return rcon.ErrAuthFailed
|
||||
}
|
||||
if packet.ID != authID {
|
||||
return rcon.ErrInvalidPacketID
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func withRcon(ctx context.Context, address, password string, action func(net.Conn) (string, error)) (string, error) {
|
||||
dialer := net.Dialer{Timeout: rcon.DefaultDialTimeout}
|
||||
conn, err := dialer.DialContext(ctx, "tcp", address)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
defer conn.Close()
|
||||
cancel := context.AfterFunc(ctx, func() { _ = conn.Close() })
|
||||
defer cancel()
|
||||
if err := conn.SetDeadline(time.Now().Add(rcon.DefaultDeadline)); err != nil {
|
||||
return "", err
|
||||
}
|
||||
if err := authenticate(conn, password); err != nil {
|
||||
return "", err
|
||||
}
|
||||
if err := conn.SetDeadline(time.Now().Add(rcon.DefaultDeadline)); err != nil {
|
||||
return "", err
|
||||
}
|
||||
return action(conn)
|
||||
}
|
||||
|
||||
func checkRcon(ctx context.Context, address, password string) error {
|
||||
_, err := withRcon(ctx, address, password, func(net.Conn) (string, error) { return "", nil })
|
||||
return err
|
||||
}
|
||||
|
||||
func execute(ctx context.Context, address, password, command string) (string, error) {
|
||||
return withRcon(ctx, address, password, func(conn net.Conn) (string, error) {
|
||||
if _, err := rcon.NewPacket(rcon.SERVERDATA_EXECCOMMAND, cmdID, command).WriteTo(conn); err != nil {
|
||||
return "", err
|
||||
}
|
||||
var first rcon.Packet
|
||||
if _, err := first.ReadFrom(conn); err != nil {
|
||||
return "", err
|
||||
}
|
||||
if first.ID != cmdID || first.Type != rcon.SERVERDATA_RESPONSE_VALUE {
|
||||
return "", fmt.Errorf("unexpected RCON response")
|
||||
}
|
||||
// Wait for the command response before sending the delimiter; Minecraft drops pipelined requests.
|
||||
if _, err := rcon.NewPacket(rcon.SERVERDATA_RESPONSE_VALUE, endID, "").WriteTo(conn); err != nil {
|
||||
return "", err
|
||||
}
|
||||
var output strings.Builder
|
||||
output.WriteString(first.Body())
|
||||
for {
|
||||
var packet rcon.Packet
|
||||
if _, err := packet.ReadFrom(conn); err != nil {
|
||||
return "", err
|
||||
}
|
||||
if packet.ID == endID {
|
||||
return output.String(), nil
|
||||
}
|
||||
if packet.ID != cmdID || packet.Type != rcon.SERVERDATA_RESPONSE_VALUE {
|
||||
return "", fmt.Errorf("unexpected RCON response")
|
||||
}
|
||||
output.WriteString(packet.Body())
|
||||
}
|
||||
})
|
||||
}
|
||||
128
packages/grafana-rcon/backend/rcon_test.go
Normal file
128
packages/grafana-rcon/backend/rcon_test.go
Normal file
|
|
@ -0,0 +1,128 @@
|
|||
package main
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"context"
|
||||
"errors"
|
||||
"net"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"github.com/gorcon/rcon"
|
||||
)
|
||||
|
||||
func TestMultipart(t *testing.T) {
|
||||
listener, err := net.Listen("tcp", "127.0.0.1:0")
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
defer listener.Close()
|
||||
done := make(chan error, 1)
|
||||
go func() {
|
||||
conn, err := listener.Accept()
|
||||
if err != nil {
|
||||
done <- err
|
||||
return
|
||||
}
|
||||
defer conn.Close()
|
||||
var packet rcon.Packet
|
||||
if _, err := packet.ReadFrom(conn); err != nil {
|
||||
done <- err
|
||||
return
|
||||
}
|
||||
if _, err := rcon.NewPacket(2, packet.ID, "").WriteTo(conn); err != nil {
|
||||
done <- err
|
||||
return
|
||||
}
|
||||
if _, err := packet.ReadFrom(conn); err != nil {
|
||||
done <- err
|
||||
return
|
||||
}
|
||||
commandID := packet.ID
|
||||
if _, err := rcon.NewPacket(0, commandID, "first packet\n").WriteTo(conn); err != nil {
|
||||
done <- err
|
||||
return
|
||||
}
|
||||
if _, err := packet.ReadFrom(conn); err != nil {
|
||||
done <- err
|
||||
return
|
||||
}
|
||||
markerID := packet.ID
|
||||
for _, response := range []*rcon.Packet{
|
||||
rcon.NewPacket(0, commandID, "second packet"),
|
||||
rcon.NewPacket(0, markerID, "Unknown request 0"),
|
||||
} {
|
||||
if _, err := response.WriteTo(conn); err != nil {
|
||||
done <- err
|
||||
return
|
||||
}
|
||||
}
|
||||
done <- nil
|
||||
}()
|
||||
output, err := execute(context.Background(), listener.Addr().String(), "synthetic", "help")
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if output != "first packet\nsecond packet" {
|
||||
t.Fatalf("output=%q", output)
|
||||
}
|
||||
if err := <-done; err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
}
|
||||
|
||||
func TestSplitAuth(t *testing.T) {
|
||||
client, server := net.Pipe()
|
||||
defer client.Close()
|
||||
defer server.Close()
|
||||
_ = client.SetDeadline(time.Now().Add(time.Second))
|
||||
done := make(chan error, 1)
|
||||
go func() {
|
||||
defer server.Close()
|
||||
var request rcon.Packet
|
||||
if _, err := request.ReadFrom(server); err != nil {
|
||||
done <- err
|
||||
return
|
||||
}
|
||||
var reply bytes.Buffer
|
||||
_, _ = rcon.NewPacket(rcon.SERVERDATA_AUTH_RESPONSE, request.ID, "").WriteTo(&reply)
|
||||
packet := reply.Bytes()
|
||||
for _, part := range [][]byte{packet[:len(packet)-1], packet[len(packet)-1:]} {
|
||||
if _, err := server.Write(part); err != nil {
|
||||
done <- err
|
||||
return
|
||||
}
|
||||
}
|
||||
_, err := rcon.NewPacket(rcon.SERVERDATA_RESPONSE_VALUE, cmdID, "ok").WriteTo(server)
|
||||
done <- err
|
||||
}()
|
||||
if err := authenticate(client, "synthetic-password"); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
var response rcon.Packet
|
||||
if _, err := response.ReadFrom(client); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if response.ID != cmdID || response.Body() != "ok" {
|
||||
t.Fatalf("id=%d body=%q", response.ID, response.Body())
|
||||
}
|
||||
if err := <-done; err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
}
|
||||
|
||||
func TestAuthFailure(t *testing.T) {
|
||||
client, server := net.Pipe()
|
||||
defer client.Close()
|
||||
defer server.Close()
|
||||
_ = client.SetDeadline(time.Now().Add(time.Second))
|
||||
go func() {
|
||||
defer server.Close()
|
||||
var request rcon.Packet
|
||||
_, _ = request.ReadFrom(server)
|
||||
_, _ = rcon.NewPacket(rcon.SERVERDATA_AUTH_RESPONSE, -1, "").WriteTo(server)
|
||||
}()
|
||||
if err := authenticate(client, "wrong-password"); !errors.Is(err, rcon.ErrAuthFailed) {
|
||||
t.Fatalf("error=%v", err)
|
||||
}
|
||||
}
|
||||
9
packages/grafana-rcon/build.mjs
Normal file
9
packages/grafana-rcon/build.mjs
Normal file
|
|
@ -0,0 +1,9 @@
|
|||
import { mkdir, cp, copyFile } from 'node:fs/promises';
|
||||
await mkdir('dist', { recursive: true });
|
||||
await copyFile('plugin.json', 'dist/plugin.json');
|
||||
for (const name of ['panel', 'datasource']) {
|
||||
await mkdir(`dist/${name}`, { recursive: true });
|
||||
await copyFile(`${name}.json`, `dist/${name}/plugin.json`);
|
||||
await cp('img', `dist/${name}/img`, { recursive: true });
|
||||
}
|
||||
await cp('img', 'dist/img', { recursive: true });
|
||||
23
packages/grafana-rcon/datasource.json
Normal file
23
packages/grafana-rcon/datasource.json
Normal file
|
|
@ -0,0 +1,23 @@
|
|||
{
|
||||
"type": "datasource",
|
||||
"name": "Minecraft Console",
|
||||
"id": "quorate-datasource",
|
||||
"info": {
|
||||
"author": {
|
||||
"name": "Quorate"
|
||||
},
|
||||
"description": "Minecraft remote console",
|
||||
"logos": {
|
||||
"small": "img/logo.svg",
|
||||
"large": "img/logo.svg"
|
||||
},
|
||||
"version": "0.1.0",
|
||||
"updated": "2026-10-06"
|
||||
},
|
||||
"dependencies": {
|
||||
"grafanaDependency": ">=12.4.1",
|
||||
"plugins": []
|
||||
},
|
||||
"metrics": true,
|
||||
"annotations": false
|
||||
}
|
||||
84
packages/grafana-rcon/go.mod
Normal file
84
packages/grafana-rcon/go.mod
Normal file
|
|
@ -0,0 +1,84 @@
|
|||
module quorate.dev/grafana-rcon
|
||||
|
||||
go 1.26.5
|
||||
|
||||
require (
|
||||
github.com/go-jose/go-jose/v4 v4.1.5
|
||||
github.com/gorcon/rcon v1.4.0
|
||||
github.com/grafana/authlib v0.0.0-20260930204439-665ccbf27cbe
|
||||
github.com/grafana/grafana-plugin-sdk-go v0.296.5
|
||||
)
|
||||
|
||||
require (
|
||||
github.com/apache/arrow-go/v18 v18.8.0 // indirect
|
||||
github.com/beorn7/perks v1.0.1 // indirect
|
||||
github.com/cenkalti/backoff/v5 v5.0.3 // indirect
|
||||
github.com/cespare/xxhash/v2 v2.3.0 // indirect
|
||||
github.com/cheekybits/genny v1.0.0 // indirect
|
||||
github.com/clipperhouse/displaywidth v0.11.0 // indirect
|
||||
github.com/clipperhouse/uax29/v2 v2.7.0 // indirect
|
||||
github.com/fatih/color v1.19.0 // indirect
|
||||
github.com/go-logr/logr v1.4.4 // indirect
|
||||
github.com/go-logr/stdr v1.2.2 // indirect
|
||||
github.com/goccy/go-json v0.10.6 // indirect
|
||||
github.com/gogo/googleapis v1.4.1 // indirect
|
||||
github.com/gogo/protobuf v1.3.2 // indirect
|
||||
github.com/golang/protobuf v1.5.4 // indirect
|
||||
github.com/google/flatbuffers v25.12.19+incompatible // indirect
|
||||
github.com/google/go-cmp v0.7.0 // indirect
|
||||
github.com/google/uuid v1.6.0 // indirect
|
||||
github.com/grafana/authlib/types v0.0.0-20260930204439-665ccbf27cbe // indirect
|
||||
github.com/grafana/dskit v0.0.0-20260108123158-1a1acfb6ef2e // indirect
|
||||
github.com/grafana/otel-profiling-go v0.6.0 // indirect
|
||||
github.com/grafana/pyroscope-go/godeltaprof v0.1.12 // indirect
|
||||
github.com/grpc-ecosystem/go-grpc-middleware/providers/prometheus v1.1.0 // indirect
|
||||
github.com/grpc-ecosystem/go-grpc-middleware/v2 v2.3.4 // indirect
|
||||
github.com/grpc-ecosystem/grpc-gateway/v2 v2.30.0 // indirect
|
||||
github.com/hashicorp/go-hclog v1.6.3 // indirect
|
||||
github.com/hashicorp/go-plugin v1.8.0 // indirect
|
||||
github.com/hashicorp/yamux v0.1.2 // indirect
|
||||
github.com/jaegertracing/jaeger-idl v0.12.0 // indirect
|
||||
github.com/json-iterator/go v1.1.12 // indirect
|
||||
github.com/klauspost/compress v1.20.0 // indirect
|
||||
github.com/klauspost/cpuid/v2 v2.4.0 // indirect
|
||||
github.com/mattetti/filebuffer v1.0.1 // indirect
|
||||
github.com/mattn/go-colorable v0.1.15 // indirect
|
||||
github.com/mattn/go-isatty v0.0.24 // indirect
|
||||
github.com/mattn/go-runewidth v0.0.30 // indirect
|
||||
github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd // indirect
|
||||
github.com/modern-go/reflect2 v1.0.2 // indirect
|
||||
github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822 // indirect
|
||||
github.com/oklog/run v1.2.0 // indirect
|
||||
github.com/olekukonko/cat v0.0.0-20250911104152-50322a0618f6 // indirect
|
||||
github.com/olekukonko/errors v1.3.0 // indirect
|
||||
github.com/olekukonko/ll v0.1.8 // indirect
|
||||
github.com/olekukonko/tablewriter v1.1.4 // indirect
|
||||
github.com/patrickmn/go-cache v2.1.0+incompatible // indirect
|
||||
github.com/pierrec/lz4/v4 v4.1.29 // indirect
|
||||
github.com/prometheus/client_golang v1.24.1 // indirect
|
||||
github.com/prometheus/client_model v0.6.3 // indirect
|
||||
github.com/prometheus/common v0.71.0 // indirect
|
||||
github.com/prometheus/procfs v0.22.0 // indirect
|
||||
github.com/zeebo/xxh3 v1.1.0 // indirect
|
||||
go.opentelemetry.io/auto/sdk v1.2.1 // indirect
|
||||
go.opentelemetry.io/contrib/instrumentation/google.golang.org/grpc/otelgrpc v0.71.0 // indirect
|
||||
go.opentelemetry.io/contrib/instrumentation/net/http/httptrace/otelhttptrace v0.71.0 // indirect
|
||||
go.opentelemetry.io/contrib/propagators/jaeger v1.46.0 // indirect
|
||||
go.opentelemetry.io/contrib/samplers/jaegerremote v0.37.3 // indirect
|
||||
go.opentelemetry.io/otel v1.46.0 // indirect
|
||||
go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.46.0 // indirect
|
||||
go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracegrpc v1.46.0 // indirect
|
||||
go.opentelemetry.io/otel/metric v1.46.0 // indirect
|
||||
go.opentelemetry.io/otel/sdk v1.46.0 // indirect
|
||||
go.opentelemetry.io/otel/trace v1.46.0 // indirect
|
||||
go.opentelemetry.io/proto/otlp v1.11.0 // indirect
|
||||
golang.org/x/exp v0.0.0-20260908205506-85c1c2202aba // indirect
|
||||
golang.org/x/net v0.59.0 // indirect
|
||||
golang.org/x/sync v0.23.0 // indirect
|
||||
golang.org/x/sys v0.48.0 // indirect
|
||||
golang.org/x/text v0.42.0 // indirect
|
||||
google.golang.org/genproto/googleapis/api v0.0.0-20260911204522-f61a6ca850bd // indirect
|
||||
google.golang.org/genproto/googleapis/rpc v0.0.0-20260911204522-f61a6ca850bd // indirect
|
||||
google.golang.org/grpc v1.84.0 // indirect
|
||||
google.golang.org/protobuf v1.36.12 // indirect
|
||||
)
|
||||
248
packages/grafana-rcon/go.sum
Normal file
248
packages/grafana-rcon/go.sum
Normal file
|
|
@ -0,0 +1,248 @@
|
|||
github.com/andybalholm/brotli v1.2.3 h1:8H1qwOkl2LPfjf3YezB90JnCliZb6SInJ/OJkEbA5NQ=
|
||||
github.com/andybalholm/brotli v1.2.3/go.mod h1:rzTDkvFWvIrjDXZHkuS16NPggd91W3kUSvPlQ1pLaKY=
|
||||
github.com/apache/arrow-go/v18 v18.8.0 h1:BLOzbPv7bxMPgXPacAg6HQjnxupYsZzC4tf+FkqPU/M=
|
||||
github.com/apache/arrow-go/v18 v18.8.0/go.mod h1:uJCFfCwq0KsxCmsCfQg4ft+LsW+iHYzAXiSDh5ug/8U=
|
||||
github.com/apache/thrift v0.24.0 h1:zy31L1a49QTNB2bG1BBfMXol3yJrTH975G3pPubQVLQ=
|
||||
github.com/apache/thrift v0.24.0/go.mod h1:zPt6WxgvTOM6hF92y8C+MkEM5LMxZuk4JcQOiU4Esvs=
|
||||
github.com/beorn7/perks v1.0.1 h1:VlbKKnNfV8bJzeqoa4cOKqO6bYr3WgKZxO8Z16+hsOM=
|
||||
github.com/beorn7/perks v1.0.1/go.mod h1:G2ZrVWU2WbWT9wwq4/hrbKbnv/1ERSJQ0ibhJ6rlkpw=
|
||||
github.com/bufbuild/protocompile v0.14.1 h1:iA73zAf/fyljNjQKwYzUHD6AD4R8KMasmwa/FBatYVw=
|
||||
github.com/bufbuild/protocompile v0.14.1/go.mod h1:ppVdAIhbr2H8asPk6k4pY7t9zB1OU5DoEw9xY/FUi1c=
|
||||
github.com/cenkalti/backoff/v5 v5.0.3 h1:ZN+IMa753KfX5hd8vVaMixjnqRZ3y8CuJKRKj1xcsSM=
|
||||
github.com/cenkalti/backoff/v5 v5.0.3/go.mod h1:rkhZdG3JZukswDf7f0cwqPNk4K0sa+F97BxZthm/crw=
|
||||
github.com/cespare/xxhash/v2 v2.3.0 h1:UL815xU9SqsFlibzuggzjXhog7bL6oX9BbNZnL2UFvs=
|
||||
github.com/cespare/xxhash/v2 v2.3.0/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs=
|
||||
github.com/cheekybits/genny v1.0.0 h1:uGGa4nei+j20rOSeDeP5Of12XVm7TGUd4dJA9RDitfE=
|
||||
github.com/cheekybits/genny v1.0.0/go.mod h1:+tQajlRqAUrPI7DOSpB0XAqZYtQakVtB7wXkRAgjxjQ=
|
||||
github.com/chromedp/cdproto v0.0.0-20260912003405-686a5c723acc h1:DGGf6Tq9Qz1g0kbDvYCzoqRUpNRw8bicEHH+USB7ROE=
|
||||
github.com/chromedp/cdproto v0.0.0-20260912003405-686a5c723acc/go.mod h1:3v4FIp5njIUyPDvqXsxEOxnB34lijG0up98/5kM1KaE=
|
||||
github.com/clipperhouse/displaywidth v0.11.0 h1:lBc6kY44VFw+TDx4I8opi/EtL9m20WSEFgwIwO+UVM8=
|
||||
github.com/clipperhouse/displaywidth v0.11.0/go.mod h1:bkrFNkf81G8HyVqmKGxsPufD3JhNl3dSqnGhOoSD/o0=
|
||||
github.com/clipperhouse/uax29/v2 v2.7.0 h1:+gs4oBZ2gPfVrKPthwbMzWZDaAFPGYK72F0NJv2v7Vk=
|
||||
github.com/clipperhouse/uax29/v2 v2.7.0/go.mod h1:EFJ2TJMRUaplDxHKj1qAEhCtQPW2tJSwu5BF98AuoVM=
|
||||
github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
|
||||
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
|
||||
github.com/elazarl/goproxy v1.9.1 h1:1gQuANpCxN2UQKWwkNPpWqAk1LnAMYuBxqfIdU8mMoE=
|
||||
github.com/elazarl/goproxy v1.9.1/go.mod h1:THdE5ix2clxX9lZzcICPpZ67d6CdrPZxdOYsNgU5e30=
|
||||
github.com/fatih/color v1.13.0/go.mod h1:kLAiJbzzSOZDVNGyDpeOxJ47H46qBXwg5ILebYFFOfk=
|
||||
github.com/fatih/color v1.19.0 h1:Zp3PiM21/9Ld6FzSKyL5c/BULoe/ONr9KlbYVOfG8+w=
|
||||
github.com/fatih/color v1.19.0/go.mod h1:zNk67I0ZUT1bEGsSGyCZYZNrHuTkJJB+r6Q9VuMi0LE=
|
||||
github.com/felixge/httpsnoop v1.1.0 h1:3YtUj32ZZkqZtt3sZZsClsymw/QDuVfpNhoA31zeORc=
|
||||
github.com/felixge/httpsnoop v1.1.0/go.mod h1:Zqxgdd+1Rkcz8euOqdr7lqgCRJztwr5hp9vDSi5UZCE=
|
||||
github.com/getkin/kin-openapi v0.149.0 h1:ZbhmVJ4yq5RZDUsyP8lcBcGMsjsaTqXEFt6isdtMDfA=
|
||||
github.com/getkin/kin-openapi v0.149.0/go.mod h1:1+BHDzstro+P5CKtPy1X4PfofnFgmRe6uvMy9+r9fKY=
|
||||
github.com/go-jose/go-jose/v4 v4.1.5 h1:RjgjO2LOtWOJKUC5wpwY9LR3B3vwVAz6JS2YHfYU6eA=
|
||||
github.com/go-jose/go-jose/v4 v4.1.5/go.mod h1:x4oUasVrzR7071A4TnHLGSPpNOm2a21K9Kf04k1rs08=
|
||||
github.com/go-logr/logr v1.2.2/go.mod h1:jdQByPbusPIv2/zmleS9BjJVeZ6kBagPoEUsqbVz/1A=
|
||||
github.com/go-logr/logr v1.4.4 h1:tG4xh9yMsRCAiodLVTxyrkzSZ9+o0L1Kg/+cPVcbP/8=
|
||||
github.com/go-logr/logr v1.4.4/go.mod h1:9T104GzyrTigFIr8wt5mBrctHMim0Nb2HLGrmQ40KvY=
|
||||
github.com/go-logr/stdr v1.2.2 h1:hSWxHoqTgW2S2qGc0LTAI563KZ5YKYRhT3MFKZMbjag=
|
||||
github.com/go-logr/stdr v1.2.2/go.mod h1:mMo/vtBO5dYbehREoey6XUKy/eSumjCCveDpRre4VKE=
|
||||
github.com/go-openapi/jsonpointer v1.0.1 h1:2KxywRmNwJkT/FMBa3iRNHEaAxSJvjqoufQZy3au1Mg=
|
||||
github.com/go-openapi/jsonpointer v1.0.1/go.mod h1:wI7ZYsFmbIi9nBXOZqgDaS/bqOchRGZjqxFli7FBYxY=
|
||||
github.com/goccy/go-json v0.10.6 h1:p8HrPJzOakx/mn/bQtjgNjdTcN+/S6FcG2CTtQOrHVU=
|
||||
github.com/goccy/go-json v0.10.6/go.mod h1:oq7eo15ShAhp70Anwd5lgX2pLfOS3QCiwU/PULtXL6M=
|
||||
github.com/gogo/googleapis v1.4.1 h1:1Yx4Myt7BxzvUr5ldGSbwYiZG6t9wGBZ+8/fX3Wvtq0=
|
||||
github.com/gogo/googleapis v1.4.1/go.mod h1:2lpHqI5OcWCtVElxXnPt+s8oJvMpySlOyM6xDCrzib4=
|
||||
github.com/gogo/protobuf v1.3.2 h1:Ov1cvc58UF3b5XjBnZv7+opcTcQFZebYjWzi34vdm4Q=
|
||||
github.com/gogo/protobuf v1.3.2/go.mod h1:P1XiOD3dCwIKUDQYPy72D8LYyHL2YPYrpS2s69NZV8Q=
|
||||
github.com/golang/protobuf v1.5.4 h1:i7eJL8qZTpSEXOPTxNKhASYpMn+8e5Q6AdndVa1dWek=
|
||||
github.com/golang/protobuf v1.5.4/go.mod h1:lnTiLA8Wa4RWRcIUkrtSVa5nRhsEGBg48fD6rSs7xps=
|
||||
github.com/google/flatbuffers v25.12.19+incompatible h1:haMV2JRRJCe1998HeW/p0X9UaMTK6SDo0ffLn2+DbLs=
|
||||
github.com/google/flatbuffers v25.12.19+incompatible/go.mod h1:1AeVuKshWv4vARoZatz6mlQ0JxURH0Kv5+zNeJKJCa8=
|
||||
github.com/google/go-cmp v0.7.0 h1:wk8382ETsv4JYUZwIsn6YpYiWiBsYLSJiTsyBybVuN8=
|
||||
github.com/google/go-cmp v0.7.0/go.mod h1:pXiqmnSA92OHEEa9HXL2W4E7lf9JzCmGVUdgjX3N/iU=
|
||||
github.com/google/gofuzz v1.0.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg=
|
||||
github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0=
|
||||
github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
|
||||
github.com/gorcon/rcon v1.4.0 h1:pYwZ8Rhcgfh/LhdPBncecuEo5thoFvPIuMSWovz1FME=
|
||||
github.com/gorcon/rcon v1.4.0/go.mod h1:M6v6sNmr/NET9YIf+2rq+cIjTBridoy62uzQ58WgC1I=
|
||||
github.com/gorilla/mux v1.8.1 h1:TuBL49tXwgrFYWhqrNgrUNEY92u81SPhu7sTdzQEiWY=
|
||||
github.com/gorilla/mux v1.8.1/go.mod h1:AKf9I4AEqPTmMytcMc0KkNouC66V3BtZ4qD5fmWSiMQ=
|
||||
github.com/grafana/authlib v0.0.0-20260930204439-665ccbf27cbe h1:R0tVnQLt1m4Qn0F2tw7G6JUGVTZXg8x0ZcYv2zs5PXs=
|
||||
github.com/grafana/authlib v0.0.0-20260930204439-665ccbf27cbe/go.mod h1:1y/QFuWzHw/S70DfBTA9odWcmqd/r/YuCUylNXohIrI=
|
||||
github.com/grafana/authlib/types v0.0.0-20260930204439-665ccbf27cbe h1:rZ4YTid1IkjrwEMT5zilAfZrt8wzM7k1C4iiFaOEOWc=
|
||||
github.com/grafana/authlib/types v0.0.0-20260930204439-665ccbf27cbe/go.mod h1:/nf5Y9wbsrPAOGKScnfFYO9Bdb7eLsP/VPEuJUsDzzM=
|
||||
github.com/grafana/dskit v0.0.0-20260108123158-1a1acfb6ef2e h1:zP30ZbtnWjy0KJxToqCLARauAtgFD+TAEB/sk4IBb9s=
|
||||
github.com/grafana/dskit v0.0.0-20260108123158-1a1acfb6ef2e/go.mod h1:Tl9i4F2CPp3cOiXo06JObTh05tENG/NV/ombn2XOY7Y=
|
||||
github.com/grafana/grafana-plugin-sdk-go v0.296.5 h1:86zOFJV6mSSYMPvcug6X4TlrrPm77q+PQH2VYGVV5ME=
|
||||
github.com/grafana/grafana-plugin-sdk-go v0.296.5/go.mod h1:E2WBpInzUESFrzNow91khsHYJPVEKTV0O05XLDMMKus=
|
||||
github.com/grafana/otel-profiling-go v0.6.0 h1:W7lOZaJj4IJISXMcM1UBk3fJF3tzF2OD6MJBJaQp1H8=
|
||||
github.com/grafana/otel-profiling-go v0.6.0/go.mod h1:cqLIDgNXlnzknJ0WLiEe+JPjZk2MZ4ftMdqRJRWj1ZM=
|
||||
github.com/grafana/pyroscope-go/godeltaprof v0.1.12 h1:X6OemT2WcLtxdmNukEQuIp0c+efWVI/tBTd0oeWeDHI=
|
||||
github.com/grafana/pyroscope-go/godeltaprof v0.1.12/go.mod h1:aNSXN1bn1VHAd06EiepmwhAabHsMc67gx8itecdF2c8=
|
||||
github.com/grpc-ecosystem/go-grpc-middleware/providers/prometheus v1.1.0 h1:QGLs/O40yoNK9vmy4rhUGBVyMf1lISBGtXRpsu/Qu/o=
|
||||
github.com/grpc-ecosystem/go-grpc-middleware/providers/prometheus v1.1.0/go.mod h1:hM2alZsMUni80N33RBe6J0e423LB+odMj7d3EMP9l20=
|
||||
github.com/grpc-ecosystem/go-grpc-middleware/v2 v2.3.4 h1:9ZJYjPEJpcleIKcqysXOo14NLQYwu23fzmKRFpIjPjc=
|
||||
github.com/grpc-ecosystem/go-grpc-middleware/v2 v2.3.4/go.mod h1:0xydIeg2omQ9DH6zYMP24Kk57793rnLUvP8fwdbFvz8=
|
||||
github.com/grpc-ecosystem/grpc-gateway/v2 v2.30.0 h1:/Tnpcb2E0Pz/tN9s3bfEY2Q8ePCEX9iuS+cneUwncnw=
|
||||
github.com/grpc-ecosystem/grpc-gateway/v2 v2.30.0/go.mod h1:zOBXOsUaBSjKgmH4OGzV1esUpR3oUSCPYVd2cUBjKYY=
|
||||
github.com/hashicorp/go-hclog v1.6.3 h1:Qr2kF+eVWjTiYmU7Y31tYlP1h0q/X3Nl3tPGdaB11/k=
|
||||
github.com/hashicorp/go-hclog v1.6.3/go.mod h1:W4Qnvbt70Wk/zYJryRzDRU/4r0kIg0PVHBcfoyhpF5M=
|
||||
github.com/hashicorp/go-plugin v1.8.0 h1:ie8S6RRY8RvB2usYZv+AAZ/wBvx2AU5p5QeP5j/FORs=
|
||||
github.com/hashicorp/go-plugin v1.8.0/go.mod h1:BExt6KEaIYx804z8k4gRzRLEvxKVb+kn0NMcihqOqb8=
|
||||
github.com/hashicorp/yamux v0.1.2 h1:XtB8kyFOyHXYVFnwT5C3+Bdo8gArse7j2AQ0DA0Uey8=
|
||||
github.com/hashicorp/yamux v0.1.2/go.mod h1:C+zze2n6e/7wshOZep2A70/aQU6QBRWJO/G6FT1wIns=
|
||||
github.com/jaegertracing/jaeger-idl v0.12.0 h1:FDk3ezIqKk7n9/gzxG9NNjDWzZZLh0GiCxhF4H1LRNU=
|
||||
github.com/jaegertracing/jaeger-idl v0.12.0/go.mod h1:wWzFftH47XtPRkOM25NPNZ7zBhREWB5HtZBsWj25eW0=
|
||||
github.com/jhump/protoreflect v1.17.0 h1:qOEr613fac2lOuTgWN4tPAtLL7fUSbuJL5X5XumQh94=
|
||||
github.com/jhump/protoreflect v1.17.0/go.mod h1:h9+vUUL38jiBzck8ck+6G/aeMX8Z4QUY/NiJPwPNi+8=
|
||||
github.com/json-iterator/go v1.1.12 h1:PV8peI4a0ysnczrg+LtxykD8LfKY9ML6u2jnxaEnrnM=
|
||||
github.com/json-iterator/go v1.1.12/go.mod h1:e30LSqwooZae/UwlEbR2852Gd8hjQvJoHmT4TnhNGBo=
|
||||
github.com/kisielk/errcheck v1.5.0/go.mod h1:pFxgyoBC7bSaBwPgfKdkLd5X25qrDl4LWUI2bnpBCr8=
|
||||
github.com/kisielk/gotool v1.0.0/go.mod h1:XhKaO+MFFWcvkIS/tQcRk01m1F5IRFswLeQ+oQHNcck=
|
||||
github.com/klauspost/compress v1.20.0 h1:a3C1ke2ohxFymNlb2HWAHjDeKCI90scRskErZkR0ezA=
|
||||
github.com/klauspost/compress v1.20.0/go.mod h1:LUdAzn7YLVvxLpc7y3V1m40wESHTgc1422pwwBSKYuI=
|
||||
github.com/klauspost/cpuid/v2 v2.4.0 h1:S6Hrbc7+ywsr0r+RLapfGBHfyefhCTwEh3A0tV913Dw=
|
||||
github.com/klauspost/cpuid/v2 v2.4.0/go.mod h1:19jmZ9mjzoF//ddRSUsv0zfBTJWh3QJh9FNxZTMrGxU=
|
||||
github.com/kylelemons/godebug v1.1.0 h1:RPNrshWIDI6G2gRW9EHilWtl7Z6Sb1BR0xunSBf0SNc=
|
||||
github.com/kylelemons/godebug v1.1.0/go.mod h1:9/0rRGxNHcop5bhtWyNeEfOS8JIWk580+fNqagV/RAw=
|
||||
github.com/mattetti/filebuffer v1.0.1 h1:gG7pyfnSIZCxdoKq+cPa8T0hhYtD9NxCdI4D7PTjRLM=
|
||||
github.com/mattetti/filebuffer v1.0.1/go.mod h1:YdMURNDOttIiruleeVr6f56OrMc+MydEnTcXwtkxNVs=
|
||||
github.com/mattn/go-colorable v0.1.9/go.mod h1:u6P/XSegPjTcexA+o6vUJrdnUu04hMope9wVRipJSqc=
|
||||
github.com/mattn/go-colorable v0.1.12/go.mod h1:u5H1YNBxpqRaxsYJYSkiCWKzEfiAb1Gb520KVy5xxl4=
|
||||
github.com/mattn/go-colorable v0.1.15 h1:+u9SLTRGnXv73cEsnsmoZBom+dMU88B2M0aDcWy0/jY=
|
||||
github.com/mattn/go-colorable v0.1.15/go.mod h1:6LmQG8QLFO4G5z1gPvYEzlUgJ2wF+stgPZH1UqBm1s8=
|
||||
github.com/mattn/go-isatty v0.0.12/go.mod h1:cbi8OIDigv2wuxKPP5vlRcQ1OAZbq2CE4Kysco4FUpU=
|
||||
github.com/mattn/go-isatty v0.0.14/go.mod h1:7GGIvUiUoEMVVmxf/4nioHXj79iQHKdU27kJ6hsGG94=
|
||||
github.com/mattn/go-isatty v0.0.24 h1:tGZZoVgT/KiqK1c8ocVLeDS8BSWMRd47J3Lbz7vsReI=
|
||||
github.com/mattn/go-isatty v0.0.24/go.mod h1:nMCL3Zebbrt45jsMDgnfIwz6ydEQApk5oEI3HqDio6A=
|
||||
github.com/mattn/go-runewidth v0.0.30 h1:+KUuiDA4fF0R1p5FeueHefjDm+GIM+kWfFnDjybOPgk=
|
||||
github.com/mattn/go-runewidth v0.0.30/go.mod h1:3qAiGCV4Koz/yuveO58qUefmUTRm8r0IGEXZ9jeHp/8=
|
||||
github.com/mitchellh/reflectwalk v1.0.2 h1:G2LzWKi524PWgd3mLHV8Y5k7s6XUvT0Gef6zxSIeXaQ=
|
||||
github.com/mitchellh/reflectwalk v1.0.2/go.mod h1:mSTlrgnPZtwu0c4WaC2kGObEpuNDbx0jmZXqmk4esnw=
|
||||
github.com/modern-go/concurrent v0.0.0-20180228061459-e0a39a4cb421/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q=
|
||||
github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd h1:TRLaZ9cD/w8PVh93nsPXa1VrQ6jlwL5oN8l14QlcNfg=
|
||||
github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q=
|
||||
github.com/modern-go/reflect2 v1.0.2 h1:xBagoLtFs94CBntxluKeaWgTMpvLxC4ur3nMaC9Gz0M=
|
||||
github.com/modern-go/reflect2 v1.0.2/go.mod h1:yWuevngMOJpCy52FWWMvUC8ws7m/LJsjYzDa0/r8luk=
|
||||
github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822 h1:C3w9PqII01/Oq1c1nUAm88MOHcQC9l5mIlSMApZMrHA=
|
||||
github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822/go.mod h1:+n7T8mK8HuQTcFwEeznm/DIxMOiR9yIdICNftLE1DvQ=
|
||||
github.com/oasdiff/yaml v0.1.1 h1:6nHx+pn9gBRM6YpBlFZFQGCCd1nuvqOBtTD3KKTgGxY=
|
||||
github.com/oasdiff/yaml v0.1.1/go.mod h1:EYJNoyktvWMJ0Hmhx+6qTaqMOsalUaRGT8Sj1hNcegU=
|
||||
github.com/oasdiff/yaml3 v0.0.14 h1:aLJee3hxBK2H5wdXd9iPcIXb93Nty1Ge0pT171eHtkw=
|
||||
github.com/oasdiff/yaml3 v0.0.14/go.mod h1:csto2xfDjYccdUn/yw/bPjj/cYTdp6HtFA0J4TWG+gg=
|
||||
github.com/oklog/run v1.2.0 h1:O8x3yXwah4A73hJdlrwo/2X6J62gE5qTMusH0dvz60E=
|
||||
github.com/oklog/run v1.2.0/go.mod h1:mgDbKRSwPhJfesJ4PntqFUbKQRZ50NgmZTSPlFA0YFk=
|
||||
github.com/olekukonko/cat v0.0.0-20250911104152-50322a0618f6 h1:zrbMGy9YXpIeTnGj4EljqMiZsIcE09mmF8XsD5AYOJc=
|
||||
github.com/olekukonko/cat v0.0.0-20250911104152-50322a0618f6/go.mod h1:rEKTHC9roVVicUIfZK7DYrdIoM0EOr8mK1Hj5s3JjH0=
|
||||
github.com/olekukonko/errors v1.3.0 h1:teJvgLGUEqMzBUms+Dj3/3szNqCG/Jdw9iDbum8fR6U=
|
||||
github.com/olekukonko/errors v1.3.0/go.mod h1:ppzxA5jBKcO1vIpCXQ9ZqgDh8iwODz6OXIGKU8r5m4Y=
|
||||
github.com/olekukonko/ll v0.1.8 h1:ysHCJRGHYKzmBSdz9w5AySztx7lG8SQY+naTGYUbsz8=
|
||||
github.com/olekukonko/ll v0.1.8/go.mod h1:RPRC6UcscfFZgjo1nulkfMH5IM0QAYim0LfnMvUuozw=
|
||||
github.com/olekukonko/tablewriter v1.1.4 h1:ORUMI3dXbMnRlRggJX3+q7OzQFDdvgbN9nVWj1drm6I=
|
||||
github.com/olekukonko/tablewriter v1.1.4/go.mod h1:+kedxuyTtgoZLwif3P1Em4hARJs+mVnzKxmsCL/C5RY=
|
||||
github.com/patrickmn/go-cache v2.1.0+incompatible h1:HRMgzkcYKYpi3C8ajMPV8OFXaaRUnok+kx1WdO15EQc=
|
||||
github.com/patrickmn/go-cache v2.1.0+incompatible/go.mod h1:3Qf8kWWT7OJRJbdiICTKqZju1ZixQ/KpMGzzAfe6+WQ=
|
||||
github.com/pierrec/lz4/v4 v4.1.29 h1:CDQY6qZOLI4DW0Nx6R1vRrifrCeQHnNXkMb0hZWXFjg=
|
||||
github.com/pierrec/lz4/v4 v4.1.29/go.mod h1:EoQMVJgeeEOMsCqCzqFm2O0cJvljX2nGZjcRIPL34O4=
|
||||
github.com/pkg/errors v0.9.1 h1:FEBLx1zS214owpjy7qsBeixbURkuhQAwrK5UwLGTwt4=
|
||||
github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0=
|
||||
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
|
||||
github.com/prometheus/client_golang v1.24.1 h1:JnJkREXzWxUdCuPFpIWZiPispT9xVV59uiuyR2bPlnU=
|
||||
github.com/prometheus/client_golang v1.24.1/go.mod h1:F+oSRECHg4sse5ucfYpYDeIv/hu68Zo0uoHKetWnzcE=
|
||||
github.com/prometheus/client_model v0.6.3 h1:O0jaTVAYNxTHYInEPFJt5I3+sN8zqBtVMPTB1qyxiEo=
|
||||
github.com/prometheus/client_model v0.6.3/go.mod h1:gpN5P9S7Rr6Yr92PiQ+Ixvhf6JZEkF1dnxsYL2aPBEM=
|
||||
github.com/prometheus/common v0.71.0 h1:9KDAKb7Mj3HEVKyFCK6Dc/HIwlBzZIN2l7/lrHl3KK8=
|
||||
github.com/prometheus/common v0.71.0/go.mod h1:CLJ5H8TEsGX8bl31BdMkfhIZ+QmZ9tBPPotUxUbfcmk=
|
||||
github.com/prometheus/procfs v0.22.0 h1:6q9+/JL9IKAPbCmBrv9n5O5Ty3NKnciV5X7YGw0oics=
|
||||
github.com/prometheus/procfs v0.22.0/go.mod h1:CvmFr/GVhIjIvWJZW3tgkODBQMRIf0EyWMQLHCHab58=
|
||||
github.com/santhosh-tekuri/jsonschema/v6 v6.0.3 h1:1EYB5IzjZawrrnELUi78f9fPu57HuXjmddZPjrls/28=
|
||||
github.com/santhosh-tekuri/jsonschema/v6 v6.0.3/go.mod h1:JXeL+ps8p7/KNMjDQk3TCwPpBy0wYklyWTfbkIzdIFU=
|
||||
github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
|
||||
github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI=
|
||||
github.com/stretchr/testify v1.7.2/go.mod h1:R6va5+xMeoiuVRoj+gSkQ7d3FALtqAAGI1FQKckRals=
|
||||
github.com/stretchr/testify v1.12.1 h1:EuwCh5fleGS7H32xRwO3wRGT7DxrDhLAT6FF8MpWDWE=
|
||||
github.com/stretchr/testify v1.12.1/go.mod h1:MDEgiDPPsNp5cuIrHPPCyornHKgEVbtFUmoNlxoYthg=
|
||||
github.com/yuin/goldmark v1.1.27/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74=
|
||||
github.com/yuin/goldmark v1.2.1/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74=
|
||||
github.com/zeebo/assert v1.3.0 h1:g7C04CbJuIDKNPFHmsk4hwZDO5O+kntRxzaUoNXj+IQ=
|
||||
github.com/zeebo/assert v1.3.0/go.mod h1:Pq9JiuJQpG8JLJdtkwrJESF0Foym2/D9XMU5ciN/wJ0=
|
||||
github.com/zeebo/xxh3 v1.1.0 h1:s7DLGDK45Dyfg7++yxI0khrfwq9661w9EN78eP/UZVs=
|
||||
github.com/zeebo/xxh3 v1.1.0/go.mod h1:IisAie1LELR4xhVinxWS5+zf1lA4p0MW4T+w+W07F5s=
|
||||
go.opentelemetry.io/auto/sdk v1.2.1 h1:jXsnJ4Lmnqd11kwkBV2LgLoFMZKizbCi5fNZ/ipaZ64=
|
||||
go.opentelemetry.io/auto/sdk v1.2.1/go.mod h1:KRTj+aOaElaLi+wW1kO/DZRXwkF4C5xPbEe3ZiIhN7Y=
|
||||
go.opentelemetry.io/contrib/instrumentation/google.golang.org/grpc/otelgrpc v0.71.0 h1:B2h3uqicet1CT2N5TOFhS+Gq++9i0/CLmaxvhmhtP5s=
|
||||
go.opentelemetry.io/contrib/instrumentation/google.golang.org/grpc/otelgrpc v0.71.0/go.mod h1:dylvB+ZiiwMvsDij9O84Uy7SijLgHMX4mbkncds+4Sw=
|
||||
go.opentelemetry.io/contrib/instrumentation/net/http/httptrace/otelhttptrace v0.71.0 h1:oFNJW32h2SXnET7XXstgT7pVh4vN+jW+GfiIaBguIZE=
|
||||
go.opentelemetry.io/contrib/instrumentation/net/http/httptrace/otelhttptrace v0.71.0/go.mod h1:+H3sPOFwag14eMHTPMElZtV0e4YfVZ/85KgrKUCB5FI=
|
||||
go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.71.0 h1:3g7B90UzBltIDKq1/5mrTGxTnOFDV0ICOhLoxiZ8jlg=
|
||||
go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.71.0/go.mod h1:Ef8SuTh59BT7+ofpDxN9z+yOlc4t2GjLmKDgYNJL/NU=
|
||||
go.opentelemetry.io/contrib/propagators/jaeger v1.46.0 h1:uxl0SGcmuBkHj/Adl9oftEAyiawQBPL5RzMAmt/Yvq4=
|
||||
go.opentelemetry.io/contrib/propagators/jaeger v1.46.0/go.mod h1:LiOkxCIvoLofmRps7f8l0NkBtmObnAyQ5trteFs6wj8=
|
||||
go.opentelemetry.io/contrib/samplers/jaegerremote v0.37.3 h1:20rKrm6q8YlSVwVObflwKd2abo9f8b3WsxJ6YTyQtE8=
|
||||
go.opentelemetry.io/contrib/samplers/jaegerremote v0.37.3/go.mod h1:2yFxWbgN2VupcMTWu57donCZ9I1lALhawQjNAK7Xmm4=
|
||||
go.opentelemetry.io/otel v1.46.0 h1:FHt5/CDyVxi/8IM1CH7VE/rRgq3kLHa2mSTVMO8AWyc=
|
||||
go.opentelemetry.io/otel v1.46.0/go.mod h1:Gj3SEScelsNC45tp4nSxRYlS+f5iez7W8XPMCt905kE=
|
||||
go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.46.0 h1:OFnwLJr+pF3iHrlGSzbxyuo6/6HyBlnlN1CWEJmBVcw=
|
||||
go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.46.0/go.mod h1:716wFneO0ov19A2beH5hjfh9AK5z/VWNAtDijp1Y0/g=
|
||||
go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracegrpc v1.46.0 h1:w53CDeOA/Kurp7yRsegSr6pbbr759dOvJ+yNmWM6Hxs=
|
||||
go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracegrpc v1.46.0/go.mod h1:BOmGMCbAtvcJiSJ+hLuhgPLdDbimnraSl8irz3iY8sY=
|
||||
go.opentelemetry.io/otel/metric v1.46.0 h1:yBnkXvgV7AXFILZc5K6IZe/CBFF3OS7BJ8ov6/lj0K8=
|
||||
go.opentelemetry.io/otel/metric v1.46.0/go.mod h1:iPmdWqifKUdzziPkvvzIJXITl56fQx2mGM/DHLB3/2o=
|
||||
go.opentelemetry.io/otel/sdk v1.46.0 h1:h5CNQQjEbuQXY/JfZtgt3i7HVFV3aHPO2OAwO2eTYPI=
|
||||
go.opentelemetry.io/otel/sdk v1.46.0/go.mod h1:GAERFXFt5SYCEB+YiKUbMBeza6UaDH7GmGOZEfh2gSM=
|
||||
go.opentelemetry.io/otel/sdk/metric v1.46.0 h1:0piZ26EG4RBfebb2jhDH6ERCYHoVWduc3kLgPCwSnSE=
|
||||
go.opentelemetry.io/otel/sdk/metric v1.46.0/go.mod h1:I1PbKrdVc8Qu8HYVDNtqVIwLwjNrhsV/uFuxfwg8mO4=
|
||||
go.opentelemetry.io/otel/trace v1.46.0 h1:OULy7ccdJnZtJ0UDYFOIGaCmiWzJ8Vi2G/Rsu60qs1c=
|
||||
go.opentelemetry.io/otel/trace v1.46.0/go.mod h1:J7GAXweO77XSFkB/rmAqk9D6ihszhFjLU+d9WuUxDLI=
|
||||
go.opentelemetry.io/proto/otlp v1.11.0 h1:5rrYs0Ykyj50sdU/JU0x8etU+LubXWb+gED6TbEdMIk=
|
||||
go.opentelemetry.io/proto/otlp v1.11.0/go.mod h1:SmVizdCOAm3XBtG1g1NnOdhW6jtddT72hLMhv8VwA8E=
|
||||
go.uber.org/goleak v1.3.0 h1:2K3zAYmnTNqV73imy9J1T3WC+gmCePx2hEGkimedGto=
|
||||
go.uber.org/goleak v1.3.0/go.mod h1:CoHD4mav9JJNrW/WLlf7HGZPjdw8EucARQHekz1X6bE=
|
||||
go.yaml.in/yaml/v2 v2.4.4 h1:tuyd0P+2Ont/d6e2rl3be67goVK4R6deVxCUX5vyPaQ=
|
||||
go.yaml.in/yaml/v2 v2.4.4/go.mod h1:gMZqIpDtDqOfM0uNfy0SkpRhvUryYH0Z6wdMYcacYXQ=
|
||||
go.yaml.in/yaml/v3 v3.0.5 h1:N6y/pJk8buWs9NY5ERU2HSMfm+IuD/OtfdAnq6kESPw=
|
||||
go.yaml.in/yaml/v3 v3.0.5/go.mod h1:HVTZu1O7/Vkt2N+BFy8Zza+lnLsABggaTM2ZpNIGuKg=
|
||||
golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w=
|
||||
golang.org/x/crypto v0.0.0-20191011191535-87dc89f01550/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI=
|
||||
golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto=
|
||||
golang.org/x/exp v0.0.0-20260908205506-85c1c2202aba h1:Ck8QetSgk912qxWLMCKxd0in+aiyBQyDSMae6e/xmpU=
|
||||
golang.org/x/exp v0.0.0-20260908205506-85c1c2202aba/go.mod h1:50RgIsmK7OwqzTTeqcSXQW8SswW0o8fRcDxmqGluJ8E=
|
||||
golang.org/x/mod v0.2.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA=
|
||||
golang.org/x/mod v0.3.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA=
|
||||
golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg=
|
||||
golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
|
||||
golang.org/x/net v0.0.0-20200226121028-0de0cce0169b/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
|
||||
golang.org/x/net v0.0.0-20201021035429-f5854403a974/go.mod h1:sp8m0HH+o8qH0wwXwYZr8TS3Oi6o0r6Gce1SSxlDquU=
|
||||
golang.org/x/net v0.59.0 h1:5zfYln+w5XCxwrnMMJPufRgNoXEaGxl0wo5GqPXyues=
|
||||
golang.org/x/net v0.59.0/go.mod h1:2DA/G1UfVbCpQPeWTmMPGY7Cs2PkBkwu743bVX5PIVg=
|
||||
golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
||||
golang.org/x/sync v0.0.0-20190911185100-cd5d95a43a6e/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
||||
golang.org/x/sync v0.0.0-20201020160332-67f06af15bc9/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
||||
golang.org/x/sync v0.23.0 h1:KameEIfc1IkluZyXWLn39Wd4tURc6GbCiISGiZm2bQk=
|
||||
golang.org/x/sync v0.23.0/go.mod h1:sUUOizhqBxiL6pEWpqNLUiaJn1ShEbZ6BBqskPbjZm0=
|
||||
golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
|
||||
golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20200116001909-b77594299b42/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20200223170610-d5e6a3e2c0ae/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20200930185726-fdedc70b468f/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20210630005230-0f9fa26af87c/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.0.0-20210927094055-39ccf1dd6fa6/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.0.0-20220503163025-988cb79eb6c6/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.48.0 h1:bbX/i/6MgT9BVLM9RT1thmxL04yeTAhbEz4SyadbXoo=
|
||||
golang.org/x/sys v0.48.0/go.mod h1:hNLxWAXmnKAxqDtdwIYC4bM9oQPEecfsnNMuSxOs3og=
|
||||
golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
|
||||
golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
|
||||
golang.org/x/text v0.42.0 h1:JbOZXgfeCPU9gacVtYliJqOhD+zhrEqK4LfdpmlUZqI=
|
||||
golang.org/x/text v0.42.0/go.mod h1:ojzP1Z+2QtioaF8DTtO8K5q7JWVVYwZKenzujK0Zd0E=
|
||||
golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
|
||||
golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
|
||||
golang.org/x/tools v0.0.0-20200619180055-7c47624df98f/go.mod h1:EkVYQZoAsY45+roYkvgYkIh4xh/qjgUK9TdY2XT94GE=
|
||||
golang.org/x/tools v0.0.0-20210106214847-113979e3529a/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA=
|
||||
golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
|
||||
golang.org/x/xerrors v0.0.0-20191011141410-1b5146add898/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
|
||||
golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
|
||||
golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
|
||||
gonum.org/v1/gonum v0.17.0 h1:VbpOemQlsSMrYmn7T2OUvQ4dqxQXU+ouZFQsZOx50z4=
|
||||
gonum.org/v1/gonum v0.17.0/go.mod h1:El3tOrEuMpv2UdMrbNlKEh9vd86bmQ6vqIcDwxEOc1E=
|
||||
google.golang.org/genproto/googleapis/api v0.0.0-20260911204522-f61a6ca850bd h1:jgCQeWccunvocKNK2RjhQHNiSjSeMMuBpQ/5pbP+2lA=
|
||||
google.golang.org/genproto/googleapis/api v0.0.0-20260911204522-f61a6ca850bd/go.mod h1:r4KD2hOq82JBWpTWkJ9NZLf6EwmRvAPIFmk7hPNtd+0=
|
||||
google.golang.org/genproto/googleapis/rpc v0.0.0-20260911204522-f61a6ca850bd h1:29LZPNIJg+GEmJwZXruJJRwqy/wnWAYZXDtROToV/aQ=
|
||||
google.golang.org/genproto/googleapis/rpc v0.0.0-20260911204522-f61a6ca850bd/go.mod h1:DjtHYE8FKJLivXcBEjGwndXfIC23G0VpXiXKqG179uA=
|
||||
google.golang.org/grpc v1.84.0 h1:soMyaPJ8pAak5PIQ0DGBUir0XRo2fRoMqhNWMLlLxO0=
|
||||
google.golang.org/grpc v1.84.0/go.mod h1:ljCht0DrxQrXBDRTZp52Qxh3Ffk8CdYm2sj4O2QN2C0=
|
||||
google.golang.org/protobuf v1.36.12 h1:pJOKDDOyeXErUroCihFAd5LQuwXBSpVnKGrj5o/fwxc=
|
||||
google.golang.org/protobuf v1.36.12/go.mod h1:HTf+CrKn2C3g5S8VImy6tdcUvCska2kB7j23XfzDpco=
|
||||
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
|
||||
gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
|
||||
1
packages/grafana-rcon/img/logo.svg
Normal file
1
packages/grafana-rcon/img/logo.svg
Normal file
|
|
@ -0,0 +1 @@
|
|||
<svg xmlns="http://www.w3.org/2000/svg" width="64" height="64" viewBox="0 0 64 64"><rect x="2" y="2" width="60" height="60" rx="8" fill="#2e7d32"/><path d="M15 21l12 11-12 11m18 0h16" stroke="white" stroke-width="5" fill="none"/></svg>
|
||||
|
After Width: | Height: | Size: 236 B |
6580
packages/grafana-rcon/package-lock.json
generated
Normal file
6580
packages/grafana-rcon/package-lock.json
generated
Normal file
File diff suppressed because it is too large
Load diff
23
packages/grafana-rcon/package.json
Normal file
23
packages/grafana-rcon/package.json
Normal file
|
|
@ -0,0 +1,23 @@
|
|||
{
|
||||
"name": "grafana-rcon",
|
||||
"version": "0.1.0",
|
||||
"private": true,
|
||||
"scripts": {
|
||||
"build": "tsc --outDir build && rollup -c && node build.mjs",
|
||||
"check": "tsc --noEmit"
|
||||
},
|
||||
"dependencies": {
|
||||
"@grafana/data": "12.4.1",
|
||||
"@grafana/runtime": "12.4.1",
|
||||
"@grafana/ui": "12.4.1",
|
||||
"react": "18.3.1",
|
||||
"react-dom": "18.3.1",
|
||||
"rxjs": "7.8.2"
|
||||
},
|
||||
"devDependencies": {
|
||||
"rollup": "4.67.3",
|
||||
"typescript": "7.0.2",
|
||||
"tslib": "2.8.1",
|
||||
"@types/react": "18.3.27"
|
||||
}
|
||||
}
|
||||
21
packages/grafana-rcon/panel.json
Normal file
21
packages/grafana-rcon/panel.json
Normal file
|
|
@ -0,0 +1,21 @@
|
|||
{
|
||||
"type": "panel",
|
||||
"name": "Minecraft Console",
|
||||
"id": "quorate-rcon-panel",
|
||||
"info": {
|
||||
"author": {
|
||||
"name": "Quorate"
|
||||
},
|
||||
"description": "Minecraft remote console",
|
||||
"logos": {
|
||||
"small": "img/logo.svg",
|
||||
"large": "img/logo.svg"
|
||||
},
|
||||
"version": "0.1.0",
|
||||
"updated": "2026-10-06"
|
||||
},
|
||||
"dependencies": {
|
||||
"grafanaDependency": ">=12.4.1",
|
||||
"plugins": []
|
||||
}
|
||||
}
|
||||
32
packages/grafana-rcon/plugin.json
Normal file
32
packages/grafana-rcon/plugin.json
Normal file
|
|
@ -0,0 +1,32 @@
|
|||
{
|
||||
"type": "app",
|
||||
"name": "Minecraft Console",
|
||||
"id": "quorate-rcon-app",
|
||||
"backend": true,
|
||||
"executable": "rcon",
|
||||
"autoEnabled": true,
|
||||
"info": {
|
||||
"author": {
|
||||
"name": "Quorate"
|
||||
},
|
||||
"description": "Minecraft remote console",
|
||||
"logos": {
|
||||
"small": "img/logo.svg",
|
||||
"large": "img/logo.svg"
|
||||
},
|
||||
"version": "0.1.0",
|
||||
"updated": "2026-10-06"
|
||||
},
|
||||
"dependencies": {
|
||||
"grafanaDependency": ">=12.4.1",
|
||||
"plugins": []
|
||||
},
|
||||
"iam": {
|
||||
"permissions": [
|
||||
{
|
||||
"action": "users:read",
|
||||
"scope": "global.users:*"
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
5
packages/grafana-rcon/rollup.config.mjs
Normal file
5
packages/grafana-rcon/rollup.config.mjs
Normal file
|
|
@ -0,0 +1,5 @@
|
|||
export default ['app', 'panel', 'datasource'].map(name => ({
|
||||
input: `build/${name}.js`,
|
||||
output: { file: `dist/${name === 'app' ? '' : name + '/'}module.js`, format: 'system' },
|
||||
external: ['react', '@grafana/data', '@grafana/runtime', '@grafana/ui', 'rxjs', 'react/jsx-runtime'],
|
||||
}));
|
||||
41
packages/grafana-rcon/src/Console.tsx
Normal file
41
packages/grafana-rcon/src/Console.tsx
Normal file
|
|
@ -0,0 +1,41 @@
|
|||
import React, { useEffect, useState } from 'react';
|
||||
import { getBackendSrv } from '@grafana/runtime';
|
||||
import { Alert, Button, Input, Stack } from '@grafana/ui';
|
||||
|
||||
const root = '/api/plugins/quorate-rcon-app/resources';
|
||||
|
||||
export function Console() {
|
||||
const [allowed, setAllowed] = useState(false);
|
||||
const [command, setCommand] = useState('');
|
||||
const [busy, setBusy] = useState(false);
|
||||
const [error, setError] = useState('');
|
||||
const [transcript, setTranscript] = useState<Array<{ command: string; output: string }>>([]);
|
||||
useEffect(() => {
|
||||
getBackendSrv().get<{ submit: boolean }>(`${root}/access`).then(v => setAllowed(v.submit)).catch(() => setError('Command authorization is unavailable.'));
|
||||
}, []);
|
||||
async function submit(e: React.FormEvent) {
|
||||
e.preventDefault();
|
||||
if (busy || !allowed || !command.trim()) { return; }
|
||||
setBusy(true); setError('');
|
||||
const submitted = command;
|
||||
try {
|
||||
const result = await getBackendSrv().post<{ output: string }>(`${root}/command`, { command: submitted }, { retry: 0 });
|
||||
setTranscript(items => [...items, { command: submitted, output: result.output }]);
|
||||
setCommand('');
|
||||
} catch (e) {
|
||||
const failure = e as { data?: { message?: string }; message?: string };
|
||||
setError(failure.data?.message || failure.message || 'Command failed. Check server state before resubmitting.');
|
||||
} finally { setBusy(false); }
|
||||
}
|
||||
return <div style={{ height: '100%', overflow: 'auto', padding: 8 }}>
|
||||
{!allowed && <Alert title="Commands require a Grafana administrator" severity="info">You can continue viewing server metrics.</Alert>}
|
||||
{error && <Alert title={error} severity="error" />}
|
||||
{allowed && <form onSubmit={submit}><Stack gap={1}>
|
||||
<Input aria-label="Minecraft command" placeholder="list" value={command} disabled={busy} onChange={e => setCommand(e.currentTarget.value)} />
|
||||
<Button type="submit" disabled={busy || !command.trim()}>{busy ? 'Submitting…' : 'Submit'}</Button>
|
||||
</Stack></form>}
|
||||
<div role="log" aria-label="Command output">
|
||||
{transcript.map((item, i) => <div key={i}><strong>{`> ${item.command}`}</strong><pre style={{ whiteSpace: 'pre-wrap', overflowWrap: 'anywhere' }}>{item.output}</pre></div>)}
|
||||
</div>
|
||||
</div>;
|
||||
}
|
||||
2
packages/grafana-rcon/src/app.tsx
Normal file
2
packages/grafana-rcon/src/app.tsx
Normal file
|
|
@ -0,0 +1,2 @@
|
|||
import { AppPlugin } from '@grafana/data';
|
||||
export const plugin = new AppPlugin();
|
||||
Some files were not shown because too many files have changed in this diff Show more
Loading…
Reference in a new issue