Files
reforger_crawler_main/auth/session.go
T
ilbinek 56d9e10621
Cross Compile Go / build (push) Canceled after 0s
admin dashboard, metrics and telemetry
- embedded React admin UI at /admin/ui (go:embed), login with ADMIN_SECRET
  via signed HttpOnly session cookie, X-ADMIN-KEY still works for scripts
- admin API: overview stats, indexing queue, addon management (priority,
  reindex, delete/restore/purge, bulk), whitelist CRUD, duplicate hashes,
  traffic, metrics explorer, services, events/audit log, system info
- built-in metrics: request latency per route, visitors, top addons and
  searches, indexer/scraper counters; minute buckets in the DB with hourly
  rollups and retention; optional Prometheus /metrics (METRICS_TOKEN)
- POST /back/telemetry for indexers/scrapers (metrics, events, heartbeat)
  and a stdlib-only Go client in telemetry/
- auth hardening: empty secrets no longer allow access, constant-time
  compare, CORS not applied to /admin and /back, TRUSTED_PROXIES setting
- Dockerfile builds the UI and no longer bakes config.yaml into the image;
  CI builds the UI, runs tests and builds arm64 with CGO
2026-10-03 18:10:57 +02:00

109 lines
2.7 KiB
Go

package auth
import (
"crypto/hmac"
"crypto/rand"
"crypto/sha256"
"encoding/base64"
"fmt"
"net/http"
"strconv"
"strings"
"time"
"gitea.tbdevent.eu/TBD/reforger_crawler_main/initializers"
"github.com/gin-gonic/gin"
)
// CookieName is the admin session cookie. It is scoped to /admin so it is never
// sent to the public API.
const CookieName = "rc_admin"
var b64 = base64.RawURLEncoding
// The signing key is derived from the admin secret, so rotating ADMIN_SECRET
// invalidates every issued session.
func sessionKey() []byte {
m := hmac.New(sha256.New, []byte(initializers.AdminSecret))
m.Write([]byte("rc-admin-session/v1"))
return m.Sum(nil)
}
func sign(payload string) string {
m := hmac.New(sha256.New, sessionKey())
m.Write([]byte(payload))
return b64.EncodeToString(m.Sum(nil))
}
// IssueToken creates a stateless session token: v1.<expiry>.<nonce>.<mac>
func IssueToken(now time.Time) (string, time.Time) {
exp := now.Add(initializers.AdminSessionTTL)
nonce := make([]byte, 16)
_, _ = rand.Read(nonce)
payload := fmt.Sprintf("v1.%d.%s", exp.Unix(), b64.EncodeToString(nonce))
return payload + "." + sign(payload), exp
}
// VerifyToken checks the signature and expiry and returns the expiry time.
func VerifyToken(token string, now time.Time) (time.Time, bool) {
if initializers.AdminSecret == "" {
return time.Time{}, false
}
idx := strings.LastIndexByte(token, '.')
if idx <= 0 {
return time.Time{}, false
}
payload, mac := token[:idx], token[idx+1:]
if !hmac.Equal([]byte(mac), []byte(sign(payload))) {
return time.Time{}, false
}
parts := strings.Split(payload, ".")
if len(parts) != 3 || parts[0] != "v1" {
return time.Time{}, false
}
expUnix, err := strconv.ParseInt(parts[1], 10, 64)
if err != nil {
return time.Time{}, false
}
exp := time.Unix(expUnix, 0)
if !now.Before(exp) {
return time.Time{}, false
}
return exp, true
}
func cookieSecure(c *gin.Context) bool {
switch initializers.AdminCookieSecure {
case "true", "1", "yes":
return true
case "false", "0", "no":
return false
}
return c.Request.TLS != nil || strings.EqualFold(c.GetHeader("X-Forwarded-Proto"), "https")
}
func SetSessionCookie(c *gin.Context, token string, exp time.Time) {
http.SetCookie(c.Writer, &http.Cookie{
Name: CookieName,
Value: token,
Path: "/admin",
Expires: exp,
MaxAge: int(time.Until(exp).Seconds()),
HttpOnly: true,
Secure: cookieSecure(c),
SameSite: http.SameSiteStrictMode,
})
}
func ClearSessionCookie(c *gin.Context) {
http.SetCookie(c.Writer, &http.Cookie{
Name: CookieName,
Value: "",
Path: "/admin",
MaxAge: -1,
HttpOnly: true,
Secure: cookieSecure(c),
SameSite: http.SameSiteStrictMode,
})
}