Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
7 changes: 7 additions & 0 deletions cmd/lotus-gateway/main.go
Original file line number Diff line number Diff line change
Expand Up @@ -167,6 +167,11 @@ var runCmd = &cli.Command{
Usage: "Enable CORS headers to allow cross-origin requests from web browsers",
Value: false,
},
&cli.BoolFlag{
Name: "request-logging",
Usage: "Enable logging of incoming API requests. Note: This will log POST request bodies which may impact performance due to body buffering and may expose sensitive data in logs",
Value: false,
},
},
Action: func(cctx *cli.Context) error {
log.Info("Starting lotus gateway")
Expand Down Expand Up @@ -202,6 +207,7 @@ var runCmd = &cli.Command{
perHostConnectionsPerMinute = cctx.Int("conn-per-minute")
maxFiltersPerConn = cctx.Int("eth-max-filters-per-conn")
enableCORS = cctx.Bool("cors")
enableRequestLogging = cctx.Bool("request-logging")
)

serverOptions := make([]jsonrpc.ServerOption, 0)
Expand Down Expand Up @@ -237,6 +243,7 @@ var runCmd = &cli.Command{
gateway.WithPerHostConnectionsPerMinute(perHostConnectionsPerMinute),
gateway.WithJsonrpcServerOptions(serverOptions...),
gateway.WithCORS(enableCORS),
gateway.WithRequestLogging(enableRequestLogging),
)
if err != nil {
return xerrors.Errorf("failed to set up gateway HTTP handler")
Expand Down
71 changes: 71 additions & 0 deletions gateway/handler.go
Original file line number Diff line number Diff line change
@@ -1,7 +1,9 @@
package gateway

import (
"bytes"
"context"
"io"
"net"
"net/http"
"strings"
Expand Down Expand Up @@ -42,13 +44,15 @@ type ShutdownHandler interface {
var _ ShutdownHandler = (*statefulCallHandler)(nil)
var _ ShutdownHandler = (*RateLimitHandler)(nil)
var _ ShutdownHandler = (*CORSHandler)(nil)
var _ ShutdownHandler = (*LoggingHandler)(nil)

// handlerOptions holds the options for the Handler function.
type handlerOptions struct {
perConnectionAPIRateLimit int
perHostConnectionsPerMinute int
jsonrpcServerOptions []jsonrpc.ServerOption
enableCORS bool
enableRequestLogging bool
}

// HandlerOption is a functional option for configuring the Handler.
Expand Down Expand Up @@ -90,6 +94,13 @@ func WithCORS(enable bool) HandlerOption {
}
}

// WithRequestLogging sets whether to enable request logging.
func WithRequestLogging(enable bool) HandlerOption {
return func(opts *handlerOptions) {
opts.enableRequestLogging = enable
}
}

// Handler returns a gateway http.Handler, to be mounted as-is on the server. The handler is
// returned as a ShutdownHandler which allows for graceful shutdown of the handler via its
// Shutdown method.
Expand Down Expand Up @@ -134,6 +145,11 @@ func Handler(gateway *Node, options ...HandlerOption) (ShutdownHandler, error) {

var handler http.Handler = &statefulCallHandler{m}

// Apply logging middleware if enabled
if opts.enableRequestLogging {
handler = NewLoggingHandler(handler)
}

// Apply CORS wrapper if enabled
if opts.enableCORS {
handler = NewCORSHandler(handler)
Expand Down Expand Up @@ -335,6 +351,61 @@ func (h *CORSHandler) Shutdown(ctx context.Context) error {
return shutdown(ctx, h.next)
}

// LoggingHandler logs incoming HTTP requests with details
type LoggingHandler struct {
next http.Handler
}

// NewLoggingHandler creates a new LoggingHandler that logs request details
func NewLoggingHandler(next http.Handler) *LoggingHandler {
return &LoggingHandler{next: next}
}

func (h *LoggingHandler) ServeHTTP(w http.ResponseWriter, r *http.Request) {
// Prepare log fields
logFields := []interface{}{
"remote_ip", getRemoteIP(r),
"method", r.Method,
"url", r.URL.String(),
}

// For POST requests, try to read and log up to maxLogBodyBytes of the body
const maxLogBodyBytes = 1024
if r.Method == http.MethodPost {
limited := &io.LimitedReader{R: r.Body, N: maxLogBodyBytes + 1}
buf, err := io.ReadAll(limited)
if err == nil {
Comment thread
rvagg marked this conversation as resolved.
var bodyStr string
if int64(len(buf)) > maxLogBodyBytes {
bodyStr = string(buf[:maxLogBodyBytes]) + "...[truncated]"
} else {
bodyStr = string(buf)
}
logFields = append(logFields, "body", bodyStr)
// Reconstruct the body for downstream handlers: combine what we read and the rest
rest := io.MultiReader(bytes.NewReader(buf), r.Body)
r.Body = io.NopCloser(rest)
}
}

log.Infow("request", logFields...)

h.next.ServeHTTP(w, r)
Comment thread
masih marked this conversation as resolved.
}

func (h *LoggingHandler) Shutdown(ctx context.Context) error {
return shutdown(ctx, h.next)
}

// getRemoteIP returns the remote IP address from the request.
func getRemoteIP(r *http.Request) string {
host, _, err := net.SplitHostPort(r.RemoteAddr)
if err != nil {
return r.RemoteAddr
}
return host
}

func shutdown(ctx context.Context, handler http.Handler) error {
if sh, ok := handler.(ShutdownHandler); ok {
return sh.Shutdown(ctx)
Expand Down
Loading