1
29
30 package responder
31
32 import (
33 "bytes"
34 "context"
35 "encoding/hex"
36 "fmt"
37 "net/http"
38 "net/http/httptest"
39 "net/url"
40 "strings"
41 "testing"
42 "time"
43
44 "github.com/jmhodges/clock"
45 "github.com/prometheus/client_golang/prometheus"
46 "golang.org/x/crypto/ocsp"
47
48 blog "github.com/letsencrypt/boulder/log"
49 "github.com/letsencrypt/boulder/test"
50 )
51
52 const (
53 responseFile = "testdata/resp64.pem"
54 binResponseFile = "testdata/response.der"
55 brokenResponseFile = "testdata/response_broken.pem"
56 mixResponseFile = "testdata/response_mix.pem"
57 )
58
59 type testSource struct{}
60
61 func (ts testSource) Response(_ context.Context, r *ocsp.Request) (*Response, error) {
62 respBytes, err := hex.DecodeString("3082031D0A0100A08203163082031206092B060105050730010104820303308202FF3081E8A1453043310B300906035504061302555331123010060355040A1309676F6F6420677579733120301E06035504031317434120696E7465726D6564696174652028525341292041180F32303230303631393030333730305A30818D30818A304C300906052B0E03021A0500041417779CF67D84CD4449A2FC7EAC431F9823D8575A04149F2970E80CF9C75ECC1F2871D8C390CD19F40108021300FF8B2AEC5293C6B31D0BC0BA329CF594E7BAA116180F32303230303631393030333733305AA0030A0101180F32303230303631393030303030305AA011180F32303230303632333030303030305A300D06092A864886F70D01010B0500038202010011688303203098FC522D2C599A234B136930E3C4680F2F3192188B98D6EE90E8479449968C51335FADD1636584ACEA9D01A30790BD90190FA35A47E793718128B19E9ED156382C1B68245A6887F547B0B86C44C2354B8DBA94D8BFCAA768EB55FA84AEB4026DBEFC687DB280D21C0B3497A11909804A20F402BDD95E4843C02E30435C2570FFC4EB152FE2785B8D268AC996619644AEC9CF50959D46DEB21DFE96B4D2881D61ABBCA9B6BFEC2DB9132801CAE737C862F0AEAB4948B63F35740CE93FCDBC148F5070790D7BBA1A87E15078CD8335F83686142CE8AC3AD21FAE45B87A7B12562D9F245352A83E3901E97E5EC77E9817990712D8BE60860ABA58804DDE4ECDCA6AEFD3D8764FDBABF0AB1902FA9A7C4C3F5814C25C5E78E0754469E087CAED81E50A5873CADFCAC42963AB38CFD11096BE4201DE4589B57EC48B3DA05A65800D654160E022F6748CD93B431A17270C1B27E313734FCF85F22547D060F23F594BD68C6330C2705190A04905FBD2389E2DD21C0188809E03D713F56BF95953C9897DA6D4D074D70F164270C41BFB386B69E86EB3B9192FEA8F43CE5368CC9AF8687DEE567672A8580BA6A9F76E6E6705DD2F76F48C2C180C763CF4C48AF78C25D40EA7278CB2FBC78958B3179301825B420A7CAE7ACE4C41B5BA7D567AABC9C2701EE75A28F9181E044EDAAA55A31538AA9C526D4C324B9AE58D2922")
63 if err != nil {
64 return nil, err
65 }
66 resp, err := ocsp.ParseResponse(respBytes, nil)
67 if err != nil {
68 return nil, err
69 }
70 return &Response{resp, respBytes}, nil
71 }
72
73 type expiredSource struct{}
74
75 func (es expiredSource) Response(_ context.Context, r *ocsp.Request) (*Response, error) {
76 return nil, errOCSPResponseExpired
77 }
78
79 type testCase struct {
80 method, path string
81 expected int
82 }
83
84 func TestResponseExpired(t *testing.T) {
85 cases := []testCase{
86 {"GET", "/MFQwUjBQME4wTDAJBgUrDgMCGgUABBQ55F6w46hhx%2Fo6OXOHa%2BYfe32YhgQU%2B3hPEvlgFYMsnxd%2FNBmzLjbqQYkCEwD6Wh0MaVKu9gJ3By9DI%2F%2Fxsd4%3D", 533},
87 }
88
89 responder := Responder{
90 Source: expiredSource{},
91 responseTypes: prometheus.NewCounterVec(
92 prometheus.CounterOpts{
93 Name: "ocspResponses-test",
94 },
95 []string{"type"},
96 ),
97 clk: clock.NewFake(),
98 log: blog.NewMock(),
99 }
100
101 for _, tc := range cases {
102 t.Run(fmt.Sprintf("%s %s", tc.method, tc.path), func(t *testing.T) {
103 rw := httptest.NewRecorder()
104 responder.responseTypes.Reset()
105
106 responder.ServeHTTP(rw, &http.Request{
107 Method: tc.method,
108 URL: &url.URL{
109 Path: tc.path,
110 },
111 })
112 if rw.Code != tc.expected {
113 t.Errorf("Incorrect response code: got %d, wanted %d", rw.Code, tc.expected)
114 }
115 test.AssertByteEquals(t, ocsp.InternalErrorErrorResponse, rw.Body.Bytes())
116 })
117 }
118 }
119
120 func TestOCSP(t *testing.T) {
121 cases := []testCase{
122 {"OPTIONS", "/", http.StatusMethodNotAllowed},
123 {"GET", "/", http.StatusBadRequest},
124
125 {"GET", "%ZZFQwUjBQME4wTDAJBgUrDgMCGgUABBQ55F6w46hhx%2Fo6OXOHa%2BYfe32YhgQU%2B3hPEvlgFYMsnxd%2FNBmzLjbqQYkCEwD6Wh0MaVKu9gJ3By9DI%2F%2Fxsd4%3D", http.StatusBadRequest},
126
127 {"GET", "%%FQwUjBQME4wTDAJBgUrDgMCGgUABBQ55F6w46hhx%2Fo6OXOHa%2BYfe32YhgQU%2B3hPEvlgFYMsnxd%2FNBmzLjbqQYkCEwD6Wh0MaVKu9gJ3By9DI%2F%2Fxsd4%3D", http.StatusBadRequest},
128
129 {"GET", "==MFQwUjBQME4wTDAJBgUrDgMCGgUABBQ55F6w46hhx%2Fo6OXOHa%2BYfe32YhgQU%2B3hPEvlgFYMsnxd%2FNBmzLjbqQYkCEwD6Wh0MaVKu9gJ3By9DI%2F%2Fxsd4%3D", http.StatusBadRequest},
130
131 {"GET", "AAAMFQwUjBQME4wTDAJBgUrDgMCGgUABBQ55F6w46hhx%2Fo6OXOHa%2BYfe32YhgQU%2B3hPEvlgFYMsnxd%2FNBmzLjbqQYkCEwD6Wh0MaVKu9gJ3By9DI%2F%2Fxsd4%3D", http.StatusBadRequest},
132
133 {"GET", "MFQwUjBQME4wTDAJBgUrDgMCGgUABBQ55F6w46hhx%2Fo6OXOHa%2BYfe32YhgQU%2B3hPEvlgFYMsnxd%2FNBmzLjbqQYkCEwD6Wh0MaVKu9gJ3By9DI%2F%2Fxsd4%3D", http.StatusOK},
134
135 {"GET", "/MFQwUjBQME4wTDAJBgUrDgMCGgUABBQ55F6w46hhx%2Fo6OXOHa%2BYfe32YhgQU%2B3hPEvlgFYMsnxd%2FNBmzLjbqQYkCEwD6Wh0MaVKu9gJ3By9DI%2F%2Fxsd4%3D", http.StatusOK},
136 }
137
138 responder := Responder{
139 Source: testSource{},
140 responseTypes: prometheus.NewCounterVec(
141 prometheus.CounterOpts{
142 Name: "ocspResponses-test",
143 },
144 []string{"type"},
145 ),
146 responseAges: prometheus.NewHistogram(
147 prometheus.HistogramOpts{
148 Name: "ocspAges-test",
149 Buckets: []float64{43200},
150 },
151 ),
152 clk: clock.NewFake(),
153 log: blog.NewMock(),
154 }
155
156 for _, tc := range cases {
157 t.Run(fmt.Sprintf("%s %s", tc.method, tc.path), func(t *testing.T) {
158 rw := httptest.NewRecorder()
159 responder.responseTypes.Reset()
160
161 responder.ServeHTTP(rw, &http.Request{
162 Method: tc.method,
163 URL: &url.URL{
164 Path: tc.path,
165 },
166 })
167 if rw.Code != tc.expected {
168 t.Errorf("Incorrect response code: got %d, wanted %d", rw.Code, tc.expected)
169 }
170 if rw.Code == http.StatusOK {
171 test.AssertMetricWithLabelsEquals(
172 t, responder.responseTypes, prometheus.Labels{"type": "Success"}, 1)
173 } else if rw.Code == http.StatusBadRequest {
174 test.AssertMetricWithLabelsEquals(
175 t, responder.responseTypes, prometheus.Labels{"type": "Malformed"}, 1)
176 }
177 })
178 }
179
180 test.AssertMetricWithLabelsEquals(t, responder.responseAges, prometheus.Labels{}, 2)
181 }
182
183 func TestRequestTooBig(t *testing.T) {
184 responder := Responder{
185 Source: testSource{},
186 responseTypes: prometheus.NewCounterVec(
187 prometheus.CounterOpts{
188 Name: "ocspResponses-test",
189 },
190 []string{"type"},
191 ),
192 responseAges: prometheus.NewHistogram(
193 prometheus.HistogramOpts{
194 Name: "ocspAges-test",
195 Buckets: []float64{43200},
196 },
197 ),
198 clk: clock.NewFake(),
199 log: blog.NewMock(),
200 }
201
202 rw := httptest.NewRecorder()
203
204 responder.ServeHTTP(rw, httptest.NewRequest("POST", "/",
205 bytes.NewBuffer([]byte(strings.Repeat("a", 10001)))))
206 expected := 400
207 if rw.Code != expected {
208 t.Errorf("Incorrect response code: got %d, wanted %d", rw.Code, expected)
209 }
210 }
211
212 func TestCacheHeaders(t *testing.T) {
213 source, err := NewMemorySourceFromFile(responseFile, blog.NewMock())
214 if err != nil {
215 t.Fatalf("Error constructing source: %s", err)
216 }
217
218 fc := clock.NewFake()
219 fc.Set(time.Date(2015, 11, 12, 0, 0, 0, 0, time.UTC))
220 responder := Responder{
221 Source: source,
222 responseTypes: prometheus.NewCounterVec(
223 prometheus.CounterOpts{
224 Name: "ocspResponses-test",
225 },
226 []string{"type"},
227 ),
228 responseAges: prometheus.NewHistogram(
229 prometheus.HistogramOpts{
230 Name: "ocspAges-test",
231 Buckets: []float64{43200},
232 },
233 ),
234 clk: fc,
235 log: blog.NewMock(),
236 }
237
238 rw := httptest.NewRecorder()
239 responder.ServeHTTP(rw, &http.Request{
240 Method: "GET",
241 URL: &url.URL{
242 Path: "MEMwQTA/MD0wOzAJBgUrDgMCGgUABBSwLsMRhyg1dJUwnXWk++D57lvgagQU6aQ/7p6l5vLV13lgPJOmLiSOl6oCAhJN",
243 },
244 })
245 if rw.Code != http.StatusOK {
246 t.Errorf("Unexpected HTTP status code %d", rw.Code)
247 }
248 testCases := []struct {
249 header string
250 value string
251 }{
252 {"Last-Modified", "Tue, 20 Oct 2015 00:00:00 UTC"},
253 {"Expires", "Sun, 20 Oct 2030 00:00:00 UTC"},
254 {"Cache-Control", "max-age=471398400, public, no-transform, must-revalidate"},
255 {"Etag", "\"8169FB0843B081A76E9F6F13FD70C8411597BEACF8B182136FFDD19FBD26140A\""},
256 }
257 for _, tc := range testCases {
258 headers, ok := rw.Result().Header[tc.header]
259 if !ok {
260 t.Errorf("Header %s missing from HTTP response", tc.header)
261 continue
262 }
263 if len(headers) != 1 {
264 t.Errorf("Wrong number of headers in HTTP response. Wanted 1, got %d", len(headers))
265 continue
266 }
267 actual := headers[0]
268 if actual != tc.value {
269 t.Errorf("Got header %s: %s. Expected %s", tc.header, actual, tc.value)
270 }
271 }
272
273 rw = httptest.NewRecorder()
274 headers := http.Header{}
275 headers.Add("If-None-Match", "\"8169FB0843B081A76E9F6F13FD70C8411597BEACF8B182136FFDD19FBD26140A\"")
276 responder.ServeHTTP(rw, &http.Request{
277 Method: "GET",
278 URL: &url.URL{
279 Path: "MEMwQTA/MD0wOzAJBgUrDgMCGgUABBSwLsMRhyg1dJUwnXWk++D57lvgagQU6aQ/7p6l5vLV13lgPJOmLiSOl6oCAhJN",
280 },
281 Header: headers,
282 })
283 if rw.Code != http.StatusNotModified {
284 t.Fatalf("Got wrong status code: expected %d, got %d", http.StatusNotModified, rw.Code)
285 }
286 }
287
288 func TestNewSourceFromFile(t *testing.T) {
289 logger := blog.NewMock()
290 _, err := NewMemorySourceFromFile("", logger)
291 if err == nil {
292 t.Fatal("Didn't fail on non-file input")
293 }
294
295
296 _, err = NewMemorySourceFromFile(responseFile, logger)
297 if err != nil {
298 t.Fatal(err)
299 }
300
301
302 _, err = NewMemorySourceFromFile(binResponseFile, logger)
303 if err != nil {
304 t.Fatal(err)
305 }
306
307
308 _, err = NewMemorySourceFromFile(brokenResponseFile, logger)
309 if err != nil {
310 t.Fatal(err)
311 }
312
313
314 _, err = NewMemorySourceFromFile(mixResponseFile, logger)
315 if err != nil {
316 t.Fatal(err)
317 }
318 }
319
View as plain text