Files
edgeguard-native/internal/handlers/forwardproxy.go
Debian 72269f5b7c feat: Squid Forward-Proxy — vollständig (Renderer + Handler + UI)
Stub raus, vollständig implementiert:

* internal/services/forwardproxy: CRUD-Repo gegen forward_proxy_acls
  (priority desc, action allow|deny).
* internal/handlers/forwardproxy.go: REST /api/v1/forward-proxy/acls
  mit Validation (acl_type-Whitelist verhindert Squid-Reload-Crash
  bei Tippfehlern). Auto-Reload nach jeder Mutation.
* internal/squid/squid.cfg.tpl + squid.go: Renderer schreibt
  /etc/edgeguard/squid/squid.conf, atomic + Symlink von
  /etc/squid/squid.conf (Squid liest Distro-Pfad — gleicher
  Pattern-Fix wie wg-quick). cache_dir 100MB, cache_mem 64MB,
  http_port 3128. Default-Policy: nur localnet (10/8, 172.16/12,
  192.168/16) — verhindert Open-Relay, falls Operator keine ACLs
  anlegt.
* main.go: forwardproxy-Repo + squid-Reloader instanziiert + Handler
  registriert.
* render.go: squid.New() bekommt Pool (war () vorher, Stub-Signatur).
* postinst sudoers: edgeguard darf systemctl reload squid.service.
* Frontend /forward-proxy: PageHeader + DataTable + ACL-Modal mit
  acl_type-Dropdown (13 Squid-Vokabular-Typen), action-Select,
  Priority. Sidebar-Eintrag unter Security.
* i18n DE/EN für fwd.* Block + nav.forwardProxy.

Verified end-to-end: ACL-Insert via SQL, render → squid reload →
curl -x http://127.0.0.1:3128 http://example.com/ → 200.

Version 1.0.26.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-11 00:27:05 +02:00

163 lines
4.2 KiB
Go

package handlers
import (
"context"
"errors"
"log/slog"
"strconv"
"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) {
g := rg.Group("/forward-proxy/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")
}
// 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
}