@marcel91200 / caddy Mirror · read-only
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 11, 01:00 UTC

master
go 249 lines 8.02 KB
Raw
1 // Copyright 2015 Matthew Holt and The Caddy Authors
2 //
3 // Licensed under the Apache License, Version 2.0 (the "License");
4 // you may not use this file except in compliance with the License.
5 // You may obtain a copy of the License at
6 //
7 // http://www.apache.org/licenses/LICENSE-2.0
8 //
9 // Unless required by applicable law or agreed to in writing, software
10 // distributed under the License is distributed on an "AS IS" BASIS,
11 // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12 // See the License for the specific language governing permissions and
13 // limitations under the License.
14
15 package encode_test
16
17 import (
18 "io"
19 "net/http"
20 "net/http/httptest"
21 "strings"
22 "testing"
23
24 "github.com/caddyserver/caddy/v2"
25 "github.com/caddyserver/caddy/v2/caddyconfig"
26 "github.com/caddyserver/caddy/v2/modules/caddyhttp"
27 "github.com/caddyserver/caddy/v2/modules/caddyhttp/encode"
28 caddygzip "github.com/caddyserver/caddy/v2/modules/caddyhttp/encode/gzip"
29 )
30
31 // recordingWriter records the moment WriteHeader reaches the underlying
32 // writer, so a test can distinguish "headers flushed to the client" from
33 // "headers still buffered inside the encoder".
34 type recordingWriter struct {
35 http.ResponseWriter
36 wroteHeader bool
37 status int
38 }
39
40 func (rw *recordingWriter) WriteHeader(status int) {
41 if !rw.wroteHeader {
42 rw.wroteHeader = true
43 rw.status = status
44 }
45 rw.ResponseWriter.WriteHeader(status)
46 }
47
48 func (rw *recordingWriter) Flush() {
49 if f, ok := rw.ResponseWriter.(http.Flusher); ok {
50 f.Flush()
51 }
52 }
53
54 func (rw *recordingWriter) ReadFrom(r io.Reader) (int64, error) {
55 return io.Copy(rw.ResponseWriter, r)
56 }
57
58 type readerOnly struct {
59 io.Reader
60 }
61
62 func newSSEEncodeHandler(t *testing.T) *encode.Encode {
63 t.Helper()
64 enc := &encode.Encode{
65 EncodingsRaw: caddy.ModuleMap{
66 "gzip": caddyconfig.JSON(caddygzip.Gzip{}, nil),
67 },
68 Prefer: []string{"gzip"},
69 // A large minimum_length means a normal small response would be
70 // buffered (its header withheld) until enough bytes arrive; the SSE
71 // path must bypass this so the handshake reaches the client.
72 MinLength: 4096,
73 }
74 ctx, cancel := caddy.NewContext(caddy.Context{Context: t.Context()})
75 t.Cleanup(cancel)
76 if err := enc.Provision(ctx); err != nil {
77 t.Fatalf("Provision() error = %v", err)
78 }
79 if err := enc.Validate(); err != nil {
80 t.Fatalf("Validate() error = %v", err)
81 }
82 return enc
83 }
84
85 // An SSE upstream typically writes headers and flushes to establish the
86 // event stream before any event body is available. The encode middleware
87 // must let those headers reach the client immediately rather than holding
88 // them for minimum_length content sniffing. See #6293.
89 func TestSSEHeadersFlushedBeforeBody(t *testing.T) {
90 enc := newSSEEncodeHandler(t)
91 r := httptest.NewRequest(http.MethodGet, "/", nil)
92 r.Header.Set("Accept-Encoding", "gzip")
93 baseRec := httptest.NewRecorder()
94 rec := &recordingWriter{ResponseWriter: baseRec}
95
96 next := caddyhttp.HandlerFunc(func(w http.ResponseWriter, _ *http.Request) error {
97 w.Header().Set("Content-Type", "text/event-stream")
98 w.WriteHeader(http.StatusOK)
99 // Flush the way a real SSE handler does — through the response
100 // controller (encode's writer implements FlushError, not Flush).
101 if err := http.NewResponseController(w).Flush(); err != nil {
102 t.Errorf("flush failed: %v", err)
103 }
104 // Before any event body is written, the client must already have the
105 // headers AND the flush must have reached the underlying writer.
106 if !rec.wroteHeader {
107 t.Error("SSE response headers were not written to the client before the body")
108 }
109 if rec.status != http.StatusOK {
110 t.Errorf("underlying status = %d, want 200", rec.status)
111 }
112 if !baseRec.Flushed {
113 t.Error("underlying ResponseRecorder was not flushed for the SSE handshake")
114 }
115 return nil
116 })
117
118 if err := enc.ServeHTTP(rec, r, next); err != nil {
119 t.Fatalf("ServeHTTP() error = %v", err)
120 }
121 }
122
123 // A normal (non-SSE) small response is still allowed to buffer its header
124 // for content sniffing — the SSE change must not force every response to
125 // flush its header early. This guards the scope of the fix.
126 func TestNonSSESmallResponseStillBuffersHeader(t *testing.T) {
127 enc := newSSEEncodeHandler(t)
128 r := httptest.NewRequest(http.MethodGet, "/", nil)
129 r.Header.Set("Accept-Encoding", "gzip")
130 baseRec := httptest.NewRecorder()
131 rec := &recordingWriter{ResponseWriter: baseRec}
132
133 next := caddyhttp.HandlerFunc(func(w http.ResponseWriter, _ *http.Request) error {
134 w.Header().Set("Content-Type", "text/plain")
135 w.WriteHeader(http.StatusOK)
136 if rec.wroteHeader {
137 t.Error("non-SSE response flushed its header early; SSE bypass leaked to normal responses")
138 }
139 return nil
140 })
141
142 if err := enc.ServeHTTP(rec, r, next); err != nil {
143 t.Fatalf("ServeHTTP() error = %v", err)
144 }
145 }
146
147 // A response marked with the Incremental header field (RFC 10036) needs the
148 // same treatment as SSE: its headers must reach the client before the body,
149 // since the sender may not produce content for a long time.
150 func TestIncrementalHeadersFlushedBeforeBody(t *testing.T) {
151 enc := newSSEEncodeHandler(t)
152 r := httptest.NewRequest(http.MethodGet, "/", nil)
153 r.Header.Set("Accept-Encoding", "gzip")
154 baseRec := httptest.NewRecorder()
155 rec := &recordingWriter{ResponseWriter: baseRec}
156
157 next := caddyhttp.HandlerFunc(func(w http.ResponseWriter, _ *http.Request) error {
158 w.Header().Set("Content-Type", "application/json")
159 w.Header().Set("Incremental", "?1")
160 w.WriteHeader(http.StatusOK)
161
162 if !rec.wroteHeader {
163 t.Error("incremental response headers were not written to the client before the body")
164 }
165 if rec.status != http.StatusOK {
166 t.Errorf("underlying status = %d, want 200", rec.status)
167 }
168 if !baseRec.Flushed {
169 t.Error("incremental response headers were not flushed to the client")
170 }
171 return nil
172 })
173
174 if err := enc.ServeHTTP(rec, r, next); err != nil {
175 t.Fatalf("ServeHTTP() error = %v", err)
176 }
177 }
178
179 func TestIncrementalImplicitWriteFlushes(t *testing.T) {
180 enc := newSSEEncodeHandler(t)
181 r := httptest.NewRequest(http.MethodGet, "/", nil)
182 r.Header.Set("Accept-Encoding", "gzip")
183 baseRec := httptest.NewRecorder()
184 rec := &recordingWriter{ResponseWriter: baseRec}
185
186 next := caddyhttp.HandlerFunc(func(w http.ResponseWriter, _ *http.Request) error {
187 w.Header().Set("Content-Type", "application/json")
188 w.Header().Set("Incremental", "?1")
189 if _, err := w.Write([]byte("x")); err != nil {
190 return err
191 }
192 if !baseRec.Flushed {
193 t.Error("incremental response write was not flushed to the client")
194 }
195 return nil
196 })
197
198 if err := enc.ServeHTTP(rec, r, next); err != nil {
199 t.Fatalf("ServeHTTP() error = %v", err)
200 }
201 }
202
203 func TestIncrementalReadFromFlushes(t *testing.T) {
204 enc := newSSEEncodeHandler(t)
205 r := httptest.NewRequest(http.MethodGet, "/", nil)
206 r.Header.Set("Accept-Encoding", "gzip")
207 baseRec := httptest.NewRecorder()
208 rec := &recordingWriter{ResponseWriter: baseRec}
209
210 next := caddyhttp.HandlerFunc(func(w http.ResponseWriter, _ *http.Request) error {
211 w.Header().Set("Content-Type", "application/json")
212 w.Header().Set("Incremental", "?1")
213 if _, err := io.Copy(w, readerOnly{Reader: strings.NewReader("x")}); err != nil {
214 return err
215 }
216 if !baseRec.Flushed {
217 t.Error("incremental ReadFrom data was not flushed to the client")
218 }
219 return nil
220 })
221
222 if err := enc.ServeHTTP(rec, r, next); err != nil {
223 t.Fatalf("ServeHTTP() error = %v", err)
224 }
225 }
226
227 func TestIncrementalInformationalResponseDoesNotAffectFinalResponse(t *testing.T) {
228 enc := newSSEEncodeHandler(t)
229 r := httptest.NewRequest(http.MethodGet, "/", nil)
230 r.Header.Set("Accept-Encoding", "gzip")
231 baseRec := httptest.NewRecorder()
232 rec := &recordingWriter{ResponseWriter: baseRec}
233
234 next := caddyhttp.HandlerFunc(func(w http.ResponseWriter, _ *http.Request) error {
235 w.Header().Set("Incremental", "?1")
236 w.WriteHeader(http.StatusEarlyHints)
237 w.Header().Del("Incremental")
238 w.Header().Set("Content-Type", "text/plain")
239 w.WriteHeader(http.StatusOK)
240 if baseRec.Flushed {
241 t.Error("informational Incremental field forced the final response to flush")
242 }
243 return nil
244 })
245
246 if err := enc.ServeHTTP(rec, r, next); err != nil {
247 t.Fatalf("ServeHTTP() error = %v", err)
248 }
249 }