• Home
  • Features
  • Pricing
  • Docs
  • Announcements
  • Sign In

randombit / botan / 26995937053

04 Jun 2026 09:38PM UTC coverage: 89.394% (-2.3%) from 91.672%
26995937053

push

github

web-flow
Merge pull request #5642 from randombit/jack/prefetch-in-ks

Improve prefetching for table based implementations

110588 of 123708 relevant lines covered (89.39%)

11056434.37 hits per line

Source File
Press 'n' to go to next uncovered line, 'b' for previous

89.6
/src/lib/utils/http_util/http_util.cpp
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

21
namespace Botan::HTTP {
22

23
namespace {
24

25
constexpr size_t MaxHeaderBytes = 16 * 1024;
26

27
struct Parsed_Head {
25✔
28
      unsigned int status_code;
29
      std::string status_message;
30
      Headers headers;
31
};
32

33
Parsed_Head parse_status_and_headers(std::string_view block) {
31✔
34
   const auto first_eol = block.find("\r\n");
31✔
35
   const auto status_line_end = (first_eol == std::string_view::npos) ? block.size() : first_eol;
31✔
36
   if(status_line_end == 0) {
31✔
37
      throw HTTP_Error("No status line");
×
38
   }
39

40
   std::stringstream ss{std::string(block.substr(0, status_line_end))};
62✔
41
   std::string http_version;
31✔
42
   unsigned int status_code = 0;
31✔
43
   ss >> http_version >> status_code;
31✔
44
   std::string status_message;
31✔
45
   std::getline(ss, status_message);
31✔
46
   if(!status_message.empty() && status_message.front() == ' ') {
31✔
47
      status_message.erase(0, 1);
31✔
48
   }
49

50
   if(!ss || !http_version.starts_with("HTTP/")) {
62✔
51
      throw HTTP_Error("Not an HTTP response");
2✔
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) {
30✔
56
      throw HTTP_Error(fmt("Invalid HTTP status code {}", status_code));
4✔
57
   }
58

59
   // RFC 9110 5.6.2 tchar
60
   constexpr auto is_tchar = CharacterValidityTable::alpha_numeric_plus("!#$%&'*+-.^_`|~");
28✔
61
   // RFC 9110 5.6.3 OWS = *( SP / HTAB )
62
   constexpr auto is_ows = [](char c) { return c == ' ' || c == '\t'; };
120✔
63

64
   Headers headers;
28✔
65
   size_t pos = (first_eol == std::string_view::npos) ? block.size() : first_eol + 2;
28✔
66
   while(pos < block.size()) {
35✔
67
      const auto eol = block.find("\r\n", pos);
33✔
68
      const auto line_end = (eol == std::string_view::npos) ? block.size() : eol;
33✔
69
      const auto line = block.substr(pos, line_end - pos);
33✔
70

71
      // RFC 9110 5.5: field-line = field-name ":" OWS field-value OWS
72
      const auto sep = line.find(':');
33✔
73
      if(sep == std::string_view::npos || sep == 0) {
33✔
74
         throw HTTP_Error(fmt("Invalid HTTP header '{}'", line));
2✔
75
      }
76

77
      const auto name = line.substr(0, sep);
32✔
78
      if(!std::all_of(name.begin(), name.end(), is_tchar)) {
32✔
79
         throw HTTP_Error(fmt("Invalid HTTP header name '{}'", name));
2✔
80
      }
81

82
      auto value = line.substr(sep + 1);
31✔
83
      while(!value.empty() && is_ows(value.front())) {
63✔
84
         value.remove_prefix(1);
32✔
85
      }
86
      while(!value.empty() && is_ows(value.back())) {
33✔
87
         value.remove_suffix(1);
88
      }
89

90
      auto [it, inserted] = headers.emplace(std::string(name), std::string(value));
127✔
91
      if(!inserted) {
31✔
92
         throw HTTP_Error(fmt("Duplicate HTTP header '{}'", it->first));
2✔
93
      }
94

95
      if(eol == std::string_view::npos) {
30✔
96
         break;
97
      }
98
      pos = eol + 2;
7✔
99
   }
100

101
   return {status_code, std::move(status_message), std::move(headers)};
25✔
102
}
65✔
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
*/
110
std::optional<size_t> validate_response_headers(const Headers& headers, std::optional<size_t> max_body_size) {
25✔
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")) {
25✔
114
      throw HTTP_Error("Server sent Transfer-Encoding header in response to HTTP/1.0 request");
4✔
115
   }
116

117
   std::optional<size_t> content_length;
23✔
118
   if(auto it = headers.find("Content-Length"); it != headers.end()) {
23✔
119
      // RFC 9110 8.6: Content-Length = 1*DIGIT
120
      if(const auto cl = parse_sz(it->second)) {
18✔
121
         content_length = cl;
17✔
122
      } else {
123
         throw HTTP_Error(fmt("Invalid Content-Length value '{}'", it->second));
2✔
124
      }
125
   }
126

127
   if(content_length && max_body_size && *content_length > *max_body_size) {
22✔
128
      throw HTTP_Error(fmt("Content-Length {} exceeds maximum body size {}", *content_length, *max_body_size));
2✔
129
   }
130

131
   return content_length;
21✔
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
*/
139
Response http_transact(std::string_view hostname,
1✔
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;
1✔
145
   try {
1✔
146
      socket = OS::open_socket(hostname, service, timeout);
1✔
147
      if(!socket) {
1✔
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));
1✔
155
   return read_response_from_socket(*socket, timeout, max_body_size);
1✔
156
}
1✔
157

158
void check_no_crlf_nul(std::string_view field, std::string_view value) {
54✔
159
   for(const char c : value) {
254✔
160
      if(c == '\r' || c == '\n' || c == '\0') {
200✔
161
         throw HTTP_Error(fmt("Invalid character in HTTP {}", field));
×
162
      }
163
   }
164
}
54✔
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
*/
173
std::optional<URI> resolve_location(const URI& base, std::string_view location) {
10✔
174
   if(auto absolute = URI::parse(location)) {
10✔
175
      return absolute;
6✔
176
   }
6✔
177
   if(location.starts_with("/") && !location.starts_with("//")) {
4✔
178
      const std::string composed = base.scheme() + "://" + base.authority().original_input() + std::string(location);
6✔
179
      return URI::parse(composed);
2✔
180
   }
2✔
181
   return std::nullopt;
2✔
182
}
183

184
}  // namespace
185

186
Response read_response_from_socket(OS::Socket& socket,
33✔
187
                                   std::chrono::milliseconds timeout,
188
                                   std::optional<size_t> max_body_size) {
189
   const auto start_time = std::chrono::system_clock::now();
33✔
190
   const auto deadline_exceeded = [&] { return std::chrono::system_clock::now() - start_time > timeout; };
128✔
191

192
   if(deadline_exceeded()) {
33✔
193
      throw HTTP_Error("Timeout before reading response");
×
194
   }
195

196
   std::string buf;
33✔
197
   std::vector<uint8_t> chunk(DefaultBufferSize);
47✔
198
   size_t header_end = std::string::npos;
33✔
199

200
   while(header_end == std::string::npos) {
121✔
201
      const size_t got = socket.read(chunk.data(), chunk.size());
90✔
202
      if(got == 0) {
90✔
203
         throw HTTP_Error("Server closed connection before headers complete");
2✔
204
      }
205
      if(deadline_exceeded()) {
89✔
206
         throw HTTP_Error("Timeout while reading headers");
×
207
      }
208
      buf.append(cast_uint8_ptr_to_char(chunk.data()), got);
89✔
209
      header_end = buf.find("\r\n\r\n");
89✔
210
      if(header_end == std::string::npos && buf.size() > MaxHeaderBytes) {
177✔
211
         throw HTTP_Error("HTTP headers exceed maximum size");
2✔
212
      }
213
   }
214

215
   // Same cap re-checked once the terminator is found, since the terminator
216
   // can arrive in the chunk that crosses the limit.
217
   if(header_end > MaxHeaderBytes) {
31✔
218
      throw HTTP_Error("HTTP headers exceed maximum size");
×
219
   }
220

221
   auto parsed = parse_status_and_headers(std::string_view(buf).substr(0, header_end));
31✔
222
   const auto content_length = validate_response_headers(parsed.headers, max_body_size);
25✔
223

224
   const size_t body_cap = std::min(max_body_size.value_or(std::numeric_limits<size_t>::max()),
21✔
225
                                    content_length.value_or(std::numeric_limits<size_t>::max()));
37✔
226

227
   std::vector<uint8_t> body;
21✔
228
   if(content_length) {
21✔
229
      body.reserve(*content_length);
16✔
230
   }
231
   const size_t body_start = header_end + 4;
21✔
232
   if(body_start < buf.size()) {
21✔
233
      const size_t spill = buf.size() - body_start;
7✔
234
      if(spill > body_cap) {
7✔
235
         throw HTTP_Error("Response body exceeds maximum size");
2✔
236
      }
237
      body.insert(body.end(),
20✔
238
                  reinterpret_cast<const uint8_t*>(buf.data() + body_start),
6✔
239
                  reinterpret_cast<const uint8_t*>(buf.data() + buf.size()));
6✔
240
   }
241

242
   while(!content_length || body.size() < *content_length) {
22✔
243
      const size_t got = socket.read(chunk.data(), chunk.size());
11✔
244
      if(got == 0) {
11✔
245
         break;
246
      }
247
      if(deadline_exceeded()) {
6✔
248
         throw HTTP_Error("Timeout while reading body");
×
249
      }
250
      if(body.size() + got > body_cap) {
6✔
251
         throw HTTP_Error("Response body exceeds maximum size");
×
252
      }
253
      body.insert(body.end(), chunk.data(), chunk.data() + got);
26✔
254
   }
255

256
   if(content_length && body.size() != *content_length) {
20✔
257
      throw HTTP_Error(fmt("Content-Length disagreement, header says {} got {}", *content_length, body.size()));
2✔
258
   }
259

260
   return Response(parsed.status_code, std::move(parsed.status_message), std::move(body), std::move(parsed.headers));
38✔
261
}
54✔
262

263
std::string url_encode(std::string_view in) {
6✔
264
   constexpr auto needs_url_encoding = CharacterValidityTable::alpha_numeric_plus("-_.~").invert();
6✔
265
   constexpr std::string_view hex_digits = "0123456789ABCDEF";
6✔
266

267
   std::string out;
6✔
268
   out.reserve(in.size());
6✔
269
   for(const char c : in) {
28✔
270
      if(needs_url_encoding(c)) {
22✔
271
         const auto byte = static_cast<uint8_t>(c);
3✔
272
         out += '%';
3✔
273
         out += hex_digits[byte >> 4];
3✔
274
         out += hex_digits[byte & 0x0F];
3✔
275
      } else {
276
         out += c;
41✔
277
      }
278
   }
279
   return out;
6✔
280
}
×
281

282
std::ostream& operator<<(std::ostream& o, const Response& resp) {
×
283
   o << "HTTP " << resp.status_code() << " " << resp.status_message() << "\n";
×
284
   for(const auto& h : resp.headers()) {
×
285
      o << "Header '" << h.first << "' = '" << h.second << "'\n";
×
286
   }
287
   o << "Body " << std::to_string(resp.body().size()) << " bytes:\n";
×
288
   o.write(cast_uint8_ptr_to_char(resp.body().data()), resp.body().size());
×
289
   return o;
×
290
}
291

292
Response http_sync(const http_exch_fn& http_transact,
29✔
293
                   std::string_view verb,
294
                   const URI& uri,
295
                   std::string_view content_type,
296
                   const std::vector<uint8_t>& body,
297
                   const RequestLimits& limits) {
298
   if(uri.scheme() != "http") {
29✔
299
      throw HTTP_Error(fmt("Cannot initiate HTTP request to URI with scheme of '{}'", uri.scheme()));
4✔
300
   }
301

302
   check_no_crlf_nul("verb", verb);
27✔
303
   check_no_crlf_nul("content type", content_type);
27✔
304

305
   const std::string hostname = uri.host_to_string();
27✔
306
   const std::string service = uri.port().has_value() ? std::to_string(*uri.port()) : uri.scheme();
27✔
307

308
   // RFC 9112 3.2.1: request-target origin-form is "absolute-path [ '?' query ]".
309
   // If the URI has an empty path, the client MUST send "/". Fragment is
310
   // excluded from the request-target per RFC 9110 7.1.
311
   std::string loc = uri.path().empty() ? "/" : uri.path();
56✔
312
   if(const auto& q = uri.query()) {
27✔
313
      loc += '?';
3✔
314
      loc += *q;
3✔
315
   }
316

317
   const std::string host_header = [&]() -> std::string {
5✔
318
      const std::string h = (uri.host_kind() == URI::HostKind::IPv6) ? "[" + hostname + "]" : hostname;
28✔
319
      return uri.port().has_value() ? h + ":" + std::to_string(*uri.port()) : h;
54✔
320
   }();
54✔
321

322
   std::ostringstream outbuf;
27✔
323

324
   outbuf << verb << " " << loc << " HTTP/1.0\r\n";
27✔
325
   outbuf << "Host: " << host_header << "\r\n";
27✔
326

327
   if(verb == "GET") {
27✔
328
      outbuf << "Accept: */*\r\n";
20✔
329
      outbuf << "Cache-Control: no-cache\r\n";
20✔
330
   } else if(verb == "POST") {
7✔
331
      outbuf << "Content-Length: " << body.size() << "\r\n";
7✔
332
   }
333

334
   if(!content_type.empty()) {
27✔
335
      outbuf << "Content-Type: " << content_type << "\r\n";
7✔
336
   }
337
   outbuf << "Connection: close\r\n\r\n";
27✔
338
   outbuf.write(cast_uint8_ptr_to_char(body.data()), body.size());
27✔
339

340
   Response resp = http_transact(hostname, service, outbuf.str(), limits.max_body_size());
54✔
341

342
   const auto sc = resp.status_code();
27✔
343
   const bool is_redirect = (sc == 301 || sc == 302 || sc == 303 || sc == 307 || sc == 308);
27✔
344
   if(is_redirect) {
345
      const auto loc_it = resp.headers().find("Location");
12✔
346
      if(loc_it != resp.headers().end()) {
12✔
347
         if(limits.max_redirects() == 0) {
11✔
348
            throw HTTP_Error("HTTP redirection count exceeded");
2✔
349
         }
350
         auto redir = resolve_location(uri, loc_it->second);
10✔
351
         if(!redir) {
10✔
352
            throw HTTP_Error("HTTP redirected to invalid URL");
4✔
353
         }
354
         RequestLimits next = limits;
8✔
355
         next.set_max_redirects(limits.max_redirects() - 1);
8✔
356

357
         // 303 (RFC 9110 15.4.4) re-issues as GET; 301/302/307/308 preserve the
358
         // original method and content. The POST->GET downgrade allowed for
359
         // 301/302 by RFC 9110 15.4.2/3 exists for browser form-submission
360
         // legacy and would silently drop the request body, which is wrong here.
361
         //
362
         // The recursion goes through the same http_exch_fn so a test seam (or
363
         // any caller wrapping the network layer) sees every hop.
364
         if(sc == 303) {
8✔
365
            return http_sync(http_transact, "GET", *redir, "", std::vector<uint8_t>(), next);
5✔
366
         } else {
367
            return http_sync(http_transact, verb, *redir, content_type, body, next);
7✔
368
         }
369
      }
10✔
370
   }
371

372
   return resp;
16✔
373
}
47✔
374

375
Response http_sync(std::string_view verb,
1✔
376
                   const URI& uri,
377
                   std::string_view content_type,
378
                   const std::vector<uint8_t>& body,
379
                   const RequestLimits& limits) {
380
   auto transact_with_timeout =
1✔
381
      [timeout = limits.timeout()](
2✔
382
         std::string_view hostname, std::string_view service, std::string_view message, std::optional<size_t> mbs) {
383
         return http_transact(hostname, service, message, timeout, mbs);
1✔
384
      };
385

386
   return http_sync(transact_with_timeout, verb, uri, content_type, body, limits);
2✔
387
}
388

389
Response GET_sync(const URI& uri, const RequestLimits& limits) {
×
390
   return http_sync("GET", uri, "", std::vector<uint8_t>(), limits);
×
391
}
392

393
Response POST_sync(const URI& uri,
1✔
394
                   std::string_view content_type,
395
                   const std::vector<uint8_t>& body,
396
                   const RequestLimits& limits) {
397
   return http_sync("POST", uri, content_type, body, limits);
1✔
398
}
399

400
}  // namespace Botan::HTTP
STATUS · Troubleshooting · Open an Issue · Sales · Support · CAREERS · ENTERPRISE · START FREE · SCHEDULE DEMO
ANNOUNCEMENTS · TWITTER · TOS & SLA · Supported CI Services · What's a CI service? · Automated Testing

© 2026 Coveralls, Inc