@@ -478,7 +478,7 @@ bool set_socket_opt_time(socket_t sock, int level, int optname,
478478}
479479
480480bool is_hex(char c, int &v) {
481- if (isdigit(static_cast<unsigned char>(c) )) {
481+ if (is_ascii_digit(c )) {
482482 v = c - '0';
483483 return true;
484484 } else if ('A' <= c && c <= 'F') {
@@ -695,7 +695,11 @@ std::string base64_encode(const std::string &in) {
695695 std::string out;
696696 out.reserve(in.size());
697697
698- auto val = 0;
698+ // Unsigned: the accumulator is never masked, so with a signed int the
699+ // `val << 8` below overflows once enough bytes are folded in (undefined
700+ // behaviour before C++20). Only the low bits are ever emitted, so the
701+ // wrap-around of an unsigned accumulator does not affect the output.
702+ uint32_t val = 0;
699703 auto valb = -6;
700704
701705 for (auto c : in) {
@@ -3887,8 +3891,7 @@ bool parse_range_header(const std::string &s, Ranges &ranges) {
38873891bool parse_range_header(const std::string &s, Ranges &ranges) try {
38883892#endif
38893893 auto is_valid = [](const std::string &str) {
3890- return std::all_of(str.cbegin(), str.cend(),
3891- [](unsigned char c) { return std::isdigit(c); });
3894+ return std::all_of(str.cbegin(), str.cend(), is_ascii_digit);
38923895 };
38933896
38943897 if (s.size() > 7 && s.compare(0, 6, "bytes=") == 0) {
@@ -4336,26 +4339,55 @@ bool is_multipart_boundary_chars_valid(const std::string &boundary) {
43364339 auto valid = true;
43374340 for (size_t i = 0; i < boundary.size(); i++) {
43384341 auto c = boundary[i];
4339- if (!std::isalnum(static_cast<unsigned char>(c) ) && c != '-' && c != '_') {
4342+ if (!is_ascii_alnum(c ) && c != '-' && c != '_') {
43404343 valid = false;
43414344 break;
43424345 }
43434346 }
43444347 return valid;
43454348}
43464349
4350+ // Escape a multipart field name/filename following the WHATWG HTML standard
4351+ // ("escape a multipart form-data name"), which is what browsers send:
4352+ // '"' -> %22, CR -> %0D, LF -> %0A
4353+ // With escape_quote = false, only CR and LF are escaped; this is for header
4354+ // values outside a quoted-string (e.g. Content-Type), where '"' is legal.
4355+ std::string escape_multipart_field(const std::string &s,
4356+ bool escape_quote = true) {
4357+ std::string result;
4358+ result.reserve(s.size());
4359+ for (auto c : s) {
4360+ switch (c) {
4361+ case '"':
4362+ if (escape_quote) {
4363+ result += "%22";
4364+ } else {
4365+ result += c;
4366+ }
4367+ break;
4368+ case '\r': result += "%0D"; break;
4369+ case '\n': result += "%0A"; break;
4370+ default: result += c; break;
4371+ }
4372+ }
4373+ return result;
4374+ }
4375+
43474376template <typename T>
43484377std::string
43494378serialize_multipart_formdata_item_begin(const T &item,
43504379 const std::string &boundary) {
43514380 std::string body = "--" + boundary + "\r\n";
4352- body += "Content-Disposition: form-data; name=\"" + item.name + "\"";
4381+ body += "Content-Disposition: form-data; name=\"" +
4382+ escape_multipart_field(item.name) + "\"";
43534383 if (!item.filename.empty()) {
4354- body += "; filename=\"" + item.filename + "\"";
4384+ body += "; filename=\"" + escape_multipart_field( item.filename) + "\"";
43554385 }
43564386 body += "\r\n";
43574387 if (!item.content_type.empty()) {
4358- body += "Content-Type: " + item.content_type + "\r\n";
4388+ body +=
4389+ "Content-Type: " + escape_multipart_field(item.content_type, false) +
4390+ "\r\n";
43594391 }
43604392 body += "\r\n";
43614393
@@ -4821,10 +4853,9 @@ class ContentProviderAdapter {
48214853namespace fields {
48224854
48234855bool is_token_char(char c) {
4824- return std::isalnum(static_cast<unsigned char>(c)) || c == '!' || c == '#' ||
4825- c == '$' || c == '%' || c == '&' || c == '\'' || c == '*' ||
4826- c == '+' || c == '-' || c == '.' || c == '^' || c == '_' || c == '`' ||
4827- c == '|' || c == '~';
4856+ return is_ascii_alnum(c) || c == '!' || c == '#' || c == '$' || c == '%' ||
4857+ c == '&' || c == '\'' || c == '*' || c == '+' || c == '-' ||
4858+ c == '.' || c == '^' || c == '_' || c == '`' || c == '|' || c == '~';
48284859}
48294860
48304861bool is_token(const std::string &s) {
@@ -4873,7 +4904,8 @@ bool is_field_value(const std::string &s) { return is_field_content(s); }
48734904} // namespace fields
48744905
48754906bool perform_websocket_handshake(Stream &strm, const std::string &host,
4876- int port, const std::string &path,
4907+ int port, bool is_ssl,
4908+ const std::string &path,
48774909 const Headers &headers,
48784910 std::string &selected_subprotocol) {
48794911 // Validate path and host
@@ -4899,7 +4931,7 @@ bool perform_websocket_handshake(Stream &strm, const std::string &host,
48994931
49004932 // Build upgrade request
49014933 std::string req_str = "GET " + path + " HTTP/1.1\r\n";
4902- req_str += "Host: " + host + ":" + std::to_string( port) + "\r\n";
4934+ req_str += "Host: " + make_host_and_port_string( host, port, is_ssl ) + "\r\n";
49034935 req_str += "Upgrade: websocket\r\n";
49044936 req_str += "Connection: Upgrade\r\n";
49054937 req_str += "Sec-WebSocket-Key: " + client_key + "\r\n";
@@ -5599,9 +5631,8 @@ std::string encode_uri_component(const std::string &value) {
55995631 escaped << std::hex;
56005632
56015633 for (auto c : value) {
5602- if (std::isalnum(static_cast<uint8_t>(c)) || c == '-' || c == '_' ||
5603- c == '.' || c == '!' || c == '~' || c == '*' || c == '\'' || c == '(' ||
5604- c == ')') {
5634+ if (detail::is_ascii_alnum(c) || c == '-' || c == '_' || c == '.' ||
5635+ c == '!' || c == '~' || c == '*' || c == '\'' || c == '(' || c == ')') {
56055636 escaped << c;
56065637 } else {
56075638 escaped << std::uppercase;
@@ -5620,10 +5651,10 @@ std::string encode_uri(const std::string &value) {
56205651 escaped << std::hex;
56215652
56225653 for (auto c : value) {
5623- if (std::isalnum(static_cast<uint8_t>(c)) || c == '-' || c == '_' ||
5624- c == '. ' || c == '! ' || c == '~ ' || c == '*' || c == '\'' || c == '( ' ||
5625- c == ') ' || c == '; ' || c == '/ ' || c == '? ' || c == ': ' || c == '@ ' ||
5626- c == '&' || c == ' =' || c == '+' || c == '$' || c == ',' || c == '#') {
5654+ if (detail::is_ascii_alnum(c) || c == '-' || c == '_' || c == '. ' ||
5655+ c == '! ' || c == '~ ' || c == '* ' || c == '\'' || c == '(' || c == ') ' ||
5656+ c == '; ' || c == '/ ' || c == '? ' || c == ': ' || c == '@ ' || c == '& ' ||
5657+ c == '=' || c == '+' || c == '$' || c == ',' || c == '#') {
56275658 escaped << c;
56285659 } else {
56295660 escaped << std::uppercase;
@@ -5684,7 +5715,8 @@ std::string encode_path_component(const std::string &component) {
56845715 auto c = static_cast<unsigned char>(component[i]);
56855716
56865717 // Unreserved characters per RFC 3986: ALPHA / DIGIT / "-" / "." / "_" / "~"
5687- if (std::isalnum(c) || c == '-' || c == '.' || c == '_' || c == '~') {
5718+ if (detail::is_ascii_alnum(static_cast<char>(c)) || c == '-' || c == '.' ||
5719+ c == '_' || c == '~') {
56885720 result += static_cast<char>(c);
56895721 }
56905722 // Path-safe sub-delimiters: "!" / "$" / "&" / "'" / "(" / ")" / "*" / "+" /
@@ -5757,7 +5789,8 @@ std::string encode_query_component(const std::string &component,
57575789 auto c = static_cast<unsigned char>(component[i]);
57585790
57595791 // Unreserved characters per RFC 3986
5760- if (std::isalnum(c) || c == '-' || c == '.' || c == '_' || c == '~') {
5792+ if (detail::is_ascii_alnum(static_cast<char>(c)) || c == '-' || c == '.' ||
5793+ c == '_' || c == '~') {
57615794 result += static_cast<char>(c);
57625795 }
57635796 // Space handling
@@ -6010,6 +6043,48 @@ size_t MultipartFormData::get_file_count(const std::string &key) const {
60106043 return static_cast<size_t>(std::distance(r.first, r.second));
60116044}
60126045
6046+ // Multipart FormData writer implementation
6047+ bool is_valid_multipart_boundary(const std::string &boundary) {
6048+ return detail::is_multipart_boundary_chars_valid(boundary);
6049+ }
6050+
6051+ MultipartFormDataWriter::MultipartFormDataWriter()
6052+ : boundary_(detail::make_multipart_data_boundary()) {}
6053+
6054+ MultipartFormDataWriter::MultipartFormDataWriter(std::string boundary)
6055+ : boundary_(std::move(boundary)) {}
6056+
6057+ const std::string &MultipartFormDataWriter::boundary() const {
6058+ return boundary_;
6059+ }
6060+
6061+ std::string MultipartFormDataWriter::content_type() const {
6062+ return detail::serialize_multipart_formdata_get_content_type(boundary_);
6063+ }
6064+
6065+ std::string
6066+ MultipartFormDataWriter::serialize(const UploadFormDataItems &items) const {
6067+ return detail::serialize_multipart_formdata(items, boundary_);
6068+ }
6069+
6070+ size_t MultipartFormDataWriter::content_length(
6071+ const UploadFormDataItems &items) const {
6072+ return detail::get_multipart_content_length(items, boundary_);
6073+ }
6074+
6075+ std::string
6076+ MultipartFormDataWriter::item_begin(const UploadFormData &item) const {
6077+ return detail::serialize_multipart_formdata_item_begin(item, boundary_);
6078+ }
6079+
6080+ std::string MultipartFormDataWriter::item_end() {
6081+ return detail::serialize_multipart_formdata_item_end();
6082+ }
6083+
6084+ std::string MultipartFormDataWriter::finish() const {
6085+ return detail::serialize_multipart_formdata_finish(boundary_);
6086+ }
6087+
60136088// Response implementation
60146089size_t Response::get_header_value_u64(const std::string &key, size_t def,
60156090 size_t id) const {
@@ -6229,8 +6304,10 @@ ssize_t detail::BodyReader::read(char *buf, size_t len) {
62296304}
62306305
62316306// ThreadPool implementation
6232- ThreadPool::ThreadPool(size_t n, size_t max_n, size_t mqr)
6233- : base_thread_count_(n), max_queued_requests_(mqr), idle_thread_count_(0),
6307+ ThreadPool::ThreadPool(size_t n, size_t max_n, size_t mqr,
6308+ time_t idle_timeout_sec)
6309+ : base_thread_count_(n), max_queued_requests_(mqr),
6310+ idle_timeout_sec_(idle_timeout_sec), idle_thread_count_(0),
62346311 shutdown_(false) {
62356312#ifndef CPPHTTPLIB_NO_EXCEPTIONS
62366313 if (max_n != 0 && max_n < n) {
@@ -6340,9 +6417,9 @@ void ThreadPool::worker(bool is_dynamic) {
63406417 idle_thread_count_++;
63416418
63426419 if (is_dynamic) {
6343- auto has_work = cond_.wait_for(
6344- lock, std::chrono::seconds(CPPHTTPLIB_THREAD_POOL_IDLE_TIMEOUT ),
6345- [&] { return !jobs_.empty() || shutdown_; });
6420+ auto has_work =
6421+ cond_.wait_for( lock, std::chrono::seconds(idle_timeout_sec_ ),
6422+ [&] { return !jobs_.empty() || shutdown_; });
63466423 if (!has_work) {
63476424 // Timed out with no work - exit this dynamic thread
63486425 idle_thread_count_--;
@@ -9687,9 +9764,18 @@ bool ClientImpl::write_request(Stream &strm, Request &req,
96879764
96889765 if (!query_part.empty()) {
96899766 // Normalize the query string (decode then re-encode) while preserving
9690- // the original parameter order.
9691- auto normalized = detail::normalize_query_string(query_part);
9692- if (!normalized.empty()) { path_with_query += '?' + normalized; }
9767+ // the original parameter order. When path encoding is disabled the
9768+ // caller has supplied an already-encoded target and expects the exact
9769+ // bytes to be sent on the wire, so skip normalization for the query
9770+ // too. Normalizing here would decode-then-re-encode the query and
9771+ // corrupt pre-encoded binary payloads (e.g. turning `%20` into `+`,
9772+ // which a strict RFC 3986 server decodes back as `+`, not a space).
9773+ if (path_encode_) {
9774+ auto normalized = detail::normalize_query_string(query_part);
9775+ if (!normalized.empty()) { path_with_query += '?' + normalized; }
9776+ } else {
9777+ path_with_query += '?' + query_part;
9778+ }
96939779
96949780 // Still populate req.params for handlers/users who read them.
96959781 detail::parse_query_text(query_part, req.params);
@@ -12518,7 +12604,7 @@ bool is_ipv4_address(const std::string &str) {
1251812604 for (char c : str) {
1251912605 if (c == '.') {
1252012606 dots++;
12521- } else if (!isdigit(static_cast<unsigned char>(c) )) {
12607+ } else if (!detail::is_ascii_digit(c )) {
1252212608 return false;
1252312609 }
1252412610 }
@@ -12535,7 +12621,7 @@ bool parse_ipv4(const std::string &str, unsigned char *out) {
1253512621 }
1253612622 int val = 0;
1253712623 int digits = 0;
12538- while (*p >= '0' && *p <= '9' ) {
12624+ while (detail::is_ascii_digit(*p) ) {
1253912625 val = val * 10 + (*p - '0');
1254012626 if (val > 255) { return false; }
1254112627 p++;
@@ -16487,9 +16573,15 @@ bool WebSocketClient::connect() {
1648716573 return false;
1648816574 }
1648916575
16576+ #ifdef CPPHTTPLIB_SSL_ENABLED
16577+ auto is_ssl = is_ssl_;
16578+ #else
16579+ auto is_ssl = false;
16580+ #endif
16581+
1649016582 std::string selected_subprotocol;
16491- if (!detail::perform_websocket_handshake(*strm, host_, port_, path_, headers_ ,
16492- selected_subprotocol)) {
16583+ if (!detail::perform_websocket_handshake(*strm, host_, port_, is_ssl, path_ ,
16584+ headers_, selected_subprotocol)) {
1649316585 shutdown_and_close();
1649416586 return false;
1649516587 }
0 commit comments