Line data Source code
1 : #include "response_compression.hpp"
2 : #include "../common/http_request.hpp"
3 : #include "../common/http_response.hpp"
4 : #include "../common/syntax.hpp"
5 : #include "../../util/ascii.hpp"
6 : #include "../../util/compression.hpp"
7 :
8 : namespace thinger::http {
9 :
10 : namespace {
11 :
12 : using ::thinger::util::ascii::iequals;
13 :
14 : // Weight of the parameters of an Accept-Encoding element (";q=0.5", the only parameter
15 : // defined): 1000 without one, -1 if invalid
16 141 : int coding_weight(std::string_view parameters) {
17 141 : int quality = 1000;
18 282 : while (!parameters.empty() && quality >= 0) {
19 141 : auto next = parameters.find(';');
20 141 : auto parameter = parameters.substr(0, next);
21 141 : parameters = next == std::string_view::npos ? std::string_view{} : parameters.substr(next + 1);
22 141 : auto equals = parameter.find('=');
23 141 : if (equals != std::string_view::npos && iequals(syntax::trim(parameter.substr(0, equals)), "q")) {
24 141 : quality = syntax::parse_qvalue(syntax::trim(parameter.substr(equals + 1)));
25 : }
26 : }
27 141 : return quality;
28 : }
29 :
30 : // Quality of a content coding in `codings` (`any` for "*"), null if it is not one of them
31 701 : int* coding_quality(accepted_codings& codings, int& any, std::string_view coding) {
32 701 : if (iequals(coding, "gzip") || iequals(coding, "x-gzip")) return &codings.gzip;
33 357 : if (iequals(coding, "deflate")) return &codings.deflate;
34 172 : if (iequals(coding, "br")) return &codings.br;
35 72 : if (iequals(coding, "identity")) return &codings.identity;
36 42 : if (coding == "*") return &any;
37 9 : return nullptr;
38 : }
39 :
40 1315 : content_coding negotiate(const accepted_codings& accepted) {
41 1315 : int best = std::max(accepted.gzip, accepted.deflate);
42 1315 : if (best <= 0 || accepted.identity > best) return content_coding::identity;
43 242 : return accepted.gzip >= accepted.deflate ? content_coding::gzip : content_coding::deflate;
44 : }
45 :
46 1403 : bool is_compressible_content_type(std::string_view content_type) {
47 1403 : return content_type.starts_with("text/")
48 342 : || content_type.starts_with("application/json")
49 148 : || content_type.starts_with("application/xml")
50 146 : || content_type.starts_with("application/javascript")
51 122 : || content_type.starts_with("application/x-javascript")
52 1745 : || content_type.starts_with("image/svg+xml");
53 : }
54 :
55 : // Whether a comma-separated header (any of its values) holds `token`, case-insensitive
56 4063 : bool has_list_token(const http_response& message, std::string_view name, std::string_view token) {
57 4320 : for (const auto& value : message.get_headers_with_key(name)) {
58 616 : if (syntax::any_element(value, [&](std::string_view item) { return iequals(item, token); })) return true;
59 4063 : }
60 4023 : return false;
61 : }
62 :
63 : // Cache-Control: no-transform (RFC 9111, 5.2.2.6): no compression then, as Express
64 1281 : bool no_transform(const http_response& message) {
65 1281 : return message.has_header(header::cache_control) && has_list_token(message, header::cache_control, "no-transform");
66 : }
67 :
68 : }
69 :
70 1866 : accepted_codings parse_accept_encoding(std::string_view accept_encoding) {
71 1866 : accepted_codings listed{-1, -1, -1, -1};
72 1866 : int any = -1;
73 1866 : syntax::for_each_element(accept_encoding, [&](std::string_view element) {
74 713 : auto semicolon = element.find(';');
75 713 : auto coding = syntax::trim(element.substr(0, semicolon));
76 725 : if (coding.empty()) return;
77 713 : int quality = semicolon == std::string_view::npos ? 1000 : coding_weight(element.substr(semicolon + 1));
78 713 : if (quality < 0) return;
79 : // the first time a coding is listed counts
80 701 : int* entry = coding_quality(listed, any, coding);
81 701 : if (entry && *entry < 0) *entry = quality;
82 : });
83 :
84 : // "*" stands for the codings not listed; identity not listed nor excluded is still
85 : // acceptable, as the last choice
86 9330 : for (int* quality : {&listed.gzip, &listed.deflate, &listed.br, &listed.identity}) {
87 7464 : if (*quality < 0) *quality = any;
88 : }
89 1866 : return listed;
90 : }
91 :
92 93 : content_coding negotiate_content_coding(std::string_view accept_encoding) {
93 93 : return negotiate(parse_accept_encoding(accept_encoding));
94 : }
95 :
96 : namespace compression {
97 :
98 1752 : accepted_codings accepted_by(const http_request& request) {
99 1752 : std::string accept_encoding;
100 2101 : for (const auto& value : request.get_headers_with_key("Accept-Encoding")) {
101 349 : if (!accept_encoding.empty()) accept_encoding += ", ";
102 349 : accept_encoding += value;
103 1752 : }
104 3504 : return parse_accept_encoding(accept_encoding);
105 1752 : }
106 :
107 1222 : content_coding negotiated(const http_request& request) {
108 1222 : return negotiate(accepted_by(request));
109 : }
110 :
111 2097 : bool compressible(const http_response& message, size_t size, std::string_view content_type) {
112 2097 : return size >= threshold && is_compressible_content_type(content_type) && !no_transform(message);
113 : }
114 :
115 1548 : bool compressible(const http_response& message) {
116 1548 : return compressible(message, message.get_content().size(), message.get_content_type());
117 : }
118 :
119 2007 : void add_vary(http_response& message, std::string_view field) {
120 2007 : if (has_list_token(message, "Vary", field) || has_list_token(message, "Vary", "*")) return;
121 1970 : if (field == "*") {
122 3 : message.remove_headers("Vary");
123 12 : message.add_header("Vary", "*");
124 3 : return;
125 : }
126 1967 : const auto& vary = message.get_header("Vary");
127 1967 : if (vary.empty()) {
128 9415 : message.add_header("Vary", std::string(field));
129 : } else {
130 420 : message.set_header("Vary", vary + ", " + std::string(field));
131 : }
132 : }
133 :
134 1309 : void compress(http_response& message, const http_request& request) {
135 2342 : if (!compressible(message)) return;
136 : // Sent compressed or not depending on the request: caches must tell both apart
137 1208 : add_vary(message, "Accept-Encoding");
138 1208 : if (message.has_header("Content-Encoding")) return;
139 :
140 1208 : auto coding = negotiated(request);
141 1208 : if (coding == content_coding::identity) return;
142 175 : const auto& content = message.get_content();
143 175 : auto compressed = coding == content_coding::gzip ? ::thinger::util::gzip::compress(content)
144 175 : : ::thinger::util::deflate::compress(content);
145 175 : if (compressed) {
146 175 : message.set_content(std::move(*compressed));
147 875 : message.add_header("Content-Encoding", coding == content_coding::gzip ? "gzip" : "deflate");
148 : }
149 175 : }
150 :
151 : }
152 :
153 : } // namespace thinger::http
|