@marcel91200 / caddy Mirror · read-only / commits / e9b1d7dc
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: flush HTTP/2 response when ContentLength is unknown (#3561)

* reverse proxy: Support more h2 stream scenarios (#3556) * reverse proxy: add integration test for better h2 stream (#3556) * reverse proxy: adjust comments as francislavoie suggests * link to issue #3556 in the comments

Kevin Lin committed Jul 21, 2020 at 02:14 UTC e9b1d7dcb4cbf85da7fb4cf8c411a4f840a98cf1
3 files changed +214
caddytest/integration/stream_test.go new
+201
@@ -0,0 +1,201 @@
1 +package integration
2 +
3 +import (
4 + "context"
5 + "fmt"
6 + "io"
7 + "io/ioutil"
8 + "net/http"
9 + "net/http/httputil"
10 + "net/url"
11 + "strings"
12 + "testing"
13 + "time"
14 +
15 + "github.com/caddyserver/caddy/v2/caddytest"
16 + "golang.org/x/net/http2"
17 + "golang.org/x/net/http2/h2c"
18 +)
19 +
20 +// (see https://github.com/caddyserver/caddy/issues/3556 for use case)
21 +func TestH2ToH2CStream(t *testing.T) {
22 + tester := caddytest.NewTester(t)
23 + tester.InitServer(`
24 + {
25 + "apps": {
26 + "http": {
27 + "http_port": 9080,
28 + "https_port": 9443,
29 + "servers": {
30 + "srv0": {
31 + "listen": [
32 + ":9443"
33 + ],
34 + "routes": [
35 + {
36 + "handle": [
37 + {
38 + "handler": "reverse_proxy",
39 + "transport": {
40 + "protocol": "http",
41 + "compression": false,
42 + "versions": [
43 + "h2c",
44 + "2"
45 + ]
46 + },
47 + "upstreams": [
48 + {
49 + "dial": "localhost:54321"
50 + }
51 + ]
52 + }
53 + ],
54 + "match": [
55 + {
56 + "path": [
57 + "/tov2ray"
58 + ]
59 + }
60 + ]
61 + }
62 + ],
63 + "tls_connection_policies": [
64 + {
65 + "certificate_selection": {
66 + "any_tag": ["cert0"]
67 + },
68 + "default_sni": "a.caddy.localhost"
69 + }
70 + ]
71 + }
72 + }
73 + },
74 + "tls": {
75 + "certificates": {
76 + "load_files": [
77 + {
78 + "certificate": "/a.caddy.localhost.crt",
79 + "key": "/a.caddy.localhost.key",
80 + "tags": [
81 + "cert0"
82 + ]
83 + }
84 + ]
85 + }
86 + },
87 + "pki": {
88 + "certificate_authorities" : {
89 + "local" : {
90 + "install_trust": false
91 + }
92 + }
93 + }
94 + }
95 + }
96 + `, "json")
97 +
98 + expectedBody := "some data to be echoed"
99 + // start the server
100 + server := testH2ToH2CStreamServeH2C(t)
101 + go server.ListenAndServe()
102 + defer func() {
103 + ctx, cancel := context.WithTimeout(context.Background(), time.Nanosecond)
104 + defer cancel()
105 + server.Shutdown(ctx)
106 + }()
107 +
108 + r, w := io.Pipe()
109 + req := &http.Request{
110 + Method: "PUT",
111 + Body: ioutil.NopCloser(r),
112 + URL: &url.URL{
113 + Scheme: "https",
114 + Host: "127.0.0.1:9443",
115 + Path: "/tov2ray",
116 + },
117 + Proto: "HTTP/2",
118 + ProtoMajor: 2,
119 + ProtoMinor: 0,
120 + Header: make(http.Header),
121 + }
122 + // Disable any compression method from server.
123 + req.Header.Set("Accept-Encoding", "identity")
124 +
125 + resp := tester.AssertResponseCode(req, 200)
126 + if 200 != resp.StatusCode {
127 + return
128 + }
129 + go func() {
130 + fmt.Fprint(w, expectedBody)
131 + w.Close()
132 + }()
133 +
134 + defer resp.Body.Close()
135 + bytes, err := ioutil.ReadAll(resp.Body)
136 + if err != nil {
137 + t.Fatalf("unable to read the response body %s", err)
138 + }
139 +
140 + body := string(bytes)
141 +
142 + if !strings.Contains(body, expectedBody) {
143 + t.Errorf("requesting \"%s\" expected response body \"%s\" but got \"%s\"", req.RequestURI, expectedBody, body)
144 + }
145 + return
146 +}
147 +
148 +func testH2ToH2CStreamServeH2C(t *testing.T) *http.Server {
149 + h2s := &http2.Server{}
150 + handler := http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
151 + rstring, err := httputil.DumpRequest(r, false)
152 + if err == nil {
153 + t.Logf("h2c server received req: %s", rstring)
154 + }
155 + // We only accept HTTP/2!
156 + if r.ProtoMajor != 2 {
157 + t.Error("Not a HTTP/2 request, rejected!")
158 + w.WriteHeader(http.StatusInternalServerError)
159 + return
160 + }
161 +
162 + if r.Host != "127.0.0.1:9443" {
163 + t.Errorf("r.Host doesn't match, %v!", r.Host)
164 + w.WriteHeader(http.StatusNotFound)
165 + return
166 + }
167 +
168 + if !strings.HasPrefix(r.URL.Path, "/tov2ray") {
169 + w.WriteHeader(http.StatusNotFound)
170 + return
171 + }
172 +
173 + w.Header().Set("Cache-Control", "no-store")
174 + w.WriteHeader(200)
175 + if f, ok := w.(http.Flusher); ok {
176 + f.Flush()
177 + }
178 +
179 + buf := make([]byte, 4*1024)
180 +
181 + for {
182 + n, err := r.Body.Read(buf)
183 + if n > 0 {
184 + w.Write(buf[:n])
185 + }
186 +
187 + if err != nil {
188 + if err == io.EOF {
189 + r.Body.Close()
190 + }
191 + break
192 + }
193 + }
194 + })
195 +
196 + server := &http.Server{
197 + Addr: "127.0.0.1:54321",
198 + Handler: h2c.NewHandler(handler, h2s),
199 + }
200 + return server
201 +}
modules/caddyhttp/reverseproxy/reverseproxy.go
+8
@@ -611,6 +611,14 @@ func (h *Handler) reverseProxy(rw http.ResponseWriter, req *http.Request, di Dia
611
612 rw.WriteHeader(res.StatusCode)
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 {
618 + if wf, ok := rw.(http.Flusher); ok {
619 + wf.Flush()
620 + }
621 + }
622 err = h.copyResponse(rw, res.Body, h.flushInterval(req, res))
623 res.Body.Close() // close now, instead of defer, to populate res.Trailer
624 if err != nil {
modules/caddyhttp/reverseproxy/streaming.go
+5
@@ -96,6 +96,11 @@ 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 {
101 + return -1
102 + }
103 +
104 // TODO: more specific cases? e.g. res.ContentLength == -1? (this TODO is from the std lib)
105 return time.Duration(h.FlushInterval)
106 }