Sourcemeta Core 0.0.0
Loading...
Searching...
No Matches
http_message.h
1#ifndef SOURCEMETA_CORE_HTTP_MESSAGE_H_
2#define SOURCEMETA_CORE_HTTP_MESSAGE_H_
3
4#include <sourcemeta/core/http_syntax.h>
5#include <sourcemeta/core/text.h>
6
7#include <concepts> // std::convertible_to, std::invocable
8#include <cstddef> // std::size_t
9#include <optional> // std::nullopt, std::optional
10#include <string> // std::string
11#include <string_view> // std::string_view
12#include <utility> // std::move
13
14namespace sourcemeta::core {
15
27inline constexpr auto http_is_status_line(const std::string_view line) noexcept
28 -> bool {
29 // The prefix cannot open a field line, as RFC 9110 §5.6.2 defines tchar
30 // as "any VCHAR, except delimiters" where delimiters include the slash,
31 // and the protocol name is case-sensitive per RFC 9112 §2.3
32 return line.starts_with("HTTP/");
33}
34
53template <typename Buffer>
54 requires requires(Buffer buffer, std::string_view line) {
55 buffer.clear();
56 buffer.append(line);
57 }
58inline auto http_accumulate_header_line(Buffer &buffer,
59 const std::string_view line) -> void {
60 if (http_is_status_line(line)) {
61 buffer.clear();
62 }
63
64 buffer.append(line);
65}
66
87template <typename Callback>
88 requires std::invocable<Callback, std::string_view, std::string_view>
89inline auto http_parse_headers(const std::string_view input, Callback callback)
90 -> void {
91 std::size_t cursor{input.find("\r\n")};
92 while (cursor != std::string_view::npos) {
93 cursor += 2;
94 const auto end{input.find("\r\n", cursor)};
95 if (end == std::string_view::npos || end == cursor) {
96 break;
97 }
98
99 auto line{input.substr(cursor, end - cursor)};
100 cursor = end;
101
102 // RFC 9112 §5.2 deprecates obs-fold, defined as "OWS CRLF RWS", and
103 // mandates that a user agent "MUST replace each received obs-fold
104 // with one or more SP octets prior to interpreting the field value",
105 // so a line opening with whitespace continues the previous field
106 // line value and is reported with an empty name for the caller to join
107 if (line.front() == ' ' || line.front() == '\t') {
108 while (!line.empty() && (line.front() == ' ' || line.front() == '\t')) {
109 line.remove_prefix(1);
110 }
111
112 while (!line.empty() && (line.back() == ' ' || line.back() == '\t')) {
113 line.remove_suffix(1);
114 }
115
116 // RFC 9110 §5.5: "a recipient of CR, LF, or NUL within a field value
117 // MUST either reject the message or replace each of those characters
118 // with SP", so a continuation carrying a bare one is discarded
120 continue;
121 }
122
123 callback(std::string_view{}, line);
124 continue;
125 }
126
127 const auto parts{split_once(line, ':')};
128 if (!parts.has_value() || parts->first.empty()) {
129 continue;
130 }
131
132 const auto name{parts->first};
133 // RFC 9112 §5.1 mandates that "no whitespace is allowed between the
134 // field name and colon" because in the past such whitespace has "led
135 // to security vulnerabilities", so the field line is discarded
136 if (name.back() == ' ' || name.back() == '\t') {
137 continue;
138 }
139
140 auto value{parts->second};
141 // RFC 9112 §5 defines a field line as
142 // `field-name ":" OWS field-value OWS` where RFC 9110 §5.6.3 defines
143 // optional whitespace as `*( SP / HTAB )`
144 while (!value.empty() && (value.front() == ' ' || value.front() == '\t')) {
145 value.remove_prefix(1);
146 }
147
148 while (!value.empty() && (value.back() == ' ' || value.back() == '\t')) {
149 value.remove_suffix(1);
150 }
151
152 // RFC 9110 §5.5: "a recipient of CR, LF, or NUL within a field value MUST
153 // either reject the message or replace each of those characters with SP",
154 // so a field line whose name or value carries a bare one that survived the
155 // split on the line terminator is discarded
158 continue;
159 }
160
161 callback(name, value);
162 }
163}
164
191template <typename Callback>
192 requires std::invocable<Callback, std::string_view, std::string_view>
193inline auto http_parse_cookies(const std::string_view input, Callback callback)
194 -> void {
195 std::string_view rest{input};
196 while (!rest.empty()) {
197 std::string_view pair;
198 const auto separator{rest.find(';')};
199 if (separator == std::string_view::npos) {
200 pair = rest;
201 rest = {};
202 } else {
203 pair = rest.substr(0, separator);
204 rest = rest.substr(separator + 1);
205 }
206
207 const auto parts{split_once(trim(pair), '=')};
208 if (!parts.has_value()) {
209 continue;
210 }
211
212 const auto name{trim(parts->first)};
213 if (name.empty()) {
214 continue;
215 }
216
217 callback(name, trim(parts->second));
218 }
219}
220
240template <typename Container>
241 requires requires(Container container, std::string_view entry) {
242 container.emplace_back(entry, entry);
243 }
244inline auto http_parse_cookies(const std::string_view input, Container &cookies)
245 -> void {
246 http_parse_cookies(input,
247 [&cookies](const std::string_view name,
248 const std::string_view value) -> void {
249 cookies.emplace_back(name, value);
250 });
251}
252
280template <typename Container>
281 requires requires(Container container, std::string_view entry) {
282 container.emplace_back(entry);
283 }
284inline auto http_cookie_values(const std::string_view input,
285 const std::string_view name, Container &values)
286 -> void {
287 http_parse_cookies(input,
288 [&name, &values](const std::string_view cookie,
289 const std::string_view value) -> void {
290 if (cookie == name) {
291 values.emplace_back(value);
292 }
293 });
294}
295
317template <typename Container>
318 requires requires(Container container, std::string name, std::string value,
319 const char character) {
320 container.emplace_back(std::move(name), std::move(value));
321 { container.empty() } -> std::convertible_to<bool>;
322 container.back().second += character;
323 }
324inline auto http_parse_headers(const std::string_view input, Container &headers)
325 -> void {
326 http_parse_headers(input,
327 [&headers](const std::string_view name,
328 const std::string_view value) -> auto {
329 if (name.empty()) {
330 // RFC 9112 §5.2 mandates replacing "each received
331 // obs-fold with one or more SP octets prior to
332 // interpreting the field value"
333 if (!headers.empty()) {
334 auto &previous_value{headers.back().second};
335 previous_value += ' ';
336 previous_value += value;
337 }
338
339 return;
340 }
341
342 std::string header_name{name};
343 to_lowercase(header_name);
344 headers.emplace_back(std::move(header_name),
345 std::string{value});
346 });
347}
348
365template <typename Headers>
366inline auto http_serialize_headers(const Headers &headers) -> std::string {
367 std::size_t total_size{0};
368 for (const auto &[name, value] : headers) {
369 // A field dropped for a forbidden byte contributes no bytes, so the reserve
370 // is sized from only the fields that are actually emitted
372 continue;
373 }
374 // Account for the colon, the space, and the trailing CRLF
375 if constexpr (requires { value.bytes(); }) {
376 if (http_field_line_has_forbidden_byte(value.bytes())) {
377 continue;
378 }
379 total_size += name.size() + value.bytes().size() + 4;
380 } else {
382 continue;
383 }
384 total_size += name.size() + value.size() + 4;
385 }
386 }
387
388 std::string result;
389 result.reserve(total_size);
390 for (const auto &[name, value] : headers) {
391 // RFC 9110 §5.5: "a recipient of CR, LF, or NUL within a field value MUST
392 // either reject the message or replace each of those characters with SP
393 // before further processing or forwarding of that message", so a field
394 // whose name carries one is dropped rather than written to the wire as a
395 // header-injection defense
397 continue;
398 }
399
400 // RFC 9112 §5.1 notes that "a single SP preceding the field line
401 // value is preferred for consistent readability by humans"
402 if constexpr (requires { value.bytes(); }) {
403 if (http_field_line_has_forbidden_byte(value.bytes())) {
404 continue;
405 }
406
407 result += name;
408 result += ": ";
409 result += value.bytes();
410 } else {
412 continue;
413 }
414
415 result += name;
416 result += ": ";
417 result += value;
418 }
419
420 result += "\r\n";
421 }
422
423 return result;
424}
425
443template <typename Headers>
444inline auto http_header_find(const Headers &headers,
445 const std::string_view name)
446 -> std::optional<std::string_view> {
447 // Prefer the container's own lookup when it supports searching by the
448 // given name without converting it, as associative containers do it in
449 // logarithmic or constant time instead of a linear scan
450 if constexpr (requires { headers.find(name) != headers.end(); }) {
451 const auto match{headers.find(name)};
452 if (match != headers.end()) {
453 return std::string_view{match->second};
454 }
455
456 return std::nullopt;
457 } else {
458 for (const auto &[header_name, header_value] : headers) {
459 if (header_name == name) {
460 return std::string_view{header_value};
461 }
462 }
463
464 return std::nullopt;
465 }
466}
467
468} // namespace sourcemeta::core
469
470#endif
auto http_parse_headers(const std::string_view input, Callback callback) -> void
Definition http_message.h:89
auto http_accumulate_header_line(Buffer &buffer, const std::string_view line) -> void
Definition http_message.h:58
auto http_field_line_has_forbidden_byte(const std::string_view value) noexcept -> bool
Definition http_syntax.h:196
auto http_parse_cookies(const std::string_view input, Callback callback) -> void
Definition http_message.h:193
constexpr auto http_is_status_line(const std::string_view line) noexcept -> bool
Definition http_message.h:27
auto http_cookie_values(const std::string_view input, const std::string_view name, Container &values) -> void
Definition http_message.h:284
auto http_serialize_headers(const Headers &headers) -> std::string
Definition http_message.h:366
auto http_header_find(const Headers &headers, const std::string_view name) -> std::optional< std::string_view >
Definition http_message.h:444
SOURCEMETA_CORE_TEXT_EXPORT auto trim(const std::string_view input) noexcept -> std::string_view
constexpr auto to_lowercase(const Character character) noexcept -> Character
Definition text.h:70
SOURCEMETA_CORE_TEXT_EXPORT auto split_once(const std::string_view input, const char delimiter) noexcept -> std::optional< std::pair< std::string_view, std::string_view > >