230 lines
6.1 KiB
Go
230 lines
6.1 KiB
Go
package handlers
|
|
|
|
import (
|
|
"context"
|
|
"errors"
|
|
"log/slog"
|
|
"os/exec"
|
|
"strconv"
|
|
"strings"
|
|
|
|
"github.com/gin-gonic/gin"
|
|
|
|
"git.netcell-it.de/projekte/edgeguard-native/internal/handlers/response"
|
|
"git.netcell-it.de/projekte/edgeguard-native/internal/models"
|
|
"git.netcell-it.de/projekte/edgeguard-native/internal/services/audit"
|
|
"git.netcell-it.de/projekte/edgeguard-native/internal/services/forwardproxy"
|
|
)
|
|
|
|
type ForwardProxyHandler struct {
|
|
Repo *forwardproxy.Repo
|
|
Audit *audit.Repo
|
|
NodeID string
|
|
Reloader func(ctx context.Context) error
|
|
}
|
|
|
|
func NewForwardProxyHandler(repo *forwardproxy.Repo, a *audit.Repo, nodeID string, reloader func(context.Context) error) *ForwardProxyHandler {
|
|
return &ForwardProxyHandler{Repo: repo, Audit: a, NodeID: nodeID, Reloader: reloader}
|
|
}
|
|
|
|
func (h *ForwardProxyHandler) reload(ctx context.Context, op string) {
|
|
if h.Reloader == nil {
|
|
return
|
|
}
|
|
if err := h.Reloader(ctx); err != nil {
|
|
slog.Warn("squid: reload after mutation failed", "op", op, "error", err)
|
|
}
|
|
}
|
|
|
|
func (h *ForwardProxyHandler) Register(rg *gin.RouterGroup) {
|
|
base := rg.Group("/forward-proxy")
|
|
base.GET("/stats", h.Stats)
|
|
|
|
g := base.Group("/acls")
|
|
g.GET("", h.List)
|
|
g.POST("", h.Create)
|
|
g.GET("/:id", h.Get)
|
|
g.PUT("/:id", h.Update)
|
|
g.DELETE("/:id", h.Delete)
|
|
}
|
|
|
|
func (h *ForwardProxyHandler) List(c *gin.Context) {
|
|
out, err := h.Repo.List(c.Request.Context())
|
|
if err != nil {
|
|
response.Internal(c, err)
|
|
return
|
|
}
|
|
response.OK(c, gin.H{"acls": out})
|
|
}
|
|
|
|
func (h *ForwardProxyHandler) Get(c *gin.Context) {
|
|
id, ok := parseID(c)
|
|
if !ok {
|
|
return
|
|
}
|
|
x, err := h.Repo.Get(c.Request.Context(), id)
|
|
if err != nil {
|
|
if errors.Is(err, forwardproxy.ErrNotFound) {
|
|
response.NotFound(c, err)
|
|
return
|
|
}
|
|
response.Internal(c, err)
|
|
return
|
|
}
|
|
response.OK(c, x)
|
|
}
|
|
|
|
func (h *ForwardProxyHandler) Create(c *gin.Context) {
|
|
var req models.ForwardProxyACL
|
|
if err := c.ShouldBindJSON(&req); err != nil {
|
|
response.BadRequest(c, err)
|
|
return
|
|
}
|
|
if err := validateACL(&req); err != nil {
|
|
response.BadRequest(c, err)
|
|
return
|
|
}
|
|
out, err := h.Repo.Create(c.Request.Context(), req)
|
|
if err != nil {
|
|
response.Internal(c, err)
|
|
return
|
|
}
|
|
_ = h.Audit.Log(c.Request.Context(), actorOf(c), "forward_proxy.acl.create", out.Name, out, h.NodeID)
|
|
response.Created(c, out)
|
|
h.reload(c.Request.Context(), "create")
|
|
}
|
|
|
|
func (h *ForwardProxyHandler) Update(c *gin.Context) {
|
|
id, ok := parseID(c)
|
|
if !ok {
|
|
return
|
|
}
|
|
var req models.ForwardProxyACL
|
|
if err := c.ShouldBindJSON(&req); err != nil {
|
|
response.BadRequest(c, err)
|
|
return
|
|
}
|
|
if err := validateACL(&req); err != nil {
|
|
response.BadRequest(c, err)
|
|
return
|
|
}
|
|
out, err := h.Repo.Update(c.Request.Context(), id, req)
|
|
if err != nil {
|
|
if errors.Is(err, forwardproxy.ErrNotFound) {
|
|
response.NotFound(c, err)
|
|
return
|
|
}
|
|
response.Internal(c, err)
|
|
return
|
|
}
|
|
_ = h.Audit.Log(c.Request.Context(), actorOf(c), "forward_proxy.acl.update", out.Name, out, h.NodeID)
|
|
response.OK(c, out)
|
|
h.reload(c.Request.Context(), "update")
|
|
}
|
|
|
|
func (h *ForwardProxyHandler) Delete(c *gin.Context) {
|
|
id, ok := parseID(c)
|
|
if !ok {
|
|
return
|
|
}
|
|
if err := h.Repo.Delete(c.Request.Context(), id); err != nil {
|
|
if errors.Is(err, forwardproxy.ErrNotFound) {
|
|
response.NotFound(c, err)
|
|
return
|
|
}
|
|
response.Internal(c, err)
|
|
return
|
|
}
|
|
_ = h.Audit.Log(c.Request.Context(), actorOf(c), "forward_proxy.acl.delete", strconv.FormatInt(id, 10), gin.H{"id": id}, h.NodeID)
|
|
response.NoContent(c)
|
|
h.reload(c.Request.Context(), "delete")
|
|
}
|
|
|
|
// Stats liefert Squid-Cache-Statistiken via `squidclient mgr:counters`.
|
|
// Die Ausgabe enthält HTTP-Header gefolgt von key = value Zeilen.
|
|
func (h *ForwardProxyHandler) Stats(c *gin.Context) {
|
|
out, err := exec.Command("squidclient", "-h", "127.0.0.1", "-p", "3128", "mgr:counters").Output()
|
|
if err != nil {
|
|
response.OK(c, gin.H{
|
|
"error": "squidclient nicht verfügbar: " + err.Error(),
|
|
})
|
|
return
|
|
}
|
|
response.OK(c, gin.H{"stats": parseSquidCounters(string(out))})
|
|
}
|
|
|
|
type squidStats struct {
|
|
ClientRequests int64 `json:"client_requests"`
|
|
CacheHits int64 `json:"cache_hits"`
|
|
CacheHitPct float64 `json:"cache_hit_pct"`
|
|
ClientErrors int64 `json:"client_errors"`
|
|
BytesIn int64 `json:"bytes_in"`
|
|
BytesOut int64 `json:"bytes_out"`
|
|
ServerRequests int64 `json:"server_requests"`
|
|
ServerErrors int64 `json:"server_errors"`
|
|
}
|
|
|
|
func parseSquidCounters(out string) squidStats {
|
|
// squidclient prefixes an HTTP response header block — skip it.
|
|
body := out
|
|
if idx := strings.Index(out, "\r\n\r\n"); idx >= 0 {
|
|
body = out[idx+4:]
|
|
} else if idx := strings.Index(out, "\n\n"); idx >= 0 {
|
|
body = out[idx+2:]
|
|
}
|
|
s := squidStats{}
|
|
for _, line := range strings.Split(body, "\n") {
|
|
k, v, ok := strings.Cut(line, "=")
|
|
if !ok {
|
|
continue
|
|
}
|
|
n, _ := strconv.ParseInt(strings.TrimSpace(v), 10, 64)
|
|
switch strings.TrimSpace(k) {
|
|
case "client_http.requests":
|
|
s.ClientRequests = n
|
|
case "client_http.hits":
|
|
s.CacheHits = n
|
|
case "client_http.errors":
|
|
s.ClientErrors = n
|
|
case "client_http.kbytes_in":
|
|
s.BytesIn = n * 1024
|
|
case "client_http.kbytes_out":
|
|
s.BytesOut = n * 1024
|
|
case "server.all.requests":
|
|
s.ServerRequests = n
|
|
case "server.all.errors":
|
|
s.ServerErrors = n
|
|
}
|
|
}
|
|
if s.ClientRequests > 0 {
|
|
s.CacheHitPct = float64(s.CacheHits) / float64(s.ClientRequests) * 100
|
|
}
|
|
return s
|
|
}
|
|
|
|
// validateACL prüft Name (squid-konform), action, acl_type. Squid
|
|
// nimmt viele Typen — wir whitelisten die, die in einem Forward-
|
|
// Proxy-Setup üblich sind, damit Tippfehler nicht beim reload
|
|
// crashen.
|
|
func validateACL(a *models.ForwardProxyACL) error {
|
|
if a.Name == "" {
|
|
return errors.New("name required")
|
|
}
|
|
switch a.Action {
|
|
case "allow", "deny":
|
|
default:
|
|
return errors.New("action must be allow or deny")
|
|
}
|
|
switch a.ACLType {
|
|
case "src", "dst", "dstdomain", "srcdomain", "port",
|
|
"proto", "method", "time", "url_regex", "urlpath_regex",
|
|
"dstdom_regex", "srcdom_regex", "browser":
|
|
default:
|
|
return errors.New("acl_type not supported (allowed: src, dst, dstdomain, srcdomain, port, proto, method, time, url_regex, urlpath_regex, dstdom_regex, srcdom_regex, browser)")
|
|
}
|
|
if a.Value == "" {
|
|
return errors.New("value required")
|
|
}
|
|
return nil
|
|
}
|