Compare commits

...

8 Commits

Author SHA1 Message Date
Ciro Spaciari
faf1bc56a1 pass test/js/node/test/parallel/test-https-server-close-destroy-timeout.js 2025-04-23 20:02:07 -07:00
Ciro Spaciari
0abf0a9281 test/js/node/test/parallel/test-https-close.js 2025-04-23 19:46:43 -07:00
Ciro Spaciari
9a00247019 make test-http-url.parse-https.request.js pass 2025-04-23 19:28:37 -07:00
Ciro Spaciari
92a7a06153 more 2025-04-23 17:37:54 -07:00
Ciro Spaciari
38e45d69d5 more 2025-04-23 17:37:54 -07:00
Ciro Spaciari
46a42bf3bc more 2025-04-23 17:37:54 -07:00
Ciro Spaciari
7219e0d1ff WIP 2025-04-23 17:37:54 -07:00
Ciro Spaciari
f14f2b3b3c WIP 2025-04-23 17:37:54 -07:00
37 changed files with 1755 additions and 86 deletions

View File

@@ -1617,7 +1617,7 @@ struct us_socket_t *us_internal_ssl_socket_context_connect(
2, &context->sc, host, port, options,
sizeof(struct us_internal_ssl_socket_t) - sizeof(struct us_socket_t) +
socket_ext_size, is_connecting);
if (*is_connecting) {
if (*is_connecting && s) {
us_internal_zero_ssl_data_for_connected_socket_before_onopen(s);
}

View File

@@ -614,18 +614,22 @@ public:
httpContext->getSocketContextData()->onSocketClosed = onClose;
}
void setOnClientError(HttpContextData<SSL>::OnClientErrorCallback onClientError) {
httpContext->getSocketContextData()->onClientError = std::move(onClientError);
}
TemplatedApp &&run() {
uWS::run();
return std::move(*this);
}
TemplatedApp &&setUsingCustomExpectHandler(bool value) {
httpContext->getSocketContextData()->usingCustomExpectHandler = value;
httpContext->getSocketContextData()->flags.usingCustomExpectHandler = value;
return std::move(*this);
}
TemplatedApp &&setRequireHostHeader(bool value) {
httpContext->getSocketContextData()->requireHostHeader = value;
httpContext->getSocketContextData()->flags.requireHostHeader = value;
return std::move(*this);
}

View File

@@ -31,7 +31,7 @@
#include <string_view>
#include <iostream>
#include "MoveOnlyFunction.h"
#include "HttpParser.h"
namespace uWS {
template<bool> struct HttpResponse;
@@ -73,8 +73,8 @@ private:
// if we are closing or already closed, we don't need to do anything
if (!us_socket_is_closed(SSL, s) && !us_socket_is_shut_down(SSL, s)) {
HttpContextData<SSL> *httpContextData = getSocketContextDataS(s);
if(httpContextData->rejectUnauthorized) {
httpContextData->flags.isSecure = success;
if(httpContextData->flags.rejectUnauthorized) {
if(!success || verify_error.error != 0) {
// we failed to handshake, close the socket
us_socket_close(SSL, s, 0, nullptr);
@@ -118,8 +118,15 @@ private:
/* Get socket ext */
auto *httpResponseData = reinterpret_cast<HttpResponseData<SSL> *>(us_socket_ext(SSL, s));
/* Call filter */
HttpContextData<SSL> *httpContextData = getSocketContextDataS(s);
if(httpContextData->flags.isParsingHttp) {
if(httpContextData->onClientError) {
httpContextData->onClientError(SSL, s,uWS::HTTP_PARSER_ERROR_INVALID_EOF, nullptr, 0);
}
}
for (auto &f : httpContextData->filterHandlers) {
f((HttpResponse<SSL> *) s, -1);
}
@@ -163,7 +170,7 @@ private:
((AsyncSocket<SSL> *) s)->cork();
/* Mark that we are inside the parser now */
httpContextData->isParsingHttp = true;
httpContextData->flags.isParsingHttp = true;
// clients need to know the cursor after http parse, not servers!
// how far did we read then? we need to know to continue with websocket parsing data? or?
@@ -174,7 +181,7 @@ private:
#endif
/* The return value is entirely up to us to interpret. The HttpParser cares only for whether the returned value is DIFFERENT from passed user */
auto [err, returnedSocket] = httpResponseData->consumePostPadded(httpContextData->requireHostHeader,data, (unsigned int) length, s, proxyParser, [httpContextData](void *s, HttpRequest *httpRequest) -> void * {
auto [err, parserError, returnedSocket] = httpResponseData->consumePostPadded(httpContextData->flags.requireHostHeader,data, (unsigned int) length, s, proxyParser, [httpContextData](void *s, HttpRequest *httpRequest) -> void * {
/* For every request we reset the timeout and hang until user makes action */
/* Warning: if we are in shutdown state, resetting the timer is a security issue! */
us_socket_timeout(SSL, (us_socket_t *) s, 0);
@@ -201,6 +208,7 @@ private:
httpResponseData->fromAncientRequest = httpRequest->isAncient();
/* Select the router based on SNI (only possible for SSL) */
auto *selectedRouter = &httpContextData->router;
if constexpr (SSL) {
@@ -290,10 +298,12 @@ private:
});
/* Mark that we are no longer parsing Http */
httpContextData->isParsingHttp = false;
httpContextData->flags.isParsingHttp = false;
/* If we got fullptr that means the parser wants us to close the socket from error (same as calling the errorHandler) */
if (returnedSocket == FULLPTR) {
if(httpContextData->onClientError) {
httpContextData->onClientError(SSL, s, parserError, data, length);
}
/* For errors, we only deliver them "at most once". We don't care if they get halfways delivered or not. */
us_socket_write(SSL, s, httpErrorResponses[err].data(), (int) httpErrorResponses[err].length(), false);
us_socket_shutdown(SSL, s);
@@ -467,7 +477,7 @@ public:
/* Init socket context data */
auto* httpContextData = new ((HttpContextData<SSL> *) us_socket_context_ext(SSL, (us_socket_context_t *) httpContext)) HttpContextData<SSL>();
if(options.request_cert && options.reject_unauthorized) {
httpContextData->rejectUnauthorized = true;
httpContextData->flags.rejectUnauthorized = true;
}
return httpContext->init();
}
@@ -515,15 +525,15 @@ public:
}
}
const bool &customContinue = httpContextData->usingCustomExpectHandler;
httpContextData->currentRouter->add(methods, pattern, [handler = std::move(handler), parameterOffsets = std::move(parameterOffsets), &customContinue](auto *r) mutable {
httpContextData->currentRouter->add(methods, pattern, [handler = std::move(handler), parameterOffsets = std::move(parameterOffsets), httpContextData](auto *r) mutable {
auto user = r->getUserData();
user.httpRequest->setYield(false);
user.httpRequest->setParameters(r->getParameters());
user.httpRequest->setParameterOffsets(&parameterOffsets);
if (!customContinue) {
if (!httpContextData->flags.usingCustomExpectHandler) {
/* Middleware? Automatically respond to expectations */
std::string_view expect = user.httpRequest->getHeader("expect");
if (expect.length() && expect == "100-continue") {

View File

@@ -22,11 +22,19 @@
#include <vector>
#include "MoveOnlyFunction.h"
#include "HttpParser.h"
namespace uWS {
template<bool> struct HttpResponse;
struct HttpRequest;
struct HttpFlags {
bool isParsingHttp: 1 = false;
bool rejectUnauthorized: 1 = false;
bool usingCustomExpectHandler: 1 = false;
bool requireHostHeader: 1 = true;
bool isSecure: 1 = false;
};
template <bool SSL>
struct alignas(16) HttpContextData {
template <bool> friend struct HttpContext;
@@ -35,6 +43,7 @@ struct alignas(16) HttpContextData {
private:
std::vector<MoveOnlyFunction<void(HttpResponse<SSL> *, int)>> filterHandlers;
using OnSocketClosedCallback = void (*)(void* userData, int is_ssl, struct us_socket_t *rawSocket);
using OnClientErrorCallback = MoveOnlyFunction<void(int is_ssl, struct us_socket_t *rawSocket, uWS::HttpParserError errorCode, char *rawPacket, int rawPacketLength)>;
MoveOnlyFunction<void(const char *hostname)> missingServerNameHandler;
@@ -49,13 +58,11 @@ private:
/* This is the default router for default SNI or non-SSL */
HttpRouter<RouterData> router;
void *upgradedWebSocket = nullptr;
bool isParsingHttp = false;
bool rejectUnauthorized = false;
bool usingCustomExpectHandler = false;
bool requireHostHeader = true;
/* Used to simulate Node.js socket events. */
OnSocketClosedCallback onSocketClosed = nullptr;
OnClientErrorCallback onClientError = nullptr;
HttpFlags flags;
// TODO: SNI
void clearRoutes() {
@@ -63,6 +70,11 @@ private:
this->currentRouter = &router;
filterHandlers.clear();
}
public:
bool isSecure() const {
return flags.isSecure;
}
};
}

View File

@@ -48,6 +48,19 @@ namespace uWS
static const unsigned int MINIMUM_HTTP_POST_PADDING = 32;
static void *FULLPTR = (void *)~(uintptr_t)0;
enum HttpParserError: uint8_t {
HTTP_PARSER_ERROR_NONE = 0,
HTTP_PARSER_ERROR_INVALID_CHUNKED_ENCODING = 1,
HTTP_PARSER_ERROR_INVALID_CONTENT_LENGTH = 2,
HTTP_PARSER_ERROR_INVALID_TRANSFER_ENCODING = 3,
HTTP_PARSER_ERROR_MISSING_HOST_HEADER = 4,
HTTP_PARSER_ERROR_INVALID_REQUEST = 5,
HTTP_PARSER_ERROR_REQUEST_HEADER_FIELDS_TOO_LARGE = 6,
HTTP_PARSER_ERROR_INVALID_HTTP_VERSION = 7,
HTTP_PARSER_ERROR_INVALID_EOF = 8,
HTTP_PARSER_ERROR_INVALID_METHOD = 9,
};
struct HttpRequest
{
@@ -59,8 +72,8 @@ namespace uWS
std::string_view key, value;
} headers[UWS_HTTP_MAX_HEADERS_COUNT];
bool ancientHttp;
unsigned int querySeparator;
bool didYield;
unsigned int querySeparator;
BloomFilter bf;
std::pair<int, std::string_view *> currentParameters;
std::map<std::string, unsigned short, std::less<>> *currentParameterOffsets = nullptr;
@@ -134,6 +147,7 @@ namespace uWS
return std::string_view(nullptr, 0);
}
std::string_view getUrl()
{
return std::string_view(headers->value.data(), querySeparator);
@@ -312,6 +326,18 @@ namespace uWS
return (void *)p;
}
static bool isAlpha(std::string_view str) {
if (str.empty()) return false;
for (char c : str) {
if (!isAlphaChar(c))
return false;
}
return true;
}
static inline bool isAlphaChar(char c) {
return ((c >= 'A' && c <= 'Z') || (c >= 'a' && c <= 'z'));
}
static inline int isHTTPorHTTPSPrefixForProxies(char *data, char *end) {
// We can check 8 because:
// 1. If it's "http://" that's 7 bytes, and it's supposed to at least have a trailing slash.
@@ -357,7 +383,13 @@ namespace uWS
/* Scan until single SP, assume next is / (origin request) */
char *start = data;
/* This catches the post padded CR and fails */
while (data[0] > 32) data++;
while (data[0] > 32) {
if (!isAlphaChar(data[0])) {
return (char *) 0x3;
}
data++;
}
if (&data[1] == end) [[unlikely]] {
return nullptr;
}
@@ -365,6 +397,9 @@ namespace uWS
if (data[0] == 32 && (__builtin_expect(data[1] == '/', 1) || isHTTPorHTTPSPrefixForProxies(data + 1, end) == 1)) [[likely]] {
header.key = {start, (size_t) (data - start)};
data++;
if(!isAlpha(header.key)) {
return (char *) 0x3;
}
/* Scan for less than 33 (catches post padded CR and fails) */
start = data;
for (; true; data += 8) {
@@ -436,7 +471,7 @@ namespace uWS
}
/* End is only used for the proxy parser. The HTTP parser recognizes "\ra" as invalid "\r\n" scan and breaks. */
static unsigned int getHeaders(char *postPaddedBuffer, char *end, struct HttpRequest::Header *headers, void *reserved, unsigned int &err, bool &isAncientHTTP) {
static unsigned int getHeaders(char *postPaddedBuffer, char *end, struct HttpRequest::Header *headers, void *reserved, unsigned int &err, HttpParserError &parserError, bool &isAncientHTTP) {
char *preliminaryKey, *preliminaryValue, *start = postPaddedBuffer;
#ifdef UWS_WITH_PROXY
@@ -466,15 +501,21 @@ namespace uWS
* which is then removed, and our counters to flip due to overflow and we end up with a crash */
/* The request line is different from the field names / field values */
if ((char *) 3 > (postPaddedBuffer = consumeRequestLine(postPaddedBuffer, end, headers[0], isAncientHTTP))) {
if ((char *) 4 > (postPaddedBuffer = consumeRequestLine(postPaddedBuffer, end, headers[0], isAncientHTTP))) {
/* Error - invalid request line */
/* Assuming it is 505 HTTP Version Not Supported */
switch (reinterpret_cast<uintptr_t>(postPaddedBuffer)) {
case 0x1:
err = HTTP_ERROR_505_HTTP_VERSION_NOT_SUPPORTED;;
err = HTTP_ERROR_505_HTTP_VERSION_NOT_SUPPORTED;
parserError = HTTP_PARSER_ERROR_INVALID_HTTP_VERSION;
break;
case 0x2:
err = HTTP_ERROR_400_BAD_REQUEST;
parserError = HTTP_PARSER_ERROR_INVALID_REQUEST;
break;
case 0x3:
err = HTTP_ERROR_400_BAD_REQUEST;
parserError = HTTP_PARSER_ERROR_INVALID_METHOD;
break;
default: {
err = 0;
@@ -488,6 +529,7 @@ namespace uWS
if(buffer_size < 2) {
/* Fragmented request */
err = HTTP_ERROR_400_BAD_REQUEST;
parserError = HTTP_PARSER_ERROR_INVALID_REQUEST;
return 0;
}
if(buffer_size >= 2 && postPaddedBuffer[0] == '\r' && postPaddedBuffer[1] == '\n') {
@@ -510,6 +552,7 @@ namespace uWS
}
/* Error: invalid chars in field name */
err = HTTP_ERROR_400_BAD_REQUEST;
parserError = HTTP_PARSER_ERROR_INVALID_REQUEST;
return 0;
}
postPaddedBuffer++;
@@ -527,6 +570,7 @@ namespace uWS
}
/* Error - invalid chars in field value */
err = HTTP_ERROR_400_BAD_REQUEST;
parserError = HTTP_PARSER_ERROR_INVALID_REQUEST;
return 0;
}
break;
@@ -560,6 +604,7 @@ namespace uWS
/* \r\n\r plus non-\n letter is malformed request, or simply out of search space */
if (postPaddedBuffer + 1 < end) {
err = HTTP_ERROR_400_BAD_REQUEST;
parserError = HTTP_PARSER_ERROR_INVALID_REQUEST;
}
return 0;
}
@@ -579,25 +624,26 @@ namespace uWS
* or [consumed, nullptr] for "break; I am closed or upgraded to websocket"
* or [whatever, fullptr] for "break and close me, I am a parser error!" */
template <bool ConsumeMinimally>
std::pair<unsigned int, void *> fenceAndConsumePostPadded(bool requireHostHeader, char *data, unsigned int length, void *user, void *reserved, HttpRequest *req, MoveOnlyFunction<void *(void *, HttpRequest *)> &requestHandler, MoveOnlyFunction<void *(void *, std::string_view, bool)> &dataHandler) {
std::tuple<unsigned int, HttpParserError, void *> fenceAndConsumePostPadded(bool requireHostHeader, char *data, unsigned int length, void *user, void *reserved, HttpRequest *req, MoveOnlyFunction<void *(void *, HttpRequest *)> &requestHandler, MoveOnlyFunction<void *(void *, std::string_view, bool)> &dataHandler) {
/* How much data we CONSUMED (to throw away) */
unsigned int consumedTotal = 0;
unsigned int err = 0;
HttpParserError parserError = HTTP_PARSER_ERROR_NONE;
/* Fence two bytes past end of our buffer (buffer has post padded margins).
* This is to always catch scan for \r but not for \r\n. */
data[length] = '\r';
data[length + 1] = 'a'; /* Anything that is not \n, to trigger "invalid request" */
bool isAncientHTTP = false;
for (unsigned int consumed; length && (consumed = getHeaders(data, data + length, req->headers, reserved, err, isAncientHTTP)); ) {
for (unsigned int consumed; length && (consumed = getHeaders(data, data + length, req->headers, reserved, err, parserError, isAncientHTTP)); ) {
data += consumed;
length -= consumed;
consumedTotal += consumed;
/* Even if we could parse it, check for length here as well */
if (consumed > MAX_FALLBACK_SIZE) {
return {HTTP_ERROR_431_REQUEST_HEADER_FIELDS_TOO_LARGE, FULLPTR};
return {HTTP_ERROR_431_REQUEST_HEADER_FIELDS_TOO_LARGE, HTTP_PARSER_ERROR_REQUEST_HEADER_FIELDS_TOO_LARGE, FULLPTR};
}
/* Store HTTP version (ancient 1.0 or 1.1) */
@@ -611,7 +657,7 @@ namespace uWS
}
/* Break if no host header (but we can have empty string which is different from nullptr) */
if (!isAncientHTTP && requireHostHeader && !req->getHeader("host").data()) {
return {HTTP_ERROR_400_BAD_REQUEST, FULLPTR};
return {HTTP_ERROR_400_BAD_REQUEST, HTTP_PARSER_ERROR_MISSING_HOST_HEADER, FULLPTR};
}
/* RFC 9112 6.3
@@ -628,7 +674,7 @@ namespace uWS
/* Returning fullptr is the same as calling the errorHandler */
/* We could be smart and set an error in the context along with this, to indicate what
* http error response we might want to return */
return {HTTP_ERROR_400_BAD_REQUEST, FULLPTR};
return {HTTP_ERROR_400_BAD_REQUEST, HTTP_PARSER_ERROR_INVALID_TRANSFER_ENCODING, FULLPTR};
}
/* Parse query */
@@ -640,25 +686,17 @@ namespace uWS
remainingStreamingBytes = toUnsignedInteger(contentLengthString);
if (remainingStreamingBytes == UINT64_MAX) {
/* Parser error */
return {HTTP_ERROR_400_BAD_REQUEST, FULLPTR};
return {HTTP_ERROR_400_BAD_REQUEST, HTTP_PARSER_ERROR_INVALID_CONTENT_LENGTH, FULLPTR};
}
}
// lets check if content len is valid before calling requestHandler
if(contentLengthStringLen) {
remainingStreamingBytes = toUnsignedInteger(contentLengthString);
if (remainingStreamingBytes == UINT64_MAX) {
/* Parser error */
return {HTTP_ERROR_400_BAD_REQUEST, FULLPTR};
}
}
/* If returned socket is not what we put in we need
* to break here as we either have upgraded to
* WebSockets or otherwise closed the socket. */
void *returnedUser = requestHandler(user, req);
if (returnedUser != user) {
/* We are upgraded to WebSocket or otherwise broken */
return {consumedTotal, returnedUser};
return {consumedTotal, HTTP_PARSER_ERROR_NONE, returnedUser};
}
/* The rules at play here according to RFC 9112 for requests are essentially:
@@ -694,7 +732,7 @@ namespace uWS
}
if (isParsingInvalidChunkedEncoding(remainingStreamingBytes)) {
// TODO: what happen if we already responded?
return {HTTP_ERROR_400_BAD_REQUEST, FULLPTR};
return {HTTP_ERROR_400_BAD_REQUEST, HTTP_PARSER_ERROR_INVALID_CHUNKED_ENCODING, FULLPTR};
}
unsigned int consumed = (length - (unsigned int) dataToConsume.length());
data = (char *) dataToConsume.data();
@@ -723,13 +761,13 @@ namespace uWS
}
/* Whenever we return FULLPTR, the interpretation of "consumed" should be the HttpError enum. */
if (err) {
return {err, FULLPTR};
return {err, parserError, FULLPTR};
}
return {consumedTotal, user};
return {consumedTotal, HTTP_PARSER_ERROR_NONE, user};
}
public:
std::pair<unsigned int, void *> consumePostPadded(bool requireHostHeader, char *data, unsigned int length, void *user, void *reserved, MoveOnlyFunction<void *(void *, HttpRequest *)> &&requestHandler, MoveOnlyFunction<void *(void *, std::string_view, bool)> &&dataHandler) {
std::tuple<unsigned int, HttpParserError, void *> consumePostPadded(bool requireHostHeader, char *data, unsigned int length, void *user, void *reserved, MoveOnlyFunction<void *(void *, HttpRequest *)> &&requestHandler, MoveOnlyFunction<void *(void *, std::string_view, bool)> &&dataHandler) {
/* This resets BloomFilter by construction, but later we also reset it again.
* Optimize this to skip resetting twice (req could be made global) */
@@ -743,7 +781,7 @@ public:
dataHandler(user, chunk, chunk.length() == 0);
}
if (isParsingInvalidChunkedEncoding(remainingStreamingBytes)) {
return {HTTP_ERROR_400_BAD_REQUEST, FULLPTR};
return {HTTP_ERROR_400_BAD_REQUEST, HTTP_PARSER_ERROR_INVALID_CHUNKED_ENCODING, FULLPTR};
}
data = (char *) dataToConsume.data();
length = (unsigned int) dataToConsume.length();
@@ -753,7 +791,7 @@ public:
if (remainingStreamingBytes >= length) {
void *returnedUser = dataHandler(user, std::string_view(data, length), remainingStreamingBytes == length);
remainingStreamingBytes -= length;
return {0, returnedUser};
return {0, HTTP_PARSER_ERROR_NONE, returnedUser};
} else {
void *returnedUser = dataHandler(user, std::string_view(data, remainingStreamingBytes), true);
@@ -763,7 +801,7 @@ public:
remainingStreamingBytes = 0;
if (returnedUser != user) {
return {0, returnedUser};
return {0, HTTP_PARSER_ERROR_NONE, returnedUser};
}
}
}
@@ -778,19 +816,19 @@ public:
fallback.append(data, maxCopyDistance);
// break here on break
std::pair<unsigned int, void *> consumed = fenceAndConsumePostPadded<true>(requireHostHeader,fallback.data(), (unsigned int) fallback.length(), user, reserved, &req, requestHandler, dataHandler);
if (consumed.second != user) {
std::tuple<unsigned int, HttpParserError, void *> consumed = fenceAndConsumePostPadded<true>(requireHostHeader,fallback.data(), (unsigned int) fallback.length(), user, reserved, &req, requestHandler, dataHandler);
if (std::get<2>(consumed) != user) {
return consumed;
}
if (consumed.first) {
if (std::get<0>(consumed)) {
/* This logic assumes that we consumed everything in fallback buffer.
* This is critically important, as we will get an integer overflow in case
* of "had" being larger than what we consumed, and that we would drop data */
fallback.clear();
data += consumed.first - had;
length -= consumed.first - had;
data += std::get<0>(consumed) - had;
length -= std::get<0>(consumed) - had;
if (remainingStreamingBytes) {
/* It's either chunked or with a content-length */
@@ -800,7 +838,7 @@ public:
dataHandler(user, chunk, chunk.length() == 0);
}
if (isParsingInvalidChunkedEncoding(remainingStreamingBytes)) {
return {HTTP_ERROR_400_BAD_REQUEST, FULLPTR};
return {HTTP_ERROR_400_BAD_REQUEST, HTTP_PARSER_ERROR_INVALID_CHUNKED_ENCODING, FULLPTR};
}
data = (char *) dataToConsume.data();
length = (unsigned int) dataToConsume.length();
@@ -809,7 +847,7 @@ public:
if (remainingStreamingBytes >= (unsigned int) length) {
void *returnedUser = dataHandler(user, std::string_view(data, length), remainingStreamingBytes == (unsigned int) length);
remainingStreamingBytes -= length;
return {0, returnedUser};
return {0, HTTP_PARSER_ERROR_NONE, returnedUser};
} else {
void *returnedUser = dataHandler(user, std::string_view(data, remainingStreamingBytes), true);
@@ -819,7 +857,7 @@ public:
remainingStreamingBytes = 0;
if (returnedUser != user) {
return {0, returnedUser};
return {0, HTTP_PARSER_ERROR_NONE, returnedUser};
}
}
}
@@ -827,30 +865,30 @@ public:
} else {
if (fallback.length() == MAX_FALLBACK_SIZE) {
return {HTTP_ERROR_431_REQUEST_HEADER_FIELDS_TOO_LARGE, FULLPTR};
return {HTTP_ERROR_431_REQUEST_HEADER_FIELDS_TOO_LARGE, HTTP_PARSER_ERROR_REQUEST_HEADER_FIELDS_TOO_LARGE, FULLPTR};
}
return {0, user};
return {0, HTTP_PARSER_ERROR_NONE, user};
}
}
std::pair<unsigned int, void *> consumed = fenceAndConsumePostPadded<false>(requireHostHeader,data, length, user, reserved, &req, requestHandler, dataHandler);
if (consumed.second != user) {
std::tuple<unsigned int, HttpParserError, void *> consumed = fenceAndConsumePostPadded<false>(requireHostHeader,data, length, user, reserved, &req, requestHandler, dataHandler);
if (std::get<2>(consumed) != user) {
return consumed;
}
data += consumed.first;
length -= consumed.first;
data += std::get<0>(consumed);
length -= std::get<0>(consumed);
if (length) {
if (length < MAX_FALLBACK_SIZE) {
fallback.append(data, length);
} else {
return {HTTP_ERROR_431_REQUEST_HEADER_FIELDS_TOO_LARGE, FULLPTR};
return {HTTP_ERROR_431_REQUEST_HEADER_FIELDS_TOO_LARGE, HTTP_PARSER_ERROR_REQUEST_HEADER_FIELDS_TOO_LARGE, FULLPTR};
}
}
// added for now
return {0, user};
return {0, HTTP_PARSER_ERROR_NONE, user};
}
};

View File

@@ -331,7 +331,7 @@ public:
/* We should only mark this if inside the parser; if upgrading "async" we cannot set this */
HttpContextData<SSL> *httpContextData = httpContext->getSocketContextData();
if (httpContextData->isParsingHttp) {
if (httpContextData->flags.isParsingHttp) {
/* We need to tell the Http parser that we changed socket */
httpContextData->upgradedWebSocket = webSocket;
}

View File

@@ -5175,11 +5175,10 @@ pub fn NewServer(protocol_enum: enum { http, https }, development_kind: enum { d
pub const RequestContext = NewRequestContext(ssl_enabled, debug_mode, @This());
pub const App = uws.NewApp(ssl_enabled);
app: ?*App = null,
listener: ?*App.ListenSocket = null,
js_value: JSC.Strong = .empty,
/// Potentially null before listen() is called, and once .destroy() is called.
app: ?*App = null,
vm: *JSC.VirtualMachine,
globalThis: *JSGlobalObject,
base_url_string_for_joining: string = "",
@@ -5210,6 +5209,8 @@ pub fn NewServer(protocol_enum: enum { http, https }, development_kind: enum { d
/// So we have to store it.
user_routes: std.ArrayListUnmanaged(UserRoute) = .{},
on_clienterror: JSC.Strong = .empty,
pub const doStop = host_fn.wrapInstanceMethod(ThisServer, "stopFromJS", false);
pub const dispose = host_fn.wrapInstanceMethod(ThisServer, "disposeFromJS", false);
pub const doUpgrade = host_fn.wrapInstanceMethod(ThisServer, "onUpgrade", false);
@@ -6205,6 +6206,8 @@ pub fn NewServer(protocol_enum: enum { http, https }, development_kind: enum { d
this.user_routes.deinit(bun.default_allocator);
this.config.deinit();
this.on_clienterror.deinit();
if (this.app) |app| {
this.app = null;
app.destroy();
@@ -7338,6 +7341,21 @@ pub fn NewServer(protocol_enum: enum { http, https }, development_kind: enum { d
return route_list_value;
}
pub fn onClientErrorCallback(this: *ThisServer, socket: *uws.Socket, error_code: u8, raw_packet: []const u8) void {
if (this.on_clienterror.get()) |callback| {
const is_ssl = protocol_enum == .https;
const node_socket = Bun__createNodeHTTPServerSocket(is_ssl, socket, this.globalThis);
if (node_socket.isEmptyOrUndefinedOrNull()) {
return;
}
const error_code_value = JSValue.jsNumber(error_code);
const raw_packet_value = JSC.ArrayBuffer.createBuffer(this.globalThis, raw_packet);
_ = callback.call(this.globalThis, .undefined, &.{ JSValue.jsBoolean(is_ssl), node_socket, error_code_value, raw_packet_value }) catch |err|
this.globalThis.takeException(err);
}
}
};
}
@@ -7638,6 +7656,49 @@ pub fn Server__setIdleTimeout_(server: JSC.JSValue, seconds: JSC.JSValue, global
return globalThis.throw("Failed to set timeout: The 'this' value is not a Server.", .{});
}
}
pub export fn Server__setOnClientError(server: JSC.JSValue, callback: JSC.JSValue, globalThis: *JSC.JSGlobalObject) void {
Server__setOnClientError_(server, callback, globalThis) catch |err| switch (err) {
error.JSError => {},
error.OutOfMemory => {
_ = globalThis.throwOutOfMemoryValue();
},
};
}
pub fn Server__setOnClientError_(server: JSC.JSValue, callback: JSC.JSValue, globalThis: *JSC.JSGlobalObject) bun.JSError!void {
if (!server.isObject()) {
return globalThis.throw("Failed to set clientError: The 'this' value is not a Server.", .{});
}
if (!callback.isFunction()) {
return globalThis.throw("Failed to set clientError: The provided value is not a function.", .{});
}
if (server.as(HTTPServer)) |this| {
if (this.app) |app| {
this.on_clienterror.clearWithoutDeallocation();
this.on_clienterror = JSC.Strong.create(callback, globalThis);
app.onClientError(*HTTPServer, this, HTTPServer.onClientErrorCallback);
}
} else if (server.as(HTTPSServer)) |this| {
if (this.app) |app| {
this.on_clienterror.clearWithoutDeallocation();
this.on_clienterror = JSC.Strong.create(callback, globalThis);
app.onClientError(*HTTPSServer, this, HTTPSServer.onClientErrorCallback);
}
} else if (server.as(DebugHTTPServer)) |this| {
if (this.app) |app| {
this.on_clienterror.clearWithoutDeallocation();
this.on_clienterror = JSC.Strong.create(callback, globalThis);
app.onClientError(*DebugHTTPServer, this, DebugHTTPServer.onClientErrorCallback);
}
} else if (server.as(DebugHTTPSServer)) |this| {
if (this.app) |app| {
this.on_clienterror.clearWithoutDeallocation();
this.on_clienterror = JSC.Strong.create(callback, globalThis);
app.onClientError(*DebugHTTPSServer, this, DebugHTTPSServer.onClientErrorCallback);
}
}
}
pub export fn Server__setRequireHostHeader(server: JSC.JSValue, require_host_header: bool, globalThis: *JSC.JSGlobalObject) void {
Server__setRequireHostHeader_(server, require_host_header, globalThis) catch |err| switch (err) {
error.JSError => {},
@@ -7646,6 +7707,7 @@ pub export fn Server__setRequireHostHeader(server: JSC.JSValue, require_host_hea
},
};
}
pub fn Server__setRequireHostHeader_(server: JSC.JSValue, require_host_header: bool, globalThis: *JSC.JSGlobalObject) bun.JSError!void {
if (!server.isObject()) {
return globalThis.throw("Failed to set requireHostHeader: The 'this' value is not a Server.", .{});
@@ -7668,6 +7730,7 @@ comptime {
_ = Server__setIdleTimeout;
_ = Server__setRequireHostHeader;
_ = NodeHTTPResponse.create;
_ = Server__setOnClientError;
}
extern fn NodeHTTPServer__onRequest_http(
@@ -7694,6 +7757,8 @@ extern fn NodeHTTPServer__onRequest_https(
node_response_ptr: *?*NodeHTTPResponse,
) JSC.JSValue;
extern fn Bun__createNodeHTTPServerSocket(bool, *anyopaque, *JSC.JSGlobalObject) JSC.JSValue;
extern fn NodeHTTP_assignOnCloseFunction(bool, *anyopaque) void;
extern fn NodeHTTP_setUsingCustomExpectHandler(bool, *anyopaque, bool) void;

View File

@@ -297,6 +297,7 @@ pub fn create(
if (method.hasRequestBody() or method == HTTP.Method.GET) {
const req_len: usize = brk: {
if (request.header("content-length")) |content_length| {
log("content-length: {s}", .{content_length});
break :brk std.fmt.parseInt(usize, content_length, 10) catch 0;
}

View File

@@ -5,7 +5,7 @@
namespace WebCore {
// must match src/bun.js/node/types.zig#Encoding
enum class BufferEncodingType : uint8_t {
enum class BufferEncodingType : uint16_t {
utf8 = 0,
ucs2 = 1,
utf16le = 2,

View File

@@ -48,7 +48,7 @@ JSC_DECLARE_CUSTOM_GETTER(jsNodeHttpServerSocketGetterLocalAddress);
BUN_DECLARE_HOST_FUNCTION(Bun__drainMicrotasksFromJS);
JSC_DECLARE_CUSTOM_GETTER(jsNodeHttpServerSocketGetterDuplex);
JSC_DECLARE_CUSTOM_SETTER(jsNodeHttpServerSocketSetterDuplex);
JSC_DECLARE_CUSTOM_GETTER(jsNodeHttpServerSocketGetterIsSecureEstablished);
// Create a static hash table of values containing an onclose DOMAttributeGetterSetter and a close function
static const HashTableValue JSNodeHTTPServerSocketPrototypeTableValues[] = {
{ "onclose"_s, static_cast<unsigned>(PropertyAttribute::CustomAccessor), NoIntrinsic, { HashTableValue::GetterSetterType, jsNodeHttpServerSocketGetterOnClose, jsNodeHttpServerSocketSetterOnClose } },
@@ -58,6 +58,7 @@ static const HashTableValue JSNodeHTTPServerSocketPrototypeTableValues[] = {
{ "remoteAddress"_s, static_cast<unsigned>(PropertyAttribute::CustomAccessor | PropertyAttribute::ReadOnly), NoIntrinsic, { HashTableValue::GetterSetterType, jsNodeHttpServerSocketGetterRemoteAddress, noOpSetter } },
{ "localAddress"_s, static_cast<unsigned>(PropertyAttribute::CustomAccessor | PropertyAttribute::ReadOnly), NoIntrinsic, { HashTableValue::GetterSetterType, jsNodeHttpServerSocketGetterLocalAddress, noOpSetter } },
{ "close"_s, static_cast<unsigned>(PropertyAttribute::Function | PropertyAttribute::DontEnum), NoIntrinsic, { HashTableValue::NativeFunctionType, jsFunctionNodeHTTPServerSocketClose, 0 } },
{ "secureEstablished"_s, static_cast<unsigned>(PropertyAttribute::CustomAccessor | PropertyAttribute::ReadOnly), NoIntrinsic, { HashTableValue::GetterSetterType, jsNodeHttpServerSocketGetterIsSecureEstablished, noOpSetter } },
};
class JSNodeHTTPServerSocketPrototype final : public JSC::JSNonFinalObject {
@@ -137,7 +138,16 @@ public:
{
return !socket || us_socket_is_closed(is_ssl, socket);
}
bool isSecure() const
{
// is secure means that tls was established successfully
if (!is_ssl || !socket) return false;
auto* context = us_socket_context(is_ssl, socket);
if (!context) return false;
auto* data = (uWS::HttpContextData<true>*)us_socket_context_ext(is_ssl, context);
if (!data) return false;
return data->isSecure();
}
~JSNodeHTTPServerSocket()
{
if (socket) {
@@ -270,6 +280,14 @@ JSC_DEFINE_HOST_FUNCTION(jsFunctionNodeHTTPServerSocketClose, (JSC::JSGlobalObje
return JSValue::encode(JSC::jsUndefined());
}
JSC_DEFINE_CUSTOM_GETTER(jsNodeHttpServerSocketGetterIsSecureEstablished, (JSC::JSGlobalObject * globalObject, JSC::EncodedJSValue thisValue, JSC::PropertyName))
{
auto* thisObject = jsCast<JSNodeHTTPServerSocket*>(JSC::JSValue::decode(thisValue));
if (UNLIKELY(!thisObject)) {
return JSValue::encode(JSC::jsBoolean(false));
}
return JSValue::encode(JSC::jsBoolean(thisObject->isSecure()));
}
JSC_DEFINE_CUSTOM_GETTER(jsNodeHttpServerSocketGetterDuplex, (JSC::JSGlobalObject * globalObject, JSC::EncodedJSValue thisValue, JSC::PropertyName))
{
auto* thisObject = jsCast<JSNodeHTTPServerSocket*>(JSC::JSValue::decode(thisValue));
@@ -485,6 +503,28 @@ extern "C" void Bun__callNodeHTTPServerSocketOnClose(EncodedJSValue thisValue)
response->onClose();
}
extern "C" JSC::EncodedJSValue Bun__createNodeHTTPServerSocket(bool isSSL, us_socket_t* us_socket, Zig::GlobalObject* globalObject)
{
auto& vm = globalObject->vm();
auto scope = DECLARE_THROW_SCOPE(vm);
RETURN_IF_EXCEPTION(scope, {});
// socket without response because is not valid http
JSNodeHTTPServerSocket* socket = JSNodeHTTPServerSocket::create(
vm,
globalObject->m_JSNodeHTTPServerSocketStructure.getInitializedOnMainThread(globalObject),
(us_socket_t*)us_socket,
isSSL, nullptr);
RETURN_IF_EXCEPTION(scope, {});
if (socket) {
socket->strongThis.set(vm, socket);
return JSValue::encode(socket);
}
return JSValue::encode(JSC::jsNull());
}
BUN_DECLARE_HOST_FUNCTION(jsFunctionRequestOrResponseHasBodyValue);
BUN_DECLARE_HOST_FUNCTION(jsFunctionGetCompleteRequestOrResponseBodyValueAsArrayBuffer);
extern "C" uWS::HttpRequest* Request__getUWSRequest(void*);
@@ -493,6 +533,7 @@ extern "C" void Request__setTimeout(void*, EncodedJSValue, JSC::JSGlobalObject*)
extern "C" bool NodeHTTPResponse__setTimeout(void*, EncodedJSValue, JSC::JSGlobalObject*);
extern "C" void Server__setIdleTimeout(EncodedJSValue, EncodedJSValue, JSC::JSGlobalObject*);
extern "C" void Server__setRequireHostHeader(EncodedJSValue, bool, JSC::JSGlobalObject*);
extern "C" void Server__setOnClientError(EncodedJSValue, EncodedJSValue, JSC::JSGlobalObject*);
static EncodedJSValue assignHeadersFromFetchHeaders(FetchHeaders& impl, JSObject* prototype, JSObject* objectValue, JSC::InternalFieldTuple* tuple, JSC::JSGlobalObject* globalObject, JSC::VM& vm)
{
auto scope = DECLARE_THROW_SCOPE(vm);
@@ -1270,7 +1311,7 @@ JSC_DEFINE_HOST_FUNCTION(jsHTTPSetServerIdleTimeout, (JSGlobalObject * globalObj
return JSValue::encode(jsUndefined());
}
JSC_DEFINE_HOST_FUNCTION(jsHTTPSetRequireHostHeader, (JSGlobalObject * globalObject, CallFrame* callFrame))
JSC_DEFINE_HOST_FUNCTION(jsHTTPSetCustomOptions, (JSGlobalObject * globalObject, CallFrame* callFrame))
{
auto& vm = JSC::getVM(globalObject);
auto scope = DECLARE_THROW_SCOPE(vm);
@@ -1279,10 +1320,15 @@ JSC_DEFINE_HOST_FUNCTION(jsHTTPSetRequireHostHeader, (JSGlobalObject * globalObj
JSValue serverValue = callFrame->uncheckedArgument(0);
JSValue requireHostHeader = callFrame->uncheckedArgument(1);
ASSERT(callFrame->argumentCount() == 2);
ASSERT(callFrame->argumentCount() >= 2);
Server__setRequireHostHeader(JSValue::encode(serverValue), requireHostHeader.toBoolean(globalObject), globalObject);
if (callFrame->argumentCount() > 2) {
JSValue callback = callFrame->uncheckedArgument(2);
Server__setOnClientError(JSValue::encode(serverValue), JSValue::encode(callback), globalObject);
}
return JSValue::encode(jsUndefined());
}
@@ -1406,8 +1452,8 @@ JSValue createNodeHTTPInternalBinding(Zig::GlobalObject* globalObject)
JSC::JSFunction::create(vm, globalObject, 2, "setServerIdleTimeout"_s, jsHTTPSetServerIdleTimeout, ImplementationVisibility::Public), 0);
obj->putDirect(
vm, JSC::PropertyName(JSC::Identifier::fromString(vm, "setRequireHostHeader"_s)),
JSC::JSFunction::create(vm, globalObject, 2, "setRequireHostHeader"_s, jsHTTPSetRequireHostHeader, ImplementationVisibility::Public), 0);
vm, JSC::PropertyName(JSC::Identifier::fromString(vm, "setServerCustomOptions"_s)),
JSC::JSFunction::create(vm, globalObject, 2, "setServerCustomOptions"_s, jsHTTPSetCustomOptions, ImplementationVisibility::Public), 0);
obj->putDirect(
vm, JSC::PropertyName(JSC::Identifier::fromString(vm, "Response"_s)),
globalObject->JSResponseConstructor(), 0);

View File

@@ -2133,10 +2133,10 @@ extern "C" napi_status napi_get_value_int64(napi_env env, napi_value value, int6
}
// must match src/bun.js/node/types.zig#Encoding, which matches WebCore::BufferEncodingType
enum class NapiStringEncoding : uint8_t {
utf8 = static_cast<uint8_t>(WebCore::BufferEncodingType::utf8),
utf16le = static_cast<uint8_t>(WebCore::BufferEncodingType::utf16le),
latin1 = static_cast<uint8_t>(WebCore::BufferEncodingType::latin1),
enum class NapiStringEncoding : uint16_t {
utf8 = static_cast<uint16_t>(WebCore::BufferEncodingType::utf8),
utf16le = static_cast<uint16_t>(WebCore::BufferEncodingType::utf16le),
latin1 = static_cast<uint16_t>(WebCore::BufferEncodingType::latin1),
};
template<NapiStringEncoding...>

View File

@@ -92,7 +92,7 @@ for (let [code, constructor, name, ...other_constructors] of NodeErrors) {
if (name == null) name = constructor.name;
// it's useful to avoid the prefix, but module not found has a prefixed and unprefixed version
const codeWithoutPrefix = code === 'ERR_MODULE_NOT_FOUND' ? code : code.replace(/^ERR_/, '');
const codeWithoutPrefix = code === "ERR_MODULE_NOT_FOUND" ? code : code.replace(/^ERR_/, "");
enumHeader += ` ${code} = ${i},\n`;
listHeader += ` { JSC::ErrorType::${constructor.name}, "${name}"_s, "${code}"_s },\n`;

View File

@@ -365,6 +365,33 @@ extern "C"
}
}
void uws_app_set_on_clienterror(int ssl, uws_app_t *app, void (*handler)(void *user_data, int is_ssl, struct us_socket_t *rawSocket, uint8_t errorCode, char *rawPacket, int rawPacketLength), void *user_data)
{
if (ssl)
{
uWS::SSLApp *uwsApp = (uWS::SSLApp *)app;
if (handler == nullptr) {
uwsApp->setOnClientError(nullptr);
return;
}
uwsApp->setOnClientError([handler, user_data](int is_ssl, struct us_socket_t *rawSocket, uint8_t errorCode, char *rawPacket, int rawPacketLength) {
handler(user_data, is_ssl, rawSocket, errorCode, rawPacket, rawPacketLength);
});
}
else
{
uWS::App *uwsApp = (uWS::App *)app;
if (handler == nullptr) {
uwsApp->setOnClientError(nullptr);
return;
}
uwsApp->setOnClientError([handler, user_data](int is_ssl, struct us_socket_t *rawSocket, uint8_t errorCode, char *rawPacket, int rawPacketLength) {
handler(user_data, is_ssl, rawSocket, errorCode, rawPacket, rawPacketLength);
});
}
}
void uws_app_listen(int ssl, uws_app_t *app, int port,
uws_listen_handler handler, void *user_data)
{

View File

@@ -3032,6 +3032,7 @@ pub const ListenSocket = opaque {
extern fn us_listen_socket_close(ssl: i32, ls: *ListenSocket) void;
extern fn uws_app_close(ssl: i32, app: *uws_app_s) void;
extern fn us_socket_context_close(ssl: i32, ctx: *anyopaque) void;
extern fn uws_app_set_on_clienterror(ssl: i32, app: *uws_app_s, handler: *const fn (*anyopaque, i32, *Socket, u8, ?[*]u8, c_int) callconv(.C) void, user_data: *anyopaque) void;
pub const SocketAddress = struct {
ip: []const u8,
@@ -3476,6 +3477,25 @@ pub fn NewApp(comptime ssl: bool) type {
return uws_app_listen(ssl_flag, @as(*uws_app_t, @ptrCast(app)), port, Wrapper.handle, user_data);
}
pub fn onClientError(
app: *ThisApp,
comptime UserData: type,
user_data: UserData,
comptime handler: fn (data: UserData, socket: *Socket, error_code: u8, rawPacket: []const u8) void,
) void {
const Wrapper = struct {
pub fn handle(data: *anyopaque, _: i32, socket: *Socket, error_code: u8, raw_packet: ?[*]u8, raw_packet_length: c_int) callconv(.C) void {
@call(bun.callmod_inline, handler, .{
@as(UserData, @ptrCast(@alignCast(data))),
socket,
error_code,
if (raw_packet) |bytes| bytes[0..@intCast(@max(raw_packet_length, 0))] else "",
});
}
};
return uws_app_set_on_clienterror(ssl_flag, @as(*uws_app_t, @ptrCast(app)), Wrapper.handle, @ptrCast(user_data));
}
pub fn listenWithConfig(
app: *ThisApp,
comptime UserData: type,

View File

@@ -2896,6 +2896,7 @@ pub fn buildRequest(this: *HTTPClient, body_len: usize) picohttp.Request {
var override_accept_header = false;
var override_host_header = false;
var override_user_agent = false;
var original_content_length: ?string = null;
for (header_names, 0..) |head, i| {
const name = this.headerStr(head);
@@ -2906,7 +2907,9 @@ pub fn buildRequest(this: *HTTPClient, body_len: usize) picohttp.Request {
// we manage those
switch (hash) {
hashHeaderConst("Content-Length"),
=> continue,
=> {
original_content_length = this.headerStr(header_values[i]);
},
hashHeaderConst("Connection") => {
if (!this.flags.disable_keepalive) {
continue;
@@ -2992,6 +2995,12 @@ pub fn buildRequest(this: *HTTPClient, body_len: usize) picohttp.Request {
};
}
header_count += 1;
} else if (original_content_length) |content_length| {
request_headers_buf[header_count] = .{
.name = content_length_header_name,
.value = content_length,
};
header_count += 1;
}
return picohttp.Request{

View File

@@ -8,7 +8,7 @@ const {
setRequestTimeout,
headersTuple,
webRequestOrResponseHasBodyValue,
setRequireHostHeader,
setServerCustomOptions,
getCompleteWebRequestOrResponseBodyValueAsArrayBuffer,
drainMicrotasks,
setServerIdleTimeout,
@@ -20,7 +20,11 @@ const {
setRequestTimeout: (req: Request, timeout: number) => boolean;
headersTuple: any;
webRequestOrResponseHasBodyValue: (arg: any) => boolean;
setRequireHostHeader: (server: any, requireHostHeader: boolean) => void;
setServerCustomOptions: (
server: any,
requireHostHeader: boolean,
onClientError: (ssl: boolean, socket: any, errorCode: number, rawPacket: ArrayBuffer) => undefined,
) => void;
getCompleteWebRequestOrResponseBodyValueAsArrayBuffer: (arg: any) => ArrayBuffer | undefined;
drainMicrotasks: () => void;
setServerIdleTimeout: (server: any, timeout: number) => void;
@@ -445,5 +449,5 @@ export {
drainMicrotasks,
setServerIdleTimeout,
getRawKeys,
setRequireHostHeader,
setServerCustomOptions,
};

View File

@@ -40,7 +40,7 @@ const {
runSymbol,
drainMicrotasks,
setServerIdleTimeout,
setRequireHostHeader,
setServerCustomOptions,
} = require("internal/http");
const { format } = require("internal/util/inspect");
@@ -48,6 +48,7 @@ const { format } = require("internal/util/inspect");
const { IncomingMessage } = require("node:_http_incoming");
const { OutgoingMessage } = require("node:_http_outgoing");
const { kIncomingMessage } = require("node:_http_common");
const kConnectionsCheckingInterval = Symbol("http.server.connectionsCheckingInterval");
const getBunServerAllClosedPromise = $newZigFunction("node_http_binding.zig", "getBunServerAllClosedPromise", 1);
const sendHelper = $newZigFunction("node_cluster_binding.zig", "sendHelperChild", 3);
@@ -681,12 +682,35 @@ function onServerRequestEvent(this: NodeHTTPServerSocket, event: NodeHTTPRespons
}
}
}
function onServerClientError(ssl: boolean, socket: unknown, errorCode: number, rawPacket: ArrayBuffer) {
const self = this as Server;
const err = new Error("Parse Error");
switch (errorCode) {
case 2:
err.code = "HPE_UNEXPECTED_CONTENT_LENGTH";
break;
case 3:
err.code = "HPE_INVALID_TRANSFER_ENCODING";
break;
case 8:
err.code = "HPE_INVALID_EOF_STATE";
break;
case 9:
err.code = "HPE_INVALID_METHOD";
break;
default:
err.code = "HPE_INTERNAL";
break;
}
err.rawPacket = rawPacket;
self.emit("clientError", err, new NodeHTTPServerSocket(self, socket, ssl));
}
const ServerPrototype = {
constructor: Server,
__proto__: EventEmitter.prototype,
[kIncomingMessage]: undefined,
[kServerResponse]: undefined,
[kConnectionsCheckingInterval]: { _destroyed: false },
ref() {
this._unref = false;
this[serverSymbol]?.ref?.();
@@ -719,6 +743,7 @@ const ServerPrototype = {
return;
}
this[serverSymbol] = undefined;
this[kConnectionsCheckingInterval]._destroyed = true;
if (typeof optionalCallback === "function") setCloseCallback(this, optionalCallback);
server.stop();
},
@@ -1055,7 +1080,7 @@ const ServerPrototype = {
});
getBunServerAllClosedPromise(this[serverSymbol]).$then(emitCloseNTServer.bind(this));
isHTTPS = this[serverSymbol].protocol === "https";
setRequireHostHeader(this[serverSymbol], this.requireHostHeader);
setServerCustomOptions(this[serverSymbol], this.requireHostHeader, onServerClientError.bind(this));
if (this?._unref) {
this[serverSymbol]?.unref?.();
@@ -1109,6 +1134,10 @@ const NodeHTTPServerSocket = class Socket extends Duplex {
this.on("timeout", onNodeHTTPServerSocketTimeout);
}
get _secureEstablished() {
return !!this[kHandle]?.secureEstablished;
}
#closeHandle(handle, callback) {
this[kHandle] = undefined;
handle.onclose = this.#onCloseForDestroy.bind(this, callback);
@@ -1695,4 +1724,5 @@ function ensureReadableStreamController(run) {
export default {
Server,
ServerResponse,
kConnectionsCheckingInterval,
};

View File

@@ -23,8 +23,9 @@ import http, {
validateHeaderName,
validateHeaderValue,
} from "node:http";
import type { AddressInfo } from "node:net";
import { connect } from "node:net";
import https, { createServer as createHttpsServer } from "node:https";
import { tls as COMMON_TLS_CERT } from "harness";
import { tmpdir } from "node:os";
import * as path from "node:path";
import * as stream from "node:stream";
@@ -2417,3 +2418,95 @@ it("should reject non-standard body writes when rejectNonStandardBodyWrites is t
}
}
});
test("request.socket._secureEstablished should identify if the request is secure", async () => {
{
const { promise, resolve, reject } = Promise.withResolvers();
const server = createHttpsServer(COMMON_TLS_CERT, (request, response) => {
// Run the check function
expect(request.socket._secureEstablished).toBe(true);
response.writeHead(200, {});
response.end("ok");
server.close();
resolve();
});
server.listen(0, () => {
const port = server.address().port;
fetch(`https://localhost:${port}`, {
tls: {
ca: COMMON_TLS_CERT.cert,
},
}).catch(reject);
});
await promise;
}
{
const { promise, resolve, reject } = Promise.withResolvers();
const server = createServer((request, response) => {
// Run the check function
expect(request.socket._secureEstablished).toBe(false);
response.writeHead(200, {});
response.end("ok");
server.close();
resolve();
});
server.listen(0, () => {
const port = server.address().port;
fetch(`http://localhost:${port}`).catch(reject);
});
await promise;
}
});
test("should emit clientError when Content-Length is invalid", async () => {
const { promise, resolve, reject } = Promise.withResolvers();
const server = http.createServer(reject);
server.on("clientError", (err, socket) => {
resolve(err);
socket.destroy();
});
await once(server.listen(0), "listening");
const client = connect(server.address().port, () => {
// HTTP request with invalid Content-Length
// The Content-Length says 10 but the actual body is 20 bytes
// Send the request
client.write(
`POST /test HTTP/1.1\r\nHost: localhost:${server.address().port}\r\nContent-Type: text/plain\r\nContent-Length: invalid\r\n\r\n`,
);
});
const err = (await promise) as Error;
expect(err.code).toBe("HPE_UNEXPECTED_CONTENT_LENGTH");
});
test("should emit clientError when mixing Content-Length and Transfer-Encoding", async () => {
const { promise, resolve, reject } = Promise.withResolvers();
const server = http.createServer(reject);
server.on("clientError", (err, socket) => {
resolve(err);
socket.destroy();
});
await once(server.listen(0), "listening");
const client = connect(server.address().port, () => {
// HTTP request with invalid Content-Length
// The Content-Length says 10 but the actual body is 20 bytes
// Send the request
client.write(
`POST /test HTTP/1.1\r\nHost: localhost:${server.address().port}\r\nContent-Type: text/plain\r\nContent-Length: 5\r\nTransfer-Encoding: chunked\r\n\r\nHello`,
);
});
const err = (await promise) as Error;
expect(err.code).toBe("HPE_INVALID_TRANSFER_ENCODING");
});

View File

@@ -0,0 +1,26 @@
'use strict';
const common = require('../common');
const http = require('http');
const assert = require('assert');
// The callback should never be invoked because the server
// should respond with a 400 Client Error when a double
// Content-Length header is received.
const server = http.createServer(common.mustNotCall());
server.on('clientError', common.mustCall((err, socket) => {
assert.match(err.message, /^Parse Error/);
assert.strictEqual(err.code, 'HPE_UNEXPECTED_CONTENT_LENGTH');
socket.destroy();
}));
server.listen(0, () => {
const req = http.get({
port: server.address().port,
// Send two content-length header values.
headers: { 'Content-Length': [1, 2] }
}, common.mustNotCall('an error should have occurred'));
req.on('error', common.mustCall(() => {
server.close();
}));
});

View File

@@ -0,0 +1,154 @@
'use strict';
const common = require('../common');
const assert = require('assert');
const http = require('http');
const { duplexPair } = require('stream');
// Test 1: Simple HTTP test, no keep-alive.
{
const testData = 'Hello, World!\n';
const server = http.createServer(common.mustCall((req, res) => {
res.statusCode = 200;
res.setHeader('Content-Type', 'text/plain');
res.end(testData);
}));
const [ clientSide, serverSide ] = duplexPair();
server.emit('connection', serverSide);
const req = http.request({
createConnection: common.mustCall(() => clientSide)
}, common.mustCall((res) => {
res.setEncoding('utf8');
res.on('data', common.mustCall((data) => {
assert.strictEqual(data, testData);
}));
res.on('end', common.mustCall());
}));
req.end();
}
// Test 2: Keep-alive for 2 requests.
{
const testData = 'Hello, World!\n';
const server = http.createServer(common.mustCall((req, res) => {
res.statusCode = 200;
res.setHeader('Content-Type', 'text/plain');
res.end(testData);
}, 2));
const [ clientSide, serverSide ] = duplexPair();
server.emit('connection', serverSide);
function doRequest(cb) {
const req = http.request({
createConnection: common.mustCall(() => clientSide),
headers: { Connection: 'keep-alive' }
}, common.mustCall((res) => {
res.setEncoding('utf8');
res.on('data', common.mustCall((data) => {
assert.strictEqual(data, testData);
}));
res.on('end', common.mustCall(cb));
}));
req.shouldKeepAlive = true;
req.end();
}
doRequest(() => {
doRequest();
});
}
// Test 3: Connection: close request/response with chunked
{
const testData = 'Hello, World!\n';
const server = http.createServer(common.mustCall((req, res) => {
req.setEncoding('utf8');
req.resume();
req.on('data', common.mustCall(function test3_req_data(data) {
assert.strictEqual(data, testData);
}));
req.once('end', function() {
res.statusCode = 200;
res.setHeader('Content-Type', 'text/plain');
res.write(testData);
res.end();
});
}));
const [ clientSide, serverSide ] = duplexPair();
server.emit('connection', serverSide);
clientSide.on('end', common.mustCall());
serverSide.on('end', common.mustCall());
const req = http.request({
createConnection: common.mustCall(() => clientSide),
method: 'PUT',
headers: { 'Connection': 'close' }
}, common.mustCall((res) => {
res.setEncoding('utf8');
res.on('data', common.mustCall(function test3_res_data(data) {
assert.strictEqual(data, testData);
}));
res.on('end', common.mustCall());
}));
req.write(testData);
req.end();
}
// Test 4: Connection: close request/response with Content-Length
// The same as Test 3, but with Content-Length headers
{
const testData = 'Hello, World!\n';
const server = http.createServer(common.mustCall((req, res) => {
assert.strictEqual(req.headers['content-length'], testData.length + '');
req.setEncoding('utf8');
req.on('data', common.mustCall(function test4_req_data(data) {
assert.strictEqual(data, testData);
}));
req.once('end', function() {
res.statusCode = 200;
res.setHeader('Content-Type', 'text/plain');
res.setHeader('Content-Length', testData.length);
res.write(testData);
res.end();
});
}));
const [ clientSide, serverSide ] = duplexPair();
server.emit('connection', serverSide);
clientSide.on('end', common.mustCall());
serverSide.on('end', common.mustCall());
const req = http.request({
createConnection: common.mustCall(() => clientSide),
method: 'PUT',
headers: { 'Connection': 'close' }
}, common.mustCall((res) => {
res.setEncoding('utf8');
assert.strictEqual(res.headers['content-length'], testData.length + '');
res.on('data', common.mustCall(function test4_res_data(data) {
assert.strictEqual(data, testData);
}));
res.on('end', common.mustCall());
}));
req.setHeader('Content-Length', testData.length);
req.write(testData);
req.end();
}
// Test 5: The client sends garbage.
{
const server = http.createServer(common.mustNotCall());
const [ clientSide, serverSide ] = duplexPair();
server.emit('connection', serverSide);
server.on('clientError', common.mustCall());
// Send something that is not an HTTP request.
clientSide.end(
'Im reading a book about anti-gravity. Its impossible to put down!');
}

View File

@@ -0,0 +1,98 @@
'use strict';
const common = require('../common');
const assert = require('assert');
const http = require('http');
const { duplexPair } = require('stream');
// Test that setting the `maxHeaderSize` option works on a per-stream-basis.
// Test 1: The server sends an invalid header.
{
const [ clientSide, serverSide ] = duplexPair();
const req = http.request({
createConnection: common.mustCall(() => clientSide),
insecureHTTPParser: true
}, common.mustCall((res) => {
assert.strictEqual(res.headers.hello, 'foo\x08foo');
res.resume(); // We dont actually care about contents.
res.on('end', common.mustCall());
}));
req.end();
serverSide.resume(); // Dump the request
serverSide.end('HTTP/1.1 200 OK\r\n' +
'Host: example.com\r\n' +
'Hello: foo\x08foo\r\n' +
'Content-Length: 0\r\n' +
'\r\n\r\n');
}
// Test 2: The same as Test 1 except without the option, to make sure it fails.
{
const [ clientSide, serverSide ] = duplexPair();
const req = http.request({
createConnection: common.mustCall(() => clientSide)
}, common.mustNotCall());
req.end();
req.on('error', common.mustCall());
serverSide.resume(); // Dump the request
serverSide.end('HTTP/1.1 200 OK\r\n' +
'Host: example.com\r\n' +
'Hello: foo\x08foo\r\n' +
'Content-Length: 0\r\n' +
'\r\n\r\n');
}
// Test 3: The client sends an invalid header.
{
const testData = 'Hello, World!\n';
const server = http.createServer(
{ insecureHTTPParser: true },
common.mustCall((req, res) => {
res.statusCode = 200;
res.setHeader('Content-Type', 'text/plain');
res.end(testData);
}));
server.on('clientError', common.mustNotCall());
const [ clientSide, serverSide ] = duplexPair();
serverSide.server = server;
server.emit('connection', serverSide);
clientSide.write('GET / HTTP/1.1\r\n' +
'Host: example.com\r\n' +
'Hello: foo\x08foo\r\n' +
'\r\n\r\n');
}
// Test 4: The same as Test 3 except without the option, to make sure it fails.
{
const server = http.createServer(common.mustNotCall());
server.on('clientError', common.mustCall());
const [ clientSide, serverSide ] = duplexPair();
serverSide.server = server;
server.emit('connection', serverSide);
clientSide.write('GET / HTTP/1.1\r\n' +
'Host: example.com\r\n' +
'Hello: foo\x08foo\r\n' +
'\r\n\r\n');
}
// Test 5: Invalid argument type
{
assert.throws(
() => http.request({ insecureHTTPParser: 0 }, common.mustNotCall()),
common.expectsError({
code: 'ERR_INVALID_ARG_TYPE',
message: 'The "options.insecureHTTPParser" property must be of' +
' type boolean. Received type number (0)'
})
);
}

View File

@@ -0,0 +1,40 @@
'use strict';
const common = require('../common');
// Test https://hackerone.com/reports/735748 is fixed.
const assert = require('assert');
const http = require('http');
const net = require('net');
const REQUEST_BB = `POST / HTTP/1.1
Content-Type: text/plain; charset=utf-8
Host: hacker.exploit.com
Connection: keep-alive
Content-Length: 10
Transfer-Encoding: eee, chunked
HELLOWORLDPOST / HTTP/1.1
Content-Type: text/plain; charset=utf-8
Host: hacker.exploit.com
Connection: keep-alive
Content-Length: 28
I AM A SMUGGLED REQUEST!!!
`;
const server = http.createServer(common.mustNotCall());
server.on('clientError', common.mustCall((err) => {
assert.strictEqual(err.code, 'HPE_INVALID_TRANSFER_ENCODING');
server.close();
}));
server.listen(0, common.mustCall(() => {
const client = net.connect(
server.address().port,
common.mustCall(() => {
client.write(REQUEST_BB.replace(/\n/g, '\r\n'));
}));
}));

View File

@@ -0,0 +1,83 @@
'use strict';
const common = require('../common');
const assert = require('assert');
const http = require('http');
const { duplexPair } = require('stream');
// Test that setting the `maxHeaderSize` option works on a per-stream-basis.
// Test 1: The server sends larger headers than what would otherwise be allowed.
{
const [ clientSide, serverSide ] = duplexPair();
const req = http.request({
createConnection: common.mustCall(() => clientSide),
maxHeaderSize: http.maxHeaderSize * 4
}, common.mustCall((res) => {
assert.strictEqual(res.headers.hello, 'A'.repeat(http.maxHeaderSize * 3));
res.resume(); // We dont actually care about contents.
res.on('end', common.mustCall());
}));
req.end();
serverSide.resume(); // Dump the request
serverSide.end('HTTP/1.1 200 OK\r\n' +
'Hello: ' + 'A'.repeat(http.maxHeaderSize * 3) + '\r\n' +
'Content-Length: 0\r\n' +
'\r\n\r\n');
}
// Test 2: The same as Test 1 except without the option, to make sure it fails.
{
const [ clientSide, serverSide ] = duplexPair();
const req = http.request({
createConnection: common.mustCall(() => clientSide)
}, common.mustNotCall());
req.end();
req.on('error', common.mustCall());
serverSide.resume(); // Dump the request
serverSide.end('HTTP/1.1 200 OK\r\n' +
'Hello: ' + 'A'.repeat(http.maxHeaderSize * 3) + '\r\n' +
'Content-Length: 0\r\n' +
'\r\n\r\n');
}
// Test 3: The client sends larger headers than what would otherwise be allowed.
{
const testData = 'Hello, World!\n';
const server = http.createServer(
{ maxHeaderSize: http.maxHeaderSize * 4 },
common.mustCall((req, res) => {
res.statusCode = 200;
res.setHeader('Content-Type', 'text/plain');
res.end(testData);
}));
server.on('clientError', common.mustNotCall());
const [ clientSide, serverSide ] = duplexPair();
serverSide.server = server;
server.emit('connection', serverSide);
clientSide.write('GET / HTTP/1.1\r\n' +
'Host: example.com\r\n' +
'Hello: ' + 'A'.repeat(http.maxHeaderSize * 3) + '\r\n' +
'\r\n\r\n');
}
// Test 4: The same as Test 3 except without the option, to make sure it fails.
{
const server = http.createServer(common.mustNotCall());
server.on('clientError', common.mustCall());
const [ clientSide, serverSide ] = duplexPair();
serverSide.server = server;
server.emit('connection', serverSide);
clientSide.write('GET / HTTP/1.1\r\n' +
'Hello: ' + 'A'.repeat(http.maxHeaderSize * 3) + '\r\n' +
'\r\n\r\n');
}

View File

@@ -0,0 +1,178 @@
// Flags: --expose-internals
'use strict';
const common = require('../common');
const assert = require('assert');
const http = require('http');
const net = require('net');
const MAX = +(process.argv[2] || 16 * 1024); // Command line option, or 16KB.
console.log('pid is', process.pid);
// Verify that we cannot receive more than 16KB of headers.
function once(cb) {
let called = false;
return () => {
if (!called) {
called = true;
cb();
}
};
}
function finished(client, callback) {
['abort', 'error', 'end'].forEach((e) => {
client.on(e, once(() => setImmediate(callback)));
});
}
function fillHeaders(headers, currentSize, valid = false) {
// `llhttp` counts actual header name/value sizes, excluding the whitespace
// and stripped chars.
// OK, Content-Length, 0, X-CRASH, aaa...
headers += 'a'.repeat(MAX - currentSize);
// Generate valid headers
if (valid) {
headers = headers.slice(0, -1);
}
return headers + '\r\n\r\n';
}
function writeHeaders(socket, headers) {
const array = [];
const chunkSize = 100;
let last = 0;
for (let i = 0; i < headers.length / chunkSize; i++) {
const current = (i + 1) * chunkSize;
array.push(headers.slice(last, current));
last = current;
}
// Safety check we are chunking correctly
assert.strictEqual(array.join(''), headers);
next();
function next() {
if (socket.destroyed) {
console.log('socket was destroyed early, data left to write:',
array.join('').length);
return;
}
const chunk = array.shift();
if (chunk) {
console.log('writing chunk of size', chunk.length);
socket.write(chunk, next);
} else {
socket.end();
}
}
}
function test1() {
console.log('test1');
let headers =
'HTTP/1.1 200 OK\r\n' +
'Content-Length: 0\r\n' +
'X-CRASH: ';
// OK, Content-Length, 0, X-CRASH, aaa...
const currentSize = 2 + 14 + 1 + 7;
headers = fillHeaders(headers, currentSize);
const server = net.createServer((sock) => {
sock.once('data', () => {
writeHeaders(sock, headers);
sock.resume();
});
// The socket might error but that's ok
sock.on('error', () => {});
});
server.listen(0, common.mustCall(() => {
const port = server.address().port;
const client = http.get({ port: port }, common.mustNotCall());
client.on('error', common.mustCall((err) => {
assert.strictEqual(err.code, 'HPE_HEADER_OVERFLOW');
server.close(test2);
}));
}));
}
const test2 = common.mustCall(() => {
console.log('test2');
let headers =
'GET / HTTP/1.1\r\n' +
'Host: localhost\r\n' +
'Agent: nod2\r\n' +
'X-CRASH: ';
// /, Host, localhost, Agent, node, X-CRASH, a...
const currentSize = 1 + 4 + 9 + 5 + 4 + 7;
headers = fillHeaders(headers, currentSize);
const server = http.createServer(common.mustNotCall());
server.once('clientError', common.mustCall((err) => {
assert.strictEqual(err.code, 'HPE_HEADER_OVERFLOW');
}));
server.listen(0, common.mustCall(() => {
const client = net.connect(server.address().port);
client.on('connect', () => {
writeHeaders(client, headers);
client.resume();
});
finished(client, common.mustCall(() => {
server.close(test3);
}));
}));
});
const test3 = common.mustCall(() => {
console.log('test3');
let headers =
'GET / HTTP/1.1\r\n' +
'Host: localhost\r\n' +
'Agent: nod3\r\n' +
'X-CRASH: ';
// /, Host, localhost, Agent, node, X-CRASH, a...
const currentSize = 1 + 4 + 9 + 5 + 4 + 7;
headers = fillHeaders(headers, currentSize, true);
console.log('writing', headers.length);
const server = http.createServer(common.mustCall((req, res) => {
res.end('hello from test3 server');
server.close();
}));
server.on('clientError', (err) => {
console.log(err.code);
if (err.code === 'HPE_HEADER_OVERFLOW') {
console.log(err.rawPacket.toString('hex'));
}
});
server.on('clientError', common.mustNotCall());
server.listen(0, common.mustCall(() => {
const client = net.connect(server.address().port);
client.on('connect', () => {
writeHeaders(client, headers);
client.resume();
});
client.pipe(process.stdout);
}));
});
test1();

View File

@@ -0,0 +1,28 @@
'use strict';
const common = require('../common');
const net = require('net');
const http = require('http');
const assert = require('assert');
const str = 'GET / HTTP/1.1\r\n' +
'Content-Length:';
const server = http.createServer(common.mustNotCall());
server.on('clientError', common.mustCall((err, socket) => {
assert.match(err.message, /^Parse Error/);
assert.strictEqual(err.code, 'HPE_INVALID_EOF_STATE');
socket.destroy();
}, 1));
server.listen(0, () => {
const client = net.connect({ port: server.address().port }, () => {
client.on('data', common.mustNotCall());
client.on('end', common.mustCall(() => {
server.close();
}));
client.write(str);
client.end();
});
});

View File

@@ -0,0 +1,45 @@
'use strict';
const common = require('../common');
const assert = require('assert');
const http = require('http');
const net = require('net');
const server = http.createServer(common.mustCall(function(req, res) {
res.end();
}));
server.on('clientError', common.mustCall(function(err, socket) {
assert.strictEqual(err instanceof Error, true);
assert.strictEqual(err.code, 'HPE_INVALID_METHOD');
assert.strictEqual(err.bytesParsed, 1);
assert.strictEqual(err.message, 'Parse Error: Invalid method encountered');
assert.strictEqual(err.rawPacket.toString(), 'Oopsie-doopsie\r\n');
socket.end('HTTP/1.1 400 Bad Request\r\n\r\n');
server.close();
}));
server.listen(0, function() {
function next() {
// Invalid request
const client = net.connect(server.address().port);
client.end('Oopsie-doopsie\r\n');
let chunks = '';
client.on('data', function(chunk) {
chunks += chunk;
});
client.once('end', function() {
assert.strictEqual(chunks, 'HTTP/1.1 400 Bad Request\r\n\r\n');
});
}
// Normal request
http.get({ port: this.address().port, path: '/' }, function(res) {
assert.strictEqual(res.statusCode, 200);
res.resume();
res.once('end', next);
});
});

View File

@@ -0,0 +1,47 @@
'use strict';
const { expectsError, mustCall } = require('../common');
// Test that the request socket is destroyed if the `'clientError'` event is
// emitted and there is no listener for it.
const assert = require('assert');
const { createServer } = require('http');
const { createConnection } = require('net');
const server = createServer();
server.on('connection', mustCall((socket) => {
socket.on('error', expectsError({
name: 'Error',
message: 'Parse Error: Invalid method encountered',
code: 'HPE_INVALID_METHOD',
bytesParsed: 1,
rawPacket: Buffer.from('FOO /\r\n')
}));
}));
server.listen(0, () => {
const chunks = [];
const socket = createConnection({
allowHalfOpen: true,
port: server.address().port
});
socket.on('connect', mustCall(() => {
socket.write('FOO /\r\n');
}));
socket.on('data', (chunk) => {
chunks.push(chunk);
});
socket.on('end', mustCall(() => {
const expected = Buffer.from(
'HTTP/1.1 400 Bad Request\r\nConnection: close\r\n\r\n'
);
assert(Buffer.concat(chunks).equals(expected));
server.close();
}));
});

View File

@@ -0,0 +1,59 @@
'use strict';
const common = require('../common');
// Test that the `'clientError'` event can be emitted multiple times even if the
// socket is correctly destroyed and that no warning is raised.
const assert = require('assert');
const http = require('http');
const net = require('net');
process.on('warning', common.mustNotCall());
function socketListener(socket) {
const firstByte = socket.read(1);
if (firstByte === null) {
socket.once('readable', () => {
socketListener(socket);
});
return;
}
socket.unshift(firstByte);
httpServer.emit('connection', socket);
}
const netServer = net.createServer(socketListener);
const httpServer = http.createServer(common.mustNotCall());
httpServer.once('clientError', common.mustCall((err, socket) => {
assert.strictEqual(err.code, 'HPE_INVALID_METHOD');
assert.strictEqual(err.rawPacket.toString(), '1');
socket.destroy();
httpServer.once('clientError', common.mustCall((err) => {
assert.strictEqual(err.code, 'HPE_INVALID_METHOD');
assert.strictEqual(
err.rawPacket.toString(),
'23 http://example.com HTTP/1.1\r\n\r\n'
);
}));
}));
netServer.listen(0, common.mustCall(() => {
const socket = net.createConnection(netServer.address().port);
socket.on('connect', common.mustCall(() => {
// Note: do not use letters here for the method.
// There is a very small chance that a method with that initial
// might be added in the future and thus this test might fail.
// Numbers will likely never have this issue.
socket.end('123 http://example.com HTTP/1.1\r\n\r\n');
}));
socket.on('close', () => {
netServer.close();
});
socket.resume();
}));

View File

@@ -0,0 +1,29 @@
'use strict';
const common = require('../common');
const http = require('http');
const net = require('net');
const assert = require('assert');
const reqstr = 'POST / HTTP/1.1\r\n' +
'Content-Length: 1\r\n' +
'Transfer-Encoding: chunked\r\n\r\n';
const server = http.createServer(common.mustNotCall());
server.on('clientError', common.mustCall((err) => {
assert.match(err.message, /^Parse Error/);
assert.strictEqual(err.code, 'HPE_INVALID_TRANSFER_ENCODING');
server.close();
}));
server.listen(0, () => {
const client = net.connect({ port: server.address().port }, () => {
client.write(reqstr);
client.end();
});
client.on('data', (data) => {
// Should not get to this point because the server should simply
// close the connection without returning any data.
assert.fail('no data should be returned by the server');
});
client.on('end', common.mustCall());
});

View File

@@ -0,0 +1,29 @@
'use strict';
const common = require('../common');
const net = require('net');
const http = require('http');
const assert = require('assert');
const str = 'GET / HTTP/1.1\r\n' +
'Dummy: Header\r' +
'Content-Length: 1\r\n' +
'\r\n';
const server = http.createServer(common.mustNotCall());
server.on('clientError', common.mustCall((err) => {
assert.match(err.message, /^Parse Error/);
assert.strictEqual(err.code, 'HPE_LF_EXPECTED');
server.close();
}));
server.listen(0, () => {
const client = net.connect({ port: server.address().port }, () => {
client.on('data', common.mustNotCall());
client.on('end', common.mustCall(() => {
server.close();
}));
client.write(str);
client.end();
});
});

View File

@@ -0,0 +1,45 @@
'use strict';
const common = require('../common');
const assert = require('assert');
const http = require('http');
const net = require('net');
// This test sends an invalid character to a HTTP server and purposely
// does not handle clientError (even if it sets an event handler).
//
// The idea is to let the server emit multiple errors on the socket,
// mostly due to parsing error, and make sure they don't result
// in leaking event listeners.
{
let i = 0;
let socket;
process.on('warning', common.mustNotCall());
const server = http.createServer(common.mustNotCall());
server.on('clientError', common.mustCallAtLeast((err) => {
assert.strictEqual(err.code, 'HPE_INVALID_METHOD');
assert.strictEqual(err.rawPacket.toString(), '*');
if (i === 20) {
socket.end();
} else {
socket.write('*');
i++;
}
}, 1));
server.listen(0, () => {
socket = net.createConnection({ port: server.address().port });
socket.on('connect', () => {
socket.write('*');
});
socket.on('close', () => {
server.close();
});
});
}

View File

@@ -0,0 +1,61 @@
// Copyright Joyent, Inc. and other Node contributors.
//
// Permission is hereby granted, free of charge, to any person obtaining a
// copy of this software and associated documentation files (the
// "Software"), to deal in the Software without restriction, including
// without limitation the rights to use, copy, modify, merge, publish,
// distribute, sublicense, and/or sell copies of the Software, and to permit
// persons to whom the Software is furnished to do so, subject to the
// following conditions:
//
// The above copyright notice and this permission notice shall be included
// in all copies or substantial portions of the Software.
//
// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS
// OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
// MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN
// NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM,
// DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR
// OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE
// USE OR OTHER DEALINGS IN THE SOFTWARE.
'use strict';
const common = require('../common');
if (!common.hasCrypto)
common.skip('missing crypto');
const { readKey } = require('../common/fixtures');
const assert = require('assert');
const https = require('https');
const url = require('url');
// https options
const httpsOptions = {
key: readKey('agent1-key.pem'),
cert: readKey('agent1-cert.pem')
};
function check(request) {
// Assert that I'm https
assert.ok(request.socket._secureEstablished);
}
const server = https.createServer(httpsOptions, function(request, response) {
// Run the check function
check(request);
response.writeHead(200, {});
response.end('ok');
server.close();
});
server.listen(0, function() {
const testURL = url.parse(`https://localhost:${this.address().port}`);
testURL.rejectUnauthorized = false;
// make the request
const clientRequest = https.request(testURL);
// Since there is a little magic with the agent
// make sure that the request uses the https.Agent
assert.ok(clientRequest.agent instanceof https.Agent);
clientRequest.end();
});

View File

@@ -0,0 +1,54 @@
'use strict';
const common = require('../common');
if (!common.hasCrypto)
common.skip('missing crypto');
const fixtures = require('../common/fixtures');
const https = require('https');
const options = {
key: fixtures.readKey('agent1-key.pem'),
cert: fixtures.readKey('agent1-cert.pem')
};
const connections = {};
const server = https.createServer(options, (req, res) => {
const interval = setInterval(() => {
res.write('data');
}, 1000);
interval.unref();
});
server.on('connection', (connection) => {
const key = `${connection.remoteAddress}:${connection.remotePort}`;
connection.on('close', () => {
delete connections[key];
});
connections[key] = connection;
});
function shutdown() {
server.close(common.mustSucceed());
for (const key in connections) {
connections[key].destroy();
delete connections[key];
}
}
server.listen(0, () => {
const requestOptions = {
hostname: '127.0.0.1',
port: server.address().port,
path: '/',
method: 'GET',
rejectUnauthorized: false
};
const req = https.request(requestOptions, (res) => {
res.on('data', () => {});
setImmediate(shutdown);
});
req.end();
});

View File

@@ -0,0 +1,134 @@
'use strict';
const common = require('../common');
if (!common.hasCrypto) {
common.skip('missing crypto');
}
const fixtures = require('../common/fixtures');
const assert = require('assert');
const https = require('https');
const tls = require('tls');
const { finished, duplexPair } = require('stream');
const certFixture = {
key: fixtures.readKey('agent1-key.pem'),
cert: fixtures.readKey('agent1-cert.pem'),
ca: fixtures.readKey('ca1-cert.pem'),
};
// Test that setting the `insecureHTTPParse` option works on a per-stream-basis.
// Test 1: The server sends an invalid header.
{
const [ clientSide, serverSide ] = duplexPair();
const req = https.request({
rejectUnauthorized: false,
createConnection: common.mustCall(() => clientSide),
insecureHTTPParser: true
}, common.mustCall((res) => {
assert.strictEqual(res.headers.hello, 'foo\x08foo');
res.resume(); // We dont actually care about contents.
res.on('end', common.mustCall());
}));
req.end();
serverSide.resume(); // Dump the request
serverSide.end('HTTP/1.1 200 OK\r\n' +
'Host: example.com\r\n' +
'Hello: foo\x08foo\r\n' +
'Content-Length: 0\r\n' +
'\r\n\r\n');
}
// Test 2: The same as Test 1 except without the option, to make sure it fails.
{
const [ clientSide, serverSide ] = duplexPair();
const req = https.request({
rejectUnauthorized: false,
createConnection: common.mustCall(() => clientSide)
}, common.mustNotCall());
req.end();
req.on('error', common.mustCall());
serverSide.resume(); // Dump the request
serverSide.end('HTTP/1.1 200 OK\r\n' +
'Host: example.com\r\n' +
'Hello: foo\x08foo\r\n' +
'Content-Length: 0\r\n' +
'\r\n\r\n');
}
// Test 3: The client sends an invalid header.
{
const testData = 'Hello, World!\n';
const server = https.createServer(
{ insecureHTTPParser: true,
...certFixture },
common.mustCall((req, res) => {
res.statusCode = 200;
res.setHeader('Content-Type', 'text/plain');
res.end(testData);
}));
server.on('clientError', common.mustNotCall());
server.listen(0, common.mustCall(() => {
const client = tls.connect({
port: server.address().port,
rejectUnauthorized: false
});
client.write(
'GET / HTTP/1.1\r\n' +
'Host: example.com\r\n' +
'Hello: foo\x08foo\r\n' +
'\r\n\r\n');
client.end();
client.on('data', () => {});
finished(client, common.mustCall(() => {
server.close();
}));
}));
}
// Test 4: The same as Test 3 except without the option, to make sure it fails.
{
const server = https.createServer(
{ ...certFixture },
common.mustNotCall());
server.on('clientError', common.mustCall());
server.listen(0, common.mustCall(() => {
const client = tls.connect({
port: server.address().port,
rejectUnauthorized: false
});
client.write(
'GET / HTTP/1.1\r\n' +
'Host: example.com\r\n' +
'Hello: foo\x08foo\r\n' +
'\r\n\r\n');
client.end();
client.on('data', () => {});
finished(client, common.mustCall(() => {
server.close();
}));
}));
}
// Test 5: Invalid argument type
{
assert.throws(
() => https.request({ insecureHTTPParser: 0 }, common.mustNotCall()),
common.expectsError({
code: 'ERR_INVALID_ARG_TYPE',
message: 'The "options.insecureHTTPParser" property must be of' +
' type boolean. Received type number (0)'
})
);
}

View File

@@ -0,0 +1,122 @@
'use strict';
const common = require('../common');
if (!common.hasCrypto) {
common.skip('missing crypto');
}
const fixtures = require('../common/fixtures');
const assert = require('assert');
const https = require('https');
const http = require('http');
const tls = require('tls');
const { finished, duplexPair } = require('stream');
const certFixture = {
key: fixtures.readKey('agent1-key.pem'),
cert: fixtures.readKey('agent1-cert.pem'),
ca: fixtures.readKey('ca1-cert.pem'),
};
// Test that setting the `maxHeaderSize` option works on a per-stream-basis.
// Test 1: The server sends larger headers than what would otherwise be allowed.
{
const [ clientSide, serverSide ] = duplexPair();
const req = https.request({
createConnection: common.mustCall(() => clientSide),
maxHeaderSize: http.maxHeaderSize * 4
}, common.mustCall((res) => {
assert.strictEqual(res.headers.hello, 'A'.repeat(http.maxHeaderSize * 3));
res.resume(); // We dont actually care about contents.
res.on('end', common.mustCall());
}));
req.end();
serverSide.resume(); // Dump the request
serverSide.end('HTTP/1.1 200 OK\r\n' +
'Host: example.com\r\n' +
'Hello: ' + 'A'.repeat(http.maxHeaderSize * 3) + '\r\n' +
'Content-Length: 0\r\n' +
'\r\n\r\n');
}
// Test 2: The same as Test 1 except without the option, to make sure it fails.
{
const [ clientSide, serverSide ] = duplexPair();
const req = https.request({
createConnection: common.mustCall(() => clientSide)
}, common.mustNotCall());
req.end();
req.on('error', common.mustCall());
serverSide.resume(); // Dump the request
serverSide.end('HTTP/1.1 200 OK\r\n' +
'Host: example.com\r\n' +
'Hello: ' + 'A'.repeat(http.maxHeaderSize * 3) + '\r\n' +
'Content-Length: 0\r\n' +
'\r\n\r\n');
}
// Test 3: The client sends larger headers than what would otherwise be allowed.
{
const testData = 'Hello, World!\n';
const server = https.createServer(
{ maxHeaderSize: http.maxHeaderSize * 4,
...certFixture },
common.mustCall((req, res) => {
res.statusCode = 200;
res.setHeader('Content-Type', 'text/plain');
res.end(testData);
}));
server.on('clientError', common.mustNotCall());
server.listen(0, common.mustCall(() => {
const client = tls.connect({
port: server.address().port,
rejectUnauthorized: false
});
client.write(
'GET / HTTP/1.1\r\n' +
'Host: example.com\r\n' +
'Hello: ' + 'A'.repeat(http.maxHeaderSize * 3) + '\r\n' +
'\r\n\r\n');
client.end();
client.on('data', () => {});
finished(client, common.mustCall(() => {
server.close();
}));
}));
}
// Test 4: The same as Test 3 except without the option, to make sure it fails.
{
const server = https.createServer({ ...certFixture }, common.mustNotCall());
// clientError may be emitted multiple times when header is larger than
// maxHeaderSize.
server.on('clientError', common.mustCallAtLeast(1));
server.listen(0, common.mustCall(() => {
const client = tls.connect({
port: server.address().port,
rejectUnauthorized: false
});
client.write(
'GET / HTTP/1.1\r\n' +
'Host: example.com\r\n' +
'Hello: ' + 'A'.repeat(http.maxHeaderSize * 3) + '\r\n' +
'\r\n\r\n');
client.end();
client.on('data', () => {});
finished(client, common.mustCall(() => {
server.close();
}));
}));
}

View File

@@ -0,0 +1,24 @@
'use strict';
const common = require('../common');
const assert = require('assert');
if (!common.hasCrypto) {
common.skip('missing crypto');
}
const { createServer } = require('https');
const { kConnectionsCheckingInterval } = require('_http_server');
const fixtures = require('../common/fixtures');
const options = {
key: fixtures.readKey('agent1-key.pem'),
cert: fixtures.readKey('agent1-cert.pem')
};
const server = createServer(options, function(req, res) {});
server.listen(0, common.mustCall(function() {
assert.strictEqual(server[kConnectionsCheckingInterval]._destroyed, false);
server.close(common.mustCall(() => {
assert(server[kConnectionsCheckingInterval]._destroyed);
}));
}));

View File

@@ -0,0 +1,54 @@
// Copyright Joyent, Inc. and other Node contributors.
//
// Permission is hereby granted, free of charge, to any person obtaining a
// copy of this software and associated documentation files (the
// "Software"), to deal in the Software without restriction, including
// without limitation the rights to use, copy, modify, merge, publish,
// distribute, sublicense, and/or sell copies of the Software, and to permit
// persons to whom the Software is furnished to do so, subject to the
// following conditions:
//
// The above copyright notice and this permission notice shall be included
// in all copies or substantial portions of the Software.
//
// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS
// OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
// MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN
// NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM,
// DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR
// OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE
// USE OR OTHER DEALINGS IN THE SOFTWARE.
'use strict';
const common = require('../common');
if (!common.hasCrypto)
common.skip('missing crypto');
const fixtures = require('../common/fixtures');
const assert = require('assert');
const https = require('https');
const net = require('net');
const options = {
key: fixtures.readKey('agent1-key.pem'),
cert: fixtures.readKey('agent1-cert.pem'),
handshakeTimeout: 50
};
const server = https.createServer(options, common.mustNotCall());
server.on('clientError', common.mustCall(function(err, conn) {
// Don't hesitate to update the asserts if the internal structure of
// the cleartext object ever changes. We're checking that the https.Server
// has closed the client connection.
assert.strictEqual(conn._secureEstablished, false);
server.close();
conn.destroy();
}));
server.listen(0, function() {
net.connect({ host: '127.0.0.1', port: this.address().port });
});