Fast and extensible multi-platform HTTP/1-2-3 web server with automatic HTTPS
Mirrored from https://github.com/Slazee/caddy.git
Synced Nov 14, 05:15 UTC
reverse_proxy: fix bidirectional streams with encodings (fix #3606) (#3620)
* reverse_proxy: fix bi-h2stream breaking gzip encode handle(#3606). * reverse_proxy: check http version of both sides to avoid affecting non-h2 upstream. * Minor cleanup; apply review suggestions Co-authored-by: Matthew Holt <mholt@users.noreply.github.com>
Kevin Lin committed
Aug 4, 2020 at 10:50 UTC
904f149e5b2f0ae9eff09dacb4f1ec41c4a76298
3 files changed
+259
-5
caddytest/integration/stream_test.go
+236
@@ -1,7 +1,9 @@
1
package integration
2
3
import (
4
+ "compress/gzip"
5
"context"
6
+ "crypto/rand"
7
"fmt"
8
"io"
9
"io/ioutil"
@@ -199,3 +201,237 @@ func testH2ToH2CStreamServeH2C(t *testing.T) *http.Server {
201
}
202
return server
203
}
204
+
205
+// (see https://github.com/caddyserver/caddy/issues/3606 for use case)
206
+func TestH2ToH1ChunkedResponse(t *testing.T) {
207
+ tester := caddytest.NewTester(t)
208
+ tester.InitServer(`
209
+{
210
+ "logging": {
211
+ "logs": {
212
+ "default": {
213
+ "level": "DEBUG"
214
+ }
215
+ }
216
+ },
217
+ "apps": {
218
+ "http": {
219
+ "http_port": 9080,
220
+ "https_port": 9443,
221
+ "servers": {
222
+ "srv0": {
223
+ "listen": [
224
+ ":9443"
225
+ ],
226
+ "routes": [
227
+ {
228
+ "handle": [
229
+ {
230
+ "handler": "subroute",
231
+ "routes": [
232
+ {
233
+ "handle": [
234
+ {
235
+ "encodings": {
236
+ "gzip": {}
237
+ },
238
+ "handler": "encode"
239
+ }
240
+ ]
241
+ },
242
+ {
243
+ "handle": [
244
+ {
245
+ "handler": "reverse_proxy",
246
+ "upstreams": [
247
+ {
248
+ "dial": "localhost:54321"
249
+ }
250
+ ]
251
+ }
252
+ ],
253
+ "match": [
254
+ {
255
+ "path": [
256
+ "/tov2ray"
257
+ ]
258
+ }
259
+ ]
260
+ }
261
+ ]
262
+ }
263
+ ],
264
+ "terminal": true
265
+ }
266
+ ],
267
+ "tls_connection_policies": [
268
+ {
269
+ "certificate_selection": {
270
+ "any_tag": [
271
+ "cert0"
272
+ ]
273
+ },
274
+ "default_sni": "a.caddy.localhost"
275
+ }
276
+ ]
277
+ }
278
+ }
279
+ },
280
+ "tls": {
281
+ "certificates": {
282
+ "load_files": [
283
+ {
284
+ "certificate": "/a.caddy.localhost.crt",
285
+ "key": "/a.caddy.localhost.key",
286
+ "tags": [
287
+ "cert0"
288
+ ]
289
+ }
290
+ ]
291
+ }
292
+ },
293
+ "pki": {
294
+ "certificate_authorities": {
295
+ "local": {
296
+ "install_trust": false
297
+ }
298
+ }
299
+ }
300
+ }
301
+}
302
+ `, "json")
303
+
304
+ // need a large body here to trigger caddy's compression, larger than gzip.miniLength
305
+ expectedBody, err := GenerateRandomString(1024)
306
+ if err != nil {
307
+ t.Fatalf("generate expected body failed, err: %s", err)
308
+ }
309
+
310
+ // start the server
311
+ server := testH2ToH1ChunkedResponseServeH1(t)
312
+ go server.ListenAndServe()
313
+ defer func() {
314
+ ctx, cancel := context.WithTimeout(context.Background(), time.Nanosecond)
315
+ defer cancel()
316
+ server.Shutdown(ctx)
317
+ }()
318
+
319
+ r, w := io.Pipe()
320
+ req := &http.Request{
321
+ Method: "PUT",
322
+ Body: ioutil.NopCloser(r),
323
+ URL: &url.URL{
324
+ Scheme: "https",
325
+ Host: "127.0.0.1:9443",
326
+ Path: "/tov2ray",
327
+ },
328
+ Proto: "HTTP/2",
329
+ ProtoMajor: 2,
330
+ ProtoMinor: 0,
331
+ Header: make(http.Header),
332
+ }
333
+ // underlying transport will automaticlly add gzip
334
+ // req.Header.Set("Accept-Encoding", "gzip")
335
+ go func() {
336
+ fmt.Fprint(w, expectedBody)
337
+ w.Close()
338
+ }()
339
+ resp := tester.AssertResponseCode(req, 200)
340
+ if 200 != resp.StatusCode {
341
+ return
342
+ }
343
+
344
+ defer resp.Body.Close()
345
+ bytes, err := ioutil.ReadAll(resp.Body)
346
+ if err != nil {
347
+ t.Fatalf("unable to read the response body %s", err)
348
+ }
349
+
350
+ body := string(bytes)
351
+
352
+ if body != expectedBody {
353
+ t.Errorf("requesting \"%s\" expected response body \"%s\" but got \"%s\"", req.RequestURI, expectedBody, body)
354
+ }
355
+ return
356
+}
357
+
358
+func testH2ToH1ChunkedResponseServeH1(t *testing.T) *http.Server {
359
+ handler := http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
360
+
361
+ if r.Host != "127.0.0.1:9443" {
362
+ t.Errorf("r.Host doesn't match, %v!", r.Host)
363
+ w.WriteHeader(http.StatusNotFound)
364
+ return
365
+ }
366
+
367
+ if !strings.HasPrefix(r.URL.Path, "/tov2ray") {
368
+ w.WriteHeader(http.StatusNotFound)
369
+ return
370
+ }
371
+
372
+ defer r.Body.Close()
373
+ bytes, err := ioutil.ReadAll(r.Body)
374
+ if err != nil {
375
+ t.Fatalf("unable to read the response body %s", err)
376
+ }
377
+
378
+ n := len(bytes)
379
+
380
+ var writer io.Writer
381
+ if strings.Contains(r.Header.Get("Accept-Encoding"), "gzip") {
382
+ gw, err := gzip.NewWriterLevel(w, 5)
383
+ if err != nil {
384
+ t.Error("can't return gzip data")
385
+ w.WriteHeader(http.StatusInternalServerError)
386
+ return
387
+ }
388
+ defer gw.Close()
389
+ writer = gw
390
+ w.Header().Set("Content-Encoding", "gzip")
391
+ w.Header().Del("Content-Length")
392
+ w.WriteHeader(200)
393
+ } else {
394
+ writer = w
395
+ }
396
+ if n > 0 {
397
+ writer.Write(bytes[:])
398
+ }
399
+ })
400
+
401
+ server := &http.Server{
402
+ Addr: "127.0.0.1:54321",
403
+ Handler: handler,
404
+ }
405
+ return server
406
+}
407
+
408
+// GenerateRandomBytes returns securely generated random bytes.
409
+// It will return an error if the system's secure random
410
+// number generator fails to function correctly, in which
411
+// case the caller should not continue.
412
+func GenerateRandomBytes(n int) ([]byte, error) {
413
+ b := make([]byte, n)
414
+ _, err := rand.Read(b)
415
+ // Note that err == nil only if we read len(b) bytes.
416
+ if err != nil {
417
+ return nil, err
418
+ }
419
+
420
+ return b, nil
421
+}
422
+
423
+// GenerateRandomString returns a securely generated random string.
424
+// It will return an error if the system's secure random
425
+// number generator fails to function correctly, in which
426
+// case the caller should not continue.
427
+func GenerateRandomString(n int) (string, error) {
428
+ const letters = "0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz-"
429
+ bytes, err := GenerateRandomBytes(n)
430
+ if err != nil {
431
+ return "", err
432
+ }
433
+ for i, b := range bytes {
434
+ bytes[i] = letters[b%byte(len(letters))]
435
+ }
436
+ return string(bytes), nil
437
+}
modules/caddyhttp/reverseproxy/reverseproxy.go
+2
-2
@@ -613,8 +613,8 @@ func (h *Handler) reverseProxy(rw http.ResponseWriter, req *http.Request, di Dia
613
614
// some apps need the response headers before starting to stream content with http2,
615
// so it's important to explicitly flush the headers to the client before streaming the data.
616
- // (see https://github.com/caddyserver/caddy/issues/3556 for use case)
617
- if req.ProtoMajor == 2 && res.ContentLength == -1 {
616
+ // (see https://github.com/caddyserver/caddy/issues/3556 for use case and nuances)
617
+ if h.isBidirectionalStream(req, res) {
618
if wf, ok := rw.(http.Flusher); ok {
619
wf.Flush()
620
}
modules/caddyhttp/reverseproxy/streaming.go
+21
-3
@@ -96,15 +96,33 @@ func (h Handler) flushInterval(req *http.Request, res *http.Response) time.Durat
96
return -1 // negative means immediately
97
}
98
99
- // for h2 and h2c upstream streaming data to client (issue #3556)
100
- if req.ProtoMajor == 2 && res.ContentLength == -1 {
99
+ // for h2 and h2c upstream streaming data to client (issues #3556 and #3606)
100
+ if h.isBidirectionalStream(req, res) {
101
return -1
102
}
103
104
- // TODO: more specific cases? e.g. res.ContentLength == -1? (this TODO is from the std lib)
104
+ // TODO: more specific cases? e.g. res.ContentLength == -1? (this TODO is from the std lib, but
105
+ // strangely similar to our isBidirectionalStream function that we implemented ourselves)
106
return time.Duration(h.FlushInterval)
107
}
108
109
+// isBidirectionalStream returns whether we should work in bi-directional stream mode.
110
+//
111
+// See https://github.com/caddyserver/caddy/pull/3620 for discussion of nuances.
112
+func (h Handler) isBidirectionalStream(req *http.Request, res *http.Response) bool {
113
+ // We have to check the encoding here; only flush headers with identity encoding.
114
+ // Non-identity encoding might combine with "encode" directive, and in that case,
115
+ // if body size larger than enc.MinLength, upper level encode handle might have
116
+ // Content-Encoding header to write.
117
+ // (see https://github.com/caddyserver/caddy/issues/3606 for use case)
118
+ ae := req.Header.Get("Accept-Encoding")
119
+
120
+ return req.ProtoMajor == 2 &&
121
+ res.ProtoMajor == 2 &&
122
+ res.ContentLength == -1 &&
123
+ (ae == "identity" || ae == "")
124
+}
125
+
126
func (h Handler) copyResponse(dst io.Writer, src io.Reader, flushInterval time.Duration) error {
127
if flushInterval != 0 {
128
if wf, ok := dst.(writeFlusher); ok {