mirror of
https://github.com/oven-sh/bun
synced 2026-02-06 08:58:52 +00:00
Compare commits
35 Commits
dylan/byte
...
ciro/node-
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
82d45318e0 | ||
|
|
c7e4d9cc86 | ||
|
|
4ae29bf8a8 | ||
|
|
bf0327cb77 | ||
|
|
7f1657d59e | ||
|
|
d0099f2e59 | ||
|
|
56498f9ddc | ||
|
|
314e187625 | ||
|
|
522b0500af | ||
|
|
5e31ab90f2 | ||
|
|
07f87e13f4 | ||
|
|
65292fcef5 | ||
|
|
21c1561297 | ||
|
|
2d9a33d4cc | ||
|
|
d121f76fdb | ||
|
|
e83a1aaf0c | ||
|
|
e5f53b0904 | ||
|
|
d360f33608 | ||
|
|
68292d30ea | ||
|
|
c8fd689512 | ||
|
|
80fe35773f | ||
|
|
e48626d990 | ||
|
|
bd1b9bb8a4 | ||
|
|
b0689698c3 | ||
|
|
5fb0584b0e | ||
|
|
4581569d7b | ||
|
|
38901d79cb | ||
|
|
48987b8bac | ||
|
|
bb829a9d78 | ||
|
|
8c2b7b65a1 | ||
|
|
92a7a06153 | ||
|
|
38e45d69d5 | ||
|
|
46a42bf3bc | ||
|
|
7219e0d1ff | ||
|
|
f14f2b3b3c |
@@ -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);
|
||||
}
|
||||
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
|
||||
@@ -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(¶meterOffsets);
|
||||
|
||||
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") {
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
};
|
||||
|
||||
}
|
||||
|
||||
@@ -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};
|
||||
}
|
||||
};
|
||||
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
@@ -111,6 +111,10 @@ export default [
|
||||
fn: "end",
|
||||
length: 2,
|
||||
},
|
||||
getBytesWritten: {
|
||||
fn: "getBytesWritten",
|
||||
length: 0,
|
||||
},
|
||||
flushHeaders: {
|
||||
fn: "flushHeaders",
|
||||
length: 0,
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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`;
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -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;
|
||||
|
||||
11
src/http.zig
11
src/http.zig
@@ -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{
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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) {}
|
||||
|
||||
@@ -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,
|
||||
};
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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() {
|
||||
|
||||
@@ -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,
|
||||
};
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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");
|
||||
});
|
||||
|
||||
21
test/js/node/test/parallel/test-http-agent-remove.js
Normal file
21
test/js/node/test/parallel/test-http-agent-remove.js
Normal 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();
|
||||
}));
|
||||
}));
|
||||
55
test/js/node/test/parallel/test-http-byteswritten.js
Normal file
55
test/js/node/test/parallel/test-http-byteswritten.js
Normal 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 });
|
||||
});
|
||||
@@ -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');
|
||||
}
|
||||
@@ -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();
|
||||
@@ -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();
|
||||
}));
|
||||
});
|
||||
20
test/js/node/test/parallel/test-http-flush-headers.js
Normal file
20
test/js/node/test/parallel/test-http-flush-headers.js
Normal 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();
|
||||
});
|
||||
40
test/js/node/test/parallel/test-http-invalid-te.js
Normal file
40
test/js/node/test/parallel/test-http-invalid-te.js
Normal 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'));
|
||||
}));
|
||||
}));
|
||||
16
test/js/node/test/parallel/test-http-listening.js
Normal file
16
test/js/node/test/parallel/test-http-listening.js
Normal 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);
|
||||
}));
|
||||
}));
|
||||
136
test/js/node/test/parallel/test-http-outgoing-proto.js
Normal file
136
test/js/node/test/parallel/test-http-outgoing-proto.js
Normal 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);
|
||||
}
|
||||
174
test/js/node/test/parallel/test-http-response-setheaders.js
Normal file
174
test/js/node/test/parallel/test-http-response-setheaders.js
Normal 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();
|
||||
}));
|
||||
});
|
||||
}));
|
||||
}
|
||||
14
test/js/node/test/parallel/test-http-server-async-dispose.js
Normal file
14
test/js/node/test/parallel/test-http-server-async-dispose.js
Normal 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);
|
||||
}));
|
||||
}));
|
||||
@@ -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);
|
||||
}));
|
||||
}));
|
||||
@@ -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();
|
||||
}));
|
||||
});
|
||||
80
test/js/node/test/parallel/test-http-server-multiheaders.js
Normal file
80
test/js/node/test/parallel/test-http-server-multiheaders.js
Normal 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'],
|
||||
]
|
||||
});
|
||||
});
|
||||
@@ -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());
|
||||
});
|
||||
@@ -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();
|
||||
});
|
||||
});
|
||||
}
|
||||
79
test/js/node/test/parallel/test-http-write-head-2.js
Normal file
79
test/js/node/test/parallel/test-http-write-head-2.js
Normal 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();
|
||||
}));
|
||||
}));
|
||||
}));
|
||||
}
|
||||
106
test/js/node/test/parallel/test-http-write-head.js
Normal file
106
test/js/node/test/parallel/test-http-write-head.js
Normal 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();
|
||||
}));
|
||||
});
|
||||
}));
|
||||
}
|
||||
@@ -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
|
||||
);
|
||||
});
|
||||
});
|
||||
54
test/js/node/test/parallel/test-https-byteswritten.js
Normal file
54
test/js/node/test/parallel/test-https-byteswritten.js
Normal 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
|
||||
});
|
||||
});
|
||||
54
test/js/node/test/parallel/test-https-close.js
Normal file
54
test/js/node/test/parallel/test-https-close.js
Normal 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();
|
||||
});
|
||||
@@ -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);
|
||||
}));
|
||||
}));
|
||||
@@ -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);
|
||||
}));
|
||||
}));
|
||||
Reference in New Issue
Block a user