cannam@147
|
1 // Copyright (c) 2017 Sandstorm Development Group, Inc. and contributors
|
cannam@147
|
2 // Licensed under the MIT License:
|
cannam@147
|
3 //
|
cannam@147
|
4 // Permission is hereby granted, free of charge, to any person obtaining a copy
|
cannam@147
|
5 // of this software and associated documentation files (the "Software"), to deal
|
cannam@147
|
6 // in the Software without restriction, including without limitation the rights
|
cannam@147
|
7 // to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
cannam@147
|
8 // copies of the Software, and to permit persons to whom the Software is
|
cannam@147
|
9 // furnished to do so, subject to the following conditions:
|
cannam@147
|
10 //
|
cannam@147
|
11 // The above copyright notice and this permission notice shall be included in
|
cannam@147
|
12 // all copies or substantial portions of the Software.
|
cannam@147
|
13 //
|
cannam@147
|
14 // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
cannam@147
|
15 // IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
cannam@147
|
16 // FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
cannam@147
|
17 // AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
cannam@147
|
18 // LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
cannam@147
|
19 // OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
|
cannam@147
|
20 // THE SOFTWARE.
|
cannam@147
|
21
|
cannam@147
|
22 #include "http.h"
|
cannam@147
|
23 #include <kj/debug.h>
|
cannam@147
|
24 #include <kj/test.h>
|
cannam@147
|
25 #include <map>
|
cannam@147
|
26
|
cannam@147
|
27 namespace kj {
|
cannam@147
|
28 namespace {
|
cannam@147
|
29
|
cannam@147
|
30 KJ_TEST("HttpMethod parse / stringify") {
|
cannam@147
|
31 #define TRY(name) \
|
cannam@147
|
32 KJ_EXPECT(kj::str(HttpMethod::name) == #name); \
|
cannam@147
|
33 KJ_IF_MAYBE(parsed, tryParseHttpMethod(#name)) { \
|
cannam@147
|
34 KJ_EXPECT(*parsed == HttpMethod::name); \
|
cannam@147
|
35 } else { \
|
cannam@147
|
36 KJ_FAIL_EXPECT("couldn't parse \"" #name "\" as HttpMethod"); \
|
cannam@147
|
37 }
|
cannam@147
|
38
|
cannam@147
|
39 KJ_HTTP_FOR_EACH_METHOD(TRY)
|
cannam@147
|
40 #undef TRY
|
cannam@147
|
41
|
cannam@147
|
42 KJ_EXPECT(tryParseHttpMethod("FOO") == nullptr);
|
cannam@147
|
43 KJ_EXPECT(tryParseHttpMethod("") == nullptr);
|
cannam@147
|
44 KJ_EXPECT(tryParseHttpMethod("G") == nullptr);
|
cannam@147
|
45 KJ_EXPECT(tryParseHttpMethod("GE") == nullptr);
|
cannam@147
|
46 KJ_EXPECT(tryParseHttpMethod("GET ") == nullptr);
|
cannam@147
|
47 KJ_EXPECT(tryParseHttpMethod("get") == nullptr);
|
cannam@147
|
48 }
|
cannam@147
|
49
|
cannam@147
|
50 KJ_TEST("HttpHeaderTable") {
|
cannam@147
|
51 HttpHeaderTable::Builder builder;
|
cannam@147
|
52
|
cannam@147
|
53 auto host = builder.add("Host");
|
cannam@147
|
54 auto host2 = builder.add("hOsT");
|
cannam@147
|
55 auto fooBar = builder.add("Foo-Bar");
|
cannam@147
|
56 auto bazQux = builder.add("baz-qux");
|
cannam@147
|
57 auto bazQux2 = builder.add("Baz-Qux");
|
cannam@147
|
58
|
cannam@147
|
59 auto table = builder.build();
|
cannam@147
|
60
|
cannam@147
|
61 uint builtinHeaderCount = 0;
|
cannam@147
|
62 #define INCREMENT(id, name) ++builtinHeaderCount;
|
cannam@147
|
63 KJ_HTTP_FOR_EACH_BUILTIN_HEADER(INCREMENT)
|
cannam@147
|
64 #undef INCREMENT
|
cannam@147
|
65
|
cannam@147
|
66 KJ_EXPECT(table->idCount() == builtinHeaderCount + 2);
|
cannam@147
|
67
|
cannam@147
|
68 KJ_EXPECT(host == HttpHeaderId::HOST);
|
cannam@147
|
69 KJ_EXPECT(host != HttpHeaderId::DATE);
|
cannam@147
|
70 KJ_EXPECT(host2 == host);
|
cannam@147
|
71
|
cannam@147
|
72 KJ_EXPECT(host != fooBar);
|
cannam@147
|
73 KJ_EXPECT(host != bazQux);
|
cannam@147
|
74 KJ_EXPECT(fooBar != bazQux);
|
cannam@147
|
75 KJ_EXPECT(bazQux == bazQux2);
|
cannam@147
|
76
|
cannam@147
|
77 KJ_EXPECT(kj::str(host) == "Host");
|
cannam@147
|
78 KJ_EXPECT(kj::str(host2) == "Host");
|
cannam@147
|
79 KJ_EXPECT(kj::str(fooBar) == "Foo-Bar");
|
cannam@147
|
80 KJ_EXPECT(kj::str(bazQux) == "baz-qux");
|
cannam@147
|
81 KJ_EXPECT(kj::str(HttpHeaderId::HOST) == "Host");
|
cannam@147
|
82
|
cannam@147
|
83 KJ_EXPECT(table->idToString(HttpHeaderId::DATE) == "Date");
|
cannam@147
|
84 KJ_EXPECT(table->idToString(fooBar) == "Foo-Bar");
|
cannam@147
|
85
|
cannam@147
|
86 KJ_EXPECT(KJ_ASSERT_NONNULL(table->stringToId("Date")) == HttpHeaderId::DATE);
|
cannam@147
|
87 KJ_EXPECT(KJ_ASSERT_NONNULL(table->stringToId("dATE")) == HttpHeaderId::DATE);
|
cannam@147
|
88 KJ_EXPECT(KJ_ASSERT_NONNULL(table->stringToId("Foo-Bar")) == fooBar);
|
cannam@147
|
89 KJ_EXPECT(KJ_ASSERT_NONNULL(table->stringToId("foo-BAR")) == fooBar);
|
cannam@147
|
90 KJ_EXPECT(table->stringToId("foobar") == nullptr);
|
cannam@147
|
91 KJ_EXPECT(table->stringToId("barfoo") == nullptr);
|
cannam@147
|
92 }
|
cannam@147
|
93
|
cannam@147
|
94 KJ_TEST("HttpHeaders::parseRequest") {
|
cannam@147
|
95 HttpHeaderTable::Builder builder;
|
cannam@147
|
96
|
cannam@147
|
97 auto fooBar = builder.add("Foo-Bar");
|
cannam@147
|
98 auto bazQux = builder.add("baz-qux");
|
cannam@147
|
99
|
cannam@147
|
100 auto table = builder.build();
|
cannam@147
|
101
|
cannam@147
|
102 HttpHeaders headers(*table);
|
cannam@147
|
103 auto text = kj::heapString(
|
cannam@147
|
104 "POST /some/path \t HTTP/1.1\r\n"
|
cannam@147
|
105 "Foo-BaR: Baz\r\n"
|
cannam@147
|
106 "Host: example.com\r\n"
|
cannam@147
|
107 "Content-Length: 123\r\n"
|
cannam@147
|
108 "DATE: early\r\n"
|
cannam@147
|
109 "other-Header: yep\r\n"
|
cannam@147
|
110 "\r\n");
|
cannam@147
|
111 auto result = KJ_ASSERT_NONNULL(headers.tryParseRequest(text.asArray()));
|
cannam@147
|
112
|
cannam@147
|
113 KJ_EXPECT(result.method == HttpMethod::POST);
|
cannam@147
|
114 KJ_EXPECT(result.url == "/some/path");
|
cannam@147
|
115 KJ_EXPECT(KJ_ASSERT_NONNULL(headers.get(HttpHeaderId::HOST)) == "example.com");
|
cannam@147
|
116 KJ_EXPECT(KJ_ASSERT_NONNULL(headers.get(HttpHeaderId::DATE)) == "early");
|
cannam@147
|
117 KJ_EXPECT(KJ_ASSERT_NONNULL(headers.get(fooBar)) == "Baz");
|
cannam@147
|
118 KJ_EXPECT(headers.get(bazQux) == nullptr);
|
cannam@147
|
119 KJ_EXPECT(headers.get(HttpHeaderId::CONTENT_TYPE) == nullptr);
|
cannam@147
|
120 KJ_EXPECT(result.connectionHeaders.contentLength == "123");
|
cannam@147
|
121 KJ_EXPECT(result.connectionHeaders.transferEncoding == nullptr);
|
cannam@147
|
122
|
cannam@147
|
123 std::map<kj::StringPtr, kj::StringPtr> unpackedHeaders;
|
cannam@147
|
124 headers.forEach([&](kj::StringPtr name, kj::StringPtr value) {
|
cannam@147
|
125 KJ_EXPECT(unpackedHeaders.insert(std::make_pair(name, value)).second);
|
cannam@147
|
126 });
|
cannam@147
|
127 KJ_EXPECT(unpackedHeaders.size() == 4);
|
cannam@147
|
128 KJ_EXPECT(unpackedHeaders["Host"] == "example.com");
|
cannam@147
|
129 KJ_EXPECT(unpackedHeaders["Date"] == "early");
|
cannam@147
|
130 KJ_EXPECT(unpackedHeaders["Foo-Bar"] == "Baz");
|
cannam@147
|
131 KJ_EXPECT(unpackedHeaders["other-Header"] == "yep");
|
cannam@147
|
132
|
cannam@147
|
133 KJ_EXPECT(headers.serializeRequest(result.method, result.url, result.connectionHeaders) ==
|
cannam@147
|
134 "POST /some/path HTTP/1.1\r\n"
|
cannam@147
|
135 "Content-Length: 123\r\n"
|
cannam@147
|
136 "Host: example.com\r\n"
|
cannam@147
|
137 "Date: early\r\n"
|
cannam@147
|
138 "Foo-Bar: Baz\r\n"
|
cannam@147
|
139 "other-Header: yep\r\n"
|
cannam@147
|
140 "\r\n");
|
cannam@147
|
141 }
|
cannam@147
|
142
|
cannam@147
|
143 KJ_TEST("HttpHeaders::parseResponse") {
|
cannam@147
|
144 HttpHeaderTable::Builder builder;
|
cannam@147
|
145
|
cannam@147
|
146 auto fooBar = builder.add("Foo-Bar");
|
cannam@147
|
147 auto bazQux = builder.add("baz-qux");
|
cannam@147
|
148
|
cannam@147
|
149 auto table = builder.build();
|
cannam@147
|
150
|
cannam@147
|
151 HttpHeaders headers(*table);
|
cannam@147
|
152 auto text = kj::heapString(
|
cannam@147
|
153 "HTTP/1.1\t\t 418\t I'm a teapot\r\n"
|
cannam@147
|
154 "Foo-BaR: Baz\r\n"
|
cannam@147
|
155 "Host: example.com\r\n"
|
cannam@147
|
156 "Content-Length: 123\r\n"
|
cannam@147
|
157 "DATE: early\r\n"
|
cannam@147
|
158 "other-Header: yep\r\n"
|
cannam@147
|
159 "\r\n");
|
cannam@147
|
160 auto result = KJ_ASSERT_NONNULL(headers.tryParseResponse(text.asArray()));
|
cannam@147
|
161
|
cannam@147
|
162 KJ_EXPECT(result.statusCode == 418);
|
cannam@147
|
163 KJ_EXPECT(result.statusText == "I'm a teapot");
|
cannam@147
|
164 KJ_EXPECT(KJ_ASSERT_NONNULL(headers.get(HttpHeaderId::HOST)) == "example.com");
|
cannam@147
|
165 KJ_EXPECT(KJ_ASSERT_NONNULL(headers.get(HttpHeaderId::DATE)) == "early");
|
cannam@147
|
166 KJ_EXPECT(KJ_ASSERT_NONNULL(headers.get(fooBar)) == "Baz");
|
cannam@147
|
167 KJ_EXPECT(headers.get(bazQux) == nullptr);
|
cannam@147
|
168 KJ_EXPECT(headers.get(HttpHeaderId::CONTENT_TYPE) == nullptr);
|
cannam@147
|
169 KJ_EXPECT(result.connectionHeaders.contentLength == "123");
|
cannam@147
|
170 KJ_EXPECT(result.connectionHeaders.transferEncoding == nullptr);
|
cannam@147
|
171
|
cannam@147
|
172 std::map<kj::StringPtr, kj::StringPtr> unpackedHeaders;
|
cannam@147
|
173 headers.forEach([&](kj::StringPtr name, kj::StringPtr value) {
|
cannam@147
|
174 KJ_EXPECT(unpackedHeaders.insert(std::make_pair(name, value)).second);
|
cannam@147
|
175 });
|
cannam@147
|
176 KJ_EXPECT(unpackedHeaders.size() == 4);
|
cannam@147
|
177 KJ_EXPECT(unpackedHeaders["Host"] == "example.com");
|
cannam@147
|
178 KJ_EXPECT(unpackedHeaders["Date"] == "early");
|
cannam@147
|
179 KJ_EXPECT(unpackedHeaders["Foo-Bar"] == "Baz");
|
cannam@147
|
180 KJ_EXPECT(unpackedHeaders["other-Header"] == "yep");
|
cannam@147
|
181
|
cannam@147
|
182 KJ_EXPECT(headers.serializeResponse(
|
cannam@147
|
183 result.statusCode, result.statusText, result.connectionHeaders) ==
|
cannam@147
|
184 "HTTP/1.1 418 I'm a teapot\r\n"
|
cannam@147
|
185 "Content-Length: 123\r\n"
|
cannam@147
|
186 "Host: example.com\r\n"
|
cannam@147
|
187 "Date: early\r\n"
|
cannam@147
|
188 "Foo-Bar: Baz\r\n"
|
cannam@147
|
189 "other-Header: yep\r\n"
|
cannam@147
|
190 "\r\n");
|
cannam@147
|
191 }
|
cannam@147
|
192
|
cannam@147
|
193 KJ_TEST("HttpHeaders parse invalid") {
|
cannam@147
|
194 auto table = HttpHeaderTable::Builder().build();
|
cannam@147
|
195 HttpHeaders headers(*table);
|
cannam@147
|
196
|
cannam@147
|
197 // NUL byte in request.
|
cannam@147
|
198 KJ_EXPECT(headers.tryParseRequest(kj::heapString(
|
cannam@147
|
199 "POST \0 /some/path \t HTTP/1.1\r\n"
|
cannam@147
|
200 "Foo-BaR: Baz\r\n"
|
cannam@147
|
201 "Host: example.com\r\n"
|
cannam@147
|
202 "DATE: early\r\n"
|
cannam@147
|
203 "other-Header: yep\r\n"
|
cannam@147
|
204 "\r\n")) == nullptr);
|
cannam@147
|
205
|
cannam@147
|
206 // Control character in header name.
|
cannam@147
|
207 KJ_EXPECT(headers.tryParseRequest(kj::heapString(
|
cannam@147
|
208 "POST /some/path \t HTTP/1.1\r\n"
|
cannam@147
|
209 "Foo-BaR: Baz\r\n"
|
cannam@147
|
210 "Cont\001ent-Length: 123\r\n"
|
cannam@147
|
211 "DATE: early\r\n"
|
cannam@147
|
212 "other-Header: yep\r\n"
|
cannam@147
|
213 "\r\n")) == nullptr);
|
cannam@147
|
214
|
cannam@147
|
215 // Separator character in header name.
|
cannam@147
|
216 KJ_EXPECT(headers.tryParseRequest(kj::heapString(
|
cannam@147
|
217 "POST /some/path \t HTTP/1.1\r\n"
|
cannam@147
|
218 "Foo-BaR: Baz\r\n"
|
cannam@147
|
219 "Host: example.com\r\n"
|
cannam@147
|
220 "DATE/: early\r\n"
|
cannam@147
|
221 "other-Header: yep\r\n"
|
cannam@147
|
222 "\r\n")) == nullptr);
|
cannam@147
|
223
|
cannam@147
|
224 // Response status code not numeric.
|
cannam@147
|
225 KJ_EXPECT(headers.tryParseResponse(kj::heapString(
|
cannam@147
|
226 "HTTP/1.1\t\t abc\t I'm a teapot\r\n"
|
cannam@147
|
227 "Foo-BaR: Baz\r\n"
|
cannam@147
|
228 "Host: example.com\r\n"
|
cannam@147
|
229 "DATE: early\r\n"
|
cannam@147
|
230 "other-Header: yep\r\n"
|
cannam@147
|
231 "\r\n")) == nullptr);
|
cannam@147
|
232 }
|
cannam@147
|
233
|
cannam@147
|
234 // =======================================================================================
|
cannam@147
|
235
|
cannam@147
|
236 class ReadFragmenter final: public kj::AsyncIoStream {
|
cannam@147
|
237 public:
|
cannam@147
|
238 ReadFragmenter(AsyncIoStream& inner, size_t limit): inner(inner), limit(limit) {}
|
cannam@147
|
239
|
cannam@147
|
240 Promise<size_t> read(void* buffer, size_t minBytes, size_t maxBytes) override {
|
cannam@147
|
241 return inner.read(buffer, minBytes, kj::max(minBytes, kj::min(limit, maxBytes)));
|
cannam@147
|
242 }
|
cannam@147
|
243 Promise<size_t> tryRead(void* buffer, size_t minBytes, size_t maxBytes) override {
|
cannam@147
|
244 return inner.tryRead(buffer, minBytes, kj::max(minBytes, kj::min(limit, maxBytes)));
|
cannam@147
|
245 }
|
cannam@147
|
246
|
cannam@147
|
247 Maybe<uint64_t> tryGetLength() override { return inner.tryGetLength(); }
|
cannam@147
|
248
|
cannam@147
|
249 Promise<uint64_t> pumpTo(AsyncOutputStream& output, uint64_t amount) override {
|
cannam@147
|
250 return inner.pumpTo(output, amount);
|
cannam@147
|
251 }
|
cannam@147
|
252
|
cannam@147
|
253 Promise<void> write(const void* buffer, size_t size) override {
|
cannam@147
|
254 return inner.write(buffer, size);
|
cannam@147
|
255 }
|
cannam@147
|
256 Promise<void> write(ArrayPtr<const ArrayPtr<const byte>> pieces) override {
|
cannam@147
|
257 return inner.write(pieces);
|
cannam@147
|
258 }
|
cannam@147
|
259
|
cannam@147
|
260 Maybe<Promise<uint64_t>> tryPumpFrom(AsyncInputStream& input, uint64_t amount) override {
|
cannam@147
|
261 return inner.tryPumpFrom(input, amount);
|
cannam@147
|
262 }
|
cannam@147
|
263
|
cannam@147
|
264 void shutdownWrite() override {
|
cannam@147
|
265 return inner.shutdownWrite();
|
cannam@147
|
266 }
|
cannam@147
|
267
|
cannam@147
|
268 void abortRead() override { return inner.abortRead(); }
|
cannam@147
|
269
|
cannam@147
|
270 void getsockopt(int level, int option, void* value, uint* length) override {
|
cannam@147
|
271 return inner.getsockopt(level, option, value, length);
|
cannam@147
|
272 }
|
cannam@147
|
273 void setsockopt(int level, int option, const void* value, uint length) override {
|
cannam@147
|
274 return inner.setsockopt(level, option, value, length);
|
cannam@147
|
275 }
|
cannam@147
|
276
|
cannam@147
|
277 void getsockname(struct sockaddr* addr, uint* length) override {
|
cannam@147
|
278 return inner.getsockname(addr, length);
|
cannam@147
|
279 }
|
cannam@147
|
280 void getpeername(struct sockaddr* addr, uint* length) override {
|
cannam@147
|
281 return inner.getsockname(addr, length);
|
cannam@147
|
282 }
|
cannam@147
|
283
|
cannam@147
|
284 private:
|
cannam@147
|
285 kj::AsyncIoStream& inner;
|
cannam@147
|
286 size_t limit;
|
cannam@147
|
287 };
|
cannam@147
|
288
|
cannam@147
|
289 template <typename T>
|
cannam@147
|
290 class InitializeableArray: public Array<T> {
|
cannam@147
|
291 public:
|
cannam@147
|
292 InitializeableArray(std::initializer_list<T> init)
|
cannam@147
|
293 : Array<T>(kj::heapArray(init)) {}
|
cannam@147
|
294 };
|
cannam@147
|
295
|
cannam@147
|
296 enum Side { BOTH, CLIENT_ONLY, SERVER_ONLY };
|
cannam@147
|
297
|
cannam@147
|
298 struct HeaderTestCase {
|
cannam@147
|
299 HttpHeaderId id;
|
cannam@147
|
300 kj::StringPtr value;
|
cannam@147
|
301 };
|
cannam@147
|
302
|
cannam@147
|
303 struct HttpRequestTestCase {
|
cannam@147
|
304 kj::StringPtr raw;
|
cannam@147
|
305
|
cannam@147
|
306 HttpMethod method;
|
cannam@147
|
307 kj::StringPtr path;
|
cannam@147
|
308 InitializeableArray<HeaderTestCase> requestHeaders;
|
cannam@147
|
309 kj::Maybe<uint64_t> requestBodySize;
|
cannam@147
|
310 InitializeableArray<kj::StringPtr> requestBodyParts;
|
cannam@147
|
311
|
cannam@147
|
312 Side side = BOTH;
|
cannam@147
|
313
|
cannam@147
|
314 // TODO(cleanup): Delete this constructor if/when we move to C++14.
|
cannam@147
|
315 HttpRequestTestCase(kj::StringPtr raw, HttpMethod method, kj::StringPtr path,
|
cannam@147
|
316 InitializeableArray<HeaderTestCase> requestHeaders,
|
cannam@147
|
317 kj::Maybe<uint64_t> requestBodySize,
|
cannam@147
|
318 InitializeableArray<kj::StringPtr> requestBodyParts,
|
cannam@147
|
319 Side side = BOTH)
|
cannam@147
|
320 : raw(raw), method(method), path(path), requestHeaders(kj::mv(requestHeaders)),
|
cannam@147
|
321 requestBodySize(requestBodySize), requestBodyParts(kj::mv(requestBodyParts)),
|
cannam@147
|
322 side(side) {}
|
cannam@147
|
323 };
|
cannam@147
|
324
|
cannam@147
|
325 struct HttpResponseTestCase {
|
cannam@147
|
326 kj::StringPtr raw;
|
cannam@147
|
327
|
cannam@147
|
328 uint64_t statusCode;
|
cannam@147
|
329 kj::StringPtr statusText;
|
cannam@147
|
330 InitializeableArray<HeaderTestCase> responseHeaders;
|
cannam@147
|
331 kj::Maybe<uint64_t> responseBodySize;
|
cannam@147
|
332 InitializeableArray<kj::StringPtr> responseBodyParts;
|
cannam@147
|
333
|
cannam@147
|
334 HttpMethod method = HttpMethod::GET;
|
cannam@147
|
335
|
cannam@147
|
336 Side side = BOTH;
|
cannam@147
|
337
|
cannam@147
|
338 // TODO(cleanup): Delete this constructor if/when we move to C++14.
|
cannam@147
|
339 HttpResponseTestCase(kj::StringPtr raw, uint64_t statusCode, kj::StringPtr statusText,
|
cannam@147
|
340 InitializeableArray<HeaderTestCase> responseHeaders,
|
cannam@147
|
341 kj::Maybe<uint64_t> responseBodySize,
|
cannam@147
|
342 InitializeableArray<kj::StringPtr> responseBodyParts,
|
cannam@147
|
343 HttpMethod method = HttpMethod::GET,
|
cannam@147
|
344 Side side = BOTH)
|
cannam@147
|
345 : raw(raw), statusCode(statusCode), statusText(statusText),
|
cannam@147
|
346 responseHeaders(kj::mv(responseHeaders)), responseBodySize(responseBodySize),
|
cannam@147
|
347 responseBodyParts(kj::mv(responseBodyParts)), method(method), side(side) {}
|
cannam@147
|
348 };
|
cannam@147
|
349
|
cannam@147
|
350 struct HttpTestCase {
|
cannam@147
|
351 HttpRequestTestCase request;
|
cannam@147
|
352 HttpResponseTestCase response;
|
cannam@147
|
353 };
|
cannam@147
|
354
|
cannam@147
|
355 kj::Promise<void> writeEach(kj::AsyncOutputStream& out, kj::ArrayPtr<const kj::StringPtr> parts) {
|
cannam@147
|
356 if (parts.size() == 0) return kj::READY_NOW;
|
cannam@147
|
357
|
cannam@147
|
358 return out.write(parts[0].begin(), parts[0].size())
|
cannam@147
|
359 .then([&out,parts]() {
|
cannam@147
|
360 return writeEach(out, parts.slice(1, parts.size()));
|
cannam@147
|
361 });
|
cannam@147
|
362 }
|
cannam@147
|
363
|
cannam@147
|
364 kj::Promise<void> expectRead(kj::AsyncInputStream& in, kj::StringPtr expected) {
|
cannam@147
|
365 if (expected.size() == 0) return kj::READY_NOW;
|
cannam@147
|
366
|
cannam@147
|
367 auto buffer = kj::heapArray<char>(expected.size());
|
cannam@147
|
368
|
cannam@147
|
369 auto promise = in.tryRead(buffer.begin(), 1, buffer.size());
|
cannam@147
|
370 return promise.then(kj::mvCapture(buffer, [&in,expected](kj::Array<char> buffer, size_t amount) {
|
cannam@147
|
371 if (amount == 0) {
|
cannam@147
|
372 KJ_FAIL_ASSERT("expected data never sent", expected);
|
cannam@147
|
373 }
|
cannam@147
|
374
|
cannam@147
|
375 auto actual = buffer.slice(0, amount);
|
cannam@147
|
376 if (memcmp(actual.begin(), expected.begin(), actual.size()) != 0) {
|
cannam@147
|
377 KJ_FAIL_ASSERT("data from stream doesn't match expected", expected, actual);
|
cannam@147
|
378 }
|
cannam@147
|
379
|
cannam@147
|
380 return expectRead(in, expected.slice(amount));
|
cannam@147
|
381 }));
|
cannam@147
|
382 }
|
cannam@147
|
383
|
cannam@147
|
384 void testHttpClientRequest(kj::AsyncIoContext& io, const HttpRequestTestCase& testCase) {
|
cannam@147
|
385 auto pipe = io.provider->newTwoWayPipe();
|
cannam@147
|
386
|
cannam@147
|
387 auto serverTask = expectRead(*pipe.ends[1], testCase.raw).then([&]() {
|
cannam@147
|
388 static const char SIMPLE_RESPONSE[] =
|
cannam@147
|
389 "HTTP/1.1 200 OK\r\n"
|
cannam@147
|
390 "Content-Length: 0\r\n"
|
cannam@147
|
391 "\r\n";
|
cannam@147
|
392 return pipe.ends[1]->write(SIMPLE_RESPONSE, strlen(SIMPLE_RESPONSE));
|
cannam@147
|
393 }).then([&]() -> kj::Promise<void> {
|
cannam@147
|
394 return kj::NEVER_DONE;
|
cannam@147
|
395 });
|
cannam@147
|
396
|
cannam@147
|
397 HttpHeaderTable table;
|
cannam@147
|
398 auto client = newHttpClient(table, *pipe.ends[0]);
|
cannam@147
|
399
|
cannam@147
|
400 HttpHeaders headers(table);
|
cannam@147
|
401 for (auto& header: testCase.requestHeaders) {
|
cannam@147
|
402 headers.set(header.id, header.value);
|
cannam@147
|
403 }
|
cannam@147
|
404
|
cannam@147
|
405 auto request = client->request(testCase.method, testCase.path, headers, testCase.requestBodySize);
|
cannam@147
|
406 if (testCase.requestBodyParts.size() > 0) {
|
cannam@147
|
407 writeEach(*request.body, testCase.requestBodyParts).wait(io.waitScope);
|
cannam@147
|
408 }
|
cannam@147
|
409 request.body = nullptr;
|
cannam@147
|
410 auto clientTask = request.response
|
cannam@147
|
411 .then([&](HttpClient::Response&& response) {
|
cannam@147
|
412 auto promise = response.body->readAllText();
|
cannam@147
|
413 return promise.attach(kj::mv(response.body));
|
cannam@147
|
414 }).ignoreResult();
|
cannam@147
|
415
|
cannam@147
|
416 serverTask.exclusiveJoin(kj::mv(clientTask)).wait(io.waitScope);
|
cannam@147
|
417
|
cannam@147
|
418 // Verify no more data written by client.
|
cannam@147
|
419 client = nullptr;
|
cannam@147
|
420 pipe.ends[0]->shutdownWrite();
|
cannam@147
|
421 KJ_EXPECT(pipe.ends[1]->readAllText().wait(io.waitScope) == "");
|
cannam@147
|
422 }
|
cannam@147
|
423
|
cannam@147
|
424 void testHttpClientResponse(kj::AsyncIoContext& io, const HttpResponseTestCase& testCase,
|
cannam@147
|
425 size_t readFragmentSize) {
|
cannam@147
|
426 auto pipe = io.provider->newTwoWayPipe();
|
cannam@147
|
427 ReadFragmenter fragmenter(*pipe.ends[0], readFragmentSize);
|
cannam@147
|
428
|
cannam@147
|
429 auto expectedReqText = testCase.method == HttpMethod::GET || testCase.method == HttpMethod::HEAD
|
cannam@147
|
430 ? kj::str(testCase.method, " / HTTP/1.1\r\n\r\n")
|
cannam@147
|
431 : kj::str(testCase.method, " / HTTP/1.1\r\nContent-Length: 0\r\n");
|
cannam@147
|
432
|
cannam@147
|
433 auto serverTask = expectRead(*pipe.ends[1], expectedReqText).then([&]() {
|
cannam@147
|
434 return pipe.ends[1]->write(testCase.raw.begin(), testCase.raw.size());
|
cannam@147
|
435 }).then([&]() -> kj::Promise<void> {
|
cannam@147
|
436 pipe.ends[1]->shutdownWrite();
|
cannam@147
|
437 return kj::NEVER_DONE;
|
cannam@147
|
438 });
|
cannam@147
|
439
|
cannam@147
|
440 HttpHeaderTable table;
|
cannam@147
|
441 auto client = newHttpClient(table, fragmenter);
|
cannam@147
|
442
|
cannam@147
|
443 HttpHeaders headers(table);
|
cannam@147
|
444 auto request = client->request(testCase.method, "/", headers, uint64_t(0));
|
cannam@147
|
445 request.body = nullptr;
|
cannam@147
|
446 auto clientTask = request.response
|
cannam@147
|
447 .then([&](HttpClient::Response&& response) {
|
cannam@147
|
448 KJ_EXPECT(response.statusCode == testCase.statusCode);
|
cannam@147
|
449 KJ_EXPECT(response.statusText == testCase.statusText);
|
cannam@147
|
450
|
cannam@147
|
451 for (auto& header: testCase.responseHeaders) {
|
cannam@147
|
452 KJ_EXPECT(KJ_ASSERT_NONNULL(response.headers->get(header.id)) == header.value);
|
cannam@147
|
453 }
|
cannam@147
|
454 auto promise = response.body->readAllText();
|
cannam@147
|
455 return promise.attach(kj::mv(response.body));
|
cannam@147
|
456 }).then([&](kj::String body) {
|
cannam@147
|
457 KJ_EXPECT(body == kj::strArray(testCase.responseBodyParts, ""), body);
|
cannam@147
|
458 });
|
cannam@147
|
459
|
cannam@147
|
460 serverTask.exclusiveJoin(kj::mv(clientTask)).wait(io.waitScope);
|
cannam@147
|
461
|
cannam@147
|
462 // Verify no more data written by client.
|
cannam@147
|
463 client = nullptr;
|
cannam@147
|
464 pipe.ends[0]->shutdownWrite();
|
cannam@147
|
465 KJ_EXPECT(pipe.ends[1]->readAllText().wait(io.waitScope) == "");
|
cannam@147
|
466 }
|
cannam@147
|
467
|
cannam@147
|
468 class TestHttpService final: public HttpService {
|
cannam@147
|
469 public:
|
cannam@147
|
470 TestHttpService(const HttpRequestTestCase& expectedRequest,
|
cannam@147
|
471 const HttpResponseTestCase& response,
|
cannam@147
|
472 HttpHeaderTable& table)
|
cannam@147
|
473 : singleExpectedRequest(&expectedRequest),
|
cannam@147
|
474 singleResponse(&response),
|
cannam@147
|
475 responseHeaders(table) {}
|
cannam@147
|
476 TestHttpService(kj::ArrayPtr<const HttpTestCase> testCases,
|
cannam@147
|
477 HttpHeaderTable& table)
|
cannam@147
|
478 : singleExpectedRequest(nullptr),
|
cannam@147
|
479 singleResponse(nullptr),
|
cannam@147
|
480 testCases(testCases),
|
cannam@147
|
481 responseHeaders(table) {}
|
cannam@147
|
482
|
cannam@147
|
483 uint getRequestCount() { return requestCount; }
|
cannam@147
|
484
|
cannam@147
|
485 kj::Promise<void> request(
|
cannam@147
|
486 HttpMethod method, kj::StringPtr url, const HttpHeaders& headers,
|
cannam@147
|
487 kj::AsyncInputStream& requestBody, Response& responseSender) override {
|
cannam@147
|
488 auto& expectedRequest = testCases == nullptr ? *singleExpectedRequest :
|
cannam@147
|
489 testCases[requestCount % testCases.size()].request;
|
cannam@147
|
490 auto& response = testCases == nullptr ? *singleResponse :
|
cannam@147
|
491 testCases[requestCount % testCases.size()].response;
|
cannam@147
|
492
|
cannam@147
|
493 ++requestCount;
|
cannam@147
|
494
|
cannam@147
|
495 KJ_EXPECT(method == expectedRequest.method, method);
|
cannam@147
|
496 KJ_EXPECT(url == expectedRequest.path, url);
|
cannam@147
|
497
|
cannam@147
|
498 for (auto& header: expectedRequest.requestHeaders) {
|
cannam@147
|
499 KJ_EXPECT(KJ_ASSERT_NONNULL(headers.get(header.id)) == header.value);
|
cannam@147
|
500 }
|
cannam@147
|
501
|
cannam@147
|
502 auto size = requestBody.tryGetLength();
|
cannam@147
|
503 KJ_IF_MAYBE(expectedSize, expectedRequest.requestBodySize) {
|
cannam@147
|
504 KJ_IF_MAYBE(s, size) {
|
cannam@147
|
505 KJ_EXPECT(*s == *expectedSize, *s);
|
cannam@147
|
506 } else {
|
cannam@147
|
507 KJ_FAIL_EXPECT("tryGetLength() returned nullptr; expected known size");
|
cannam@147
|
508 }
|
cannam@147
|
509 } else {
|
cannam@147
|
510 KJ_EXPECT(size == nullptr);
|
cannam@147
|
511 }
|
cannam@147
|
512
|
cannam@147
|
513 return requestBody.readAllText()
|
cannam@147
|
514 .then([this,&expectedRequest,&response,&responseSender](kj::String text) {
|
cannam@147
|
515 KJ_EXPECT(text == kj::strArray(expectedRequest.requestBodyParts, ""), text);
|
cannam@147
|
516
|
cannam@147
|
517 responseHeaders.clear();
|
cannam@147
|
518 for (auto& header: response.responseHeaders) {
|
cannam@147
|
519 responseHeaders.set(header.id, header.value);
|
cannam@147
|
520 }
|
cannam@147
|
521
|
cannam@147
|
522 auto stream = responseSender.send(response.statusCode, response.statusText,
|
cannam@147
|
523 responseHeaders, response.responseBodySize);
|
cannam@147
|
524 auto promise = writeEach(*stream, response.responseBodyParts);
|
cannam@147
|
525 return promise.attach(kj::mv(stream));
|
cannam@147
|
526 });
|
cannam@147
|
527 }
|
cannam@147
|
528
|
cannam@147
|
529 private:
|
cannam@147
|
530 const HttpRequestTestCase* singleExpectedRequest;
|
cannam@147
|
531 const HttpResponseTestCase* singleResponse;
|
cannam@147
|
532 kj::ArrayPtr<const HttpTestCase> testCases;
|
cannam@147
|
533 HttpHeaders responseHeaders;
|
cannam@147
|
534 uint requestCount = 0;
|
cannam@147
|
535 };
|
cannam@147
|
536
|
cannam@147
|
537 void testHttpServerRequest(kj::AsyncIoContext& io,
|
cannam@147
|
538 const HttpRequestTestCase& requestCase,
|
cannam@147
|
539 const HttpResponseTestCase& responseCase) {
|
cannam@147
|
540 auto pipe = io.provider->newTwoWayPipe();
|
cannam@147
|
541
|
cannam@147
|
542 HttpHeaderTable table;
|
cannam@147
|
543 TestHttpService service(requestCase, responseCase, table);
|
cannam@147
|
544 HttpServer server(io.provider->getTimer(), table, service);
|
cannam@147
|
545
|
cannam@147
|
546 auto listenTask = server.listenHttp(kj::mv(pipe.ends[0]));
|
cannam@147
|
547
|
cannam@147
|
548 pipe.ends[1]->write(requestCase.raw.begin(), requestCase.raw.size()).wait(io.waitScope);
|
cannam@147
|
549 pipe.ends[1]->shutdownWrite();
|
cannam@147
|
550
|
cannam@147
|
551 expectRead(*pipe.ends[1], responseCase.raw).wait(io.waitScope);
|
cannam@147
|
552
|
cannam@147
|
553 listenTask.wait(io.waitScope);
|
cannam@147
|
554
|
cannam@147
|
555 KJ_EXPECT(service.getRequestCount() == 1);
|
cannam@147
|
556 }
|
cannam@147
|
557
|
cannam@147
|
558 kj::ArrayPtr<const HttpRequestTestCase> requestTestCases() {
|
cannam@147
|
559 static const auto HUGE_STRING = kj::strArray(kj::repeat("abcdefgh", 4096), "");
|
cannam@147
|
560 static const auto HUGE_REQUEST = kj::str(
|
cannam@147
|
561 "GET / HTTP/1.1\r\n"
|
cannam@147
|
562 "Host: ", HUGE_STRING, "\r\n"
|
cannam@147
|
563 "\r\n");
|
cannam@147
|
564
|
cannam@147
|
565 static const HttpRequestTestCase REQUEST_TEST_CASES[] {
|
cannam@147
|
566 {
|
cannam@147
|
567 "GET /foo/bar HTTP/1.1\r\n"
|
cannam@147
|
568 "Host: example.com\r\n"
|
cannam@147
|
569 "\r\n",
|
cannam@147
|
570
|
cannam@147
|
571 HttpMethod::GET,
|
cannam@147
|
572 "/foo/bar",
|
cannam@147
|
573 {{HttpHeaderId::HOST, "example.com"}},
|
cannam@147
|
574 nullptr, {},
|
cannam@147
|
575 },
|
cannam@147
|
576
|
cannam@147
|
577 {
|
cannam@147
|
578 "HEAD /foo/bar HTTP/1.1\r\n"
|
cannam@147
|
579 "Host: example.com\r\n"
|
cannam@147
|
580 "\r\n",
|
cannam@147
|
581
|
cannam@147
|
582 HttpMethod::HEAD,
|
cannam@147
|
583 "/foo/bar",
|
cannam@147
|
584 {{HttpHeaderId::HOST, "example.com"}},
|
cannam@147
|
585 nullptr, {},
|
cannam@147
|
586 },
|
cannam@147
|
587
|
cannam@147
|
588 {
|
cannam@147
|
589 "POST / HTTP/1.1\r\n"
|
cannam@147
|
590 "Content-Length: 9\r\n"
|
cannam@147
|
591 "Host: example.com\r\n"
|
cannam@147
|
592 "Content-Type: text/plain\r\n"
|
cannam@147
|
593 "\r\n"
|
cannam@147
|
594 "foobarbaz",
|
cannam@147
|
595
|
cannam@147
|
596 HttpMethod::POST,
|
cannam@147
|
597 "/",
|
cannam@147
|
598 {
|
cannam@147
|
599 {HttpHeaderId::HOST, "example.com"},
|
cannam@147
|
600 {HttpHeaderId::CONTENT_TYPE, "text/plain"},
|
cannam@147
|
601 },
|
cannam@147
|
602 9, { "foo", "bar", "baz" },
|
cannam@147
|
603 },
|
cannam@147
|
604
|
cannam@147
|
605 {
|
cannam@147
|
606 "POST / HTTP/1.1\r\n"
|
cannam@147
|
607 "Transfer-Encoding: chunked\r\n"
|
cannam@147
|
608 "Host: example.com\r\n"
|
cannam@147
|
609 "Content-Type: text/plain\r\n"
|
cannam@147
|
610 "\r\n"
|
cannam@147
|
611 "3\r\n"
|
cannam@147
|
612 "foo\r\n"
|
cannam@147
|
613 "6\r\n"
|
cannam@147
|
614 "barbaz\r\n"
|
cannam@147
|
615 "0\r\n"
|
cannam@147
|
616 "\r\n",
|
cannam@147
|
617
|
cannam@147
|
618 HttpMethod::POST,
|
cannam@147
|
619 "/",
|
cannam@147
|
620 {
|
cannam@147
|
621 {HttpHeaderId::HOST, "example.com"},
|
cannam@147
|
622 {HttpHeaderId::CONTENT_TYPE, "text/plain"},
|
cannam@147
|
623 },
|
cannam@147
|
624 nullptr, { "foo", "barbaz" },
|
cannam@147
|
625 },
|
cannam@147
|
626
|
cannam@147
|
627 {
|
cannam@147
|
628 "POST / HTTP/1.1\r\n"
|
cannam@147
|
629 "Transfer-Encoding: chunked\r\n"
|
cannam@147
|
630 "Host: example.com\r\n"
|
cannam@147
|
631 "Content-Type: text/plain\r\n"
|
cannam@147
|
632 "\r\n"
|
cannam@147
|
633 "1d\r\n"
|
cannam@147
|
634 "0123456789abcdef0123456789abc\r\n"
|
cannam@147
|
635 "0\r\n"
|
cannam@147
|
636 "\r\n",
|
cannam@147
|
637
|
cannam@147
|
638 HttpMethod::POST,
|
cannam@147
|
639 "/",
|
cannam@147
|
640 {
|
cannam@147
|
641 {HttpHeaderId::HOST, "example.com"},
|
cannam@147
|
642 {HttpHeaderId::CONTENT_TYPE, "text/plain"},
|
cannam@147
|
643 },
|
cannam@147
|
644 nullptr, { "0123456789abcdef0123456789abc" },
|
cannam@147
|
645 },
|
cannam@147
|
646
|
cannam@147
|
647 {
|
cannam@147
|
648 HUGE_REQUEST,
|
cannam@147
|
649
|
cannam@147
|
650 HttpMethod::GET,
|
cannam@147
|
651 "/",
|
cannam@147
|
652 {{HttpHeaderId::HOST, HUGE_STRING}},
|
cannam@147
|
653 nullptr, {}
|
cannam@147
|
654 },
|
cannam@147
|
655 };
|
cannam@147
|
656
|
cannam@147
|
657 // TODO(cleanup): A bug in GCC 4.8, fixed in 4.9, prevents REQUEST_TEST_CASES from implicitly
|
cannam@147
|
658 // casting to our return type.
|
cannam@147
|
659 return kj::arrayPtr(REQUEST_TEST_CASES, kj::size(REQUEST_TEST_CASES));
|
cannam@147
|
660 }
|
cannam@147
|
661
|
cannam@147
|
662 kj::ArrayPtr<const HttpResponseTestCase> responseTestCases() {
|
cannam@147
|
663 static const HttpResponseTestCase RESPONSE_TEST_CASES[] {
|
cannam@147
|
664 {
|
cannam@147
|
665 "HTTP/1.1 200 OK\r\n"
|
cannam@147
|
666 "Content-Type: text/plain\r\n"
|
cannam@147
|
667 "Connection: close\r\n"
|
cannam@147
|
668 "\r\n"
|
cannam@147
|
669 "baz qux",
|
cannam@147
|
670
|
cannam@147
|
671 200, "OK",
|
cannam@147
|
672 {{HttpHeaderId::CONTENT_TYPE, "text/plain"}},
|
cannam@147
|
673 nullptr, {"baz qux"},
|
cannam@147
|
674
|
cannam@147
|
675 HttpMethod::GET,
|
cannam@147
|
676 CLIENT_ONLY, // Server never sends connection: close
|
cannam@147
|
677 },
|
cannam@147
|
678
|
cannam@147
|
679 {
|
cannam@147
|
680 "HTTP/1.1 200 OK\r\n"
|
cannam@147
|
681 "Content-Length: 123\r\n"
|
cannam@147
|
682 "Content-Type: text/plain\r\n"
|
cannam@147
|
683 "\r\n",
|
cannam@147
|
684
|
cannam@147
|
685 200, "OK",
|
cannam@147
|
686 {{HttpHeaderId::CONTENT_TYPE, "text/plain"}},
|
cannam@147
|
687 123, {},
|
cannam@147
|
688
|
cannam@147
|
689 HttpMethod::HEAD,
|
cannam@147
|
690 },
|
cannam@147
|
691
|
cannam@147
|
692 {
|
cannam@147
|
693 "HTTP/1.1 200 OK\r\n"
|
cannam@147
|
694 "Content-Length: 8\r\n"
|
cannam@147
|
695 "Content-Type: text/plain\r\n"
|
cannam@147
|
696 "\r\n"
|
cannam@147
|
697 "quxcorge",
|
cannam@147
|
698
|
cannam@147
|
699 200, "OK",
|
cannam@147
|
700 {{HttpHeaderId::CONTENT_TYPE, "text/plain"}},
|
cannam@147
|
701 8, { "qux", "corge" }
|
cannam@147
|
702 },
|
cannam@147
|
703
|
cannam@147
|
704 {
|
cannam@147
|
705 "HTTP/1.1 200 OK\r\n"
|
cannam@147
|
706 "Transfer-Encoding: chunked\r\n"
|
cannam@147
|
707 "Content-Type: text/plain\r\n"
|
cannam@147
|
708 "\r\n"
|
cannam@147
|
709 "3\r\n"
|
cannam@147
|
710 "qux\r\n"
|
cannam@147
|
711 "5\r\n"
|
cannam@147
|
712 "corge\r\n"
|
cannam@147
|
713 "0\r\n"
|
cannam@147
|
714 "\r\n",
|
cannam@147
|
715
|
cannam@147
|
716 200, "OK",
|
cannam@147
|
717 {{HttpHeaderId::CONTENT_TYPE, "text/plain"}},
|
cannam@147
|
718 nullptr, { "qux", "corge" }
|
cannam@147
|
719 },
|
cannam@147
|
720 };
|
cannam@147
|
721
|
cannam@147
|
722 // TODO(cleanup): A bug in GCC 4.8, fixed in 4.9, prevents RESPONSE_TEST_CASES from implicitly
|
cannam@147
|
723 // casting to our return type.
|
cannam@147
|
724 return kj::arrayPtr(RESPONSE_TEST_CASES, kj::size(RESPONSE_TEST_CASES));
|
cannam@147
|
725 }
|
cannam@147
|
726
|
cannam@147
|
727 KJ_TEST("HttpClient requests") {
|
cannam@147
|
728 auto io = kj::setupAsyncIo();
|
cannam@147
|
729
|
cannam@147
|
730 for (auto& testCase: requestTestCases()) {
|
cannam@147
|
731 if (testCase.side == SERVER_ONLY) continue;
|
cannam@147
|
732 KJ_CONTEXT(testCase.raw);
|
cannam@147
|
733 testHttpClientRequest(io, testCase);
|
cannam@147
|
734 }
|
cannam@147
|
735 }
|
cannam@147
|
736
|
cannam@147
|
737 KJ_TEST("HttpClient responses") {
|
cannam@147
|
738 auto io = kj::setupAsyncIo();
|
cannam@147
|
739 size_t FRAGMENT_SIZES[] = { 1, 2, 3, 4, 5, 6, 7, 8, 16, 31, kj::maxValue };
|
cannam@147
|
740
|
cannam@147
|
741 for (auto& testCase: responseTestCases()) {
|
cannam@147
|
742 if (testCase.side == SERVER_ONLY) continue;
|
cannam@147
|
743 for (size_t fragmentSize: FRAGMENT_SIZES) {
|
cannam@147
|
744 KJ_CONTEXT(testCase.raw, fragmentSize);
|
cannam@147
|
745 testHttpClientResponse(io, testCase, fragmentSize);
|
cannam@147
|
746 }
|
cannam@147
|
747 }
|
cannam@147
|
748 }
|
cannam@147
|
749
|
cannam@147
|
750 KJ_TEST("HttpServer requests") {
|
cannam@147
|
751 HttpResponseTestCase RESPONSE = {
|
cannam@147
|
752 "HTTP/1.1 200 OK\r\n"
|
cannam@147
|
753 "Content-Length: 3\r\n"
|
cannam@147
|
754 "\r\n"
|
cannam@147
|
755 "foo",
|
cannam@147
|
756
|
cannam@147
|
757 200, "OK",
|
cannam@147
|
758 {},
|
cannam@147
|
759 3, {"foo"}
|
cannam@147
|
760 };
|
cannam@147
|
761
|
cannam@147
|
762 HttpResponseTestCase HEAD_RESPONSE = {
|
cannam@147
|
763 "HTTP/1.1 200 OK\r\n"
|
cannam@147
|
764 "Content-Length: 3\r\n"
|
cannam@147
|
765 "\r\n",
|
cannam@147
|
766
|
cannam@147
|
767 200, "OK",
|
cannam@147
|
768 {},
|
cannam@147
|
769 3, {"foo"}
|
cannam@147
|
770 };
|
cannam@147
|
771
|
cannam@147
|
772 auto io = kj::setupAsyncIo();
|
cannam@147
|
773
|
cannam@147
|
774 for (auto& testCase: requestTestCases()) {
|
cannam@147
|
775 if (testCase.side == CLIENT_ONLY) continue;
|
cannam@147
|
776 KJ_CONTEXT(testCase.raw);
|
cannam@147
|
777 testHttpServerRequest(io, testCase,
|
cannam@147
|
778 testCase.method == HttpMethod::HEAD ? HEAD_RESPONSE : RESPONSE);
|
cannam@147
|
779 }
|
cannam@147
|
780 }
|
cannam@147
|
781
|
cannam@147
|
782 KJ_TEST("HttpServer responses") {
|
cannam@147
|
783 HttpRequestTestCase REQUEST = {
|
cannam@147
|
784 "GET / HTTP/1.1\r\n"
|
cannam@147
|
785 "\r\n",
|
cannam@147
|
786
|
cannam@147
|
787 HttpMethod::GET,
|
cannam@147
|
788 "/",
|
cannam@147
|
789 {},
|
cannam@147
|
790 nullptr, {},
|
cannam@147
|
791 };
|
cannam@147
|
792
|
cannam@147
|
793 HttpRequestTestCase HEAD_REQUEST = {
|
cannam@147
|
794 "HEAD / HTTP/1.1\r\n"
|
cannam@147
|
795 "\r\n",
|
cannam@147
|
796
|
cannam@147
|
797 HttpMethod::HEAD,
|
cannam@147
|
798 "/",
|
cannam@147
|
799 {},
|
cannam@147
|
800 nullptr, {},
|
cannam@147
|
801 };
|
cannam@147
|
802
|
cannam@147
|
803 auto io = kj::setupAsyncIo();
|
cannam@147
|
804
|
cannam@147
|
805 for (auto& testCase: responseTestCases()) {
|
cannam@147
|
806 if (testCase.side == CLIENT_ONLY) continue;
|
cannam@147
|
807 KJ_CONTEXT(testCase.raw);
|
cannam@147
|
808 testHttpServerRequest(io,
|
cannam@147
|
809 testCase.method == HttpMethod::HEAD ? HEAD_REQUEST : REQUEST, testCase);
|
cannam@147
|
810 }
|
cannam@147
|
811 }
|
cannam@147
|
812
|
cannam@147
|
813 // -----------------------------------------------------------------------------
|
cannam@147
|
814
|
cannam@147
|
815 kj::ArrayPtr<const HttpTestCase> pipelineTestCases() {
|
cannam@147
|
816 static const HttpTestCase PIPELINE_TESTS[] = {
|
cannam@147
|
817 {
|
cannam@147
|
818 {
|
cannam@147
|
819 "GET / HTTP/1.1\r\n"
|
cannam@147
|
820 "\r\n",
|
cannam@147
|
821
|
cannam@147
|
822 HttpMethod::GET, "/", {}, nullptr, {},
|
cannam@147
|
823 },
|
cannam@147
|
824 {
|
cannam@147
|
825 "HTTP/1.1 200 OK\r\n"
|
cannam@147
|
826 "Content-Length: 7\r\n"
|
cannam@147
|
827 "\r\n"
|
cannam@147
|
828 "foo bar",
|
cannam@147
|
829
|
cannam@147
|
830 200, "OK", {}, 7, { "foo bar" }
|
cannam@147
|
831 },
|
cannam@147
|
832 },
|
cannam@147
|
833
|
cannam@147
|
834 {
|
cannam@147
|
835 {
|
cannam@147
|
836 "POST /foo HTTP/1.1\r\n"
|
cannam@147
|
837 "Content-Length: 6\r\n"
|
cannam@147
|
838 "\r\n"
|
cannam@147
|
839 "grault",
|
cannam@147
|
840
|
cannam@147
|
841 HttpMethod::POST, "/foo", {}, 6, { "grault" },
|
cannam@147
|
842 },
|
cannam@147
|
843 {
|
cannam@147
|
844 "HTTP/1.1 404 Not Found\r\n"
|
cannam@147
|
845 "Content-Length: 13\r\n"
|
cannam@147
|
846 "\r\n"
|
cannam@147
|
847 "baz qux corge",
|
cannam@147
|
848
|
cannam@147
|
849 404, "Not Found", {}, 13, { "baz qux corge" }
|
cannam@147
|
850 },
|
cannam@147
|
851 },
|
cannam@147
|
852
|
cannam@147
|
853 {
|
cannam@147
|
854 {
|
cannam@147
|
855 "POST /bar HTTP/1.1\r\n"
|
cannam@147
|
856 "Transfer-Encoding: chunked\r\n"
|
cannam@147
|
857 "\r\n"
|
cannam@147
|
858 "6\r\n"
|
cannam@147
|
859 "garply\r\n"
|
cannam@147
|
860 "5\r\n"
|
cannam@147
|
861 "waldo\r\n"
|
cannam@147
|
862 "0\r\n"
|
cannam@147
|
863 "\r\n",
|
cannam@147
|
864
|
cannam@147
|
865 HttpMethod::POST, "/bar", {}, nullptr, { "garply", "waldo" },
|
cannam@147
|
866 },
|
cannam@147
|
867 {
|
cannam@147
|
868 "HTTP/1.1 200 OK\r\n"
|
cannam@147
|
869 "Transfer-Encoding: chunked\r\n"
|
cannam@147
|
870 "\r\n"
|
cannam@147
|
871 "4\r\n"
|
cannam@147
|
872 "fred\r\n"
|
cannam@147
|
873 "5\r\n"
|
cannam@147
|
874 "plugh\r\n"
|
cannam@147
|
875 "0\r\n"
|
cannam@147
|
876 "\r\n",
|
cannam@147
|
877
|
cannam@147
|
878 200, "OK", {}, nullptr, { "fred", "plugh" }
|
cannam@147
|
879 },
|
cannam@147
|
880 },
|
cannam@147
|
881
|
cannam@147
|
882 {
|
cannam@147
|
883 {
|
cannam@147
|
884 "HEAD / HTTP/1.1\r\n"
|
cannam@147
|
885 "\r\n",
|
cannam@147
|
886
|
cannam@147
|
887 HttpMethod::HEAD, "/", {}, nullptr, {},
|
cannam@147
|
888 },
|
cannam@147
|
889 {
|
cannam@147
|
890 "HTTP/1.1 200 OK\r\n"
|
cannam@147
|
891 "Content-Length: 7\r\n"
|
cannam@147
|
892 "\r\n",
|
cannam@147
|
893
|
cannam@147
|
894 200, "OK", {}, 7, { "foo bar" }
|
cannam@147
|
895 },
|
cannam@147
|
896 },
|
cannam@147
|
897 };
|
cannam@147
|
898
|
cannam@147
|
899 // TODO(cleanup): A bug in GCC 4.8, fixed in 4.9, prevents RESPONSE_TEST_CASES from implicitly
|
cannam@147
|
900 // casting to our return type.
|
cannam@147
|
901 return kj::arrayPtr(PIPELINE_TESTS, kj::size(PIPELINE_TESTS));
|
cannam@147
|
902 }
|
cannam@147
|
903
|
cannam@147
|
904 KJ_TEST("HttpClient pipeline") {
|
cannam@147
|
905 auto PIPELINE_TESTS = pipelineTestCases();
|
cannam@147
|
906
|
cannam@147
|
907 auto io = kj::setupAsyncIo();
|
cannam@147
|
908 auto pipe = io.provider->newTwoWayPipe();
|
cannam@147
|
909
|
cannam@147
|
910 kj::Promise<void> writeResponsesPromise = kj::READY_NOW;
|
cannam@147
|
911 for (auto& testCase: PIPELINE_TESTS) {
|
cannam@147
|
912 writeResponsesPromise = writeResponsesPromise
|
cannam@147
|
913 .then([&]() {
|
cannam@147
|
914 return expectRead(*pipe.ends[1], testCase.request.raw);
|
cannam@147
|
915 }).then([&]() {
|
cannam@147
|
916 return pipe.ends[1]->write(testCase.response.raw.begin(), testCase.response.raw.size());
|
cannam@147
|
917 });
|
cannam@147
|
918 }
|
cannam@147
|
919
|
cannam@147
|
920 HttpHeaderTable table;
|
cannam@147
|
921 auto client = newHttpClient(table, *pipe.ends[0]);
|
cannam@147
|
922
|
cannam@147
|
923 for (auto& testCase: PIPELINE_TESTS) {
|
cannam@147
|
924 KJ_CONTEXT(testCase.request.raw, testCase.response.raw);
|
cannam@147
|
925
|
cannam@147
|
926 HttpHeaders headers(table);
|
cannam@147
|
927 for (auto& header: testCase.request.requestHeaders) {
|
cannam@147
|
928 headers.set(header.id, header.value);
|
cannam@147
|
929 }
|
cannam@147
|
930
|
cannam@147
|
931 auto request = client->request(
|
cannam@147
|
932 testCase.request.method, testCase.request.path, headers, testCase.request.requestBodySize);
|
cannam@147
|
933 for (auto& part: testCase.request.requestBodyParts) {
|
cannam@147
|
934 request.body->write(part.begin(), part.size()).wait(io.waitScope);
|
cannam@147
|
935 }
|
cannam@147
|
936 request.body = nullptr;
|
cannam@147
|
937
|
cannam@147
|
938 auto response = request.response.wait(io.waitScope);
|
cannam@147
|
939
|
cannam@147
|
940 KJ_EXPECT(response.statusCode == testCase.response.statusCode);
|
cannam@147
|
941 auto body = response.body->readAllText().wait(io.waitScope);
|
cannam@147
|
942 if (testCase.request.method == HttpMethod::HEAD) {
|
cannam@147
|
943 KJ_EXPECT(body == "");
|
cannam@147
|
944 } else {
|
cannam@147
|
945 KJ_EXPECT(body == kj::strArray(testCase.response.responseBodyParts, ""), body);
|
cannam@147
|
946 }
|
cannam@147
|
947 }
|
cannam@147
|
948
|
cannam@147
|
949 client = nullptr;
|
cannam@147
|
950 pipe.ends[0]->shutdownWrite();
|
cannam@147
|
951
|
cannam@147
|
952 writeResponsesPromise.wait(io.waitScope);
|
cannam@147
|
953 }
|
cannam@147
|
954
|
cannam@147
|
955 KJ_TEST("HttpClient parallel pipeline") {
|
cannam@147
|
956 auto PIPELINE_TESTS = pipelineTestCases();
|
cannam@147
|
957
|
cannam@147
|
958 auto io = kj::setupAsyncIo();
|
cannam@147
|
959 auto pipe = io.provider->newTwoWayPipe();
|
cannam@147
|
960
|
cannam@147
|
961 kj::Promise<void> writeResponsesPromise = kj::READY_NOW;
|
cannam@147
|
962 for (auto& testCase: PIPELINE_TESTS) {
|
cannam@147
|
963 writeResponsesPromise = writeResponsesPromise
|
cannam@147
|
964 .then([&]() {
|
cannam@147
|
965 return expectRead(*pipe.ends[1], testCase.request.raw);
|
cannam@147
|
966 }).then([&]() {
|
cannam@147
|
967 return pipe.ends[1]->write(testCase.response.raw.begin(), testCase.response.raw.size());
|
cannam@147
|
968 });
|
cannam@147
|
969 }
|
cannam@147
|
970
|
cannam@147
|
971 HttpHeaderTable table;
|
cannam@147
|
972 auto client = newHttpClient(table, *pipe.ends[0]);
|
cannam@147
|
973
|
cannam@147
|
974 auto responsePromises = KJ_MAP(testCase, PIPELINE_TESTS) {
|
cannam@147
|
975 KJ_CONTEXT(testCase.request.raw, testCase.response.raw);
|
cannam@147
|
976
|
cannam@147
|
977 HttpHeaders headers(table);
|
cannam@147
|
978 for (auto& header: testCase.request.requestHeaders) {
|
cannam@147
|
979 headers.set(header.id, header.value);
|
cannam@147
|
980 }
|
cannam@147
|
981
|
cannam@147
|
982 auto request = client->request(
|
cannam@147
|
983 testCase.request.method, testCase.request.path, headers, testCase.request.requestBodySize);
|
cannam@147
|
984 for (auto& part: testCase.request.requestBodyParts) {
|
cannam@147
|
985 request.body->write(part.begin(), part.size()).wait(io.waitScope);
|
cannam@147
|
986 }
|
cannam@147
|
987
|
cannam@147
|
988 return kj::mv(request.response);
|
cannam@147
|
989 };
|
cannam@147
|
990
|
cannam@147
|
991 for (auto i: kj::indices(PIPELINE_TESTS)) {
|
cannam@147
|
992 auto& testCase = PIPELINE_TESTS[i];
|
cannam@147
|
993 auto response = responsePromises[i].wait(io.waitScope);
|
cannam@147
|
994
|
cannam@147
|
995 KJ_EXPECT(response.statusCode == testCase.response.statusCode);
|
cannam@147
|
996 auto body = response.body->readAllText().wait(io.waitScope);
|
cannam@147
|
997 if (testCase.request.method == HttpMethod::HEAD) {
|
cannam@147
|
998 KJ_EXPECT(body == "");
|
cannam@147
|
999 } else {
|
cannam@147
|
1000 KJ_EXPECT(body == kj::strArray(testCase.response.responseBodyParts, ""), body);
|
cannam@147
|
1001 }
|
cannam@147
|
1002 }
|
cannam@147
|
1003
|
cannam@147
|
1004 client = nullptr;
|
cannam@147
|
1005 pipe.ends[0]->shutdownWrite();
|
cannam@147
|
1006
|
cannam@147
|
1007 writeResponsesPromise.wait(io.waitScope);
|
cannam@147
|
1008 }
|
cannam@147
|
1009
|
cannam@147
|
1010 KJ_TEST("HttpServer pipeline") {
|
cannam@147
|
1011 auto PIPELINE_TESTS = pipelineTestCases();
|
cannam@147
|
1012
|
cannam@147
|
1013 auto io = kj::setupAsyncIo();
|
cannam@147
|
1014 auto pipe = io.provider->newTwoWayPipe();
|
cannam@147
|
1015
|
cannam@147
|
1016 HttpHeaderTable table;
|
cannam@147
|
1017 TestHttpService service(PIPELINE_TESTS, table);
|
cannam@147
|
1018 HttpServer server(io.provider->getTimer(), table, service);
|
cannam@147
|
1019
|
cannam@147
|
1020 auto listenTask = server.listenHttp(kj::mv(pipe.ends[0]));
|
cannam@147
|
1021
|
cannam@147
|
1022 for (auto& testCase: PIPELINE_TESTS) {
|
cannam@147
|
1023 KJ_CONTEXT(testCase.request.raw, testCase.response.raw);
|
cannam@147
|
1024
|
cannam@147
|
1025 pipe.ends[1]->write(testCase.request.raw.begin(), testCase.request.raw.size())
|
cannam@147
|
1026 .wait(io.waitScope);
|
cannam@147
|
1027
|
cannam@147
|
1028 expectRead(*pipe.ends[1], testCase.response.raw).wait(io.waitScope);
|
cannam@147
|
1029 }
|
cannam@147
|
1030
|
cannam@147
|
1031 pipe.ends[1]->shutdownWrite();
|
cannam@147
|
1032 listenTask.wait(io.waitScope);
|
cannam@147
|
1033
|
cannam@147
|
1034 KJ_EXPECT(service.getRequestCount() == kj::size(PIPELINE_TESTS));
|
cannam@147
|
1035 }
|
cannam@147
|
1036
|
cannam@147
|
1037 KJ_TEST("HttpServer parallel pipeline") {
|
cannam@147
|
1038 auto PIPELINE_TESTS = pipelineTestCases();
|
cannam@147
|
1039
|
cannam@147
|
1040 auto io = kj::setupAsyncIo();
|
cannam@147
|
1041 auto pipe = io.provider->newTwoWayPipe();
|
cannam@147
|
1042
|
cannam@147
|
1043 auto allRequestText =
|
cannam@147
|
1044 kj::strArray(KJ_MAP(testCase, PIPELINE_TESTS) { return testCase.request.raw; }, "");
|
cannam@147
|
1045 auto allResponseText =
|
cannam@147
|
1046 kj::strArray(KJ_MAP(testCase, PIPELINE_TESTS) { return testCase.response.raw; }, "");
|
cannam@147
|
1047
|
cannam@147
|
1048 HttpHeaderTable table;
|
cannam@147
|
1049 TestHttpService service(PIPELINE_TESTS, table);
|
cannam@147
|
1050 HttpServer server(io.provider->getTimer(), table, service);
|
cannam@147
|
1051
|
cannam@147
|
1052 auto listenTask = server.listenHttp(kj::mv(pipe.ends[0]));
|
cannam@147
|
1053
|
cannam@147
|
1054 pipe.ends[1]->write(allRequestText.begin(), allRequestText.size()).wait(io.waitScope);
|
cannam@147
|
1055 pipe.ends[1]->shutdownWrite();
|
cannam@147
|
1056
|
cannam@147
|
1057 auto rawResponse = pipe.ends[1]->readAllText().wait(io.waitScope);
|
cannam@147
|
1058 KJ_EXPECT(rawResponse == allResponseText, rawResponse);
|
cannam@147
|
1059
|
cannam@147
|
1060 listenTask.wait(io.waitScope);
|
cannam@147
|
1061
|
cannam@147
|
1062 KJ_EXPECT(service.getRequestCount() == kj::size(PIPELINE_TESTS));
|
cannam@147
|
1063 }
|
cannam@147
|
1064
|
cannam@147
|
1065 KJ_TEST("HttpClient <-> HttpServer") {
|
cannam@147
|
1066 auto PIPELINE_TESTS = pipelineTestCases();
|
cannam@147
|
1067
|
cannam@147
|
1068 auto io = kj::setupAsyncIo();
|
cannam@147
|
1069 auto pipe = io.provider->newTwoWayPipe();
|
cannam@147
|
1070
|
cannam@147
|
1071 HttpHeaderTable table;
|
cannam@147
|
1072 TestHttpService service(PIPELINE_TESTS, table);
|
cannam@147
|
1073 HttpServer server(io.provider->getTimer(), table, service);
|
cannam@147
|
1074
|
cannam@147
|
1075 auto listenTask = server.listenHttp(kj::mv(pipe.ends[1]));
|
cannam@147
|
1076 auto client = newHttpClient(table, *pipe.ends[0]);
|
cannam@147
|
1077
|
cannam@147
|
1078 for (auto& testCase: PIPELINE_TESTS) {
|
cannam@147
|
1079 KJ_CONTEXT(testCase.request.raw, testCase.response.raw);
|
cannam@147
|
1080
|
cannam@147
|
1081 HttpHeaders headers(table);
|
cannam@147
|
1082 for (auto& header: testCase.request.requestHeaders) {
|
cannam@147
|
1083 headers.set(header.id, header.value);
|
cannam@147
|
1084 }
|
cannam@147
|
1085
|
cannam@147
|
1086 auto request = client->request(
|
cannam@147
|
1087 testCase.request.method, testCase.request.path, headers, testCase.request.requestBodySize);
|
cannam@147
|
1088 for (auto& part: testCase.request.requestBodyParts) {
|
cannam@147
|
1089 request.body->write(part.begin(), part.size()).wait(io.waitScope);
|
cannam@147
|
1090 }
|
cannam@147
|
1091 request.body = nullptr;
|
cannam@147
|
1092
|
cannam@147
|
1093 auto response = request.response.wait(io.waitScope);
|
cannam@147
|
1094
|
cannam@147
|
1095 KJ_EXPECT(response.statusCode == testCase.response.statusCode);
|
cannam@147
|
1096 auto body = response.body->readAllText().wait(io.waitScope);
|
cannam@147
|
1097 if (testCase.request.method == HttpMethod::HEAD) {
|
cannam@147
|
1098 KJ_EXPECT(body == "");
|
cannam@147
|
1099 } else {
|
cannam@147
|
1100 KJ_EXPECT(body == kj::strArray(testCase.response.responseBodyParts, ""), body);
|
cannam@147
|
1101 }
|
cannam@147
|
1102 }
|
cannam@147
|
1103
|
cannam@147
|
1104 client = nullptr;
|
cannam@147
|
1105 pipe.ends[0]->shutdownWrite();
|
cannam@147
|
1106 listenTask.wait(io.waitScope);
|
cannam@147
|
1107 KJ_EXPECT(service.getRequestCount() == kj::size(PIPELINE_TESTS));
|
cannam@147
|
1108 }
|
cannam@147
|
1109
|
cannam@147
|
1110 // -----------------------------------------------------------------------------
|
cannam@147
|
1111
|
cannam@147
|
1112 KJ_TEST("HttpServer request timeout") {
|
cannam@147
|
1113 auto PIPELINE_TESTS = pipelineTestCases();
|
cannam@147
|
1114
|
cannam@147
|
1115 auto io = kj::setupAsyncIo();
|
cannam@147
|
1116 auto pipe = io.provider->newTwoWayPipe();
|
cannam@147
|
1117
|
cannam@147
|
1118 HttpHeaderTable table;
|
cannam@147
|
1119 TestHttpService service(PIPELINE_TESTS, table);
|
cannam@147
|
1120 HttpServerSettings settings;
|
cannam@147
|
1121 settings.headerTimeout = 1 * kj::MILLISECONDS;
|
cannam@147
|
1122 HttpServer server(io.provider->getTimer(), table, service, settings);
|
cannam@147
|
1123
|
cannam@147
|
1124 // Shouldn't hang! Should time out.
|
cannam@147
|
1125 server.listenHttp(kj::mv(pipe.ends[0])).wait(io.waitScope);
|
cannam@147
|
1126
|
cannam@147
|
1127 // Sends back 408 Request Timeout.
|
cannam@147
|
1128 KJ_EXPECT(pipe.ends[1]->readAllText().wait(io.waitScope)
|
cannam@147
|
1129 .startsWith("HTTP/1.1 408 Request Timeout"));
|
cannam@147
|
1130 }
|
cannam@147
|
1131
|
cannam@147
|
1132 KJ_TEST("HttpServer pipeline timeout") {
|
cannam@147
|
1133 auto PIPELINE_TESTS = pipelineTestCases();
|
cannam@147
|
1134
|
cannam@147
|
1135 auto io = kj::setupAsyncIo();
|
cannam@147
|
1136 auto pipe = io.provider->newTwoWayPipe();
|
cannam@147
|
1137
|
cannam@147
|
1138 HttpHeaderTable table;
|
cannam@147
|
1139 TestHttpService service(PIPELINE_TESTS, table);
|
cannam@147
|
1140 HttpServerSettings settings;
|
cannam@147
|
1141 settings.pipelineTimeout = 1 * kj::MILLISECONDS;
|
cannam@147
|
1142 HttpServer server(io.provider->getTimer(), table, service, settings);
|
cannam@147
|
1143
|
cannam@147
|
1144 auto listenTask = server.listenHttp(kj::mv(pipe.ends[0]));
|
cannam@147
|
1145
|
cannam@147
|
1146 // Do one request.
|
cannam@147
|
1147 pipe.ends[1]->write(PIPELINE_TESTS[0].request.raw.begin(), PIPELINE_TESTS[0].request.raw.size())
|
cannam@147
|
1148 .wait(io.waitScope);
|
cannam@147
|
1149 expectRead(*pipe.ends[1], PIPELINE_TESTS[0].response.raw).wait(io.waitScope);
|
cannam@147
|
1150
|
cannam@147
|
1151 // Listen task should time out even though we didn't shutdown the socket.
|
cannam@147
|
1152 listenTask.wait(io.waitScope);
|
cannam@147
|
1153
|
cannam@147
|
1154 // In this case, no data is sent back.
|
cannam@147
|
1155 KJ_EXPECT(pipe.ends[1]->readAllText().wait(io.waitScope) == "");
|
cannam@147
|
1156 }
|
cannam@147
|
1157
|
cannam@147
|
1158 class BrokenHttpService final: public HttpService {
|
cannam@147
|
1159 // HttpService that doesn't send a response.
|
cannam@147
|
1160 public:
|
cannam@147
|
1161 BrokenHttpService() = default;
|
cannam@147
|
1162 explicit BrokenHttpService(kj::Exception&& exception): exception(kj::mv(exception)) {}
|
cannam@147
|
1163
|
cannam@147
|
1164 kj::Promise<void> request(
|
cannam@147
|
1165 HttpMethod method, kj::StringPtr url, const HttpHeaders& headers,
|
cannam@147
|
1166 kj::AsyncInputStream& requestBody, Response& responseSender) override {
|
cannam@147
|
1167 return requestBody.readAllBytes().then([this](kj::Array<byte>&&) -> kj::Promise<void> {
|
cannam@147
|
1168 KJ_IF_MAYBE(e, exception) {
|
cannam@147
|
1169 return kj::cp(*e);
|
cannam@147
|
1170 } else {
|
cannam@147
|
1171 return kj::READY_NOW;
|
cannam@147
|
1172 }
|
cannam@147
|
1173 });
|
cannam@147
|
1174 }
|
cannam@147
|
1175
|
cannam@147
|
1176 private:
|
cannam@147
|
1177 kj::Maybe<kj::Exception> exception;
|
cannam@147
|
1178 };
|
cannam@147
|
1179
|
cannam@147
|
1180 KJ_TEST("HttpServer no response") {
|
cannam@147
|
1181 auto PIPELINE_TESTS = pipelineTestCases();
|
cannam@147
|
1182
|
cannam@147
|
1183 auto io = kj::setupAsyncIo();
|
cannam@147
|
1184 auto pipe = io.provider->newTwoWayPipe();
|
cannam@147
|
1185
|
cannam@147
|
1186 HttpHeaderTable table;
|
cannam@147
|
1187 BrokenHttpService service;
|
cannam@147
|
1188 HttpServer server(io.provider->getTimer(), table, service);
|
cannam@147
|
1189
|
cannam@147
|
1190 auto listenTask = server.listenHttp(kj::mv(pipe.ends[0]));
|
cannam@147
|
1191
|
cannam@147
|
1192 // Do one request.
|
cannam@147
|
1193 pipe.ends[1]->write(PIPELINE_TESTS[0].request.raw.begin(), PIPELINE_TESTS[0].request.raw.size())
|
cannam@147
|
1194 .wait(io.waitScope);
|
cannam@147
|
1195 auto text = pipe.ends[1]->readAllText().wait(io.waitScope);
|
cannam@147
|
1196
|
cannam@147
|
1197 KJ_EXPECT(text ==
|
cannam@147
|
1198 "HTTP/1.1 500 Internal Server Error\r\n"
|
cannam@147
|
1199 "Connection: close\r\n"
|
cannam@147
|
1200 "Content-Length: 51\r\n"
|
cannam@147
|
1201 "Content-Type: text/plain\r\n"
|
cannam@147
|
1202 "\r\n"
|
cannam@147
|
1203 "ERROR: The HttpService did not generate a response.", text);
|
cannam@147
|
1204 }
|
cannam@147
|
1205
|
cannam@147
|
1206 KJ_TEST("HttpServer disconnected") {
|
cannam@147
|
1207 auto PIPELINE_TESTS = pipelineTestCases();
|
cannam@147
|
1208
|
cannam@147
|
1209 auto io = kj::setupAsyncIo();
|
cannam@147
|
1210 auto pipe = io.provider->newTwoWayPipe();
|
cannam@147
|
1211
|
cannam@147
|
1212 HttpHeaderTable table;
|
cannam@147
|
1213 BrokenHttpService service(KJ_EXCEPTION(DISCONNECTED, "disconnected"));
|
cannam@147
|
1214 HttpServer server(io.provider->getTimer(), table, service);
|
cannam@147
|
1215
|
cannam@147
|
1216 auto listenTask = server.listenHttp(kj::mv(pipe.ends[0]));
|
cannam@147
|
1217
|
cannam@147
|
1218 // Do one request.
|
cannam@147
|
1219 pipe.ends[1]->write(PIPELINE_TESTS[0].request.raw.begin(), PIPELINE_TESTS[0].request.raw.size())
|
cannam@147
|
1220 .wait(io.waitScope);
|
cannam@147
|
1221 auto text = pipe.ends[1]->readAllText().wait(io.waitScope);
|
cannam@147
|
1222
|
cannam@147
|
1223 KJ_EXPECT(text == "", text);
|
cannam@147
|
1224 }
|
cannam@147
|
1225
|
cannam@147
|
1226 KJ_TEST("HttpServer overloaded") {
|
cannam@147
|
1227 auto PIPELINE_TESTS = pipelineTestCases();
|
cannam@147
|
1228
|
cannam@147
|
1229 auto io = kj::setupAsyncIo();
|
cannam@147
|
1230 auto pipe = io.provider->newTwoWayPipe();
|
cannam@147
|
1231
|
cannam@147
|
1232 HttpHeaderTable table;
|
cannam@147
|
1233 BrokenHttpService service(KJ_EXCEPTION(OVERLOADED, "overloaded"));
|
cannam@147
|
1234 HttpServer server(io.provider->getTimer(), table, service);
|
cannam@147
|
1235
|
cannam@147
|
1236 auto listenTask = server.listenHttp(kj::mv(pipe.ends[0]));
|
cannam@147
|
1237
|
cannam@147
|
1238 // Do one request.
|
cannam@147
|
1239 pipe.ends[1]->write(PIPELINE_TESTS[0].request.raw.begin(), PIPELINE_TESTS[0].request.raw.size())
|
cannam@147
|
1240 .wait(io.waitScope);
|
cannam@147
|
1241 auto text = pipe.ends[1]->readAllText().wait(io.waitScope);
|
cannam@147
|
1242
|
cannam@147
|
1243 KJ_EXPECT(text.startsWith("HTTP/1.1 503 Service Unavailable"), text);
|
cannam@147
|
1244 }
|
cannam@147
|
1245
|
cannam@147
|
1246 KJ_TEST("HttpServer unimplemented") {
|
cannam@147
|
1247 auto PIPELINE_TESTS = pipelineTestCases();
|
cannam@147
|
1248
|
cannam@147
|
1249 auto io = kj::setupAsyncIo();
|
cannam@147
|
1250 auto pipe = io.provider->newTwoWayPipe();
|
cannam@147
|
1251
|
cannam@147
|
1252 HttpHeaderTable table;
|
cannam@147
|
1253 BrokenHttpService service(KJ_EXCEPTION(UNIMPLEMENTED, "unimplemented"));
|
cannam@147
|
1254 HttpServer server(io.provider->getTimer(), table, service);
|
cannam@147
|
1255
|
cannam@147
|
1256 auto listenTask = server.listenHttp(kj::mv(pipe.ends[0]));
|
cannam@147
|
1257
|
cannam@147
|
1258 // Do one request.
|
cannam@147
|
1259 pipe.ends[1]->write(PIPELINE_TESTS[0].request.raw.begin(), PIPELINE_TESTS[0].request.raw.size())
|
cannam@147
|
1260 .wait(io.waitScope);
|
cannam@147
|
1261 auto text = pipe.ends[1]->readAllText().wait(io.waitScope);
|
cannam@147
|
1262
|
cannam@147
|
1263 KJ_EXPECT(text.startsWith("HTTP/1.1 501 Not Implemented"), text);
|
cannam@147
|
1264 }
|
cannam@147
|
1265
|
cannam@147
|
1266 KJ_TEST("HttpServer threw exception") {
|
cannam@147
|
1267 auto PIPELINE_TESTS = pipelineTestCases();
|
cannam@147
|
1268
|
cannam@147
|
1269 auto io = kj::setupAsyncIo();
|
cannam@147
|
1270 auto pipe = io.provider->newTwoWayPipe();
|
cannam@147
|
1271
|
cannam@147
|
1272 HttpHeaderTable table;
|
cannam@147
|
1273 BrokenHttpService service(KJ_EXCEPTION(FAILED, "failed"));
|
cannam@147
|
1274 HttpServer server(io.provider->getTimer(), table, service);
|
cannam@147
|
1275
|
cannam@147
|
1276 auto listenTask = server.listenHttp(kj::mv(pipe.ends[0]));
|
cannam@147
|
1277
|
cannam@147
|
1278 // Do one request.
|
cannam@147
|
1279 pipe.ends[1]->write(PIPELINE_TESTS[0].request.raw.begin(), PIPELINE_TESTS[0].request.raw.size())
|
cannam@147
|
1280 .wait(io.waitScope);
|
cannam@147
|
1281 auto text = pipe.ends[1]->readAllText().wait(io.waitScope);
|
cannam@147
|
1282
|
cannam@147
|
1283 KJ_EXPECT(text.startsWith("HTTP/1.1 500 Internal Server Error"), text);
|
cannam@147
|
1284 }
|
cannam@147
|
1285
|
cannam@147
|
1286 class PartialResponseService final: public HttpService {
|
cannam@147
|
1287 // HttpService that sends a partial response then throws.
|
cannam@147
|
1288 public:
|
cannam@147
|
1289 kj::Promise<void> request(
|
cannam@147
|
1290 HttpMethod method, kj::StringPtr url, const HttpHeaders& headers,
|
cannam@147
|
1291 kj::AsyncInputStream& requestBody, Response& response) override {
|
cannam@147
|
1292 return requestBody.readAllBytes()
|
cannam@147
|
1293 .then([this,&response](kj::Array<byte>&&) -> kj::Promise<void> {
|
cannam@147
|
1294 HttpHeaders headers(table);
|
cannam@147
|
1295 auto body = response.send(200, "OK", headers, 32);
|
cannam@147
|
1296 auto promise = body->write("foo", 3);
|
cannam@147
|
1297 return promise.attach(kj::mv(body)).then([]() -> kj::Promise<void> {
|
cannam@147
|
1298 return KJ_EXCEPTION(FAILED, "failed");
|
cannam@147
|
1299 });
|
cannam@147
|
1300 });
|
cannam@147
|
1301 }
|
cannam@147
|
1302
|
cannam@147
|
1303 private:
|
cannam@147
|
1304 kj::Maybe<kj::Exception> exception;
|
cannam@147
|
1305 HttpHeaderTable table;
|
cannam@147
|
1306 };
|
cannam@147
|
1307
|
cannam@147
|
1308 KJ_TEST("HttpServer threw exception after starting response") {
|
cannam@147
|
1309 auto PIPELINE_TESTS = pipelineTestCases();
|
cannam@147
|
1310
|
cannam@147
|
1311 auto io = kj::setupAsyncIo();
|
cannam@147
|
1312 auto pipe = io.provider->newTwoWayPipe();
|
cannam@147
|
1313
|
cannam@147
|
1314 HttpHeaderTable table;
|
cannam@147
|
1315 PartialResponseService service;
|
cannam@147
|
1316 HttpServer server(io.provider->getTimer(), table, service);
|
cannam@147
|
1317
|
cannam@147
|
1318 auto listenTask = server.listenHttp(kj::mv(pipe.ends[0]));
|
cannam@147
|
1319
|
cannam@147
|
1320 KJ_EXPECT_LOG(ERROR, "HttpService threw exception after generating a partial response");
|
cannam@147
|
1321
|
cannam@147
|
1322 // Do one request.
|
cannam@147
|
1323 pipe.ends[1]->write(PIPELINE_TESTS[0].request.raw.begin(), PIPELINE_TESTS[0].request.raw.size())
|
cannam@147
|
1324 .wait(io.waitScope);
|
cannam@147
|
1325 auto text = pipe.ends[1]->readAllText().wait(io.waitScope);
|
cannam@147
|
1326
|
cannam@147
|
1327 KJ_EXPECT(text ==
|
cannam@147
|
1328 "HTTP/1.1 200 OK\r\n"
|
cannam@147
|
1329 "Content-Length: 32\r\n"
|
cannam@147
|
1330 "\r\n"
|
cannam@147
|
1331 "foo", text);
|
cannam@147
|
1332 }
|
cannam@147
|
1333
|
cannam@147
|
1334 // -----------------------------------------------------------------------------
|
cannam@147
|
1335
|
cannam@147
|
1336 KJ_TEST("HttpClient to capnproto.org") {
|
cannam@147
|
1337 auto io = kj::setupAsyncIo();
|
cannam@147
|
1338
|
cannam@147
|
1339 auto maybeConn = io.provider->getNetwork().parseAddress("capnproto.org", 80)
|
cannam@147
|
1340 .then([](kj::Own<kj::NetworkAddress> addr) {
|
cannam@147
|
1341 auto promise = addr->connect();
|
cannam@147
|
1342 return promise.attach(kj::mv(addr));
|
cannam@147
|
1343 }).then([](kj::Own<kj::AsyncIoStream>&& connection) -> kj::Maybe<kj::Own<kj::AsyncIoStream>> {
|
cannam@147
|
1344 return kj::mv(connection);
|
cannam@147
|
1345 }, [](kj::Exception&& e) -> kj::Maybe<kj::Own<kj::AsyncIoStream>> {
|
cannam@147
|
1346 KJ_LOG(WARNING, "skipping test because couldn't connect to capnproto.org");
|
cannam@147
|
1347 return nullptr;
|
cannam@147
|
1348 }).wait(io.waitScope);
|
cannam@147
|
1349
|
cannam@147
|
1350 KJ_IF_MAYBE(conn, maybeConn) {
|
cannam@147
|
1351 // Successfully connected to capnproto.org. Try doing GET /. We expect to get a redirect to
|
cannam@147
|
1352 // HTTPS, because what kind of horrible web site would serve in plaintext, really?
|
cannam@147
|
1353
|
cannam@147
|
1354 HttpHeaderTable table;
|
cannam@147
|
1355 auto client = newHttpClient(table, **conn);
|
cannam@147
|
1356
|
cannam@147
|
1357 HttpHeaders headers(table);
|
cannam@147
|
1358 headers.set(HttpHeaderId::HOST, "capnproto.org");
|
cannam@147
|
1359
|
cannam@147
|
1360 auto response = client->request(HttpMethod::GET, "/", headers).response.wait(io.waitScope);
|
cannam@147
|
1361 KJ_EXPECT(response.statusCode / 100 == 3);
|
cannam@147
|
1362 auto location = KJ_ASSERT_NONNULL(response.headers->get(HttpHeaderId::LOCATION));
|
cannam@147
|
1363 KJ_EXPECT(location == "https://capnproto.org/");
|
cannam@147
|
1364
|
cannam@147
|
1365 auto body = response.body->readAllText().wait(io.waitScope);
|
cannam@147
|
1366 }
|
cannam@147
|
1367 }
|
cannam@147
|
1368
|
cannam@147
|
1369 } // namespace
|
cannam@147
|
1370 } // namespace kj
|