github.com/letsencrypt/boulder@v0.20251208.0/web/context.go (about)

     1  package web
     2  
     3  import (
     4  	"context"
     5  	"crypto"
     6  	"crypto/ecdsa"
     7  	"crypto/rsa"
     8  	"encoding/json"
     9  	"fmt"
    10  	"net/http"
    11  	"net/netip"
    12  	"strings"
    13  	"time"
    14  
    15  	"github.com/letsencrypt/boulder/features"
    16  	"github.com/letsencrypt/boulder/identifier"
    17  	blog "github.com/letsencrypt/boulder/log"
    18  )
    19  
    20  type userAgentContextKey struct{}
    21  
    22  func UserAgent(ctx context.Context) string {
    23  	// The below type assertion is safe because this context key can only be
    24  	// set by this package and is only set to a string.
    25  	val, ok := ctx.Value(userAgentContextKey{}).(string)
    26  	if !ok {
    27  		return ""
    28  	}
    29  	return val
    30  }
    31  
    32  func WithUserAgent(ctx context.Context, ua string) context.Context {
    33  	return context.WithValue(ctx, userAgentContextKey{}, ua)
    34  }
    35  
    36  // RequestEvent is a structured record of the metadata we care about for a
    37  // single web request. It is generated when a request is received, passed to
    38  // the request handler which can populate its fields as appropriate, and then
    39  // logged when the request completes.
    40  type RequestEvent struct {
    41  	// These fields are not rendered in JSON; instead, they are rendered
    42  	// whitespace-separated ahead of the JSON. This saves bytes in the logs since
    43  	// we don't have to include field names, quotes, or commas -- all of these
    44  	// fields are known to not include whitespace.
    45  	Method    string  `json:"-"`
    46  	Endpoint  string  `json:"-"`
    47  	Requester int64   `json:"-"`
    48  	Code      int     `json:"-"`
    49  	Latency   float64 `json:"-"`
    50  	RealIP    string  `json:"-"`
    51  
    52  	Slug           string   `json:",omitempty"`
    53  	InternalErrors []string `json:",omitempty"`
    54  	Error          string   `json:",omitempty"`
    55  	// If there is an error checking the data store for our rate limits
    56  	// we ignore it, but attach the error to the log event for analysis.
    57  	// TODO(#7796): Treat errors from the rate limit system as normal
    58  	// errors and put them into InternalErrors.
    59  	IgnoredRateLimitError string `json:",omitempty"`
    60  	UserAgent             string `json:"ua,omitempty"`
    61  	// Origin is sent by the browser from XHR-based clients.
    62  	Origin string         `json:",omitempty"`
    63  	Extra  map[string]any `json:",omitempty"`
    64  
    65  	// For endpoints that create objects, the ID of the newly created object.
    66  	Created string `json:",omitempty"`
    67  
    68  	// For challenge and authorization GETs and POSTs:
    69  	// the status of the authorization at the time the request began.
    70  	Status string `json:",omitempty"`
    71  	// The set of identifiers, for instance in an authorization, challenge,
    72  	// new-order, finalize, or revoke request.
    73  	Identifiers identifier.ACMEIdentifiers `json:",omitempty"`
    74  
    75  	// For challenge POSTs, the challenge type.
    76  	ChallengeType string `json:",omitempty"`
    77  
    78  	// suppressed controls whether this event will be logged when the request
    79  	// completes. If true, no log line will be emitted. Can only be set by
    80  	// calling .Suppress(); automatically unset by adding an internal error.
    81  	suppressed bool `json:"-"`
    82  }
    83  
    84  // AddError formats the given message with the given args and appends it to the
    85  // list of internal errors that have occurred as part of handling this event.
    86  // If the RequestEvent has been suppressed, this un-suppresses it.
    87  func (e *RequestEvent) AddError(msg string, args ...any) {
    88  	e.InternalErrors = append(e.InternalErrors, fmt.Sprintf(msg, args...))
    89  	e.suppressed = false
    90  }
    91  
    92  // Suppress causes the RequestEvent to not be logged at all when the request
    93  // is complete. This is a no-op if an internal error has been added to the event
    94  // (logging errors takes precedence over suppressing output).
    95  func (e *RequestEvent) Suppress() {
    96  	if len(e.InternalErrors) == 0 {
    97  		e.suppressed = true
    98  	}
    99  }
   100  
   101  type WFEHandlerFunc func(context.Context, *RequestEvent, http.ResponseWriter, *http.Request)
   102  
   103  func (f WFEHandlerFunc) ServeHTTP(e *RequestEvent, w http.ResponseWriter, r *http.Request) {
   104  	f(r.Context(), e, w, r)
   105  }
   106  
   107  type wfeHandler interface {
   108  	ServeHTTP(e *RequestEvent, w http.ResponseWriter, r *http.Request)
   109  }
   110  
   111  type TopHandler struct {
   112  	wfe wfeHandler
   113  	log blog.Logger
   114  }
   115  
   116  func NewTopHandler(log blog.Logger, wfe wfeHandler) *TopHandler {
   117  	return &TopHandler{
   118  		wfe: wfe,
   119  		log: log,
   120  	}
   121  }
   122  
   123  // responseWriterWithStatus satisfies http.ResponseWriter, but keeps track of the
   124  // status code for logging.
   125  type responseWriterWithStatus struct {
   126  	http.ResponseWriter
   127  	code int
   128  }
   129  
   130  // WriteHeader stores a status code for generating stats.
   131  func (r *responseWriterWithStatus) WriteHeader(code int) {
   132  	r.code = code
   133  	r.ResponseWriter.WriteHeader(code)
   134  }
   135  
   136  func (th *TopHandler) ServeHTTP(w http.ResponseWriter, r *http.Request) {
   137  	// Check that this header is well-formed, since we assume it is when logging.
   138  	realIP := r.Header.Get("X-Real-IP")
   139  	_, err := netip.ParseAddr(realIP)
   140  	if err != nil {
   141  		realIP = "0.0.0.0"
   142  	}
   143  
   144  	userAgent := r.Header.Get("User-Agent")
   145  
   146  	logEvent := &RequestEvent{
   147  		RealIP:    realIP,
   148  		Method:    r.Method,
   149  		UserAgent: userAgent,
   150  		Origin:    r.Header.Get("Origin"),
   151  		Extra:     make(map[string]any),
   152  	}
   153  
   154  	ctx := WithUserAgent(r.Context(), userAgent)
   155  	r = r.WithContext(ctx)
   156  
   157  	if !features.Get().PropagateCancels {
   158  		// We specifically override the default r.Context() because we would prefer
   159  		// for clients to not be able to cancel our operations in arbitrary places.
   160  		// Instead we start a new context, and apply timeouts in our various RPCs.
   161  		ctx := context.WithoutCancel(r.Context())
   162  		r = r.WithContext(ctx)
   163  	}
   164  
   165  	// Some clients will send a HTTP Host header that includes the default port
   166  	// for the scheme that they are using. Previously when we were fronted by
   167  	// Akamai they would rewrite the header and strip out the unnecessary port,
   168  	// now that they are not in our request path we need to strip these ports out
   169  	// ourselves.
   170  	//
   171  	// The main reason we want to strip these ports out is so that when this header
   172  	// is sent to the /directory endpoint we don't reply with directory URLs that
   173  	// also contain these ports.
   174  	//
   175  	// We unconditionally strip :443 even when r.TLS is nil because the WFE2
   176  	// may be deployed HTTP-only behind another service that terminates HTTPS on
   177  	// its behalf.
   178  	r.Host = strings.TrimSuffix(r.Host, ":443")
   179  	r.Host = strings.TrimSuffix(r.Host, ":80")
   180  
   181  	begin := time.Now()
   182  	rwws := &responseWriterWithStatus{w, 0}
   183  	defer func() {
   184  		logEvent.Code = rwws.code
   185  		if logEvent.Code == 0 {
   186  			// If we haven't explicitly set a status code golang will set it
   187  			// to 200 itself when writing to the wire
   188  			logEvent.Code = http.StatusOK
   189  		}
   190  		logEvent.Latency = time.Since(begin).Seconds()
   191  		th.logEvent(logEvent)
   192  	}()
   193  	th.wfe.ServeHTTP(logEvent, rwws, r)
   194  }
   195  
   196  func (th *TopHandler) logEvent(logEvent *RequestEvent) {
   197  	if logEvent.suppressed {
   198  		return
   199  	}
   200  	var msg string
   201  	jsonEvent, err := json.Marshal(logEvent)
   202  	if err != nil {
   203  		th.log.AuditErrf("failed to marshal logEvent - %s - %#v", msg, err)
   204  		return
   205  	}
   206  	th.log.Infof("%s %s %d %d %d %s JSON=%s",
   207  		logEvent.Method, logEvent.Endpoint, logEvent.Requester, logEvent.Code,
   208  		int(logEvent.Latency*1000), logEvent.RealIP, jsonEvent)
   209  }
   210  
   211  func KeyTypeToString(pub crypto.PublicKey) string {
   212  	switch pk := pub.(type) {
   213  	case *rsa.PublicKey:
   214  		return fmt.Sprintf("RSA %d", pk.N.BitLen())
   215  	case *ecdsa.PublicKey:
   216  		return fmt.Sprintf("ECDSA %s", pk.Params().Name)
   217  	}
   218  	return "unknown"
   219  }