This is an automated email from the ASF dual-hosted git repository.
eze pushed a commit to branch 8.1.x
in repository https://gitbox.apache.org/repos/asf/trafficserver.git
The following commit(s) were added to refs/heads/8.1.x by this push:
new a8b8b92c1d proxy.config.http.drop_chunked_trailers (#11605)
a8b8b92c1d is described below
commit a8b8b92c1d62db8a4f7a8a833223f5dd4032d89c
Author: Brian Neradt <[email protected]>
AuthorDate: Tue Jul 23 17:14:02 2024 -0500
proxy.config.http.drop_chunked_trailers (#11605)
This adds the proxy.config.http.drop_chunked_trailers configuration that
allows dropping of chunked trailers.
---
doc/admin-guide/files/records.config.en.rst | 12 +++
doc/admin-guide/plugins/lua.en.rst | 1 +
.../api/functions/TSHttpOverridableConfig.en.rst | 1 +
.../api/types/TSOverridableConfigKey.en.rst | 1 +
include/ts/apidefs.h.in | 1 +
mgmt/RecordsConfig.cc | 2 +
plugins/lua/ts_lua_http_config.c | 2 +
proxy/http/HttpConfig.cc | 2 +
proxy/http/HttpConfig.h | 7 +-
proxy/http/HttpSM.cc | 22 ++--
proxy/http/HttpTunnel.cc | 115 +++++++++++++++------
proxy/http/HttpTunnel.h | 34 +++++-
src/traffic_server/FetchSM.cc | 2 +-
src/traffic_server/InkAPI.cc | 7 ++
src/traffic_server/InkAPITest.cc | 3 +-
.../chunked_encoding/chunked_encoding.test.py | 96 +++++++++++++++++
.../replays/chunked_trailer_dropped.replay.yaml | 68 ++++++++++++
.../replays/chunked_trailer_proxied.replay.yaml | 68 ++++++++++++
18 files changed, 392 insertions(+), 52 deletions(-)
diff --git a/doc/admin-guide/files/records.config.en.rst
b/doc/admin-guide/files/records.config.en.rst
index 67f930703b..a431c27881 100644
--- a/doc/admin-guide/files/records.config.en.rst
+++ b/doc/admin-guide/files/records.config.en.rst
@@ -906,6 +906,18 @@ mptcp
request, this option determines the size of the chunks, in bytes, to use
when sending content to an HTTP/1.1 client.
+.. ts:cv:: CONFIG proxy.config.http.drop_chunked_trailers INT 0
+ :reloadable:
+ :overridable:
+
+ Specifies whether |TS| should drop chunked trailers. If enabled (``1``),
|TS|
+ will drop any chunked trailers in a ``Transfer-Encoded: chunked`` request or
+ response body. If disabled (``0``), |TS| will pass the chunked trailers
+ unmodified to the receiving peer. See `RFC 9112, section 7.1.2
+ <https://www.rfc-editor.org/rfc/rfc9112.html#name-chunked-trailer-section>`_
+ for details about chunked trailers. By default, this option is disabled
+ and therefore |TS| will not drop chunked trailers.
+
.. ts:cv:: CONFIG proxy.config.http.send_http11_requests INT 1
:reloadable:
:overridable:
diff --git a/doc/admin-guide/plugins/lua.en.rst
b/doc/admin-guide/plugins/lua.en.rst
index 3e27e1ba44..904118f7b3 100644
--- a/doc/admin-guide/plugins/lua.en.rst
+++ b/doc/admin-guide/plugins/lua.en.rst
@@ -3537,6 +3537,7 @@ Http config constants
TS_LUA_CONFIG_NET_SOCK_PACKET_TOS_OUT
TS_LUA_CONFIG_HTTP_INSERT_AGE_IN_RESPONSE
TS_LUA_CONFIG_HTTP_CHUNKING_SIZE
+ TS_LUA_CONFIG_HTTP_DROP_CHUNKED_TRAILERS
TS_LUA_CONFIG_HTTP_FLOW_CONTROL_ENABLED
TS_LUA_CONFIG_HTTP_FLOW_CONTROL_LOW_WATER_MARK
TS_LUA_CONFIG_HTTP_FLOW_CONTROL_HIGH_WATER_MARK
diff --git a/doc/developer-guide/api/functions/TSHttpOverridableConfig.en.rst
b/doc/developer-guide/api/functions/TSHttpOverridableConfig.en.rst
index 4ea4be412d..b9cd006be8 100644
--- a/doc/developer-guide/api/functions/TSHttpOverridableConfig.en.rst
+++ b/doc/developer-guide/api/functions/TSHttpOverridableConfig.en.rst
@@ -111,6 +111,7 @@ TSOverridableConfigKey Value
Configuratio
:c:macro:`TS_CONFIG_HTTP_CACHE_WHEN_TO_REVALIDATE`
:ts:cv:`proxy.config.http.cache.when_to_revalidate`
:c:macro:`TS_CONFIG_HTTP_CHUNKING_ENABLED`
:ts:cv:`proxy.config.http.chunking_enabled`
:c:macro:`TS_CONFIG_HTTP_CHUNKING_SIZE`
:ts:cv:`proxy.config.http.chunking.size`
+:c:macro:`TS_CONFIG_HTTP_DROP_CHUNKED_TRAILERS`
:ts:cv:`proxy.config.http.drop_chunked_trailers`
:c:macro:`TS_CONFIG_HTTP_CONNECT_ATTEMPTS_MAX_RETRIES_DEAD_SERVER`
:ts:cv:`proxy.config.http.connect_attempts_max_retries_dead_server`
:c:macro:`TS_CONFIG_HTTP_CONNECT_ATTEMPTS_MAX_RETRIES`
:ts:cv:`proxy.config.http.connect_attempts_max_retries`
:c:macro:`TS_CONFIG_HTTP_CONNECT_ATTEMPTS_RR_RETRIES`
:ts:cv:`proxy.config.http.connect_attempts_rr_retries`
diff --git a/doc/developer-guide/api/types/TSOverridableConfigKey.en.rst
b/doc/developer-guide/api/types/TSOverridableConfigKey.en.rst
index f391d15df3..7dc0da5435 100644
--- a/doc/developer-guide/api/types/TSOverridableConfigKey.en.rst
+++ b/doc/developer-guide/api/types/TSOverridableConfigKey.en.rst
@@ -94,6 +94,7 @@ Enumeration Members
.. c:macro:: TS_CONFIG_NET_SOCK_PACKET_TOS_OUT
.. c:macro:: TS_CONFIG_HTTP_INSERT_AGE_IN_RESPONSE
.. c:macro:: TS_CONFIG_HTTP_CHUNKING_SIZE
+ .. c:macro:: TS_CONFIG_HTTP_DROP_CHUNKED_TRAILERS
.. c:macro:: TS_CONFIG_HTTP_FLOW_CONTROL_ENABLED
.. c:macro:: TS_CONFIG_HTTP_FLOW_CONTROL_LOW_WATER_MARK
.. c:macro:: TS_CONFIG_HTTP_FLOW_CONTROL_HIGH_WATER_MARK
diff --git a/include/ts/apidefs.h.in b/include/ts/apidefs.h.in
index 862ac31ab7..0cedf725a7 100644
--- a/include/ts/apidefs.h.in
+++ b/include/ts/apidefs.h.in
@@ -769,6 +769,7 @@ typedef enum {
TS_CONFIG_HTTP_ALLOW_MULTI_RANGE,
TS_CONFIG_HTTP_REQUEST_BUFFER_ENABLED,
TS_CONFIG_HTTP_ALLOW_HALF_OPEN,
+ TS_CONFIG_HTTP_DROP_CHUNKED_TRAILERS,
TS_CONFIG_LAST_ENTRY
} TSOverridableConfigKey;
diff --git a/mgmt/RecordsConfig.cc b/mgmt/RecordsConfig.cc
index fba9fd52ce..6778d61e30 100644
--- a/mgmt/RecordsConfig.cc
+++ b/mgmt/RecordsConfig.cc
@@ -346,6 +346,8 @@ static const RecordElement RecordsConfig[] =
,
{RECT_CONFIG, "proxy.config.http.chunking.size", RECD_INT, "4096",
RECU_DYNAMIC, RR_NULL, RECC_NULL, nullptr, RECA_NULL}
,
+ {RECT_CONFIG, "proxy.config.http.drop_chunked_trailers", RECD_INT, "0",
RECU_DYNAMIC, RR_NULL, RECC_NULL, "[0-1]", RECA_NULL}
+ ,
{RECT_CONFIG, "proxy.config.http.flow_control.enabled", RECD_INT, "0",
RECU_DYNAMIC, RR_NULL, RECC_NULL, nullptr, RECA_NULL}
,
{RECT_CONFIG, "proxy.config.http.flow_control.high_water", RECD_INT, "0",
RECU_DYNAMIC, RR_NULL, RECC_NULL, nullptr, RECA_NULL}
diff --git a/plugins/lua/ts_lua_http_config.c b/plugins/lua/ts_lua_http_config.c
index f4c3ae99e0..b22d2056bb 100644
--- a/plugins/lua/ts_lua_http_config.c
+++ b/plugins/lua/ts_lua_http_config.c
@@ -81,6 +81,7 @@ typedef enum {
TS_LUA_CONFIG_NET_SOCK_PACKET_TOS_OUT =
TS_CONFIG_NET_SOCK_PACKET_TOS_OUT,
TS_LUA_CONFIG_HTTP_INSERT_AGE_IN_RESPONSE =
TS_CONFIG_HTTP_INSERT_AGE_IN_RESPONSE,
TS_LUA_CONFIG_HTTP_CHUNKING_SIZE =
TS_CONFIG_HTTP_CHUNKING_SIZE,
+ TS_LUA_CONFIG_HTTP_DROP_CHUNKED_TRAILERS =
TS_CONFIG_HTTP_DROP_CHUNKED_TRAILERS,
TS_LUA_CONFIG_HTTP_FLOW_CONTROL_ENABLED =
TS_CONFIG_HTTP_FLOW_CONTROL_ENABLED,
TS_LUA_CONFIG_HTTP_FLOW_CONTROL_LOW_WATER_MARK =
TS_CONFIG_HTTP_FLOW_CONTROL_LOW_WATER_MARK,
TS_LUA_CONFIG_HTTP_FLOW_CONTROL_HIGH_WATER_MARK =
TS_CONFIG_HTTP_FLOW_CONTROL_HIGH_WATER_MARK,
@@ -207,6 +208,7 @@ ts_lua_var_item ts_lua_http_config_vars[] = {
TS_LUA_MAKE_VAR_ITEM(TS_LUA_CONFIG_NET_SOCK_PACKET_TOS_OUT),
TS_LUA_MAKE_VAR_ITEM(TS_LUA_CONFIG_HTTP_INSERT_AGE_IN_RESPONSE),
TS_LUA_MAKE_VAR_ITEM(TS_LUA_CONFIG_HTTP_CHUNKING_SIZE),
+ TS_LUA_MAKE_VAR_ITEM(TS_LUA_CONFIG_HTTP_DROP_CHUNKED_TRAILERS),
TS_LUA_MAKE_VAR_ITEM(TS_LUA_CONFIG_HTTP_FLOW_CONTROL_ENABLED),
TS_LUA_MAKE_VAR_ITEM(TS_LUA_CONFIG_HTTP_FLOW_CONTROL_LOW_WATER_MARK),
TS_LUA_MAKE_VAR_ITEM(TS_LUA_CONFIG_HTTP_FLOW_CONTROL_HIGH_WATER_MARK),
diff --git a/proxy/http/HttpConfig.cc b/proxy/http/HttpConfig.cc
index 5df6bdbc42..8087a8ea76 100644
--- a/proxy/http/HttpConfig.cc
+++ b/proxy/http/HttpConfig.cc
@@ -1020,6 +1020,7 @@ HttpConfig::startup()
HttpEstablishStaticConfigByte(c.oride.keep_alive_enabled_out,
"proxy.config.http.keep_alive_enabled_out");
HttpEstablishStaticConfigByte(c.oride.chunking_enabled,
"proxy.config.http.chunking_enabled");
HttpEstablishStaticConfigLongLong(c.oride.http_chunking_size,
"proxy.config.http.chunking.size");
+ HttpEstablishStaticConfigByte(c.oride.http_drop_chunked_trailers,
"proxy.config.http.drop_chunked_trailers");
HttpEstablishStaticConfigByte(c.oride.flow_control_enabled,
"proxy.config.http.flow_control.enabled");
HttpEstablishStaticConfigLongLong(c.oride.flow_high_water_mark,
"proxy.config.http.flow_control.high_water");
HttpEstablishStaticConfigLongLong(c.oride.flow_low_water_mark,
"proxy.config.http.flow_control.low_water");
@@ -1295,6 +1296,7 @@ HttpConfig::reconfigure()
params->oride.keep_alive_enabled_in =
INT_TO_BOOL(m_master.oride.keep_alive_enabled_in);
params->oride.keep_alive_enabled_out =
INT_TO_BOOL(m_master.oride.keep_alive_enabled_out);
params->oride.chunking_enabled =
INT_TO_BOOL(m_master.oride.chunking_enabled);
+ params->oride.http_drop_chunked_trailers =
m_master.oride.http_drop_chunked_trailers;
params->oride.auth_server_session_private =
INT_TO_BOOL(m_master.oride.auth_server_session_private);
params->oride.http_chunking_size = m_master.oride.http_chunking_size;
diff --git a/proxy/http/HttpConfig.h b/proxy/http/HttpConfig.h
index 4a3aba763b..791a9e6771 100644
--- a/proxy/http/HttpConfig.h
+++ b/proxy/http/HttpConfig.h
@@ -734,9 +734,10 @@ struct OverridableHttpConfigParams {
MgmtInt background_fill_active_timeout;
- MgmtInt http_chunking_size; // Maximum chunk size for chunked output.
- MgmtInt flow_high_water_mark; ///< Flow control high water mark.
- MgmtInt flow_low_water_mark; ///< Flow control low water mark.
+ MgmtInt http_chunking_size; // Maximum chunk size for chunked
output.
+ MgmtByte http_drop_chunked_trailers; ///< Whether to drop chunked trailers.
+ MgmtInt flow_high_water_mark; ///< Flow control high water mark.
+ MgmtInt flow_low_water_mark; ///< Flow control low water mark.
MgmtInt default_buffer_size_index;
MgmtInt default_buffer_water_mark;
diff --git a/proxy/http/HttpSM.cc b/proxy/http/HttpSM.cc
index 84c4fec2a7..13b52c0f1e 100644
--- a/proxy/http/HttpSM.cc
+++ b/proxy/http/HttpSM.cc
@@ -838,7 +838,8 @@ HttpSM::wait_for_full_body()
ua_buffer_reader->consume(client_request_body_bytes);
p = tunnel.add_producer(ua_entry->vc, post_bytes, buf_start,
&HttpSM::tunnel_handler_post_ua, HT_BUFFER_READ, "ua post buffer");
if (chunked) {
- tunnel.set_producer_chunking_action(p, 0, TCA_PASSTHRU_CHUNKED_CONTENT);
+ bool const drop_chunked_trailers =
t_state.http_config_param->oride.http_drop_chunked_trailers == 1;
+ tunnel.set_producer_chunking_action(p, 0, TCA_PASSTHRU_CHUNKED_CONTENT,
drop_chunked_trailers);
}
ua_entry->in_tunnel = true;
ua_txn->set_inactivity_timeout(HRTIME_SECONDS(t_state.txn_conf->transaction_no_activity_timeout_in));
@@ -5711,10 +5712,11 @@ HttpSM::do_setup_post_tunnel(HttpVC_t to_vc_type)
// The user agent may support chunked (HTTP/1.1) or not (HTTP/2)
// In either case, the server will support chunked (HTTP/1.1)
if (chunked) {
+ bool const drop_chunked_trailers =
t_state.http_config_param->oride.http_drop_chunked_trailers == 1;
if (ua_txn->is_chunked_encoding_supported()) {
- tunnel.set_producer_chunking_action(p, 0, TCA_PASSTHRU_CHUNKED_CONTENT);
+ tunnel.set_producer_chunking_action(p, 0, TCA_PASSTHRU_CHUNKED_CONTENT,
drop_chunked_trailers);
} else {
- tunnel.set_producer_chunking_action(p, 0, TCA_CHUNK_CONTENT);
+ tunnel.set_producer_chunking_action(p, 0, TCA_CHUNK_CONTENT,
drop_chunked_trailers);
tunnel.set_producer_chunking_size(p, 0);
}
}
@@ -6150,7 +6152,8 @@ HttpSM::setup_cache_read_transfer()
// this only applies to read-while-write cases where origin server sends a
dynamically generated chunked content
// w/o providing a Content-Length header
if (t_state.client_info.receive_chunked_response) {
- tunnel.set_producer_chunking_action(p, client_response_hdr_bytes,
TCA_CHUNK_CONTENT);
+ bool const drop_chunked_trailers =
t_state.http_config_param->oride.http_drop_chunked_trailers == 1;
+ tunnel.set_producer_chunking_action(p, client_response_hdr_bytes,
TCA_CHUNK_CONTENT, drop_chunked_trailers);
tunnel.set_producer_chunking_size(p, t_state.txn_conf->http_chunking_size);
}
ua_entry->in_tunnel = true;
@@ -6477,7 +6480,7 @@ HttpSM::setup_server_transfer_to_transform()
if (t_state.current.server->transfer_encoding ==
HttpTransact::CHUNKED_ENCODING) {
client_response_hdr_bytes = 0; // fixed by YTS Team, yamsat
- tunnel.set_producer_chunking_action(p, client_response_hdr_bytes,
TCA_DECHUNK_CONTENT);
+ tunnel.set_producer_chunking_action(p, client_response_hdr_bytes,
TCA_DECHUNK_CONTENT, HttpTunnel::DROP_CHUNKED_TRAILERS);
}
return p;
@@ -6516,7 +6519,8 @@ HttpSM::setup_transfer_from_transform()
this->setup_plugin_agents(p);
if (t_state.client_info.receive_chunked_response) {
- tunnel.set_producer_chunking_action(p, client_response_hdr_bytes,
TCA_CHUNK_CONTENT);
+ bool const drop_chunked_trailers =
t_state.http_config_param->oride.http_drop_chunked_trailers == 1;
+ tunnel.set_producer_chunking_action(p, client_response_hdr_bytes,
TCA_CHUNK_CONTENT, drop_chunked_trailers);
tunnel.set_producer_chunking_size(p, t_state.txn_conf->http_chunking_size);
}
@@ -6572,7 +6576,8 @@ HttpSM::setup_server_transfer_to_cache_only()
HttpTunnelProducer *p =
tunnel.add_producer(server_entry->vc, nbytes, buf_start,
&HttpSM::tunnel_handler_server, HT_HTTP_SERVER, "http server");
- tunnel.set_producer_chunking_action(p, 0, action);
+ bool const drop_chunked_trailers =
t_state.http_config_param->oride.http_drop_chunked_trailers == 1;
+ tunnel.set_producer_chunking_action(p, 0, action, drop_chunked_trailers);
tunnel.set_producer_chunking_size(p, t_state.txn_conf->http_chunking_size);
setup_cache_write_transfer(&cache_sm, server_entry->vc,
&t_state.cache_info.object_store, 0, "cache write");
@@ -6667,7 +6672,8 @@ HttpSM::setup_server_transfer()
else action = TCA_PASSTHRU_CHUNKED_CONTENT;
}
*/
- tunnel.set_producer_chunking_action(p, client_response_hdr_bytes, action);
+ bool const drop_chunked_trailers =
t_state.http_config_param->oride.http_drop_chunked_trailers == 1;
+ tunnel.set_producer_chunking_action(p, client_response_hdr_bytes, action,
drop_chunked_trailers);
tunnel.set_producer_chunking_size(p, t_state.txn_conf->http_chunking_size);
return p;
}
diff --git a/proxy/http/HttpTunnel.cc b/proxy/http/HttpTunnel.cc
index 94e0044c7c..9c40f38831 100644
--- a/proxy/http/HttpTunnel.cc
+++ b/proxy/http/HttpTunnel.cc
@@ -57,7 +57,7 @@ ChunkedHandler::ChunkedHandler()
skip_bytes(0),
state(CHUNK_READ_CHUNK),
cur_chunk_size(0),
- bytes_left(0),
+ cur_chunk_bytes_left(0),
last_server_event(VC_EVENT_NONE),
running_sum(0),
num_digits(0),
@@ -67,27 +67,27 @@ ChunkedHandler::ChunkedHandler()
}
void
-ChunkedHandler::init(IOBufferReader *buffer_in, HttpTunnelProducer *p)
+ChunkedHandler::init(IOBufferReader *buffer_in, HttpTunnelProducer *p, bool
drop_chunked_trailers)
{
if (p->do_chunking) {
- init_by_action(buffer_in, ACTION_DOCHUNK);
+ init_by_action(buffer_in, ACTION_DOCHUNK, drop_chunked_trailers);
} else if (p->do_dechunking) {
- init_by_action(buffer_in, ACTION_DECHUNK);
+ init_by_action(buffer_in, ACTION_DECHUNK, drop_chunked_trailers);
} else {
- init_by_action(buffer_in, ACTION_PASSTHRU);
+ init_by_action(buffer_in, ACTION_PASSTHRU, drop_chunked_trailers);
}
return;
}
void
-ChunkedHandler::init_by_action(IOBufferReader *buffer_in, Action action)
+ChunkedHandler::init_by_action(IOBufferReader *buffer_in, Action action, bool
drop_chunked_trailers)
{
- running_sum = 0;
- num_digits = 0;
- cur_chunk_size = 0;
- bytes_left = 0;
- truncation = false;
- this->action = action;
+ running_sum = 0;
+ num_digits = 0;
+ cur_chunk_size = 0;
+ cur_chunk_bytes_left = 0;
+ truncation = false;
+ this->action = action;
switch (action) {
case ACTION_DOCHUNK:
@@ -103,6 +103,18 @@ ChunkedHandler::init_by_action(IOBufferReader *buffer_in,
Action action)
break;
case ACTION_PASSTHRU:
chunked_reader = buffer_in->mbuf->clone_reader(buffer_in);
+ if (drop_chunked_trailers) {
+ // Note that dropping chunked trailers only applies in the passthrough
+ // case in which we are filtering out chunked trailers as we proxy.
+ this->drop_chunked_trailers = drop_chunked_trailers;
+
+ // We only need an intermediate buffer when modifying the chunks by
+ // filtering out the trailers. Otherwise, a simple passthrough needs no
+ // intermediary buffer as consumers will simply read directly from
+ // chunked_reader.
+ chunked_buffer = new_MIOBuffer(CHUNK_IOBUFFER_SIZE_INDEX);
+ chunked_size = 0;
+ }
break;
default:
ink_release_assert(!"Unknown action");
@@ -116,12 +128,14 @@ ChunkedHandler::clear()
{
switch (action) {
case ACTION_DOCHUNK:
- free_MIOBuffer(chunked_buffer);
+ case ACTION_PASSTHRU:
+ if (chunked_buffer) {
+ free_MIOBuffer(chunked_buffer);
+ }
break;
case ACTION_DECHUNK:
free_MIOBuffer(dechunked_buffer);
break;
- case ACTION_PASSTHRU:
default:
break;
}
@@ -184,9 +198,9 @@ ChunkedHandler::read_size()
} else if (state == CHUNK_READ_SIZE_CRLF) { // Scan for a linefeed
if (ParseRules::is_lf(*tmp)) {
Debug("http_chunk", "read chunk size of %d bytes", running_sum);
- bytes_left = (cur_chunk_size = running_sum);
- state = (running_sum == 0) ? CHUNK_READ_TRAILER_BLANK :
CHUNK_READ_CHUNK;
- done = true;
+ cur_chunk_bytes_left = (cur_chunk_size = running_sum);
+ state = (running_sum == 0) ? CHUNK_READ_TRAILER_BLANK
: CHUNK_READ_CHUNK;
+ done = true;
break;
}
} else if (state == CHUNK_READ_SIZE_START) {
@@ -199,15 +213,19 @@ ChunkedHandler::read_size()
tmp++;
data_size--;
}
+ if (drop_chunked_trailers) {
+ chunked_buffer->write(chunked_reader, bytes_used);
+ chunked_size += bytes_used;
+ }
chunked_reader->consume(bytes_used);
}
}
// int ChunkedHandler::transfer_bytes()
//
-// Transfer bytes from chunked_reader to dechunked buffer
+// Transfer bytes from chunked_reader to dechunked buffer.
// Use block reference method when there is a sufficient
-// size to move. Otherwise, uses memcpy method
+// size to move. Otherwise, uses memcpy method.
//
int64_t
ChunkedHandler::transfer_bytes()
@@ -216,22 +234,26 @@ ChunkedHandler::transfer_bytes()
// Handle the case where we are doing chunked passthrough.
if (!dechunked_buffer) {
- moved = std::min(bytes_left, chunked_reader->read_avail());
+ moved = std::min(cur_chunk_bytes_left, chunked_reader->read_avail());
+ if (drop_chunked_trailers) {
+ chunked_buffer->write(chunked_reader, moved);
+ chunked_size += moved;
+ }
chunked_reader->consume(moved);
- bytes_left = bytes_left - moved;
+ cur_chunk_bytes_left = cur_chunk_bytes_left - moved;
return moved;
}
- while (bytes_left > 0) {
+ while (cur_chunk_bytes_left > 0) {
block_read_avail = chunked_reader->block_read_avail();
- to_move = std::min(bytes_left, block_read_avail);
+ to_move = std::min(cur_chunk_bytes_left, block_read_avail);
if (to_move <= 0) {
break;
}
if (to_move >= min_block_transfer_bytes) {
- moved = dechunked_buffer->write(chunked_reader, bytes_left);
+ moved = dechunked_buffer->write(chunked_reader, cur_chunk_bytes_left);
} else {
// Small amount of data available. We want to copy the
// data rather than block reference to prevent the buildup
@@ -242,7 +264,7 @@ ChunkedHandler::transfer_bytes()
if (moved > 0) {
chunked_reader->consume(moved);
- bytes_left = bytes_left - moved;
+ cur_chunk_bytes_left = cur_chunk_bytes_left - moved;
dechunked_size += moved;
total_moved += moved;
} else {
@@ -257,12 +279,12 @@ ChunkedHandler::read_chunk()
{
int64_t b = transfer_bytes();
- ink_assert(bytes_left >= 0);
- if (bytes_left == 0) {
+ ink_assert(cur_chunk_bytes_left >= 0);
+ if (cur_chunk_bytes_left == 0) {
Debug("http_chunk", "completed read of chunk of %" PRId64 " bytes",
cur_chunk_size);
state = CHUNK_READ_SIZE_START;
- } else if (bytes_left > 0) {
+ } else if (cur_chunk_bytes_left > 0) {
Debug("http_chunk", "read %" PRId64 " bytes of an %" PRId64 " chunk", b,
cur_chunk_size);
}
}
@@ -293,6 +315,13 @@ ChunkedHandler::read_trailer()
if (state == CHUNK_READ_TRAILER_CR || state ==
CHUNK_READ_TRAILER_BLANK) {
state = CHUNK_READ_DONE;
Debug("http_chunk", "completed read of trailers");
+
+ if (this->drop_chunked_trailers) {
+ // We skip passing through chunked trailers to the peer and only
write
+ // the final CRLF that ends all chunked content.
+ chunked_buffer->write(FINAL_CRLF.data(), FINAL_CRLF.size());
+ chunked_size += FINAL_CRLF.size();
+ }
done = true;
break;
} else {
@@ -628,10 +657,12 @@ HttpTunnel::deallocate_buffers()
}
void
-HttpTunnel::set_producer_chunking_action(HttpTunnelProducer *p, int64_t
skip_bytes, TunnelChunkingAction_t action)
+HttpTunnel::set_producer_chunking_action(HttpTunnelProducer *p, int64_t
skip_bytes, TunnelChunkingAction_t action,
+ bool drop_chunked_trailers)
{
- p->chunked_handler.skip_bytes = skip_bytes;
- p->chunking_action = action;
+ this->http_drop_chunked_trailers = drop_chunked_trailers;
+ p->chunked_handler.skip_bytes = skip_bytes;
+ p->chunking_action = action;
switch (action) {
case TCA_CHUNK_CONTENT:
@@ -840,9 +871,11 @@ HttpTunnel::producer_run(HttpTunnelProducer *p)
ink_assert(p->vc != nullptr);
active = true;
- IOBufferReader *chunked_buffer_start = nullptr, *dechunked_buffer_start =
nullptr;
+ IOBufferReader *chunked_buffer_start = nullptr;
+ IOBufferReader *dechunked_buffer_start = nullptr;
+ IOBufferReader *passthrough_buffer_start = nullptr;
if (p->do_chunking || p->do_dechunking || p->do_chunked_passthru) {
- p->chunked_handler.init(p->buffer_start, p);
+ p->chunked_handler.init(p->buffer_start, p,
this->http_drop_chunked_trailers);
// Copy the header into the chunked/dechunked buffers.
if (p->do_chunking) {
@@ -865,6 +898,11 @@ HttpTunnel::producer_run(HttpTunnelProducer *p)
Debug("http_tunnel", "[producer_run] do_dechunking::Copied header of
size %" PRId64 "", p->chunked_handler.skip_bytes);
}
}
+ if (p->chunked_handler.drop_chunked_trailers) {
+ // initialize a reader to passthrough buffer start before writing to
keep ref count
+ passthrough_buffer_start =
p->chunked_handler.chunked_buffer->alloc_reader();
+ p->chunked_handler.chunked_buffer->write(p->buffer_start,
p->chunked_handler.skip_bytes);
+ }
}
int64_t read_start_pos = 0;
@@ -904,7 +942,13 @@ HttpTunnel::producer_run(HttpTunnelProducer *p)
c->buffer_reader =
p->chunked_handler.chunked_buffer->clone_reader(chunked_buffer_start);
} else if (action == TCA_DECHUNK_CONTENT) {
c->buffer_reader =
p->chunked_handler.dechunked_buffer->clone_reader(dechunked_buffer_start);
- } else {
+ } else if (action == TCA_PASSTHRU_CHUNKED_CONTENT) {
+ if (p->chunked_handler.drop_chunked_trailers) {
+ c->buffer_reader =
p->chunked_handler.chunked_buffer->clone_reader(passthrough_buffer_start);
+ } else {
+ c->buffer_reader = p->read_buffer->clone_reader(p->buffer_start);
+ }
+ } else { // TCA_PASSTHRU_DECHUNKED_CONTENT
c->buffer_reader = p->read_buffer->clone_reader(p->buffer_start);
}
@@ -949,6 +993,9 @@ HttpTunnel::producer_run(HttpTunnelProducer *p)
if (p->do_dechunking && dechunked_buffer_start) {
p->chunked_handler.dechunked_buffer->dealloc_reader(dechunked_buffer_start);
}
+ if (p->do_chunked_passthru && passthrough_buffer_start) {
+
p->chunked_handler.chunked_buffer->dealloc_reader(passthrough_buffer_start);
+ }
// bz57413
// If there is no transformation plugin, then we didn't add the header,
hence no need to consume it
diff --git a/proxy/http/HttpTunnel.h b/proxy/http/HttpTunnel.h
index 4d2f6f0c12..9d7a297168 100644
--- a/proxy/http/HttpTunnel.h
+++ b/proxy/http/HttpTunnel.h
@@ -104,15 +104,22 @@ struct ChunkedHandler {
MIOBuffer *chunked_buffer;
int64_t chunked_size;
+ /** When passing through chunked content, filter out chunked trailers.
+ *
+ * @note this is only true when: (1) we are passing through chunked content
+ * and (2) we are configured to filter out chunked trailers.
+ */
+ bool drop_chunked_trailers = false;
+
bool truncation;
int64_t skip_bytes;
ChunkedState state;
int64_t cur_chunk_size;
- int64_t bytes_left;
+ int64_t cur_chunk_bytes_left;
int last_server_event;
- // Parsing Info
+ // Chunked header size parsing info.
int running_sum;
int num_digits;
@@ -129,8 +136,8 @@ struct ChunkedHandler {
//@}
ChunkedHandler();
- void init(IOBufferReader *buffer_in, HttpTunnelProducer *p);
- void init_by_action(IOBufferReader *buffer_in, Action action);
+ void init(IOBufferReader *buffer_in, HttpTunnelProducer *p, bool
drop_chunked_trailers);
+ void init_by_action(IOBufferReader *buffer_in, Action action, bool
drop_chunked_trailers);
void clear();
/// Set the max chunk @a size.
@@ -146,6 +153,8 @@ private:
void read_chunk();
void read_trailer();
int64_t transfer_bytes();
+
+ constexpr static std::string_view FINAL_CRLF = "\r\n";
};
struct HttpTunnelConsumer {
@@ -287,7 +296,19 @@ public:
HttpTunnelProducer *add_producer(VConnection *vc, int64_t nbytes,
IOBufferReader *reader_start, HttpProducerHandler sm_handler,
HttpTunnelType_t vc_type, const char *name);
- void set_producer_chunking_action(HttpTunnelProducer *p, int64_t skip_bytes,
TunnelChunkingAction_t action);
+ /// A named variable for the @a drop_chunked_trailers parameter to @a
set_producer_chunking_action.
+ static constexpr bool DROP_CHUNKED_TRAILERS = true;
+
+ /** Configure how the producer should behave with chunked content.
+ * @param[in] p Producer to configure.
+ * @param[in] skip_bytes Number of bytes to skip at the beginning of the
stream (typically the headers).
+ * @param[in] action Action to take with the chunked content.
+ * @param[in] drop_chunked_trailers If @c true, chunked trailers are filtered
+ * out. Logically speaking, this is only applicable when proxying chunked
+ * content, thus only when @a action is @c TCA_PASSTHRU_CHUNKED_CONTENT.
+ */
+ void set_producer_chunking_action(HttpTunnelProducer *p, int64_t skip_bytes,
TunnelChunkingAction_t action,
+ bool drop_chunked_trailers);
/// Set the maximum (preferred) chunk @a size of chunked output for @a
producer.
void set_producer_chunking_size(HttpTunnelProducer *producer, int64_t size);
@@ -355,6 +376,9 @@ private:
private:
int reentrancy_count = 0;
bool call_sm = false;
+
+ /// Corresponds to proxy.config.http.drop_chunked_trailers having a value of
1.
+ bool http_drop_chunked_trailers = false;
};
// void HttpTunnel::abort_cache_write_finish_others
diff --git a/src/traffic_server/FetchSM.cc b/src/traffic_server/FetchSM.cc
index 436405fa34..474d31c935 100644
--- a/src/traffic_server/FetchSM.cc
+++ b/src/traffic_server/FetchSM.cc
@@ -197,7 +197,7 @@ FetchSM::check_chunked()
if (resp_is_chunked && (fetch_flags & TS_FETCH_FLAGS_DECHUNK)) {
ChunkedHandler *ch = &chunked_handler;
- ch->init_by_action(resp_reader, ChunkedHandler::ACTION_DECHUNK);
+ ch->init_by_action(resp_reader, ChunkedHandler::ACTION_DECHUNK,
HttpTunnel::DROP_CHUNKED_TRAILERS);
ch->dechunked_reader = ch->dechunked_buffer->alloc_reader();
ch->state = ChunkedHandler::CHUNK_READ_SIZE;
resp_reader->dealloc();
diff --git a/src/traffic_server/InkAPI.cc b/src/traffic_server/InkAPI.cc
index 691ff9dc45..2d3ba4fac3 100644
--- a/src/traffic_server/InkAPI.cc
+++ b/src/traffic_server/InkAPI.cc
@@ -24,6 +24,7 @@
#include <cstdio>
#include <atomic>
+#include "ts/apidefs.h"
#include "tscore/ink_platform.h"
#include "tscore/ink_base64.h"
#include "tscore/I_Layout.h"
@@ -8098,6 +8099,9 @@ _conf_to_memberp(TSOverridableConfigKey conf,
OverridableHttpConfigParams *overr
case TS_CONFIG_HTTP_CHUNKING_SIZE:
ret = _memberp_to_generic(&overridableHttpConfig->http_chunking_size,
typep);
break;
+ case TS_CONFIG_HTTP_DROP_CHUNKED_TRAILERS:
+ ret =
_memberp_to_generic(&overridableHttpConfig->http_drop_chunked_trailers, typep);
+ break;
case TS_CONFIG_HTTP_FLOW_CONTROL_ENABLED:
ret = _memberp_to_generic(&overridableHttpConfig->flow_control_enabled,
typep);
break;
@@ -8623,6 +8627,9 @@ TSHttpTxnConfigFind(const char *name, int length,
TSOverridableConfigKey *conf,
if (!strncmp(name, "proxy.config.http.doc_in_cache_skip_dns", length)) {
cnf = TS_CONFIG_HTTP_DOC_IN_CACHE_SKIP_DNS;
}
+ if (!strncmp(name, "proxy.config.http.drop_chunked_trailers", length)) {
+ cnf = TS_CONFIG_HTTP_DROP_CHUNKED_TRAILERS;
+ }
break;
}
break;
diff --git a/src/traffic_server/InkAPITest.cc b/src/traffic_server/InkAPITest.cc
index b56fd553f8..c7c0c6ff64 100644
--- a/src/traffic_server/InkAPITest.cc
+++ b/src/traffic_server/InkAPITest.cc
@@ -8704,7 +8704,8 @@ const char *SDK_Overridable_Configs[TS_CONFIG_LAST_ENTRY]
= {"proxy.config.url_r
"proxy.config.http.insert_forwarded",
"proxy.config.http.allow_multi_range",
"proxy.config.http.request_buffer_enabled",
-
"proxy.config.http.allow_half_open"};
+
"proxy.config.http.allow_half_open",
+
"proxy.config.http.drop_chunked_trailers"};
REGRESSION_TEST(SDK_API_OVERRIDABLE_CONFIGS)(RegressionTest *test, int /*
atype ATS_UNUSED */, int *pstatus)
{
diff --git a/tests/gold_tests/chunked_encoding/chunked_encoding.test.py
b/tests/gold_tests/chunked_encoding/chunked_encoding.test.py
index 1a7cde79fd..89c3354686 100644
--- a/tests/gold_tests/chunked_encoding/chunked_encoding.test.py
+++ b/tests/gold_tests/chunked_encoding/chunked_encoding.test.py
@@ -129,3 +129,99 @@ tr.Processes.Default.Command = 'curl http://127.0.0.1:{0}
-H "Host: www.yetanoth
tr.Processes.Default.ReturnCode = 0
tr.Processes.Default.Streams.stderr = "gold/chunked_POST_200.gold"
tr.StillRunningAfter = server
+
+
+class TestChunkedTrailers:
+ """Verify chunked trailer proxy behavior."""
+
+ _chunked_dropped_replay: str =
"replays/chunked_trailer_dropped.replay.yaml"
+ _proxied_dropped_replay: str =
"replays/chunked_trailer_proxied.replay.yaml"
+
+ def __init__(self, configure_drop_trailers: bool):
+ """Create a test to verify chunked trailer behavior.
+
+ :param configure_drop_trailers: Whether to configure ATS to drop
+ trailers or not.
+ """
+ self._configure_drop_trailers = configure_drop_trailers
+ self._replay_file = self._chunked_dropped_replay if
configure_drop_trailers else self._proxied_dropped_replay
+ behavior_description = "drop" if configure_drop_trailers else "proxy"
+ tr = Test.AddTestRun(f'Verify chunked tailers behavior:
{behavior_description}')
+ self._configure_dns(tr)
+ self._configure_server(tr)
+ self._configure_ts(tr)
+ self._configure_client(tr)
+
+ def _configure_dns(self, tr: 'TestRun') -> "Process":
+ """Configure DNS for the test run.
+
+ :param tr: The TestRun to configure DNS for.
+ :return: The DNS process.
+ """
+ name = 'dns-drop-trailers' if self._configure_drop_trailers else
'dns-proxy-trailers'
+ self._dns = tr.MakeDNServer(name, default='127.0.0.1')
+ return self._dns
+
+ def _configure_server(self, tr: 'TestRun') -> 'Process':
+ """Configure the origin server for the test run.
+
+ :param tr: The TestRun to configure the server for.
+ :return: The origin server process.
+ """
+ name = 'server-drop-trailers' if self._configure_drop_trailers else
'server-proxy-trailers'
+ self._server = tr.AddVerifierServerProcess(name, self._replay_file)
+ if self._configure_drop_trailers:
+ self._server.Streams.All += Testers.ExcludesExpression('Client:
ATS', 'Verify the Client trailer was dropped.')
+ self._server.Streams.All += Testers.ExcludesExpression('ETag:
"abc"', 'Verify the ETag trailer was dropped.')
+ else:
+ self._server.Streams.All += Testers.ContainsExpression('Client:
ATS', 'Verify the Client trailer was proxied.')
+ self._server.Streams.All += Testers.ContainsExpression('ETag:
"abc"', 'Verify the ETag trailer was proxied.')
+ return self._server
+
+ def _configure_ts(self, tr: 'TestRun') -> 'Process':
+ """Configure ATS for the test run.
+
+ :param tr: The TestRun to configure ATS for.
+ :return: The ATS process.
+ """
+ name = 'ts-drop-trailers' if self._configure_drop_trailers else
'ts-proxy-trailers'
+ ts = tr.MakeATSProcess(name, enable_cache=False)
+ self._ts = ts
+ port = self._server.Variables.http_port
+ ts.Disk.remap_config.AddLine(f'map /
http://backend.example.com:{port}/')
+ ts.Disk.records_config.update(
+ {
+ 'proxy.config.diags.debug.enabled': 1,
+ 'proxy.config.diags.debug.tags': 'http',
+ 'proxy.config.dns.nameservers':
f'127.0.0.1:{self._dns.Variables.Port}',
+ 'proxy.config.dns.resolv_conf': 'NULL'
+ })
+ if self._configure_drop_trailers:
+ ts.Disk.records_config.update({
+ 'proxy.config.http.drop_chunked_trailers': 1,
+ })
+ return ts
+
+ def _configure_client(self, tr: 'TestRun') -> 'Process':
+ """Configure the client for the test run.
+
+ :param tr: The TestRun to configure the client for.
+ :return: The client process.
+ """
+ name = 'client-drop-trailers' if self._configure_drop_trailers else
'client-proxy-trailers'
+ self._client = tr.AddVerifierClientProcess(name, self._replay_file,
http_ports=[self._ts.Variables.port])
+ self._client.StartBefore(self._dns)
+ self._client.StartBefore(self._server)
+ self._client.StartBefore(self._ts)
+
+ if self._configure_drop_trailers:
+ self._client.Streams.All += Testers.ExcludesExpression('Sever:
ATS', 'Verify the Server trailer was dropped.')
+ self._client.Streams.All += Testers.ExcludesExpression('ETag:
"def"', 'Verify the ETag trailer was dropped.')
+ else:
+ self._client.Streams.All += Testers.ContainsExpression('Sever:
ATS', 'Verify the Server trailer was proxied.')
+ self._client.Streams.All += Testers.ContainsExpression('ETag:
"def"', 'Verify the ETag trailer was proxied.')
+ return self._client
+
+
+TestChunkedTrailers(configure_drop_trailers=True)
+TestChunkedTrailers(configure_drop_trailers=False)
diff --git
a/tests/gold_tests/chunked_encoding/replays/chunked_trailer_dropped.replay.yaml
b/tests/gold_tests/chunked_encoding/replays/chunked_trailer_dropped.replay.yaml
new file mode 100644
index 0000000000..9dcc5cf8b2
--- /dev/null
+++
b/tests/gold_tests/chunked_encoding/replays/chunked_trailer_dropped.replay.yaml
@@ -0,0 +1,68 @@
+# Licensed to the Apache Software Foundation (ASF) under one
+# or more contributor license agreements. See the NOTICE file
+# distributed with this work for additional information
+# regarding copyright ownership. The ASF licenses this file
+# to you under the Apache License, Version 2.0 (the
+# "License"); you may not use this file except in compliance
+# with the License. You may obtain a copy of the License at
+#
+# http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+
+meta:
+ version: "1.0"
+
+# Verify that we handle dropping chunked trailers correctly. This assumes ATS
is
+# configured to drop chunked trailers.
+
+sessions:
+- transactions:
+ - client-request:
+ method: "POST"
+ version: "1.1"
+ url: /some/path
+ headers:
+ fields:
+ - [ Host, example.com ]
+ - [ Transfer-Encoding, chunked ]
+ - [ uuid, 1 ]
+ content:
+ transfer: plain
+ encoding: uri
+ # 3-byte chunk, abc.
+ # Then chunked trailers between 0\r\n and a final \r\n (per
specification).
+ data:
3%0D%0Aabc%0D%0A0%0D%0AClient%3A%20ATS%0D%0AETag%3A%20%22abc%22%0D%0A%0D%0A
+
+ proxy-request:
+ content:
+ transfer: plain
+ encoding: uri
+ # Note: same as client-request, but the trailer is dropped.
+ data: 3%0D%0Aabc%0D%0A0%0D%0A%0D%0A
+ verify: { as: equal }
+
+ server-response:
+ status: 200
+ reason: OK
+ headers:
+ fields:
+ - [ Transfer-Encoding, chunked ]
+ - [ Content-Type, text/html ]
+ content:
+ transfer: plain
+ encoding: uri
+ # Note: same content as the client-request.
+ data:
3%0D%0Aabc%0D%0A0%0D%0ASever%3A%20ATS%0D%0AETag%3A%20%22def%22%0D%0A%0D%0A
+
+ proxy-request:
+ content:
+ transfer: plain
+ encoding: uri
+ # Note: same as server-response, but the trailer is dropped.
+ data: 3%0D%0Aabc%0D%0A0%0D%0A%0D%0A
+ verify: { as: equal }
diff --git
a/tests/gold_tests/chunked_encoding/replays/chunked_trailer_proxied.replay.yaml
b/tests/gold_tests/chunked_encoding/replays/chunked_trailer_proxied.replay.yaml
new file mode 100644
index 0000000000..8ecf513ffa
--- /dev/null
+++
b/tests/gold_tests/chunked_encoding/replays/chunked_trailer_proxied.replay.yaml
@@ -0,0 +1,68 @@
+# Licensed to the Apache Software Foundation (ASF) under one
+# or more contributor license agreements. See the NOTICE file
+# distributed with this work for additional information
+# regarding copyright ownership. The ASF licenses this file
+# to you under the Apache License, Version 2.0 (the
+# "License"); you may not use this file except in compliance
+# with the License. You may obtain a copy of the License at
+#
+# http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+
+meta:
+ version: "1.0"
+
+# Verify that we handle passing through chunked trailers correctly. This
assumes
+# ATS is configured to pass through (i.e., not drop) chunked trailers.
+
+sessions:
+- transactions:
+ - client-request:
+ method: "POST"
+ version: "1.1"
+ url: /some/path
+ headers:
+ fields:
+ - [ Host, example.com ]
+ - [ Transfer-Encoding, chunked ]
+ - [ uuid, 1 ]
+ content:
+ transfer: plain
+ encoding: uri
+ # 3-byte chunk, abc.
+ # Then chunked trailers between 0\r\n and a final \r\n (per
specification).
+ data:
3%0D%0Aabc%0D%0A0%0D%0AClient%3A%20ATS%0D%0AETag%3A%20%22abc%22%0D%0A%0D%0A
+
+ proxy-request:
+ content:
+ transfer: plain
+ encoding: uri
+ # Same content as client-request above.
+ data:
3%0D%0Aabc%0D%0A0%0D%0AClient%3A%20ATS%0D%0AETag%3A%20%22abc%22%0D%0A%0D%0A
+ verify: { as: equal }
+
+ server-response:
+ status: 200
+ reason: OK
+ headers:
+ fields:
+ - [ Transfer-Encoding, chunked ]
+ - [ Content-Type, text/html ]
+ content:
+ transfer: plain
+ encoding: uri
+ # Note: same content as the client-request.
+ data:
3%0D%0Aabc%0D%0A0%0D%0ASever%3A%20ATS%0D%0AETag%3A%20%22def%22%0D%0A%0D%0A
+
+ proxy-request:
+ content:
+ transfer: plain
+ encoding: uri
+ # Same content as server-response above.
+ data:
3%0D%0Aabc%0D%0A0%0D%0ASever%3A%20ATS%0D%0AETag%3A%20%22def%22%0D%0A%0D%0A
+ verify: { as: equal }