Unverified Commit 2ec71986 authored by Harris Hancock's avatar Harris Hancock Committed by GitHub

Merge pull request #866 from capnproto/harris/http-include-raw-content-in-protocol-errors

Report raw HTTP content when handling client protocol errors in kj-http
parents 81b2a4c2 350b2f2b
...@@ -24,6 +24,7 @@ ...@@ -24,6 +24,7 @@
#include "http.h" #include "http.h"
#include <kj/debug.h> #include <kj/debug.h>
#include <kj/test.h> #include <kj/test.h>
#include <kj/encoding.h>
#include <map> #include <map>
#if KJ_HTTP_TEST_USE_OS_PIPE #if KJ_HTTP_TEST_USE_OS_PIPE
...@@ -126,7 +127,7 @@ KJ_TEST("HttpHeaders::parseRequest") { ...@@ -126,7 +127,7 @@ KJ_TEST("HttpHeaders::parseRequest") {
"DATE: early\r\n" "DATE: early\r\n"
"other-Header: yep\r\n" "other-Header: yep\r\n"
"\r\n"); "\r\n");
auto result = KJ_ASSERT_NONNULL(headers.tryParseRequest(text.asArray())); auto result = headers.tryParseRequest(text.asArray()).get<HttpHeaders::Request>();
KJ_EXPECT(result.method == HttpMethod::POST); KJ_EXPECT(result.method == HttpMethod::POST);
KJ_EXPECT(result.url == "/some/path"); KJ_EXPECT(result.url == "/some/path");
...@@ -176,7 +177,7 @@ KJ_TEST("HttpHeaders::parseResponse") { ...@@ -176,7 +177,7 @@ KJ_TEST("HttpHeaders::parseResponse") {
"DATE: early\r\n" "DATE: early\r\n"
"other-Header: yep\r\n" "other-Header: yep\r\n"
"\r\n"); "\r\n");
auto result = KJ_ASSERT_NONNULL(headers.tryParseResponse(text.asArray())); auto result = headers.tryParseResponse(text.asArray()).get<HttpHeaders::Response>();
KJ_EXPECT(result.statusCode == 418); KJ_EXPECT(result.statusCode == 418);
KJ_EXPECT(result.statusText == "I'm a teapot"); KJ_EXPECT(result.statusText == "I'm a teapot");
...@@ -215,40 +216,72 @@ KJ_TEST("HttpHeaders parse invalid") { ...@@ -215,40 +216,72 @@ KJ_TEST("HttpHeaders parse invalid") {
HttpHeaders headers(*table); HttpHeaders headers(*table);
// NUL byte in request. // NUL byte in request.
KJ_EXPECT(headers.tryParseRequest(kj::heapString( {
"POST \0 /some/path \t HTTP/1.1\r\n" auto input = kj::heapString(
"Foo-BaR: Baz\r\n" "POST \0 /some/path \t HTTP/1.1\r\n"
"Host: example.com\r\n" "Foo-BaR: Baz\r\n"
"DATE: early\r\n" "Host: example.com\r\n"
"other-Header: yep\r\n" "DATE: early\r\n"
"\r\n")) == nullptr); "other-Header: yep\r\n"
"\r\n");
auto protocolError = headers.tryParseRequest(input).get<HttpHeaders::ProtocolError>();
KJ_EXPECT(protocolError.description == "ERROR: Request headers have no terminal newline.",
protocolError.description);
KJ_EXPECT(protocolError.rawContent.asChars() == input);
}
// Control character in header name. // Control character in header name.
KJ_EXPECT(headers.tryParseRequest(kj::heapString( {
"POST /some/path \t HTTP/1.1\r\n" auto input = kj::heapString(
"Foo-BaR: Baz\r\n" "POST /some/path \t HTTP/1.1\r\n"
"Cont\001ent-Length: 123\r\n" "Foo-BaR: Baz\r\n"
"DATE: early\r\n" "Cont\001ent-Length: 123\r\n"
"other-Header: yep\r\n" "DATE: early\r\n"
"\r\n")) == nullptr); "other-Header: yep\r\n"
"\r\n");
auto protocolError = headers.tryParseRequest(input).get<HttpHeaders::ProtocolError>();
KJ_EXPECT(protocolError.description == "ERROR: The headers sent by your client are not valid.",
protocolError.description);
KJ_EXPECT(protocolError.rawContent.asChars() == input);
}
// Separator character in header name. // Separator character in header name.
KJ_EXPECT(headers.tryParseRequest(kj::heapString( {
"POST /some/path \t HTTP/1.1\r\n" auto input = kj::heapString(
"Foo-BaR: Baz\r\n" "POST /some/path \t HTTP/1.1\r\n"
"Host: example.com\r\n" "Foo-BaR: Baz\r\n"
"DATE/: early\r\n" "Host: example.com\r\n"
"other-Header: yep\r\n" "DATE/: early\r\n"
"\r\n")) == nullptr); "other-Header: yep\r\n"
"\r\n");
auto protocolError = headers.tryParseRequest(input).get<HttpHeaders::ProtocolError>();
KJ_EXPECT(protocolError.description == "ERROR: The headers sent by your client are not valid.",
protocolError.description);
KJ_EXPECT(protocolError.rawContent.asChars() == input);
}
// Response status code not numeric. // Response status code not numeric.
KJ_EXPECT(headers.tryParseResponse(kj::heapString( {
auto input = kj::heapString(
"HTTP/1.1\t\t abc\t I'm a teapot\r\n" "HTTP/1.1\t\t abc\t I'm a teapot\r\n"
"Foo-BaR: Baz\r\n" "Foo-BaR: Baz\r\n"
"Host: example.com\r\n" "Host: example.com\r\n"
"DATE: early\r\n" "DATE: early\r\n"
"other-Header: yep\r\n" "other-Header: yep\r\n"
"\r\n")) == nullptr); "\r\n");
auto protocolError = headers.tryParseRequest(input).get<HttpHeaders::ProtocolError>();
KJ_EXPECT(protocolError.description == "ERROR: Unrecognized request method.",
protocolError.description);
KJ_EXPECT(protocolError.rawContent.asChars() == input);
}
} }
KJ_TEST("HttpHeaders validation") { KJ_TEST("HttpHeaders validation") {
...@@ -2420,10 +2453,10 @@ KJ_TEST("HttpServer bad request") { ...@@ -2420,10 +2453,10 @@ KJ_TEST("HttpServer bad request") {
static constexpr auto expectedResponse = static constexpr auto expectedResponse =
"HTTP/1.1 400 Bad Request\r\n" "HTTP/1.1 400 Bad Request\r\n"
"Connection: close\r\n" "Connection: close\r\n"
"Content-Length: 54\r\n" "Content-Length: 35\r\n"
"Content-Type: text/plain\r\n" "Content-Type: text/plain\r\n"
"\r\n" "\r\n"
"ERROR: The headers sent by your client were not valid."_kj; "ERROR: Unrecognized request method."_kj;
KJ_EXPECT(expectedResponse == response, expectedResponse, response); KJ_EXPECT(expectedResponse == response, expectedResponse, response);
} }
...@@ -2434,8 +2467,11 @@ KJ_UNUSED static constexpr HttpServerSettings STATIC_CONSTEXPR_SETTINGS {}; ...@@ -2434,8 +2467,11 @@ KJ_UNUSED static constexpr HttpServerSettings STATIC_CONSTEXPR_SETTINGS {};
class TestErrorHandler: public HttpServerErrorHandler { class TestErrorHandler: public HttpServerErrorHandler {
public: public:
kj::Promise<void> handleClientProtocolError( kj::Promise<void> handleClientProtocolError(
kj::StringPtr message, kj::HttpService::Response& response) override { HttpHeaders::ProtocolError protocolError, kj::HttpService::Response& response) override {
return sendError(400, "Bad Request", kj::str("Saw protocol error: ", message), response); // In a real error handler, you should redact `protocolError.rawContent`.
auto message = kj::str("Saw protocol error: ", protocolError.description, "; rawContent = ",
encodeCEscape(protocolError.rawContent));
return sendError(400, "Bad Request", kj::mv(message), response);
} }
kj::Promise<void> handleApplicationError( kj::Promise<void> handleApplicationError(
...@@ -2548,9 +2584,10 @@ KJ_TEST("HttpServer bad request, custom error handler") { ...@@ -2548,9 +2584,10 @@ KJ_TEST("HttpServer bad request, custom error handler") {
static constexpr auto expectedResponse = static constexpr auto expectedResponse =
"HTTP/1.1 400 Bad Request\r\n" "HTTP/1.1 400 Bad Request\r\n"
"Connection: close\r\n" "Connection: close\r\n"
"Content-Length: 74\r\n" "Content-Length: 87\r\n"
"\r\n" "\r\n"
"Saw protocol error: ERROR: The headers sent by your client were not valid."_kj; "Saw protocol error: ERROR: Unrecognized request method.; "
"rawContent = bad request\\000\\n"_kj;
KJ_EXPECT(expectedResponse == response, expectedResponse, response); KJ_EXPECT(expectedResponse == response, expectedResponse, response);
} }
......
...@@ -845,9 +845,11 @@ static char* trimHeaderEnding(kj::ArrayPtr<char> content) { ...@@ -845,9 +845,11 @@ static char* trimHeaderEnding(kj::ArrayPtr<char> content) {
return end; return end;
} }
kj::Maybe<HttpHeaders::Request> HttpHeaders::tryParseRequest(kj::ArrayPtr<char> content) { HttpHeaders::RequestOrProtocolError HttpHeaders::tryParseRequest(kj::ArrayPtr<char> content) {
char* end = trimHeaderEnding(content); char* end = trimHeaderEnding(content);
if (end == nullptr) return nullptr; if (end == nullptr) {
return ProtocolError { "ERROR: Request headers have no terminal newline.", content };
}
char* ptr = content.begin(); char* ptr = content.begin();
...@@ -856,50 +858,58 @@ kj::Maybe<HttpHeaders::Request> HttpHeaders::tryParseRequest(kj::ArrayPtr<char> ...@@ -856,50 +858,58 @@ kj::Maybe<HttpHeaders::Request> HttpHeaders::tryParseRequest(kj::ArrayPtr<char>
KJ_IF_MAYBE(method, consumeHttpMethod(ptr)) { KJ_IF_MAYBE(method, consumeHttpMethod(ptr)) {
request.method = *method; request.method = *method;
if (*ptr != ' ' && *ptr != '\t') { if (*ptr != ' ' && *ptr != '\t') {
return nullptr; return ProtocolError { "ERROR: Unrecognized request method.", content };
} }
++ptr; ++ptr;
} else { } else {
return nullptr; return ProtocolError { "ERROR: Unrecognized request method.", content };
} }
KJ_IF_MAYBE(path, consumeWord(ptr)) { KJ_IF_MAYBE(path, consumeWord(ptr)) {
request.url = *path; request.url = *path;
} else { } else {
return nullptr; return ProtocolError { "ERROR: Invalid request line.", content };
} }
// Ignore rest of line. Don't care about "HTTP/1.1" or whatever. // Ignore rest of line. Don't care about "HTTP/1.1" or whatever.
consumeLine(ptr); consumeLine(ptr);
if (!parseHeaders(ptr, end)) return nullptr; if (!parseHeaders(ptr, end)) {
return ProtocolError { "ERROR: The headers sent by your client are not valid.", content };
}
return request; return request;
} }
kj::Maybe<HttpHeaders::Response> HttpHeaders::tryParseResponse(kj::ArrayPtr<char> content) { HttpHeaders::ResponseOrProtocolError HttpHeaders::tryParseResponse(kj::ArrayPtr<char> content) {
char* end = trimHeaderEnding(content); char* end = trimHeaderEnding(content);
if (end == nullptr) return nullptr; if (end == nullptr) {
return ProtocolError { "ERROR: Response headers have no terminal newline.", content };
}
char* ptr = content.begin(); char* ptr = content.begin();
HttpHeaders::Response response; HttpHeaders::Response response;
KJ_IF_MAYBE(version, consumeWord(ptr)) { KJ_IF_MAYBE(version, consumeWord(ptr)) {
if (!version->startsWith("HTTP/")) return nullptr; if (!version->startsWith("HTTP/")) {
return ProtocolError { "ERROR: Invalid response status line.", content };
}
} else { } else {
return nullptr; return ProtocolError { "ERROR: Invalid response status line.", content };
} }
KJ_IF_MAYBE(code, consumeNumber(ptr)) { KJ_IF_MAYBE(code, consumeNumber(ptr)) {
response.statusCode = *code; response.statusCode = *code;
} else { } else {
return nullptr; return ProtocolError { "ERROR: Invalid response status code.", content };
} }
response.statusText = consumeLine(ptr); response.statusText = consumeLine(ptr);
if (!parseHeaders(ptr, end)) return nullptr; if (!parseHeaders(ptr, end)) {
return ProtocolError { "ERROR: The headers sent by the server are not valid.", content };
}
return response; return response;
} }
...@@ -1012,8 +1022,10 @@ public: ...@@ -1012,8 +1022,10 @@ public:
kj::Promise<Request> readRequest() override { kj::Promise<Request> readRequest() override {
return readRequestHeaders() return readRequestHeaders()
.then([this](kj::Maybe<HttpHeaders::Request>&& maybeRequest) -> HttpInputStream::Request { .then([this](HttpHeaders::RequestOrProtocolError&& requestOrProtocolError)
auto request = KJ_REQUIRE_NONNULL(maybeRequest, "bad request"); -> HttpInputStream::Request {
auto request = KJ_REQUIRE_NONNULL(
requestOrProtocolError.tryGet<HttpHeaders::Request>(), "bad request");
auto body = getEntityBody(HttpInputStreamImpl::REQUEST, request.method, 0, headers); auto body = getEntityBody(HttpInputStreamImpl::REQUEST, request.method, 0, headers);
return { request.method, request.url, headers, kj::mv(body) }; return { request.method, request.url, headers, kj::mv(body) };
...@@ -1022,9 +1034,10 @@ public: ...@@ -1022,9 +1034,10 @@ public:
kj::Promise<Response> readResponse(HttpMethod requestMethod) override { kj::Promise<Response> readResponse(HttpMethod requestMethod) override {
return readResponseHeaders() return readResponseHeaders()
.then([this,requestMethod](kj::Maybe<HttpHeaders::Response>&& maybeResponse) .then([this,requestMethod](HttpHeaders::ResponseOrProtocolError&& responseOrProtocolError)
-> HttpInputStream::Response { -> HttpInputStream::Response {
auto response = KJ_REQUIRE_NONNULL(maybeResponse, "bad response"); auto response = KJ_REQUIRE_NONNULL(
responseOrProtocolError.tryGet<HttpHeaders::Response>(), "bad response");
auto body = getEntityBody(HttpInputStreamImpl::RESPONSE, requestMethod, 0, headers); auto body = getEntityBody(HttpInputStreamImpl::RESPONSE, requestMethod, 0, headers);
return { response.statusCode, response.statusText, headers, kj::mv(body) }; return { response.statusCode, response.statusText, headers, kj::mv(body) };
...@@ -1148,14 +1161,14 @@ public: ...@@ -1148,14 +1161,14 @@ public:
}); });
} }
inline kj::Promise<kj::Maybe<HttpHeaders::Request>> readRequestHeaders() { inline kj::Promise<HttpHeaders::RequestOrProtocolError> readRequestHeaders() {
return readMessageHeaders().then([this](kj::ArrayPtr<char> text) { return readMessageHeaders().then([this](kj::ArrayPtr<char> text) {
headers.clear(); headers.clear();
return headers.tryParseRequest(text); return headers.tryParseRequest(text);
}); });
} }
inline kj::Promise<kj::Maybe<HttpHeaders::Response>> readResponseHeaders() { inline kj::Promise<HttpHeaders::ResponseOrProtocolError> readResponseHeaders() {
// Note: readResponseHeaders() could be called multiple times concurrently when pipelining // Note: readResponseHeaders() could be called multiple times concurrently when pipelining
// requests. readMessageHeaders() will serialize these, but it's important not to mess with // requests. readMessageHeaders() will serialize these, but it's important not to mess with
// state (like calling headers.clear()) before said serialization has taken place. // state (like calling headers.clear()) before said serialization has taken place.
...@@ -1276,7 +1289,7 @@ private: ...@@ -1276,7 +1289,7 @@ private:
readPromise = leftover.size(); readPromise = leftover.size();
leftover = nullptr; leftover = nullptr;
} else { } else {
// Need to read more data from the unfderlying stream. // Need to read more data from the underlying stream.
if (bufferEnd == headerBuffer.size()) { if (bufferEnd == headerBuffer.size()) {
// Out of buffer space. // Out of buffer space.
...@@ -3231,31 +3244,41 @@ public: ...@@ -3231,31 +3244,41 @@ public:
auto id = ++counter; auto id = ++counter;
auto responsePromise = httpInput.readResponseHeaders().then( auto responsePromise = httpInput.readResponseHeaders().then(
[this,method,id](kj::Maybe<HttpHeaders::Response>&& response) -> HttpClient::Response { [this,method,id](HttpHeaders::ResponseOrProtocolError&& responseOrProtocolError)
KJ_IF_MAYBE(r, response) { -> HttpClient::Response {
auto& headers = httpInput.getHeaders(); KJ_SWITCH_ONEOF(responseOrProtocolError) {
HttpClient::Response result { KJ_CASE_ONEOF(response, HttpHeaders::Response) {
r->statusCode, auto& responseHeaders = httpInput.getHeaders();
r->statusText, HttpClient::Response result {
&headers, response.statusCode,
httpInput.getEntityBody(HttpInputStreamImpl::RESPONSE, method, r->statusCode, headers) response.statusText,
}; &responseHeaders,
httpInput.getEntityBody(
HttpInputStreamImpl::RESPONSE, method, response.statusCode, responseHeaders)
};
if (fastCaseCmp<'c', 'l', 'o', 's', 'e'>( if (fastCaseCmp<'c', 'l', 'o', 's', 'e'>(
headers.get(HttpHeaderId::CONNECTION).orDefault(nullptr).cStr())) { responseHeaders.get(HttpHeaderId::CONNECTION).orDefault(nullptr).cStr())) {
closed = true;
} else if (counter == id) {
watchForClose();
} else {
// Another request was already queued after this one, so we don't want to watch for
// stream closure because we're fully expecting another response.
}
return result;
}
KJ_CASE_ONEOF(protocolError, HttpHeaders::ProtocolError) {
closed = true; closed = true;
} else if (counter == id) { // TODO(someday): Do something with ProtocolError::rawContent. Exceptions feel like the
watchForClose(); // most idiomatic way to report errors when using HttpClient::request(), but we don't
} else { // have a good way of attaching the raw content to the exception.
// Anothe request was already queued after this one, so we don't want to watch for KJ_FAIL_REQUIRE(protocolError.description) { break; }
// stream closure because we're fully expecting another response. return HttpClient::Response();
} }
return result;
} else {
closed = true;
KJ_FAIL_REQUIRE("received invalid HTTP response") { break; }
return HttpClient::Response();
} }
KJ_UNREACHABLE;
}); });
return { kj::mv(bodyStream), kj::mv(responsePromise) }; return { kj::mv(bodyStream), kj::mv(responsePromise) };
...@@ -3294,61 +3317,69 @@ public: ...@@ -3294,61 +3317,69 @@ public:
auto id = ++counter; auto id = ++counter;
return httpInput.readResponseHeaders() return httpInput.readResponseHeaders()
.then(kj::mvCapture(keyBase64, .then([this,id,keyBase64 = kj::mv(keyBase64)](
[this,id](kj::StringPtr keyBase64, kj::Maybe<HttpHeaders::Response>&& response) HttpHeaders::ResponseOrProtocolError&& responseOrProtocolError)
-> HttpClient::WebSocketResponse { -> HttpClient::WebSocketResponse {
KJ_IF_MAYBE(r, response) { KJ_SWITCH_ONEOF(responseOrProtocolError) {
auto& headers = httpInput.getHeaders(); KJ_CASE_ONEOF(response, HttpHeaders::Response) {
if (r->statusCode == 101) { auto& responseHeaders = httpInput.getHeaders();
if (!fastCaseCmp<'w', 'e', 'b', 's', 'o', 'c', 'k', 'e', 't'>( if (response.statusCode == 101) {
headers.get(HttpHeaderId::UPGRADE).orDefault(nullptr).cStr())) { if (!fastCaseCmp<'w', 'e', 'b', 's', 'o', 'c', 'k', 'e', 't'>(
KJ_FAIL_REQUIRE("server returned incorrect Upgrade header; should be 'websocket'", responseHeaders.get(HttpHeaderId::UPGRADE).orDefault(nullptr).cStr())) {
headers.get(HttpHeaderId::UPGRADE).orDefault("(null)")) { KJ_FAIL_REQUIRE("server returned incorrect Upgrade header; should be 'websocket'",
break; responseHeaders.get(HttpHeaderId::UPGRADE).orDefault("(null)")) {
break;
}
return HttpClient::WebSocketResponse();
} }
return HttpClient::WebSocketResponse();
}
auto expectedAccept = generateWebSocketAccept(keyBase64); auto expectedAccept = generateWebSocketAccept(keyBase64);
if (headers.get(HttpHeaderId::SEC_WEBSOCKET_ACCEPT).orDefault(nullptr) if (responseHeaders.get(HttpHeaderId::SEC_WEBSOCKET_ACCEPT).orDefault(nullptr)
!= expectedAccept) { != expectedAccept) {
KJ_FAIL_REQUIRE("server returned incorrect Sec-WebSocket-Accept header", KJ_FAIL_REQUIRE("server returned incorrect Sec-WebSocket-Accept header",
headers.get(HttpHeaderId::SEC_WEBSOCKET_ACCEPT).orDefault("(null)"), responseHeaders.get(HttpHeaderId::SEC_WEBSOCKET_ACCEPT).orDefault("(null)"),
expectedAccept) { break; } expectedAccept) { break; }
return HttpClient::WebSocketResponse(); return HttpClient::WebSocketResponse();
} }
return { return {
r->statusCode, response.statusCode,
r->statusText, response.statusText,
&httpInput.getHeaders(), &httpInput.getHeaders(),
upgradeToWebSocket(kj::mv(ownStream), httpInput, httpOutput, settings.entropySource), upgradeToWebSocket(kj::mv(ownStream), httpInput, httpOutput, settings.entropySource),
}; };
} else {
upgraded = false;
HttpClient::WebSocketResponse result {
r->statusCode,
r->statusText,
&headers,
httpInput.getEntityBody(HttpInputStreamImpl::RESPONSE, HttpMethod::GET, r->statusCode,
headers)
};
if (fastCaseCmp<'c', 'l', 'o', 's', 'e'>(
headers.get(HttpHeaderId::CONNECTION).orDefault(nullptr).cStr())) {
closed = true;
} else if (counter == id) {
watchForClose();
} else { } else {
// Anothe request was already queued after this one, so we don't want to watch for upgraded = false;
// stream closure because we're fully expecting another response. HttpClient::WebSocketResponse result {
response.statusCode,
response.statusText,
&responseHeaders,
httpInput.getEntityBody(HttpInputStreamImpl::RESPONSE, HttpMethod::GET,
response.statusCode, responseHeaders)
};
if (fastCaseCmp<'c', 'l', 'o', 's', 'e'>(
responseHeaders.get(HttpHeaderId::CONNECTION).orDefault(nullptr).cStr())) {
closed = true;
} else if (counter == id) {
watchForClose();
} else {
// Another request was already queued after this one, so we don't want to watch for
// stream closure because we're fully expecting another response.
}
return result;
} }
return result;
} }
} else { KJ_CASE_ONEOF(protocolError, HttpHeaders::ProtocolError) {
KJ_FAIL_REQUIRE("received invalid HTTP response") { break; } // TODO(someday): Do something with ProtocolError::rawContent. Exceptions feel like the
return HttpClient::WebSocketResponse(); // most idiomatic way to report errors when using HttpClient::request(), but we don't
// have a good way of attaching the raw content to the exception.
KJ_FAIL_REQUIRE(protocolError.description) { break; }
return HttpClient::WebSocketResponse();
}
} }
}));
KJ_UNREACHABLE;
});
} }
private: private:
...@@ -4679,7 +4710,8 @@ public: ...@@ -4679,7 +4710,8 @@ public:
} }
auto receivedHeaders = firstByte auto receivedHeaders = firstByte
.then([this,firstRequest](bool hasData)-> kj::Promise<kj::Maybe<HttpHeaders::Request>> { .then([this,firstRequest](bool hasData)
-> kj::Promise<HttpHeaders::RequestOrProtocolError> {
if (hasData) { if (hasData) {
auto readHeaders = httpInput.readRequestHeaders(); auto readHeaders = httpInput.readRequestHeaders();
if (!firstRequest) { if (!firstRequest) {
...@@ -4687,9 +4719,11 @@ public: ...@@ -4687,9 +4719,11 @@ public:
// the first byte of a pipeline response. // the first byte of a pipeline response.
readHeaders = readHeaders.exclusiveJoin( readHeaders = readHeaders.exclusiveJoin(
server.timer.afterDelay(server.settings.headerTimeout) server.timer.afterDelay(server.settings.headerTimeout)
.then([this]() -> kj::Maybe<HttpHeaders::Request> { .then([this]() -> HttpHeaders::RequestOrProtocolError {
timedOut = true; timedOut = true;
return nullptr; return HttpHeaders::ProtocolError {
"ERROR: Timed out waiting for next request headers.", nullptr
};
})); }));
} }
return kj::mv(readHeaders); return kj::mv(readHeaders);
...@@ -4697,7 +4731,10 @@ public: ...@@ -4697,7 +4731,10 @@ public:
// Client closed connection or pipeline timed out with no bytes received. This is not an // Client closed connection or pipeline timed out with no bytes received. This is not an
// error, so don't report one. // error, so don't report one.
this->closed = true; this->closed = true;
return kj::Maybe<HttpHeaders::Request>(nullptr); return HttpHeaders::RequestOrProtocolError(HttpHeaders::ProtocolError {
"ERROR: Client closed connection or connection timeout "
"while waiting for request headers.", nullptr
});
} }
}); });
...@@ -4705,15 +4742,18 @@ public: ...@@ -4705,15 +4742,18 @@ public:
// On the first request, the header timeout starts ticking immediately upon request opening. // On the first request, the header timeout starts ticking immediately upon request opening.
auto timeoutPromise = server.timer.afterDelay(server.settings.headerTimeout) auto timeoutPromise = server.timer.afterDelay(server.settings.headerTimeout)
.exclusiveJoin(server.onDrain.addBranch()) .exclusiveJoin(server.onDrain.addBranch())
.then([this]() -> kj::Maybe<HttpHeaders::Request> { .then([this]() -> HttpHeaders::RequestOrProtocolError {
timedOut = true; timedOut = true;
return nullptr; return HttpHeaders::ProtocolError {
"ERROR: Timed out waiting for initial request headers.", nullptr
};
}); });
receivedHeaders = receivedHeaders.exclusiveJoin(kj::mv(timeoutPromise)); receivedHeaders = receivedHeaders.exclusiveJoin(kj::mv(timeoutPromise));
} }
return receivedHeaders return receivedHeaders
.then([this](kj::Maybe<HttpHeaders::Request>&& request) -> kj::Promise<bool> { .then([this](HttpHeaders::RequestOrProtocolError&& requestOrProtocolError)
-> kj::Promise<bool> {
if (timedOut) { if (timedOut) {
// Client took too long to send anything, so we're going to close the connection. In // Client took too long to send anything, so we're going to close the connection. In
// theory, we should send back an HTTP 408 error -- it is designed exactly for this // theory, we should send back an HTTP 408 error -- it is designed exactly for this
...@@ -4739,118 +4779,122 @@ public: ...@@ -4739,118 +4779,122 @@ public:
return httpOutput.flush().then([]() { return false; }); return httpOutput.flush().then([]() { return false; });
} }
KJ_IF_MAYBE(req, request) { KJ_SWITCH_ONEOF(requestOrProtocolError) {
auto& headers = httpInput.getHeaders(); KJ_CASE_ONEOF(request, HttpHeaders::Request) {
auto& headers = httpInput.getHeaders();
currentMethod = req->method;
auto body = httpInput.getEntityBody( currentMethod = request.method;
HttpInputStreamImpl::REQUEST, req->method, 0, headers); auto body = httpInput.getEntityBody(
HttpInputStreamImpl::REQUEST, request.method, 0, headers);
// TODO(perf): If the client disconnects, should we cancel the response? Probably, to
// prevent permanent deadlock. It's slightly weird in that arguably the client should // TODO(perf): If the client disconnects, should we cancel the response? Probably, to
// be able to shutdown the upstream but still wait on the downstream, but I believe many // prevent permanent deadlock. It's slightly weird in that arguably the client should
// other HTTP servers do similar things. // be able to shutdown the upstream but still wait on the downstream, but I believe many
// other HTTP servers do similar things.
auto promise = service.request(
req->method, req->url, headers, *body, *this); auto promise = service.request(
return promise.then(kj::mvCapture(body, request.method, request.url, headers, *body, *this);
[this](kj::Own<kj::AsyncInputStream> body) -> kj::Promise<bool> { return promise.then([this, body = kj::mv(body)]() mutable -> kj::Promise<bool> {
// Response done. Await next request. // Response done. Await next request.
KJ_IF_MAYBE(p, webSocketError) { KJ_IF_MAYBE(p, webSocketError) {
// sendWebSocketError() was called. Finish sending and close the connection. // sendWebSocketError() was called. Finish sending and close the connection.
auto promise = kj::mv(*p); auto promise = kj::mv(*p);
webSocketError = nullptr; webSocketError = nullptr;
return kj::mv(promise); return kj::mv(promise);
}
if (upgraded) {
// We've upgraded to WebSocket, and by now we should have closed the WebSocket.
if (!webSocketClosed) {
// This is gonna segfault later so abort now instead.
KJ_LOG(FATAL, "Accepted WebSocket object must be destroyed before HttpService "
"request handler completes.");
abort();
} }
// Once we start a WebSocket there's no going back to HTTP. if (upgraded) {
return false; // We've upgraded to WebSocket, and by now we should have closed the WebSocket.
} if (!webSocketClosed) {
// This is gonna segfault later so abort now instead.
if (currentMethod != nullptr) { KJ_LOG(FATAL, "Accepted WebSocket object must be destroyed before HttpService "
return sendError(); "request handler completes.");
} abort();
}
if (httpOutput.isBroken()) {
// We started a response but didn't finish it. But HttpService returns success? Perhaps // Once we start a WebSocket there's no going back to HTTP.
// it decided that it doesn't want to finish this response. We'll have to disconnect return false;
// here. If the response body is not complete (e.g. Content-Length not reached), the }
// client should notice. We don't want to log an error because this condition might be
// intentional on the service's part.
return false;
}
return httpOutput.flush().then(kj::mvCapture(body, if (currentMethod != nullptr) {
[this](kj::Own<kj::AsyncInputStream> body) -> kj::Promise<bool> { return sendError();
if (httpInput.canReuse()) { }
// Things look clean. Go ahead and accept the next request.
// Note that we don't have to handle server.draining here because we'll take care of if (httpOutput.isBroken()) {
// it the next time around the loop. // We started a response but didn't finish it. But HttpService returns success?
return loop(false); // Perhaps it decided that it doesn't want to finish this response. We'll have to
} else { // disconnect here. If the response body is not complete (e.g. Content-Length not
// Apparently, the application did not read the request body. Maybe this is a bug, // reached), the client should notice. We don't want to log an error because this
// or maybe not: maybe the client tried to upload too much data and the application // condition might be intentional on the service's part.
// legitimately wants to cancel the upload without reading all it it. return false;
//
// We have a problem, though: We did send a response, and we didn't send
// `Connection: close`, so the client may expect that it can send another request.
// Perhaps the client has even finished sending the previous request's body, in
// which case the moment it finishes receiving the response, it could be completely
// within its rights to start a new request. If we close the socket now, we might
// interrupt that new request.
//
// There's no way we can get out of this perfectly cleanly. HTTP just isn't good
// enough at connection management. The best we can do is give the client some grace
// period and then abort the connection.
auto dummy = kj::heap<HttpDiscardingEntityWriter>();
auto lengthGrace = body->pumpTo(*dummy, server.settings.canceledUploadGraceBytes)
.then([this](size_t amount) {
if (httpInput.canReuse()) {
// Success, we can continue.
return true;
} else {
// Still more data. Give up.
return false;
}
});
lengthGrace = lengthGrace.attach(kj::mv(dummy), kj::mv(body));
auto timeGrace = server.timer.afterDelay(server.settings.canceledUploadGacePeriod)
.then([]() { return false; });
return lengthGrace.exclusiveJoin(kj::mv(timeGrace))
.then([this](bool clean) -> kj::Promise<bool> {
if (clean) {
// We recovered. Continue loop.
return loop(false);
} else {
// Client still not done. Return broken.
return false;
}
});
} }
}));
}));
} else {
// Bad request.
// sendError() uses Response::send(), which requires that we have a currentMethod, but we return httpOutput.flush().then(
// never read one. GET seems like the correct choice here. [this, body = kj::mv(body)]() mutable -> kj::Promise<bool> {
currentMethod = HttpMethod::GET; if (httpInput.canReuse()) {
return sendError("ERROR: The headers sent by your client were not valid."); // Things look clean. Go ahead and accept the next request.
// Note that we don't have to handle server.draining here because we'll take care of
// it the next time around the loop.
return loop(false);
} else {
// Apparently, the application did not read the request body. Maybe this is a bug,
// or maybe not: maybe the client tried to upload too much data and the application
// legitimately wants to cancel the upload without reading all it it.
//
// We have a problem, though: We did send a response, and we didn't send
// `Connection: close`, so the client may expect that it can send another request.
// Perhaps the client has even finished sending the previous request's body, in
// which case the moment it finishes receiving the response, it could be completely
// within its rights to start a new request. If we close the socket now, we might
// interrupt that new request.
//
// There's no way we can get out of this perfectly cleanly. HTTP just isn't good
// enough at connection management. The best we can do is give the client some grace
// period and then abort the connection.
auto dummy = kj::heap<HttpDiscardingEntityWriter>();
auto lengthGrace = body->pumpTo(*dummy, server.settings.canceledUploadGraceBytes)
.then([this](size_t amount) {
if (httpInput.canReuse()) {
// Success, we can continue.
return true;
} else {
// Still more data. Give up.
return false;
}
});
lengthGrace = lengthGrace.attach(kj::mv(dummy), kj::mv(body));
auto timeGrace = server.timer.afterDelay(server.settings.canceledUploadGacePeriod)
.then([]() { return false; });
return lengthGrace.exclusiveJoin(kj::mv(timeGrace))
.then([this](bool clean) -> kj::Promise<bool> {
if (clean) {
// We recovered. Continue loop.
return loop(false);
} else {
// Client still not done. Return broken.
return false;
}
});
}
});
});
}
KJ_CASE_ONEOF(protocolError, HttpHeaders::ProtocolError) {
// Bad request.
// sendError() uses Response::send(), which requires that we have a currentMethod, but we
// never read one. GET seems like the correct choice here.
currentMethod = HttpMethod::GET;
return sendError(kj::mv(protocolError));
}
} }
KJ_UNREACHABLE;
}).catch_([this](kj::Exception&& e) -> kj::Promise<bool> { }).catch_([this](kj::Exception&& e) -> kj::Promise<bool> {
// Exception; report 5xx. // Exception; report 5xx.
...@@ -4984,13 +5028,13 @@ private: ...@@ -4984,13 +5028,13 @@ private:
httpInput, httpOutput, nullptr); httpInput, httpOutput, nullptr);
} }
kj::Promise<bool> sendError(kj::StringPtr message) { kj::Promise<bool> sendError(HttpHeaders::ProtocolError protocolError) {
closeAfterSend = true; closeAfterSend = true;
// Client protocol errors always happen on request headers parsing, before we call into the // Client protocol errors always happen on request headers parsing, before we call into the
// HttpService, meaning no response has been sent and we can provide a Response object. // HttpService, meaning no response has been sent and we can provide a Response object.
auto promise = server.settings.errorHandler.orDefault(*this).handleClientProtocolError( auto promise = server.settings.errorHandler.orDefault(*this).handleClientProtocolError(
message, *this); kj::mv(protocolError), *this);
return promise.then([this]() { return httpOutput.flush(); }) return promise.then([this]() { return httpOutput.flush(); })
.then([]() { return false; }); // loop ends after flush .then([]() { return false; }); // loop ends after flush
...@@ -5020,7 +5064,7 @@ private: ...@@ -5020,7 +5064,7 @@ private:
kj::Own<WebSocket> sendWebSocketError(StringPtr errorMessage) { kj::Own<WebSocket> sendWebSocketError(StringPtr errorMessage) {
kj::Exception exception = KJ_EXCEPTION(FAILED, kj::Exception exception = KJ_EXCEPTION(FAILED,
"received bad WebSocket handshake", errorMessage); "received bad WebSocket handshake", errorMessage);
webSocketError = sendError(errorMessage); webSocketError = sendError(HttpHeaders::ProtocolError { errorMessage, nullptr });
kj::throwRecoverableException(kj::mv(exception)); kj::throwRecoverableException(kj::mv(exception));
// Fallback path when exceptions are disabled. // Fallback path when exceptions are disabled.
...@@ -5145,14 +5189,14 @@ void HttpServer::taskFailed(kj::Exception&& exception) { ...@@ -5145,14 +5189,14 @@ void HttpServer::taskFailed(kj::Exception&& exception) {
} }
kj::Promise<void> HttpServerErrorHandler::handleClientProtocolError( kj::Promise<void> HttpServerErrorHandler::handleClientProtocolError(
kj::StringPtr message, kj::HttpService::Response& response) { HttpHeaders::ProtocolError protocolError, kj::HttpService::Response& response) {
// Default error handler implementation. // Default error handler implementation.
HttpHeaderTable headerTable {}; HttpHeaderTable headerTable {};
HttpHeaders headers(headerTable); HttpHeaders headers(headerTable);
headers.set(HttpHeaderId::CONTENT_TYPE, "text/plain"); headers.set(HttpHeaderId::CONTENT_TYPE, "text/plain");
auto errorMessage = kj::str(message); auto errorMessage = kj::str(protocolError.description);
auto body = response.send(400, "Bad Request", headers, errorMessage.size()); auto body = response.send(400, "Bad Request", headers, errorMessage.size());
return body->write(errorMessage.begin(), errorMessage.size()) return body->write(errorMessage.begin(), errorMessage.size())
......
...@@ -321,8 +321,34 @@ public: ...@@ -321,8 +321,34 @@ public:
kj::StringPtr statusText; kj::StringPtr statusText;
}; };
kj::Maybe<Request> tryParseRequest(kj::ArrayPtr<char> content); struct ProtocolError {
kj::Maybe<Response> tryParseResponse(kj::ArrayPtr<char> content); // Represents a protocol error, such as a bad request method or invalid headers. Debugging such
// errors is difficult without a copy of the data which we tried to parse, but this data is
// sensitive, so we can't just lump it into the error description directly. ProtocolError
// provides this sensitive data separate from the error description.
//
// TODO(cleanup): Should maybe not live in HttpHeaders? HttpServerErrorHandler::ProtocolError?
// Or HttpProtocolError? Or maybe we need a more general way of attaching sensitive context to
// kj::Exceptions?
kj::StringPtr description;
// An error description safe for all the world to see.
kj::ArrayPtr<char> rawContent;
// Unredacted data which led to the error condition. This may contain anything transported over
// HTTP, to include sensitive PII, so you must take care to sanitize this before using it in any
// error report that may leak to unprivileged eyes.
//
// This ArrayPtr is merely a copy of the `content` parameter passed to `tryParseRequest()` /
// `tryParseResponse()`, thus it remains valid for as long as a successfully-parsed HttpHeaders
// object would remain valid.
};
using RequestOrProtocolError = kj::OneOf<Request, ProtocolError>;
using ResponseOrProtocolError = kj::OneOf<Response, ProtocolError>;
RequestOrProtocolError tryParseRequest(kj::ArrayPtr<char> content);
ResponseOrProtocolError tryParseResponse(kj::ArrayPtr<char> content);
// Parse an HTTP header blob and add all the headers to this object. // Parse an HTTP header blob and add all the headers to this object.
// //
// `content` should be all text from the start of the request to the first occurrance of two // `content` should be all text from the start of the request to the first occurrance of two
...@@ -778,7 +804,7 @@ struct HttpServerSettings { ...@@ -778,7 +804,7 @@ struct HttpServerSettings {
class HttpServerErrorHandler { class HttpServerErrorHandler {
public: public:
virtual kj::Promise<void> handleClientProtocolError( virtual kj::Promise<void> handleClientProtocolError(
kj::StringPtr message, kj::HttpService::Response& response); HttpHeaders::ProtocolError protocolError, kj::HttpService::Response& response);
virtual kj::Promise<void> handleApplicationError( virtual kj::Promise<void> handleApplicationError(
kj::Exception exception, kj::Maybe<kj::HttpService::Response&> response); kj::Exception exception, kj::Maybe<kj::HttpService::Response&> response);
virtual kj::Promise<void> handleNoResponse(kj::HttpService::Response& response); virtual kj::Promise<void> handleNoResponse(kj::HttpService::Response& response);
......
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment