Line data Source code
1 : #include "headers.hpp"
2 : #include <array>
3 : #include <charconv>
4 : #include <cstdint>
5 : #include <cstring>
6 : #include <regex>
7 : #include "syntax.hpp"
8 : #include "../../util/ascii.hpp"
9 : #include "../../util/logger.hpp"
10 :
11 : namespace thinger::http{
12 :
13 302489 : void headers::process_header(std::string key, std::string value){
14 302489 : if(boost::indeterminate(keep_alive_) && is_header(key, header::connection)){
15 : /*
16 : * Firefox send both keep-alive and upgrade values in Connection header, i.e., when opening a WebSocket,
17 : * so it is necessary to test values separately.
18 : */
19 : static constexpr std::string_view whitespace = " \t\n\v\f\r";
20 10638 : std::string_view options = value;
21 : while(true){
22 10743 : auto comma = options.find(',');
23 10743 : auto option = options.substr(0, comma);
24 10743 : auto first = option.find_first_not_of(whitespace);
25 10743 : option = first == std::string_view::npos ? std::string_view{} :
26 10734 : option.substr(first, option.find_last_not_of(whitespace) - first + 1);
27 10743 : if(is_header(option, connection::keep_alive)){
28 2427 : keep_alive_ = true;
29 : }
30 8316 : else if (is_header(option, connection::close)){
31 4135 : keep_alive_ = false;
32 : }
33 4181 : else if(is_header(option, connection::upgrade)){
34 4154 : upgrade_ = true;
35 : }
36 10743 : if(comma == std::string_view::npos) break;
37 105 : options.remove_prefix(comma + 1);
38 105 : }
39 : }
40 :
41 302489 : if(is_header(key, header::accept)){
42 396 : stream_ = ::thinger::util::ascii::iequals(value, accept::event_stream);
43 : }else{
44 302093 : process_framing_header(key, value);
45 : }
46 :
47 302489 : store_header(std::move(key), std::move(value));
48 302489 : }
49 :
50 302093 : void headers::process_framing_header(std::string_view key, std::string_view value){
51 302093 : if(is_header(key, header::content_length)){
52 : // Content-Length = 1*DIGIT: anything else (sign, spaces, lists, overflow) or a
53 : // repeated header with a different value makes the message framing ambiguous
54 76702 : size_t length = 0;
55 76702 : auto [ptr, ec] = std::from_chars(value.data(), value.data() + value.size(), length);
56 76702 : if (ec != std::errc{} || ptr != value.data() + value.size()) {
57 949 : invalid_content_length_ = true;
58 949 : length = 0;
59 75753 : } else if (content_length_header_ && length != content_length_) {
60 69 : invalid_content_length_ = true;
61 : }
62 76702 : content_length_ = length;
63 225391 : }else if(is_header(key, header::transfer_encoding)){
64 : // chunked must be the only coding, in a single header
65 61872 : if(::thinger::util::ascii::iequals(value, "chunked") && !transfer_encoding_header_){
66 1202 : chunked_transfer_ = true;
67 : }else{
68 60670 : invalid_transfer_encoding_ = true;
69 : }
70 : }
71 302093 : }
72 :
73 638304 : void headers::add_header(std::string key, std::string value){
74 638304 : if(key.empty() || !accept_header(key, value)) return;
75 395049 : store_header(std::move(key), std::move(value));
76 : }
77 :
78 876922 : void headers::store_header(std::string&& key, std::string&& value){
79 876922 : on_header_added(key);
80 876922 : headers_.emplace_back(std::move(key), std::move(value));
81 876922 : }
82 :
83 876922 : void headers::on_header_added(std::string_view key){
84 876922 : if(is_header(key, header::content_length)){
85 101241 : content_length_header_ = true;
86 775681 : }else if(is_header(key, header::transfer_encoding)){
87 62308 : transfer_encoding_header_ = true;
88 : }
89 876922 : }
90 :
91 1017 : void headers::on_header_removed(std::string_view key){
92 1017 : if(is_header(key, header::content_length)){
93 731 : content_length_header_ = has_header(header::content_length);
94 286 : }else if(is_header(key, header::transfer_encoding)){
95 18 : transfer_encoding_header_ = has_header(header::transfer_encoding);
96 : }
97 1017 : }
98 :
99 21 : void headers::add_proxy(std::string key, std::string value){
100 21 : if(key.empty() || !accept_header(key, value)) return;
101 15 : proxy_headers_.emplace_back(std::move(key), std::move(value));
102 : }
103 :
104 111833 : void headers::set_header(std::string key, std::string value){
105 111833 : if(key.empty() || !accept_header(key, value)) return;
106 111809 : replace_header(std::move(key), std::move(value));
107 : }
108 :
109 66 : void headers::replace_headers(std::string key, std::string value){
110 114 : if(key.empty() || !accept_header(key, value)) return;
111 30 : auto same_name = [&key](const http_header& header){ return is_header(header.first, key); };
112 57 : auto first = std::find_if(headers_.begin(), headers_.end(), same_name);
113 57 : if(first == headers_.end()){
114 48 : store_header(std::move(key), std::move(value));
115 48 : return;
116 : }
117 9 : first->second = std::move(value);
118 9 : headers_.erase(std::remove_if(first + 1, headers_.end(), same_name), headers_.end());
119 : }
120 :
121 189728 : void headers::replace_header(std::string&& key, std::string&& value){
122 277633 : for(auto & header : headers_)
123 : {
124 98297 : if(is_header(header.first, key)){
125 10392 : header.second = std::move(value);
126 10392 : return;
127 : }
128 : }
129 179336 : store_header(std::move(key), std::move(value));
130 : }
131 :
132 18 : void headers::set_proxy(std::string key, std::string value){
133 18 : if(key.empty() || !accept_header(key, value)) return;
134 9 : for(auto & header : proxy_headers_)
135 : {
136 3 : if(is_header(header.first, key)){
137 3 : header.second = std::move(value);
138 3 : return;
139 : }
140 : }
141 6 : proxy_headers_.emplace_back(std::move(key), std::move(value));
142 : }
143 :
144 18 : bool headers::upgrade() const{
145 18 : return upgrade_;
146 : }
147 :
148 9 : bool headers::stream() const{
149 9 : return stream_;
150 : }
151 :
152 628927 : bool headers::has_header(std::string_view key) const{
153 955481 : for(const auto & header : headers_)
154 : {
155 675224 : if(is_header(header.first, key)){
156 348670 : return true;
157 : }
158 : }
159 280257 : return false;
160 : }
161 :
162 34130 : const std::string& headers::get_header(std::string_view key) const
163 : {
164 103323 : for(const auto & header : headers_)
165 : {
166 90706 : if(is_header(header.first, key)){
167 21513 : return header.second;
168 : }
169 : }
170 12617 : static std::string emtpy;
171 12617 : return emtpy;
172 : }
173 :
174 17404 : std::vector<std::string> headers::get_headers_with_key(std::string_view key) const{
175 17404 : std::vector<std::string> headers;
176 79286 : for(const auto & header : headers_)
177 : {
178 61882 : if(is_header(header.first, key)){
179 6471 : headers.push_back(header.second);
180 : }
181 : }
182 17404 : return headers;
183 0 : }
184 :
185 5830 : const std::vector<headers::http_header>& headers::get_headers() const{
186 5830 : return headers_;
187 : }
188 :
189 50497 : std::vector<headers::http_header>& headers::get_headers(){
190 50497 : return headers_;
191 : }
192 :
193 9 : bool headers::remove_header(std::string_view key)
194 : {
195 9 : for(auto it=headers_.begin(); it!=headers_.end(); ++it){
196 6 : if(is_header(it->first, key)){
197 6 : headers_.erase(it);
198 6 : on_header_removed(key);
199 6 : return true;
200 : }
201 : }
202 3 : return false;
203 : }
204 :
205 17607 : size_t headers::remove_headers(std::string_view key)
206 : {
207 17607 : auto removed = std::erase_if(headers_, [&](const http_header& header){
208 50780 : return is_header(header.first, key);
209 : });
210 17607 : if(removed) on_header_removed(key);
211 17607 : return removed;
212 : }
213 :
214 6 : const std::string& headers::get_authorization() const
215 : {
216 6 : return get_header(header::authorization);
217 : }
218 :
219 6 : const std::string& headers::get_cookie() const
220 : {
221 6 : return get_header(header::cookie);
222 : }
223 :
224 6 : const std::string& headers::get_user_agent() const{
225 6 : return get_header(header::user_agent);
226 : }
227 :
228 2674 : const std::string& headers::get_content_type() const
229 : {
230 2674 : return get_header(header::content_type);
231 : }
232 :
233 9 : bool headers::is_content_type(const std::string& value) const
234 : {
235 9 : return ::thinger::util::ascii::istarts_with(get_header(header::content_type), value);
236 : }
237 :
238 6 : bool headers::empty_headers() const{
239 6 : return headers_.empty();
240 : }
241 :
242 3 : void headers::debug_headers(std::ostream& os) const{
243 9 : for(const std::pair<std::string, std::string>& t: headers_){
244 6 : os << "\t> " << t.first << ": " << t.second << std::endl;
245 : }
246 3 : }
247 :
248 : namespace {
249 :
250 : [[maybe_unused]] constexpr std::string_view redacted = "[redacted]";
251 :
252 : // The first `max_length` bytes of `data` for a log line: control characters escaped,
253 : // so a value cannot forge log lines, and how much is left out
254 245733 : [[maybe_unused]] std::string printable(std::string_view data, size_t max_length) {
255 : static constexpr char hex[] = "0123456789abcdef";
256 245733 : auto shown = data.substr(0, max_length);
257 245733 : std::string out;
258 245733 : out.reserve(shown.size() + 32);
259 4415814 : for (unsigned char c : shown) {
260 4170081 : switch (c) {
261 1188 : case '\n': out += "\\n"; break;
262 1185 : case '\r': out += "\\r"; break;
263 1155 : case '\t': out += "\\t"; break;
264 1152 : case '\\': out += "\\\\"; break;
265 4165401 : default:
266 4165401 : if (c < 0x20 || c == 0x7f) {
267 34572 : out += "\\x";
268 34572 : out += hex[c >> 4];
269 34572 : out += hex[c & 0xf];
270 : } else {
271 4130829 : out += static_cast<char>(c);
272 : }
273 : }
274 : }
275 245733 : if (shown.size() < data.size()) {
276 18 : out += "... (";
277 18 : out += std::to_string(data.size() - shown.size());
278 18 : out += " more bytes)";
279 : }
280 491466 : return out;
281 0 : }
282 :
283 : // The headers whose value is a URI, which may have secrets in its query
284 228 : [[maybe_unused]] bool holds_uri(std::string_view name) {
285 420 : return headers::is_header(name, "Location") || headers::is_header(name, "Content-Location") ||
286 420 : headers::is_header(name, "Referer");
287 : }
288 :
289 2172 : [[maybe_unused]] void log_header(std::string_view label, const std::string& name, const std::string& value,
290 : const body_logging& options) {
291 2172 : if (!options.enabled) {
292 1923 : LOG_DEBUG(" {}{}", label, printable(name, name.size()));
293 249 : } else if (headers::is_sensitive_header(name)) {
294 21 : LOG_DEBUG(" {}{}: {}", label, printable(name, name.size()), redacted);
295 228 : } else if (holds_uri(name)) {
296 : // Redacted first: truncating it may leave a part of the secret unrecognized
297 54 : LOG_DEBUG(" {}{}: {}", label, printable(name, name.size()),
298 : printable(util::url::loggable_uri(value, options.redact), options.max_length));
299 : } else {
300 174 : LOG_DEBUG(" {}{}: {}", label, printable(name, name.size()), printable(value, options.max_length));
301 : }
302 2172 : }
303 :
304 : // syntax::is_token_char of each byte
305 : constexpr auto token_chars = [] {
306 : std::array<bool, 256> table{};
307 : for (unsigned c = 0; c < table.size(); c++) table[c] = syntax::is_token_char(static_cast<unsigned char>(c));
308 : return table;
309 : }();
310 :
311 : // syntax::is_token, with a table lookup per byte
312 750233 : bool valid_name(std::string_view name) {
313 6522397 : for (unsigned char c : name) {
314 5978453 : if (!token_chars[c]) return false;
315 : }
316 543944 : return !name.empty();
317 : }
318 :
319 : // syntax::is_field_value, eight bytes at a time: a word without a byte under 0x20 or
320 : // equal to 0x7F is valid (the exact any-byte tests of "Bit Twiddling Hacks"). A value
321 : // under eight bytes, or with such a byte (HTAB is one), is checked a byte at a time
322 543944 : bool valid_value(std::string_view value) {
323 543944 : constexpr uint64_t ones = 0x0101010101010101u, highs = 0x8080808080808080u;
324 1053432 : auto valid_word = [&value](size_t at) {
325 : uint64_t word;
326 1053432 : std::memcpy(&word, value.data() + at, sizeof word);
327 1053432 : uint64_t del = word ^ (ones * 0x7f);
328 1053432 : return ((((word - ones * 0x20) & ~word) | ((del - ones) & ~del)) & highs) == 0;
329 543944 : };
330 543944 : size_t size = value.size();
331 543944 : if (size >= 8) {
332 420219 : size_t at = 0;
333 1155315 : while (at + 8 <= size && valid_word(at)) at += 8;
334 420219 : if (at + 8 > size && (at == size || valid_word(size - 8))) return true;
335 : }
336 159470 : return syntax::is_field_value(value);
337 : }
338 :
339 : }
340 :
341 750233 : bool headers::accept_header(std::string_view key, std::string_view value) {
342 750233 : if (valid_name(key) && valid_value(value)) [[likely]] return true;
343 : // The value is never logged: it may be a credential
344 243294 : refused_header_ = true;
345 243294 : LOG_DEBUG("Refused header {}: invalid {}", printable(key, 64), syntax::is_token(key) ? "value" : "name");
346 243294 : return false;
347 : }
348 :
349 249 : bool headers::is_sensitive_header(std::string_view name) {
350 732 : return is_header(name, "Authorization") || is_header(name, "Proxy-Authorization") ||
351 732 : is_header(name, "Cookie") || is_header(name, "Set-Cookie");
352 : }
353 :
354 672 : void headers::log_headers(const body_logging& options) const {
355 672 : if (!LOG_DEBUG_ENABLED()) return;
356 2844 : for (const auto& [name, value] : headers_) {
357 2172 : log_header("", name, value, options);
358 : }
359 672 : for (const auto& [name, value] : proxy_headers_) {
360 0 : log_header("(PROXY) ", name, value, options);
361 : }
362 : }
363 :
364 696 : void headers::log_body(std::string_view body, size_t size, const body_logging& options) {
365 696 : if (size == 0 || !LOG_TRACE_ENABLED()) return;
366 297 : LOG_TRACE("Body: {} bytes", size);
367 297 : if (options.enabled && !body.empty()) {
368 39 : LOG_TRACE(" {}", printable(body, options.max_length));
369 : }
370 : }
371 :
372 0 : void headers::log(const char* scope, const body_logging& options) const {
373 0 : if (!LOG_DEBUG_ENABLED()) return;
374 0 : LOG_DEBUG("[{}] Headers:", scope);
375 0 : log_headers(options);
376 : }
377 :
378 157979 : size_t headers::get_content_length() const{
379 157979 : return content_length_;
380 : }
381 :
382 35290 : bool headers::keep_alive() const
383 : {
384 35290 : if(boost::indeterminate(keep_alive_)){
385 28740 : return http_version_major_>=1 && http_version_minor_>=1;
386 : }else{
387 6550 : return (bool) keep_alive_;
388 : }
389 : }
390 :
391 19595 : void headers::set_keep_alive(bool keep_alive){
392 19595 : keep_alive_ = keep_alive;
393 19595 : replace_header(std::string(http::header::connection), keep_alive ? "Keep-Alive" : "Close");
394 19595 : }
395 :
396 41882 : void headers::set_http_version_major(uint8_t http_version_major) {
397 41882 : http_version_major_ = http_version_major;
398 41882 : }
399 :
400 41907 : void headers::set_http_version_minor(uint8_t http_version_minor) {
401 41907 : http_version_minor_ = http_version_minor;
402 41907 : }
403 :
404 7832 : int headers::get_http_version_major() const {
405 7832 : return http_version_major_;
406 : }
407 :
408 4472 : int headers::get_http_version_minor() const {
409 4472 : return http_version_minor_;
410 : }
411 :
412 24 : std::string headers::get_parameter(const std::string& header_value, std::string_view name) {
413 :
414 24 : auto start = header_value.begin();
415 24 : auto end = header_value.end();
416 :
417 30 : if(start==end) return "";
418 :
419 21 : static std::regex cookie_regex("([^;^\\s]+)=\"?([^;^\"]*)\"?");
420 21 : std::smatch what;
421 :
422 36 : while (std::regex_search(start, end, what, cookie_regex))
423 : {
424 66 : std::string key(what[1].first, what[1].second);
425 33 : std::string value(what[2].first, what[2].second);
426 :
427 33 : if(key==name) return value;
428 :
429 15 : start = what[0].second;
430 51 : }
431 :
432 6 : return "";
433 21 : }
434 :
435 : }
|