From 0acedb034a54ac7a4a0ab8ded7e37223438bc7b6 Mon Sep 17 00:00:00 2001 From: yusing Date: Sun, 15 Feb 2026 16:48:39 +0800 Subject: [PATCH] feat: add event emission for blocked requests and provider changes - Emit ACL blocked events with matched rule information - Emit HTTP blocked events from CIDR whitelist, ForwardAuth, and OIDC middlewares - Emit global events for provider file/docker changes - Add MatchedIndex method to ACL matchers for rule identification - Update goutils submodule for events package update --- goutils | 2 +- internal/acl/config.go | 37 +++++++++++-------- internal/acl/matcher.go | 9 +++++ .../net/gphttp/middleware/cidr_whitelist.go | 2 + internal/net/gphttp/middleware/forwardauth.go | 5 ++- internal/net/gphttp/middleware/oidc.go | 5 +++ internal/route/provider/provider.go | 15 +++++++- internal/watcher/events/events.go | 18 +++++++-- 8 files changed, 69 insertions(+), 24 deletions(-) diff --git a/goutils b/goutils index 90d25861..494ab85a 160000 --- a/goutils +++ b/goutils @@ -1 +1 @@ -Subproject commit 90d25861782121dd9d1b68131a2aa812d0cbd65e +Subproject commit 494ab85a33aea48b8be5bcd08a506596727248f4 diff --git a/internal/acl/config.go b/internal/acl/config.go index 534dbfa6..b25ab083 100644 --- a/internal/acl/config.go +++ b/internal/acl/config.go @@ -14,6 +14,7 @@ import ( "github.com/yusing/godoxy/internal/maxmind" "github.com/yusing/godoxy/internal/notif" gperr "github.com/yusing/goutils/errs" + aclevents "github.com/yusing/goutils/events/acl" strutils "github.com/yusing/goutils/strings" "github.com/yusing/goutils/task" ) @@ -70,8 +71,9 @@ type checkCache struct { } type ipLog struct { - info *maxmind.IPInfo - allowed bool + info *maxmind.IPInfo + allowed bool + blockedRule string } const cacheTTL = 1 * time.Minute @@ -211,23 +213,26 @@ func (c *Config) logNotifyLoop(parent task.Parent) { select { case <-parent.Context().Done(): return - case log := <-c.logNotifyCh: + case req := <-c.logNotifyCh: if c.logger != nil { - if !log.allowed || c.logAllowed { - c.logger.LogACL(log.info, !log.allowed) + if !req.allowed || c.logAllowed { + c.logger.LogACL(req.info, !req.allowed) } } if c.needNotify() { - if log.allowed { + if req.allowed { if c.notifyAllowed { - c.allowedCount[log.info.Str]++ + c.allowedCount[req.info.Str]++ c.totalAllowedCount++ } } else { - c.blockedCount[log.info.Str]++ + c.blockedCount[req.info.Str]++ c.totalBlockedCount++ } } + if !req.allowed { + aclevents.Blocked(req.info.Str, req.blockedRule) + } case <-c.notifyTicker.C: // will never tick when notify is disabled total := len(c.allowedCount) + len(c.blockedCount) if total == 0 { @@ -259,9 +264,9 @@ func (c *Config) logNotifyLoop(parent task.Parent) { } // log and notify if needed -func (c *Config) logAndNotify(info *maxmind.IPInfo, allowed bool) { +func (c *Config) logAndNotify(info *maxmind.IPInfo, allowed bool, blockedRule string) { if c.logNotifyCh != nil { - c.logNotifyCh <- ipLog{info: info, allowed: allowed} + c.logNotifyCh <- ipLog{info: info, allowed: allowed, blockedRule: blockedRule} } } @@ -276,30 +281,30 @@ func (c *Config) IPAllowed(ip net.IP) bool { } if c.allowLocal && ip.IsPrivate() { - c.logAndNotify(&maxmind.IPInfo{IP: ip, Str: ip.String()}, true) + c.logAndNotify(&maxmind.IPInfo{IP: ip, Str: ip.String()}, true, "") return true } ipStr := ip.String() record, ok := c.ipCache.Load(ipStr) if ok && !record.Expired() { - c.logAndNotify(record.IPInfo, record.allow) + c.logAndNotify(record.IPInfo, record.allow, "") return record.allow } ipAndStr := &maxmind.IPInfo{IP: ip, Str: ipStr} - if c.Deny.Match(ipAndStr) { - c.logAndNotify(ipAndStr, false) + if index := c.Deny.MatchedIndex(ipAndStr); index != -1 { + c.logAndNotify(ipAndStr, false, c.Deny[index].raw) c.cacheRecord(ipAndStr, false) return false } if c.Allow.Match(ipAndStr) { - c.logAndNotify(ipAndStr, true) + c.logAndNotify(ipAndStr, true, "") c.cacheRecord(ipAndStr, true) return true } - c.logAndNotify(ipAndStr, c.defaultAllow) + c.logAndNotify(ipAndStr, c.defaultAllow, "deny by default") c.cacheRecord(ipAndStr, c.defaultAllow) return c.defaultAllow } diff --git a/internal/acl/matcher.go b/internal/acl/matcher.go index 07f30c46..5c390f4e 100644 --- a/internal/acl/matcher.go +++ b/internal/acl/matcher.go @@ -83,6 +83,15 @@ func (matchers Matchers) Match(ip *maxmind.IPInfo) bool { return false } +func (matchers Matchers) MatchedIndex(ip *maxmind.IPInfo) int { + for i, m := range matchers { + if m.match(ip) { + return i + } + } + return -1 +} + func (matchers Matchers) MarshalText() ([]byte, error) { if len(matchers) == 0 { return []byte("[]"), nil diff --git a/internal/net/gphttp/middleware/cidr_whitelist.go b/internal/net/gphttp/middleware/cidr_whitelist.go index fff9d611..269f9266 100644 --- a/internal/net/gphttp/middleware/cidr_whitelist.go +++ b/internal/net/gphttp/middleware/cidr_whitelist.go @@ -8,6 +8,7 @@ import ( "github.com/puzpuzpuz/xsync/v4" nettypes "github.com/yusing/godoxy/internal/net/types" "github.com/yusing/godoxy/internal/serialization" + httpevents "github.com/yusing/goutils/events/http" httputils "github.com/yusing/goutils/http" ) @@ -71,6 +72,7 @@ func (wl *cidrWhitelist) checkIP(w http.ResponseWriter, r *http.Request) bool { } } if !allow { + defer httpevents.Blocked(r, "CIDRWhitelist", "IP not allowed") http.Error(w, wl.Message, wl.StatusCode) return false } diff --git a/internal/net/gphttp/middleware/forwardauth.go b/internal/net/gphttp/middleware/forwardauth.go index e24086b2..cd2c3a30 100644 --- a/internal/net/gphttp/middleware/forwardauth.go +++ b/internal/net/gphttp/middleware/forwardauth.go @@ -3,12 +3,14 @@ package middleware import ( "context" "errors" + "fmt" "net" "net/http" "strings" "time" entrypoint "github.com/yusing/godoxy/internal/entrypoint/types" + httpevents "github.com/yusing/goutils/events/http" httputils "github.com/yusing/goutils/http" "github.com/yusing/goutils/http/httpheaders" ) @@ -92,6 +94,8 @@ func (m *forwardAuthMiddleware) before(w http.ResponseWriter, r *http.Request) ( defer resp.Body.Close() if resp.StatusCode < http.StatusOK || resp.StatusCode >= http.StatusMultipleChoices { + defer httpevents.Blocked(r, "ForwardAuth", fmt.Sprintf("HTTP %d", resp.StatusCode)) + body, release, err := httputils.ReadAllBody(resp) defer release(body) @@ -100,7 +104,6 @@ func (m *forwardAuthMiddleware) before(w http.ResponseWriter, r *http.Request) ( w.WriteHeader(http.StatusInternalServerError) return false } - httpheaders.CopyHeader(w.Header(), resp.Header) httpheaders.RemoveHopByHopHeaders(w.Header()) diff --git a/internal/net/gphttp/middleware/oidc.go b/internal/net/gphttp/middleware/oidc.go index f0e7c115..3eb61bea 100644 --- a/internal/net/gphttp/middleware/oidc.go +++ b/internal/net/gphttp/middleware/oidc.go @@ -9,6 +9,7 @@ import ( "github.com/rs/zerolog/log" "github.com/yusing/godoxy/internal/auth" + httpevents "github.com/yusing/goutils/events/http" "github.com/yusing/goutils/http/httpheaders" ) @@ -118,6 +119,10 @@ func (amw *oidcMiddleware) before(w http.ResponseWriter, r *http.Request) (proce return true } + if r.Method != http.MethodHead { + defer httpevents.Blocked(r, "OIDC", err.Error()) + } + isGet := r.Method == http.MethodGet isWS := httpheaders.IsWebsocket(r.Header) switch { diff --git a/internal/route/provider/provider.go b/internal/route/provider/provider.go index e9c01866..8869cee0 100644 --- a/internal/route/provider/provider.go +++ b/internal/route/provider/provider.go @@ -18,6 +18,7 @@ import ( watcherEvents "github.com/yusing/godoxy/internal/watcher/events" gperr "github.com/yusing/goutils/errs" "github.com/yusing/goutils/eventqueue" + "github.com/yusing/goutils/events" "github.com/yusing/goutils/task" ) @@ -118,11 +119,21 @@ func (p *Provider) Start(parent task.Parent) error { opts := eventqueue.Options[watcherEvents.Event]{ FlushInterval: providerEventFlushInterval, - OnFlush: func(events []watcherEvents.Event) { + OnFlush: func(evs []watcherEvents.Event) { handler := p.newEventHandler() // routes' lifetime should follow the provider's lifetime - handler.Handle(t, events) + handler.Handle(t, evs) handler.Log() + + globalEvents := make([]events.Event, len(evs)) + for i, ev := range evs { + globalEvents[i] = events.NewEvent(events.LevelInfo, "provider_event", ev.Action.String(), map[string]any{ + "provider": p.String(), + "type": ev.Type, // file / docker + "actor": ev.ActorName, // file path / container name + }) + } + events.Global.AddAll(globalEvents) }, OnError: func(err error) { p.Logger().Err(err).Msg("event error") diff --git a/internal/watcher/events/events.go b/internal/watcher/events/events.go index 70249dbd..239acd47 100644 --- a/internal/watcher/events/events.go +++ b/internal/watcher/events/events.go @@ -65,12 +65,22 @@ var fileActionNameMap = map[Action]string{ ActionFileRenamed: "renamed", } +var dockerActionNameMap = map[Action]string{ + ActionContainerCreate: "created", + ActionContainerStart: "started", + ActionContainerUnpause: "unpaused", + ActionContainerKill: "killed", + ActionContainerStop: "stopped", + ActionContainerPause: "paused", + ActionContainerDie: "died", + ActionContainerDestroy: "destroyed", +} + var actionNameMap = func() (m map[Action]string) { - m = make(map[Action]string, len(DockerEventMap)) - for k, v := range DockerEventMap { - m[v] = string(k) - } + m = make(map[Action]string, len(fileActionNameMap)+len(dockerActionNameMap)+1) maps.Copy(m, fileActionNameMap) + maps.Copy(m, dockerActionNameMap) + m[ActionForceReload] = "force-reloaded" return m }()