Botan 3.13.0
Crypto and TLS for C&
http_util.cpp
Go to the documentation of this file.
1/*
2* HTTP 1.0 client
3* (C) 2013,2016,2026 Jack Lloyd
4* 2017 René Korthaus, Rohde & Schwarz Cybersecurity
5*
6* Botan is released under the Simplified BSD License (see license.txt)
7*/
8
9#include <botan/internal/http_util.h>
10
11#include <botan/mem_ops.h>
12#include <botan/uri.h>
13#include <botan/internal/charset.h>
14#include <botan/internal/fmt.h>
15#include <botan/internal/mem_utils.h>
16#include <botan/internal/parsing.h>
17#include <botan/internal/socket.h>
18#include <limits>
19#include <sstream>
20
21namespace Botan::HTTP {
22
23namespace {
24
25constexpr size_t MaxHeaderBytes = 16 * 1024;
26
27struct Parsed_Head {
28 unsigned int status_code;
29 std::string status_message;
30 Headers headers;
31};
32
33Parsed_Head parse_status_and_headers(std::string_view block) {
34 const auto first_eol = block.find("\r\n");
35 const auto status_line_end = (first_eol == std::string_view::npos) ? block.size() : first_eol;
36 if(status_line_end == 0) {
37 throw HTTP_Error("No status line");
38 }
39
40 std::stringstream ss{std::string(block.substr(0, status_line_end))};
41 std::string http_version;
42 unsigned int status_code = 0;
43 ss >> http_version >> status_code;
44 std::string status_message;
45 std::getline(ss, status_message);
46 if(!status_message.empty() && status_message.front() == ' ') {
47 status_message.erase(0, 1);
48 }
49
50 if(!ss || !http_version.starts_with("HTTP/")) {
51 throw HTTP_Error("Not an HTTP response");
52 }
53
54 // RFC 9110 Section 15: "All valid status codes are within the range of 100 to 599, inclusive."
55 if(status_code < 100 || status_code > 599) {
56 throw HTTP_Error(fmt("Invalid HTTP status code {}", status_code));
57 }
58
59 // RFC 9110 5.6.2 tchar
60 constexpr auto is_tchar = CharacterValidityTable::alpha_numeric_plus("!#$%&'*+-.^_`|~");
61 // RFC 9110 5.6.3 OWS = *( SP / HTAB )
62 constexpr auto is_ows = [](char c) { return c == ' ' || c == '\t'; };
63
64 Headers headers;
65 size_t pos = (first_eol == std::string_view::npos) ? block.size() : first_eol + 2;
66 while(pos < block.size()) {
67 const auto eol = block.find("\r\n", pos);
68 const auto line_end = (eol == std::string_view::npos) ? block.size() : eol;
69 const auto line = block.substr(pos, line_end - pos);
70
71 // RFC 9110 5.5: field-line = field-name ":" OWS field-value OWS
72 const auto sep = line.find(':');
73 if(sep == std::string_view::npos || sep == 0) {
74 throw HTTP_Error(fmt("Invalid HTTP header '{}'", line));
75 }
76
77 const auto name = line.substr(0, sep);
78 if(!std::all_of(name.begin(), name.end(), is_tchar)) {
79 throw HTTP_Error(fmt("Invalid HTTP header name '{}'", name));
80 }
81
82 auto value = line.substr(sep + 1);
83 while(!value.empty() && is_ows(value.front())) {
84 value.remove_prefix(1);
85 }
86 while(!value.empty() && is_ows(value.back())) {
87 value.remove_suffix(1);
88 }
89
90 auto [it, inserted] = headers.emplace(std::string(name), std::string(value));
91 if(!inserted) {
92 throw HTTP_Error(fmt("Duplicate HTTP header '{}'", it->first));
93 }
94
95 if(eol == std::string_view::npos) {
96 break;
97 }
98 pos = eol + 2;
99 }
100
101 return {status_code, std::move(status_message), std::move(headers)};
102}
103
104/*
105* Post-header validation shared by the streaming reader and the in-memory
106* parser. Rejects Transfer-Encoding outright (we only speak HTTP/1.0) and
107* enforces Content-Length against max_body_size. Returns the parsed
108* Content-Length on success, if present.
109*/
110std::optional<size_t> validate_response_headers(const Headers& headers, std::optional<size_t> max_body_size) {
111 // RFC 9112 6.1: "A server MUST NOT send a response containing Transfer-Encoding
112 // unless the corresponding request indicates HTTP/1.1 (or later minor revisions)."
113 if(headers.contains("Transfer-Encoding")) {
114 throw HTTP_Error("Server sent Transfer-Encoding header in response to HTTP/1.0 request");
115 }
116
117 std::optional<size_t> content_length;
118 if(auto it = headers.find("Content-Length"); it != headers.end()) {
119 // RFC 9110 8.6: Content-Length = 1*DIGIT
120 if(const auto cl = parse_sz(it->second)) {
121 content_length = cl;
122 } else {
123 throw HTTP_Error(fmt("Invalid Content-Length value '{}'", it->second));
124 }
125 }
126
127 if(content_length && max_body_size && *content_length > *max_body_size) {
128 throw HTTP_Error(fmt("Content-Length {} exceeds maximum body size {}", *content_length, *max_body_size));
129 }
130
131 return content_length;
132}
133
134/*
135* Connect to a host, write the request, then delegate to
136* read_response_from_socket. Body- and header-size caps are enforced
137* there; this just owns the socket lifecycle.
138*/
139Response http_transact(std::string_view hostname,
140 std::string_view service,
141 std::string_view message,
142 std::chrono::milliseconds timeout,
143 std::optional<size_t> max_body_size) {
144 std::unique_ptr<OS::Socket> socket;
145 try {
146 socket = OS::open_socket(hostname, service, timeout);
147 if(!socket) {
148 throw Not_Implemented("No socket support enabled in build");
149 }
150 } catch(std::exception& e) {
151 throw HTTP_Error(fmt("HTTP connection to {} failed: {}", hostname, e.what()));
152 }
153
154 socket->write(as_span_of_bytes(message));
155 return read_response_from_socket(*socket, timeout, max_body_size);
156}
157
158void check_no_crlf_nul(std::string_view field, std::string_view value) {
159 for(const char c : value) {
160 if(c == '\r' || c == '\n' || c == '\0') {
161 throw HTTP_Error(fmt("Invalid character in HTTP {}", field));
162 }
163 }
164}
165
166/*
167* Resolve a Location header value against the request URI per RFC 9110 10.2.2.
168* Handles two cases: an absolute URI, or a path-absolute reference (begins
169* with '/' but not '//') which is composed against the request URI's scheme
170* and authority. Other relative forms (network-path "//host/p", protocol-
171* relative, dot-segments) are rejected.
172*/
173std::optional<URI> resolve_location(const URI& base, std::string_view location) {
174 if(auto absolute = URI::from_string(location)) {
175 return absolute;
176 }
177 if(location.starts_with("/") && !location.starts_with("//")) {
178 const auto raw_authority = base.raw_authority();
179 if(!raw_authority.has_value()) {
180 return std::nullopt;
181 }
182 const std::string composed = base.scheme() + "://" + std::string(*raw_authority) + std::string(location);
183 return URI::from_string(composed);
184 }
185 return std::nullopt;
186}
187
188} // namespace
189
191 std::chrono::milliseconds timeout,
192 std::optional<size_t> max_body_size) {
193 const auto start_time = std::chrono::system_clock::now();
194 const auto deadline_exceeded = [&] { return std::chrono::system_clock::now() - start_time > timeout; };
195
196 if(deadline_exceeded()) {
197 throw HTTP_Error("Timeout before reading response");
198 }
199
200 std::string buf;
201 std::vector<uint8_t> chunk(DefaultBufferSize);
202 size_t header_end = std::string::npos;
203
204 while(header_end == std::string::npos) {
205 const size_t got = socket.read(chunk.data(), chunk.size());
206 if(got == 0) {
207 throw HTTP_Error("Server closed connection before headers complete");
208 }
209 if(deadline_exceeded()) {
210 throw HTTP_Error("Timeout while reading headers");
211 }
212 buf.append(cast_uint8_ptr_to_char(chunk.data()), got);
213 header_end = buf.find("\r\n\r\n");
214 if(header_end == std::string::npos && buf.size() > MaxHeaderBytes) {
215 throw HTTP_Error("HTTP headers exceed maximum size");
216 }
217 }
218
219 // Same cap re-checked once the terminator is found, since the terminator
220 // can arrive in the chunk that crosses the limit.
221 if(header_end > MaxHeaderBytes) {
222 throw HTTP_Error("HTTP headers exceed maximum size");
223 }
224
225 auto parsed = parse_status_and_headers(std::string_view(buf).substr(0, header_end));
226 const auto content_length = validate_response_headers(parsed.headers, max_body_size);
227
228 const size_t body_cap = std::min(max_body_size.value_or(std::numeric_limits<size_t>::max()),
229 content_length.value_or(std::numeric_limits<size_t>::max()));
230
231 std::vector<uint8_t> body;
232 if(content_length) {
233 body.reserve(*content_length);
234 }
235 const size_t body_start = header_end + 4;
236 if(body_start < buf.size()) {
237 const size_t spill = buf.size() - body_start;
238 if(spill > body_cap) {
239 throw HTTP_Error("Response body exceeds maximum size");
240 }
241 body.insert(body.end(),
242 reinterpret_cast<const uint8_t*>(buf.data() + body_start),
243 reinterpret_cast<const uint8_t*>(buf.data() + buf.size()));
244 }
245
246 while(!content_length || body.size() < *content_length) {
247 const size_t got = socket.read(chunk.data(), chunk.size());
248 if(got == 0) {
249 break;
250 }
251 if(deadline_exceeded()) {
252 throw HTTP_Error("Timeout while reading body");
253 }
254 if(body.size() + got > body_cap) {
255 throw HTTP_Error("Response body exceeds maximum size");
256 }
257 body.insert(body.end(), chunk.data(), chunk.data() + got);
258 }
259
260 if(content_length && body.size() != *content_length) {
261 throw HTTP_Error(fmt("Content-Length disagreement, header says {} got {}", *content_length, body.size()));
262 }
263
264 return Response(parsed.status_code, std::move(parsed.status_message), std::move(body), std::move(parsed.headers));
265}
266
267std::string url_encode(std::string_view in) {
268 constexpr auto needs_url_encoding = CharacterValidityTable::alpha_numeric_plus("-_.~").invert();
269 constexpr std::string_view hex_digits = "0123456789ABCDEF";
270
271 std::string out;
272 out.reserve(in.size());
273 for(const char c : in) {
274 if(needs_url_encoding(c)) {
275 const auto byte = static_cast<uint8_t>(c);
276 out += '%';
277 out += hex_digits[byte >> 4];
278 out += hex_digits[byte & 0x0F];
279 } else {
280 out += c;
281 }
282 }
283 return out;
284}
285
286std::ostream& operator<<(std::ostream& o, const Response& resp) {
287 o << "HTTP " << resp.status_code() << " " << resp.status_message() << "\n";
288 for(const auto& h : resp.headers()) {
289 o << "Header '" << h.first << "' = '" << h.second << "'\n";
290 }
291 o << "Body " << std::to_string(resp.body().size()) << " bytes:\n";
292 o.write(cast_uint8_ptr_to_char(resp.body().data()), resp.body().size());
293 return o;
294}
295
296Response http_sync(const http_exch_fn& http_transact,
297 std::string_view verb,
298 const URI& uri,
299 std::string_view content_type,
300 const std::vector<uint8_t>& body,
301 const RequestLimits& limits) {
302 if(uri.scheme() != "http") {
303 throw HTTP_Error(fmt("Cannot initiate HTTP request to URI with scheme of '{}'", uri.scheme()));
304 }
305
306 const auto& authority = uri.authority();
307 if(!authority.has_value()) {
308 throw HTTP_Error("Cannot initiate HTTP request to URI without authority");
309 }
310
311 check_no_crlf_nul("verb", verb);
312 check_no_crlf_nul("content type", content_type);
313
314 const std::string hostname = authority->host_to_string();
315 const auto port = authority->port();
316 const std::string service = port.has_value() ? std::to_string(*port) : uri.scheme();
317
318 // RFC 9112 3.2.1: request-target origin-form is "absolute-path [ '?' query ]".
319 // If the URI has an empty path, the client MUST send "/". Fragment is
320 // excluded from the request-target per RFC 9110 7.1.
321 std::string loc = uri.path().empty() ? "/" : uri.path();
322 if(const auto& q = uri.query()) {
323 loc += '?';
324 loc += *q;
325 }
326
327 const std::string host_header = [&]() -> std::string {
328 const std::string h = (authority->host_kind() == URI::HostKind::IPv6) ? "[" + hostname + "]" : hostname;
329 return port.has_value() ? h + ":" + std::to_string(*port) : h;
330 }();
331
332 std::ostringstream outbuf;
333
334 outbuf << verb << " " << loc << " HTTP/1.0\r\n";
335 outbuf << "Host: " << host_header << "\r\n";
336
337 if(verb == "GET") {
338 outbuf << "Accept: */*\r\n";
339 outbuf << "Cache-Control: no-cache\r\n";
340 } else if(verb == "POST") {
341 outbuf << "Content-Length: " << body.size() << "\r\n";
342 }
343
344 if(!content_type.empty()) {
345 outbuf << "Content-Type: " << content_type << "\r\n";
346 }
347 outbuf << "Connection: close\r\n\r\n";
348 outbuf.write(cast_uint8_ptr_to_char(body.data()), body.size());
349
350 Response resp = http_transact(hostname, service, outbuf.str(), limits.max_body_size());
351
352 const auto sc = resp.status_code();
353 const bool is_redirect = (sc == 301 || sc == 302 || sc == 303 || sc == 307 || sc == 308);
354 if(is_redirect) {
355 const auto loc_it = resp.headers().find("Location");
356 if(loc_it != resp.headers().end()) {
357 if(limits.max_redirects() == 0) {
358 throw HTTP_Error("HTTP redirection count exceeded");
359 }
360 auto redir = resolve_location(uri, loc_it->second);
361 if(!redir) {
362 throw HTTP_Error("HTTP redirected to invalid URL");
363 }
364 RequestLimits next = limits;
365 next.set_max_redirects(limits.max_redirects() - 1);
366
367 // 303 (RFC 9110 15.4.4) re-issues as GET; 301/302/307/308 preserve the
368 // original method and content. The POST->GET downgrade allowed for
369 // 301/302 by RFC 9110 15.4.2/3 exists for browser form-submission
370 // legacy and would silently drop the request body, which is wrong here.
371 //
372 // The recursion goes through the same http_exch_fn so a test seam (or
373 // any caller wrapping the network layer) sees every hop.
374 if(sc == 303) {
375 return http_sync(http_transact, "GET", *redir, "", std::vector<uint8_t>(), next);
376 } else {
377 return http_sync(http_transact, verb, *redir, content_type, body, next);
378 }
379 }
380 }
381
382 return resp;
383}
384
385Response http_sync(std::string_view verb,
386 const URI& uri,
387 std::string_view content_type,
388 const std::vector<uint8_t>& body,
389 const RequestLimits& limits) {
390 auto transact_with_timeout =
391 [timeout = limits.timeout()](
392 std::string_view hostname, std::string_view service, std::string_view message, std::optional<size_t> mbs) {
393 return http_transact(hostname, service, message, timeout, mbs);
394 };
395
396 return http_sync(transact_with_timeout, verb, uri, content_type, body, limits);
397}
398
399Response GET_sync(const URI& uri, const RequestLimits& limits) {
400 return http_sync("GET", uri, "", std::vector<uint8_t>(), limits);
401}
402
404 std::string_view content_type,
405 const std::vector<uint8_t>& body,
406 const RequestLimits& limits) {
407 return http_sync("POST", uri, content_type, body, limits);
408}
409
410} // namespace Botan::HTTP
static constexpr CharacterValidityTable alpha_numeric_plus(std::string_view extras)
Definition charset.h:114
constexpr CharacterValidityTable invert() const
Definition charset.h:130
std::chrono::milliseconds timeout() const
Definition http_util.h:105
size_t max_redirects() const
Definition http_util.h:103
RequestLimits & set_max_redirects(size_t n)
Definition http_util.h:109
std::optional< size_t > max_body_size() const
Definition http_util.h:107
const std::vector< uint8_t > & body() const
Definition http_util.h:75
const Headers & headers() const
Definition http_util.h:77
unsigned int status_code() const
Definition http_util.h:73
std::string status_message() const
Definition http_util.h:79
virtual size_t read(uint8_t buf[], size_t len)=0
static std::optional< URI > from_string(std::string_view raw)
Definition uri.cpp:164
const std::string & scheme() const
Definition uri.h:127
const std::optional< std::string > & query() const
Definition uri.h:161
const std::optional< Authority > & authority() const
Definition uri.h:132
std::optional< std::string_view > raw_authority() const
Definition uri.cpp:130
const std::string & path() const
Definition uri.h:154
std::string url_encode(std::string_view in)
Response GET_sync(const URI &uri, const RequestLimits &limits)
std::function< Response(std::string_view, std::string_view, std::string_view, std::optional< size_t >)> http_exch_fn
Definition http_util.h:131
std::map< std::string, std::string, Case_Insensitive_Less > Headers
Definition http_util.h:61
Response http_sync(const http_exch_fn &http_transact, std::string_view verb, const URI &uri, std::string_view content_type, const std::vector< uint8_t > &body, const RequestLimits &limits)
Response read_response_from_socket(OS::Socket &socket, std::chrono::milliseconds timeout, std::optional< size_t > max_body_size)
Response POST_sync(const URI &uri, std::string_view content_type, const std::vector< uint8_t > &body, const RequestLimits &limits)
std::ostream & operator<<(std::ostream &o, const Response &resp)
std::unique_ptr< Socket > BOTAN_TEST_API open_socket(std::string_view hostname, std::string_view service, std::chrono::milliseconds timeout)
Definition socket.cpp:480
std::span< const uint8_t > as_span_of_bytes(const char *s, size_t len)
Definition mem_utils.h:59
std::string fmt(std::string_view format, const T &... args)
Definition fmt.h:53
std::optional< size_t > parse_sz(std::string_view input, bool require_canonical)
Definition parsing.cpp:72
const char * cast_uint8_ptr_to_char(const uint8_t *b)
Definition mem_ops.h:323
constexpr size_t DefaultBufferSize
Definition types.h:150