mirror of
https://github.com/yusing/godoxy.git
synced 2026-04-10 02:43:37 +02:00
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:
@@ -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
|
||||
}
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
}
|
||||
|
||||
@@ -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())
|
||||
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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")
|
||||
|
||||
@@ -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
|
||||
}()
|
||||
|
||||
|
||||
Reference in New Issue
Block a user