Commit 350b2f2b authored by Harris Hancock's avatar Harris Hancock

Report raw HTTP content when handling client protocol errors in kj-http

This information is typically necessary to debug such protocol errors. However, it requires special handling, since it could contain sensitive information.

The code which has the best view on protocol errors is HttpHeaders::tryParseRequest(), which previously returned an empty Maybe<Request> on failure. This commit changes that function to return a OneOf<Request, ProtocolError>. This required some surgery in various other parts of the code to deal with the OneOf.
parent 81b2a4c2
...@@ -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( {
auto input = kj::heapString(
"POST \0 /some/path \t HTTP/1.1\r\n" "POST \0 /some/path \t HTTP/1.1\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: 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( {
auto input = kj::heapString(
"POST /some/path \t HTTP/1.1\r\n" "POST /some/path \t HTTP/1.1\r\n"
"Foo-BaR: Baz\r\n" "Foo-BaR: Baz\r\n"
"Cont\001ent-Length: 123\r\n" "Cont\001ent-Length: 123\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: 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( {
auto input = kj::heapString(
"POST /some/path \t HTTP/1.1\r\n" "POST /some/path \t HTTP/1.1\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: 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) {
KJ_CASE_ONEOF(response, HttpHeaders::Response) {
auto& responseHeaders = httpInput.getHeaders();
HttpClient::Response result { HttpClient::Response result {
r->statusCode, response.statusCode,
r->statusText, response.statusText,
&headers, &responseHeaders,
httpInput.getEntityBody(HttpInputStreamImpl::RESPONSE, method, r->statusCode, headers) 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; closed = true;
} else if (counter == id) { } else if (counter == id) {
watchForClose(); watchForClose();
} else { } else {
// Anothe request was already queued after this one, so we don't want to watch for // 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. // stream closure because we're fully expecting another response.
} }
return result; return result;
} else { }
KJ_CASE_ONEOF(protocolError, HttpHeaders::ProtocolError) {
closed = true; closed = true;
KJ_FAIL_REQUIRE("received invalid HTTP response") { break; } // TODO(someday): Do something with ProtocolError::rawContent. Exceptions feel like the
// 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::Response(); 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 (response.statusCode == 101) {
if (!fastCaseCmp<'w', 'e', 'b', 's', 'o', 'c', 'k', 'e', 't'>( if (!fastCaseCmp<'w', 'e', 'b', 's', 'o', 'c', 'k', 'e', 't'>(
headers.get(HttpHeaderId::UPGRADE).orDefault(nullptr).cStr())) { responseHeaders.get(HttpHeaderId::UPGRADE).orDefault(nullptr).cStr())) {
KJ_FAIL_REQUIRE("server returned incorrect Upgrade header; should be 'websocket'", KJ_FAIL_REQUIRE("server returned incorrect Upgrade header; should be 'websocket'",
headers.get(HttpHeaderId::UPGRADE).orDefault("(null)")) { responseHeaders.get(HttpHeaderId::UPGRADE).orDefault("(null)")) {
break; 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 { } else {
upgraded = false; upgraded = false;
HttpClient::WebSocketResponse result { HttpClient::WebSocketResponse result {
r->statusCode, response.statusCode,
r->statusText, response.statusText,
&headers, &responseHeaders,
httpInput.getEntityBody(HttpInputStreamImpl::RESPONSE, HttpMethod::GET, r->statusCode, httpInput.getEntityBody(HttpInputStreamImpl::RESPONSE, HttpMethod::GET,
headers) 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; closed = true;
} else if (counter == id) { } else if (counter == id) {
watchForClose(); watchForClose();
} else { } else {
// Anothe request was already queued after this one, so we don't want to watch for // 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. // stream closure because we're fully expecting another response.
} }
return result; return result;
} }
} else { }
KJ_FAIL_REQUIRE("received invalid HTTP response") { break; } KJ_CASE_ONEOF(protocolError, HttpHeaders::ProtocolError) {
// TODO(someday): Do something with ProtocolError::rawContent. Exceptions feel like the
// 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(); 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,12 +4779,13 @@ public: ...@@ -4739,12 +4779,13 @@ public:
return httpOutput.flush().then([]() { return false; }); return httpOutput.flush().then([]() { return false; });
} }
KJ_IF_MAYBE(req, request) { KJ_SWITCH_ONEOF(requestOrProtocolError) {
KJ_CASE_ONEOF(request, HttpHeaders::Request) {
auto& headers = httpInput.getHeaders(); auto& headers = httpInput.getHeaders();
currentMethod = req->method; currentMethod = request.method;
auto body = httpInput.getEntityBody( auto body = httpInput.getEntityBody(
HttpInputStreamImpl::REQUEST, req->method, 0, headers); HttpInputStreamImpl::REQUEST, request.method, 0, headers);
// TODO(perf): If the client disconnects, should we cancel the response? Probably, to // 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 // prevent permanent deadlock. It's slightly weird in that arguably the client should
...@@ -4752,9 +4793,8 @@ public: ...@@ -4752,9 +4793,8 @@ public:
// other HTTP servers do similar things. // other HTTP servers do similar things.
auto promise = service.request( auto promise = service.request(
req->method, req->url, headers, *body, *this); request.method, request.url, headers, *body, *this);
return promise.then(kj::mvCapture(body, return promise.then([this, body = kj::mv(body)]() mutable -> kj::Promise<bool> {
[this](kj::Own<kj::AsyncInputStream> body) -> kj::Promise<bool> {
// Response done. Await next request. // Response done. Await next request.
KJ_IF_MAYBE(p, webSocketError) { KJ_IF_MAYBE(p, webSocketError) {
...@@ -4782,16 +4822,16 @@ public: ...@@ -4782,16 +4822,16 @@ public:
} }
if (httpOutput.isBroken()) { if (httpOutput.isBroken()) {
// We started a response but didn't finish it. But HttpService returns success? Perhaps // We started a response but didn't finish it. But HttpService returns success?
// it decided that it doesn't want to finish this response. We'll have to disconnect // Perhaps it decided that it doesn't want to finish this response. We'll have to
// here. If the response body is not complete (e.g. Content-Length not reached), the // disconnect here. If the response body is not complete (e.g. Content-Length not
// client should notice. We don't want to log an error because this condition might be // reached), the client should notice. We don't want to log an error because this
// intentional on the service's part. // condition might be intentional on the service's part.
return false; return false;
} }
return httpOutput.flush().then(kj::mvCapture(body, return httpOutput.flush().then(
[this](kj::Own<kj::AsyncInputStream> body) -> kj::Promise<bool> { [this, body = kj::mv(body)]() mutable -> kj::Promise<bool> {
if (httpInput.canReuse()) { if (httpInput.canReuse()) {
// Things look clean. Go ahead and accept the next request. // Things look clean. Go ahead and accept the next request.
...@@ -4841,16 +4881,20 @@ public: ...@@ -4841,16 +4881,20 @@ public:
} }
}); });
} }
})); });
})); });
} else { }
KJ_CASE_ONEOF(protocolError, HttpHeaders::ProtocolError) {
// Bad request. // Bad request.
// sendError() uses Response::send(), which requires that we have a currentMethod, but we // sendError() uses Response::send(), which requires that we have a currentMethod, but we
// never read one. GET seems like the correct choice here. // never read one. GET seems like the correct choice here.
currentMethod = HttpMethod::GET; currentMethod = HttpMethod::GET;
return sendError("ERROR: The headers sent by your client were not valid."); 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