Skip to content

Commit fa9178b

Browse files
bneradtbneradt
authored andcommitted
Log malformed HTTP/2 requests
Unlike HTTP/1 transactions, malformed HTTP/2 requests are rejected before HttpSM creation, so they bypassed the normal transaction logging path. That left malformed h2 traffic out of squid.log even when similar h1 failures were visible. This adds a pre-transaction LogAccess path for malformed h2 request headers and emits a best-effort access log entry before resetting the stream.
1 parent 0719e5f commit fa9178b

File tree

9 files changed

+1259
-140
lines changed

9 files changed

+1259
-140
lines changed

include/proxy/http2/Http2Stream.h

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -138,6 +138,12 @@ class Http2Stream : public ProxyTransaction
138138
return &_send_header;
139139
}
140140

141+
HTTPHdr const *
142+
get_receive_header() const
143+
{
144+
return &_receive_header;
145+
}
146+
141147
void update_read_length(int count);
142148
void set_read_done();
143149

include/proxy/logging/LogAccess.h

Lines changed: 75 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -25,13 +25,16 @@
2525
#pragma once
2626

2727
#include "tscore/ink_align.h"
28+
#include "proxy/Milestones.h"
29+
#include "proxy/hdrs/HTTP.h"
2830
#include "proxy/logging/LogField.h"
2931

30-
class HTTPHdr;
3132
class HttpSM;
3233
class IpClass;
3334
union IpEndpoint;
3435

36+
#include <string>
37+
3538
/*-------------------------------------------------------------------------
3639
LogAccess
3740
@@ -113,8 +116,56 @@ enum LogCacheWriteCodeType {
113116
class LogAccess
114117
{
115118
public:
119+
/** Data used to access-log requests that fail before transaction creation.
120+
*
121+
* Malformed HTTP/2 request headers can be rejected while the connection is
122+
* still decoding and validating the stream, before the request progresses
123+
* far enough to create an @c HttpSM. This payload carries the copied request
124+
* and session metadata needed to emit a best-effort transaction log entry
125+
* for those failures.
126+
*/
127+
struct PreTransactionLogData {
128+
TransactionMilestones milestones;
129+
HTTPHdr client_request;
130+
131+
std::string method;
132+
std::string scheme;
133+
std::string authority;
134+
std::string path;
135+
std::string url;
136+
std::string client_protocol = "http/2";
137+
138+
sockaddr_storage client_addr = {};
139+
sockaddr_storage local_addr = {};
140+
sockaddr_storage verified_client_addr = {};
141+
142+
int64_t client_request_body_bytes = 0;
143+
int64_t server_transact_count = 0;
144+
SquidLogCode log_code = SquidLogCode::ERR_INVALID_REQ;
145+
SquidSubcode subcode = SquidSubcode::EMPTY;
146+
SquidHitMissCode hit_miss_code = SQUID_MISS_NONE;
147+
SquidHierarchyCode hier_code = SquidHierarchyCode::NONE;
148+
149+
bool has_client_request = false;
150+
bool has_verified_client_addr = false;
151+
bool client_tcp_reused = false;
152+
bool client_connection_is_ssl = false;
153+
bool client_ssl_reused = false;
154+
bool is_internal = false;
155+
156+
~PreTransactionLogData()
157+
{
158+
if (client_request.valid()) {
159+
client_request.destroy();
160+
}
161+
}
162+
};
163+
116164
LogAccess() = delete;
117165
explicit LogAccess(HttpSM *sm);
166+
// The caller retains ownership of @a data, which must outlive init() and
167+
// the synchronous Log::access() call that marshals this entry.
168+
explicit LogAccess(PreTransactionLogData const &data);
118169

119170
~LogAccess() {}
120171
void init();
@@ -376,7 +427,29 @@ class LogAccess
376427
LogAccess &operator=(LogAccess &rhs) = delete; // or assignment
377428

378429
private:
379-
HttpSM *m_http_sm = nullptr;
430+
/** Check whether this accessor is backed by a live @c HttpSM.
431+
* @return @c true if normal transaction logging state is available.
432+
*/
433+
bool has_http_sm() const;
434+
435+
/** Check whether this accessor is logging before @c HttpSM creation.
436+
*
437+
* This mode is used when a request did not progress far enough to create an
438+
* @c HttpSM, but ATS still wants to emit a best-effort access log entry.
439+
*
440+
* @return @c true if this instance is backed by pre-transaction log data.
441+
*/
442+
bool is_pre_transaction_logging() const;
443+
444+
PreTransactionLogData const *get_pre_transaction_log_data() const;
445+
TransactionMilestones const &get_milestones() const;
446+
447+
// Invariant: after init(), exactly one of these members is non-nullptr.
448+
// Normal transaction logging uses m_http_sm, while pre-transaction logging
449+
// uses m_pre_transaction_log_data for requests rejected before HttpSM
450+
// creation.
451+
HttpSM *m_http_sm = nullptr;
452+
PreTransactionLogData const *m_pre_transaction_log_data = nullptr;
380453

381454
Arena m_arena;
382455

src/proxy/http2/Http2ConnectionState.cc

Lines changed: 130 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -33,6 +33,8 @@
3333
#include "proxy/http2/Http2DebugNames.h"
3434
#include "proxy/http/HttpDebugNames.h"
3535
#include "proxy/http/HttpSM.h"
36+
#include "proxy/logging/Log.h"
37+
#include "proxy/logging/LogAccess.h"
3638

3739
#include "iocore/net/TLSSNISupport.h"
3840

@@ -76,6 +78,132 @@ const int buffer_size_index[HTTP2_FRAME_TYPE_MAX] = {
7678
BUFFER_SIZE_INDEX_16K, // HTTP2_FRAME_TYPE_CONTINUATION
7779
};
7880

81+
/** Fetch a header field value from a decoded HTTP/2 request.
82+
*
83+
* @param[in] hdr The decoded request header block to inspect.
84+
* @param[in] name The field name to look up.
85+
* @return The field value, or an empty view if the field is absent.
86+
*/
87+
std::string_view
88+
get_header_field_value(HTTPHdr const &hdr, std::string_view name)
89+
{
90+
if (auto const *field = hdr.field_find(name); field != nullptr) {
91+
return field->value_get();
92+
}
93+
94+
return {};
95+
}
96+
97+
/** Build a best-effort request target for access logging.
98+
*
99+
* @param[in] method The decoded request method or :method pseudo-header.
100+
* @param[in] scheme The decoded :scheme pseudo-header, if present.
101+
* @param[in] authority The decoded :authority pseudo-header, if present.
102+
* @param[in] path The decoded :path pseudo-header, if present.
103+
* @return A synthesized request target suitable for transaction logging.
104+
*/
105+
std::string
106+
synthesize_client_request_target(std::string_view method, std::string_view scheme, std::string_view authority,
107+
std::string_view path)
108+
{
109+
if (method == static_cast<std::string_view>(HTTP_METHOD_CONNECT)) {
110+
if (!authority.empty()) {
111+
return std::string(authority);
112+
}
113+
if (!path.empty()) {
114+
return std::string(path);
115+
}
116+
return {};
117+
}
118+
119+
if (!scheme.empty() && !authority.empty()) {
120+
std::string url;
121+
url.reserve(scheme.size() + authority.size() + path.size() + 4);
122+
url.append(scheme);
123+
url.append("://");
124+
url.append(authority);
125+
if (!path.empty()) {
126+
url.append(path);
127+
} else {
128+
url.push_back('/');
129+
}
130+
return url;
131+
}
132+
133+
if (!path.empty()) {
134+
return std::string(path);
135+
}
136+
137+
if (!authority.empty()) {
138+
return std::string(authority);
139+
}
140+
141+
return {};
142+
}
143+
144+
/** Emit a best-effort access log entry for a malformed decoded request.
145+
*
146+
* This is used when the request is rejected before stream processing reaches
147+
* the point of creating an @c HttpSM.
148+
*
149+
* @param[in] stream The stream whose decoded request headers were rejected.
150+
* @return None.
151+
*/
152+
void
153+
log_malformed_request_access(Http2Stream *stream)
154+
{
155+
if (stream == nullptr || stream->get_sm() != nullptr || stream->trailing_header_is_possible() ||
156+
stream->is_outbound_connection()) {
157+
return;
158+
}
159+
160+
auto const *request = stream->get_receive_header();
161+
if (request == nullptr || !request->valid() || request->type_get() != HTTPType::REQUEST) {
162+
return;
163+
}
164+
165+
auto *proxy_session = stream->get_proxy_ssn();
166+
if (proxy_session == nullptr) {
167+
return;
168+
}
169+
170+
LogAccess::PreTransactionLogData data;
171+
data.client_request.create(HTTPType::REQUEST, request->version_get());
172+
data.client_request.copy(request);
173+
data.has_client_request = true;
174+
data.client_connection_is_ssl = proxy_session->ssl() != nullptr;
175+
176+
auto const method = get_header_field_value(*request, PSEUDO_HEADER_METHOD);
177+
auto const scheme = get_header_field_value(*request, PSEUDO_HEADER_SCHEME);
178+
auto const authority = get_header_field_value(*request, PSEUDO_HEADER_AUTHORITY);
179+
auto const path = get_header_field_value(*request, PSEUDO_HEADER_PATH);
180+
181+
if (!method.empty()) {
182+
data.method.assign(method.data(), method.size());
183+
} else {
184+
auto const method_value = const_cast<HTTPHdr *>(request)->method_get();
185+
data.method.assign(method_value.data(), method_value.size());
186+
}
187+
188+
data.scheme.assign(scheme.data(), scheme.size());
189+
data.authority.assign(authority.data(), authority.size());
190+
data.path.assign(path.data(), path.size());
191+
data.url = synthesize_client_request_target(data.method, data.scheme, data.authority, data.path);
192+
193+
ats_ip_copy(reinterpret_cast<sockaddr *>(&data.client_addr), proxy_session->get_remote_addr());
194+
ats_ip_copy(reinterpret_cast<sockaddr *>(&data.local_addr), proxy_session->get_local_addr());
195+
196+
ink_hrtime const now = ink_get_hrtime();
197+
data.milestones[TS_MILESTONE_SM_START] = now;
198+
data.milestones[TS_MILESTONE_UA_BEGIN] = now;
199+
data.milestones[TS_MILESTONE_UA_FIRST_READ] = now;
200+
data.milestones[TS_MILESTONE_UA_READ_HEADER_DONE] = now;
201+
data.milestones[TS_MILESTONE_SM_FINISH] = now;
202+
203+
LogAccess access(data);
204+
Log::access(&access);
205+
}
206+
79207
inline unsigned
80208
read_rcv_buffer(char *buf, size_t bufsize, unsigned &nbytes, const Http2Frame &frame)
81209
{
@@ -496,6 +624,7 @@ Http2ConnectionState::rcv_headers_frame(const Http2Frame &frame)
496624
return Http2Error(Http2ErrorClass::HTTP2_ERROR_CLASS_CONNECTION, Http2ErrorCode::HTTP2_ERROR_ENHANCE_YOUR_CALM,
497625
"recv headers enhance your calm");
498626
} else {
627+
log_malformed_request_access(stream);
499628
return Http2Error(Http2ErrorClass::HTTP2_ERROR_CLASS_STREAM, Http2ErrorCode::HTTP2_ERROR_PROTOCOL_ERROR,
500629
"recv headers malformed request");
501630
}
@@ -1108,6 +1237,7 @@ Http2ConnectionState::rcv_continuation_frame(const Http2Frame &frame)
11081237
return Http2Error(Http2ErrorClass::HTTP2_ERROR_CLASS_CONNECTION, Http2ErrorCode::HTTP2_ERROR_ENHANCE_YOUR_CALM,
11091238
"continuation enhance your calm");
11101239
} else {
1240+
log_malformed_request_access(stream);
11111241
return Http2Error(Http2ErrorClass::HTTP2_ERROR_CLASS_CONNECTION, Http2ErrorCode::HTTP2_ERROR_PROTOCOL_ERROR,
11121242
"continuation malformed request");
11131243
}

src/proxy/logging/CMakeLists.txt

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -48,6 +48,10 @@ if(BUILD_TESTING)
4848
target_compile_definitions(test_RolledLogDeleter PRIVATE TEST_LOG_UTILS)
4949
target_link_libraries(test_RolledLogDeleter tscore ts::inkevent records Catch2::Catch2WithMain)
5050
add_catch2_test(NAME test_RolledLogDeleter COMMAND test_RolledLogDeleter)
51+
52+
add_executable(test_LogAccess unit-tests/test_LogAccess.cc)
53+
target_link_libraries(test_LogAccess ts::logging ts::http ts::inkevent records Catch2::Catch2WithMain)
54+
add_catch2_test(NAME test_LogAccess COMMAND test_LogAccess)
5155
endif()
5256

5357
clang_tidy_check(logging)

0 commit comments

Comments
 (0)