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
}