Skip to content

Commit

Permalink
Fix OCSP-Responder double slash collapsing (letsencrypt#2748)
Browse files Browse the repository at this point in the history
Uses a special mux for the OCSP Responder so that we stop collapsing double slashes in GET requests which cause a small number of requests to be considered malformed.
  • Loading branch information
Roland Bracewell Shoemaker authored and cpu committed May 10, 2017
1 parent dfd0857 commit bd045b9
Show file tree
Hide file tree
Showing 4 changed files with 41 additions and 8 deletions.
17 changes: 14 additions & 3 deletions cmd/ocsp-responder/main.go
Original file line number Diff line number Diff line change
Expand Up @@ -234,6 +234,19 @@ as generated by Boulder's single-ocsp command.
<-forever
}

// ocspMux partially implements the interface defined for http.ServeMux but doesn't implement
// the path cleaning its Handler method does. Notably http.ServeMux will collapse repeated
// slashes into a single slash which breaks the base64 encoding that is used in OCSP GET
// requests. CFSSL explicitly recommends against using http.ServeMux for this reason:
// https://github.com/cloudflare/cfssl/blob/6388e1ec18d2933c35f0f8dfbbe383713eb04b1e/ocsp/responder.go#L170
type ocspMux struct {
handler http.Handler
}

func (om *ocspMux) Handler(_ *http.Request) (http.Handler, string) {
return om.handler, "/"
}

func mux(scope metrics.Scope, responderPath string, source cfocsp.Source) http.Handler {
stripPrefix := http.StripPrefix(responderPath, cfocsp.NewResponder(source))
h := http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
Expand All @@ -244,7 +257,5 @@ func mux(scope metrics.Scope, responderPath string, source cfocsp.Source) http.H
}
stripPrefix.ServeHTTP(w, r)
})
m := http.NewServeMux()
m.Handle("/", h)
return measured_http.New(m, clock.Default())
return measured_http.New(&ocspMux{h}, clock.Default())
}
16 changes: 15 additions & 1 deletion cmd/ocsp-responder/main_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ package main

import (
"bytes"
"encoding/base64"
"fmt"
"io/ioutil"
"net/http"
Expand Down Expand Up @@ -29,16 +30,29 @@ func TestMux(t *testing.T) {
if err != nil {
t.Fatalf("ocsp.ParseRequest: %s", err)
}
doubleSlashBytes, err := base64.StdEncoding.DecodeString("MFMwUTBPME0wSzAJBgUrDgMCGgUABBR+5mrncpqz/PiiIGRsFqEtYHEIXQQUqEpqYwR93brm0Tm3pkVl7/Oo7KECEgO/AC2R1FW8hePAj4xp//8Jhw==")
if err != nil {
t.Fatalf("failed to decode double slash OCSP request")
}
doubleSlashReq, err := ocsp.ParseRequest(doubleSlashBytes)
if err != nil {
t.Fatalf("failed to parse double slash OCSP request")
}
src := make(cfocsp.InMemorySource)
src[ocspReq.SerialNumber.String()] = resp.OCSPResponse
src[doubleSlashReq.SerialNumber.String()] = resp.OCSPResponse
h := mux(stats, "/foobar/", src)
type muxTest struct {
method string
path string
reqBody []byte
respBody []byte
}
mts := []muxTest{{"POST", "/foobar/", req, resp.OCSPResponse}, {"GET", "/", nil, nil}}
mts := []muxTest{
{"POST", "/foobar/", req, resp.OCSPResponse},
{"GET", "/", nil, nil},
{"GET", "/foobar/MFMwUTBPME0wSzAJBgUrDgMCGgUABBR+5mrncpqz/PiiIGRsFqEtYHEIXQQUqEpqYwR93brm0Tm3pkVl7/Oo7KECEgO/AC2R1FW8hePAj4xp//8Jhw==", nil, resp.OCSPResponse},
}
for i, mt := range mts {
w := httptest.NewRecorder()
r, err := http.NewRequest(mt.method, mt.path, bytes.NewReader(mt.reqBody))
Expand Down
14 changes: 11 additions & 3 deletions metrics/measured_http/http.go
Original file line number Diff line number Diff line change
Expand Up @@ -34,17 +34,25 @@ func (r *responseWriterWithStatus) WriteHeader(code int) {
r.ResponseWriter.WriteHeader(code)
}

// serveMux is a partial interface wrapper for the method http.ServeMux
// exposes that we use. This is needed so that we can replace the default
// http.ServeMux in ocsp-responder where we don't want to use its path
// canonicalization.
type serveMux interface {
Handler(*http.Request) (http.Handler, string)
}

// MeasuredHandler wraps an http.Handler and records prometheus stats
type MeasuredHandler struct {
*http.ServeMux
serveMux
clk clock.Clock
// Normally this is always responseTime, but we override it for testing.
stat *prometheus.HistogramVec
}

func New(m *http.ServeMux, clk clock.Clock) *MeasuredHandler {
func New(m serveMux, clk clock.Clock) *MeasuredHandler {
return &MeasuredHandler{
ServeMux: m,
serveMux: m,
clk: clk,
stat: responseTime,
}
Expand Down
2 changes: 1 addition & 1 deletion metrics/measured_http/http_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -36,7 +36,7 @@ func TestMeasuring(t *testing.T) {
mux := http.NewServeMux()
mux.Handle("/foo", sleepyHandler{clk})
mh := MeasuredHandler{
ServeMux: mux,
serveMux: mux,
clk: clk,
stat: stat,
}
Expand Down

0 comments on commit bd045b9

Please sign in to comment.