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

randombit / botan / 27930394332

22 Jun 2026 02:29AM UTC coverage: 89.361% (-2.3%) from 91.664%
27930394332

push

github

randombit
Escape control chars in X509_Certificate::to_string

111792 of 125101 relevant lines covered (89.36%)

10818223.53 hits per line

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

88.94
/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 auto raw_authority = base.raw_authority();
2✔
179
      if(!raw_authority.has_value()) {
2✔
180
         return std::nullopt;
×
181
      }
182
      const std::string composed = base.scheme() + "://" + std::string(*raw_authority) + 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
   const auto& authority = uri.authority();
27✔
307
   if(!authority.has_value()) {
27✔
308
      throw HTTP_Error("Cannot initiate HTTP request to URI without authority");
×
309
   }
310

311
   check_no_crlf_nul("verb", verb);
27✔
312
   check_no_crlf_nul("content type", content_type);
27✔
313

314
   const std::string hostname = authority->host_to_string();
27✔
315
   const auto port = authority->port();
27✔
316
   const std::string service = port.has_value() ? std::to_string(*port) : uri.scheme();
27✔
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();
56✔
322
   if(const auto& q = uri.query()) {
27✔
323
      loc += '?';
3✔
324
      loc += *q;
3✔
325
   }
326

327
   const std::string host_header = [&]() -> std::string {
5✔
328
      const std::string h = (authority->host_kind() == URI::HostKind::IPv6) ? "[" + hostname + "]" : hostname;
28✔
329
      return port.has_value() ? h + ":" + std::to_string(*port) : h;
54✔
330
   }();
54✔
331

332
   std::ostringstream outbuf;
27✔
333

334
   outbuf << verb << " " << loc << " HTTP/1.0\r\n";
27✔
335
   outbuf << "Host: " << host_header << "\r\n";
27✔
336

337
   if(verb == "GET") {
27✔
338
      outbuf << "Accept: */*\r\n";
20✔
339
      outbuf << "Cache-Control: no-cache\r\n";
20✔
340
   } else if(verb == "POST") {
7✔
341
      outbuf << "Content-Length: " << body.size() << "\r\n";
7✔
342
   }
343

344
   if(!content_type.empty()) {
27✔
345
      outbuf << "Content-Type: " << content_type << "\r\n";
7✔
346
   }
347
   outbuf << "Connection: close\r\n\r\n";
27✔
348
   outbuf.write(cast_uint8_ptr_to_char(body.data()), body.size());
27✔
349

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

352
   const auto sc = resp.status_code();
27✔
353
   const bool is_redirect = (sc == 301 || sc == 302 || sc == 303 || sc == 307 || sc == 308);
27✔
354
   if(is_redirect) {
355
      const auto loc_it = resp.headers().find("Location");
12✔
356
      if(loc_it != resp.headers().end()) {
12✔
357
         if(limits.max_redirects() == 0) {
11✔
358
            throw HTTP_Error("HTTP redirection count exceeded");
2✔
359
         }
360
         auto redir = resolve_location(uri, loc_it->second);
10✔
361
         if(!redir) {
10✔
362
            throw HTTP_Error("HTTP redirected to invalid URL");
4✔
363
         }
364
         RequestLimits next = limits;
8✔
365
         next.set_max_redirects(limits.max_redirects() - 1);
8✔
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) {
8✔
375
            return http_sync(http_transact, "GET", *redir, "", std::vector<uint8_t>(), next);
5✔
376
         } else {
377
            return http_sync(http_transact, verb, *redir, content_type, body, next);
7✔
378
         }
379
      }
10✔
380
   }
381

382
   return resp;
16✔
383
}
47✔
384

385
Response http_sync(std::string_view verb,
1✔
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 =
1✔
391
      [timeout = limits.timeout()](
2✔
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);
1✔
394
      };
395

396
   return http_sync(transact_with_timeout, verb, uri, content_type, body, limits);
2✔
397
}
398

399
Response GET_sync(const URI& uri, const RequestLimits& limits) {
×
400
   return http_sync("GET", uri, "", std::vector<uint8_t>(), limits);
×
401
}
402

403
Response POST_sync(const URI& uri,
1✔
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);
1✔
408
}
409

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