mirror of
https://github.com/oven-sh/bun
synced 2026-02-02 23:18:47 +00:00
Compare commits
4 Commits
dylan/pyth
...
jarred/mic
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
8aebcf6376 | ||
|
|
60fba75d4d | ||
|
|
2d483255f1 | ||
|
|
b066ca5f37 |
@@ -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
|
||||
|
||||
@@ -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:
|
||||
|
||||
@@ -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 {};
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -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) {
|
||||
|
||||
@@ -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&);
|
||||
|
||||
@@ -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) } };
|
||||
}
|
||||
|
||||
}
|
||||
@@ -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
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@@ -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));
|
||||
|
||||
@@ -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 == ','
|
||||
|
||||
@@ -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)` */
|
||||
|
||||
@@ -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;
|
||||
|
||||
Reference in New Issue
Block a user