boulder/web/context.go

180 lines
5.6 KiB
Go

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"
}