mirror of
https://github.com/mudler/LocalAI.git
synced 2026-11-06 12:33:16 -05:00
Echo's RealIP trusted X-Forwarded-For and X-Real-Ip from any peer when no IPExtractor is set. That fed the login and registration rate limits and the API trace client address, so a client could dodge a limit by rotating a forged header. Add pkg/clientip, one resolver for the whole server. With --trusted-proxies / LOCALAI_TRUSTED_PROXIES (IPs and CIDRs, empty by default), the forwarded chain is read only when the TCP peer is listed. The client is the right-most X-Forwarded-For entry that is not a trusted proxy; a malformed entry stops the walk. With the flag unset every request resolves to its TCP peer. The resolver is set as Echo's IPExtractor and used by the access log (--log-client), so all consumers agree. Assisted-by: Claude:claude-sonnet-5-5 go gofmt
107 lines
3.1 KiB
Go
107 lines
3.1 KiB
Go
package http
|
|
|
|
import (
|
|
"errors"
|
|
"net/http"
|
|
"strings"
|
|
"time"
|
|
"unicode"
|
|
|
|
"github.com/labstack/echo/v4"
|
|
"github.com/mudler/LocalAI/pkg/clientip"
|
|
"github.com/mudler/LocalAI/pkg/diagnostics"
|
|
"github.com/mudler/xlog"
|
|
)
|
|
|
|
// maxLoggedUserAgent caps the user agent in the access log so a client
|
|
// cannot flood the log with one header.
|
|
const maxLoggedUserAgent = 80
|
|
|
|
// maxLoggedRequestID caps the request id for the same reason.
|
|
const maxLoggedRequestID = 64
|
|
|
|
// accessLog logs one line per request. When logClient is true the line also
|
|
// carries the client address (the TCP peer, or the forwarded client when the
|
|
// peer is a trusted proxy) and the user agent, so an unauthorized caller can
|
|
// be identified.
|
|
func accessLog(logClient bool, clients *clientip.Resolver) echo.MiddlewareFunc {
|
|
return func(next echo.HandlerFunc) echo.HandlerFunc {
|
|
return func(c echo.Context) error {
|
|
start := time.Now()
|
|
req := c.Request()
|
|
res := c.Response()
|
|
err := next(c)
|
|
|
|
// Echo's central HTTPErrorHandler runs *after* this middleware
|
|
// returns, so res.Status still reads the default 200 here when a
|
|
// handler returned an error without writing a response. Mirror
|
|
// echo.DefaultHTTPErrorHandler's status derivation so the access
|
|
// log reflects the status the client actually receives — without
|
|
// this, every silent handler error logs as 200.
|
|
status := res.Status
|
|
if err != nil && !res.Committed {
|
|
status = http.StatusInternalServerError
|
|
var he *echo.HTTPError
|
|
if errors.As(err, &he) {
|
|
status = he.Code
|
|
}
|
|
}
|
|
|
|
// Fix for #7989: Reduce log verbosity of Web UI polling, resources API, and health checks
|
|
// These paths are logged at DEBUG level (hidden by default) instead of INFO.
|
|
isQuietPath := false
|
|
for _, path := range quietPaths {
|
|
if req.URL.Path == path {
|
|
isQuietPath = true
|
|
break
|
|
}
|
|
}
|
|
|
|
// URL.Path never holds the query string, so keys passed as
|
|
// ?api_key=... stay out of the log.
|
|
attrs := []any{"method", req.Method, "path", req.URL.Path, "status", status}
|
|
if logClient {
|
|
attrs = append(attrs, "duration", time.Since(start).String(), "remote", clients.Resolve(req), "user_agent", sanitizeLogValue(req.UserAgent(), maxLoggedUserAgent))
|
|
if id := requestID(req, res); id != "" {
|
|
attrs = append(attrs, "request_id", id)
|
|
}
|
|
}
|
|
|
|
if isQuietPath && status == 200 {
|
|
xlog.Debug("HTTP request", attrs...)
|
|
} else {
|
|
xlog.Info("HTTP request", attrs...)
|
|
}
|
|
return err
|
|
}
|
|
}
|
|
}
|
|
|
|
// requestID returns the diagnostic id the server set on the response, or the
|
|
// X-Request-Id the caller sent, or an empty string.
|
|
func requestID(req *http.Request, res *echo.Response) string {
|
|
id := res.Header().Get(diagnostics.HeaderDiagnosticID)
|
|
if id == "" {
|
|
id = req.Header.Get(echo.HeaderXRequestID)
|
|
}
|
|
return sanitizeLogValue(id, maxLoggedRequestID)
|
|
}
|
|
|
|
// sanitizeLogValue replaces control characters with a space and truncates
|
|
// the value to max characters.
|
|
func sanitizeLogValue(ua string, max int) string {
|
|
var b strings.Builder
|
|
n := 0
|
|
for _, r := range ua {
|
|
if n == max {
|
|
break
|
|
}
|
|
if unicode.IsControl(r) {
|
|
r = ' '
|
|
}
|
|
b.WriteRune(r)
|
|
n++
|
|
}
|
|
return b.String()
|
|
}
|