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

randombit / botan / 26141725099

19 May 2026 08:32PM UTC coverage: 89.343% (+0.009%) from 89.334%
26141725099

push

github

web-flow
Merge pull request #5609 from randombit/jack/improve-http

Improve the HTTP 1.0 client used for OCSP/CRL

109341 of 122383 relevant lines covered (89.34%)

11264402.07 hits per line

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

88.35
/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
      const auto& cl = it->second;
18✔
121
      if(cl.empty() || !std::all_of(cl.begin(), cl.end(), [](unsigned char c) { return c >= '0' && c <= '9'; })) {
41✔
122
         throw HTTP_Error(fmt("Invalid Content-Length value '{}'", cl));
2✔
123
      }
124
      try {
17✔
125
         content_length = to_u32bit(cl);
17✔
126
      } catch(const Invalid_Argument& e) {
×
127
         throw HTTP_Error(fmt("Invalid Content-Length value '{}': {}", cl, e.what()));
×
128
      }
×
129
   }
130

131
   if(content_length && max_body_size && *content_length > *max_body_size) {
22✔
132
      throw HTTP_Error(fmt("Content-Length {} exceeds maximum body size {}", *content_length, *max_body_size));
2✔
133
   }
134

135
   return content_length;
21✔
136
}
137

138
/*
139
* Connect to a host, write the request, then delegate to
140
* read_response_from_socket. Body- and header-size caps are enforced
141
* there; this just owns the socket lifecycle.
142
*/
143
Response http_transact(std::string_view hostname,
1✔
144
                       std::string_view service,
145
                       std::string_view message,
146
                       std::chrono::milliseconds timeout,
147
                       std::optional<size_t> max_body_size) {
148
   std::unique_ptr<OS::Socket> socket;
1✔
149
   try {
1✔
150
      socket = OS::open_socket(hostname, service, timeout);
1✔
151
      if(!socket) {
1✔
152
         throw Not_Implemented("No socket support enabled in build");
×
153
      }
154
   } catch(std::exception& e) {
×
155
      throw HTTP_Error(fmt("HTTP connection to {} failed: {}", hostname, e.what()));
×
156
   }
×
157

158
   socket->write(as_span_of_bytes(message));
1✔
159
   return read_response_from_socket(*socket, timeout, max_body_size);
1✔
160
}
1✔
161

162
void check_no_crlf_nul(std::string_view field, std::string_view value) {
54✔
163
   for(const char c : value) {
254✔
164
      if(c == '\r' || c == '\n' || c == '\0') {
200✔
165
         throw HTTP_Error(fmt("Invalid character in HTTP {}", field));
×
166
      }
167
   }
168
}
54✔
169

170
/*
171
* Resolve a Location header value against the request URI per RFC 9110 10.2.2.
172
* Handles two cases: an absolute URI, or a path-absolute reference (begins
173
* with '/' but not '//') which is composed against the request URI's scheme
174
* and authority. Other relative forms (network-path "//host/p", protocol-
175
* relative, dot-segments) are rejected.
176
*/
177
std::optional<URI> resolve_location(const URI& base, std::string_view location) {
10✔
178
   if(auto absolute = URI::parse(location)) {
10✔
179
      return absolute;
6✔
180
   }
6✔
181
   if(location.starts_with("/") && !location.starts_with("//")) {
4✔
182
      const std::string composed = base.scheme() + "://" + base.authority().original_input() + std::string(location);
6✔
183
      return URI::parse(composed);
2✔
184
   }
185
   return std::nullopt;
2✔
186
}
187

188
}  // namespace
189

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

196
   if(deadline_exceeded()) {
33✔
197
      throw HTTP_Error("Timeout before reading response");
×
198
   }
199

200
   std::string buf;
33✔
201
   std::vector<uint8_t> chunk(DefaultBufferSize);
47✔
202
   size_t header_end = std::string::npos;
33✔
203

204
   while(header_end == std::string::npos) {
121✔
205
      const size_t got = socket.read(chunk.data(), chunk.size());
90✔
206
      if(got == 0) {
90✔
207
         throw HTTP_Error("Server closed connection before headers complete");
2✔
208
      }
209
      if(deadline_exceeded()) {
89✔
210
         throw HTTP_Error("Timeout while reading headers");
×
211
      }
212
      buf.append(cast_uint8_ptr_to_char(chunk.data()), got);
89✔
213
      header_end = buf.find("\r\n\r\n");
89✔
214
      if(header_end == std::string::npos && buf.size() > MaxHeaderBytes) {
177✔
215
         throw HTTP_Error("HTTP headers exceed maximum size");
2✔
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) {
31✔
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));
31✔
226
   const auto content_length = validate_response_headers(parsed.headers, max_body_size);
25✔
227

228
   const size_t body_cap = std::min(max_body_size.value_or(std::numeric_limits<size_t>::max()),
21✔
229
                                    content_length.value_or(std::numeric_limits<size_t>::max()));
37✔
230

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

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

260
   if(content_length && body.size() != *content_length) {
20✔
261
      throw HTTP_Error(fmt("Content-Length disagreement, header says {} got {}", *content_length, body.size()));
2✔
262
   }
263

264
   return Response(parsed.status_code, std::move(parsed.status_message), std::move(body), std::move(parsed.headers));
38✔
265
}
54✔
266

267
std::string url_encode(std::string_view in) {
6✔
268
   constexpr auto needs_url_encoding = CharacterValidityTable::alpha_numeric_plus("-_.~").invert();
6✔
269
   constexpr std::string_view hex_digits = "0123456789ABCDEF";
6✔
270

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

286
std::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

296
Response http_sync(const http_exch_fn& http_transact,
29✔
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") {
29✔
303
      throw HTTP_Error(fmt("Cannot initiate HTTP request to URI with scheme of '{}'", uri.scheme()));
4✔
304
   }
305

306
   check_no_crlf_nul("verb", verb);
27✔
307
   check_no_crlf_nul("content type", content_type);
27✔
308

309
   const std::string hostname = uri.host_to_string();
27✔
310
   const std::string service = uri.port().has_value() ? std::to_string(*uri.port()) : uri.scheme();
27✔
311

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

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

326
   std::ostringstream outbuf;
27✔
327

328
   outbuf << verb << " " << loc << " HTTP/1.0\r\n";
27✔
329
   outbuf << "Host: " << host_header << "\r\n";
27✔
330

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

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

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

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

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

376
   return resp;
16✔
377
}
47✔
378

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

390
   return http_sync(transact_with_timeout, verb, uri, content_type, body, limits);
2✔
391
}
392

393
Response GET_sync(const URI& uri, const RequestLimits& limits) {
×
394
   return http_sync("GET", uri, "", std::vector<uint8_t>(), limits);
×
395
}
396

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

404
}  // 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