mirror of
https://github.com/oven-sh/bun
synced 2026-02-03 15:38:46 +00:00
Compare commits
8 Commits
fix-covera
...
ciro/node-
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
faf1bc56a1 | ||
|
|
0abf0a9281 | ||
|
|
9a00247019 | ||
|
|
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,22 @@ public:
|
||||
httpContext->getSocketContextData()->onSocketClosed = onClose;
|
||||
}
|
||||
|
||||
void setOnClientError(HttpContextData<SSL>::OnClientErrorCallback onClientError) {
|
||||
httpContext->getSocketContextData()->onClientError = std::move(onClientError);
|
||||
}
|
||||
|
||||
TemplatedApp &&run() {
|
||||
uWS::run();
|
||||
return std::move(*this);
|
||||
}
|
||||
|
||||
TemplatedApp &&setUsingCustomExpectHandler(bool value) {
|
||||
httpContext->getSocketContextData()->usingCustomExpectHandler = value;
|
||||
httpContext->getSocketContextData()->flags.usingCustomExpectHandler = value;
|
||||
return std::move(*this);
|
||||
}
|
||||
|
||||
TemplatedApp &&setRequireHostHeader(bool value) {
|
||||
httpContext->getSocketContextData()->requireHostHeader = value;
|
||||
httpContext->getSocketContextData()->flags.requireHostHeader = value;
|
||||
return std::move(*this);
|
||||
}
|
||||
|
||||
|
||||
@@ -31,7 +31,7 @@
|
||||
#include <string_view>
|
||||
#include <iostream>
|
||||
#include "MoveOnlyFunction.h"
|
||||
|
||||
#include "HttpParser.h"
|
||||
namespace uWS {
|
||||
template<bool> struct HttpResponse;
|
||||
|
||||
@@ -73,8 +73,8 @@ private:
|
||||
// if we are closing or already closed, we don't need to do anything
|
||||
if (!us_socket_is_closed(SSL, s) && !us_socket_is_shut_down(SSL, s)) {
|
||||
HttpContextData<SSL> *httpContextData = getSocketContextDataS(s);
|
||||
|
||||
if(httpContextData->rejectUnauthorized) {
|
||||
httpContextData->flags.isSecure = success;
|
||||
if(httpContextData->flags.rejectUnauthorized) {
|
||||
if(!success || verify_error.error != 0) {
|
||||
// we failed to handshake, close the socket
|
||||
us_socket_close(SSL, s, 0, nullptr);
|
||||
@@ -118,8 +118,15 @@ private:
|
||||
/* Get socket ext */
|
||||
auto *httpResponseData = reinterpret_cast<HttpResponseData<SSL> *>(us_socket_ext(SSL, s));
|
||||
|
||||
|
||||
|
||||
/* Call filter */
|
||||
HttpContextData<SSL> *httpContextData = getSocketContextDataS(s);
|
||||
if(httpContextData->flags.isParsingHttp) {
|
||||
if(httpContextData->onClientError) {
|
||||
httpContextData->onClientError(SSL, s,uWS::HTTP_PARSER_ERROR_INVALID_EOF, nullptr, 0);
|
||||
}
|
||||
}
|
||||
for (auto &f : httpContextData->filterHandlers) {
|
||||
f((HttpResponse<SSL> *) s, -1);
|
||||
}
|
||||
@@ -163,7 +170,7 @@ private:
|
||||
((AsyncSocket<SSL> *) s)->cork();
|
||||
|
||||
/* Mark that we are inside the parser now */
|
||||
httpContextData->isParsingHttp = true;
|
||||
httpContextData->flags.isParsingHttp = true;
|
||||
|
||||
// clients need to know the cursor after http parse, not servers!
|
||||
// how far did we read then? we need to know to continue with websocket parsing data? or?
|
||||
@@ -174,7 +181,7 @@ private:
|
||||
#endif
|
||||
|
||||
/* The return value is entirely up to us to interpret. The HttpParser cares only for whether the returned value is DIFFERENT from passed user */
|
||||
auto [err, returnedSocket] = httpResponseData->consumePostPadded(httpContextData->requireHostHeader,data, (unsigned int) length, s, proxyParser, [httpContextData](void *s, HttpRequest *httpRequest) -> void * {
|
||||
auto [err, parserError, returnedSocket] = httpResponseData->consumePostPadded(httpContextData->flags.requireHostHeader,data, (unsigned int) length, s, proxyParser, [httpContextData](void *s, HttpRequest *httpRequest) -> void * {
|
||||
/* For every request we reset the timeout and hang until user makes action */
|
||||
/* Warning: if we are in shutdown state, resetting the timer is a security issue! */
|
||||
us_socket_timeout(SSL, (us_socket_t *) s, 0);
|
||||
@@ -201,6 +208,7 @@ private:
|
||||
|
||||
httpResponseData->fromAncientRequest = httpRequest->isAncient();
|
||||
|
||||
|
||||
/* Select the router based on SNI (only possible for SSL) */
|
||||
auto *selectedRouter = &httpContextData->router;
|
||||
if constexpr (SSL) {
|
||||
@@ -290,10 +298,12 @@ private:
|
||||
});
|
||||
|
||||
/* Mark that we are no longer parsing Http */
|
||||
httpContextData->isParsingHttp = false;
|
||||
|
||||
httpContextData->flags.isParsingHttp = false;
|
||||
/* If we got fullptr that means the parser wants us to close the socket from error (same as calling the errorHandler) */
|
||||
if (returnedSocket == FULLPTR) {
|
||||
if(httpContextData->onClientError) {
|
||||
httpContextData->onClientError(SSL, s, parserError, data, length);
|
||||
}
|
||||
/* For errors, we only deliver them "at most once". We don't care if they get halfways delivered or not. */
|
||||
us_socket_write(SSL, s, httpErrorResponses[err].data(), (int) httpErrorResponses[err].length(), false);
|
||||
us_socket_shutdown(SSL, s);
|
||||
@@ -467,7 +477,7 @@ public:
|
||||
/* Init socket context data */
|
||||
auto* httpContextData = new ((HttpContextData<SSL> *) us_socket_context_ext(SSL, (us_socket_context_t *) httpContext)) HttpContextData<SSL>();
|
||||
if(options.request_cert && options.reject_unauthorized) {
|
||||
httpContextData->rejectUnauthorized = true;
|
||||
httpContextData->flags.rejectUnauthorized = true;
|
||||
}
|
||||
return httpContext->init();
|
||||
}
|
||||
@@ -515,15 +525,15 @@ public:
|
||||
}
|
||||
}
|
||||
|
||||
const bool &customContinue = httpContextData->usingCustomExpectHandler;
|
||||
|
||||
|
||||
httpContextData->currentRouter->add(methods, pattern, [handler = std::move(handler), parameterOffsets = std::move(parameterOffsets), &customContinue](auto *r) mutable {
|
||||
httpContextData->currentRouter->add(methods, pattern, [handler = std::move(handler), parameterOffsets = std::move(parameterOffsets), httpContextData](auto *r) mutable {
|
||||
auto user = r->getUserData();
|
||||
user.httpRequest->setYield(false);
|
||||
user.httpRequest->setParameters(r->getParameters());
|
||||
user.httpRequest->setParameterOffsets(¶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,19 @@
|
||||
|
||||
#include <vector>
|
||||
#include "MoveOnlyFunction.h"
|
||||
|
||||
#include "HttpParser.h"
|
||||
namespace uWS {
|
||||
template<bool> struct HttpResponse;
|
||||
struct HttpRequest;
|
||||
|
||||
struct HttpFlags {
|
||||
bool isParsingHttp: 1 = false;
|
||||
bool rejectUnauthorized: 1 = false;
|
||||
bool usingCustomExpectHandler: 1 = false;
|
||||
bool requireHostHeader: 1 = true;
|
||||
bool isSecure: 1 = false;
|
||||
};
|
||||
|
||||
template <bool SSL>
|
||||
struct alignas(16) HttpContextData {
|
||||
template <bool> friend struct HttpContext;
|
||||
@@ -35,6 +43,7 @@ struct alignas(16) HttpContextData {
|
||||
private:
|
||||
std::vector<MoveOnlyFunction<void(HttpResponse<SSL> *, int)>> filterHandlers;
|
||||
using OnSocketClosedCallback = void (*)(void* userData, int is_ssl, struct us_socket_t *rawSocket);
|
||||
using OnClientErrorCallback = MoveOnlyFunction<void(int is_ssl, struct us_socket_t *rawSocket, uWS::HttpParserError errorCode, char *rawPacket, int rawPacketLength)>;
|
||||
|
||||
MoveOnlyFunction<void(const char *hostname)> missingServerNameHandler;
|
||||
|
||||
@@ -49,13 +58,11 @@ private:
|
||||
/* This is the default router for default SNI or non-SSL */
|
||||
HttpRouter<RouterData> router;
|
||||
void *upgradedWebSocket = nullptr;
|
||||
bool isParsingHttp = false;
|
||||
bool rejectUnauthorized = false;
|
||||
bool usingCustomExpectHandler = false;
|
||||
bool requireHostHeader = true;
|
||||
|
||||
/* Used to simulate Node.js socket events. */
|
||||
OnSocketClosedCallback onSocketClosed = nullptr;
|
||||
OnClientErrorCallback onClientError = nullptr;
|
||||
|
||||
HttpFlags flags;
|
||||
|
||||
// TODO: SNI
|
||||
void clearRoutes() {
|
||||
@@ -63,6 +70,11 @@ private:
|
||||
this->currentRouter = &router;
|
||||
filterHandlers.clear();
|
||||
}
|
||||
|
||||
public:
|
||||
bool isSecure() const {
|
||||
return flags.isSecure;
|
||||
}
|
||||
};
|
||||
|
||||
}
|
||||
|
||||
@@ -48,6 +48,19 @@ namespace uWS
|
||||
static const unsigned int MINIMUM_HTTP_POST_PADDING = 32;
|
||||
static void *FULLPTR = (void *)~(uintptr_t)0;
|
||||
|
||||
enum HttpParserError: uint8_t {
|
||||
HTTP_PARSER_ERROR_NONE = 0,
|
||||
HTTP_PARSER_ERROR_INVALID_CHUNKED_ENCODING = 1,
|
||||
HTTP_PARSER_ERROR_INVALID_CONTENT_LENGTH = 2,
|
||||
HTTP_PARSER_ERROR_INVALID_TRANSFER_ENCODING = 3,
|
||||
HTTP_PARSER_ERROR_MISSING_HOST_HEADER = 4,
|
||||
HTTP_PARSER_ERROR_INVALID_REQUEST = 5,
|
||||
HTTP_PARSER_ERROR_REQUEST_HEADER_FIELDS_TOO_LARGE = 6,
|
||||
HTTP_PARSER_ERROR_INVALID_HTTP_VERSION = 7,
|
||||
HTTP_PARSER_ERROR_INVALID_EOF = 8,
|
||||
HTTP_PARSER_ERROR_INVALID_METHOD = 9,
|
||||
};
|
||||
|
||||
struct HttpRequest
|
||||
{
|
||||
|
||||
@@ -59,8 +72,8 @@ namespace uWS
|
||||
std::string_view key, value;
|
||||
} headers[UWS_HTTP_MAX_HEADERS_COUNT];
|
||||
bool ancientHttp;
|
||||
unsigned int querySeparator;
|
||||
bool didYield;
|
||||
unsigned int querySeparator;
|
||||
BloomFilter bf;
|
||||
std::pair<int, std::string_view *> currentParameters;
|
||||
std::map<std::string, unsigned short, std::less<>> *currentParameterOffsets = nullptr;
|
||||
@@ -134,6 +147,7 @@ namespace uWS
|
||||
return std::string_view(nullptr, 0);
|
||||
}
|
||||
|
||||
|
||||
std::string_view getUrl()
|
||||
{
|
||||
return std::string_view(headers->value.data(), querySeparator);
|
||||
@@ -312,6 +326,18 @@ namespace uWS
|
||||
return (void *)p;
|
||||
}
|
||||
|
||||
static bool isAlpha(std::string_view str) {
|
||||
if (str.empty()) return false;
|
||||
|
||||
for (char c : str) {
|
||||
if (!isAlphaChar(c))
|
||||
return false;
|
||||
}
|
||||
return true;
|
||||
}
|
||||
static inline bool isAlphaChar(char c) {
|
||||
return ((c >= 'A' && c <= 'Z') || (c >= 'a' && c <= 'z'));
|
||||
}
|
||||
static inline int isHTTPorHTTPSPrefixForProxies(char *data, char *end) {
|
||||
// We can check 8 because:
|
||||
// 1. If it's "http://" that's 7 bytes, and it's supposed to at least have a trailing slash.
|
||||
@@ -357,7 +383,13 @@ namespace uWS
|
||||
/* Scan until single SP, assume next is / (origin request) */
|
||||
char *start = data;
|
||||
/* This catches the post padded CR and fails */
|
||||
while (data[0] > 32) data++;
|
||||
while (data[0] > 32) {
|
||||
if (!isAlphaChar(data[0])) {
|
||||
return (char *) 0x3;
|
||||
}
|
||||
data++;
|
||||
|
||||
}
|
||||
if (&data[1] == end) [[unlikely]] {
|
||||
return nullptr;
|
||||
}
|
||||
@@ -365,6 +397,9 @@ namespace uWS
|
||||
if (data[0] == 32 && (__builtin_expect(data[1] == '/', 1) || isHTTPorHTTPSPrefixForProxies(data + 1, end) == 1)) [[likely]] {
|
||||
header.key = {start, (size_t) (data - start)};
|
||||
data++;
|
||||
if(!isAlpha(header.key)) {
|
||||
return (char *) 0x3;
|
||||
}
|
||||
/* Scan for less than 33 (catches post padded CR and fails) */
|
||||
start = data;
|
||||
for (; true; data += 8) {
|
||||
@@ -436,7 +471,7 @@ namespace uWS
|
||||
}
|
||||
|
||||
/* End is only used for the proxy parser. The HTTP parser recognizes "\ra" as invalid "\r\n" scan and breaks. */
|
||||
static unsigned int getHeaders(char *postPaddedBuffer, char *end, struct HttpRequest::Header *headers, void *reserved, unsigned int &err, bool &isAncientHTTP) {
|
||||
static unsigned int getHeaders(char *postPaddedBuffer, char *end, struct HttpRequest::Header *headers, void *reserved, unsigned int &err, HttpParserError &parserError, bool &isAncientHTTP) {
|
||||
char *preliminaryKey, *preliminaryValue, *start = postPaddedBuffer;
|
||||
|
||||
#ifdef UWS_WITH_PROXY
|
||||
@@ -466,15 +501,21 @@ namespace uWS
|
||||
* which is then removed, and our counters to flip due to overflow and we end up with a crash */
|
||||
|
||||
/* The request line is different from the field names / field values */
|
||||
if ((char *) 3 > (postPaddedBuffer = consumeRequestLine(postPaddedBuffer, end, headers[0], isAncientHTTP))) {
|
||||
if ((char *) 4 > (postPaddedBuffer = consumeRequestLine(postPaddedBuffer, end, headers[0], isAncientHTTP))) {
|
||||
/* Error - invalid request line */
|
||||
/* Assuming it is 505 HTTP Version Not Supported */
|
||||
switch (reinterpret_cast<uintptr_t>(postPaddedBuffer)) {
|
||||
case 0x1:
|
||||
err = HTTP_ERROR_505_HTTP_VERSION_NOT_SUPPORTED;;
|
||||
err = HTTP_ERROR_505_HTTP_VERSION_NOT_SUPPORTED;
|
||||
parserError = HTTP_PARSER_ERROR_INVALID_HTTP_VERSION;
|
||||
break;
|
||||
case 0x2:
|
||||
err = HTTP_ERROR_400_BAD_REQUEST;
|
||||
parserError = HTTP_PARSER_ERROR_INVALID_REQUEST;
|
||||
break;
|
||||
case 0x3:
|
||||
err = HTTP_ERROR_400_BAD_REQUEST;
|
||||
parserError = HTTP_PARSER_ERROR_INVALID_METHOD;
|
||||
break;
|
||||
default: {
|
||||
err = 0;
|
||||
@@ -488,6 +529,7 @@ namespace uWS
|
||||
if(buffer_size < 2) {
|
||||
/* Fragmented request */
|
||||
err = HTTP_ERROR_400_BAD_REQUEST;
|
||||
parserError = HTTP_PARSER_ERROR_INVALID_REQUEST;
|
||||
return 0;
|
||||
}
|
||||
if(buffer_size >= 2 && postPaddedBuffer[0] == '\r' && postPaddedBuffer[1] == '\n') {
|
||||
@@ -510,6 +552,7 @@ namespace uWS
|
||||
}
|
||||
/* Error: invalid chars in field name */
|
||||
err = HTTP_ERROR_400_BAD_REQUEST;
|
||||
parserError = HTTP_PARSER_ERROR_INVALID_REQUEST;
|
||||
return 0;
|
||||
}
|
||||
postPaddedBuffer++;
|
||||
@@ -527,6 +570,7 @@ namespace uWS
|
||||
}
|
||||
/* Error - invalid chars in field value */
|
||||
err = HTTP_ERROR_400_BAD_REQUEST;
|
||||
parserError = HTTP_PARSER_ERROR_INVALID_REQUEST;
|
||||
return 0;
|
||||
}
|
||||
break;
|
||||
@@ -560,6 +604,7 @@ namespace uWS
|
||||
/* \r\n\r plus non-\n letter is malformed request, or simply out of search space */
|
||||
if (postPaddedBuffer + 1 < end) {
|
||||
err = HTTP_ERROR_400_BAD_REQUEST;
|
||||
parserError = HTTP_PARSER_ERROR_INVALID_REQUEST;
|
||||
}
|
||||
return 0;
|
||||
}
|
||||
@@ -579,25 +624,26 @@ namespace uWS
|
||||
* or [consumed, nullptr] for "break; I am closed or upgraded to websocket"
|
||||
* or [whatever, fullptr] for "break and close me, I am a parser error!" */
|
||||
template <bool ConsumeMinimally>
|
||||
std::pair<unsigned int, void *> fenceAndConsumePostPadded(bool requireHostHeader, char *data, unsigned int length, void *user, void *reserved, HttpRequest *req, MoveOnlyFunction<void *(void *, HttpRequest *)> &requestHandler, MoveOnlyFunction<void *(void *, std::string_view, bool)> &dataHandler) {
|
||||
std::tuple<unsigned int, HttpParserError, void *> fenceAndConsumePostPadded(bool requireHostHeader, char *data, unsigned int length, void *user, void *reserved, HttpRequest *req, MoveOnlyFunction<void *(void *, HttpRequest *)> &requestHandler, MoveOnlyFunction<void *(void *, std::string_view, bool)> &dataHandler) {
|
||||
|
||||
/* How much data we CONSUMED (to throw away) */
|
||||
unsigned int consumedTotal = 0;
|
||||
unsigned int err = 0;
|
||||
HttpParserError parserError = HTTP_PARSER_ERROR_NONE;
|
||||
|
||||
/* Fence two bytes past end of our buffer (buffer has post padded margins).
|
||||
* This is to always catch scan for \r but not for \r\n. */
|
||||
data[length] = '\r';
|
||||
data[length + 1] = 'a'; /* Anything that is not \n, to trigger "invalid request" */
|
||||
bool isAncientHTTP = false;
|
||||
for (unsigned int consumed; length && (consumed = getHeaders(data, data + length, req->headers, reserved, err, isAncientHTTP)); ) {
|
||||
for (unsigned int consumed; length && (consumed = getHeaders(data, data + length, req->headers, reserved, err, parserError, isAncientHTTP)); ) {
|
||||
data += consumed;
|
||||
length -= consumed;
|
||||
consumedTotal += consumed;
|
||||
|
||||
/* Even if we could parse it, check for length here as well */
|
||||
if (consumed > MAX_FALLBACK_SIZE) {
|
||||
return {HTTP_ERROR_431_REQUEST_HEADER_FIELDS_TOO_LARGE, FULLPTR};
|
||||
return {HTTP_ERROR_431_REQUEST_HEADER_FIELDS_TOO_LARGE, HTTP_PARSER_ERROR_REQUEST_HEADER_FIELDS_TOO_LARGE, FULLPTR};
|
||||
}
|
||||
|
||||
/* Store HTTP version (ancient 1.0 or 1.1) */
|
||||
@@ -611,7 +657,7 @@ namespace uWS
|
||||
}
|
||||
/* Break if no host header (but we can have empty string which is different from nullptr) */
|
||||
if (!isAncientHTTP && requireHostHeader && !req->getHeader("host").data()) {
|
||||
return {HTTP_ERROR_400_BAD_REQUEST, FULLPTR};
|
||||
return {HTTP_ERROR_400_BAD_REQUEST, HTTP_PARSER_ERROR_MISSING_HOST_HEADER, FULLPTR};
|
||||
}
|
||||
|
||||
/* RFC 9112 6.3
|
||||
@@ -628,7 +674,7 @@ namespace uWS
|
||||
/* Returning fullptr is the same as calling the errorHandler */
|
||||
/* We could be smart and set an error in the context along with this, to indicate what
|
||||
* http error response we might want to return */
|
||||
return {HTTP_ERROR_400_BAD_REQUEST, FULLPTR};
|
||||
return {HTTP_ERROR_400_BAD_REQUEST, HTTP_PARSER_ERROR_INVALID_TRANSFER_ENCODING, FULLPTR};
|
||||
}
|
||||
|
||||
/* Parse query */
|
||||
@@ -640,25 +686,17 @@ namespace uWS
|
||||
remainingStreamingBytes = toUnsignedInteger(contentLengthString);
|
||||
if (remainingStreamingBytes == UINT64_MAX) {
|
||||
/* Parser error */
|
||||
return {HTTP_ERROR_400_BAD_REQUEST, FULLPTR};
|
||||
return {HTTP_ERROR_400_BAD_REQUEST, HTTP_PARSER_ERROR_INVALID_CONTENT_LENGTH, FULLPTR};
|
||||
}
|
||||
}
|
||||
|
||||
// lets check if content len is valid before calling requestHandler
|
||||
if(contentLengthStringLen) {
|
||||
remainingStreamingBytes = toUnsignedInteger(contentLengthString);
|
||||
if (remainingStreamingBytes == UINT64_MAX) {
|
||||
/* Parser error */
|
||||
return {HTTP_ERROR_400_BAD_REQUEST, FULLPTR};
|
||||
}
|
||||
}
|
||||
/* If returned socket is not what we put in we need
|
||||
* to break here as we either have upgraded to
|
||||
* WebSockets or otherwise closed the socket. */
|
||||
void *returnedUser = requestHandler(user, req);
|
||||
if (returnedUser != user) {
|
||||
/* We are upgraded to WebSocket or otherwise broken */
|
||||
return {consumedTotal, returnedUser};
|
||||
return {consumedTotal, HTTP_PARSER_ERROR_NONE, returnedUser};
|
||||
}
|
||||
|
||||
/* The rules at play here according to RFC 9112 for requests are essentially:
|
||||
@@ -694,7 +732,7 @@ namespace uWS
|
||||
}
|
||||
if (isParsingInvalidChunkedEncoding(remainingStreamingBytes)) {
|
||||
// TODO: what happen if we already responded?
|
||||
return {HTTP_ERROR_400_BAD_REQUEST, FULLPTR};
|
||||
return {HTTP_ERROR_400_BAD_REQUEST, HTTP_PARSER_ERROR_INVALID_CHUNKED_ENCODING, FULLPTR};
|
||||
}
|
||||
unsigned int consumed = (length - (unsigned int) dataToConsume.length());
|
||||
data = (char *) dataToConsume.data();
|
||||
@@ -723,13 +761,13 @@ namespace uWS
|
||||
}
|
||||
/* Whenever we return FULLPTR, the interpretation of "consumed" should be the HttpError enum. */
|
||||
if (err) {
|
||||
return {err, FULLPTR};
|
||||
return {err, parserError, FULLPTR};
|
||||
}
|
||||
return {consumedTotal, user};
|
||||
return {consumedTotal, HTTP_PARSER_ERROR_NONE, user};
|
||||
}
|
||||
|
||||
public:
|
||||
std::pair<unsigned int, void *> consumePostPadded(bool requireHostHeader, char *data, unsigned int length, void *user, void *reserved, MoveOnlyFunction<void *(void *, HttpRequest *)> &&requestHandler, MoveOnlyFunction<void *(void *, std::string_view, bool)> &&dataHandler) {
|
||||
std::tuple<unsigned int, HttpParserError, void *> consumePostPadded(bool requireHostHeader, char *data, unsigned int length, void *user, void *reserved, MoveOnlyFunction<void *(void *, HttpRequest *)> &&requestHandler, MoveOnlyFunction<void *(void *, std::string_view, bool)> &&dataHandler) {
|
||||
|
||||
/* This resets BloomFilter by construction, but later we also reset it again.
|
||||
* Optimize this to skip resetting twice (req could be made global) */
|
||||
@@ -743,7 +781,7 @@ public:
|
||||
dataHandler(user, chunk, chunk.length() == 0);
|
||||
}
|
||||
if (isParsingInvalidChunkedEncoding(remainingStreamingBytes)) {
|
||||
return {HTTP_ERROR_400_BAD_REQUEST, FULLPTR};
|
||||
return {HTTP_ERROR_400_BAD_REQUEST, HTTP_PARSER_ERROR_INVALID_CHUNKED_ENCODING, FULLPTR};
|
||||
}
|
||||
data = (char *) dataToConsume.data();
|
||||
length = (unsigned int) dataToConsume.length();
|
||||
@@ -753,7 +791,7 @@ public:
|
||||
if (remainingStreamingBytes >= length) {
|
||||
void *returnedUser = dataHandler(user, std::string_view(data, length), remainingStreamingBytes == length);
|
||||
remainingStreamingBytes -= length;
|
||||
return {0, returnedUser};
|
||||
return {0, HTTP_PARSER_ERROR_NONE, returnedUser};
|
||||
} else {
|
||||
void *returnedUser = dataHandler(user, std::string_view(data, remainingStreamingBytes), true);
|
||||
|
||||
@@ -763,7 +801,7 @@ public:
|
||||
remainingStreamingBytes = 0;
|
||||
|
||||
if (returnedUser != user) {
|
||||
return {0, returnedUser};
|
||||
return {0, HTTP_PARSER_ERROR_NONE, returnedUser};
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -778,19 +816,19 @@ public:
|
||||
fallback.append(data, maxCopyDistance);
|
||||
|
||||
// break here on break
|
||||
std::pair<unsigned int, void *> consumed = fenceAndConsumePostPadded<true>(requireHostHeader,fallback.data(), (unsigned int) fallback.length(), user, reserved, &req, requestHandler, dataHandler);
|
||||
if (consumed.second != user) {
|
||||
std::tuple<unsigned int, HttpParserError, void *> consumed = fenceAndConsumePostPadded<true>(requireHostHeader,fallback.data(), (unsigned int) fallback.length(), user, reserved, &req, requestHandler, dataHandler);
|
||||
if (std::get<2>(consumed) != user) {
|
||||
return consumed;
|
||||
}
|
||||
|
||||
if (consumed.first) {
|
||||
if (std::get<0>(consumed)) {
|
||||
|
||||
/* This logic assumes that we consumed everything in fallback buffer.
|
||||
* This is critically important, as we will get an integer overflow in case
|
||||
* of "had" being larger than what we consumed, and that we would drop data */
|
||||
fallback.clear();
|
||||
data += consumed.first - had;
|
||||
length -= consumed.first - had;
|
||||
data += std::get<0>(consumed) - had;
|
||||
length -= std::get<0>(consumed) - had;
|
||||
|
||||
if (remainingStreamingBytes) {
|
||||
/* It's either chunked or with a content-length */
|
||||
@@ -800,7 +838,7 @@ public:
|
||||
dataHandler(user, chunk, chunk.length() == 0);
|
||||
}
|
||||
if (isParsingInvalidChunkedEncoding(remainingStreamingBytes)) {
|
||||
return {HTTP_ERROR_400_BAD_REQUEST, FULLPTR};
|
||||
return {HTTP_ERROR_400_BAD_REQUEST, HTTP_PARSER_ERROR_INVALID_CHUNKED_ENCODING, FULLPTR};
|
||||
}
|
||||
data = (char *) dataToConsume.data();
|
||||
length = (unsigned int) dataToConsume.length();
|
||||
@@ -809,7 +847,7 @@ public:
|
||||
if (remainingStreamingBytes >= (unsigned int) length) {
|
||||
void *returnedUser = dataHandler(user, std::string_view(data, length), remainingStreamingBytes == (unsigned int) length);
|
||||
remainingStreamingBytes -= length;
|
||||
return {0, returnedUser};
|
||||
return {0, HTTP_PARSER_ERROR_NONE, returnedUser};
|
||||
} else {
|
||||
void *returnedUser = dataHandler(user, std::string_view(data, remainingStreamingBytes), true);
|
||||
|
||||
@@ -819,7 +857,7 @@ public:
|
||||
remainingStreamingBytes = 0;
|
||||
|
||||
if (returnedUser != user) {
|
||||
return {0, returnedUser};
|
||||
return {0, HTTP_PARSER_ERROR_NONE, returnedUser};
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -827,30 +865,30 @@ public:
|
||||
|
||||
} else {
|
||||
if (fallback.length() == MAX_FALLBACK_SIZE) {
|
||||
return {HTTP_ERROR_431_REQUEST_HEADER_FIELDS_TOO_LARGE, FULLPTR};
|
||||
return {HTTP_ERROR_431_REQUEST_HEADER_FIELDS_TOO_LARGE, HTTP_PARSER_ERROR_REQUEST_HEADER_FIELDS_TOO_LARGE, FULLPTR};
|
||||
}
|
||||
return {0, user};
|
||||
return {0, HTTP_PARSER_ERROR_NONE, user};
|
||||
}
|
||||
}
|
||||
|
||||
std::pair<unsigned int, void *> consumed = fenceAndConsumePostPadded<false>(requireHostHeader,data, length, user, reserved, &req, requestHandler, dataHandler);
|
||||
if (consumed.second != user) {
|
||||
std::tuple<unsigned int, HttpParserError, void *> consumed = fenceAndConsumePostPadded<false>(requireHostHeader,data, length, user, reserved, &req, requestHandler, dataHandler);
|
||||
if (std::get<2>(consumed) != user) {
|
||||
return consumed;
|
||||
}
|
||||
|
||||
data += consumed.first;
|
||||
length -= consumed.first;
|
||||
data += std::get<0>(consumed);
|
||||
length -= std::get<0>(consumed);
|
||||
|
||||
if (length) {
|
||||
if (length < MAX_FALLBACK_SIZE) {
|
||||
fallback.append(data, length);
|
||||
} else {
|
||||
return {HTTP_ERROR_431_REQUEST_HEADER_FIELDS_TOO_LARGE, FULLPTR};
|
||||
return {HTTP_ERROR_431_REQUEST_HEADER_FIELDS_TOO_LARGE, HTTP_PARSER_ERROR_REQUEST_HEADER_FIELDS_TOO_LARGE, FULLPTR};
|
||||
}
|
||||
}
|
||||
|
||||
// added for now
|
||||
return {0, user};
|
||||
return {0, HTTP_PARSER_ERROR_NONE, user};
|
||||
}
|
||||
};
|
||||
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
@@ -5175,11 +5175,10 @@ pub fn NewServer(protocol_enum: enum { http, https }, development_kind: enum { d
|
||||
pub const RequestContext = NewRequestContext(ssl_enabled, debug_mode, @This());
|
||||
|
||||
pub const App = uws.NewApp(ssl_enabled);
|
||||
|
||||
app: ?*App = null,
|
||||
listener: ?*App.ListenSocket = null,
|
||||
js_value: JSC.Strong = .empty,
|
||||
/// Potentially null before listen() is called, and once .destroy() is called.
|
||||
app: ?*App = null,
|
||||
vm: *JSC.VirtualMachine,
|
||||
globalThis: *JSGlobalObject,
|
||||
base_url_string_for_joining: string = "",
|
||||
@@ -5210,6 +5209,8 @@ pub fn NewServer(protocol_enum: enum { http, https }, development_kind: enum { d
|
||||
/// So we have to store it.
|
||||
user_routes: std.ArrayListUnmanaged(UserRoute) = .{},
|
||||
|
||||
on_clienterror: JSC.Strong = .empty,
|
||||
|
||||
pub const doStop = host_fn.wrapInstanceMethod(ThisServer, "stopFromJS", false);
|
||||
pub const dispose = host_fn.wrapInstanceMethod(ThisServer, "disposeFromJS", false);
|
||||
pub const doUpgrade = host_fn.wrapInstanceMethod(ThisServer, "onUpgrade", false);
|
||||
@@ -6205,6 +6206,8 @@ pub fn NewServer(protocol_enum: enum { http, https }, development_kind: enum { d
|
||||
this.user_routes.deinit(bun.default_allocator);
|
||||
|
||||
this.config.deinit();
|
||||
|
||||
this.on_clienterror.deinit();
|
||||
if (this.app) |app| {
|
||||
this.app = null;
|
||||
app.destroy();
|
||||
@@ -7338,6 +7341,21 @@ pub fn NewServer(protocol_enum: enum { http, https }, development_kind: enum { d
|
||||
|
||||
return route_list_value;
|
||||
}
|
||||
|
||||
pub fn onClientErrorCallback(this: *ThisServer, socket: *uws.Socket, error_code: u8, raw_packet: []const u8) void {
|
||||
if (this.on_clienterror.get()) |callback| {
|
||||
const is_ssl = protocol_enum == .https;
|
||||
const node_socket = Bun__createNodeHTTPServerSocket(is_ssl, socket, this.globalThis);
|
||||
if (node_socket.isEmptyOrUndefinedOrNull()) {
|
||||
return;
|
||||
}
|
||||
|
||||
const error_code_value = JSValue.jsNumber(error_code);
|
||||
const raw_packet_value = JSC.ArrayBuffer.createBuffer(this.globalThis, raw_packet);
|
||||
_ = callback.call(this.globalThis, .undefined, &.{ JSValue.jsBoolean(is_ssl), node_socket, error_code_value, raw_packet_value }) catch |err|
|
||||
this.globalThis.takeException(err);
|
||||
}
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
@@ -7638,6 +7656,49 @@ pub fn Server__setIdleTimeout_(server: JSC.JSValue, seconds: JSC.JSValue, global
|
||||
return globalThis.throw("Failed to set timeout: The 'this' value is not a Server.", .{});
|
||||
}
|
||||
}
|
||||
pub export fn Server__setOnClientError(server: JSC.JSValue, callback: JSC.JSValue, globalThis: *JSC.JSGlobalObject) void {
|
||||
Server__setOnClientError_(server, callback, globalThis) catch |err| switch (err) {
|
||||
error.JSError => {},
|
||||
error.OutOfMemory => {
|
||||
_ = globalThis.throwOutOfMemoryValue();
|
||||
},
|
||||
};
|
||||
}
|
||||
pub fn Server__setOnClientError_(server: JSC.JSValue, callback: JSC.JSValue, globalThis: *JSC.JSGlobalObject) bun.JSError!void {
|
||||
if (!server.isObject()) {
|
||||
return globalThis.throw("Failed to set clientError: The 'this' value is not a Server.", .{});
|
||||
}
|
||||
|
||||
if (!callback.isFunction()) {
|
||||
return globalThis.throw("Failed to set clientError: The provided value is not a function.", .{});
|
||||
}
|
||||
|
||||
if (server.as(HTTPServer)) |this| {
|
||||
if (this.app) |app| {
|
||||
this.on_clienterror.clearWithoutDeallocation();
|
||||
this.on_clienterror = JSC.Strong.create(callback, globalThis);
|
||||
app.onClientError(*HTTPServer, this, HTTPServer.onClientErrorCallback);
|
||||
}
|
||||
} else if (server.as(HTTPSServer)) |this| {
|
||||
if (this.app) |app| {
|
||||
this.on_clienterror.clearWithoutDeallocation();
|
||||
this.on_clienterror = JSC.Strong.create(callback, globalThis);
|
||||
app.onClientError(*HTTPSServer, this, HTTPSServer.onClientErrorCallback);
|
||||
}
|
||||
} else if (server.as(DebugHTTPServer)) |this| {
|
||||
if (this.app) |app| {
|
||||
this.on_clienterror.clearWithoutDeallocation();
|
||||
this.on_clienterror = JSC.Strong.create(callback, globalThis);
|
||||
app.onClientError(*DebugHTTPServer, this, DebugHTTPServer.onClientErrorCallback);
|
||||
}
|
||||
} else if (server.as(DebugHTTPSServer)) |this| {
|
||||
if (this.app) |app| {
|
||||
this.on_clienterror.clearWithoutDeallocation();
|
||||
this.on_clienterror = JSC.Strong.create(callback, globalThis);
|
||||
app.onClientError(*DebugHTTPSServer, this, DebugHTTPSServer.onClientErrorCallback);
|
||||
}
|
||||
}
|
||||
}
|
||||
pub export fn Server__setRequireHostHeader(server: JSC.JSValue, require_host_header: bool, globalThis: *JSC.JSGlobalObject) void {
|
||||
Server__setRequireHostHeader_(server, require_host_header, globalThis) catch |err| switch (err) {
|
||||
error.JSError => {},
|
||||
@@ -7646,6 +7707,7 @@ pub export fn Server__setRequireHostHeader(server: JSC.JSValue, require_host_hea
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
pub fn Server__setRequireHostHeader_(server: JSC.JSValue, require_host_header: bool, globalThis: *JSC.JSGlobalObject) bun.JSError!void {
|
||||
if (!server.isObject()) {
|
||||
return globalThis.throw("Failed to set requireHostHeader: The 'this' value is not a Server.", .{});
|
||||
@@ -7668,6 +7730,7 @@ comptime {
|
||||
_ = Server__setIdleTimeout;
|
||||
_ = Server__setRequireHostHeader;
|
||||
_ = NodeHTTPResponse.create;
|
||||
_ = Server__setOnClientError;
|
||||
}
|
||||
|
||||
extern fn NodeHTTPServer__onRequest_http(
|
||||
@@ -7694,6 +7757,8 @@ extern fn NodeHTTPServer__onRequest_https(
|
||||
node_response_ptr: *?*NodeHTTPResponse,
|
||||
) JSC.JSValue;
|
||||
|
||||
extern fn Bun__createNodeHTTPServerSocket(bool, *anyopaque, *JSC.JSGlobalObject) JSC.JSValue;
|
||||
|
||||
extern fn NodeHTTP_assignOnCloseFunction(bool, *anyopaque) void;
|
||||
|
||||
extern fn NodeHTTP_setUsingCustomExpectHandler(bool, *anyopaque, bool) void;
|
||||
|
||||
@@ -297,6 +297,7 @@ pub fn create(
|
||||
if (method.hasRequestBody() or method == HTTP.Method.GET) {
|
||||
const req_len: usize = brk: {
|
||||
if (request.header("content-length")) |content_length| {
|
||||
log("content-length: {s}", .{content_length});
|
||||
break :brk std.fmt.parseInt(usize, content_length, 10) catch 0;
|
||||
}
|
||||
|
||||
|
||||
@@ -5,7 +5,7 @@
|
||||
namespace WebCore {
|
||||
|
||||
// must match src/bun.js/node/types.zig#Encoding
|
||||
enum class BufferEncodingType : uint8_t {
|
||||
enum class BufferEncodingType : uint16_t {
|
||||
utf8 = 0,
|
||||
ucs2 = 1,
|
||||
utf16le = 2,
|
||||
|
||||
@@ -48,7 +48,7 @@ JSC_DECLARE_CUSTOM_GETTER(jsNodeHttpServerSocketGetterLocalAddress);
|
||||
BUN_DECLARE_HOST_FUNCTION(Bun__drainMicrotasksFromJS);
|
||||
JSC_DECLARE_CUSTOM_GETTER(jsNodeHttpServerSocketGetterDuplex);
|
||||
JSC_DECLARE_CUSTOM_SETTER(jsNodeHttpServerSocketSetterDuplex);
|
||||
|
||||
JSC_DECLARE_CUSTOM_GETTER(jsNodeHttpServerSocketGetterIsSecureEstablished);
|
||||
// Create a static hash table of values containing an onclose DOMAttributeGetterSetter and a close function
|
||||
static const HashTableValue JSNodeHTTPServerSocketPrototypeTableValues[] = {
|
||||
{ "onclose"_s, static_cast<unsigned>(PropertyAttribute::CustomAccessor), NoIntrinsic, { HashTableValue::GetterSetterType, jsNodeHttpServerSocketGetterOnClose, jsNodeHttpServerSocketSetterOnClose } },
|
||||
@@ -58,6 +58,7 @@ static const HashTableValue JSNodeHTTPServerSocketPrototypeTableValues[] = {
|
||||
{ "remoteAddress"_s, static_cast<unsigned>(PropertyAttribute::CustomAccessor | PropertyAttribute::ReadOnly), NoIntrinsic, { HashTableValue::GetterSetterType, jsNodeHttpServerSocketGetterRemoteAddress, noOpSetter } },
|
||||
{ "localAddress"_s, static_cast<unsigned>(PropertyAttribute::CustomAccessor | PropertyAttribute::ReadOnly), NoIntrinsic, { HashTableValue::GetterSetterType, jsNodeHttpServerSocketGetterLocalAddress, noOpSetter } },
|
||||
{ "close"_s, static_cast<unsigned>(PropertyAttribute::Function | PropertyAttribute::DontEnum), NoIntrinsic, { HashTableValue::NativeFunctionType, jsFunctionNodeHTTPServerSocketClose, 0 } },
|
||||
{ "secureEstablished"_s, static_cast<unsigned>(PropertyAttribute::CustomAccessor | PropertyAttribute::ReadOnly), NoIntrinsic, { HashTableValue::GetterSetterType, jsNodeHttpServerSocketGetterIsSecureEstablished, noOpSetter } },
|
||||
};
|
||||
|
||||
class JSNodeHTTPServerSocketPrototype final : public JSC::JSNonFinalObject {
|
||||
@@ -137,7 +138,16 @@ public:
|
||||
{
|
||||
return !socket || us_socket_is_closed(is_ssl, socket);
|
||||
}
|
||||
|
||||
bool isSecure() const
|
||||
{
|
||||
// is secure means that tls was established successfully
|
||||
if (!is_ssl || !socket) return false;
|
||||
auto* context = us_socket_context(is_ssl, socket);
|
||||
if (!context) return false;
|
||||
auto* data = (uWS::HttpContextData<true>*)us_socket_context_ext(is_ssl, context);
|
||||
if (!data) return false;
|
||||
return data->isSecure();
|
||||
}
|
||||
~JSNodeHTTPServerSocket()
|
||||
{
|
||||
if (socket) {
|
||||
@@ -270,6 +280,14 @@ JSC_DEFINE_HOST_FUNCTION(jsFunctionNodeHTTPServerSocketClose, (JSC::JSGlobalObje
|
||||
return JSValue::encode(JSC::jsUndefined());
|
||||
}
|
||||
|
||||
JSC_DEFINE_CUSTOM_GETTER(jsNodeHttpServerSocketGetterIsSecureEstablished, (JSC::JSGlobalObject * globalObject, JSC::EncodedJSValue thisValue, JSC::PropertyName))
|
||||
{
|
||||
auto* thisObject = jsCast<JSNodeHTTPServerSocket*>(JSC::JSValue::decode(thisValue));
|
||||
if (UNLIKELY(!thisObject)) {
|
||||
return JSValue::encode(JSC::jsBoolean(false));
|
||||
}
|
||||
return JSValue::encode(JSC::jsBoolean(thisObject->isSecure()));
|
||||
}
|
||||
JSC_DEFINE_CUSTOM_GETTER(jsNodeHttpServerSocketGetterDuplex, (JSC::JSGlobalObject * globalObject, JSC::EncodedJSValue thisValue, JSC::PropertyName))
|
||||
{
|
||||
auto* thisObject = jsCast<JSNodeHTTPServerSocket*>(JSC::JSValue::decode(thisValue));
|
||||
@@ -485,6 +503,28 @@ extern "C" void Bun__callNodeHTTPServerSocketOnClose(EncodedJSValue thisValue)
|
||||
response->onClose();
|
||||
}
|
||||
|
||||
extern "C" JSC::EncodedJSValue Bun__createNodeHTTPServerSocket(bool isSSL, us_socket_t* us_socket, Zig::GlobalObject* globalObject)
|
||||
{
|
||||
auto& vm = globalObject->vm();
|
||||
auto scope = DECLARE_THROW_SCOPE(vm);
|
||||
|
||||
RETURN_IF_EXCEPTION(scope, {});
|
||||
|
||||
// socket without response because is not valid http
|
||||
JSNodeHTTPServerSocket* socket = JSNodeHTTPServerSocket::create(
|
||||
vm,
|
||||
globalObject->m_JSNodeHTTPServerSocketStructure.getInitializedOnMainThread(globalObject),
|
||||
(us_socket_t*)us_socket,
|
||||
isSSL, nullptr);
|
||||
|
||||
RETURN_IF_EXCEPTION(scope, {});
|
||||
if (socket) {
|
||||
socket->strongThis.set(vm, socket);
|
||||
return JSValue::encode(socket);
|
||||
}
|
||||
return JSValue::encode(JSC::jsNull());
|
||||
}
|
||||
|
||||
BUN_DECLARE_HOST_FUNCTION(jsFunctionRequestOrResponseHasBodyValue);
|
||||
BUN_DECLARE_HOST_FUNCTION(jsFunctionGetCompleteRequestOrResponseBodyValueAsArrayBuffer);
|
||||
extern "C" uWS::HttpRequest* Request__getUWSRequest(void*);
|
||||
@@ -493,6 +533,7 @@ extern "C" void Request__setTimeout(void*, EncodedJSValue, JSC::JSGlobalObject*)
|
||||
extern "C" bool NodeHTTPResponse__setTimeout(void*, EncodedJSValue, JSC::JSGlobalObject*);
|
||||
extern "C" void Server__setIdleTimeout(EncodedJSValue, EncodedJSValue, JSC::JSGlobalObject*);
|
||||
extern "C" void Server__setRequireHostHeader(EncodedJSValue, bool, JSC::JSGlobalObject*);
|
||||
extern "C" void Server__setOnClientError(EncodedJSValue, EncodedJSValue, JSC::JSGlobalObject*);
|
||||
static EncodedJSValue assignHeadersFromFetchHeaders(FetchHeaders& impl, JSObject* prototype, JSObject* objectValue, JSC::InternalFieldTuple* tuple, JSC::JSGlobalObject* globalObject, JSC::VM& vm)
|
||||
{
|
||||
auto scope = DECLARE_THROW_SCOPE(vm);
|
||||
@@ -1270,7 +1311,7 @@ JSC_DEFINE_HOST_FUNCTION(jsHTTPSetServerIdleTimeout, (JSGlobalObject * globalObj
|
||||
return JSValue::encode(jsUndefined());
|
||||
}
|
||||
|
||||
JSC_DEFINE_HOST_FUNCTION(jsHTTPSetRequireHostHeader, (JSGlobalObject * globalObject, CallFrame* callFrame))
|
||||
JSC_DEFINE_HOST_FUNCTION(jsHTTPSetCustomOptions, (JSGlobalObject * globalObject, CallFrame* callFrame))
|
||||
{
|
||||
auto& vm = JSC::getVM(globalObject);
|
||||
auto scope = DECLARE_THROW_SCOPE(vm);
|
||||
@@ -1279,10 +1320,15 @@ JSC_DEFINE_HOST_FUNCTION(jsHTTPSetRequireHostHeader, (JSGlobalObject * globalObj
|
||||
JSValue serverValue = callFrame->uncheckedArgument(0);
|
||||
JSValue requireHostHeader = callFrame->uncheckedArgument(1);
|
||||
|
||||
ASSERT(callFrame->argumentCount() == 2);
|
||||
ASSERT(callFrame->argumentCount() >= 2);
|
||||
|
||||
Server__setRequireHostHeader(JSValue::encode(serverValue), requireHostHeader.toBoolean(globalObject), globalObject);
|
||||
|
||||
if (callFrame->argumentCount() > 2) {
|
||||
JSValue callback = callFrame->uncheckedArgument(2);
|
||||
Server__setOnClientError(JSValue::encode(serverValue), JSValue::encode(callback), globalObject);
|
||||
}
|
||||
|
||||
return JSValue::encode(jsUndefined());
|
||||
}
|
||||
|
||||
@@ -1406,8 +1452,8 @@ JSValue createNodeHTTPInternalBinding(Zig::GlobalObject* globalObject)
|
||||
JSC::JSFunction::create(vm, globalObject, 2, "setServerIdleTimeout"_s, jsHTTPSetServerIdleTimeout, ImplementationVisibility::Public), 0);
|
||||
|
||||
obj->putDirect(
|
||||
vm, JSC::PropertyName(JSC::Identifier::fromString(vm, "setRequireHostHeader"_s)),
|
||||
JSC::JSFunction::create(vm, globalObject, 2, "setRequireHostHeader"_s, jsHTTPSetRequireHostHeader, ImplementationVisibility::Public), 0);
|
||||
vm, JSC::PropertyName(JSC::Identifier::fromString(vm, "setServerCustomOptions"_s)),
|
||||
JSC::JSFunction::create(vm, globalObject, 2, "setServerCustomOptions"_s, jsHTTPSetCustomOptions, ImplementationVisibility::Public), 0);
|
||||
obj->putDirect(
|
||||
vm, JSC::PropertyName(JSC::Identifier::fromString(vm, "Response"_s)),
|
||||
globalObject->JSResponseConstructor(), 0);
|
||||
|
||||
@@ -2133,10 +2133,10 @@ extern "C" napi_status napi_get_value_int64(napi_env env, napi_value value, int6
|
||||
}
|
||||
|
||||
// must match src/bun.js/node/types.zig#Encoding, which matches WebCore::BufferEncodingType
|
||||
enum class NapiStringEncoding : uint8_t {
|
||||
utf8 = static_cast<uint8_t>(WebCore::BufferEncodingType::utf8),
|
||||
utf16le = static_cast<uint8_t>(WebCore::BufferEncodingType::utf16le),
|
||||
latin1 = static_cast<uint8_t>(WebCore::BufferEncodingType::latin1),
|
||||
enum class NapiStringEncoding : uint16_t {
|
||||
utf8 = static_cast<uint16_t>(WebCore::BufferEncodingType::utf8),
|
||||
utf16le = static_cast<uint16_t>(WebCore::BufferEncodingType::utf16le),
|
||||
latin1 = static_cast<uint16_t>(WebCore::BufferEncodingType::latin1),
|
||||
};
|
||||
|
||||
template<NapiStringEncoding...>
|
||||
|
||||
@@ -92,7 +92,7 @@ for (let [code, constructor, name, ...other_constructors] of NodeErrors) {
|
||||
if (name == null) name = constructor.name;
|
||||
|
||||
// it's useful to avoid the prefix, but module not found has a prefixed and unprefixed version
|
||||
const codeWithoutPrefix = code === 'ERR_MODULE_NOT_FOUND' ? code : code.replace(/^ERR_/, '');
|
||||
const codeWithoutPrefix = code === "ERR_MODULE_NOT_FOUND" ? code : code.replace(/^ERR_/, "");
|
||||
|
||||
enumHeader += ` ${code} = ${i},\n`;
|
||||
listHeader += ` { JSC::ErrorType::${constructor.name}, "${name}"_s, "${code}"_s },\n`;
|
||||
|
||||
@@ -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)
|
||||
{
|
||||
|
||||
@@ -3032,6 +3032,7 @@ pub const ListenSocket = opaque {
|
||||
extern fn us_listen_socket_close(ssl: i32, ls: *ListenSocket) void;
|
||||
extern fn uws_app_close(ssl: i32, app: *uws_app_s) void;
|
||||
extern fn us_socket_context_close(ssl: i32, ctx: *anyopaque) void;
|
||||
extern fn uws_app_set_on_clienterror(ssl: i32, app: *uws_app_s, handler: *const fn (*anyopaque, i32, *Socket, u8, ?[*]u8, c_int) callconv(.C) void, user_data: *anyopaque) void;
|
||||
|
||||
pub const SocketAddress = struct {
|
||||
ip: []const u8,
|
||||
@@ -3476,6 +3477,25 @@ pub fn NewApp(comptime ssl: bool) type {
|
||||
return uws_app_listen(ssl_flag, @as(*uws_app_t, @ptrCast(app)), port, Wrapper.handle, user_data);
|
||||
}
|
||||
|
||||
pub fn onClientError(
|
||||
app: *ThisApp,
|
||||
comptime UserData: type,
|
||||
user_data: UserData,
|
||||
comptime handler: fn (data: UserData, socket: *Socket, error_code: u8, rawPacket: []const u8) void,
|
||||
) void {
|
||||
const Wrapper = struct {
|
||||
pub fn handle(data: *anyopaque, _: i32, socket: *Socket, error_code: u8, raw_packet: ?[*]u8, raw_packet_length: c_int) callconv(.C) void {
|
||||
@call(bun.callmod_inline, handler, .{
|
||||
@as(UserData, @ptrCast(@alignCast(data))),
|
||||
socket,
|
||||
error_code,
|
||||
if (raw_packet) |bytes| bytes[0..@intCast(@max(raw_packet_length, 0))] else "",
|
||||
});
|
||||
}
|
||||
};
|
||||
return uws_app_set_on_clienterror(ssl_flag, @as(*uws_app_t, @ptrCast(app)), Wrapper.handle, @ptrCast(user_data));
|
||||
}
|
||||
|
||||
pub fn listenWithConfig(
|
||||
app: *ThisApp,
|
||||
comptime UserData: type,
|
||||
|
||||
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{
|
||||
|
||||
@@ -8,7 +8,7 @@ const {
|
||||
setRequestTimeout,
|
||||
headersTuple,
|
||||
webRequestOrResponseHasBodyValue,
|
||||
setRequireHostHeader,
|
||||
setServerCustomOptions,
|
||||
getCompleteWebRequestOrResponseBodyValueAsArrayBuffer,
|
||||
drainMicrotasks,
|
||||
setServerIdleTimeout,
|
||||
@@ -20,7 +20,11 @@ const {
|
||||
setRequestTimeout: (req: Request, timeout: number) => boolean;
|
||||
headersTuple: any;
|
||||
webRequestOrResponseHasBodyValue: (arg: any) => boolean;
|
||||
setRequireHostHeader: (server: any, requireHostHeader: boolean) => void;
|
||||
setServerCustomOptions: (
|
||||
server: any,
|
||||
requireHostHeader: boolean,
|
||||
onClientError: (ssl: boolean, socket: any, errorCode: number, rawPacket: ArrayBuffer) => undefined,
|
||||
) => void;
|
||||
getCompleteWebRequestOrResponseBodyValueAsArrayBuffer: (arg: any) => ArrayBuffer | undefined;
|
||||
drainMicrotasks: () => void;
|
||||
setServerIdleTimeout: (server: any, timeout: number) => void;
|
||||
@@ -445,5 +449,5 @@ export {
|
||||
drainMicrotasks,
|
||||
setServerIdleTimeout,
|
||||
getRawKeys,
|
||||
setRequireHostHeader,
|
||||
setServerCustomOptions,
|
||||
};
|
||||
|
||||
@@ -40,7 +40,7 @@ const {
|
||||
runSymbol,
|
||||
drainMicrotasks,
|
||||
setServerIdleTimeout,
|
||||
setRequireHostHeader,
|
||||
setServerCustomOptions,
|
||||
} = require("internal/http");
|
||||
|
||||
const { format } = require("internal/util/inspect");
|
||||
@@ -48,6 +48,7 @@ const { format } = require("internal/util/inspect");
|
||||
const { IncomingMessage } = require("node:_http_incoming");
|
||||
const { OutgoingMessage } = require("node:_http_outgoing");
|
||||
const { kIncomingMessage } = require("node:_http_common");
|
||||
const kConnectionsCheckingInterval = Symbol("http.server.connectionsCheckingInterval");
|
||||
|
||||
const getBunServerAllClosedPromise = $newZigFunction("node_http_binding.zig", "getBunServerAllClosedPromise", 1);
|
||||
const sendHelper = $newZigFunction("node_cluster_binding.zig", "sendHelperChild", 3);
|
||||
@@ -681,12 +682,35 @@ function onServerRequestEvent(this: NodeHTTPServerSocket, event: NodeHTTPRespons
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
function onServerClientError(ssl: boolean, socket: unknown, errorCode: number, rawPacket: ArrayBuffer) {
|
||||
const self = this as Server;
|
||||
const err = new Error("Parse Error");
|
||||
switch (errorCode) {
|
||||
case 2:
|
||||
err.code = "HPE_UNEXPECTED_CONTENT_LENGTH";
|
||||
break;
|
||||
case 3:
|
||||
err.code = "HPE_INVALID_TRANSFER_ENCODING";
|
||||
break;
|
||||
case 8:
|
||||
err.code = "HPE_INVALID_EOF_STATE";
|
||||
break;
|
||||
case 9:
|
||||
err.code = "HPE_INVALID_METHOD";
|
||||
break;
|
||||
default:
|
||||
err.code = "HPE_INTERNAL";
|
||||
break;
|
||||
}
|
||||
err.rawPacket = rawPacket;
|
||||
self.emit("clientError", err, new NodeHTTPServerSocket(self, socket, ssl));
|
||||
}
|
||||
const ServerPrototype = {
|
||||
constructor: Server,
|
||||
__proto__: EventEmitter.prototype,
|
||||
[kIncomingMessage]: undefined,
|
||||
[kServerResponse]: undefined,
|
||||
[kConnectionsCheckingInterval]: { _destroyed: false },
|
||||
ref() {
|
||||
this._unref = false;
|
||||
this[serverSymbol]?.ref?.();
|
||||
@@ -719,6 +743,7 @@ const ServerPrototype = {
|
||||
return;
|
||||
}
|
||||
this[serverSymbol] = undefined;
|
||||
this[kConnectionsCheckingInterval]._destroyed = true;
|
||||
if (typeof optionalCallback === "function") setCloseCallback(this, optionalCallback);
|
||||
server.stop();
|
||||
},
|
||||
@@ -1055,7 +1080,7 @@ const ServerPrototype = {
|
||||
});
|
||||
getBunServerAllClosedPromise(this[serverSymbol]).$then(emitCloseNTServer.bind(this));
|
||||
isHTTPS = this[serverSymbol].protocol === "https";
|
||||
setRequireHostHeader(this[serverSymbol], this.requireHostHeader);
|
||||
setServerCustomOptions(this[serverSymbol], this.requireHostHeader, onServerClientError.bind(this));
|
||||
|
||||
if (this?._unref) {
|
||||
this[serverSymbol]?.unref?.();
|
||||
@@ -1109,6 +1134,10 @@ const NodeHTTPServerSocket = class Socket extends Duplex {
|
||||
this.on("timeout", onNodeHTTPServerSocketTimeout);
|
||||
}
|
||||
|
||||
get _secureEstablished() {
|
||||
return !!this[kHandle]?.secureEstablished;
|
||||
}
|
||||
|
||||
#closeHandle(handle, callback) {
|
||||
this[kHandle] = undefined;
|
||||
handle.onclose = this.#onCloseForDestroy.bind(this, callback);
|
||||
@@ -1695,4 +1724,5 @@ function ensureReadableStreamController(run) {
|
||||
export default {
|
||||
Server,
|
||||
ServerResponse,
|
||||
kConnectionsCheckingInterval,
|
||||
};
|
||||
|
||||
@@ -23,8 +23,9 @@ import http, {
|
||||
validateHeaderName,
|
||||
validateHeaderValue,
|
||||
} from "node:http";
|
||||
import type { AddressInfo } from "node:net";
|
||||
import { connect } from "node:net";
|
||||
import https, { createServer as createHttpsServer } from "node:https";
|
||||
import { tls as COMMON_TLS_CERT } from "harness";
|
||||
import { tmpdir } from "node:os";
|
||||
import * as path from "node:path";
|
||||
import * as stream from "node:stream";
|
||||
@@ -2417,3 +2418,95 @@ it("should reject non-standard body writes when rejectNonStandardBodyWrites is t
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
test("request.socket._secureEstablished should identify if the request is secure", async () => {
|
||||
{
|
||||
const { promise, resolve, reject } = Promise.withResolvers();
|
||||
const server = createHttpsServer(COMMON_TLS_CERT, (request, response) => {
|
||||
// Run the check function
|
||||
expect(request.socket._secureEstablished).toBe(true);
|
||||
response.writeHead(200, {});
|
||||
response.end("ok");
|
||||
server.close();
|
||||
resolve();
|
||||
});
|
||||
|
||||
server.listen(0, () => {
|
||||
const port = server.address().port;
|
||||
fetch(`https://localhost:${port}`, {
|
||||
tls: {
|
||||
ca: COMMON_TLS_CERT.cert,
|
||||
},
|
||||
}).catch(reject);
|
||||
});
|
||||
|
||||
await promise;
|
||||
}
|
||||
|
||||
{
|
||||
const { promise, resolve, reject } = Promise.withResolvers();
|
||||
const server = createServer((request, response) => {
|
||||
// Run the check function
|
||||
expect(request.socket._secureEstablished).toBe(false);
|
||||
response.writeHead(200, {});
|
||||
response.end("ok");
|
||||
server.close();
|
||||
resolve();
|
||||
});
|
||||
|
||||
server.listen(0, () => {
|
||||
const port = server.address().port;
|
||||
fetch(`http://localhost:${port}`).catch(reject);
|
||||
});
|
||||
|
||||
await promise;
|
||||
}
|
||||
});
|
||||
|
||||
test("should emit clientError when Content-Length is invalid", async () => {
|
||||
const { promise, resolve, reject } = Promise.withResolvers();
|
||||
const server = http.createServer(reject);
|
||||
|
||||
server.on("clientError", (err, socket) => {
|
||||
resolve(err);
|
||||
socket.destroy();
|
||||
});
|
||||
|
||||
await once(server.listen(0), "listening");
|
||||
|
||||
const client = connect(server.address().port, () => {
|
||||
// HTTP request with invalid Content-Length
|
||||
// The Content-Length says 10 but the actual body is 20 bytes
|
||||
// Send the request
|
||||
client.write(
|
||||
`POST /test HTTP/1.1\r\nHost: localhost:${server.address().port}\r\nContent-Type: text/plain\r\nContent-Length: invalid\r\n\r\n`,
|
||||
);
|
||||
});
|
||||
|
||||
const err = (await promise) as Error;
|
||||
expect(err.code).toBe("HPE_UNEXPECTED_CONTENT_LENGTH");
|
||||
});
|
||||
|
||||
test("should emit clientError when mixing Content-Length and Transfer-Encoding", async () => {
|
||||
const { promise, resolve, reject } = Promise.withResolvers();
|
||||
const server = http.createServer(reject);
|
||||
|
||||
server.on("clientError", (err, socket) => {
|
||||
resolve(err);
|
||||
socket.destroy();
|
||||
});
|
||||
|
||||
await once(server.listen(0), "listening");
|
||||
|
||||
const client = connect(server.address().port, () => {
|
||||
// HTTP request with invalid Content-Length
|
||||
// The Content-Length says 10 but the actual body is 20 bytes
|
||||
// Send the request
|
||||
client.write(
|
||||
`POST /test HTTP/1.1\r\nHost: localhost:${server.address().port}\r\nContent-Type: text/plain\r\nContent-Length: 5\r\nTransfer-Encoding: chunked\r\n\r\nHello`,
|
||||
);
|
||||
});
|
||||
|
||||
const err = (await promise) as Error;
|
||||
expect(err.code).toBe("HPE_INVALID_TRANSFER_ENCODING");
|
||||
});
|
||||
|
||||
@@ -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();
|
||||
}));
|
||||
});
|
||||
154
test/js/node/test/parallel/test-http-generic-streams.js
Normal file
154
test/js/node/test/parallel/test-http-generic-streams.js
Normal file
@@ -0,0 +1,154 @@
|
||||
'use strict';
|
||||
const common = require('../common');
|
||||
const assert = require('assert');
|
||||
const http = require('http');
|
||||
const { duplexPair } = require('stream');
|
||||
|
||||
// Test 1: Simple HTTP test, no keep-alive.
|
||||
{
|
||||
const testData = 'Hello, World!\n';
|
||||
const server = http.createServer(common.mustCall((req, res) => {
|
||||
res.statusCode = 200;
|
||||
res.setHeader('Content-Type', 'text/plain');
|
||||
res.end(testData);
|
||||
}));
|
||||
|
||||
const [ clientSide, serverSide ] = duplexPair();
|
||||
server.emit('connection', serverSide);
|
||||
|
||||
const req = http.request({
|
||||
createConnection: common.mustCall(() => clientSide)
|
||||
}, common.mustCall((res) => {
|
||||
res.setEncoding('utf8');
|
||||
res.on('data', common.mustCall((data) => {
|
||||
assert.strictEqual(data, testData);
|
||||
}));
|
||||
res.on('end', common.mustCall());
|
||||
}));
|
||||
req.end();
|
||||
}
|
||||
|
||||
// Test 2: Keep-alive for 2 requests.
|
||||
{
|
||||
const testData = 'Hello, World!\n';
|
||||
const server = http.createServer(common.mustCall((req, res) => {
|
||||
res.statusCode = 200;
|
||||
res.setHeader('Content-Type', 'text/plain');
|
||||
res.end(testData);
|
||||
}, 2));
|
||||
|
||||
const [ clientSide, serverSide ] = duplexPair();
|
||||
server.emit('connection', serverSide);
|
||||
|
||||
function doRequest(cb) {
|
||||
const req = http.request({
|
||||
createConnection: common.mustCall(() => clientSide),
|
||||
headers: { Connection: 'keep-alive' }
|
||||
}, common.mustCall((res) => {
|
||||
res.setEncoding('utf8');
|
||||
res.on('data', common.mustCall((data) => {
|
||||
assert.strictEqual(data, testData);
|
||||
}));
|
||||
res.on('end', common.mustCall(cb));
|
||||
}));
|
||||
req.shouldKeepAlive = true;
|
||||
req.end();
|
||||
}
|
||||
|
||||
doRequest(() => {
|
||||
doRequest();
|
||||
});
|
||||
}
|
||||
|
||||
// Test 3: Connection: close request/response with chunked
|
||||
{
|
||||
const testData = 'Hello, World!\n';
|
||||
const server = http.createServer(common.mustCall((req, res) => {
|
||||
req.setEncoding('utf8');
|
||||
req.resume();
|
||||
req.on('data', common.mustCall(function test3_req_data(data) {
|
||||
assert.strictEqual(data, testData);
|
||||
}));
|
||||
req.once('end', function() {
|
||||
res.statusCode = 200;
|
||||
res.setHeader('Content-Type', 'text/plain');
|
||||
res.write(testData);
|
||||
res.end();
|
||||
});
|
||||
}));
|
||||
|
||||
const [ clientSide, serverSide ] = duplexPair();
|
||||
server.emit('connection', serverSide);
|
||||
clientSide.on('end', common.mustCall());
|
||||
serverSide.on('end', common.mustCall());
|
||||
|
||||
const req = http.request({
|
||||
createConnection: common.mustCall(() => clientSide),
|
||||
method: 'PUT',
|
||||
headers: { 'Connection': 'close' }
|
||||
}, common.mustCall((res) => {
|
||||
res.setEncoding('utf8');
|
||||
res.on('data', common.mustCall(function test3_res_data(data) {
|
||||
assert.strictEqual(data, testData);
|
||||
}));
|
||||
res.on('end', common.mustCall());
|
||||
}));
|
||||
req.write(testData);
|
||||
req.end();
|
||||
}
|
||||
|
||||
// Test 4: Connection: close request/response with Content-Length
|
||||
// The same as Test 3, but with Content-Length headers
|
||||
{
|
||||
const testData = 'Hello, World!\n';
|
||||
const server = http.createServer(common.mustCall((req, res) => {
|
||||
assert.strictEqual(req.headers['content-length'], testData.length + '');
|
||||
req.setEncoding('utf8');
|
||||
req.on('data', common.mustCall(function test4_req_data(data) {
|
||||
assert.strictEqual(data, testData);
|
||||
}));
|
||||
req.once('end', function() {
|
||||
res.statusCode = 200;
|
||||
res.setHeader('Content-Type', 'text/plain');
|
||||
res.setHeader('Content-Length', testData.length);
|
||||
res.write(testData);
|
||||
res.end();
|
||||
});
|
||||
|
||||
}));
|
||||
|
||||
const [ clientSide, serverSide ] = duplexPair();
|
||||
server.emit('connection', serverSide);
|
||||
clientSide.on('end', common.mustCall());
|
||||
serverSide.on('end', common.mustCall());
|
||||
|
||||
const req = http.request({
|
||||
createConnection: common.mustCall(() => clientSide),
|
||||
method: 'PUT',
|
||||
headers: { 'Connection': 'close' }
|
||||
}, common.mustCall((res) => {
|
||||
res.setEncoding('utf8');
|
||||
assert.strictEqual(res.headers['content-length'], testData.length + '');
|
||||
res.on('data', common.mustCall(function test4_res_data(data) {
|
||||
assert.strictEqual(data, testData);
|
||||
}));
|
||||
res.on('end', common.mustCall());
|
||||
}));
|
||||
req.setHeader('Content-Length', testData.length);
|
||||
req.write(testData);
|
||||
req.end();
|
||||
}
|
||||
|
||||
// Test 5: The client sends garbage.
|
||||
{
|
||||
const server = http.createServer(common.mustNotCall());
|
||||
|
||||
const [ clientSide, serverSide ] = duplexPair();
|
||||
server.emit('connection', serverSide);
|
||||
|
||||
server.on('clientError', common.mustCall());
|
||||
|
||||
// Send something that is not an HTTP request.
|
||||
clientSide.end(
|
||||
'I’m reading a book about anti-gravity. It’s impossible to put down!');
|
||||
}
|
||||
@@ -0,0 +1,98 @@
|
||||
'use strict';
|
||||
const common = require('../common');
|
||||
const assert = require('assert');
|
||||
const http = require('http');
|
||||
const { duplexPair } = require('stream');
|
||||
|
||||
// Test that setting the `maxHeaderSize` option works on a per-stream-basis.
|
||||
|
||||
// Test 1: The server sends an invalid header.
|
||||
{
|
||||
const [ clientSide, serverSide ] = duplexPair();
|
||||
|
||||
const req = http.request({
|
||||
createConnection: common.mustCall(() => clientSide),
|
||||
insecureHTTPParser: true
|
||||
}, common.mustCall((res) => {
|
||||
assert.strictEqual(res.headers.hello, 'foo\x08foo');
|
||||
res.resume(); // We don’t actually care about contents.
|
||||
res.on('end', common.mustCall());
|
||||
}));
|
||||
req.end();
|
||||
|
||||
serverSide.resume(); // Dump the request
|
||||
serverSide.end('HTTP/1.1 200 OK\r\n' +
|
||||
'Host: example.com\r\n' +
|
||||
'Hello: foo\x08foo\r\n' +
|
||||
'Content-Length: 0\r\n' +
|
||||
'\r\n\r\n');
|
||||
}
|
||||
|
||||
// Test 2: The same as Test 1 except without the option, to make sure it fails.
|
||||
{
|
||||
const [ clientSide, serverSide ] = duplexPair();
|
||||
|
||||
const req = http.request({
|
||||
createConnection: common.mustCall(() => clientSide)
|
||||
}, common.mustNotCall());
|
||||
req.end();
|
||||
req.on('error', common.mustCall());
|
||||
|
||||
serverSide.resume(); // Dump the request
|
||||
serverSide.end('HTTP/1.1 200 OK\r\n' +
|
||||
'Host: example.com\r\n' +
|
||||
'Hello: foo\x08foo\r\n' +
|
||||
'Content-Length: 0\r\n' +
|
||||
'\r\n\r\n');
|
||||
}
|
||||
|
||||
// Test 3: The client sends an invalid header.
|
||||
{
|
||||
const testData = 'Hello, World!\n';
|
||||
const server = http.createServer(
|
||||
{ insecureHTTPParser: true },
|
||||
common.mustCall((req, res) => {
|
||||
res.statusCode = 200;
|
||||
res.setHeader('Content-Type', 'text/plain');
|
||||
res.end(testData);
|
||||
}));
|
||||
|
||||
server.on('clientError', common.mustNotCall());
|
||||
|
||||
const [ clientSide, serverSide ] = duplexPair();
|
||||
serverSide.server = server;
|
||||
server.emit('connection', serverSide);
|
||||
|
||||
clientSide.write('GET / HTTP/1.1\r\n' +
|
||||
'Host: example.com\r\n' +
|
||||
'Hello: foo\x08foo\r\n' +
|
||||
'\r\n\r\n');
|
||||
}
|
||||
|
||||
// Test 4: The same as Test 3 except without the option, to make sure it fails.
|
||||
{
|
||||
const server = http.createServer(common.mustNotCall());
|
||||
|
||||
server.on('clientError', common.mustCall());
|
||||
|
||||
const [ clientSide, serverSide ] = duplexPair();
|
||||
serverSide.server = server;
|
||||
server.emit('connection', serverSide);
|
||||
|
||||
clientSide.write('GET / HTTP/1.1\r\n' +
|
||||
'Host: example.com\r\n' +
|
||||
'Hello: foo\x08foo\r\n' +
|
||||
'\r\n\r\n');
|
||||
}
|
||||
|
||||
// Test 5: Invalid argument type
|
||||
{
|
||||
assert.throws(
|
||||
() => http.request({ insecureHTTPParser: 0 }, common.mustNotCall()),
|
||||
common.expectsError({
|
||||
code: 'ERR_INVALID_ARG_TYPE',
|
||||
message: 'The "options.insecureHTTPParser" property must be of' +
|
||||
' type boolean. Received type number (0)'
|
||||
})
|
||||
);
|
||||
}
|
||||
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'));
|
||||
}));
|
||||
}));
|
||||
@@ -0,0 +1,83 @@
|
||||
'use strict';
|
||||
const common = require('../common');
|
||||
const assert = require('assert');
|
||||
const http = require('http');
|
||||
const { duplexPair } = require('stream');
|
||||
|
||||
// Test that setting the `maxHeaderSize` option works on a per-stream-basis.
|
||||
|
||||
// Test 1: The server sends larger headers than what would otherwise be allowed.
|
||||
{
|
||||
const [ clientSide, serverSide ] = duplexPair();
|
||||
|
||||
const req = http.request({
|
||||
createConnection: common.mustCall(() => clientSide),
|
||||
maxHeaderSize: http.maxHeaderSize * 4
|
||||
}, common.mustCall((res) => {
|
||||
assert.strictEqual(res.headers.hello, 'A'.repeat(http.maxHeaderSize * 3));
|
||||
res.resume(); // We don’t actually care about contents.
|
||||
res.on('end', common.mustCall());
|
||||
}));
|
||||
req.end();
|
||||
|
||||
serverSide.resume(); // Dump the request
|
||||
serverSide.end('HTTP/1.1 200 OK\r\n' +
|
||||
'Hello: ' + 'A'.repeat(http.maxHeaderSize * 3) + '\r\n' +
|
||||
'Content-Length: 0\r\n' +
|
||||
'\r\n\r\n');
|
||||
}
|
||||
|
||||
// Test 2: The same as Test 1 except without the option, to make sure it fails.
|
||||
{
|
||||
const [ clientSide, serverSide ] = duplexPair();
|
||||
|
||||
const req = http.request({
|
||||
createConnection: common.mustCall(() => clientSide)
|
||||
}, common.mustNotCall());
|
||||
req.end();
|
||||
req.on('error', common.mustCall());
|
||||
|
||||
serverSide.resume(); // Dump the request
|
||||
serverSide.end('HTTP/1.1 200 OK\r\n' +
|
||||
'Hello: ' + 'A'.repeat(http.maxHeaderSize * 3) + '\r\n' +
|
||||
'Content-Length: 0\r\n' +
|
||||
'\r\n\r\n');
|
||||
}
|
||||
|
||||
// Test 3: The client sends larger headers than what would otherwise be allowed.
|
||||
{
|
||||
const testData = 'Hello, World!\n';
|
||||
const server = http.createServer(
|
||||
{ maxHeaderSize: http.maxHeaderSize * 4 },
|
||||
common.mustCall((req, res) => {
|
||||
res.statusCode = 200;
|
||||
res.setHeader('Content-Type', 'text/plain');
|
||||
res.end(testData);
|
||||
}));
|
||||
|
||||
server.on('clientError', common.mustNotCall());
|
||||
|
||||
const [ clientSide, serverSide ] = duplexPair();
|
||||
serverSide.server = server;
|
||||
server.emit('connection', serverSide);
|
||||
|
||||
clientSide.write('GET / HTTP/1.1\r\n' +
|
||||
'Host: example.com\r\n' +
|
||||
'Hello: ' + 'A'.repeat(http.maxHeaderSize * 3) + '\r\n' +
|
||||
'\r\n\r\n');
|
||||
}
|
||||
|
||||
// Test 4: The same as Test 3 except without the option, to make sure it fails.
|
||||
{
|
||||
const server = http.createServer(common.mustNotCall());
|
||||
|
||||
server.on('clientError', common.mustCall());
|
||||
|
||||
const [ clientSide, serverSide ] = duplexPair();
|
||||
serverSide.server = server;
|
||||
server.emit('connection', serverSide);
|
||||
|
||||
clientSide.write('GET / HTTP/1.1\r\n' +
|
||||
'Hello: ' + 'A'.repeat(http.maxHeaderSize * 3) + '\r\n' +
|
||||
'\r\n\r\n');
|
||||
}
|
||||
178
test/js/node/test/parallel/test-http-max-http-headers.js
Normal file
178
test/js/node/test/parallel/test-http-max-http-headers.js
Normal file
@@ -0,0 +1,178 @@
|
||||
// Flags: --expose-internals
|
||||
'use strict';
|
||||
const common = require('../common');
|
||||
const assert = require('assert');
|
||||
const http = require('http');
|
||||
const net = require('net');
|
||||
const MAX = +(process.argv[2] || 16 * 1024); // Command line option, or 16KB.
|
||||
|
||||
|
||||
console.log('pid is', process.pid);
|
||||
|
||||
// Verify that we cannot receive more than 16KB of headers.
|
||||
|
||||
function once(cb) {
|
||||
let called = false;
|
||||
return () => {
|
||||
if (!called) {
|
||||
called = true;
|
||||
cb();
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
function finished(client, callback) {
|
||||
['abort', 'error', 'end'].forEach((e) => {
|
||||
client.on(e, once(() => setImmediate(callback)));
|
||||
});
|
||||
}
|
||||
|
||||
function fillHeaders(headers, currentSize, valid = false) {
|
||||
// `llhttp` counts actual header name/value sizes, excluding the whitespace
|
||||
// and stripped chars.
|
||||
// OK, Content-Length, 0, X-CRASH, aaa...
|
||||
headers += 'a'.repeat(MAX - currentSize);
|
||||
|
||||
// Generate valid headers
|
||||
if (valid) {
|
||||
headers = headers.slice(0, -1);
|
||||
}
|
||||
return headers + '\r\n\r\n';
|
||||
}
|
||||
|
||||
function writeHeaders(socket, headers) {
|
||||
const array = [];
|
||||
const chunkSize = 100;
|
||||
let last = 0;
|
||||
|
||||
for (let i = 0; i < headers.length / chunkSize; i++) {
|
||||
const current = (i + 1) * chunkSize;
|
||||
array.push(headers.slice(last, current));
|
||||
last = current;
|
||||
}
|
||||
|
||||
// Safety check we are chunking correctly
|
||||
assert.strictEqual(array.join(''), headers);
|
||||
|
||||
next();
|
||||
|
||||
function next() {
|
||||
if (socket.destroyed) {
|
||||
console.log('socket was destroyed early, data left to write:',
|
||||
array.join('').length);
|
||||
return;
|
||||
}
|
||||
|
||||
const chunk = array.shift();
|
||||
|
||||
if (chunk) {
|
||||
console.log('writing chunk of size', chunk.length);
|
||||
socket.write(chunk, next);
|
||||
} else {
|
||||
socket.end();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
function test1() {
|
||||
console.log('test1');
|
||||
let headers =
|
||||
'HTTP/1.1 200 OK\r\n' +
|
||||
'Content-Length: 0\r\n' +
|
||||
'X-CRASH: ';
|
||||
|
||||
// OK, Content-Length, 0, X-CRASH, aaa...
|
||||
const currentSize = 2 + 14 + 1 + 7;
|
||||
headers = fillHeaders(headers, currentSize);
|
||||
|
||||
const server = net.createServer((sock) => {
|
||||
sock.once('data', () => {
|
||||
writeHeaders(sock, headers);
|
||||
sock.resume();
|
||||
});
|
||||
|
||||
// The socket might error but that's ok
|
||||
sock.on('error', () => {});
|
||||
});
|
||||
|
||||
server.listen(0, common.mustCall(() => {
|
||||
const port = server.address().port;
|
||||
const client = http.get({ port: port }, common.mustNotCall());
|
||||
|
||||
client.on('error', common.mustCall((err) => {
|
||||
assert.strictEqual(err.code, 'HPE_HEADER_OVERFLOW');
|
||||
server.close(test2);
|
||||
}));
|
||||
}));
|
||||
}
|
||||
|
||||
const test2 = common.mustCall(() => {
|
||||
console.log('test2');
|
||||
let headers =
|
||||
'GET / HTTP/1.1\r\n' +
|
||||
'Host: localhost\r\n' +
|
||||
'Agent: nod2\r\n' +
|
||||
'X-CRASH: ';
|
||||
|
||||
// /, Host, localhost, Agent, node, X-CRASH, a...
|
||||
const currentSize = 1 + 4 + 9 + 5 + 4 + 7;
|
||||
headers = fillHeaders(headers, currentSize);
|
||||
|
||||
const server = http.createServer(common.mustNotCall());
|
||||
|
||||
server.once('clientError', common.mustCall((err) => {
|
||||
assert.strictEqual(err.code, 'HPE_HEADER_OVERFLOW');
|
||||
}));
|
||||
|
||||
server.listen(0, common.mustCall(() => {
|
||||
const client = net.connect(server.address().port);
|
||||
client.on('connect', () => {
|
||||
writeHeaders(client, headers);
|
||||
client.resume();
|
||||
});
|
||||
|
||||
finished(client, common.mustCall(() => {
|
||||
server.close(test3);
|
||||
}));
|
||||
}));
|
||||
});
|
||||
|
||||
const test3 = common.mustCall(() => {
|
||||
console.log('test3');
|
||||
let headers =
|
||||
'GET / HTTP/1.1\r\n' +
|
||||
'Host: localhost\r\n' +
|
||||
'Agent: nod3\r\n' +
|
||||
'X-CRASH: ';
|
||||
|
||||
// /, Host, localhost, Agent, node, X-CRASH, a...
|
||||
const currentSize = 1 + 4 + 9 + 5 + 4 + 7;
|
||||
headers = fillHeaders(headers, currentSize, true);
|
||||
|
||||
console.log('writing', headers.length);
|
||||
|
||||
const server = http.createServer(common.mustCall((req, res) => {
|
||||
res.end('hello from test3 server');
|
||||
server.close();
|
||||
}));
|
||||
|
||||
server.on('clientError', (err) => {
|
||||
console.log(err.code);
|
||||
if (err.code === 'HPE_HEADER_OVERFLOW') {
|
||||
console.log(err.rawPacket.toString('hex'));
|
||||
}
|
||||
});
|
||||
server.on('clientError', common.mustNotCall());
|
||||
|
||||
server.listen(0, common.mustCall(() => {
|
||||
const client = net.connect(server.address().port);
|
||||
client.on('connect', () => {
|
||||
writeHeaders(client, headers);
|
||||
client.resume();
|
||||
});
|
||||
|
||||
client.pipe(process.stdout);
|
||||
}));
|
||||
});
|
||||
|
||||
test1();
|
||||
28
test/js/node/test/parallel/test-http-parser-finish-error.js
Normal file
28
test/js/node/test/parallel/test-http-parser-finish-error.js
Normal file
@@ -0,0 +1,28 @@
|
||||
'use strict';
|
||||
|
||||
const common = require('../common');
|
||||
const net = require('net');
|
||||
const http = require('http');
|
||||
const assert = require('assert');
|
||||
|
||||
const str = 'GET / HTTP/1.1\r\n' +
|
||||
'Content-Length:';
|
||||
|
||||
|
||||
const server = http.createServer(common.mustNotCall());
|
||||
|
||||
server.on('clientError', common.mustCall((err, socket) => {
|
||||
assert.match(err.message, /^Parse Error/);
|
||||
assert.strictEqual(err.code, 'HPE_INVALID_EOF_STATE');
|
||||
socket.destroy();
|
||||
}, 1));
|
||||
server.listen(0, () => {
|
||||
const client = net.connect({ port: server.address().port }, () => {
|
||||
client.on('data', common.mustNotCall());
|
||||
client.on('end', common.mustCall(() => {
|
||||
server.close();
|
||||
}));
|
||||
client.write(str);
|
||||
client.end();
|
||||
});
|
||||
});
|
||||
45
test/js/node/test/parallel/test-http-server-client-error.js
Normal file
45
test/js/node/test/parallel/test-http-server-client-error.js
Normal file
@@ -0,0 +1,45 @@
|
||||
'use strict';
|
||||
const common = require('../common');
|
||||
const assert = require('assert');
|
||||
|
||||
const http = require('http');
|
||||
const net = require('net');
|
||||
|
||||
const server = http.createServer(common.mustCall(function(req, res) {
|
||||
res.end();
|
||||
}));
|
||||
|
||||
server.on('clientError', common.mustCall(function(err, socket) {
|
||||
assert.strictEqual(err instanceof Error, true);
|
||||
assert.strictEqual(err.code, 'HPE_INVALID_METHOD');
|
||||
assert.strictEqual(err.bytesParsed, 1);
|
||||
assert.strictEqual(err.message, 'Parse Error: Invalid method encountered');
|
||||
assert.strictEqual(err.rawPacket.toString(), 'Oopsie-doopsie\r\n');
|
||||
|
||||
socket.end('HTTP/1.1 400 Bad Request\r\n\r\n');
|
||||
|
||||
server.close();
|
||||
}));
|
||||
|
||||
server.listen(0, function() {
|
||||
function next() {
|
||||
// Invalid request
|
||||
const client = net.connect(server.address().port);
|
||||
client.end('Oopsie-doopsie\r\n');
|
||||
|
||||
let chunks = '';
|
||||
client.on('data', function(chunk) {
|
||||
chunks += chunk;
|
||||
});
|
||||
client.once('end', function() {
|
||||
assert.strictEqual(chunks, 'HTTP/1.1 400 Bad Request\r\n\r\n');
|
||||
});
|
||||
}
|
||||
|
||||
// Normal request
|
||||
http.get({ port: this.address().port, path: '/' }, function(res) {
|
||||
assert.strictEqual(res.statusCode, 200);
|
||||
res.resume();
|
||||
res.once('end', next);
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,47 @@
|
||||
'use strict';
|
||||
|
||||
const { expectsError, mustCall } = require('../common');
|
||||
|
||||
// Test that the request socket is destroyed if the `'clientError'` event is
|
||||
// emitted and there is no listener for it.
|
||||
|
||||
const assert = require('assert');
|
||||
const { createServer } = require('http');
|
||||
const { createConnection } = require('net');
|
||||
|
||||
const server = createServer();
|
||||
|
||||
server.on('connection', mustCall((socket) => {
|
||||
socket.on('error', expectsError({
|
||||
name: 'Error',
|
||||
message: 'Parse Error: Invalid method encountered',
|
||||
code: 'HPE_INVALID_METHOD',
|
||||
bytesParsed: 1,
|
||||
rawPacket: Buffer.from('FOO /\r\n')
|
||||
}));
|
||||
}));
|
||||
|
||||
server.listen(0, () => {
|
||||
const chunks = [];
|
||||
const socket = createConnection({
|
||||
allowHalfOpen: true,
|
||||
port: server.address().port
|
||||
});
|
||||
|
||||
socket.on('connect', mustCall(() => {
|
||||
socket.write('FOO /\r\n');
|
||||
}));
|
||||
|
||||
socket.on('data', (chunk) => {
|
||||
chunks.push(chunk);
|
||||
});
|
||||
|
||||
socket.on('end', mustCall(() => {
|
||||
const expected = Buffer.from(
|
||||
'HTTP/1.1 400 Bad Request\r\nConnection: close\r\n\r\n'
|
||||
);
|
||||
assert(Buffer.concat(chunks).equals(expected));
|
||||
|
||||
server.close();
|
||||
}));
|
||||
});
|
||||
@@ -0,0 +1,59 @@
|
||||
'use strict';
|
||||
const common = require('../common');
|
||||
|
||||
// Test that the `'clientError'` event can be emitted multiple times even if the
|
||||
// socket is correctly destroyed and that no warning is raised.
|
||||
|
||||
const assert = require('assert');
|
||||
const http = require('http');
|
||||
const net = require('net');
|
||||
|
||||
process.on('warning', common.mustNotCall());
|
||||
|
||||
function socketListener(socket) {
|
||||
const firstByte = socket.read(1);
|
||||
if (firstByte === null) {
|
||||
socket.once('readable', () => {
|
||||
socketListener(socket);
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
socket.unshift(firstByte);
|
||||
httpServer.emit('connection', socket);
|
||||
}
|
||||
|
||||
const netServer = net.createServer(socketListener);
|
||||
const httpServer = http.createServer(common.mustNotCall());
|
||||
|
||||
httpServer.once('clientError', common.mustCall((err, socket) => {
|
||||
assert.strictEqual(err.code, 'HPE_INVALID_METHOD');
|
||||
assert.strictEqual(err.rawPacket.toString(), '1');
|
||||
socket.destroy();
|
||||
|
||||
httpServer.once('clientError', common.mustCall((err) => {
|
||||
assert.strictEqual(err.code, 'HPE_INVALID_METHOD');
|
||||
assert.strictEqual(
|
||||
err.rawPacket.toString(),
|
||||
'23 http://example.com HTTP/1.1\r\n\r\n'
|
||||
);
|
||||
}));
|
||||
}));
|
||||
|
||||
netServer.listen(0, common.mustCall(() => {
|
||||
const socket = net.createConnection(netServer.address().port);
|
||||
|
||||
socket.on('connect', common.mustCall(() => {
|
||||
// Note: do not use letters here for the method.
|
||||
// There is a very small chance that a method with that initial
|
||||
// might be added in the future and thus this test might fail.
|
||||
// Numbers will likely never have this issue.
|
||||
socket.end('123 http://example.com HTTP/1.1\r\n\r\n');
|
||||
}));
|
||||
|
||||
socket.on('close', () => {
|
||||
netServer.close();
|
||||
});
|
||||
|
||||
socket.resume();
|
||||
}));
|
||||
@@ -0,0 +1,29 @@
|
||||
'use strict';
|
||||
|
||||
const common = require('../common');
|
||||
const http = require('http');
|
||||
const net = require('net');
|
||||
const assert = require('assert');
|
||||
|
||||
const reqstr = 'POST / HTTP/1.1\r\n' +
|
||||
'Content-Length: 1\r\n' +
|
||||
'Transfer-Encoding: chunked\r\n\r\n';
|
||||
|
||||
const server = http.createServer(common.mustNotCall());
|
||||
server.on('clientError', common.mustCall((err) => {
|
||||
assert.match(err.message, /^Parse Error/);
|
||||
assert.strictEqual(err.code, 'HPE_INVALID_TRANSFER_ENCODING');
|
||||
server.close();
|
||||
}));
|
||||
server.listen(0, () => {
|
||||
const client = net.connect({ port: server.address().port }, () => {
|
||||
client.write(reqstr);
|
||||
client.end();
|
||||
});
|
||||
client.on('data', (data) => {
|
||||
// Should not get to this point because the server should simply
|
||||
// close the connection without returning any data.
|
||||
assert.fail('no data should be returned by the server');
|
||||
});
|
||||
client.on('end', common.mustCall());
|
||||
});
|
||||
@@ -0,0 +1,29 @@
|
||||
'use strict';
|
||||
|
||||
const common = require('../common');
|
||||
const net = require('net');
|
||||
const http = require('http');
|
||||
const assert = require('assert');
|
||||
|
||||
const str = 'GET / HTTP/1.1\r\n' +
|
||||
'Dummy: Header\r' +
|
||||
'Content-Length: 1\r\n' +
|
||||
'\r\n';
|
||||
|
||||
|
||||
const server = http.createServer(common.mustNotCall());
|
||||
server.on('clientError', common.mustCall((err) => {
|
||||
assert.match(err.message, /^Parse Error/);
|
||||
assert.strictEqual(err.code, 'HPE_LF_EXPECTED');
|
||||
server.close();
|
||||
}));
|
||||
server.listen(0, () => {
|
||||
const client = net.connect({ port: server.address().port }, () => {
|
||||
client.on('data', common.mustNotCall());
|
||||
client.on('end', common.mustCall(() => {
|
||||
server.close();
|
||||
}));
|
||||
client.write(str);
|
||||
client.end();
|
||||
});
|
||||
});
|
||||
@@ -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();
|
||||
});
|
||||
});
|
||||
}
|
||||
@@ -0,0 +1,61 @@
|
||||
// Copyright Joyent, Inc. and other Node contributors.
|
||||
//
|
||||
// Permission is hereby granted, free of charge, to any person obtaining a
|
||||
// copy of this software and associated documentation files (the
|
||||
// "Software"), to deal in the Software without restriction, including
|
||||
// without limitation the rights to use, copy, modify, merge, publish,
|
||||
// distribute, sublicense, and/or sell copies of the Software, and to permit
|
||||
// persons to whom the Software is furnished to do so, subject to the
|
||||
// following conditions:
|
||||
//
|
||||
// The above copyright notice and this permission notice shall be included
|
||||
// in all copies or substantial portions of the Software.
|
||||
//
|
||||
// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS
|
||||
// OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
|
||||
// MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN
|
||||
// NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM,
|
||||
// DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR
|
||||
// OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE
|
||||
// USE OR OTHER DEALINGS IN THE SOFTWARE.
|
||||
|
||||
'use strict';
|
||||
const common = require('../common');
|
||||
if (!common.hasCrypto)
|
||||
common.skip('missing crypto');
|
||||
const { readKey } = require('../common/fixtures');
|
||||
|
||||
const assert = require('assert');
|
||||
const https = require('https');
|
||||
const url = require('url');
|
||||
|
||||
// https options
|
||||
const httpsOptions = {
|
||||
key: readKey('agent1-key.pem'),
|
||||
cert: readKey('agent1-cert.pem')
|
||||
};
|
||||
|
||||
function check(request) {
|
||||
// Assert that I'm https
|
||||
assert.ok(request.socket._secureEstablished);
|
||||
}
|
||||
|
||||
const server = https.createServer(httpsOptions, function(request, response) {
|
||||
// Run the check function
|
||||
check(request);
|
||||
response.writeHead(200, {});
|
||||
response.end('ok');
|
||||
server.close();
|
||||
});
|
||||
|
||||
server.listen(0, function() {
|
||||
const testURL = url.parse(`https://localhost:${this.address().port}`);
|
||||
testURL.rejectUnauthorized = false;
|
||||
|
||||
// make the request
|
||||
const clientRequest = https.request(testURL);
|
||||
// Since there is a little magic with the agent
|
||||
// make sure that the request uses the https.Agent
|
||||
assert.ok(clientRequest.agent instanceof https.Agent);
|
||||
clientRequest.end();
|
||||
});
|
||||
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,134 @@
|
||||
'use strict';
|
||||
const common = require('../common');
|
||||
if (!common.hasCrypto) {
|
||||
common.skip('missing crypto');
|
||||
}
|
||||
|
||||
const fixtures = require('../common/fixtures');
|
||||
const assert = require('assert');
|
||||
const https = require('https');
|
||||
const tls = require('tls');
|
||||
const { finished, duplexPair } = require('stream');
|
||||
|
||||
const certFixture = {
|
||||
key: fixtures.readKey('agent1-key.pem'),
|
||||
cert: fixtures.readKey('agent1-cert.pem'),
|
||||
ca: fixtures.readKey('ca1-cert.pem'),
|
||||
};
|
||||
|
||||
|
||||
// Test that setting the `insecureHTTPParse` option works on a per-stream-basis.
|
||||
|
||||
// Test 1: The server sends an invalid header.
|
||||
{
|
||||
const [ clientSide, serverSide ] = duplexPair();
|
||||
|
||||
const req = https.request({
|
||||
rejectUnauthorized: false,
|
||||
createConnection: common.mustCall(() => clientSide),
|
||||
insecureHTTPParser: true
|
||||
}, common.mustCall((res) => {
|
||||
assert.strictEqual(res.headers.hello, 'foo\x08foo');
|
||||
res.resume(); // We don’t actually care about contents.
|
||||
res.on('end', common.mustCall());
|
||||
}));
|
||||
req.end();
|
||||
|
||||
serverSide.resume(); // Dump the request
|
||||
serverSide.end('HTTP/1.1 200 OK\r\n' +
|
||||
'Host: example.com\r\n' +
|
||||
'Hello: foo\x08foo\r\n' +
|
||||
'Content-Length: 0\r\n' +
|
||||
'\r\n\r\n');
|
||||
}
|
||||
|
||||
// Test 2: The same as Test 1 except without the option, to make sure it fails.
|
||||
{
|
||||
const [ clientSide, serverSide ] = duplexPair();
|
||||
|
||||
const req = https.request({
|
||||
rejectUnauthorized: false,
|
||||
createConnection: common.mustCall(() => clientSide)
|
||||
}, common.mustNotCall());
|
||||
req.end();
|
||||
req.on('error', common.mustCall());
|
||||
|
||||
serverSide.resume(); // Dump the request
|
||||
serverSide.end('HTTP/1.1 200 OK\r\n' +
|
||||
'Host: example.com\r\n' +
|
||||
'Hello: foo\x08foo\r\n' +
|
||||
'Content-Length: 0\r\n' +
|
||||
'\r\n\r\n');
|
||||
}
|
||||
|
||||
// Test 3: The client sends an invalid header.
|
||||
{
|
||||
const testData = 'Hello, World!\n';
|
||||
const server = https.createServer(
|
||||
{ insecureHTTPParser: true,
|
||||
...certFixture },
|
||||
common.mustCall((req, res) => {
|
||||
res.statusCode = 200;
|
||||
res.setHeader('Content-Type', 'text/plain');
|
||||
res.end(testData);
|
||||
}));
|
||||
|
||||
server.on('clientError', common.mustNotCall());
|
||||
|
||||
server.listen(0, common.mustCall(() => {
|
||||
const client = tls.connect({
|
||||
port: server.address().port,
|
||||
rejectUnauthorized: false
|
||||
});
|
||||
client.write(
|
||||
'GET / HTTP/1.1\r\n' +
|
||||
'Host: example.com\r\n' +
|
||||
'Hello: foo\x08foo\r\n' +
|
||||
'\r\n\r\n');
|
||||
client.end();
|
||||
|
||||
client.on('data', () => {});
|
||||
finished(client, common.mustCall(() => {
|
||||
server.close();
|
||||
}));
|
||||
}));
|
||||
}
|
||||
|
||||
// Test 4: The same as Test 3 except without the option, to make sure it fails.
|
||||
{
|
||||
const server = https.createServer(
|
||||
{ ...certFixture },
|
||||
common.mustNotCall());
|
||||
|
||||
server.on('clientError', common.mustCall());
|
||||
|
||||
server.listen(0, common.mustCall(() => {
|
||||
const client = tls.connect({
|
||||
port: server.address().port,
|
||||
rejectUnauthorized: false
|
||||
});
|
||||
client.write(
|
||||
'GET / HTTP/1.1\r\n' +
|
||||
'Host: example.com\r\n' +
|
||||
'Hello: foo\x08foo\r\n' +
|
||||
'\r\n\r\n');
|
||||
client.end();
|
||||
|
||||
client.on('data', () => {});
|
||||
finished(client, common.mustCall(() => {
|
||||
server.close();
|
||||
}));
|
||||
}));
|
||||
}
|
||||
|
||||
// Test 5: Invalid argument type
|
||||
{
|
||||
assert.throws(
|
||||
() => https.request({ insecureHTTPParser: 0 }, common.mustNotCall()),
|
||||
common.expectsError({
|
||||
code: 'ERR_INVALID_ARG_TYPE',
|
||||
message: 'The "options.insecureHTTPParser" property must be of' +
|
||||
' type boolean. Received type number (0)'
|
||||
})
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,122 @@
|
||||
'use strict';
|
||||
const common = require('../common');
|
||||
|
||||
if (!common.hasCrypto) {
|
||||
common.skip('missing crypto');
|
||||
}
|
||||
|
||||
const fixtures = require('../common/fixtures');
|
||||
const assert = require('assert');
|
||||
const https = require('https');
|
||||
const http = require('http');
|
||||
const tls = require('tls');
|
||||
const { finished, duplexPair } = require('stream');
|
||||
|
||||
const certFixture = {
|
||||
key: fixtures.readKey('agent1-key.pem'),
|
||||
cert: fixtures.readKey('agent1-cert.pem'),
|
||||
ca: fixtures.readKey('ca1-cert.pem'),
|
||||
};
|
||||
|
||||
|
||||
// Test that setting the `maxHeaderSize` option works on a per-stream-basis.
|
||||
|
||||
// Test 1: The server sends larger headers than what would otherwise be allowed.
|
||||
{
|
||||
const [ clientSide, serverSide ] = duplexPair();
|
||||
|
||||
const req = https.request({
|
||||
createConnection: common.mustCall(() => clientSide),
|
||||
maxHeaderSize: http.maxHeaderSize * 4
|
||||
}, common.mustCall((res) => {
|
||||
assert.strictEqual(res.headers.hello, 'A'.repeat(http.maxHeaderSize * 3));
|
||||
res.resume(); // We don’t actually care about contents.
|
||||
res.on('end', common.mustCall());
|
||||
}));
|
||||
req.end();
|
||||
|
||||
serverSide.resume(); // Dump the request
|
||||
serverSide.end('HTTP/1.1 200 OK\r\n' +
|
||||
'Host: example.com\r\n' +
|
||||
'Hello: ' + 'A'.repeat(http.maxHeaderSize * 3) + '\r\n' +
|
||||
'Content-Length: 0\r\n' +
|
||||
'\r\n\r\n');
|
||||
}
|
||||
|
||||
// Test 2: The same as Test 1 except without the option, to make sure it fails.
|
||||
{
|
||||
const [ clientSide, serverSide ] = duplexPair();
|
||||
|
||||
const req = https.request({
|
||||
createConnection: common.mustCall(() => clientSide)
|
||||
}, common.mustNotCall());
|
||||
req.end();
|
||||
req.on('error', common.mustCall());
|
||||
|
||||
serverSide.resume(); // Dump the request
|
||||
serverSide.end('HTTP/1.1 200 OK\r\n' +
|
||||
'Host: example.com\r\n' +
|
||||
'Hello: ' + 'A'.repeat(http.maxHeaderSize * 3) + '\r\n' +
|
||||
'Content-Length: 0\r\n' +
|
||||
'\r\n\r\n');
|
||||
}
|
||||
|
||||
// Test 3: The client sends larger headers than what would otherwise be allowed.
|
||||
{
|
||||
const testData = 'Hello, World!\n';
|
||||
const server = https.createServer(
|
||||
{ maxHeaderSize: http.maxHeaderSize * 4,
|
||||
...certFixture },
|
||||
common.mustCall((req, res) => {
|
||||
res.statusCode = 200;
|
||||
res.setHeader('Content-Type', 'text/plain');
|
||||
res.end(testData);
|
||||
}));
|
||||
|
||||
server.on('clientError', common.mustNotCall());
|
||||
|
||||
server.listen(0, common.mustCall(() => {
|
||||
const client = tls.connect({
|
||||
port: server.address().port,
|
||||
rejectUnauthorized: false
|
||||
});
|
||||
client.write(
|
||||
'GET / HTTP/1.1\r\n' +
|
||||
'Host: example.com\r\n' +
|
||||
'Hello: ' + 'A'.repeat(http.maxHeaderSize * 3) + '\r\n' +
|
||||
'\r\n\r\n');
|
||||
client.end();
|
||||
|
||||
client.on('data', () => {});
|
||||
finished(client, common.mustCall(() => {
|
||||
server.close();
|
||||
}));
|
||||
}));
|
||||
}
|
||||
|
||||
// Test 4: The same as Test 3 except without the option, to make sure it fails.
|
||||
{
|
||||
const server = https.createServer({ ...certFixture }, common.mustNotCall());
|
||||
|
||||
// clientError may be emitted multiple times when header is larger than
|
||||
// maxHeaderSize.
|
||||
server.on('clientError', common.mustCallAtLeast(1));
|
||||
|
||||
server.listen(0, common.mustCall(() => {
|
||||
const client = tls.connect({
|
||||
port: server.address().port,
|
||||
rejectUnauthorized: false
|
||||
});
|
||||
client.write(
|
||||
'GET / HTTP/1.1\r\n' +
|
||||
'Host: example.com\r\n' +
|
||||
'Hello: ' + 'A'.repeat(http.maxHeaderSize * 3) + '\r\n' +
|
||||
'\r\n\r\n');
|
||||
client.end();
|
||||
|
||||
client.on('data', () => {});
|
||||
finished(client, common.mustCall(() => {
|
||||
server.close();
|
||||
}));
|
||||
}));
|
||||
}
|
||||
@@ -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);
|
||||
}));
|
||||
}));
|
||||
54
test/js/node/test/parallel/test-https-timeout-server.js
Normal file
54
test/js/node/test/parallel/test-https-timeout-server.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');
|
||||
|
||||
if (!common.hasCrypto)
|
||||
common.skip('missing crypto');
|
||||
|
||||
const fixtures = require('../common/fixtures');
|
||||
|
||||
const assert = require('assert');
|
||||
const https = require('https');
|
||||
|
||||
const net = require('net');
|
||||
|
||||
const options = {
|
||||
key: fixtures.readKey('agent1-key.pem'),
|
||||
cert: fixtures.readKey('agent1-cert.pem'),
|
||||
handshakeTimeout: 50
|
||||
};
|
||||
|
||||
const server = https.createServer(options, common.mustNotCall());
|
||||
|
||||
server.on('clientError', common.mustCall(function(err, conn) {
|
||||
// Don't hesitate to update the asserts if the internal structure of
|
||||
// the cleartext object ever changes. We're checking that the https.Server
|
||||
// has closed the client connection.
|
||||
assert.strictEqual(conn._secureEstablished, false);
|
||||
server.close();
|
||||
conn.destroy();
|
||||
}));
|
||||
|
||||
server.listen(0, function() {
|
||||
net.connect({ host: '127.0.0.1', port: this.address().port });
|
||||
});
|
||||
Reference in New Issue
Block a user