package web import ( "context" "crypto" "crypto/ecdsa" "crypto/rsa" "encoding/json" "fmt" "net" "net/http" "strings" "time" "github.com/honeycombio/beeline-go" blog "github.com/letsencrypt/boulder/log" ) type RequestEvent struct { // These fields are not rendered in JSON; instead, they are rendered // whitespace-separated ahead of the JSON. This saves bytes in the logs since // we don't have to include field names, quotes, or commas -- all of these // fields are known to not include whitespace. Method string `json:"-"` Endpoint string `json:"-"` Requester int64 `json:"-"` Code int `json:"-"` Latency float64 `json:"-"` RealIP string `json:"-"` Slug string `json:",omitempty"` InternalErrors []string `json:",omitempty"` Error string `json:",omitempty"` Contacts []string `json:",omitempty"` UserAgent string `json:"ua,omitempty"` // Origin is sent by the browser from XHR-based clients. Origin string `json:",omitempty"` Payload string `json:",omitempty"` Extra map[string]interface{} `json:",omitempty"` // For endpoints that create objects, the ID of the newly created object. Created string `json:",omitempty"` // For challenge and authorization GETs and POSTs: // the status of the authorization at the time the request began. Status string `json:",omitempty"` // The DNS name, if applicable DNSName string `json:",omitempty"` // For challenge POSTs, the challenge type. ChallengeType string `json:",omitempty"` } func (e *RequestEvent) AddError(msg string, args ...interface{}) { e.InternalErrors = append(e.InternalErrors, fmt.Sprintf(msg, args...)) } type WFEHandlerFunc func(context.Context, *RequestEvent, http.ResponseWriter, *http.Request) func (f WFEHandlerFunc) ServeHTTP(e *RequestEvent, w http.ResponseWriter, r *http.Request) { f(r.Context(), e, w, r) } type wfeHandler interface { ServeHTTP(e *RequestEvent, w http.ResponseWriter, r *http.Request) } type TopHandler struct { wfe wfeHandler log blog.Logger } func NewTopHandler(log blog.Logger, wfe wfeHandler) *TopHandler { return &TopHandler{ wfe: wfe, log: log, } } // responseWriterWithStatus satisfies http.ResponseWriter, but keeps track of the // status code for logging. type responseWriterWithStatus struct { http.ResponseWriter code int } // WriteHeader stores a status code for generating stats. func (r *responseWriterWithStatus) WriteHeader(code int) { r.code = code r.ResponseWriter.WriteHeader(code) } func (th *TopHandler) ServeHTTP(w http.ResponseWriter, r *http.Request) { // Check that this header is well-formed, since we assume it is when logging. realIP := r.Header.Get("X-Real-IP") if net.ParseIP(realIP) == nil { realIP = "0.0.0.0" } logEvent := &RequestEvent{ RealIP: realIP, Method: r.Method, UserAgent: r.Header.Get("User-Agent"), Origin: r.Header.Get("Origin"), Extra: make(map[string]interface{}), } ctx := r.Context() beeline.AddFieldToTrace(ctx, "real_ip", logEvent.RealIP) beeline.AddFieldToTrace(ctx, "method", logEvent.Method) beeline.AddFieldToTrace(ctx, "user_agent", logEvent.UserAgent) beeline.AddFieldToTrace(ctx, "origin", logEvent.Origin) // Some clients will send a HTTP Host header that includes the default port // for the scheme that they are using. Previously when we were fronted by // Akamai they would rewrite the header and strip out the unnecessary port, // now that they are not in our request path we need to strip these ports out // ourselves. // // The main reason we want to strip these ports out is so that when this header // is sent to the /directory endpoint we don't reply with directory URLs that // also contain these ports, which would then in turn end up being sent in the JWS // signature 'url' header, which we don't support. // // We unconditionally strip :443 even when r.TLS is nil because the WFE/WFE2 // may be deployed HTTP-only behind another service that terminates HTTPS on // its behalf. r.Host = strings.TrimSuffix(r.Host, ":443") r.Host = strings.TrimSuffix(r.Host, ":80") begin := time.Now() rwws := &responseWriterWithStatus{w, 0} defer func() { beeline.AddFieldToTrace(ctx, "internal_errors", logEvent.InternalErrors) logEvent.Code = rwws.code if logEvent.Code == 0 { // If we haven't explicitly set a status code golang will set it // to 200 itself when writing to the wire logEvent.Code = http.StatusOK } beeline.AddFieldToTrace(ctx, "code", logEvent.Code) logEvent.Latency = time.Since(begin).Seconds() beeline.AddFieldToTrace(ctx, "latency", logEvent.Latency) th.logEvent(logEvent) }() th.wfe.ServeHTTP(logEvent, rwws, r) } func (th *TopHandler) logEvent(logEvent *RequestEvent) { var msg string jsonEvent, err := json.Marshal(logEvent) if err != nil { th.log.AuditErrf("failed to marshal logEvent - %s - %#v", msg, err) return } th.log.Infof("%s %s %d %d %d %s JSON=%s", logEvent.Method, logEvent.Endpoint, logEvent.Requester, logEvent.Code, int(logEvent.Latency*1000), logEvent.RealIP, jsonEvent) } // Comma-separated list of HTTP clients involved in making this // request, starting with the original requestor and ending with the // remote end of our TCP connection (which is typically our own // proxy). func GetClientAddr(r *http.Request) string { if xff := r.Header.Get("X-Forwarded-For"); xff != "" { return xff + "," + r.RemoteAddr } return r.RemoteAddr } func KeyTypeToString(pub crypto.PublicKey) string { switch pk := pub.(type) { case *rsa.PublicKey: return fmt.Sprintf("RSA %d", pk.N.BitLen()) case *ecdsa.PublicKey: return fmt.Sprintf("ECDSA %s", pk.Params().Name) } return "unknown" }