@marcel91200 / caddy Mirror · read-only / commits / 904f149e
Fork 0

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 {