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
This commit is contained in:
yusing
2026-02-15 16:48:39 +08:00
parent 1244af0e38
commit 0acedb034a
8 changed files with 69 additions and 24 deletions

View File

@@ -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
}

View File

@@ -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

View File

@@ -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
}

View File

@@ -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())

View File

@@ -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 {

View File

@@ -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")

View File

@@ -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
}()