Compare commits

...

35 Commits

Author SHA1 Message Date
Ciro Spaciari
82d45318e0 WIP 2025-04-24 12:52:56 -07:00
Ciro Spaciari
c7e4d9cc86 Merge branch 'main' into ciro/node-http-server-parser-clientError-part1 2025-04-24 08:01:02 -07:00
Ciro Spaciari
4ae29bf8a8 more precise 2025-04-24 08:01:53 -07:00
Ciro Spaciari
bf0327cb77 more tests 2025-04-24 07:53:33 -07:00
Ciro Spaciari
7f1657d59e compat(node:http) more passing part 2 (#19248) 2025-04-24 06:22:11 -07:00
Ciro Spaciari
d0099f2e59 opsie 2025-04-24 02:57:00 -07:00
Ciro Spaciari
56498f9ddc comments 2025-04-24 02:09:28 -07:00
Ciro Spaciari
314e187625 remove flaky ones 2025-04-23 23:16:19 -07:00
Ciro Spaciari
522b0500af no pipes yet 2025-04-23 22:43:20 -07:00
Ciro Spaciari
5e31ab90f2 fix JSC.Strong > JSC.Strong.Optional 2025-04-23 22:35:46 -07:00
Ciro Spaciari
07f87e13f4 Merge branch 'main' into ciro/node-http-server-parser-clientError-part1 2025-04-23 22:27:47 -07:00
Ciro Spaciari
65292fcef5 remove flaky 2025-04-23 22:25:07 -07:00
Ciro Spaciari
21c1561297 fix headers 2025-04-23 22:23:54 -07:00
Ciro Spaciari
2d9a33d4cc one more 2025-04-23 21:59:32 -07:00
Ciro Spaciari
d121f76fdb one more 2025-04-23 21:53:09 -07:00
Ciro Spaciari
e83a1aaf0c more passing 2025-04-23 21:49:10 -07:00
Ciro Spaciari
e5f53b0904 one more 2025-04-23 21:47:35 -07:00
Ciro Spaciari
d360f33608 one more 2025-04-23 21:42:29 -07:00
Ciro Spaciari
68292d30ea one more 2025-04-23 21:29:52 -07:00
Ciro Spaciari
c8fd689512 one more 2025-04-23 21:23:49 -07:00
Ciro Spaciari
80fe35773f one more 2025-04-23 21:20:12 -07:00
Ciro Spaciari
e48626d990 remove flaky 2025-04-23 21:15:37 -07:00
Ciro Spaciari
bd1b9bb8a4 one more passing 2025-04-23 21:14:46 -07:00
Ciro Spaciari
b0689698c3 pass test/js/node/test/parallel/test-http2-createsecureserver-options.js 2025-04-23 20:33:41 -07:00
Ciro Spaciari
5fb0584b0e maybe 2025-04-23 20:15:04 -07:00
Ciro Spaciari
4581569d7b de-flaky 2025-04-23 19:22:15 -07:00
Ciro Spaciari
38901d79cb de-flaky 2025-04-23 19:21:39 -07:00
Ciro Spaciari
48987b8bac isSecure 2025-04-23 19:16:15 -07:00
Ciro Spaciari
bb829a9d78 remove this 2025-04-23 18:21:57 -07:00
Ciro Spaciari
8c2b7b65a1 only passing 2025-04-23 17:47:12 -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
48 changed files with 2373 additions and 169 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,23 @@ 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;
TemplatedApp &&setRequireHostHeaderAndMethodValidation(bool requireHostHeader, bool useStrictMethodValidation) {
httpContext->getSocketContextData()->flags.requireHostHeader = requireHostHeader;
httpContext->getSocketContextData()->flags.useStrictMethodValidation = useStrictMethodValidation;
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,13 +73,14 @@ 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.isAuthorized = 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);
return;
}
httpContextData->flags.isAuthorized = true;
}
/* Any connected socket should timeout until it has a request */
@@ -118,8 +119,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 +171,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 +182,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.useStrictMethodValidation, 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 +209,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 +299,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 +478,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 +526,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,20 @@
#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 isAuthorized: 1 = false;
bool useStrictMethodValidation: 1 = false;
};
template <bool SSL>
struct alignas(16) HttpContextData {
template <bool> friend struct HttpContext;
@@ -35,6 +44,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 +59,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 +71,11 @@ private:
this->currentRouter = &router;
filterHandlers.clear();
}
public:
bool isAuthorized() const {
return flags.isAuthorized;
}
};
}

View File

@@ -40,6 +40,7 @@
#include "HttpErrors.h"
extern "C" size_t BUN_DEFAULT_MAX_HTTP_HEADER_SIZE;
extern "C" int16_t Bun__HTTPMethod__from(const char *str, size_t len);
namespace uWS
{
@@ -48,6 +49,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 +73,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 +148,7 @@ namespace uWS
return std::string_view(nullptr, 0);
}
std::string_view getUrl()
{
return std::string_view(headers->value.data(), querySeparator);
@@ -312,6 +327,25 @@ namespace uWS
return (void *)p;
}
static bool isValidMethod(std::string_view str, bool useStrictMethodValidation) {
if (useStrictMethodValidation) {
return Bun__HTTPMethod__from(str.data(), str.length()) != -1;
}
if (str.empty()) return false;
for (char c : str) {
if (!isValidMethodChar(c))
return false;
}
return true;
}
static inline bool isValidMethodChar(char c) {
return ((c >= 'A' && c <= 'Z') || (c >= 'a' && c <= 'z')) || c == '-';
}
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.
@@ -353,11 +387,17 @@ namespace uWS
}
/* Puts method as key, target as value and returns non-null (or nullptr on error). */
static inline char *consumeRequestLine(char *data, char *end, HttpRequest::Header &header, bool &isAncientHTTP) {
static inline char *consumeRequestLine(char *data, char *end, HttpRequest::Header &header, bool &isAncientHTTP, bool useStrictMethodValidation) {
/* 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 (!isValidMethodChar(data[0]) ) {
return (char *) 0x3;
}
data++;
}
if (&data[1] == end) [[unlikely]] {
return nullptr;
}
@@ -365,6 +405,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(!isValidMethod(header.key, useStrictMethodValidation)) {
return (char *) 0x3;
}
/* Scan for less than 33 (catches post padded CR and fails) */
start = data;
for (; true; data += 8) {
@@ -436,7 +479,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, bool useStrictMethodValidation) {
char *preliminaryKey, *preliminaryValue, *start = postPaddedBuffer;
#ifdef UWS_WITH_PROXY
@@ -466,15 +509,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, useStrictMethodValidation))) {
/* 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 +537,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 +560,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 +578,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 +612,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 +632,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 useStrictMethodValidation, 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, useStrictMethodValidation)); ) {
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 +665,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 +682,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 +694,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 +740,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 +769,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 useStrictMethodValidation,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 +789,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 +799,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 +809,7 @@ public:
remainingStreamingBytes = 0;
if (returnedUser != user) {
return {0, returnedUser};
return {0, HTTP_PARSER_ERROR_NONE, returnedUser};
}
}
}
@@ -778,19 +824,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>(useStrictMethodValidation, 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 +846,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 +855,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 +865,7 @@ public:
remainingStreamingBytes = 0;
if (returnedUser != user) {
return {0, returnedUser};
return {0, HTTP_PARSER_ERROR_NONE, returnedUser};
}
}
}
@@ -827,30 +873,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>(useStrictMethodValidation, 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

@@ -111,6 +111,10 @@ export default [
fn: "end",
length: 2,
},
getBytesWritten: {
fn: "getBytesWritten",
length: 0,
},
flushHeaders: {
fn: "flushHeaders",
length: 0,

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.Optional = .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.Optional = .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);
@@ -5318,9 +5319,9 @@ pub fn NewServer(protocol_enum: enum { http, https }, development_kind: enum { d
this.config.idleTimeout = @truncate(@min(seconds, 255));
}
pub fn setRequireHostHeader(this: *ThisServer, require_host_header: bool) void {
pub fn setFlags(this: *ThisServer, require_host_header: bool, use_strict_method_validation: bool) void {
if (this.app) |app| {
app.setRequireHostHeader(require_host_header);
app.setFlags(require_host_header, use_strict_method_validation);
}
}
@@ -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,27 +7656,71 @@ 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__setRequireHostHeader(server: JSC.JSValue, require_host_header: bool, globalThis: *JSC.JSGlobalObject) void {
Server__setRequireHostHeader_(server, require_host_header, globalThis) catch |err| switch (err) {
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__setRequireHostHeader_(server: JSC.JSValue, require_host_header: bool, globalThis: *JSC.JSGlobalObject) bun.JSError!void {
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.Optional.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.Optional.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.Optional.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.Optional.create(callback, globalThis);
app.onClientError(*DebugHTTPSServer, this, DebugHTTPSServer.onClientErrorCallback);
}
}
}
pub export fn Server__setAppFlags(server: JSC.JSValue, require_host_header: bool, use_strict_method_validation: bool, globalThis: *JSC.JSGlobalObject) void {
Server__setAppFlags_(server, require_host_header, use_strict_method_validation, globalThis) catch |err| switch (err) {
error.JSError => {},
error.OutOfMemory => {
_ = globalThis.throwOutOfMemoryValue();
},
};
}
pub fn Server__setAppFlags_(server: JSC.JSValue, require_host_header: bool, use_strict_method_validation: bool, globalThis: *JSC.JSGlobalObject) bun.JSError!void {
if (!server.isObject()) {
return globalThis.throw("Failed to set requireHostHeader: The 'this' value is not a Server.", .{});
}
if (server.as(HTTPServer)) |this| {
this.setRequireHostHeader(require_host_header);
this.setFlags(require_host_header, use_strict_method_validation);
} else if (server.as(HTTPSServer)) |this| {
this.setRequireHostHeader(require_host_header);
this.setFlags(require_host_header, use_strict_method_validation);
} else if (server.as(DebugHTTPServer)) |this| {
this.setRequireHostHeader(require_host_header);
this.setFlags(require_host_header, use_strict_method_validation);
} else if (server.as(DebugHTTPSServer)) |this| {
this.setRequireHostHeader(require_host_header);
this.setFlags(require_host_header, use_strict_method_validation);
} else {
return globalThis.throw("Failed to set timeout: The 'this' value is not a Server.", .{});
}
@@ -7666,8 +7728,9 @@ pub fn Server__setRequireHostHeader_(server: JSC.JSValue, require_host_header: b
comptime {
_ = Server__setIdleTimeout;
_ = Server__setRequireHostHeader;
_ = Server__setAppFlags;
_ = 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

@@ -28,6 +28,7 @@ server: AnyServer,
/// So we need to buffer that data.
/// This should be pretty uncommon though.
buffered_request_body_data_during_pause: bun.ByteList = .{},
bytes_written: usize = 0,
upgrade_context: UpgradeCTX = .{},
@@ -297,6 +298,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;
}
@@ -816,6 +818,13 @@ fn writeOrEnd(
break :brk .undefined;
};
const strict_content_length: ?u32 = brk: {
if (arguments.len > 3 and arguments[3].isNumber()) {
break :brk arguments[3].toU32();
}
break :brk null;
};
const string_or_buffer: JSC.Node.StringOrBuffer = brk: {
if (input_value == .null or input_value == .undefined) {
break :brk JSC.Node.StringOrBuffer.empty;
@@ -850,8 +859,22 @@ fn writeOrEnd(
} else {
log("write('{s}', {d})", .{ bytes[0..@min(bytes.len, 128)], bytes.len });
}
if (strict_content_length) |content_length| {
const bytes_written = this.bytes_written + bytes.len;
if (is_end) {
if (bytes_written != content_length) {
return globalObject.ERR(.HTTP_CONTENT_LENGTH_MISMATCH, "Content-Length mismatch", .{}).throw();
}
} else if (bytes_written > content_length) {
return globalObject.ERR(.HTTP_CONTENT_LENGTH_MISMATCH, "Content-Length mismatch", .{}).throw();
}
this.bytes_written = bytes_written;
} else {
this.bytes_written +|= bytes.len;
}
if (is_end) {
// Discard the body read ref if it's pending and no onData callback is set at this point.
// This is the equivalent of req._dump().
if (this.body_read_ref.has and this.body_read_state == .pending and (!this.flags.hasCustomOnData or js.onDataGetCached(this_value) == null)) {
@@ -994,7 +1017,7 @@ pub fn setOnData(this: *NodeHTTPResponse, thisValue: JSC.JSValue, globalObject:
}
pub fn write(this: *NodeHTTPResponse, globalObject: *JSC.JSGlobalObject, callframe: *JSC.CallFrame) bun.JSError!JSC.JSValue {
const arguments = callframe.arguments_old(3).slice();
const arguments = callframe.arguments_old(4).slice();
return writeOrEnd(this, globalObject, arguments, .zero, false);
}
@@ -1005,12 +1028,16 @@ pub fn flushHeaders(this: *NodeHTTPResponse, _: *JSC.JSGlobalObject, _: *JSC.Cal
}
pub fn end(this: *NodeHTTPResponse, globalObject: *JSC.JSGlobalObject, callframe: *JSC.CallFrame) bun.JSError!JSC.JSValue {
const arguments = callframe.arguments_old(3).slice();
const arguments = callframe.arguments_old(4).slice();
//We dont wanna a paused socket when we call end, so is important to resume the socket
resumeSocket(this);
return writeOrEnd(this, globalObject, arguments, callframe.this(), true);
}
pub fn getBytesWritten(this: *NodeHTTPResponse, _: *JSC.JSGlobalObject, _: *JSC.CallFrame) JSC.JSValue {
return JSC.JSValue.jsNumber(this.bytes_written);
}
fn handleCorked(globalObject: *JSC.JSGlobalObject, function: JSC.JSValue, result: *JSValue, is_exception: *bool) void {
result.* = function.call(globalObject, .undefined, &.{}) catch |err| {
result.* = globalObject.takeException(err);

View File

@@ -79,8 +79,10 @@ const errors: ErrorCodeMapping = [
["ERR_FS_EISDIR", Error],
["ERR_HTTP_BODY_NOT_ALLOWED", Error],
["ERR_HTTP_HEADERS_SENT", Error],
["ERR_HTTP_CONTENT_LENGTH_MISMATCH", Error],
["ERR_HTTP_INVALID_HEADER_VALUE", TypeError],
["ERR_HTTP_INVALID_STATUS_CODE", RangeError],
["ERR_HTTP_TRAILER_INVALID", Error],
["ERR_HTTP_SOCKET_ASSIGNED", Error],
["ERR_HTTP2_ALTSVC_INVALID_ORIGIN", TypeError],
["ERR_HTTP2_ALTSVC_LENGTH", TypeError],
@@ -267,5 +269,10 @@ const errors: ErrorCodeMapping = [
["ERR_REDIS_INVALID_RESPONSE_TYPE", Error, "RedisError"],
["ERR_REDIS_CONNECTION_TIMEOUT", Error, "RedisError"],
["ERR_REDIS_IDLE_TIMEOUT", Error, "RedisError"],
["HPE_UNEXPECTED_CONTENT_LENGTH", Error],
["HPE_INVALID_TRANSFER_ENCODING", Error],
["HPE_INVALID_EOF_STATE", Error],
["HPE_INVALID_METHOD", Error],
["HPE_INTERNAL", Error],
];
export default errors;

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,20 @@ public:
{
return !socket || us_socket_is_closed(is_ssl, socket);
}
// This means:
// - [x] TLS
// - [x] Handshake has completed
// - [x] Handshake marked the connection as authorized
bool isAuthorized() 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->isAuthorized();
}
~JSNodeHTTPServerSocket()
{
if (socket) {
@@ -270,6 +284,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->isAuthorized()));
}
JSC_DEFINE_CUSTOM_GETTER(jsNodeHttpServerSocketGetterDuplex, (JSC::JSGlobalObject * globalObject, JSC::EncodedJSValue thisValue, JSC::PropertyName))
{
auto* thisObject = jsCast<JSNodeHTTPServerSocket*>(JSC::JSValue::decode(thisValue));
@@ -485,6 +507,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*);
@@ -492,7 +536,9 @@ extern "C" void Request__setInternalEventCallback(void*, EncodedJSValue, JSC::JS
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__setAppFlags(EncodedJSValue, bool, 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,18 +1316,20 @@ 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);
ASSERT(callFrame->argumentCount() == 4);
// This is an internal binding.
JSValue serverValue = callFrame->uncheckedArgument(0);
JSValue requireHostHeader = callFrame->uncheckedArgument(1);
JSValue useStrictMethodValidation = callFrame->uncheckedArgument(2);
JSValue callback = callFrame->uncheckedArgument(3);
ASSERT(callFrame->argumentCount() == 2);
Server__setAppFlags(JSValue::encode(serverValue), requireHostHeader.toBoolean(globalObject), useStrictMethodValidation.toBoolean(globalObject), globalObject);
Server__setRequireHostHeader(JSValue::encode(serverValue), requireHostHeader.toBoolean(globalObject), globalObject);
Server__setOnClientError(JSValue::encode(serverValue), JSValue::encode(callback), globalObject);
return JSValue::encode(jsUndefined());
}
@@ -1406,8 +1454,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

@@ -12,11 +12,8 @@ const extra_count = NodeErrors.map(x => x.slice(3))
.reduce((ac, cv) => ac + cv.length, 0);
const count = NodeErrors.length + extra_count;
if (count > 256) {
// increase size of enum's to have more tags
// src/bun.js/node/types.zig#Encoding
// src/bun.js/bindings/BufferEncodingType.h
throw new Error("NodeError count exceeds u8");
if (count > 65536) {
throw new Error("NodeError count exceeds u16");
}
let enumHeader = ``;
@@ -33,7 +30,7 @@ enumHeader = `
namespace Bun {
static constexpr size_t NODE_ERROR_COUNT = ${count};
enum class ErrorCode : uint8_t {
enum class ErrorCode : uint16_t {
`;
listHeader = `
@@ -83,7 +80,7 @@ pub fn ErrorBuilder(comptime code: Error, comptime fmt: [:0]const u8, Args: type
};
}
pub const Error = enum(u8) {
pub const Error = enum(u16) {
`;
@@ -92,7 +89,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)
{
@@ -470,13 +497,13 @@ extern "C"
uwsApp->domain(server_name);
}
}
void uws_app_set_require_host_header(int ssl, uws_app_t *app, bool require_host_header) {
void uws_app_set_flags(int ssl, uws_app_t *app, bool require_host_header, bool use_strict_method_validation) {
if (ssl) {
uWS::SSLApp *uwsApp = (uWS::SSLApp *)app;
uwsApp->setRequireHostHeader(require_host_header);
uwsApp->setRequireHostHeaderAndMethodValidation(require_host_header, use_strict_method_validation);
} else {
uWS::App *uwsApp = (uWS::App *)app;
uwsApp->setRequireHostHeader(require_host_header);
uwsApp->setRequireHostHeaderAndMethodValidation(require_host_header, use_strict_method_validation);
}
}

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,
@@ -3286,8 +3287,8 @@ pub fn NewApp(comptime ssl: bool) type {
return uws_app_destroy(ssl_flag, @as(*uws_app_s, @ptrCast(app)));
}
pub fn setRequireHostHeader(this: *ThisApp, require_host_header: bool) void {
return uws_app_set_require_host_header(ssl_flag, @as(*uws_app_t, @ptrCast(this)), require_host_header);
pub fn setFlags(this: *ThisApp, require_host_header: bool, use_strict_method_validation: bool) void {
return uws_app_set_flags(ssl_flag, @as(*uws_app_t, @ptrCast(this)), require_host_header, use_strict_method_validation);
}
pub fn clearRoutes(app: *ThisApp) void {
@@ -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,
@@ -3929,7 +3949,7 @@ extern fn uws_res_get_native_handle(ssl: i32, res: *uws_res) *Socket;
extern fn uws_res_get_remote_address_as_text(ssl: i32, res: *uws_res, dest: *[*]const u8) usize;
extern fn uws_create_app(ssl: i32, options: us_bun_socket_context_options_t) ?*uws_app_t;
extern fn uws_app_destroy(ssl: i32, app: *uws_app_t) void;
extern fn uws_app_set_require_host_header(ssl: i32, app: *uws_app_t, require_host_header: bool) void;
extern fn uws_app_set_flags(ssl: i32, app: *uws_app_t, require_host_header: bool, use_strict_method_validation: bool) void;
extern fn uws_app_get(ssl: i32, app: *uws_app_t, pattern: [*c]const u8, handler: uws_method_handler, user_data: ?*anyopaque) void;
extern fn uws_app_post(ssl: i32, app: *uws_app_t, pattern: [*c]const u8, handler: uws_method_handler, user_data: ?*anyopaque) void;
extern fn uws_app_options(ssl: i32, app: *uws_app_t, pattern: [*c]const u8, handler: uws_method_handler, user_data: ?*anyopaque) void;

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

@@ -162,3 +162,12 @@ pub const Method = enum(u8) {
const JSC = bun.JSC;
};
export fn Bun__HTTPMethod__from(str: [*]const u8, len: usize) i16 {
const method: Method = Method.find(str[0..len]) orelse return -1;
return @intFromEnum(method);
}
comptime {
_ = Bun__HTTPMethod__from;
}

View File

@@ -6,7 +6,7 @@ const {
setRequestTimeout,
headersTuple,
webRequestOrResponseHasBodyValue,
setRequireHostHeader,
setServerCustomOptions,
getCompleteWebRequestOrResponseBodyValueAsArrayBuffer,
drainMicrotasks,
setServerIdleTimeout,
@@ -18,7 +18,12 @@ const {
setRequestTimeout: (req: Request, timeout: number) => boolean;
headersTuple: any;
webRequestOrResponseHasBodyValue: (arg: any) => boolean;
setRequireHostHeader: (server: any, requireHostHeader: boolean) => void;
setServerCustomOptions: (
server: any,
requireHostHeader: boolean,
useStrictMethodValidation: 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;
@@ -419,7 +424,7 @@ export {
setHeader,
setIsNextIncomingMessageHTTPS,
setRequestTimeout,
setRequireHostHeader,
setServerCustomOptions,
setServerIdleTimeout,
STATUS_CODES,
statusCodeSymbol,

View File

@@ -12,6 +12,11 @@ var FakeSocket = class Socket extends Duplex {
isServer = false;
#address;
_httpMessage: any;
constructor(httpMessage: any) {
super();
this._httpMessage = httpMessage;
}
address() {
// Call server.requestIP() without doing any property getter twice.
var internalData;
@@ -31,9 +36,13 @@ var FakeSocket = class Socket extends Duplex {
};
_destroy(_err, _callback) {
this._httpMessage?.destroy?.();
const socketData = this[kInternalSocketData];
if (!socketData) return; // sometimes 'this' is Socket not FakeSocket
if (!socketData[1]["req"][kAutoDestroyed]) socketData[1].end();
if (socketData) {
if (!socketData[1]["req"][kAutoDestroyed]) socketData[1].end();
}
_callback?.(_err);
}
_final(_callback) {}

View File

@@ -106,4 +106,5 @@ export default {
validateBuffer: $newCppFunction("NodeValidator.cpp", "jsFunction_validateBuffer", 0),
/** `(value, name, oneOf)` */
validateOneOf: $newCppFunction("NodeValidator.cpp", "jsFunction_validateOneOf", 0),
isUint8Array: value => typeof value === "object" && value !== null && value instanceof Uint8Array,
};

View File

@@ -174,6 +174,10 @@ function ClientRequest(input, options, cb) {
return this;
};
this.flushHeaders = function () {
send();
};
this.destroy = function (err?: Error) {
if (this.destroyed) return this;
this.destroyed = true;
@@ -194,7 +198,6 @@ function ClientRequest(input, options, cb) {
// If request is destroyed we abort the current response
this[kAbortController]?.abort?.();
this.socket.destroy(err);
return this;
};
@@ -777,16 +780,42 @@ function ClientRequest(input, options, cb) {
const headersArray = $isJSArray(headers);
if (headersArray) {
const length = headers.length;
if (length % 2 !== 0) {
throw $ERR_INVALID_ARG_VALUE("options.headers", "headers");
}
for (let i = 0; i < length; ) {
this.appendHeader(headers[i++], headers[i++]);
if ($isJSArray(headers[0])) {
// [[key, value], [key, value], ...]
for (let i = 0; i < length; i++) {
const actualHeader = headers[i];
if (actualHeader.length !== 2) {
throw $ERR_INVALID_ARG_VALUE("options.headers", "expected array of [key, value]");
}
const key = actualHeader[0];
const lowerKey = key?.toLowerCase();
if (lowerKey === "host") {
if (!this.getHeader(key)) {
this.setHeader(key, actualHeader[1]);
}
} else {
this.appendHeader(key, actualHeader[1]);
}
}
} else {
// [key, value, key, value, ...]
if (length % 2 !== 0) {
throw $ERR_INVALID_ARG_VALUE("options.headers", "expected [key, value, key, value, ...]");
}
for (let i = 0; i < length; ) {
this.appendHeader(headers[i++], headers[i++]);
}
}
} else {
if (headers) {
for (let key in headers) {
this.setHeader(key, headers[key]);
const value = headers[key];
if (key === "host" || key === "hostname") {
if (value !== null && value !== undefined && typeof value !== "string") {
throw $ERR_INVALID_ARG_TYPE(`options.${key}`, ["string", "undefined", "null"], value);
}
}
this.setHeader(key, value);
}
}

View File

@@ -295,7 +295,6 @@ const IncomingMessagePrototype = {
},
_destroy: function IncomingMessage_destroy(err, cb) {
const shouldEmitAborted = !this.readableEnded || !this.complete;
if (shouldEmitAborted) {
this[abortedSymbol] = true;
// IncomingMessage emits 'aborted'.
@@ -328,7 +327,7 @@ const IncomingMessagePrototype = {
stream?.cancel?.().catch(nop);
}
const socket = this[fakeSocketSymbol];
const socket = this.socket;
if (socket && !socket.destroyed && shouldEmitAborted) {
socket.destroy(err);
}
@@ -345,7 +344,7 @@ const IncomingMessagePrototype = {
this[abortedSymbol] = value;
},
get connection() {
return (this[fakeSocketSymbol] ??= new FakeSocket());
return (this[fakeSocketSymbol] ??= new FakeSocket(this));
},
get statusCode() {
return this[statusCodeSymbol];
@@ -403,7 +402,7 @@ const IncomingMessagePrototype = {
return this;
},
get socket() {
return (this[fakeSocketSymbol] ??= new FakeSocket());
return (this[fakeSocketSymbol] ??= new FakeSocket(this));
},
set socket(value) {
this[fakeSocketSymbol] = value;

View File

@@ -1,5 +1,5 @@
const { Stream } = require("internal/stream");
const { validateFunction } = require("internal/validators");
const { validateFunction, isUint8Array, validateString } = require("internal/validators");
const {
headerStateSymbol,
@@ -21,9 +21,143 @@ const {
getRawKeys,
} = require("internal/http");
const { validateHeaderName, validateHeaderValue } = require("node:_http_common");
const {
validateHeaderName,
validateHeaderValue,
_checkInvalidHeaderChar: checkInvalidHeaderChar,
} = require("node:_http_common");
const kUniqueHeaders = Symbol("kUniqueHeaders");
const kBytesWritten = Symbol("kBytesWritten");
const kRejectNonStandardBodyWrites = Symbol("kRejectNonStandardBodyWrites");
const kCorked = Symbol("corked");
const kChunkedBuffer = Symbol("kChunkedBuffer");
const kHighWaterMark = Symbol("kHighWaterMark");
const kChunkedLength = Symbol("kChunkedLength");
const { FakeSocket } = require("internal/http/FakeSocket");
const nop = () => {};
function emitErrorNt(msg, err, callback) {
callback(err);
if (typeof msg.emit === "function" && !msg.destroyed) {
msg.emit("error", err);
}
}
function onError(msg, err, callback) {
if (msg.destroyed) {
return;
}
process.nextTick(emitErrorNt, msg, err, callback);
}
function write_(msg, chunk, encoding, callback, fromEnd) {
if (typeof callback !== "function") callback = nop;
if (chunk === null) {
throw $ERR_STREAM_NULL_VALUES();
} else if (typeof chunk !== "string" && !isUint8Array(chunk)) {
throw $ERR_INVALID_ARG_TYPE("chunk", ["string", "Buffer", "Uint8Array"], chunk);
}
let err;
if (msg.finished) {
err = $ERR_STREAM_WRITE_AFTER_END();
} else if (msg.destroyed) {
err = $ERR_STREAM_DESTROYED("write");
}
if (err) {
if (!msg.destroyed) {
onError(msg, err, callback);
} else {
process.nextTick(callback, err);
}
return false;
}
let len;
if (msg.strictContentLength) {
len ??= typeof chunk === "string" ? Buffer.byteLength(chunk, encoding) : chunk.byteLength;
if (
strictContentLength(msg) &&
(fromEnd ? msg[kBytesWritten] + len !== msg._contentLength : msg[kBytesWritten] + len > msg._contentLength)
) {
const err = new Error(
`Response body's content-length of ${len + msg[kBytesWritten]} byte(s) does not match the content-length of ${msg._contentLength} byte(s) set in header`,
);
throw err;
}
msg[kBytesWritten] += len;
}
function connectionCorkNT(conn) {
conn.uncork();
}
let __crlf_buf;
function getCrlfBuf() {
if (!__crlf_buf) {
__crlf_buf = Buffer.from("\r\n");
}
return __crlf_buf;
}
function strictContentLength(msg) {
return (
msg.strictContentLength &&
msg._contentLength != null &&
msg._hasBody &&
!msg._removedContLen &&
!msg.chunkedEncoding &&
!msg.hasHeader("transfer-encoding")
);
}
if (!msg._header) {
if (fromEnd) {
len ??= typeof chunk === "string" ? Buffer.byteLength(chunk, encoding) : chunk.byteLength;
msg._contentLength = len;
}
msg._implicitHeader();
}
if (!msg._hasBody) {
if (msg[kRejectNonStandardBodyWrites]) {
throw $ERR_HTTP_BODY_NOT_ALLOWED();
} else {
process.nextTick(callback);
return true;
}
}
if (!fromEnd && msg.socket && !msg.socket.writableCorked) {
msg.socket.cork();
process.nextTick(connectionCorkNT, msg.socket);
}
let ret;
if (msg.chunkedEncoding && chunk.length !== 0) {
len ??= typeof chunk === "string" ? Buffer.byteLength(chunk, encoding) : chunk.byteLength;
if (msg[kCorked] && msg._headerSent) {
msg[kChunkedBuffer].push(chunk, encoding, callback);
msg[kChunkedLength] += len;
ret = msg[kChunkedLength] < msg[kHighWaterMark];
} else {
const crlf_buf = getCrlfBuf();
msg._send(len.toString(16), "latin1", null);
msg._send(crlf_buf, null, null);
msg._send(chunk, encoding, null, len);
ret = msg._send(crlf_buf, null, callback);
}
} else {
ret = msg._send(chunk, encoding, callback, len);
}
return ret;
}
function OutgoingMessage(options) {
if (!new.target) {
@@ -45,6 +179,7 @@ function OutgoingMessage(options) {
this._closed = false;
this._header = null;
this._headerSent = false;
this[kHighWaterMark] = options?.highWaterMark ?? (process.platform === "win32" ? 16 * 1024 : 64 * 1024);
}
const OutgoingMessagePrototype = {
constructor: OutgoingMessage,
@@ -65,6 +200,7 @@ const OutgoingMessagePrototype = {
_closed: false,
appendHeader(name, value) {
validateString(name, "name");
var headers = (this[headersSymbol] ??= new Headers());
headers.append(name, value);
return this;
@@ -75,30 +211,18 @@ const OutgoingMessagePrototype = {
},
flushHeaders() {},
getHeader(name) {
validateString(name, "name");
return getHeader(this[headersSymbol], name);
},
// Overridden by ClientRequest and ServerResponse; this version will be called only if the user constructs OutgoingMessage directly.
write(chunk, encoding, callback) {
if ($isCallable(chunk)) {
callback = chunk;
chunk = undefined;
} else if ($isCallable(encoding)) {
if (typeof encoding === "function") {
callback = encoding;
encoding = undefined;
} else if (!$isCallable(callback)) {
callback = undefined;
encoding = undefined;
encoding = null;
}
hasServerResponseFinished(this, chunk, callback);
if (chunk) {
const len = Buffer.byteLength(chunk, encoding || (typeof chunk === "string" ? "utf8" : "buffer"));
if (len > 0) {
this.outputSize += len;
this.outputData.push(chunk);
}
}
return this.writableHighWaterMark >= this.outputSize;
return write_(this, chunk, encoding, callback, false);
},
getHeaderNames() {
@@ -120,6 +244,7 @@ const OutgoingMessagePrototype = {
},
removeHeader(name) {
validateString(name, "name");
if ((this._header !== undefined && this._header !== null) || this[headerStateSymbol] === NodeHTTPHeaderState.sent) {
throw $ERR_HTTP_HEADERS_SENT("remove");
}
@@ -129,7 +254,7 @@ const OutgoingMessagePrototype = {
},
setHeader(name, value) {
if ((this._header !== undefined && this._header !== null) || this[headerStateSymbol] === NodeHTTPHeaderState.sent) {
if ((this._header !== undefined && this._header !== null) || this[headerStateSymbol] == NodeHTTPHeaderState.sent) {
throw $ERR_HTTP_HEADERS_SENT("set");
}
validateHeaderName(name);
@@ -138,8 +263,42 @@ const OutgoingMessagePrototype = {
setHeader(headers, name, value);
return this;
},
setHeaders(headers) {
if (this._header || this[headerStateSymbol] !== NodeHTTPHeaderState.none) {
throw $ERR_HTTP_HEADERS_SENT("set");
}
if (!headers || $isArray(headers) || typeof headers.keys !== "function" || typeof headers.get !== "function") {
throw $ERR_INVALID_ARG_TYPE("headers", ["Headers", "Map"], headers);
}
// Headers object joins multiple cookies with a comma when using
// the getter to retrieve the value,
// unless iterating over the headers directly.
// We also cannot safely split by comma.
// To avoid setHeader overwriting the previous value we push
// set-cookie values in array and set them all at once.
const cookies = [];
for (const { 0: key, 1: value } of headers) {
if (key === "set-cookie") {
if ($isArray(value)) {
cookies.push(...value);
} else {
cookies.push(value);
}
continue;
}
this.setHeader(key, value);
}
if (cookies.length) {
this.setHeader("set-cookie", cookies);
}
return this;
},
hasHeader(name) {
validateString(name, "name");
const headers = this[headersSymbol];
if (!headers) return false;
return headers.has(name);
@@ -154,8 +313,48 @@ const OutgoingMessagePrototype = {
this[headersSymbol] = new Headers(value);
},
addTrailers(_headers) {
throw new Error("not implemented");
addTrailers(headers) {
this._trailer = "";
const keys = Object.keys(headers);
const isArray = $isArray(headers);
// Retain for(;;) loop for performance reasons
// Refs: https://github.com/nodejs/node/pull/30958
for (let i = 0, l = keys.length; i < l; i++) {
let field, value;
const key = keys[i];
if (isArray) {
field = headers[key][0];
value = headers[key][1];
} else {
field = key;
value = headers[key];
}
validateHeaderName(field, "Trailer name");
// Check if the field must be sent several times
const isArrayValue = $isArray(value);
if (
isArrayValue &&
value.length > 1 &&
(!this[kUniqueHeaders] || !this[kUniqueHeaders].has(field.toLowerCase()))
) {
for (let j = 0, l = value.length; j < l; j++) {
if (checkInvalidHeaderChar(value[j])) {
throw $ERR_INVALID_CHAR("trailer content", field);
}
this._trailer += field + ": " + value[j] + "\r\n";
}
} else {
if (isArrayValue) {
value = value.join("; ");
}
if (checkInvalidHeaderChar(value)) {
throw $ERR_INVALID_CHAR("trailer content", field);
}
this._trailer += field + ": " + value + "\r\n";
}
}
},
setTimeout(msecs, callback) {
@@ -189,9 +388,12 @@ const OutgoingMessagePrototype = {
get connection() {
return this.socket;
},
set connection(value) {
this.socket = value;
},
get socket() {
this[fakeSocketSymbol] = this[fakeSocketSymbol] ?? new FakeSocket();
this[fakeSocketSymbol] = this[fakeSocketSymbol] ?? new FakeSocket(this);
return this[fakeSocketSymbol];
},
@@ -231,12 +433,63 @@ const OutgoingMessagePrototype = {
return this.finished && !!(this[kEmitState] & (1 << ClientRequestEmitState.finish));
},
_send(data, encoding, callback, _byteLength) {
if (this.destroyed) {
// _send(data, encoding, callback, _byteLength) {
// if (this.destroyed) {
// return false;
// }
// return this.write(data, encoding, callback);
// },
_send(data, encoding, callback, byteLength) {
// This is a shameful hack to get the headers and first body chunk onto
// the same packet. Future versions of Node are going to take care of
// this at a lower level and in a more general way.
if (!this._headerSent && this._header !== null) {
// `this._header` can be null if OutgoingMessage is used without a proper Socket
// See: /test/parallel/test-http-outgoing-message-inheritance.js
if (typeof data === "string" && (encoding === "utf8" || encoding === "latin1" || !encoding)) {
data = this._header + data;
} else {
const header = this._header;
this.outputData.unshift({
data: header,
encoding: "latin1",
callback: null,
});
this.outputSize += header.length;
this._onPendingData(header.length);
}
this._headerSent = true;
}
return this._writeRaw(data, encoding, callback, byteLength);
},
_writeRaw(data, encoding, callback, size) {
const conn = this[kHandle];
if (conn?.destroyed) {
// The socket was destroyed. If we're still trying to write to it,
// then we haven't gotten the 'close' event yet.
return false;
}
return this.write(data, encoding, callback);
if (typeof encoding === "function") {
callback = encoding;
encoding = null;
}
if (conn && conn._httpMessage === this && conn.writable) {
// There might be pending data in the this.output buffer.
if (this.outputData.length) {
this._flushOutput(conn);
}
// Directly write to socket.
return conn.write(data, encoding, callback);
}
// Buffer, as long as we're not destroyed.
this.outputData.push({ data, encoding, callback });
this.outputSize += data.length;
this._onPendingData(data.length);
return this.outputSize < this[kHighWaterMark];
},
end(_chunk, _encoding, _callback) {
return this;
},
@@ -251,6 +504,7 @@ const OutgoingMessagePrototype = {
},
};
OutgoingMessage.prototype = OutgoingMessagePrototype;
$setPrototypeDirect.$call(OutgoingMessage, Stream);
function onTimeout() {

View File

@@ -39,7 +39,7 @@ const {
runSymbol,
drainMicrotasks,
setServerIdleTimeout,
setRequireHostHeader,
setServerCustomOptions,
} = require("internal/http");
const { format } = require("internal/util/inspect");
@@ -47,6 +47,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);
@@ -113,6 +114,28 @@ function onServerResponseClose() {
}
}
function strictContentLength(response) {
if (response.strictContentLength) {
let contentLength = response._contentLength ?? response.getHeader("content-length");
if (
contentLength &&
response._hasBody &&
!response._removedContLen &&
!response.chunkedEncoding &&
!response.hasHeader("transfer-encoding")
) {
if (typeof contentLength === "number") {
return contentLength;
} else if (typeof contentLength === "string") {
contentLength = parseInt(contentLength, 10);
if (isNaN(contentLength)) {
return;
}
return contentLength;
}
}
}
}
const ServerResponsePrototype = {
constructor: ServerResponse,
__proto__: OutgoingMessage.prototype,
@@ -229,13 +252,13 @@ const ServerResponsePrototype = {
this[headerStateSymbol] = NodeHTTPHeaderState.sent;
// https://github.com/nodejs/node/blob/2eff28fb7a93d3f672f80b582f664a7c701569fb/lib/_http_outgoing.js#L987
this._contentLength = handle.end(chunk, encoding);
this._contentLength = handle.end(chunk, encoding, undefined, strictContentLength(this));
});
} else {
// If there's no data but you already called end, then you're done.
// We can ignore it in that case.
if (!(!chunk && handle.ended) && !handle.aborted) {
handle.end(chunk, encoding);
handle.end(chunk, encoding, undefined, strictContentLength(this));
}
}
this._header = " ";
@@ -315,8 +338,10 @@ const ServerResponsePrototype = {
if (!handle) {
if (this.socket) {
console.log("writing to socket");
return this.socket.write(chunk, encoding, callback);
} else {
console.log("writing to outgoing message");
return OutgoingMessagePrototype.write.$call(this, chunk, encoding, callback);
}
}
@@ -335,10 +360,10 @@ const ServerResponsePrototype = {
// If handle.writeHead throws, we don't want headersSent to be set to true.
// So we set it here.
this[headerStateSymbol] = NodeHTTPHeaderState.sent;
result = handle.write(chunk, encoding, allowWritesToContinue.bind(this));
result = handle.write(chunk, encoding, allowWritesToContinue.bind(this), strictContentLength(this));
});
} else {
result = handle.write(chunk, encoding, allowWritesToContinue.bind(this));
result = handle.write(chunk, encoding, allowWritesToContinue.bind(this), strictContentLength(this));
}
if (result < 0) {
@@ -390,6 +415,7 @@ const ServerResponsePrototype = {
},
_implicitHeader() {
if (this.headersSent) return;
// @ts-ignore
this.writeHead(this.statusCode);
},
@@ -424,19 +450,20 @@ const ServerResponsePrototype = {
handle.cork(() => {
handle.writeHead(this.statusCode, this.statusMessage, this[headersSymbol]);
this[headerStateSymbol] = NodeHTTPHeaderState.sent;
handle.write(data, encoding, callback);
handle.write(data, encoding, callback, strictContentLength(this));
});
} else {
handle.write(data, encoding, callback);
handle.write(data, encoding, callback, strictContentLength(this));
}
},
writeHead(statusCode, statusMessage, headers) {
if (this[headerStateSymbol] === NodeHTTPHeaderState.none) {
_writeHead(statusCode, statusMessage, headers, this);
updateHasBody(this, statusCode);
this[headerStateSymbol] = NodeHTTPHeaderState.assigned;
if (this.headersSent) {
throw $ERR_HTTP_HEADERS_SENT("writeHead");
}
_writeHead(statusCode, statusMessage, headers, this);
updateHasBody(this, statusCode);
this[headerStateSymbol] = NodeHTTPHeaderState.assigned;
return this;
},
@@ -499,6 +526,7 @@ const ServerResponsePrototype = {
if (handle) {
if (this[headerStateSymbol] === NodeHTTPHeaderState.assigned) {
this[headerStateSymbol] = NodeHTTPHeaderState.sent;
handle.writeHead(this.statusCode, this.statusMessage, this[headersSymbol]);
}
handle.flushHeaders();
@@ -591,7 +619,7 @@ const Server = function Server(options, callback) {
this.maxRequestsPerSocket = 0;
this[kInternalSocketData] = undefined;
this[tlsSymbol] = null;
this.noDelay = true;
if (typeof options === "function") {
callback = options;
options = {};
@@ -671,12 +699,42 @@ function onServerRequestEvent(this: NodeHTTPServerSocket, event: NodeHTTPRespons
}
}
}
function onServerClientError(ssl: boolean, socket: unknown, errorCode: number, rawPacket: ArrayBuffer) {
const self = this as Server;
let err;
switch (errorCode) {
case 2:
err = $HPE_UNEXPECTED_CONTENT_LENGTH("Parse Error");
break;
case 3:
err = $HPE_INVALID_TRANSFER_ENCODING("Parse Error");
break;
case 8:
err = $HPE_INVALID_EOF_STATE("Parse Error");
break;
case 9:
err = $HPE_INVALID_METHOD("Parse Error: Invalid method encountered");
err.bytesParsed = 1; // always 1 for now because is the first byte of the request line
break;
default:
err = $HPE_INTERNAL("Parse Error");
break;
}
err.rawPacket = rawPacket;
const nodeSocket = new NodeHTTPServerSocket(self, socket, ssl);
self.emit("connection", nodeSocket);
self.emit("clientError", err, nodeSocket);
if (nodeSocket.listenerCount("error") > 0) {
nodeSocket.emit("error", err);
}
}
const ServerPrototype = {
constructor: Server,
__proto__: EventEmitter.prototype,
[kIncomingMessage]: undefined,
[kServerResponse]: undefined,
[kConnectionsCheckingInterval]: { _destroyed: false },
ref() {
this._unref = false;
this[serverSymbol]?.ref?.();
@@ -695,6 +753,9 @@ const ServerPrototype = {
return;
}
this[serverSymbol] = undefined;
this[kConnectionsCheckingInterval]._destroyed = true;
this.listening = false;
server.stop(true);
},
@@ -709,7 +770,9 @@ const ServerPrototype = {
return;
}
this[serverSymbol] = undefined;
this[kConnectionsCheckingInterval]._destroyed = true;
if (typeof optionalCallback === "function") setCloseCallback(this, optionalCallback);
this.listening = false;
server.stop();
},
@@ -1045,7 +1108,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, true, onServerClientError.bind(this));
if (this?._unref) {
this[serverSymbol]?.unref?.();
@@ -1082,23 +1145,28 @@ $setPrototypeDirect.$call(Server, EventEmitter);
const NodeHTTPServerSocket = class Socket extends Duplex {
bytesRead = 0;
bytesWritten = 0;
connecting = false;
timeout = 0;
[kHandle];
server: Server;
_httpMessage;
_secureEstablished = false;
constructor(server: Server, handle, encrypted) {
super();
this.server = server;
this[kHandle] = handle;
this._secureEstablished = !!handle?.secureEstablished;
handle.onclose = this.#onClose.bind(this);
handle.duplex = this;
this.encrypted = encrypted;
this.on("timeout", onNodeHTTPServerSocketTimeout);
}
get bytesWritten() {
return this[kHandle]?.response?.getBytesWritten?.() ?? 0;
}
set bytesWritten(value) {}
#closeHandle(handle, callback) {
this[kHandle] = undefined;
handle.onclose = this.#onCloseForDestroy.bind(this, callback);
@@ -1364,7 +1432,22 @@ function _writeHead(statusCode, reason, obj, response) {
}
} else {
if (length % 2 !== 0) {
throw new Error("raw headers must have an even number of elements");
throw $ERR_INVALID_ARG_VALUE("headers", obj);
}
// Test non-chunked message does not have trailer header set,
// message will be terminated by the first empty line after the
// header fields, regardless of the header fields present in the
// message, and thus cannot contain a message body or 'trailers'.
if (response.chunkedEncoding !== true && response._trailer) {
throw $ERR_HTTP_TRAILER_INVALID("Trailers are invalid with this transfer encoding");
}
// Headers in obj should override previous headers but still
// allow explicit duplicates. To do so, we first remove any
// existing conflicts, then use appendHeader.
for (let n = 0; n < length; n += 2) {
k = obj[n + 0];
response.removeHeader(k);
}
for (let n = 0; n < length; n += 2) {
@@ -1685,4 +1768,5 @@ function ensureReadableStreamController(run) {
export default {
Server,
ServerResponse,
kConnectionsCheckingInterval,
};

View File

@@ -80,6 +80,7 @@ const {
validateFunction,
checkIsHttpToken,
validateLinkHeaderValue,
validateUint32,
} = require("internal/validators");
let utcCache;
@@ -3514,13 +3515,25 @@ class Http2SecureServer extends tls.Server {
timeout = 0;
constructor(options, onRequestHandler) {
//TODO: add 'http/1.1' on ALPNProtocols list after allowHTTP1 support
if (typeof options === "function") {
onRequestHandler = options;
options = { ALPNProtocols: ["h2"] };
} else if (options == null || typeof options == "object") {
options = { ...options, ALPNProtocols: ["h2"] };
if (typeof options !== "undefined") {
if (options && typeof options === "object") {
options = { ...options, ALPNProtocols: ["h2"] };
} else {
throw $ERR_INVALID_ARG_TYPE("options", "object", options);
}
} else {
throw $ERR_INVALID_ARG_TYPE("options", "object", options);
options = { ALPNProtocols: ["h2"] };
}
const settings = options.settings;
if (typeof settings !== "undefined") {
validateObject(settings, "options.settings");
}
if (options.maxSessionInvalidFrames !== undefined)
validateUint32(options.maxSessionInvalidFrames, "maxSessionInvalidFrames");
if (options.maxSessionRejectedStreams !== undefined) {
validateUint32(options.maxSessionRejectedStreams, "maxSessionRejectedStreams");
}
super(options, connectionListener);
this.setMaxListeners(0);

View File

@@ -23,14 +23,17 @@ import http, {
validateHeaderName,
validateHeaderValue,
} from "node:http";
import { connect, createConnection } from "node:net";
import type { AddressInfo } 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";
import { PassThrough } from "node:stream";
import * as zlib from "node:zlib";
import { run as runHTTPProxyTest } from "./node-http-proxy.js";
import { assert } from "node:console";
const { describe, expect, it, beforeAll, afterAll, createDoneDotAll, mock, test } = createTest(import.meta.path);
function listen(server: Server, protocol: string = "http"): Promise<URL> {
return new Promise((resolve, reject) => {
@@ -2417,3 +2420,287 @@ it("should reject non-standard body writes when rejectNonStandardBodyWrites is t
}
}
});
test("should emit clientError when Content-Length is invalid", async () => {
const { promise, resolve, reject } = Promise.withResolvers();
await using 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();
await using 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");
});
test("should be able to flush headers socket._httpMessage must be set", async () => {
let server: Server | undefined;
try {
server = http.createServer((req, res) => {
res.flushHeaders();
});
await once(server.listen(0), "listening");
const { promise, resolve } = Promise.withResolvers();
const address = server.address() as AddressInfo;
const req = http.get(
{
hostname: address.address,
port: address.port,
},
resolve,
);
const { socket } = req;
await promise;
expect(socket._httpMessage).toBe(req);
socket.destroy();
} finally {
server?.closeAllConnections();
}
});
test("req.connection.bytesWritten must be supported on the server", async () => {
let httpServer: Server;
try {
const { promise, resolve } = Promise.withResolvers();
httpServer = http.createServer(function (req, res) {
res.on("finish", () => resolve(req.connection.bytesWritten));
res.writeHead(200, { "Content-Type": "text/plain" });
const chunk = "7".repeat(1024);
const bchunk = Buffer.from(chunk);
res.write(chunk);
res.write(bchunk);
expect(res.connection.bytesWritten).toBe(1024 * 2);
res.end("bunbunbun");
});
await once(httpServer.listen(0), "listening");
const address = httpServer.address() as AddressInfo;
const req = http.get({ port: address.port });
await once(req, "response");
const bytesWritten = await promise;
expect(typeof bytesWritten).toBe("number");
expect(bytesWritten).toBe(1024 * 2 + 9);
req.destroy();
} finally {
httpServer?.closeAllConnections();
}
});
test("req.connection.bytesWritten must be supported on the https server", async () => {
let httpServer: Server;
try {
const { promise, resolve } = Promise.withResolvers();
httpServer = createHttpsServer(COMMON_TLS_CERT, function (req, res) {
res.on("finish", () => resolve(req.connection.bytesWritten));
res.writeHead(200, { "Content-Type": "text/plain" });
// Write 1.5mb to cause some requests to buffer
// Also, mix up the encodings a bit.
const chunk = "7".repeat(1024);
const bchunk = Buffer.from(chunk);
for (let i = 0; i < 1024; i++) {
res.write(chunk);
res.write(bchunk);
res.write(chunk, "hex");
}
// Get .bytesWritten while buffer is not empty
expect(res.connection.bytesWritten).toBeGreaterThan(0);
res.end("bunbubun");
});
await once(httpServer.listen(0), "listening");
const address = httpServer.address() as AddressInfo;
const req = https.get({ port: address.port, rejectUnauthorized: false });
await once(req, "response");
const bytesWritten = await promise;
expect(typeof bytesWritten).toBe("number");
expect(bytesWritten).toBeGreaterThan(0);
req.destroy();
} finally {
httpServer?.closeAllConnections();
}
});
test("host array should throw in http.request", () => {
expect(() =>
http.request({
host: [1, 2, 3],
}),
).toThrow('The "options.host" property must be of type string, undefined, or null. Received an instance of Array');
});
test("strictContentLength should work on server", async () => {
const { promise, resolve, reject } = Promise.withResolvers();
await using server = http.createServer((req, res) => {
try {
res.strictContentLength = true;
res.writeHead(200, { "Content-Length": 10 });
res.write("123456789");
// Too much data
try {
res.write("123456789");
expect.unreachable();
} catch (e: any) {
expect(e).toBeInstanceOf(Error);
expect(e.code).toBe("ERR_HTTP_CONTENT_LENGTH_MISMATCH");
}
// Too little data
try {
res.end();
expect.unreachable();
} catch (e: any) {
expect(e).toBeInstanceOf(Error);
expect(e.code).toBe("ERR_HTTP_CONTENT_LENGTH_MISMATCH");
}
// Just right
res.end("0");
resolve();
} catch (e: any) {
reject(e);
} finally {
}
});
await once(server.listen(0), "listening");
const url = `http://localhost:${server.address().port}`;
await fetch(url, {
method: "GET",
}).catch(() => {});
await promise;
});
test("client side flushHeaders should work", async () => {
const { promise, resolve } = Promise.withResolvers();
await using server = http.createServer((req, res) => {
resolve(req.headers);
res.end();
});
await once(server.listen(0), "listening");
const address = server.address() as AddressInfo;
const req = http.request({
method: "GET",
host: "127.0.0.1",
port: address.port,
});
req.setHeader("foo", "bar");
req.flushHeaders();
const headers = await promise;
expect(headers).toBeDefined();
expect(headers.foo).toEqual("bar");
});
test("server.listening should work", async () => {
const server = http.createServer();
await once(server.listen(0), "listening");
expect(server.listening).toBe(true);
server.closeAllConnections();
expect(server.listening).toBe(false);
});
test("asyncDispose should work in http.Server", async () => {
const server = http.createServer();
await once(server.listen(0), "listening");
expect(server.listening).toBe(true);
await server[Symbol.asyncDispose]();
expect(server.listening).toBe(false);
});
test("timeout destruction should be visible using kConnectionsCheckingInterval", async () => {
const { kConnectionsCheckingInterval } = require("_http_server");
const server = http.createServer();
await once(server.listen(0), "listening");
server.closeAllConnections();
expect(server[kConnectionsCheckingInterval]._destroyed).toBe(true);
});
test("client should be able to send a array of [key, value] as headers", async () => {
const { promise, resolve } = Promise.withResolvers();
await using server = http.createServer((req, res) => {
resolve([req, res]);
});
await once(server.listen(0), "listening");
const address = server.address() as AddressInfo;
http.get({
host: "127.0.0.1",
port: address.port,
headers: [
["foo", "bar"],
["foo", "baz"],
["host", "127.0.0.1"],
["host", "127.0.0.2"],
["host", "127.0.0.3"],
],
});
const [req, res] = await promise;
expect(req.headers.foo).toBe("bar, baz");
expect(req.headers.host).toBe("127.0.0.1");
res.end();
});
test("clientError should fire when receiving invalid method", async () => {
await using server = http.createServer((req, res) => {
res.end();
});
let socket;
server.on("clientError", err => {
expect(err.code).toBe("HPE_INVALID_METHOD");
expect(err.rawPacket.toString()).toBe("*");
socket.end();
});
await once(server.listen(0), "listening");
const address = server.address() as AddressInfo;
socket = createConnection({ port: address.port });
await once(socket, "connect");
socket.write("*");
await once(socket, "close");
});

View File

@@ -0,0 +1,21 @@
'use strict';
const { mustCall } = require('../common');
const http = require('http');
const { strictEqual } = require('assert');
const server = http.createServer(mustCall((req, res) => {
res.flushHeaders();
}));
server.listen(0, mustCall(() => {
const req = http.get({
port: server.address().port
}, mustCall(() => {
const { socket } = req;
socket.emit('agentRemove');
strictEqual(socket._httpMessage, req);
socket.destroy();
server.close();
}));
}));

View File

@@ -0,0 +1,55 @@
// 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');
const assert = require('assert');
const http = require('http');
const body = 'hello world\n';
const httpServer = http.createServer(common.mustCall(function(req, res) {
httpServer.close();
res.on('finish', common.mustCall(function() {
assert.strictEqual(typeof req.connection.bytesWritten, 'number');
assert(req.connection.bytesWritten > 0);
}));
res.writeHead(200, { 'Content-Type': 'text/plain' });
// Write 1.5mb to cause some requests to buffer
// Also, mix up the encodings a bit.
const chunk = '7'.repeat(1024);
const bchunk = Buffer.from(chunk);
for (let i = 0; i < 1024; i++) {
res.write(chunk);
res.write(bchunk);
res.write(chunk, 'hex');
}
// Get .bytesWritten while buffer is not empty
assert(res.connection.bytesWritten > 0);
res.end(body);
}));
httpServer.listen(0, function() {
http.get({ port: this.address().port });
});

View File

@@ -0,0 +1,23 @@
'use strict';
require('../common');
const assert = require('assert');
const http = require('http');
{
const options = {
port: '80',
path: '/',
headers: {
host: []
}
};
assert.throws(() => {
http.request(options);
}, {
code: /ERR_INVALID_ARG_TYPE/
}, 'http request should throw when passing array as header host');
}

View File

@@ -0,0 +1,80 @@
'use strict';
const common = require('../common');
const assert = require('assert');
const http = require('http');
function shouldThrowOnMoreBytes() {
const server = http.createServer(common.mustCall((req, res) => {
res.strictContentLength = true;
res.setHeader('Content-Length', 5);
res.write('hello');
assert.throws(() => {
res.write('a');
}, {
code: 'ERR_HTTP_CONTENT_LENGTH_MISMATCH'
});
res.statusCode = 200;
res.end();
}));
server.listen(0, () => {
const req = http.get({
port: server.address().port,
}, common.mustCall((res) => {
res.resume();
assert.strictEqual(res.statusCode, 200);
server.close();
}));
req.end();
});
}
function shouldNotThrow() {
const server = http.createServer(common.mustCall((req, res) => {
res.strictContentLength = true;
res.write('helloaa');
res.statusCode = 200;
res.end('ending');
}));
server.listen(0, () => {
http.get({
port: server.address().port,
}, common.mustCall((res) => {
res.resume();
assert.strictEqual(res.statusCode, 200);
server.close();
}));
});
}
function shouldThrowOnFewerBytes() {
const server = http.createServer(common.mustCall((req, res) => {
res.strictContentLength = true;
res.setHeader('Content-Length', 5);
res.write('a');
res.statusCode = 200;
assert.throws(() => {
res.end('aaa');
}, {
code: 'ERR_HTTP_CONTENT_LENGTH_MISMATCH'
});
res.end('aaaa');
}));
server.listen(0, () => {
http.get({
port: server.address().port,
}, common.mustCall((res) => {
res.resume();
assert.strictEqual(res.statusCode, 200);
server.close();
}));
});
}
shouldThrowOnMoreBytes();
shouldNotThrow();
shouldThrowOnFewerBytes();

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,20 @@
'use strict';
require('../common');
const assert = require('assert');
const http = require('http');
const server = http.createServer();
server.on('request', function(req, res) {
assert.strictEqual(req.headers.foo, 'bar');
res.end('ok');
server.close();
});
server.listen(0, '127.0.0.1', function() {
const req = http.request({
method: 'GET',
host: '127.0.0.1',
port: this.address().port,
});
req.setHeader('foo', 'bar');
req.flushHeaders();
});

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,16 @@
'use strict';
const common = require('../common');
const assert = require('assert');
const http = require('http');
const server = http.createServer();
assert.strictEqual(server.listening, false);
server.listen(0, common.mustCall(() => {
assert.strictEqual(server.listening, true);
server.close(common.mustCall(() => {
assert.strictEqual(server.listening, false);
}));
}));

View File

@@ -0,0 +1,136 @@
'use strict';
require('../common');
const assert = require('assert');
const http = require('http');
const OutgoingMessage = http.OutgoingMessage;
const ClientRequest = http.ClientRequest;
const ServerResponse = http.ServerResponse;
assert.strictEqual(
typeof ClientRequest.prototype._implicitHeader, 'function');
assert.strictEqual(
typeof ServerResponse.prototype._implicitHeader, 'function');
// validateHeader
assert.throws(() => {
const outgoingMessage = new OutgoingMessage();
outgoingMessage.setHeader();
}, {
code: 'ERR_INVALID_HTTP_TOKEN',
name: 'TypeError',
message: 'Header name must be a valid HTTP token ["undefined"]'
});
assert.throws(() => {
const outgoingMessage = new OutgoingMessage();
outgoingMessage.setHeader('test');
}, {
code: 'ERR_HTTP_INVALID_HEADER_VALUE',
name: 'TypeError',
message: 'Invalid value "undefined" for header "test"'
});
assert.throws(() => {
const outgoingMessage = new OutgoingMessage();
outgoingMessage.setHeader(404);
}, {
code: 'ERR_INVALID_HTTP_TOKEN',
name: 'TypeError',
message: 'Header name must be a valid HTTP token ["404"]'
});
assert.throws(() => {
const outgoingMessage = new OutgoingMessage();
outgoingMessage.setHeader.call({ _header: 'test' }, 'test', 'value');
}, {
code: 'ERR_HTTP_HEADERS_SENT',
name: 'Error',
message: 'Cannot set headers after they are sent to the client'
});
assert.throws(() => {
const outgoingMessage = new OutgoingMessage();
outgoingMessage.setHeader('200', 'あ');
}, {
code: 'ERR_INVALID_CHAR',
name: 'TypeError',
message: 'Invalid character in header content ["200"]'
});
// write
{
const outgoingMessage = new OutgoingMessage();
assert.throws(
() => {
outgoingMessage.write('');
},
{
code: 'ERR_METHOD_NOT_IMPLEMENTED',
name: 'Error',
message: 'The _implicitHeader() method is not implemented'
}
);
}
assert.throws(() => {
const outgoingMessage = new OutgoingMessage();
outgoingMessage.write.call({ _header: 'test', _hasBody: 'test' });
}, {
code: 'ERR_INVALID_ARG_TYPE',
name: 'TypeError',
message: 'The "chunk" argument must be of type string, Buffer, or Uint8Array. Received undefined'
});
assert.throws(() => {
const outgoingMessage = new OutgoingMessage();
outgoingMessage.write.call({ _header: 'test', _hasBody: 'test' }, 1);
}, {
code: "ERR_INVALID_ARG_TYPE",
name: "TypeError",
message:
'The "chunk" argument must be of type string, Buffer, or Uint8Array. Received type number (1)',
});
assert.throws(() => {
const outgoingMessage = new OutgoingMessage();
outgoingMessage.write.call({ _header: 'test', _hasBody: 'test' }, null);
}, {
code: 'ERR_STREAM_NULL_VALUES',
name: 'TypeError'
});
// addTrailers()
// The `Error` comes from the JavaScript engine so confirm that it is a
// `TypeError` but do not check the message. It will be different in different
// JavaScript engines.
assert.throws(() => {
const outgoingMessage = new OutgoingMessage();
outgoingMessage.addTrailers();
}, TypeError);
assert.throws(() => {
const outgoingMessage = new OutgoingMessage();
outgoingMessage.addTrailers({ 'あ': 'value' });
}, {
code: 'ERR_INVALID_HTTP_TOKEN',
name: 'TypeError',
message: 'Trailer name must be a valid HTTP token ["あ"]'
});
assert.throws(() => {
const outgoingMessage = new OutgoingMessage();
outgoingMessage.addTrailers({ 404: 'あ' });
}, {
code: 'ERR_INVALID_CHAR',
name: 'TypeError',
message: 'Invalid character in trailer content ["404"]'
});
{
const outgoingMessage = new OutgoingMessage();
assert.strictEqual(outgoingMessage.destroyed, false);
outgoingMessage.destroy();
assert.strictEqual(outgoingMessage.destroyed, true);
}

View File

@@ -0,0 +1,174 @@
'use strict';
const common = require('../common');
const http = require('http');
const assert = require('assert');
{
const server = http.createServer({ requireHostHeader: false }, common.mustCall((req, res) => {
res.writeHead(200); // Headers already sent
const headers = new globalThis.Headers({ foo: '1' });
assert.throws(() => {
res.setHeaders(headers);
}, {
code: 'ERR_HTTP_HEADERS_SENT'
});
res.end();
}));
server.listen(0, common.mustCall(() => {
http.get({ port: server.address().port }, (res) => {
assert.strictEqual(res.headers.foo, undefined);
res.resume().on('end', common.mustCall(() => {
server.close();
}));
});
}));
}
{
const server = http.createServer({ requireHostHeader: false }, common.mustCall((req, res) => {
assert.throws(() => {
res.setHeaders(['foo', '1']);
}, {
code: 'ERR_INVALID_ARG_TYPE'
});
assert.throws(() => {
res.setHeaders({ foo: '1' });
}, {
code: 'ERR_INVALID_ARG_TYPE'
});
assert.throws(() => {
res.setHeaders(null);
}, {
code: 'ERR_INVALID_ARG_TYPE'
});
assert.throws(() => {
res.setHeaders(undefined);
}, {
code: 'ERR_INVALID_ARG_TYPE'
});
assert.throws(() => {
res.setHeaders('test');
}, {
code: 'ERR_INVALID_ARG_TYPE'
});
assert.throws(() => {
res.setHeaders(1);
}, {
code: 'ERR_INVALID_ARG_TYPE'
});
res.end();
}));
server.listen(0, common.mustCall(() => {
http.get({ port: server.address().port }, (res) => {
assert.strictEqual(res.headers.foo, undefined);
res.resume().on('end', common.mustCall(() => {
server.close();
}));
});
}));
}
{
const server = http.createServer({ requireHostHeader: false }, common.mustCall((req, res) => {
const headers = new globalThis.Headers({ foo: '1', bar: '2' });
res.setHeaders(headers);
res.writeHead(200);
res.end();
}));
server.listen(0, common.mustCall(() => {
http.get({ port: server.address().port }, (res) => {
assert.strictEqual(res.statusCode, 200);
assert.strictEqual(res.headers.foo, '1');
assert.strictEqual(res.headers.bar, '2');
res.resume().on('end', common.mustCall(() => {
server.close();
}));
});
}));
}
{
const server = http.createServer({ requireHostHeader: false }, common.mustCall((req, res) => {
const headers = new globalThis.Headers({ foo: '1', bar: '2' });
res.setHeaders(headers);
res.writeHead(200, ['foo', '3']);
res.end();
}));
server.listen(0, common.mustCall(() => {
http.get({ port: server.address().port }, (res) => {
assert.strictEqual(res.statusCode, 200);
assert.strictEqual(res.headers.foo, '3'); // Override by writeHead
assert.strictEqual(res.headers.bar, '2');
res.resume().on('end', common.mustCall(() => {
server.close();
}));
});
}));
}
{
const server = http.createServer({ requireHostHeader: false }, common.mustCall((req, res) => {
const headers = new Map([['foo', '1'], ['bar', '2']]);
res.setHeaders(headers);
res.writeHead(200);
res.end();
}));
server.listen(0, common.mustCall(() => {
http.get({ port: server.address().port }, (res) => {
assert.strictEqual(res.statusCode, 200);
assert.strictEqual(res.headers.foo, '1');
assert.strictEqual(res.headers.bar, '2');
res.resume().on('end', common.mustCall(() => {
server.close();
}));
});
}));
}
{
const server = http.createServer({ requireHostHeader: false }, common.mustCall((req, res) => {
const headers = new Headers();
headers.append('Set-Cookie', 'a=b');
headers.append('Set-Cookie', 'c=d');
res.setHeaders(headers);
res.end();
}));
server.listen(0, common.mustCall(() => {
http.get({ port: server.address().port }, (res) => {
assert(Array.isArray(res.headers['set-cookie']));
assert.strictEqual(res.headers['set-cookie'].length, 2);
assert.strictEqual(res.headers['set-cookie'][0], 'a=b');
assert.strictEqual(res.headers['set-cookie'][1], 'c=d');
res.resume().on('end', common.mustCall(() => {
server.close();
}));
});
}));
}
{
const server = http.createServer({ requireHostHeader: false }, common.mustCall((req, res) => {
const headers = new Map();
headers.set('Set-Cookie', ['a=b', 'c=d']);
res.setHeaders(headers);
res.end();
}));
server.listen(0, common.mustCall(() => {
http.get({ port: server.address().port }, (res) => {
assert(Array.isArray(res.headers['set-cookie']));
assert.strictEqual(res.headers['set-cookie'].length, 2);
assert.strictEqual(res.headers['set-cookie'][0], 'a=b');
assert.strictEqual(res.headers['set-cookie'][1], 'c=d');
res.resume().on('end', common.mustCall(() => {
server.close();
}));
});
}));
}

View File

@@ -0,0 +1,14 @@
'use strict';
const common = require('../common');
const assert = require('assert');
const { createServer } = require('http');
const { kConnectionsCheckingInterval } = require('_http_server');
const server = createServer();
server.listen(0, common.mustCall(() => {
server.on('close', common.mustCall());
server[Symbol.asyncDispose]().then(common.mustCall(() => {
assert(server[kConnectionsCheckingInterval]._destroyed);
}));
}));

View File

@@ -0,0 +1,13 @@
'use strict';
const common = require('../common');
const assert = require('assert');
const { createServer } = require('http');
const { kConnectionsCheckingInterval } = require('_http_server');
const server = createServer(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,48 @@
'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: false,
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,80 @@
// 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';
// Verify that the HTTP server implementation handles multiple instances
// of the same header as per RFC2616: joining the handful of fields by ', '
// that support it, and dropping duplicates for other fields.
require('../common');
const assert = require('assert');
const http = require('http');
const server = http.createServer(function(req, res) {
assert.strictEqual(req.headers.accept, 'abc, def, ghijklmnopqrst');
assert.strictEqual(req.headers.host, 'foo');
assert.strictEqual(req.headers['www-authenticate'], 'foo, bar, baz');
assert.strictEqual(req.headers['proxy-authenticate'], 'foo, bar, baz');
assert.strictEqual(req.headers['x-foo'], 'bingo');
assert.strictEqual(req.headers['x-bar'], 'banjo, bango');
assert.strictEqual(req.headers['sec-websocket-protocol'], 'chat, share');
assert.strictEqual(req.headers['sec-websocket-extensions'],
'foo; 1, bar; 2, baz');
assert.strictEqual(req.headers.constructor, 'foo, bar, baz');
res.writeHead(200, { 'Content-Type': 'text/plain' });
res.end('EOF');
server.close();
});
server.listen(0, function() {
http.get({
host: 'localhost',
port: this.address().port,
path: '/',
headers: [
['accept', 'abc'],
['accept', 'def'],
['Accept', 'ghijklmnopqrst'],
['host', 'foo'],
['Host', 'bar'],
['hOst', 'baz'],
['www-authenticate', 'foo'],
['WWW-Authenticate', 'bar'],
['WWW-AUTHENTICATE', 'baz'],
['proxy-authenticate', 'foo'],
['Proxy-Authenticate', 'bar'],
['PROXY-AUTHENTICATE', 'baz'],
['x-foo', 'bingo'],
['x-bar', 'banjo'],
['x-bar', 'bango'],
['sec-websocket-protocol', 'chat'],
['sec-websocket-protocol', 'share'],
['sec-websocket-extensions', 'foo; 1'],
['sec-websocket-extensions', 'bar; 2'],
['sec-websocket-extensions', 'baz'],
['constructor', 'foo'],
['constructor', 'bar'],
['constructor', 'baz'],
]
});
});

View File

@@ -0,0 +1,30 @@
'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' +
'Host: localhost\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,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,79 @@
'use strict';
const common = require('../common');
const assert = require('assert');
const http = require('http');
// Verify that ServerResponse.writeHead() works with arrays.
{
const server = http.createServer(common.mustCall((req, res) => {
res.setHeader('test', '1');
res.writeHead(200, [ 'test', '2', 'test2', '2' ]);
res.end();
}));
server.listen(0, common.mustCall(() => {
http.get({ port: server.address().port }, common.mustCall((res) => {
assert.strictEqual(res.headers.test, '2');
assert.strictEqual(res.headers.test2, '2');
res.resume().on('end', common.mustCall(() => {
server.close();
}));
}));
}));
}
{
const server = http.createServer(common.mustCall((req, res) => {
res.writeHead(200, [ 'test', '1', 'test2', '2' ]);
res.end();
}));
server.listen(0, common.mustCall(() => {
http.get({ port: server.address().port }, common.mustCall((res) => {
assert.strictEqual(res.headers.test, '1');
assert.strictEqual(res.headers.test2, '2');
res.resume().on('end', common.mustCall(() => {
server.close();
}));
}));
}));
}
{
const server = http.createServer(common.mustCall((req, res) => {
try {
res.writeHead(200, [ 'test', '1', 'test2', '2', 'asd' ]);
} catch (err) {
assert.strictEqual(err.code, 'ERR_INVALID_ARG_VALUE');
}
res.end();
}));
server.listen(0, common.mustCall(() => {
http.get({ port: server.address().port }, common.mustCall((res) => {
res.resume().on('end', common.mustCall(() => {
server.close();
}));
}));
}));
}
{
const server = http.createServer(common.mustCall((req, res) => {
res.writeHead(200, undefined, [ 'foo', 'bar' ]);
res.end();
}));
server.listen(0, common.mustCall(() => {
http.get({ port: server.address().port }, common.mustCall((res) => {
assert.strictEqual(res.statusMessage, 'OK');
assert.strictEqual(res.statusCode, 200);
assert.strictEqual(res.headers.foo, 'bar');
res.resume().on('end', common.mustCall(() => {
server.close();
}));
}));
}));
}

View File

@@ -0,0 +1,106 @@
// 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');
const assert = require('assert');
const http = require('http');
// Verify that ServerResponse.writeHead() works as setHeader.
// Issue 5036 on github.
const s = http.createServer(common.mustCall((req, res) => {
res.setHeader('test', '1');
// toLowerCase() is used on the name argument, so it must be a string.
// Non-String header names should throw
assert.throws(
() => res.setHeader(0xf00, 'bar'),
{
code: 'ERR_INVALID_HTTP_TOKEN',
name: 'TypeError',
message: 'Header name must be a valid HTTP token ["3840"]'
}
);
// Undefined value should throw, via 979d0ca8
assert.throws(
() => res.setHeader('foo', undefined),
{
code: 'ERR_HTTP_INVALID_HEADER_VALUE',
name: 'TypeError',
message: 'Invalid value "undefined" for header "foo"'
}
);
assert.throws(() => {
res.writeHead(200, ['invalid', 'headers', 'args']);
}, {
code: 'ERR_INVALID_ARG_VALUE'
});
res.writeHead(200, { Test: '2' });
assert.throws(() => {
res.writeHead(100, {});
}, {
code: 'ERR_HTTP_HEADERS_SENT',
name: 'Error',
});
res.end();
}));
s.listen(0, common.mustCall(runTest));
function runTest() {
http.get({ port: this.address().port }, common.mustCall((response) => {
response.on('end', common.mustCall(() => {
assert.strictEqual(response.headers.test, '2');
assert(response.rawHeaders.includes('test'));
s.close();
}));
response.resume();
}));
}
{
const server = http.createServer(common.mustCall((req, res) => {
res.writeHead(220, [ 'test', '1' ]); // 220 is not a standard status code
assert.strictEqual(res.statusMessage, 'unknown');
assert.throws(() => res.writeHead(200, [ 'test2', '2' ]), {
code: 'ERR_HTTP_HEADERS_SENT',
name: 'Error',
});
res.end();
}));
server.listen(0, common.mustCall(() => {
http.get({ port: server.address().port }, (res) => {
assert.strictEqual(res.headers.test, '1');
assert.strictEqual('test2' in res.headers, false);
res.resume().on('end', common.mustCall(() => {
server.close();
}));
});
}));
}

View File

@@ -0,0 +1,78 @@
'use strict';
const common = require('../common');
if (!common.hasCrypto)
common.skip('missing crypto');
const assert = require('assert');
const http2 = require('http2');
// Error if invalid options are passed to createSecureServer.
const invalidOptions = [() => {}, 1, 'test', null, Symbol('test')];
invalidOptions.forEach((invalidOption) => {
assert.throws(
() => http2.createSecureServer(invalidOption),
{
name: 'TypeError',
code: 'ERR_INVALID_ARG_TYPE',
message: 'The "options" argument must be of type object.' +
common.invalidArgTypeHelper(invalidOption)
}
);
});
// Error if invalid options.settings are passed to createSecureServer.
invalidOptions.forEach((invalidSettingsOption) => {
assert.throws(
() => http2.createSecureServer({ settings: invalidSettingsOption }),
{
name: 'TypeError',
code: 'ERR_INVALID_ARG_TYPE',
message: 'The "options.settings" property must be of type object.' +
common.invalidArgTypeHelper(invalidSettingsOption)
}
);
});
// Test that http2.createSecureServer validates input options.
Object.entries({
maxSessionInvalidFrames: [
{
val: -1,
err: {
name: 'RangeError',
code: 'ERR_OUT_OF_RANGE',
},
},
{
val: Number.NEGATIVE_INFINITY,
err: {
name: 'RangeError',
code: 'ERR_OUT_OF_RANGE',
},
},
],
maxSessionRejectedStreams: [
{
val: -1,
err: {
name: 'RangeError',
code: 'ERR_OUT_OF_RANGE',
},
},
{
val: Number.NEGATIVE_INFINITY,
err: {
name: 'RangeError',
code: 'ERR_OUT_OF_RANGE',
},
},
],
}).forEach(([opt, tests]) => {
tests.forEach(({ val, err }) => {
assert.throws(
() => http2.createSecureServer({ [opt]: val }),
err
);
});
});

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');
const fixtures = require('../common/fixtures');
if (!common.hasCrypto)
common.skip('missing crypto');
const assert = require('assert');
const https = require('https');
const options = {
key: fixtures.readKey('agent1-key.pem'),
cert: fixtures.readKey('agent1-cert.pem')
};
const body = 'hello world\n';
const httpsServer = https.createServer(options, function(req, res) {
res.on('finish', function() {
assert.strictEqual(typeof req.connection.bytesWritten, 'number');
assert(req.connection.bytesWritten > 0);
httpsServer.close();
console.log('ok');
});
res.writeHead(200, { 'Content-Type': 'text/plain' });
res.end(body);
});
httpsServer.listen(0, function() {
https.get({
port: this.address().port,
rejectUnauthorized: false
});
});

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,19 @@
'use strict';
const common = require('../common');
if (!common.hasCrypto)
common.skip('missing crypto');
const assert = require('assert');
const { createServer } = require('https');
const { kConnectionsCheckingInterval } = require('_http_server');
const server = createServer();
server.listen(0, common.mustCall(() => {
server.on('close', common.mustCall());
server[Symbol.asyncDispose]().then(common.mustCall(() => {
assert(server[kConnectionsCheckingInterval]._destroyed);
}));
}));

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);
}));
}));