mirror of
https://gitcode.com/JianFeeeee/HomeAgent.git
synced 2026-10-04 00:03:59 +00:00
feat(webui): 请求IP日志记录中间件
- clientIP(): 提取真实客户端IP,优先 X-Forwarded-For(取第一跳) / X-Real-IP,回退 RemoteAddr - logged(): 最外层中间件,覆盖全部路由,记录方法/路径/IP/认证方式/状态码/耗时 - 认证方式判定: api-key(X-API-Key/Bearer) / session(cookie) / none - SSE长连接(/chat/events)启动即记,不阻塞等待完成 - statusWriter 捕获响应状态码,支持 Flush/Hijack 透传 - 用于排查'谁调用了什么接口'(如插件禁用等变更操作)
This commit is contained in:
@ -740,6 +740,97 @@ func (h *Handler) requireWeb(fn http.HandlerFunc) http.HandlerFunc {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// clientIP 提取请求的真实客户端 IP。
|
||||||
|
// 优先取 X-Forwarded-For / X-Real-IP(反向代理/frp 场景),回退 RemoteAddr。
|
||||||
|
func clientIP(r *http.Request) string {
|
||||||
|
if xff := r.Header.Get("X-Forwarded-For"); xff != "" {
|
||||||
|
// 取第一个(最接近客户端的地址)
|
||||||
|
if i := strings.IndexByte(xff, ','); i >= 0 {
|
||||||
|
return strings.TrimSpace(xff[:i])
|
||||||
|
}
|
||||||
|
return strings.TrimSpace(xff)
|
||||||
|
}
|
||||||
|
if xr := r.Header.Get("X-Real-IP"); xr != "" {
|
||||||
|
return strings.TrimSpace(xr)
|
||||||
|
}
|
||||||
|
host, _, err := net.SplitHostPort(r.RemoteAddr)
|
||||||
|
if err != nil {
|
||||||
|
return r.RemoteAddr
|
||||||
|
}
|
||||||
|
return host
|
||||||
|
}
|
||||||
|
|
||||||
|
// requestLog 每次请求的日志行(含 IP、认证方式、状态码)。
|
||||||
|
// 由 logged 中间件在请求完成后调用,authType 为 caller 预先判定。
|
||||||
|
func requestLog(r *http.Request, authType, ip string, status int, dur time.Duration) {
|
||||||
|
// 认证方式判定(供排查谁调用了变更接口)
|
||||||
|
if authType == "" {
|
||||||
|
switch {
|
||||||
|
case r.Header.Get("X-API-Key") != "" || strings.HasPrefix(r.Header.Get("Authorization"), "Bearer "):
|
||||||
|
authType = "api-key"
|
||||||
|
case func() bool { c, err := r.Cookie("homeagent_session"); return err == nil && c.Value != "" }():
|
||||||
|
authType = "session"
|
||||||
|
default:
|
||||||
|
authType = "none"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
log.Printf("[webui] %s %s from=%s auth=%s status=%d (%s)", r.Method, r.URL.Path, ip, authType, status, dur.Round(time.Millisecond))
|
||||||
|
}
|
||||||
|
|
||||||
|
// logged 中间件:包装任意 handler,记录请求 IP / 方法 / 路径 / 认证方式 / 状态码。
|
||||||
|
// 置于最外层(mux 之上),覆盖所有路由(含登录页、静态资源)。
|
||||||
|
func (h *Handler) logged(next http.Handler) http.Handler {
|
||||||
|
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||||
|
// 预判认证方式(在 requireAPI/requireWeb 之前的原始请求判定)
|
||||||
|
authType := "none"
|
||||||
|
switch {
|
||||||
|
case h.validAPIKey(r):
|
||||||
|
authType = "api-key"
|
||||||
|
case h.validSession(r):
|
||||||
|
authType = "session"
|
||||||
|
}
|
||||||
|
// SSE 长连接:不阻塞在完成时记录(连接可能持续很久),启动即记一条
|
||||||
|
if strings.HasSuffix(r.URL.Path, "/chat/events") {
|
||||||
|
requestLog(r, authType, clientIP(r), http.StatusOK, 0)
|
||||||
|
next.ServeHTTP(w, r)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
start := time.Now()
|
||||||
|
sw := &statusWriter{ResponseWriter: w, status: http.StatusOK}
|
||||||
|
next.ServeHTTP(sw, r)
|
||||||
|
requestLog(r, authType, clientIP(r), sw.status, time.Since(start))
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
// statusWriter 包装 ResponseWriter 以捕获响应状态码。
|
||||||
|
// SSE 等流式写入直接透传不做缓冲。
|
||||||
|
type statusWriter struct {
|
||||||
|
http.ResponseWriter
|
||||||
|
status int
|
||||||
|
}
|
||||||
|
|
||||||
|
func (sw *statusWriter) WriteHeader(code int) {
|
||||||
|
sw.status = code
|
||||||
|
sw.ResponseWriter.WriteHeader(code)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (sw *statusWriter) Write(b []byte) (int, error) {
|
||||||
|
return sw.ResponseWriter.Write(b)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (sw *statusWriter) Flush() {
|
||||||
|
if f, ok := sw.ResponseWriter.(http.Flusher); ok {
|
||||||
|
f.Flush()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (sw *statusWriter) Hijack() (net.Conn, *bufio.ReadWriter, error) {
|
||||||
|
if hj, ok := sw.ResponseWriter.(http.Hijacker); ok {
|
||||||
|
return hj.Hijack()
|
||||||
|
}
|
||||||
|
return nil, nil, fmt.Errorf("hijack not supported")
|
||||||
|
}
|
||||||
|
|
||||||
func (h *Handler) RegisterRoutes(mux *http.ServeMux) {
|
func (h *Handler) RegisterRoutes(mux *http.ServeMux) {
|
||||||
mux.HandleFunc("/login", h.handleLoginPage)
|
mux.HandleFunc("/login", h.handleLoginPage)
|
||||||
mux.HandleFunc("/api/v1/login", h.handleLogin)
|
mux.HandleFunc("/api/v1/login", h.handleLogin)
|
||||||
|
|||||||
@ -232,7 +232,9 @@ func (p *Plugin) Start(s *sdk.PluginSDK) error {
|
|||||||
p.handler = NewHandler(s)
|
p.handler = NewHandler(s)
|
||||||
p.handler.RegisterRoutes(p.mux)
|
p.handler.RegisterRoutes(p.mux)
|
||||||
|
|
||||||
p.server = &http.Server{Addr: addr, Handler: p.mux}
|
// 最外层套 logged 中间件:记录每个请求的来源 IP / 方法 / 路径 / 认证方式 / 状态码。
|
||||||
|
// 用于排查“谁调用了什么接口”(如插件禁用等变更操作)。
|
||||||
|
p.server = &http.Server{Addr: addr, Handler: p.handler.logged(p.mux)}
|
||||||
go func() {
|
go func() {
|
||||||
log.Printf("[webui] HTTP server listening on %s", addr)
|
log.Printf("[webui] HTTP server listening on %s", addr)
|
||||||
if err := p.server.ListenAndServe(); err != nil && err != http.ErrServerClosed {
|
if err := p.server.ListenAndServe(); err != nil && err != http.ErrServerClosed {
|
||||||
|
|||||||
Reference in New Issue
Block a user