This is an automated email from the ASF dual-hosted git repository.
bcall pushed a commit to branch 9.2.x
in repository https://gitbox.apache.org/repos/asf/trafficserver.git
The following commit(s) were added to refs/heads/9.2.x by this push:
new b383265368 proxy.config.http.drop_chunked_trailers (#11604)
b383265368 is described below
commit b383265368d65cb7c2e6a1a55a6449c17ff59602
Author: Brian Neradt <[email protected]>
AuthorDate: Tue Jul 23 16:28:07 2024 -0500
proxy.config.http.drop_chunked_trailers (#11604)
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 | 113 +++++++++++++++------
proxy/http/HttpTunnel.h | 40 ++++++--
src/shared/overridable_txn_vars.cc | 1 +
src/traffic_server/FetchSM.cc | 2 +-
src/traffic_server/InkAPI.cc | 3 +
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 +++++++++++++
tools/clang-format.sh | 2 +-
20 files changed, 392 insertions(+), 55 deletions(-)
diff --git a/doc/admin-guide/files/records.config.en.rst
b/doc/admin-guide/files/records.config.en.rst
index 979c8bda2f..f782c71198 100644
--- a/doc/admin-guide/files/records.config.en.rst
+++ b/doc/admin-guide/files/records.config.en.rst
@@ -974,6 +974,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 06db5cb023..0736e114df 100644
--- a/doc/admin-guide/plugins/lua.en.rst
+++ b/doc/admin-guide/plugins/lua.en.rst
@@ -4167,6 +4167,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 94a9583fb7..2ec2983153 100644
--- a/doc/developer-guide/api/functions/TSHttpOverridableConfig.en.rst
+++ b/doc/developer-guide/api/functions/TSHttpOverridableConfig.en.rst
@@ -112,6 +112,7 @@ TSOverridableConfigKey Value
Config
:c:enumerator:`TS_CONFIG_HTTP_CHUNKING_ENABLED`
:ts:cv:`proxy.config.http.chunking_enabled`
:c:enumerator:`TS_CONFIG_HTTP_CHUNKING_SIZE`
:ts:cv:`proxy.config.http.chunking.size`
:c:enumerator:`TS_CONFIG_HTTP_CONNECT_ATTEMPTS_MAX_RETRIES_DEAD_SERVER`
:ts:cv:`proxy.config.http.connect_attempts_max_retries_dead_server`
+:c:enumerator:`TS_CONFIG_HTTP_DROP_CHUNKED_TRAILERS`
:ts:cv:`proxy.config.http.drop_chunked_trailers`
:c:enumerator:`TS_CONFIG_HTTP_CONNECT_ATTEMPTS_MAX_RETRIES`
:ts:cv:`proxy.config.http.connect_attempts_max_retries`
:c:enumerator:`TS_CONFIG_HTTP_CONNECT_ATTEMPTS_RR_RETRIES`
:ts:cv:`proxy.config.http.connect_attempts_rr_retries`
:c:enumerator:`TS_CONFIG_HTTP_CONNECT_ATTEMPTS_TIMEOUT`
:ts:cv:`proxy.config.http.connect_attempts_timeout`
diff --git a/doc/developer-guide/api/types/TSOverridableConfigKey.en.rst
b/doc/developer-guide/api/types/TSOverridableConfigKey.en.rst
index 30000da334..2d0941efde 100644
--- a/doc/developer-guide/api/types/TSOverridableConfigKey.en.rst
+++ b/doc/developer-guide/api/types/TSOverridableConfigKey.en.rst
@@ -91,6 +91,7 @@ Enumeration Members
.. c:enumerator:: TS_CONFIG_NET_SOCK_PACKET_TOS_OUT
.. c:enumerator:: TS_CONFIG_HTTP_INSERT_AGE_IN_RESPONSE
.. c:enumerator:: TS_CONFIG_HTTP_CHUNKING_SIZE
+.. c:enumerator:: TS_CONFIG_HTTP_DROP_CHUNKED_TRAILERS
.. c:enumerator:: TS_CONFIG_HTTP_FLOW_CONTROL_ENABLED
.. c:enumerator:: TS_CONFIG_HTTP_FLOW_CONTROL_LOW_WATER_MARK
.. c:enumerator:: TS_CONFIG_HTTP_FLOW_CONTROL_HIGH_WATER_MARK
diff --git a/include/ts/apidefs.h.in b/include/ts/apidefs.h.in
index 24a26a28e2..1641565a1a 100644
--- a/include/ts/apidefs.h.in
+++ b/include/ts/apidefs.h.in
@@ -874,6 +874,7 @@ typedef enum {
TS_CONFIG_BODY_FACTORY_RESPONSE_SUPPRESSION_MODE,
TS_CONFIG_HTTP_ENABLE_PARENT_TIMEOUT_MARKDOWNS,
TS_CONFIG_HTTP_DISABLE_PARENT_MARKDOWNS,
+ TS_CONFIG_HTTP_DROP_CHUNKED_TRAILERS,
TS_CONFIG_LAST_ENTRY
} TSOverridableConfigKey;
diff --git a/mgmt/RecordsConfig.cc b/mgmt/RecordsConfig.cc
index a3752ea835..8940ab2263 100644
--- a/mgmt/RecordsConfig.cc
+++ b/mgmt/RecordsConfig.cc
@@ -361,6 +361,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 5bcc583dd6..a25d8ab8c8 100644
--- a/plugins/lua/ts_lua_http_config.c
+++ b/plugins/lua/ts_lua_http_config.c
@@ -84,6 +84,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,
@@ -221,6 +222,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 8cd28ed053..d5c1c00a28 100644
--- a/proxy/http/HttpConfig.cc
+++ b/proxy/http/HttpConfig.cc
@@ -1189,6 +1189,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");
@@ -1494,6 +1495,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 c2659836de..6c1763f84e 100644
--- a/proxy/http/HttpConfig.h
+++ b/proxy/http/HttpConfig.h
@@ -701,9 +701,10 @@ struct OverridableHttpConfigParams {
MgmtInt background_fill_active_timeout = 60;
- MgmtInt http_chunking_size = 4096; // Maximum chunk size for chunked
output.
- MgmtInt flow_high_water_mark = 0; ///< Flow control high water mark.
- MgmtInt flow_low_water_mark = 0; ///< Flow control low water mark.
+ MgmtInt http_chunking_size = 4096; // Maximum chunk size for
chunked output.
+ MgmtByte http_drop_chunked_trailers = 0; ///< Whether to drop chunked
trailers.
+ MgmtInt flow_high_water_mark = 0; ///< Flow control high water
mark.
+ MgmtInt flow_low_water_mark = 0; ///< Flow control low water mark.
MgmtInt default_buffer_size_index = 8;
MgmtInt default_buffer_water_mark = 32768;
diff --git a/proxy/http/HttpSM.cc b/proxy/http/HttpSM.cc
index f9b1c1f926..608c6f91c6 100644
--- a/proxy/http/HttpSM.cc
+++ b/proxy/http/HttpSM.cc
@@ -978,7 +978,8 @@ HttpSM::wait_for_full_body()
ua_txn->get_remote_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));
@@ -6161,10 +6162,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);
}
}
@@ -6572,7 +6574,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;
@@ -6889,7 +6892,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;
@@ -6928,7 +6931,8 @@ HttpSM::setup_transfer_from_transform()
this->setup_plugin_agents(p, client_response_hdr_bytes);
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);
}
@@ -6984,7 +6988,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");
@@ -7072,7 +7077,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 25beb78e4c..4b20784f39 100644
--- a/proxy/http/HttpTunnel.cc
+++ b/proxy/http/HttpTunnel.cc
@@ -48,27 +48,27 @@ static int const CHUNK_IOBUFFER_SIZE_INDEX =
MIN_IOBUFFER_SIZE;
ChunkedHandler::ChunkedHandler() : max_chunk_size(DEFAULT_MAX_CHUNK_SIZE) {}
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:
@@ -84,6 +84,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");
@@ -97,12 +109,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;
}
@@ -166,9 +180,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) {
@@ -187,15 +201,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()
@@ -204,22 +222,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
@@ -230,7 +252,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 {
@@ -245,12 +267,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);
}
}
@@ -281,6 +303,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 {
@@ -596,10 +625,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:
@@ -808,9 +839,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) {
@@ -834,6 +867,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;
@@ -873,7 +911,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);
}
@@ -918,6 +962,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 b4a876ef89..3ce994b11e 100644
--- a/proxy/http/HttpTunnel.h
+++ b/proxy/http/HttpTunnel.h
@@ -105,15 +105,22 @@ struct ChunkedHandler {
MIOBuffer *chunked_buffer = nullptr;
int64_t chunked_size = 0;
+ /** 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 = false;
int64_t skip_bytes = 0;
- ChunkedState state = CHUNK_READ_CHUNK;
- int64_t cur_chunk_size = 0;
- int64_t bytes_left = 0;
- int last_server_event = VC_EVENT_NONE;
+ ChunkedState state = CHUNK_READ_CHUNK;
+ int64_t cur_chunk_size = 0;
+ int64_t cur_chunk_bytes_left = 0;
+ int last_server_event = VC_EVENT_NONE;
- // Parsing Info
+ // Chunked header size parsing info.
int running_sum = 0;
int num_digits = 0;
@@ -130,8 +137,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.
@@ -147,6 +154,8 @@ private:
void read_chunk();
void read_trailer();
int64_t transfer_bytes();
+
+ constexpr static std::string_view FINAL_CRLF = "\r\n";
};
struct HttpTunnelConsumer {
@@ -289,7 +298,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);
@@ -364,6 +385,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;
};
////
diff --git a/src/shared/overridable_txn_vars.cc
b/src/shared/overridable_txn_vars.cc
index 5b6f3db581..1a5d740794 100644
--- a/src/shared/overridable_txn_vars.cc
+++ b/src/shared/overridable_txn_vars.cc
@@ -30,6 +30,7 @@ const std::unordered_map<std::string_view, std::tuple<const
TSOverridableConfigK
{"proxy.config.ssl.hsts_max_age", {TS_CONFIG_SSL_HSTS_MAX_AGE,
TS_RECORDDATATYPE_INT}},
{"proxy.config.http.normalize_ae", {TS_CONFIG_HTTP_NORMALIZE_AE,
TS_RECORDDATATYPE_INT}},
{"proxy.config.http.chunking.size", {TS_CONFIG_HTTP_CHUNKING_SIZE,
TS_RECORDDATATYPE_INT}},
+ {"proxy.config.http.drop_chunked_trailers",
{TS_CONFIG_HTTP_DROP_CHUNKED_TRAILERS, TS_RECORDDATATYPE_INT}},
{"proxy.config.ssl.client.cert.path", {TS_CONFIG_SSL_CERT_FILEPATH,
TS_RECORDDATATYPE_STRING}},
{"proxy.config.http.allow_half_open", {TS_CONFIG_HTTP_ALLOW_HALF_OPEN,
TS_RECORDDATATYPE_INT}},
{"proxy.config.http.chunking_enabled", {TS_CONFIG_HTTP_CHUNKING_ENABLED,
TS_RECORDDATATYPE_INT}},
diff --git a/src/traffic_server/FetchSM.cc b/src/traffic_server/FetchSM.cc
index aa73597847..788d5335ac 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 6e3f68fb12..71adb94d0c 100644
--- a/src/traffic_server/InkAPI.cc
+++ b/src/traffic_server/InkAPI.cc
@@ -8925,6 +8925,9 @@ _conf_to_memberp(TSOverridableConfigKey conf,
OverridableHttpConfigParams *overr
case TS_CONFIG_HTTP_CHUNKING_SIZE:
ret = _memberp_to_generic(&overridableHttpConfig->http_chunking_size,
conv);
break;
+ case TS_CONFIG_HTTP_DROP_CHUNKED_TRAILERS:
+ ret =
_memberp_to_generic(&overridableHttpConfig->http_drop_chunked_trailers, conv);
+ break;
case TS_CONFIG_HTTP_FLOW_CONTROL_ENABLED:
ret = _memberp_to_generic(&overridableHttpConfig->flow_control_enabled,
conv);
break;
diff --git a/src/traffic_server/InkAPITest.cc b/src/traffic_server/InkAPITest.cc
index 873d5ba113..a6e7217291 100644
--- a/src/traffic_server/InkAPITest.cc
+++ b/src/traffic_server/InkAPITest.cc
@@ -8773,7 +8773,8 @@ std::array<std::string_view, TS_CONFIG_LAST_ENTRY>
SDK_Overridable_Configs = {
"proxy.config.net.sock_notsent_lowat",
"proxy.config.body_factory.response_suppression_mode",
"proxy.config.http.parent_proxy.enable_parent_timeout_markdowns",
- "proxy.config.http.parent_proxy.disable_parent_markdowns"}};
+ "proxy.config.http.parent_proxy.disable_parent_markdowns",
+ "proxy.config.http.drop_chunked_trailers"}};
extern ClassAllocator<HttpSM> httpSMAllocator;
diff --git a/tests/gold_tests/chunked_encoding/chunked_encoding.test.py
b/tests/gold_tests/chunked_encoding/chunked_encoding.test.py
index e518031d22..ef52d02a29 100644
--- a/tests/gold_tests/chunked_encoding/chunked_encoding.test.py
+++ b/tests/gold_tests/chunked_encoding/chunked_encoding.test.py
@@ -146,3 +146,99 @@ tr.Processes.Default.ReturnCode = 0
tr.Processes.Default.Streams.All =
Testers.ExcludesExpression("content-length:", "Response should not include
content length")
# Transfer encoding to origin, but no content-length
# No extra bytes in body seen by origin
+
+
+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 }
diff --git a/tools/clang-format.sh b/tools/clang-format.sh
index 255ea30ab5..b8063e1de9 100755
--- a/tools/clang-format.sh
+++ b/tools/clang-format.sh
@@ -29,7 +29,7 @@ function main() {
# Check for the option to just install clang-format without running it.
just_install=0
- if [ $1 = "--install" ] ; then
+ if [ $# -gt 0 ] && [ $1 = "--install" ] ; then
just_install=1
if [ $# -ne 1 ] ; then
echo "No other arguments should be used with --install."