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 }