Line data Source code
1 : #include "trusted_proxies.hpp"
2 : #include "../common/headers.hpp"
3 : #include <boost/algorithm/string.hpp>
4 : #include <algorithm>
5 : #include <charconv>
6 : #include <optional>
7 :
8 : namespace thinger::http {
9 :
10 : namespace {
11 :
12 : using boost::asio::ip::address;
13 :
14 1977 : std::string_view trim(std::string_view value) {
15 2127 : while (!value.empty() && (value.front() == ' ' || value.front() == '\t')) value.remove_prefix(1);
16 1989 : while (!value.empty() && (value.back() == ' ' || value.back() == '\t')) value.remove_suffix(1);
17 1977 : return value;
18 : }
19 :
20 : // IPv4-mapped IPv6 addresses (::ffff:10.0.0.1, as reported by dual-stack sockets) as IPv4
21 753 : address normalize(const address& ip) {
22 753 : if (ip.is_v6() && ip.to_v6().is_v4_mapped()) {
23 6 : return boost::asio::ip::make_address_v4(boost::asio::ip::v4_mapped, ip.to_v6());
24 : }
25 747 : return ip;
26 : }
27 :
28 : // Address of a forwarded entry: "1.2.3.4", "1.2.3.4:80", "2001:db8::1", "[2001:db8::1]:80"
29 285 : std::optional<address> parse_address(std::string_view entry) {
30 285 : entry = trim(entry);
31 285 : if (entry.starts_with('[')) {
32 15 : auto close = entry.find(']');
33 15 : if (close == std::string_view::npos) return std::nullopt;
34 15 : entry = entry.substr(1, close - 1);
35 270 : } else if (std::count(entry.begin(), entry.end(), ':') == 1) {
36 6 : entry = entry.substr(0, entry.find(':'));
37 : }
38 285 : boost::system::error_code ec;
39 285 : auto ip = boost::asio::ip::make_address(std::string(entry), ec);
40 285 : if (ec) return std::nullopt;
41 264 : return normalize(ip);
42 : }
43 :
44 : // Split by `separator`, except inside quoted strings
45 342 : std::vector<std::string_view> split_unquoted(std::string_view value, char separator) {
46 342 : std::vector<std::string_view> parts;
47 342 : bool quoted = false;
48 342 : size_t start = 0;
49 7023 : for (size_t i = 0; i < value.size(); ++i) {
50 6681 : if (value[i] == '\\' && quoted) {
51 0 : ++i;
52 6681 : } else if (value[i] == '"') {
53 84 : quoted = !quoted;
54 6597 : } else if (value[i] == separator && !quoted) {
55 207 : parts.push_back(value.substr(start, i - start));
56 207 : start = i + 1;
57 : }
58 : }
59 342 : parts.push_back(value.substr(start));
60 342 : return parts;
61 0 : }
62 :
63 543 : std::string unquote(std::string_view value) {
64 543 : value = trim(value);
65 1581 : if (value.size() < 2 || value.front() != '"' || value.back() != '"') return std::string(value);
66 24 : std::string result;
67 363 : for (size_t i = 1; i + 1 < value.size(); ++i) {
68 339 : if (value[i] == '\\' && i + 2 < value.size()) ++i;
69 339 : result += value[i];
70 : }
71 24 : return result;
72 24 : }
73 :
74 288 : std::string to_entry(const std::optional<address>& ip) {
75 288 : return ip ? ip->to_string() : std::string{};
76 : }
77 :
78 : // An element of a Forwarded header: its address (empty if it is not an IP) and its proto
79 : struct forwarded_element {
80 : std::string address;
81 : std::string proto;
82 : };
83 :
84 63 : std::vector<forwarded_element> parse_forwarded_elements(std::string_view value) {
85 63 : std::vector<forwarded_element> result;
86 63 : if (trim(value).empty()) return result;
87 168 : for (auto element : split_unquoted(value, ',')) {
88 : // element: pairs such as for=192.0.2.60;proto=http;by=203.0.113.43
89 105 : std::optional<address> ip;
90 105 : std::string proto;
91 105 : bool has_for = false;
92 105 : bool has_proto = false;
93 276 : for (auto pair : split_unquoted(element, ';')) {
94 171 : auto equals = pair.find('=');
95 171 : if (equals == std::string_view::npos) continue;
96 171 : auto name = trim(pair.substr(0, equals));
97 : // the first one of each parameter (each may appear once)
98 171 : if (!has_for && boost::iequals(name, "for")) {
99 102 : has_for = true;
100 102 : ip = parse_address(unquote(pair.substr(equals + 1)));
101 69 : } else if (!has_proto && boost::iequals(name, "proto")) {
102 63 : has_proto = true;
103 63 : proto = unquote(pair.substr(equals + 1));
104 : }
105 105 : }
106 105 : result.push_back({to_entry(ip), std::move(proto)});
107 168 : }
108 63 : return result;
109 105 : }
110 :
111 : }
112 :
113 138 : bool trusted_proxies::add(std::string_view value) {
114 138 : value = trim(value);
115 138 : auto slash = value.find('/');
116 :
117 138 : boost::system::error_code ec;
118 138 : auto ip = boost::asio::ip::make_address(std::string(value.substr(0, slash)), ec);
119 138 : if (ec) return false;
120 :
121 132 : unsigned max_prefix = ip.is_v4() ? 32 : 128;
122 132 : unsigned prefix = max_prefix;
123 132 : if (slash != std::string_view::npos) {
124 102 : auto bits = value.substr(slash + 1);
125 102 : auto [end, error] = std::from_chars(bits.data(), bits.data() + bits.size(), prefix);
126 102 : if (error != std::errc{} || end != bits.data() + bits.size() || bits.empty() || prefix > max_prefix) {
127 15 : return false;
128 : }
129 : }
130 :
131 : // A range of IPv4-mapped addresses is an IPv4 range
132 117 : if (ip.is_v6() && ip.to_v6().is_v4_mapped() && prefix >= 96) {
133 3 : ip = normalize(ip);
134 3 : prefix -= 96;
135 : }
136 :
137 117 : networks_.push_back({ip, prefix});
138 117 : return true;
139 : }
140 :
141 597 : bool trusted_proxies::in_network(const address& ip, const network& net) {
142 597 : if (ip.is_v4() != net.address.is_v4()) return false;
143 :
144 513 : auto compare = [&](const auto& a, const auto& b) {
145 513 : unsigned full_bytes = net.prefix / 8;
146 513 : unsigned remaining_bits = net.prefix % 8;
147 513 : if (!std::equal(a.begin(), a.begin() + full_bytes, b.begin())) return false;
148 315 : if (remaining_bits == 0) return true;
149 12 : auto mask = static_cast<uint8_t>(0xFF << (8 - remaining_bits));
150 12 : return (a[full_bytes] & mask) == (b[full_bytes] & mask);
151 513 : };
152 :
153 513 : if (ip.is_v4()) return compare(ip.to_v4().to_bytes(), net.address.to_v4().to_bytes());
154 84 : return compare(ip.to_v6().to_bytes(), net.address.to_v6().to_bytes());
155 : }
156 :
157 507 : bool trusted_proxies::contains(std::string_view ip) const {
158 507 : boost::system::error_code ec;
159 507 : auto parsed = boost::asio::ip::make_address(std::string(trim(ip)), ec);
160 507 : if (ec) return false;
161 486 : parsed = normalize(parsed);
162 486 : return std::any_of(networks_.begin(), networks_.end(), [&](const network& net) {
163 597 : return in_network(parsed, net);
164 486 : });
165 : }
166 :
167 180 : const std::string& trusted_proxies::header_name() const {
168 180 : static const std::string x_forwarded_for = "X-Forwarded-For";
169 180 : static const std::string forwarded = "Forwarded";
170 180 : return header_ == forwarded_header::forwarded ? forwarded : x_forwarded_for;
171 : }
172 :
173 33 : std::string trusted_proxies::client_ip(const std::string& peer, const std::vector<std::string>& header_values) const {
174 33 : if (!contains(peer)) return peer;
175 :
176 : // Forwarded addresses, left (client) to right (nearest proxy)
177 27 : std::vector<std::string> chain;
178 54 : for (const auto& value : header_values) {
179 27 : auto entries = header_ == forwarded_header::forwarded ? parse_forwarded(value) : parse_x_forwarded_for(value);
180 27 : chain.insert(chain.end(), entries.begin(), entries.end());
181 27 : }
182 :
183 27 : std::string client = peer;
184 48 : for (auto it = chain.rbegin(); it != chain.rend(); ++it) {
185 42 : if (it->empty()) break; // not an address: the proxy after it is the best we know
186 36 : client = *it;
187 36 : if (!contains(client)) break;
188 : }
189 27 : return client;
190 27 : }
191 :
192 228 : forwarded_client trusted_proxies::resolve(const std::string& peer, std::string_view scheme,
193 : const headers& request) const {
194 456 : forwarded_client client{peer, std::string(scheme)};
195 228 : if (!contains(peer)) return client;
196 :
197 : // Entries, left (client) to right (nearest proxy), with their scheme
198 177 : std::vector<forwarded_element> chain;
199 : // The scheme of every entry with X-Forwarded-For when the counts do not match: the last one
200 177 : std::optional<std::string> nearest_scheme;
201 177 : if (header_ == forwarded_header::forwarded) {
202 105 : for (const auto& value : request.get_headers_with_key(header_name())) {
203 45 : auto elements = parse_forwarded_elements(value);
204 45 : chain.insert(chain.end(), std::make_move_iterator(elements.begin()), std::make_move_iterator(elements.end()));
205 105 : }
206 : } else {
207 189 : for (const auto& value : request.get_headers_with_key(header_name())) {
208 186 : for (auto& address : parse_x_forwarded_for(value)) chain.push_back({std::move(address), {}});
209 117 : }
210 117 : std::vector<std::string> schemes;
211 186 : for (const auto& value : request.get_headers_with_key("X-Forwarded-Proto")) {
212 69 : if (trim(value).empty()) continue;
213 156 : for (auto entry : split_unquoted(value, ',')) schemes.emplace_back(trim(entry));
214 117 : }
215 117 : if (schemes.size() == chain.size()) {
216 81 : for (size_t i = 0; i < chain.size(); ++i) chain[i].proto = std::move(schemes[i]);
217 75 : } else if (!schemes.empty()) {
218 39 : nearest_scheme = std::move(schemes.back());
219 : }
220 117 : }
221 :
222 : // Walked from the right, as client_ip()
223 177 : const forwarded_element* last = nullptr;
224 231 : for (auto it = chain.rbegin(); it != chain.rend(); ++it) {
225 153 : last = &*it;
226 153 : if (it->address.empty()) break; // not an address: the proxy after it is the best we know
227 150 : client.ip = it->address;
228 150 : if (!contains(client.ip)) break;
229 : }
230 :
231 177 : auto forwarded = parse_scheme(nearest_scheme ? *nearest_scheme : last ? last->proto : std::string{});
232 177 : if (!forwarded.empty()) client.scheme = std::move(forwarded);
233 177 : return client;
234 291 : }
235 :
236 195 : std::string trusted_proxies::parse_scheme(std::string_view value) {
237 195 : auto scheme = unquote(value);
238 297 : if (boost::iequals(scheme, "https")) return "https";
239 234 : if (boost::iequals(scheme, "http")) return "http";
240 99 : return {};
241 195 : }
242 :
243 111 : std::vector<std::string> trusted_proxies::parse_x_forwarded_for(std::string_view value) {
244 111 : std::vector<std::string> result;
245 111 : if (trim(value).empty()) return result;
246 291 : for (auto entry : split_unquoted(value, ',')) {
247 183 : result.push_back(to_entry(parse_address(unquote(entry))));
248 108 : }
249 108 : return result;
250 0 : }
251 :
252 18 : std::vector<std::string> trusted_proxies::parse_forwarded(std::string_view value) {
253 18 : std::vector<std::string> result;
254 51 : for (auto& element : parse_forwarded_elements(value)) result.push_back(std::move(element.address));
255 18 : return result;
256 0 : }
257 :
258 : } // namespace thinger::http
|