Compare commits

...

4 Commits

Author SHA1 Message Date
Ciro Spaciari
8aebcf6376 Merge branch 'main' into jarred/micro-http 2025-04-05 11:02:47 -07:00
Jarred Sumner
60fba75d4d Skip this for now 2025-04-05 02:45:02 -07:00
Jarred Sumner
2d483255f1 Micro-optimize 2025-04-05 02:42:56 -07:00
Jarred Sumner
b066ca5f37 Fix performance regression from #18599 2025-04-05 02:42:20 -07:00
12 changed files with 396 additions and 376 deletions

View File

@@ -74,6 +74,9 @@ enum ExceptionCode {
InvalidThisError,
InvalidURLError,
InvalidHTTPTokenError,
InvalidHTTPHeaderValueError,
InvalidHttpCharacterError,
};
} // namespace WebCore
@@ -121,7 +124,10 @@ template<> struct EnumTraits<WebCore::ExceptionCode> {
WebCore::ExceptionCode::StackOverflowError,
WebCore::ExceptionCode::ExistingExceptionError,
WebCore::ExceptionCode::InvalidThisError,
WebCore::ExceptionCode::InvalidURLError>;
WebCore::ExceptionCode::InvalidURLError,
WebCore::ExceptionCode::InvalidHTTPTokenError,
WebCore::ExceptionCode::InvalidHTTPHeaderValueError,
WebCore::ExceptionCode::InvalidHttpCharacterError>;
};
} // namespace WTF

View File

@@ -182,6 +182,15 @@ JSValue createDOMException(JSGlobalObject* lexicalGlobalObject, ExceptionCode ec
case ExceptionCode::InvalidURLError:
return Bun::createError(lexicalGlobalObject, Bun::ErrorCode::ERR_INVALID_URL, message.isEmpty() ? "Invalid URL"_s : message);
case ExceptionCode::InvalidHTTPTokenError:
return Bun::createError(lexicalGlobalObject, Bun::ErrorCode::ERR_INVALID_HTTP_TOKEN, message.isEmpty() ? "Invalid HTTP token"_s : message);
case ExceptionCode::InvalidHTTPHeaderValueError:
return Bun::createError(lexicalGlobalObject, Bun::ErrorCode::ERR_HTTP_INVALID_HEADER_VALUE, message.isEmpty() ? "Invalid HTTP header value"_s : message);
case ExceptionCode::InvalidHttpCharacterError:
return Bun::createError(lexicalGlobalObject, Bun::ErrorCode::ERR_INVALID_CHAR, message.isEmpty() ? "Invalid HTTP character"_s : message);
default: {
// FIXME: All callers to createDOMException need to pass in the correct global object.
// For now, we're going to assume the lexicalGlobalObject. Which is wrong in cases like this:

View File

@@ -21,7 +21,7 @@
#include <JavaScriptCore/LazyPropertyInlines.h>
#include <JavaScriptCore/VMTrapsInlines.h>
#include "JSSocketAddressDTO.h"
#include "ErrorCode.h"
extern "C" uint64_t uws_res_get_remote_address_info(void* res, const char** dest, int* port, bool* is_ipv6);
extern "C" uint64_t uws_res_get_local_address_info(void* res, const char** dest, int* port, bool* is_ipv6);
@@ -59,7 +59,7 @@ static const HashTableValue JSNodeHTTPServerSocketPrototypeTableValues[] = {
{ "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 } },
};
// TODO: move this into a new file.
class JSNodeHTTPServerSocketPrototype final : public JSC::JSNonFinalObject {
public:
using Base = JSC::JSNonFinalObject;
@@ -98,6 +98,7 @@ private:
}
};
// TODO: move this into a new file.
class JSNodeHTTPServerSocket : public JSC::JSDestructibleObject {
public:
using Base = JSC::JSDestructibleObject;
@@ -672,6 +673,7 @@ static void assignHeadersFromUWebSocketsForCall(uWS::HttpRequest* request, Marke
MarkedArgumentBuffer arrayValues;
args.append(headersObject);
HTTPHeaderIdentifiers& identifiers = WebCore::clientData(vm)->httpHeaderIdentifiers();
for (auto it = request->begin(); it != request->end(); ++it) {
auto pair = *it;
@@ -685,7 +687,6 @@ static void assignHeadersFromUWebSocketsForCall(uWS::HttpRequest* request, Marke
JSString* jsValue = jsString(vm, value);
HTTPHeaderIdentifiers& identifiers = WebCore::clientData(vm)->httpHeaderIdentifiers();
Identifier nameIdentifier;
JSString* nameString = nullptr;
@@ -1365,6 +1366,34 @@ JSC_DEFINE_HOST_FUNCTION(jsHTTPGetHeader, (JSGlobalObject * globalObject, CallFr
return JSValue::encode(jsUndefined());
}
template<typename CharacterType>
static bool validateNodeJSHeaderNameSpan(const std::span<const CharacterType> name)
{
// This is the equivalent of checkIsHttpToken from internal/validators.js
// The regex is /^[\^_`a-zA-Z\-0-9!#$%&'*+.|~]+$/
for (const auto& c : name) {
if (!(c == '^' || c == '_' || c == '`' || (c >= 'a' && c <= 'z') || (c >= 'A' && c <= 'Z') || c == '-' || (c >= '0' && c <= '9') || c == '!' || c == '#' || c == '$' || c == '%' || c == '&' || c == '\'' || c == '*' || c == '+' || c == '.' || c == '|' || c == '~')) {
return false;
}
}
return true;
}
static bool isValidNodeJSHeaderName(const String& name)
{
if (name.isEmpty()) {
return false;
}
if (name.is8Bit()) {
return validateNodeJSHeaderNameSpan(name.span8());
}
return validateNodeJSHeaderNameSpan(name.span16());
}
JSC_DEFINE_HOST_FUNCTION(jsHTTPSetHeader, (JSGlobalObject * globalObject, CallFrame* callFrame))
{
auto& vm = JSC::getVM(globalObject);
@@ -1380,12 +1409,39 @@ JSC_DEFINE_HOST_FUNCTION(jsHTTPSetHeader, (JSGlobalObject * globalObject, CallFr
String name = nameValue.toWTFString(globalObject);
RETURN_IF_EXCEPTION(scope, {});
const auto validateHeaderName = [&]() -> bool {
if (!isValidNodeJSHeaderName(name)) {
scope.throwException(globalObject, createError(globalObject, ErrorCode::ERR_INVALID_HTTP_TOKEN, makeString("Invalid header name: "_s, name)));
return false;
}
return true;
};
FetchHeaders* impl = &headers->wrapped();
if (valueValue.isUndefined())
return JSValue::encode(jsUndefined());
const auto setHeaderValue = [&](const String& value) -> bool {
HTTPHeaderName commonName;
if (findHTTPHeaderName(name, commonName)) {
// It's already valid, don't validate the header name twice!
impl->set(commonName, value);
} else {
if (UNLIKELY(!validateHeaderName())) {
return false;
}
impl->setUncommonName(name, value);
}
return true;
};
if (isArray(globalObject, valueValue)) {
if (UNLIKELY(!validateHeaderName())) {
return {};
}
auto* array = jsCast<JSArray*>(valueValue);
unsigned length = array->length();
if (length > 0) {
@@ -1395,7 +1451,9 @@ JSC_DEFINE_HOST_FUNCTION(jsHTTPSetHeader, (JSGlobalObject * globalObject, CallFr
auto value = item.toWTFString(globalObject);
RETURN_IF_EXCEPTION(scope, {});
impl->set(name, value);
if (UNLIKELY(!setHeaderValue(value))) {
return {};
}
RETURN_IF_EXCEPTION(scope, {});
}
for (unsigned i = 1; i < length; ++i) {
@@ -1413,9 +1471,10 @@ JSC_DEFINE_HOST_FUNCTION(jsHTTPSetHeader, (JSGlobalObject * globalObject, CallFr
auto value = valueValue.toWTFString(globalObject);
RETURN_IF_EXCEPTION(scope, {});
impl->set(name, value);
RETURN_IF_EXCEPTION(scope, {});
return JSValue::encode(jsUndefined());
if (UNLIKELY(!setHeaderValue(value))) {
return {};
}
}
}

View File

@@ -47,7 +47,7 @@ static ExceptionOr<bool> canWriteHeader(const HTTPHeaderName name, const String&
{
ASSERT(value.isEmpty() || (!isHTTPSpace(value[0]) && !isHTTPSpace(value[value.length() - 1])));
if (!isValidHTTPHeaderValue((value)))
return Exception { TypeError, makeString("Header '"_s, name, "' has invalid value: '"_s, value, "'"_s) };
return Exception { InvalidHttpCharacterError, makeString("Header '"_s, name, "' has invalid value: '"_s, value, "'"_s) };
if (guard == FetchHeaders::Guard::Immutable)
return Exception { TypeError, "Headers object's guard is 'immutable'"_s };
return true;
@@ -55,11 +55,11 @@ static ExceptionOr<bool> canWriteHeader(const HTTPHeaderName name, const String&
static ExceptionOr<bool> canWriteHeader(const String& name, const String& value, const String& combinedValue, FetchHeaders::Guard guard)
{
if (!isValidHTTPToken(name))
return Exception { TypeError, makeString("Invalid header name: '"_s, name, "'"_s) };
if (!isValidHTTPHeaderName(name))
return Exception { InvalidHTTPTokenError, makeString("header name be a valid HTTP token [\""_s, name, "\"]"_s) };
ASSERT(value.isEmpty() || (!isHTTPSpace(value[0]) && !isHTTPSpace(value[value.length() - 1])));
if (!isValidHTTPHeaderValue((value)))
return Exception { TypeError, makeString("Header '"_s, name, "' has invalid value: '"_s, value, "'"_s) };
return Exception { InvalidHttpCharacterError, makeString("Header '"_s, name, "' has invalid value: '"_s, value, "'"_s) };
if (guard == FetchHeaders::Guard::Immutable)
return Exception { TypeError, "Headers object's guard is 'immutable'"_s };
return true;
@@ -216,8 +216,8 @@ ExceptionOr<void> FetchHeaders::remove(const StringView name)
return {};
}
if (!isValidHTTPToken(name))
return Exception { TypeError, makeString("Invalid header name: '"_s, name, "'"_s) };
if (!isValidHTTPHeaderName(name))
return Exception { InvalidHTTPTokenError, makeString("header name be a valid HTTP token [\""_s, name, "\"]"_s) };
++m_updateCounter;
m_headers.removeUncommonHeader(name);
@@ -234,8 +234,8 @@ ExceptionOr<String> FetchHeaders::get(const StringView name) const
{
auto result = m_headers.get(name);
if (result.isEmpty()) {
if (!isValidHTTPToken(name))
return Exception { TypeError, makeString("Invalid header name: '"_s, name, "'"_s) };
if (!isValidHTTPHeaderName(name))
return Exception { InvalidHTTPTokenError, makeString("header name be a valid HTTP token [\""_s, name, "\"]"_s) };
}
return result;
@@ -245,8 +245,8 @@ ExceptionOr<bool> FetchHeaders::has(const StringView name) const
{
bool has = m_headers.contains(name);
if (!has) {
if (!isValidHTTPToken(name))
return Exception { TypeError, makeString("Invalid header name: '"_s, name, '"') };
if (!isValidHTTPHeaderName(name))
return Exception { InvalidHTTPTokenError, makeString("header name be a valid HTTP token [\""_s, name, "\"]"_s) };
}
return has;
}
@@ -287,6 +287,24 @@ ExceptionOr<void> FetchHeaders::set(const String& name, const String& value)
return {};
}
ExceptionOr<void> FetchHeaders::setUncommonName(const String& name, const String& value)
{
String normalizedValue = value.trim(isHTTPSpace);
auto canWriteResult = canWriteHeader(name, normalizedValue, normalizedValue, m_guard);
if (canWriteResult.hasException())
return canWriteResult.releaseException();
if (!canWriteResult.releaseReturnValue())
return {};
++m_updateCounter;
m_headers.setUncommonHeader(name, normalizedValue);
if (m_guard == FetchHeaders::Guard::RequestNoCors)
removePrivilegedNoCORSRequestHeaders(m_headers);
return {};
}
void FetchHeaders::filterAndFill(const HTTPHeaderMap& headers, Guard guard)
{
for (auto& header : headers) {

View File

@@ -63,6 +63,7 @@ public:
ExceptionOr<String> get(const StringView) const;
ExceptionOr<bool> has(const StringView) const;
ExceptionOr<void> set(const String& name, const String& value);
ExceptionOr<void> setUncommonName(const String& name, const String& value);
ExceptionOr<void> set(const HTTPHeaderName name, const String& value);
ExceptionOr<void> fill(const Init&);

View File

@@ -1,226 +0,0 @@
/*
* Copyright (C) 2017 Apple Inc. All rights reserved.
*
* Redistribution and use in source and binary forms, with or without
* modification, are permitted provided that the following conditions
* are met:
* 1. Redistributions of source code must retain the above copyright
* notice, this list of conditions and the following disclaimer.
* 2. Redistributions in binary form must reproduce the above copyright
* notice, this list of conditions and the following disclaimer in the
* documentation and/or other materials provided with the distribution.
*
* THIS SOFTWARE IS PROVIDED BY APPLE INC. ``AS IS'' AND ANY
* EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
* IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR
* PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL APPLE INC. OR
* CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL,
* EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO,
* PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR
* PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY
* OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
* (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
* OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
*/
#include "config.h"
#include "HTTPHeaderField.h"
namespace WebCore {
namespace RFC7230 {
bool isTokenCharacter(UChar c)
{
return c < 0x80 && isTokenCharacter(static_cast<LChar>(c));
}
bool isDelimiter(UChar c)
{
return c < 0x80 && isDelimiter(static_cast<LChar>(c));
}
bool isTokenCharacter(LChar c)
{
return isASCIIAlpha(c) || isASCIIDigit(c)
|| c == '!' || c == '#' || c == '$'
|| c == '%' || c == '&' || c == '\''
|| c == '*' || c == '+' || c == '-'
|| c == '.' || c == '^' || c == '_'
|| c == '`' || c == '|' || c == '~';
}
bool isDelimiter(LChar c)
{
return c == '(' || c == ')' || c == ','
|| c == '/' || c == ':' || c == ';'
|| c == '<' || c == '=' || c == '>'
|| c == '?' || c == '@' || c == '['
|| c == '\\' || c == ']' || c == '{'
|| c == '}' || c == '"';
}
static bool isVisibleCharacter(UChar c)
{
return isTokenCharacter(c) || isDelimiter(c);
}
bool isWhitespace(UChar c)
{
return c == ' ' || c == '\t';
}
template<size_t min, size_t max>
static bool isInRange(UChar c)
{
return c >= min && c <= max;
}
static bool isOBSText(UChar c)
{
return isInRange<0x80, 0xFF>(c);
}
static bool isQuotedTextCharacter(UChar c)
{
return isWhitespace(c)
|| c == 0x21
|| isInRange<0x23, 0x5B>(c)
|| isInRange<0x5D, 0x7E>(c)
|| isOBSText(c);
}
bool isQuotedPairSecondOctet(UChar c)
{
return isWhitespace(c)
|| isVisibleCharacter(c)
|| isOBSText(c);
}
bool isCommentText(UChar c)
{
return isWhitespace(c)
|| isInRange<0x21, 0x27>(c)
|| isInRange<0x2A, 0x5B>(c)
|| isInRange<0x5D, 0x7E>(c)
|| isOBSText(c);
}
static bool isValidName(StringView name)
{
if (!name.length())
return false;
for (size_t i = 0; i < name.length(); ++i) {
if (!isTokenCharacter(name[i]))
return false;
}
return true;
}
static bool isValidValue(StringView value)
{
enum class State {
OptionalWhitespace,
Token,
QuotedString,
Comment,
};
State state = State::OptionalWhitespace;
size_t commentDepth = 0;
bool hadNonWhitespace = false;
for (size_t i = 0; i < value.length(); ++i) {
UChar c = value[i];
switch (state) {
case State::OptionalWhitespace:
if (isWhitespace(c))
continue;
hadNonWhitespace = true;
if (isTokenCharacter(c)) {
state = State::Token;
continue;
}
if (c == '"') {
state = State::QuotedString;
continue;
}
if (c == '(') {
ASSERT(!commentDepth);
++commentDepth;
state = State::Comment;
continue;
}
return false;
case State::Token:
if (isTokenCharacter(c))
continue;
state = State::OptionalWhitespace;
continue;
case State::QuotedString:
if (c == '"') {
state = State::OptionalWhitespace;
continue;
}
if (c == '\\') {
++i;
if (i == value.length())
return false;
if (!isQuotedPairSecondOctet(value[i]))
return false;
continue;
}
if (!isQuotedTextCharacter(c))
return false;
continue;
case State::Comment:
if (c == '(') {
++commentDepth;
continue;
}
if (c == ')') {
--commentDepth;
if (!commentDepth)
state = State::OptionalWhitespace;
continue;
}
if (c == '\\') {
++i;
if (i == value.length())
return false;
if (!isQuotedPairSecondOctet(value[i]))
return false;
continue;
}
if (!isCommentText(c))
return false;
continue;
}
}
switch (state) {
case State::OptionalWhitespace:
case State::Token:
return hadNonWhitespace;
case State::QuotedString:
case State::Comment:
// Unclosed comments or quotes are invalid values.
break;
}
return false;
}
} // namespace RFC7230
std::optional<HTTPHeaderField> HTTPHeaderField::create(String&& unparsedName, String&& unparsedValue)
{
auto trimmedName = StringView(unparsedName).trim(isTabOrSpace<UChar>);
auto trimmedValue = StringView(unparsedValue).trim(isTabOrSpace<UChar>);
if (!RFC7230::isValidName(trimmedName) || !RFC7230::isValidValue(trimmedValue))
return std::nullopt;
auto name = trimmedName.length() == unparsedName.length() ? WTFMove(unparsedName) : trimmedName.toString();
auto value = trimmedValue.length() == unparsedValue.length() ? WTFMove(unparsedValue) : trimmedValue.toString();
return { { WTFMove(name), WTFMove(value) } };
}
}

View File

@@ -1,85 +0,0 @@
/*
* Copyright (C) 2017 Apple Inc. All rights reserved.
*
* Redistribution and use in source and binary forms, with or without
* modification, are permitted provided that the following conditions
* are met:
* 1. Redistributions of source code must retain the above copyright
* notice, this list of conditions and the following disclaimer.
* 2. Redistributions in binary form must reproduce the above copyright
* notice, this list of conditions and the following disclaimer in the
* documentation and/or other materials provided with the distribution.
*
* THIS SOFTWARE IS PROVIDED BY APPLE INC. ``AS IS'' AND ANY
* EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
* IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR
* PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL APPLE INC. OR
* CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL,
* EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO,
* PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR
* PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY
* OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
* (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
* OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
*/
#pragma once
#include <wtf/text/WTFString.h>
namespace WebCore {
class WEBCORE_EXPORT HTTPHeaderField {
public:
static std::optional<HTTPHeaderField> create(String&& name, String&& value);
const String& name() const { return m_name; }
const String& value() const { return m_value; }
template<class Encoder> void encode(Encoder&) const;
template<class Decoder> static std::optional<HTTPHeaderField> decode(Decoder&);
private:
HTTPHeaderField(String&& name, String&& value)
: m_name(WTFMove(name))
, m_value(WTFMove(value))
{
}
String m_name;
String m_value;
};
template<class Encoder>
void HTTPHeaderField::encode(Encoder& encoder) const
{
encoder << m_name;
encoder << m_value;
}
template<class Decoder>
std::optional<HTTPHeaderField> HTTPHeaderField::decode(Decoder& decoder)
{
std::optional<String> name;
decoder >> name;
if (!name)
return std::nullopt;
std::optional<String> value;
decoder >> value;
if (!value)
return std::nullopt;
return { { WTFMove(*name), WTFMove(*value) } };
}
namespace RFC7230 {
bool isTokenCharacter(UChar);
bool isWhitespace(UChar);
bool isTokenCharacter(LChar);
bool isWhitespace(LChar);
bool isCommentText(UChar);
bool isQuotedPairSecondOctet(UChar);
bool isDelimiter(UChar);
} // namespace RFC7230
} // namespace WebCore

View File

@@ -33,7 +33,6 @@
#include "HTTPParsers.h"
#include "CommonAtomStrings.h"
#include "HTTPHeaderField.h"
#include "HTTPHeaderNames.h"
#include <wtf/CheckedArithmetic.h>
#include <wtf/DateMath.h>
@@ -44,6 +43,189 @@
namespace WebCore {
namespace RFC7230 {
bool isTokenCharacter(UChar c)
{
return c < 0x80 && isTokenCharacter(static_cast<LChar>(c));
}
bool isDelimiter(UChar c)
{
return c < 0x80 && isDelimiter(static_cast<LChar>(c));
}
bool isTokenCharacter(LChar c)
{
return isASCIIAlpha(c) || isASCIIDigit(c)
|| c == '!' || c == '#' || c == '$'
|| c == '%' || c == '&' || c == '\''
|| c == '*' || c == '+' || c == '-'
|| c == '.' || c == '^' || c == '_'
|| c == '`' || c == '|' || c == '~';
}
bool isDelimiter(LChar c)
{
return c == '(' || c == ')' || c == ','
|| c == '/' || c == ':' || c == ';'
|| c == '<' || c == '=' || c == '>'
|| c == '?' || c == '@' || c == '['
|| c == '\\' || c == ']' || c == '{'
|| c == '}' || c == '"';
}
static bool isVisibleCharacter(UChar c)
{
return isTokenCharacter(c) || isDelimiter(c);
}
bool isWhitespace(UChar c)
{
return c == ' ' || c == '\t';
}
template<size_t min, size_t max>
static bool isInRange(UChar c)
{
return c >= min && c <= max;
}
static bool isOBSText(UChar c)
{
return isInRange<0x80, 0xFF>(c);
}
static bool isQuotedTextCharacter(UChar c)
{
return isWhitespace(c)
|| c == 0x21
|| isInRange<0x23, 0x5B>(c)
|| isInRange<0x5D, 0x7E>(c)
|| isOBSText(c);
}
bool isQuotedPairSecondOctet(UChar c)
{
return isWhitespace(c)
|| isVisibleCharacter(c)
|| isOBSText(c);
}
bool isCommentText(UChar c)
{
return isWhitespace(c)
|| isInRange<0x21, 0x27>(c)
|| isInRange<0x2A, 0x5B>(c)
|| isInRange<0x5D, 0x7E>(c)
|| isOBSText(c);
}
static bool isValidName(StringView name)
{
if (!name.length())
return false;
for (size_t i = 0; i < name.length(); ++i) {
if (!isTokenCharacter(name[i]))
return false;
}
return true;
}
static bool isValidValue(StringView value)
{
enum class State {
OptionalWhitespace,
Token,
QuotedString,
Comment,
};
State state = State::OptionalWhitespace;
size_t commentDepth = 0;
bool hadNonWhitespace = false;
for (size_t i = 0; i < value.length(); ++i) {
UChar c = value[i];
switch (state) {
case State::OptionalWhitespace:
if (isWhitespace(c))
continue;
hadNonWhitespace = true;
if (isTokenCharacter(c)) {
state = State::Token;
continue;
}
if (c == '"') {
state = State::QuotedString;
continue;
}
if (c == '(') {
ASSERT(!commentDepth);
++commentDepth;
state = State::Comment;
continue;
}
return false;
case State::Token:
if (isTokenCharacter(c))
continue;
state = State::OptionalWhitespace;
continue;
case State::QuotedString:
if (c == '"') {
state = State::OptionalWhitespace;
continue;
}
if (c == '\\') {
++i;
if (i == value.length())
return false;
if (!isQuotedPairSecondOctet(value[i]))
return false;
continue;
}
if (!isQuotedTextCharacter(c))
return false;
continue;
case State::Comment:
if (c == '(') {
++commentDepth;
continue;
}
if (c == ')') {
--commentDepth;
if (!commentDepth)
state = State::OptionalWhitespace;
continue;
}
if (c == '\\') {
++i;
if (i == value.length())
return false;
if (!isQuotedPairSecondOctet(value[i]))
return false;
continue;
}
if (!isCommentText(c))
return false;
continue;
}
}
switch (state) {
case State::OptionalWhitespace:
case State::Token:
return hadNonWhitespace;
case State::QuotedString:
case State::Comment:
// Unclosed comments or quotes are invalid values.
break;
}
return false;
}
} // namespace RFC7230
// True if characters which satisfy the predicate are present, incrementing
// "pos" to the next character which does not satisfy the predicate.
// Note: might return pos == str.length().
@@ -116,36 +298,40 @@ bool isValidReasonPhrase(const String& value)
return true;
}
static bool isValidHTTPHeaderValue(const std::span<const LChar> value)
{
for (const auto c : value) {
if (UNLIKELY(c <= 13)) {
if (c == 0x00 || c == 0x0A || c == 0x0D)
return false;
}
}
return true;
}
static bool isValidHTTPHeaderValue(const std::span<const UChar> value)
{
for (const auto c : value) {
if (UNLIKELY(c <= 13)) {
if (c == 0x00 || c == 0x0A || c == 0x0D)
return false;
}
}
return true;
}
// See https://fetch.spec.whatwg.org/#concept-header
bool isValidHTTPHeaderValue(const StringView& value)
{
auto length = value.length();
if (length == 0) return true;
UChar c = value[0];
if (isTabOrSpace(c))
return false;
c = value[length - 1];
if (isTabOrSpace(c))
return false;
if (length == 0)
return true;
if (value.is8Bit()) {
const LChar* begin = value.span8().data();
const LChar* end = begin + value.length();
for (const LChar* p = begin; p != end; ++p) {
if (UNLIKELY(*p <= 13)) {
LChar c = *p;
if (c == 0x00 || c == 0x0A || c == 0x0D)
return false;
}
}
} else {
for (unsigned i = 0; i < value.length(); ++i) {
c = value[i];
if (c == 0x00 || c == 0x0A || c == 0x0D || c > 0x7F)
return false;
}
return isValidHTTPHeaderValue(value.span8());
}
return true;
return isValidHTTPHeaderValue(value.span16());
}
// See RFC 7231, Section 5.3.2.
@@ -194,27 +380,35 @@ bool isValidLanguageHeaderValue(const StringView& value)
return true;
}
static bool isValidHTTPHeaderName(const std::span<const LChar> value)
{
for (const auto c : value) {
if (UNLIKELY(!RFC7230::isTokenCharacter(c)))
return false;
}
return true;
}
static bool isValidHTTPHeaderName(const std::span<const UChar> value)
{
for (const auto c : value) {
if (UNLIKELY(!RFC7230::isTokenCharacter(c)))
return false;
}
return true;
}
// See RFC 7230, Section 3.2.6.
bool isValidHTTPToken(const StringView& value)
bool isValidHTTPHeaderName(const StringView& value)
{
if (value.isEmpty())
return false;
if (value.is8Bit()) {
const LChar* characters = value.span8().data();
const LChar* end = characters + value.length();
while (characters < end) {
if (!RFC7230::isTokenCharacter(*characters++))
return false;
}
return true;
return isValidHTTPHeaderName(value.span8());
}
for (UChar c : value.codeUnits()) {
if (!RFC7230::isTokenCharacter(c))
return false;
}
return true;
return isValidHTTPHeaderName(value.span16());
}
#if USE(GLIB)
@@ -986,5 +1180,4 @@ CrossOriginResourcePolicy parseCrossOriginResourcePolicyHeader(StringView header
return CrossOriginResourcePolicy::Invalid;
}
}

View File

@@ -80,7 +80,7 @@ bool isValidLanguageHeaderValue(const StringView&);
#if USE(GLIB)
WEBCORE_EXPORT bool isValidUserAgentHeaderValue(const StringView&);
#endif
bool isValidHTTPToken(const StringView&);
bool isValidHTTPHeaderName(const StringView&);
std::optional<WallTime> parseHTTPDate(const StringView&);
StringView filenameFromHTTPContentDisposition(StringView);
WEBCORE_EXPORT String extractMIMETypeFromMediaType(const String&);
@@ -143,7 +143,7 @@ inline bool isHTTPSpace(UChar character)
// --end;
// auto token = string.substring(start, end - start + 1);
// if (!isValidHTTPToken(token))
// if (!isValidHTTPHeaderName(token))
// return false;
// set.add(WTFMove(token));

View File

@@ -41,6 +41,16 @@ bool isTokenCharacter(UChar c)
|| c == '`' || c == '|' || c == '~';
}
bool isTokenCharacter(LChar c)
{
return isASCIIAlpha(c) || isASCIIDigit(c)
|| c == '!' || c == '#' || c == '$'
|| c == '%' || c == '&' || c == '\''
|| c == '*' || c == '+' || c == '-'
|| c == '.' || c == '^' || c == '_'
|| c == '`' || c == '|' || c == '~';
}
bool isDelimiter(UChar c)
{
return c == '(' || c == ')' || c == ','

View File

@@ -6,13 +6,18 @@ const ArrayPrototypeJoin = Array.prototype.join;
const ArrayPrototypeMap = Array.prototype.map;
const ArrayIsArray = Array.isArray;
const tokenRegExp = /^[\^_`a-zA-Z\-0-9!#$%&'*+.|~]+$/;
let tokenRegExp;
/**
* Verifies that the given val is a valid HTTP token
* per the rules defined in RFC 7230
* See https://tools.ietf.org/html/rfc7230#section-3.2.6
*/
function checkIsHttpToken(val) {
if (!tokenRegExp) {
tokenRegExp = /^[\^_`a-zA-Z\-0-9!#$%&'*+.|~]+$/;
}
return RegExpPrototypeExec.$call(tokenRegExp, val) !== null;
}
@@ -24,8 +29,12 @@ function checkIsHttpToken(val) {
(not necessarily a valid URI reference) followed by zero or more
link-params separated by semicolons.
*/
const linkValueRegExp = /^(?:<[^>]*>)(?:\s*;\s*[^;"\s]+(?:=(")?[^;"\s]*\1)?)*$/;
let linkValueRegExp;
function validateLinkHeaderFormat(value, name) {
if (!linkValueRegExp) {
linkValueRegExp = /^(?:<[^>]*>)(?:\s*;\s*[^;"\s]+(?:=(")?[^;"\s]*\1)?)*$/;
}
if (typeof value === "undefined" || !RegExpPrototypeExec.$call(linkValueRegExp, value)) {
throw $ERR_INVALID_ARG_VALUE(
name,
@@ -68,6 +77,14 @@ function validateLinkHeaderValue(hints) {
}
hideFromStack(validateLinkHeaderValue);
const validateFunction_ = $newCppFunction("NodeValidator.cpp", "jsFunction_validateFunction", 0);
function validateFunction(value, name) {
if (typeof value !== "function") {
validateFunction_.$apply(undefined, arguments);
}
}
export default {
/** (value, name) */
validateObject: $newCppFunction("NodeValidator.cpp", "jsFunction_validateObject", 2),
@@ -84,7 +101,7 @@ export default {
/** `(number, name, lower, upper, def)` */
checkRangesOrGetDefault: $newCppFunction("NodeValidator.cpp", "jsFunction_checkRangesOrGetDefault", 0),
/** `(value, name)` */
validateFunction: $newCppFunction("NodeValidator.cpp", "jsFunction_validateFunction", 0),
validateFunction,
/** `(value, name)` */
validateBoolean: $newCppFunction("NodeValidator.cpp", "jsFunction_validateBoolean", 0),
/** `(port, name = 'Port', allowZero = true)` */

View File

@@ -135,7 +135,7 @@ function isAbortError(err) {
const ObjectDefineProperty = Object.defineProperty;
const GlobalPromise = globalThis.Promise;
const headerCharRegex = /[^\t\x20-\x7e\x80-\xff]/;
let headerCharRegex;
/**
* True if val contains an invalid field-vchar
* field-value = *( field-content / obs-fold )
@@ -143,6 +143,9 @@ const headerCharRegex = /[^\t\x20-\x7e\x80-\xff]/;
* field-vchar = VCHAR / obs-text
*/
function checkInvalidHeaderChar(val: string) {
if (!headerCharRegex) {
headerCharRegex = /[^\t\x20-\x7e\x80-\xff]/;
}
return RegExpPrototypeExec.$call(headerCharRegex, val) !== null;
}
@@ -952,8 +955,13 @@ const ServerPrototype = {
});
isNextIncomingMessageHTTPS = prevIsNextIncomingMessageHTTPS;
handle.onabort = onServerRequestEvent.bind(socket);
// start buffering data if any, the user will need to resume() or .on("data") to read it
handle.pause();
if (hasBody) {
// Don't automatically read the body, the user will need to resume() or .on("data") to read it.
// But, avoid the system call overhead unless we are going to receive body data.
handle.pause();
}
drainMicrotasks();
let capturedError;
@@ -1178,6 +1186,7 @@ function assignHeaders(object, req) {
return true;
} else {
assignHeadersSlow(object, req);
return false;
}
}
@@ -1702,7 +1711,16 @@ const OutgoingMessagePrototype = {
},
setHeader(name, value) {
validateHeaderName(name);
// do the cheap validations the JIT can optimize in JS
if (typeof name !== "string" || !name) {
throw $ERR_INVALID_HTTP_TOKEN("Header name", name);
}
// do the cheap validations the JIT can optimize in JS
if (value === undefined) {
throw $ERR_HTTP_INVALID_HEADER_VALUE(value, name);
}
const headers = (this[headersSymbol] ??= new Headers());
setHeader(headers, name, value);
return this;