...

Source file src/github.com/letsencrypt/boulder/web/context.go

Documentation: github.com/letsencrypt/boulder/web

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

View as plain text