Copilot commented on code in PR #13420:
URL: https://github.com/apache/trafficserver/pull/13420#discussion_r4137244520
##########
include/proxy/http/HttpSM.h:
##########
@@ -739,13 +765,30 @@ HttpSM::get_cache_sm()
inline int
HttpSM::write_response_header_into_buffer(HTTPHdr *h, MIOBuffer *b)
{
- if (t_state.client_info.http_version == HTTPVersion(0, 9)) {
+ if (_ua.get_txn()->supports_direct_header_passing()) {
Review Comment:
`setup_internal_transfer()` calls this helper before checking whether a
client transaction exists. Scheduled-update transactions can have no UA, so
dereferencing `_ua.get_txn()` here crashes instead of reaching the existing
no-UA branch in `setup_internal_transfer()`. Use the buffered path when the
transaction is null.
##########
src/proxy/http2/Http2Stream.cc:
##########
@@ -850,15 +964,33 @@ Http2Stream::update_write_request(bool call_update)
// Process the new data
if (!this->parsing_header_done) {
- // Still parsing the request or response header
int bytes_used = 0;
ParseResult state;
- if (this->is_outbound_connection()) {
+ HTTPHdr *send_hdr = this->_pending_send_header();
+
+ if (send_hdr != nullptr) {
Review Comment:
The direct-send branch skips the former `parse_req()`/`parse_resp()` of the
finalized server request or client response. Those parsers checked
`Content-Length` after header hooks; the request parser also checked `Host`. A
hook can now add conflicting or malformed length fields (or an invalid Host)
and have them encoded to an HTTP/2 peer, changing framing and request handling.
Validate the finalized header by direction before taking this branch, and
preserve the old parser/error handling when it is invalid.
##########
src/proxy/http2/Http2Stream.cc:
##########
@@ -327,6 +372,73 @@ Http2Stream::send_headers(Http2ConnectionState & /* cstate
ATS_UNUSED */)
this->_http_sm_id = this->_sm->sm_id;
}
+ // parse_req is skipped here; re-apply strict_uri_parsing. Runs after the
REQUEST type
+ // check below, since path_get() asserts that polarity.
+ auto uri_ok = [&]() {
+ int const level = this->_sm->t_state.http_config_param->strict_uri_parsing;
+
+ return level == 0 ||
+ (url_is_uri_compliant(level, _receive_header.path_get()) &&
url_is_uri_compliant(level, _receive_header.query_get()) &&
+ url_is_uri_compliant(level, _receive_header.fragment_get()));
+ };
+
+ // parse_req also enforces token methods, Host and Content-Length framing
(RFC 9110 8.6),
+ // and the request line and header field size limits.
+ auto parse_req_would_accept = [&]() {
+ auto const *config = this->_sm->t_state.http_config_param;
+ auto const line_max =
static_cast<size_t>(config->http_request_line_max_size);
+ auto const field_max =
static_cast<size_t>(config->http_hdr_field_max_size);
+ auto method{_receive_header.method_get()};
+
+ if (method.empty() || std::any_of(method.begin(), method.end(), [](char c)
{ return !ParseRules::is_token(c); })) {
+ return false;
+ }
+ // Upper bound of the serialized "METHOD URL HTTP/1.1\r\n", so parse_req
makes the exact call.
+ if (method.size() +
static_cast<size_t>(_receive_header.url_get()->length_get()) + 12 > line_max) {
+ return false;
+ }
+ for (auto const &field : _receive_header) {
+ auto name{field.name_get()};
+ auto value{field.value_get()};
+
+ // parse_req drops a line whose name starts with a non-token and trims
whitespace around values.
+ if (name.empty() || name.size() + value.size() > field_max ||
(!ParseRules::is_token(name.front()) && name.front() != '@') ||
+ (!value.empty() && (ParseRules::is_ws(value.front()) ||
ParseRules::is_ws(value.back())))) {
Review Comment:
An ordinary HTTP/2 field name can contain `:` after its first character;
neither the header validator nor this gate rejects it. For example,
`content-length:extra: 0` was split at the first colon by the old `parse_req()`
path, but is copied as a distinct field name by this path. That changes request
interpretation and can forward an invalid name to an HTTP/2 origin. Fall back
to serialization and parsing for ordinary names containing `:` (or reject them
at HTTP/2 validation).
##########
src/proxy/http2/Http2Stream.cc:
##########
@@ -327,6 +372,73 @@ Http2Stream::send_headers(Http2ConnectionState & /* cstate
ATS_UNUSED */)
this->_http_sm_id = this->_sm->sm_id;
}
+ // parse_req is skipped here; re-apply strict_uri_parsing. Runs after the
REQUEST type
+ // check below, since path_get() asserts that polarity.
+ auto uri_ok = [&]() {
+ int const level = this->_sm->t_state.http_config_param->strict_uri_parsing;
+
+ return level == 0 ||
+ (url_is_uri_compliant(level, _receive_header.path_get()) &&
url_is_uri_compliant(level, _receive_header.query_get()) &&
+ url_is_uri_compliant(level, _receive_header.fragment_get()));
+ };
+
+ // parse_req also enforces token methods, Host and Content-Length framing
(RFC 9110 8.6),
+ // and the request line and header field size limits.
+ auto parse_req_would_accept = [&]() {
+ auto const *config = this->_sm->t_state.http_config_param;
+ auto const line_max =
static_cast<size_t>(config->http_request_line_max_size);
+ auto const field_max =
static_cast<size_t>(config->http_hdr_field_max_size);
+ auto method{_receive_header.method_get()};
+
+ if (method.empty() || std::any_of(method.begin(), method.end(), [](char c)
{ return !ParseRules::is_token(c); })) {
+ return false;
+ }
+ // Upper bound of the serialized "METHOD URL HTTP/1.1\r\n", so parse_req
makes the exact call.
+ if (method.size() +
static_cast<size_t>(_receive_header.url_get()->length_get()) + 12 > line_max) {
+ return false;
+ }
+ for (auto const &field : _receive_header) {
+ auto name{field.name_get()};
+ auto value{field.value_get()};
+
+ // parse_req drops a line whose name starts with a non-token and trims
whitespace around values.
+ if (name.empty() || name.size() + value.size() > field_max ||
(!ParseRules::is_token(name.front()) && name.front() != '@') ||
+ (!value.empty() && (ParseRules::is_ws(value.front()) ||
ParseRules::is_ws(value.back())))) {
+ return false;
+ }
+ }
+ // url_parse_internet() accepts the userinfo that RFC 9113 8.3.1 bans from
:authority,
+ // and the Host built from it never sees validate_hdr_host(). Mirror that
check here.
+ if (MIMEField *host =
_receive_header.field_find(static_cast<std::string_view>(MIME_FIELD_HOST));
host != nullptr) {
+ std::string_view parsed_host;
+ int port = 0;
+ bool has_port = false;
+
+ if (host->has_dups() || !http_parse_host_header(host->value_get(),
parsed_host, port, has_port)) {
+ return false;
+ }
+ }
+ return validate_hdr_content_length(_receive_header.m_heap,
_receive_header.m_http) == ParseResult::DONE;
+ };
+
+ // A failed conversion leaves a \xffVOID method that only parse_req can turn
into a 400.
+ if (conversion_ok && !this->trailing_header_is_possible() &&
!this->is_outbound_connection() &&
+ _receive_header.type_get() == HTTPType::REQUEST && this->_sm != nullptr
&& this->read_vio.nbytes > 0 && uri_ok() &&
+ parse_req_would_accept()) {
+ // The stream owns _receive_header and outlives the handoff, so the pulled
pointer cannot dangle.
+ this->_is_parsed_receive_header_ready = true;
Review Comment:
If `signal_read_event()` cannot lock the HttpSM, the ready flag stays set
until its deferred callback runs. Meanwhile the session can resume reading
frames: a DATA frame marks later HEADERS as trailers, and `rcv_headers_frame()`
calls `reset_receive_headers()` on the same `_receive_header`. HttpSM then
copies the trailer (or an empty RESPONSE header) as its request. Preserve the
initial request in a separate header until HttpSM has copied it, or prevent
trailer decoding from replacing it while the handoff is pending.
##########
src/proxy/http2/Http2Stream.cc:
##########
@@ -850,15 +964,33 @@ Http2Stream::update_write_request(bool call_update)
// Process the new data
if (!this->parsing_header_done) {
- // Still parsing the request or response header
int bytes_used = 0;
ParseResult state;
- if (this->is_outbound_connection()) {
+ HTTPHdr *send_hdr = this->_pending_send_header();
+
+ if (send_hdr != nullptr) {
+ // Field-by-field, not copy(): copy() would wipe the create(HTTP_2_0)
pseudos that the
+ // 1.1->2 conversion fills. The ready flag re-arms per header (1xx
interim, retries).
+ if (this->is_outbound_connection()) {
+ this->_send_header.method_set(send_hdr->method_get());
+ this->_send_header.url_set(send_hdr->url_get());
+ } else {
+ this->_send_header.status_set(send_hdr->status_get());
+ }
+ for (auto &field : *send_hdr) {
+ MIMEField *f = this->_send_header.field_create(field.name_get());
+
+ f->value_set(this->_send_header.m_heap, this->_send_header.m_mime,
field.value_get());
+ this->_send_header.field_attach(f);
+ }
Review Comment:
Request or response hooks can add fields after the inbound request gate
runs. The old `parse_req()`/`parse_resp()` trimmed leading and trailing spaces
or tabs from their values; this direct copy keeps those bytes, so the emitted
HTTP/2 header can be rejected by a peer. Trim surrounding SP/HT when copying
each finalized field, as the old parser did.
--
This is an automated message from the Apache Git Service.
To respond to the message, please log on to GitHub and use the
URL above to go to the specific comment.
To unsubscribe, e-mail: [email protected]
For queries about this service, please contact Infrastructure at:
[email protected]