Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
8 changes: 5 additions & 3 deletions include/proxy/hdrs/HdrToken.h
Original file line number Diff line number Diff line change
Expand Up @@ -132,9 +132,11 @@ extern HdrTokenInfoFlags hdrtoken_str_flags[];
//
////////////////////////////////////////////////////////////////////////////

extern void hdrtoken_init();
extern int hdrtoken_tokenize(const char *string, int string_len, const char **wks_string_out = nullptr);
extern int hdrtoken_method_tokenize(const char *string, int string_len);
extern void hdrtoken_init();
extern int hdrtoken_tokenize(const char *string, int string_len, const char **wks_string_out = nullptr);
extern int hdrtoken_tokenize_prehashed(const char *string, int string_len, uint32_t hash, const char **wks_string_out = nullptr);
extern int hdrtoken_field_name_scan(const char *string, int maxlen, uint32_t *hash_out, bool *all_valid_out);
extern int hdrtoken_method_tokenize(const char *string, int string_len);
Comment thread
moonchen marked this conversation as resolved.
extern const char *hdrtoken_string_to_wks(const char *string);
extern const char *hdrtoken_string_to_wks(const char *string, int length);
extern c_str_view hdrtoken_string_to_wks_sv(const char *string);
Expand Down
33 changes: 11 additions & 22 deletions src/proxy/hdrs/HTTP.cc
Original file line number Diff line number Diff line change
Expand Up @@ -1119,29 +1119,18 @@ http_parser_parse_req(HTTPParser *parser, HdrHeap *heap, HTTPHdrImpl *hh, const
ParseResult
validate_hdr_request_target(int method_wk_idx, URLImpl *url)
{
ParseResult ret = ParseResult::DONE;
auto host{url->get_host()};
auto path{url->get_path()};
auto scheme{url->get_scheme()};

if (host.empty()) {
if (path == "*"sv) { // asterisk-form
// Skip this check for now because URLImpl can't distinguish '*' and '/*'
// if (method_wk_idx != HTTP_WKSIDX_OPTIONS) {
// ret = ParseResult::ERROR;
// }
} else { // origin-form
// Nothing to check here
}
} else if (scheme.empty() && !host.empty()) { // authority-form
if (method_wk_idx != HTTP_WKSIDX_CONNECT) {
ret = ParseResult::ERROR;
}
} else { // absolute-form
// Nothing to check here
}
// The only rejected request-target is authority-form (host present, scheme
// absent) with a method other than CONNECT. A part is empty when its pointer
// is null or its length is zero, matching the getters this replaces; the
// asterisk-form check is intentionally disabled (URLImpl can't distinguish
// '*' from '/*'), so origin-, asterisk-, and absolute-form all accept.
bool const host_present = url->m_ptr_host != nullptr && url->m_len_host != 0;
bool const scheme_absent = url->m_scheme_wks_idx < 0 && (url->m_ptr_scheme == nullptr || url->m_len_scheme == 0);

return ret;
if (host_present && scheme_absent && method_wk_idx != HTTP_WKSIDX_CONNECT) {
return ParseResult::ERROR;
}
return ParseResult::DONE;
}

bool
Expand Down
74 changes: 61 additions & 13 deletions src/proxy/hdrs/HdrToken.cc
Original file line number Diff line number Diff line change
Expand Up @@ -323,7 +323,8 @@ hdrtoken_ascii_toupper(unsigned char c)
constexpr uint32_t HDRTOKEN_HASH_SEED = 0x811c9dc5u; // FNV-1a 32-bit offset basis

// The one hash function, shared by compile-time table construction and hdrtoken_tokenize(), so the
// two can never disagree.
// two can never disagree. hdrtoken_field_name_scan() folds the same steps inline because it
// discovers the length as it scans; a parity unit test pins it to this function.
constexpr uint32_t
hdrtoken_hash(std::string_view s)
{
Expand Down Expand Up @@ -674,21 +675,15 @@ hdrtoken_method_tokenize(const char *string, int string_len)
/*-------------------------------------------------------------------------
-------------------------------------------------------------------------*/

// WKS lookup for a name whose FNV-1a hash the caller has already computed
// (e.g. fused into the field-name scan). Matches by slot, hash, and length like
// hdrtoken_tokenize, but skips the interned-pointer test, so it is only valid
// for a non-interned `string`.
int
hdrtoken_tokenize(const char *string, int string_len, const char **wks_string_out)
hdrtoken_tokenize_prehashed(const char *string, int string_len, uint32_t hash, const char **wks_string_out)
{
ink_assert(string != nullptr);

if (hdrtoken_is_wks(string)) {
int const wks_idx = hdrtoken_wks_to_index(string);

if (wks_string_out) {
*wks_string_out = string;
}
return wks_idx;
}

uint32_t const hash = hdrtoken_hash(std::string_view{string, static_cast<size_t>(string_len)});
ink_assert(!hdrtoken_is_wks(string));
Comment on lines +683 to +686

HdrTokenHashBucket const &bucket = hdrtoken_hash_table[hash_to_slot(hash)];

Expand All @@ -708,6 +703,59 @@ hdrtoken_tokenize(const char *string, int string_len, const char **wks_string_ou
return -1;
}

/*-------------------------------------------------------------------------
-------------------------------------------------------------------------*/

// Single-pass field-name scan for the MIME parser. Scans up to `maxlen` bytes
// of `string` for the ':' delimiter while, in the same pass, accumulating the
// FNV-1a name hash (identical to hdrtoken_hash) and tracking whether every byte
// before ':' is a valid HTTP field-name char. Returns the index of ':' (i.e.
// the field-name length) or -1 if no ':' appears within `maxlen`. `*hash_out`
// and `*all_valid_out` describe the bytes scanned before ':' (or all `maxlen`
// bytes when ':' is absent); both are required.
int
hdrtoken_field_name_scan(const char *string, int maxlen, uint32_t *hash_out, bool *all_valid_out)
{
uint32_t hval = HDRTOKEN_HASH_SEED; // same FNV-1a name hash as hdrtoken_hash
bool all_valid = true;
int i = 0;

for (; i < maxlen; ++i) {
unsigned char const uc = static_cast<unsigned char>(string[i]);
if (uc == ':') {
break;
}
hval = (hval ^ hdrtoken_ascii_toupper(uc)) * 0x01000193u;
all_valid &= (ParseRules::is_http_field_name(static_cast<char>(uc)) != 0);
}

*hash_out = hval;
*all_valid_out = all_valid;
return (i < maxlen) ? i : -1;
Comment thread
moonchen marked this conversation as resolved.
}
Comment thread
moonchen marked this conversation as resolved.
Comment thread
moonchen marked this conversation as resolved.

/*-------------------------------------------------------------------------
-------------------------------------------------------------------------*/

int
hdrtoken_tokenize(const char *string, int string_len, const char **wks_string_out)
{
ink_assert(string != nullptr);

if (hdrtoken_is_wks(string)) {
int const wks_idx = hdrtoken_wks_to_index(string);

if (wks_string_out) {
*wks_string_out = string;
}
return wks_idx;
}

uint32_t const hash = hdrtoken_hash(std::string_view{string, static_cast<size_t>(string_len)});

return hdrtoken_tokenize_prehashed(string, string_len, hash, wks_string_out);
}

/*-------------------------------------------------------------------------
-------------------------------------------------------------------------*/

Expand Down
110 changes: 100 additions & 10 deletions src/proxy/hdrs/MIME.cc
Original file line number Diff line number Diff line change
Expand Up @@ -2536,12 +2536,30 @@ mime_parser_parse(MIMEParser *parser, HdrHeap *heap, MIMEHdrImpl *mh, const char
continue; // toss away garbage line
}

// A line so long its size cannot be represented as int cannot yield a
// storable field; reject it rather than let the narrowing below wrap.
if (parsed.size() > static_cast<size_t>(INT_MAX)) {
return ParseResult::ERROR;
}

// find name last
auto field_value = parsed; // need parsed as is later on.
auto field_name = field_value.split_prefix_at(':');
if (field_name.empty()) {
//
// Fuse the colon scan, FNV-1a name hash, and per-byte field-name validation
// into one pass over the name bytes. hdrtoken_field_name_scan returns the
// colon index (the name length) and, for those bytes, the hash reused below
// by the WKS lookup, plus whether every byte is a valid HTTP field-name
// char.
auto field_value = parsed; // need parsed as is later on.
uint32_t field_name_hash;
bool name_all_valid;
int colon_idx = hdrtoken_field_name_scan(parsed.data(), static_cast<int>(parsed.size()), &field_name_hash, &name_all_valid);
if (colon_idx <= 0) {
// colon_idx < 0: no colon; colon_idx == 0: empty name. Both are garbage,
// matching the old empty-field_name toss.
continue; // toss away garbage line
}
auto field_name = parsed.prefix(colon_idx);
field_value.remove_prefix(colon_idx + 1);

// RFC7230 section 3.2.4:
// No whitespace is allowed between the header field-name and colon. In
Expand All @@ -2553,12 +2571,15 @@ mime_parser_parse(MIMEParser *parser, HdrHeap *heap, MIMEHdrImpl *mh, const char
// A proxy MUST remove any such whitespace from a response message before
// forwarding the message downstream.
bool raw_print_field = true;
bool name_scan_stale = false;
if (is_ws(field_name.back())) {
if (!remove_ws_from_field_name) {
return ParseResult::ERROR;
}
field_name.rtrim_if(&ParseRules::is_ws);
raw_print_field = false;
// The fused scan hashed and validated the untrimmed name; recompute below.
name_scan_stale = true;
} else if (parsed.suffix(2) != "\r\n" || (parsed.size() > 2 && parsed[parsed.size() - 3] == '\r')) {
// Do not preserve malformed line endings when forwarding the field.
raw_print_field = false;
Expand Down Expand Up @@ -2597,14 +2618,23 @@ mime_parser_parse(MIMEParser *parser, HdrHeap *heap, MIMEHdrImpl *mh, const char
// tokenize the name //
///////////////////////

int field_name_wks_idx = hdrtoken_tokenize(field_name.data(), field_name.size());

if (field_name_wks_idx < 0) {
for (auto i : field_name) {
if (!ParseRules::is_http_field_name(i)) {
return ParseResult::ERROR;
int field_name_wks_idx;
if (name_scan_stale) {
// BWS trimming shortened the name after the fused scan; redo the WKS
// lookup and byte validation over the trimmed name.
field_name_wks_idx = hdrtoken_tokenize(field_name.data(), static_cast<int>(field_name.size()));
if (field_name_wks_idx < 0) {
for (auto i : field_name) {
if (!ParseRules::is_http_field_name(i)) {
return ParseResult::ERROR;
}
}
}
} else {
field_name_wks_idx = hdrtoken_tokenize_prehashed(field_name.data(), static_cast<int>(field_name.size()), field_name_hash);
if ((field_name_wks_idx < 0) && !name_all_valid) {
return ParseResult::ERROR;
}
}

// RFC 9110 Section 5.5. Field Values
Expand All @@ -2621,7 +2651,67 @@ mime_parser_parse(MIMEParser *parser, HdrHeap *heap, MIMEHdrImpl *mh, const char

MIMEField *field = mime_field_create_for_name(heap, mh, field_name);
mime_field_name_value_set(heap, mh, field, field_name_wks_idx, field_name, field_value, raw_print_field, parsed.size(), false);
mime_hdr_field_attach(mh, field, 1, nullptr);

// A clear presence bit guarantees no duplicate exists. Skip the lookup.
// Names without a presence bit still need the normal duplicate check.
//
// mime_hdr_field_attach() uses field->name_get(), which returns an interned
// string for well-known names. mime_hdr_field_find() would then check the
// same presence bit and return nullptr.
int check_for_dups = 1;
if (field_name_wks_idx >= 0) {
uint64_t const mask = hdrtoken_index_to_mask(field_name_wks_idx);
if (mask != 0 && (mh->m_presence_bits & mask) == 0) {
check_for_dups = 0;
}
}

// Append an adjacent duplicate in O(1), without searching its chain.
// The previous field must have the same name and be the chain's tail.
// Duplicate chains follow slot order, so the new field belongs after it.
//
// mime_field_create_for_name() can reuse an older slot. The pointer check
// below limits this shortcut to cases where the new field occupies
// the last allocated slot in the tail block and has a predecessor there.
// Otherwise, fall back to normal attachment.
//
// Get the previous field from the current header. Do not cache it in the
// parser: the parser can be reused after its previous header is destroyed.
bool fast_tail_append = false;
MIMEFieldBlockImpl *const tail_fblock = mh->m_fblock_list_tail;

if (tail_fblock->m_freetop >= 2 && &tail_fblock->m_field_slots[tail_fblock->m_freetop - 1] == field) {
MIMEField *const last = &tail_fblock->m_field_slots[tail_fblock->m_freetop - 2];

if (last->is_live() && last->m_next_dup == nullptr) {
bool name_matches;

if (field_name_wks_idx >= 0) {
name_matches = (last->m_wks_idx == field_name_wks_idx);
} else {
name_matches =
(last->m_wks_idx < 0) &&
ts::iequals(std::string_view{last->m_ptr_name, static_cast<std::string_view::size_type>(last->m_len_name)}, field_name);
}

if (name_matches) {
field->m_readiness = MIME_FIELD_SLOT_READINESS_LIVE;
field->m_flags = (field->m_flags & ~MIME_FIELD_SLOT_FLAGS_DUP_HEAD);
field->m_next_dup = nullptr;
last->m_next_dup = field;
// Presence bit and slot accelerator were set by the chain head; a tail
// dup leaves them untouched, matching attach's patch-after-prev branch.
if (field->m_ptr_value && field->is_cooked()) {
mh->recompute_cooked_stuff(field);
}
fast_tail_append = true;
}
}
}

if (!fast_tail_append) {
mime_hdr_field_attach(mh, field, check_for_dups, nullptr);
}
}
}

Expand Down
14 changes: 5 additions & 9 deletions src/proxy/hdrs/URL.cc
Original file line number Diff line number Diff line change
Expand Up @@ -1206,17 +1206,13 @@ url_is_strictly_compliant(const char *start, const char *end)
bool
url_is_mostly_compliant(const char *start, const char *end)
{
// Accumulate invalid bytes without an early exit so the compiler can vectorize the scan.
unsigned char bad = 0;
for (const char *i = start; i < end; ++i) {
if (isspace(*i)) {
Dbg(dbg_ctl_http, "Whitespace character [0x%.2X] found in URL", static_cast<unsigned char>(*i));
return false;
}
if (!isprint(*i)) {
Dbg(dbg_ctl_http, "Non-printable character [0x%.2X] found in URL", static_cast<unsigned char>(*i));
return false;
}
unsigned char const c = static_cast<unsigned char>(*i);
bad |= static_cast<unsigned char>((c < 0x21) | (c > 0x7E));
}
return true;
return bad == 0;
}

} // namespace UrlImpl
Expand Down
Loading