Compare commits

...

13 Commits

Author SHA1 Message Date
yhirose 4d7c9a788d Release v0.37.2 2026-03-13 00:11:00 -04:00
yhirose 1cd0347ace Refactor parse_port function to accept char pointer and length, improving flexibility and validation 2026-03-13 00:05:13 -04:00
yhirose b3a8af80b9 Add port validation and corresponding tests to prevent overflow and out-of-range values 2026-03-13 00:05:13 -04:00
yhirose 1e97c28e36 Implement request smuggling protection for duplicate Content-Length headers and add corresponding tests 2026-03-13 00:05:13 -04:00
yhirose d279eff4db Fix the proxy test error 2026-03-12 23:15:10 -04:00
yhirose 188035fb6d Add a test for the previous change 2026-03-12 22:57:11 -04:00
yhirose 125272f34b Fix TLS cert verification bypass on proxy redirect introduced in #2165 (#2396) 2026-03-12 21:54:51 -04:00
yhirose 9ced2f614d Fix #2395 2026-03-12 19:18:09 -04:00
yhirose 68fa9bce0f Release v0.37.1 2026-03-09 20:51:37 -04:00
yhirose 1f34c541b0 Add more books 2026-03-09 19:30:18 -04:00
yhirose e41ec36274 Fix handling of malformed Content-Length in open_stream and add tests 2026-03-08 22:30:20 -04:00
yhirose 7489fd3a8b Remove 32-bit limitation (#2388)
* Remove 32-bit limitation

* Fix build problems

* Add 32-bit disclaimer and fix MSVC x86 warnings

- Move 32-bit warning to top of README with strong disclaimer
- Add static_cast<size_t> to fix truncation warnings on 32-bit MSVC

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>

---------

Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-06 23:07:21 -05:00
yhirose ab3098f18b Update README 2026-03-06 18:50:23 -05:00
21 changed files with 608 additions and 63 deletions
+36
View File
@@ -0,0 +1,36 @@
name: 32-bit Build Test
on:
push:
branches: [master]
pull_request:
branches: [master]
workflow_dispatch:
concurrency:
group: ${{ github.workflow }}-${{ github.ref || github.run_id }}
cancel-in-progress: true
jobs:
test-win32:
name: Windows 32-bit (MSVC x86)
runs-on: windows-latest
timeout-minutes: 10
steps:
- uses: actions/checkout@v4
- name: Build (Win32)
shell: cmd
run: |
call "C:\Program Files\Microsoft Visual Studio\2022\Enterprise\VC\Auxiliary\Build\vcvarsall.bat" x86
cl /std:c++14 /EHsc /W4 /WX /c /Fo:NUL test\test_32bit_build.cpp
test-arm32:
name: ARM 32-bit (cross-compile)
runs-on: ubuntu-latest
timeout-minutes: 10
steps:
- uses: actions/checkout@v4
- name: Install cross compiler
run: sudo apt-get update && sudo apt-get install -y g++-arm-linux-gnueabihf
- name: Build (ARM 32-bit)
run: arm-linux-gnueabihf-g++ -std=c++11 -Wall -Wextra -Wno-psabi -Werror -c -o /dev/null test/test_32bit_build.cpp
+2 -3
View File
@@ -40,7 +40,7 @@ jobs:
clang-format --version
cd test && make style_check
build-error-check-on-32bit:
build-and-test-on-32bit:
runs-on: ubuntu-latest
if: >
(github.event_name == 'push') ||
@@ -64,9 +64,8 @@ jobs:
libssl-dev${{ matrix.config.arch_suffix }} libcurl4-openssl-dev${{ matrix.config.arch_suffix }} \
zlib1g-dev${{ matrix.config.arch_suffix }} libbrotli-dev${{ matrix.config.arch_suffix }} \
libzstd-dev${{ matrix.config.arch_suffix }}
- name: build and run tests (expect failure)
- name: build and run tests
run: cd test && make test EXTRA_CXXFLAGS="${{ matrix.config.arch_flags }}"
continue-on-error: true
ubuntu:
runs-on: ubuntu-latest
+5 -1
View File
@@ -28,6 +28,10 @@ jobs:
- name: Run proxy tests (OpenSSL)
if: matrix.tls_backend == 'openssl'
run: cd test && make proxy
env:
COMPOSE_FILE: docker-compose.yml:docker-compose.ci.yml
- name: Run proxy tests (Mbed TLS)
if: matrix.tls_backend == 'mbedtls'
run: cd test && make proxy_mbedtls
run: cd test && make proxy_mbedtls
env:
COMPOSE_FILE: docker-compose.yml:docker-compose.ci.yml
-3
View File
@@ -173,9 +173,6 @@ if(CMAKE_SYSTEM_NAME MATCHES "Windows")
message(WARNING "The target is Windows but CMAKE_SYSTEM_VERSION is not set, the default system version is set to Windows 10.")
endif()
endif()
if(CMAKE_SIZEOF_VOID_P LESS 8)
message(WARNING "Pointer size ${CMAKE_SIZEOF_VOID_P} is not supported. Please use a 64-bit compiler.")
endif()
# Set some variables that are used in-tree and while building based on our options
set(HTTPLIB_IS_COMPILED ${HTTPLIB_COMPILE})
+4 -1
View File
@@ -5,11 +5,14 @@
A C++11 single-file header-only cross platform HTTP/HTTPS library.<br>
It's extremely easy to set up. Just include the **[httplib.h](https://raw.githubusercontent.com/yhirose/cpp-httplib/refs/heads/master/httplib.h)** file in your code!
**Learn more in the [official documentation](https://yhirose.github.io/cpp-httplib/)**.
Learn more in the [official documentation](https://yhirose.github.io/cpp-httplib/) (built with [docs-gen](https://github.com/yhirose/docs-gen)).
> [!IMPORTANT]
> This library uses 'blocking' socket I/O. If you are looking for a library with 'non-blocking' socket I/O, this is not the one that you want.
> [!WARNING]
> 32-bit platforms are **NOT supported**. Use at your own risk. The library may compile on 32-bit targets, but no security review has been conducted for 32-bit environments. Integer truncation and other 32-bit-specific issues may exist. **Security reports that only affect 32-bit platforms will be closed without action.** The maintainer does not have access to 32-bit environments for testing or fixing issues. CI includes basic compile checks only, not functional or security testing.
## Main Features
- HTTP Server/Client
+11 -1
View File
@@ -4,7 +4,7 @@ langs = ["en", "ja"]
[site]
title = "cpp-httplib"
version = "0.37.0"
version = "0.37.2"
hostname = "https://yhirose.github.io"
base_path = "/cpp-httplib"
footer_message = "© 2026 Yuji Hirose. All rights reserved."
@@ -14,6 +14,16 @@ label = "Tour"
path = "tour/"
icon_svg = '<svg xmlns="http://www.w3.org/2000/svg" width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><circle cx="12" cy="12" r="10"/><polygon points="16.24 7.76 14.12 14.12 7.76 16.24 9.88 9.88 16.24 7.76"/></svg>'
[[nav]]
label = "Cookbook"
path = "cookbook/"
icon_svg = '<svg xmlns="http://www.w3.org/2000/svg" width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M4 19.5A2.5 2.5 0 0 1 6.5 17H20"/><path d="M6.5 2H20v20H6.5A2.5 2.5 0 0 1 4 19.5v-15A2.5 2.5 0 0 1 6.5 2z"/></svg>'
[[nav]]
label = "LLM App"
path = "llm-app/"
icon_svg = '<svg xmlns="http://www.w3.org/2000/svg" width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M4 19.5A2.5 2.5 0 0 1 6.5 17H20"/><path d="M6.5 2H20v20H6.5A2.5 2.5 0 0 1 4 19.5v-15A2.5 2.5 0 0 1 6.5 2z"/></svg>'
[[nav]]
label = "GitHub"
url = "https://github.com/yhirose/cpp-httplib"
+90 -3
View File
@@ -1,8 +1,95 @@
---
title: "Cookbook"
order: 1
order: 0
---
This section is under construction.
A collection of recipes that answer "How do I...?" questions. Each recipe is self-contained — read only what you need.
Check back soon for a collection of recipes organized by topic.
## Client
### Basics
- Get the response body as a string / save to a file
- Send and receive JSON
- Set default headers (`set_default_headers`)
- Follow redirects (`set_follow_location`)
### Authentication
- Use Basic authentication (`set_basic_auth`)
- Call an API with a Bearer token
### File Upload
- Upload a file as multipart form data (`make_file_provider`)
- POST a file as raw binary (`make_file_body`)
- Send the body with chunked transfer (Content Provider)
### Streaming & Progress
- Receive a response as a stream
- Use the progress callback (`set_progress`)
### Connection & Performance
- Set timeouts (`set_connection_timeout` / `set_read_timeout`)
- Set an overall timeout (`set_max_timeout`)
- Understand connection reuse and Keep-Alive behavior
- Enable compression (`set_compress` / `set_decompress`)
- Send requests through a proxy (`set_proxy`)
### Error Handling & Debugging
- Handle error codes (`Result::error()`)
- Handle SSL errors (`ssl_error()` / `ssl_backend_error()`)
- Set up client logging (`set_logger` / `set_error_logger`)
## Server
### Basics
- Register GET / POST / PUT / DELETE handlers
- Receive JSON requests and return JSON responses
- Use path parameters (`/users/:id`)
- Set up a static file server (`set_mount_point`)
### Streaming & Files
- Stream a large file in the response (`ContentProvider`)
- Return a file download response (`Content-Disposition`)
- Receive multipart data as a stream (`ContentReader`)
- Compress responses (gzip)
### Handler Chain
- Add pre-processing to all routes (Pre-routing handler)
- Add response headers with a Post-routing handler (CORS, etc.)
- Authenticate per route with a Pre-request handler (`matched_route`)
- Pass data between handlers with `res.user_data`
### Error Handling & Debugging
- Return custom error pages (`set_error_handler`)
- Catch exceptions (`set_exception_handler`)
- Log requests (Logger)
- Detect client disconnection (`req.is_connection_closed()`)
### Operations & Tuning
- Assign a port dynamically (`bind_to_any_port`)
- Control startup order with `listen_after_bind`
- Shut down gracefully (`stop()` and signal handling)
- Tune Keep-Alive (`set_keep_alive_max_count` / `set_keep_alive_timeout`)
- Configure the thread pool (`new_task_queue`)
## TLS / Security
- Choosing between OpenSSL, mbedTLS, and wolfSSL (build-time `#define` differences)
- Control SSL certificate verification (disable, custom CA, custom callback)
- Use a custom certificate verification callback (`set_server_certificate_verifier`)
- Set up an SSL/TLS server (certificate and private key)
- Configure mTLS (mutual TLS with client certificates)
- Access the peer certificate on the server (`req.peer_cert()` / SNI)
## SSE
- Implement an SSE server
- Use event names to distinguish event types
- Handle reconnection (`Last-Event-ID`)
- Receive SSE events on the client
## WebSocket
- Implement a WebSocket echo server and client
- Configure heartbeats (`set_websocket_ping_interval`)
- Handle connection close
- Send and receive binary frames
+1
View File
@@ -19,3 +19,4 @@ Under the hood, it uses blocking I/O with a thread pool. It's not built for hand
- [A Tour of cpp-httplib](tour/) — A step-by-step tutorial covering the basics. Start here if you're new
- [Cookbook](cookbook/) — A collection of recipes organized by topic. Jump to whatever you need
- [Building a Desktop LLM App](llm-app/) — A hands-on guide to building a desktop app with llama.cpp, step by step
+22
View File
@@ -0,0 +1,22 @@
---
title: "Building a Desktop LLM App with cpp-httplib"
order: 0
---
Build an LLM-powered translation desktop app step by step, learning both the server and client sides of cpp-httplib along the way. Translation is just an example — swap it out to build your own summarizer, code generator, chatbot, or any other LLM application.
## Dependencies
- [llama.cpp](https://github.com/ggml-org/llama.cpp) — LLM inference engine
- [nlohmann/json](https://github.com/nlohmann/json) — JSON parser (header-only)
- [webview/webview](https://github.com/webview/webview) — WebView wrapper (header-only)
- [cpp-httplib](https://github.com/yhirose/cpp-httplib) — HTTP server/client (header-only)
## Chapters
1. **Embed llama.cpp and create a REST API** — Start with a simple API that accepts text via POST and returns a translation as JSON
2. **Add token streaming with SSE** — Stream translation results token by token using the standard LLM API approach
3. **Add model discovery and download** — Use the client to search and download GGUF models from Hugging Face
4. **Add a Web UI** — Serve a translation UI with static file hosting, making the app accessible from a browser
5. **Turn it into a desktop app with WebView** — Wrap the web app with webview/webview to create an Electron-like desktop application
6. **Code reading: llama.cpp's server implementation** — Compare your implementation with production-quality code and learn from the differences
+1 -1
View File
@@ -1,6 +1,6 @@
---
title: "A Tour of cpp-httplib"
order: 1
order: 0
---
This is a step-by-step tutorial that walks you through the basics of cpp-httplib. Each chapter builds on the previous one, so please read them in order starting from Chapter 1.
+90 -3
View File
@@ -1,8 +1,95 @@
---
title: "Cookbook"
order: 1
order: 0
---
This section is under construction.
「〇〇をするには?」という問いに答えるレシピ集です。各レシピは独立しているので、必要なページだけ読めます。
Check back soon for a collection of recipes organized by topic.
## クライアント
### 基本
- レスポンスボディを文字列で取得する / ファイルに保存する
- JSON を送受信する
- デフォルトヘッダーを設定する(`set_default_headers`
- リダイレクトを追従する(`set_follow_location`
### 認証
- Basic 認証を使う(`set_basic_auth`
- Bearer トークンで API を呼ぶ
### ファイル送信
- ファイルをマルチパートフォームとしてアップロードする(`make_file_provider`
- ファイルを生バイナリとして POST する(`make_file_body`
- チャンク転送でボディを送る(Content Provider
### ストリーミング・進捗
- レスポンスをストリーミングで受信する
- 進捗コールバックを使う(`set_progress`
### 接続・パフォーマンス
- タイムアウトを設定する(`set_connection_timeout` / `set_read_timeout`
- 全体タイムアウトを設定する(`set_max_timeout`
- 接続の再利用と Keep-Alive の挙動を理解する
- 圧縮を有効にする(`set_compress` / `set_decompress`
- プロキシを経由してリクエストを送る(`set_proxy`
### エラー処理・デバッグ
- エラーコードをハンドリングする(`Result::error()`
- SSL エラーをハンドリングする(`ssl_error()` / `ssl_backend_error()`
- クライアントにログを設定する(`set_logger` / `set_error_logger`
## サーバー
### 基本
- GET / POST / PUT / DELETE ハンドラを登録する
- JSON リクエストを受け取り JSON レスポンスを返す
- パスパラメーターを使う(`/users/:id`
- 静的ファイルサーバーを設定する(`set_mount_point`
### ストリーミング・ファイル
- 大きなファイルをストリーミングで返す(`ContentProvider`
- ファイルダウンロードレスポンスを返す(`Content-Disposition`
- マルチパートデータをストリーミングで受け取る(`ContentReader`
- レスポンスを圧縮して返す(gzip)
### ハンドラチェーン
- 全ルートに共通の前処理をする(Pre-routing handler
- Post-routing handler でレスポンスヘッダーを追加する(CORS など)
- Pre-request handler でルート単位の認証を行う(`matched_route`
- `res.user_data` でハンドラ間データを渡す
### エラー処理・デバッグ
- カスタムエラーページを返す(`set_error_handler`
- 例外をキャッチする(`set_exception_handler`
- リクエストをログに記録する(Logger)
- クライアントが切断したか検出する(`req.is_connection_closed()`
### 運用・チューニング
- ポートを動的に割り当てる(`bind_to_any_port`
- `listen_after_bind` で起動順序を制御する
- グレースフルシャットダウンする(`stop()` とシグナルハンドリング)
- Keep-Alive を調整する(`set_keep_alive_max_count` / `set_keep_alive_timeout`
- マルチスレッド数を設定する(`new_task_queue`
## TLS / セキュリティ
- OpenSSL・mbedTLS・wolfSSL の選択指針(ビルド時の `#define` の違い)
- SSL 証明書の検証を制御する(証明書の無効化・カスタム CA・カスタムコールバック)
- カスタム証明書検証コールバックを使う(`set_server_certificate_verifier`
- SSL/TLS サーバーを立ち上げる(証明書・秘密鍵の設定)
- mTLS(クライアント証明書による相互認証)を設定する
- サーバー側でピア証明書を参照する(`req.peer_cert()` / SNI
## SSE
- SSE サーバーを実装する
- SSE でイベント名を使い分ける
- SSE の再接続を処理する(`Last-Event-ID`
- SSE をクライアントで受信する
## WebSocket
- WebSocket エコーサーバー/クライアントを実装する
- ハートビートを設定する(`set_websocket_ping_interval`
- 接続クローズをハンドリングする
- バイナリフレームを送受信する
+1
View File
@@ -19,3 +19,4 @@ HTTPSも使えます。OpenSSLやmbedTLSをリンクするだけで、サーバ
- [A Tour of cpp-httplib](tour/) — 基本を順を追って学べるチュートリアル。初めての方はここから
- [Cookbook](cookbook/) — 目的別のレシピ集。必要なトピックから読めます
- [Building a Desktop LLM App](llm-app/) — llama.cpp を組み込んだデスクトップアプリを段階的に構築する実践ガイド
+22
View File
@@ -0,0 +1,22 @@
---
title: "Building a Desktop LLM App with cpp-httplib"
order: 0
---
llama.cpp を組み込んだ LLM 翻訳デスクトップアプリを段階的に構築しながら、cpp-httplib のサーバー・クライアント両面の使い方を実践的に学びます。翻訳は一例であり、この部分を差し替えることで要約・コード生成・チャットボットなど自分のアプリに応用できます。
## 依存ライブラリ
- [llama.cpp](https://github.com/ggml-org/llama.cpp) — LLM 推論エンジン
- [nlohmann/json](https://github.com/nlohmann/json) — JSON パーサー(ヘッダーオンリー)
- [webview/webview](https://github.com/webview/webview) — WebView ラッパー(ヘッダーオンリー)
- [cpp-httplib](https://github.com/yhirose/cpp-httplib) — HTTP サーバー/クライアント(ヘッダーオンリー)
## 章立て
1. **llama.cpp を組み込んで REST API を作る** — テキストを POST すると翻訳結果を JSON で返すシンプルな API から始める
2. **SSE でトークンストリーミングを追加する** — 翻訳結果をトークン単位で逐次返す LLM API 標準の方式を実装する
3. **モデルの取得・管理機能を追加する** — Hugging Face から GGUF モデルを検索・ダウンロードするクライアント機能を実装する
4. **Web UI を追加する** — 静的ファイル配信で翻訳 UI をホストし、ブラウザから操作できるようにする
5. **WebView でデスクトップアプリ化する** — webview/webview で包み、Electron 的なデスクトップアプリとして動作させる
6. **llama.cpp 本家のサーバー実装をコードリーディング** — 自分で作ったものとプロダクション品質のコードを比較して学ぶ
+1 -1
View File
@@ -1,6 +1,6 @@
---
title: "A Tour of cpp-httplib"
order: 1
order: 0
---
cpp-httplibの基本を、順番に学んでいくチュートリアルです。各章は前の章の内容を踏まえて進む構成なので、1章から順に読んでください。
+53 -46
View File
@@ -8,28 +8,8 @@
#ifndef CPPHTTPLIB_HTTPLIB_H
#define CPPHTTPLIB_HTTPLIB_H
#define CPPHTTPLIB_VERSION "0.37.0"
#define CPPHTTPLIB_VERSION_NUM "0x002500"
/*
* Platform compatibility check
*/
#if defined(_WIN32) && !defined(_WIN64)
#if defined(_MSC_VER)
#pragma message( \
"cpp-httplib doesn't support 32-bit Windows. Please use a 64-bit compiler.")
#else
#warning \
"cpp-httplib doesn't support 32-bit Windows. Please use a 64-bit compiler."
#endif
#elif defined(__SIZEOF_POINTER__) && __SIZEOF_POINTER__ < 8
#warning \
"cpp-httplib doesn't support 32-bit platforms. Please use a 64-bit compiler."
#elif defined(__SIZEOF_SIZE_T__) && __SIZEOF_SIZE_T__ < 8
#warning \
"cpp-httplib doesn't support platforms where size_t is less than 64 bits."
#endif
#define CPPHTTPLIB_VERSION "0.37.2"
#define CPPHTTPLIB_VERSION_NUM "0x002502"
#ifdef _WIN32
#if defined(_WIN32_WINNT) && _WIN32_WINNT < 0x0A00
@@ -709,6 +689,18 @@ inline from_chars_result<double> from_chars(const char *first, const char *last,
return {first + (endptr - s.c_str()), std::errc{}};
}
inline bool parse_port(const char *s, size_t len, int &port) {
int val = 0;
auto r = from_chars(s, s + len, val);
if (r.ec != std::errc{} || val < 1 || val > 65535) { return false; }
port = val;
return true;
}
inline bool parse_port(const std::string &s, int &port) {
return parse_port(s.data(), s.size(), port);
}
} // namespace detail
enum SSLVerifierResponse {
@@ -2801,7 +2793,7 @@ inline size_t get_header_value_u64(const Headers &headers,
std::advance(it, static_cast<ssize_t>(id));
if (it != rng.second) {
if (is_numeric(it->second)) {
return std::strtoull(it->second.data(), nullptr, 10);
return static_cast<size_t>(std::strtoull(it->second.data(), nullptr, 10));
} else {
is_invalid_value = true;
}
@@ -5812,9 +5804,9 @@ inline int getaddrinfo_with_timeout(const char *node, const char *service,
memcpy((*current)->ai_addr, sockaddr_ptr, sockaddr_len);
// Set port if service is specified
if (service && strlen(service) > 0) {
int port = atoi(service);
if (port > 0) {
if (service && *service) {
int port = 0;
if (parse_port(service, strlen(service), port)) {
if (sockaddr_ptr->sa_family == AF_INET) {
reinterpret_cast<struct sockaddr_in *>((*current)->ai_addr)
->sin_port = htons(static_cast<uint16_t>(port));
@@ -6833,6 +6825,16 @@ inline bool read_headers(Stream &strm, Headers &headers) {
header_count++;
}
// RFC 9110 Section 8.6: Reject requests with multiple Content-Length
// headers that have different values to prevent request smuggling.
auto cl_range = headers.equal_range("Content-Length");
if (cl_range.first != cl_range.second) {
const auto &first_val = cl_range.first->second;
for (auto it = std::next(cl_range.first); it != cl_range.second; ++it) {
if (it->second != first_val) { return false; }
}
}
return true;
}
@@ -8241,7 +8243,8 @@ get_range_offset_and_length(Range r, size_t content_length) {
assert(r.first <= r.second &&
r.second < static_cast<ssize_t>(content_length));
(void)(content_length);
return std::make_pair(r.first, static_cast<size_t>(r.second - r.first) + 1);
return std::make_pair(static_cast<size_t>(r.first),
static_cast<size_t>(r.second - r.first) + 1);
}
inline std::string make_content_range_header_field(
@@ -11338,6 +11341,10 @@ inline bool Server::listen_internal() {
detail::set_socket_opt_time(sock, SOL_SOCKET, SO_SNDTIMEO,
write_timeout_sec_, write_timeout_usec_);
if (tcp_nodelay_) {
detail::set_socket_opt(sock, IPPROTO_TCP, TCP_NODELAY, 1);
}
if (!task_queue->enqueue(
[this, sock]() { process_and_close_socket(sock); })) {
output_error_log(Error::ResourceExhaustion, nullptr);
@@ -12433,11 +12440,17 @@ ClientImpl::open_stream(const std::string &method, const std::string &path,
handle.body_reader_.stream = handle.stream_;
handle.body_reader_.payload_max_length = payload_max_length_;
auto content_length_str = handle.response->get_header_value("Content-Length");
if (!content_length_str.empty()) {
if (handle.response->has_header("Content-Length")) {
bool is_invalid = false;
auto content_length = detail::get_header_value_u64(
handle.response->headers, "Content-Length", 0, 0, is_invalid);
if (is_invalid) {
handle.error = Error::Read;
handle.response.reset();
return handle;
}
handle.body_reader_.has_content_length = true;
handle.body_reader_.content_length =
static_cast<size_t>(std::stoull(content_length_str));
handle.body_reader_.content_length = content_length;
}
auto transfer_encoding =
@@ -12721,7 +12734,7 @@ inline bool ClientImpl::redirect(Request &req, Response &res, Error &error) {
auto next_port = port_;
if (!port_str.empty()) {
next_port = std::stoi(port_str);
if (!detail::parse_port(port_str, next_port)) { return false; }
} else if (!next_scheme.empty()) {
next_port = next_scheme == "https" ? 443 : 80;
}
@@ -12772,18 +12785,10 @@ inline bool ClientImpl::create_redirect_client(
// Setup basic client configuration first
setup_redirect_client(redirect_client);
// SSL-specific configuration for proxy environments
if (!proxy_host_.empty() && proxy_port_ != -1) {
// Critical: Disable SSL verification for proxy environments
redirect_client.enable_server_certificate_verification(false);
redirect_client.enable_server_hostname_verification(false);
} else {
// For direct SSL connections, copy SSL verification settings
redirect_client.enable_server_certificate_verification(
server_certificate_verification_);
redirect_client.enable_server_hostname_verification(
server_hostname_verification_);
}
redirect_client.enable_server_certificate_verification(
server_certificate_verification_);
redirect_client.enable_server_hostname_verification(
server_hostname_verification_);
// Transfer CA certificate to redirect client
if (!ca_cert_pem_.empty()) {
@@ -14500,7 +14505,8 @@ inline Client::Client(const std::string &scheme_host_port,
if (host.empty()) { host = m[3].str(); }
auto port_str = m[4].str();
auto port = !port_str.empty() ? std::stoi(port_str) : (is_ssl ? 443 : 80);
auto port = is_ssl ? 443 : 80;
if (!port_str.empty() && !detail::parse_port(port_str, port)) { return; }
if (is_ssl) {
#ifdef CPPHTTPLIB_SSL_ENABLED
@@ -19913,7 +19919,8 @@ inline WebSocketClient::WebSocketClient(
if (host_.empty()) { host_ = m[3].str(); }
auto port_str = m[4].str();
port_ = !port_str.empty() ? std::stoi(port_str) : (is_ssl ? 443 : 80);
port_ = is_ssl ? 443 : 80;
if (!port_str.empty() && !detail::parse_port(port_str, port_)) { return; }
path_ = m[5].str();
+1
View File
@@ -15,6 +15,7 @@ acl localnet src fc00::/7 # RFC 4193 local private network range
acl localnet src fe80::/10 # RFC 4291 link-local (directly plugged) machines
acl SSL_ports port 443
acl SSL_ports port 1025-65535
acl Safe_ports port 80 # http
acl Safe_ports port 21 # ftp
acl Safe_ports port 443 # https
+1
View File
@@ -15,6 +15,7 @@ acl localnet src fc00::/7 # RFC 4193 local private network range
acl localnet src fe80::/10 # RFC 4291 link-local (directly plugged) machines
acl SSL_ports port 443
acl SSL_ports port 1025-65535
acl Safe_ports port 80 # http
acl Safe_ports port 21 # ftp
acl Safe_ports port 443 # https
+8
View File
@@ -0,0 +1,8 @@
services:
squid_basic:
extra_hosts:
- "host.docker.internal:host-gateway"
squid_digest:
extra_hosts:
- "host.docker.internal:host-gateway"
+201
View File
@@ -2330,6 +2330,31 @@ TEST(RedirectToDifferentPort, Redirect) {
EXPECT_EQ("Hello World!", res->body);
}
TEST(RedirectToDifferentPort, OverflowPortNumber) {
Server svr;
svr.Get("/redir", [&](const Request & /*req*/, Response &res) {
// Port number that overflows int — should not crash
res.set_redirect("http://localhost:99999999999999999999/target");
});
auto port = svr.bind_to_any_port(HOST);
auto thread = std::thread([&]() { svr.listen_after_bind(); });
auto se = detail::scope_exit([&] {
svr.stop();
thread.join();
ASSERT_FALSE(svr.is_running());
});
svr.wait_until_ready();
Client cli(HOST, port);
cli.set_follow_location(true);
auto res = cli.Get("/redir");
// Should fail gracefully, not crash (no valid response due to bad port)
EXPECT_FALSE(res);
}
TEST(RedirectFromPageWithContent, Redirect) {
Server svr;
@@ -9441,6 +9466,18 @@ TEST(HostAndPortPropertiesTest, NoSSLWithSimpleAPI) {
ASSERT_EQ(1234, cli.port());
}
TEST(HostAndPortPropertiesTest, OverflowPortNumber) {
// Port number that overflows int — should not crash, client becomes invalid
httplib::Client cli("http://www.google.com:99999999999999999999");
ASSERT_FALSE(cli.is_valid());
}
TEST(HostAndPortPropertiesTest, PortOutOfRange) {
// Port 99999 exceeds valid range (1-65535) — should not crash
httplib::Client cli("http://www.google.com:99999");
ASSERT_FALSE(cli.is_valid());
}
#ifdef CPPHTTPLIB_SSL_ENABLED
TEST(HostAndPortPropertiesTest, SSL) {
httplib::SSLClient cli("www.google.com");
@@ -13051,6 +13088,66 @@ TEST(ClientInThreadTest, Issue2068) {
}
}
TEST(RequestSmugglingTest, DuplicateContentLengthDifferentValues) {
auto handled = false;
Server svr;
svr.Post("/test", [&](const Request &, Response &) { handled = true; });
thread t = thread([&]() { svr.listen(HOST, PORT); });
auto se = detail::scope_exit([&] {
svr.stop();
t.join();
ASSERT_FALSE(svr.is_running());
ASSERT_FALSE(handled);
});
svr.wait_until_ready();
// Two Content-Length headers with different values — must be rejected
auto req = "POST /test HTTP/1.1\r\n"
"Content-Length: 5\r\n"
"Content-Length: 10\r\n"
"\r\n"
"hello";
std::string response;
ASSERT_TRUE(send_request(1, req, &response));
ASSERT_EQ("HTTP/1.1 400 Bad Request",
response.substr(0, response.find("\r\n")));
}
TEST(RequestSmugglingTest, DuplicateContentLengthSameValues) {
auto handled = false;
Server svr;
svr.Post("/test", [&](const Request &, Response &res) {
handled = true;
res.set_content("ok", "text/plain");
});
thread t = thread([&]() { svr.listen(HOST, PORT); });
auto se = detail::scope_exit([&] {
svr.stop();
t.join();
ASSERT_FALSE(svr.is_running());
ASSERT_TRUE(handled);
});
svr.wait_until_ready();
// Two Content-Length headers with same value — should be accepted (RFC 9110)
auto req = "POST /test HTTP/1.1\r\n"
"Content-Length: 5\r\n"
"Content-Length: 5\r\n"
"\r\n"
"hello";
std::string response;
ASSERT_TRUE(send_request(1, req, &response));
ASSERT_EQ("HTTP/1.1 200 OK", response.substr(0, response.find("\r\n")));
}
TEST(HeaderSmugglingTest, ChunkedTrailerHeadersMerged) {
Server svr;
@@ -13712,6 +13809,102 @@ TEST_F(OpenStreamTest, ProhibitedTrailersAreIgnored_Stream) {
EXPECT_EQ(std::string(""), handle.response->get_header_value("X-Allowed"));
}
static std::thread serve_single_response(int port,
const std::string &response) {
return std::thread([port, response] {
auto srv = ::socket(AF_INET, SOCK_STREAM, 0);
default_socket_options(srv);
detail::set_socket_opt_time(srv, SOL_SOCKET, SO_RCVTIMEO, 5, 0);
detail::set_socket_opt_time(srv, SOL_SOCKET, SO_SNDTIMEO, 5, 0);
sockaddr_in addr{};
addr.sin_family = AF_INET;
addr.sin_port = htons(static_cast<uint16_t>(port));
::inet_pton(AF_INET, "127.0.0.1", &addr.sin_addr);
int opt = 1;
::setsockopt(srv, SOL_SOCKET, SO_REUSEADDR,
#ifdef _WIN32
reinterpret_cast<const char *>(&opt),
#else
&opt,
#endif
sizeof(opt));
::bind(srv, reinterpret_cast<sockaddr *>(&addr), sizeof(addr));
::listen(srv, 1);
sockaddr_in cli_addr{};
socklen_t cli_len = sizeof(cli_addr);
auto cli = ::accept(srv, reinterpret_cast<sockaddr *>(&cli_addr), &cli_len);
if (cli != INVALID_SOCKET) {
char buf[4096];
::recv(cli, buf, sizeof(buf), 0);
::send(cli,
#ifdef _WIN32
static_cast<const char *>(response.c_str()),
static_cast<int>(response.size()),
#else
response.c_str(), response.size(),
#endif
0);
detail::close_socket(cli);
}
detail::close_socket(srv);
});
}
TEST(OpenStreamMalformedContentLength, InvalidArgument) {
#ifndef _WIN32
signal(SIGPIPE, SIG_IGN);
#endif
auto server_thread =
serve_single_response(PORT + 2, "HTTP/1.1 200 OK\r\n"
"Content-Type: text/plain\r\n"
"Content-Length: not-a-number\r\n"
"Connection: close\r\n"
"\r\n"
"hello");
std::this_thread::sleep_for(std::chrono::milliseconds(200));
Client cli("127.0.0.1", PORT + 2);
auto handle = cli.open_stream("GET", "/");
EXPECT_FALSE(handle.is_valid());
server_thread.join();
}
TEST(OpenStreamMalformedContentLength, OutOfRange) {
#ifndef _WIN32
signal(SIGPIPE, SIG_IGN);
#endif
auto server_thread = serve_single_response(
PORT + 2, "HTTP/1.1 200 OK\r\n"
"Content-Type: text/plain\r\n"
"Content-Length: 99999999999999999999999999\r\n"
"Connection: close\r\n"
"\r\n"
"hello");
std::this_thread::sleep_for(std::chrono::milliseconds(200));
// Before the fix, std::stoull would throw std::out_of_range here and
// crash the process. After the fix, strtoull silently clamps to
// ULLONG_MAX so the stream opens without crashing. The important thing
// is that the process does NOT terminate.
Client cli("127.0.0.1", PORT + 2);
auto handle = cli.open_stream("GET", "/");
EXPECT_TRUE(handle.is_valid());
server_thread.join();
}
#ifdef CPPHTTPLIB_ZLIB_SUPPORT
TEST_F(OpenStreamTest, Gzip) {
Client cli("127.0.0.1", port_);
@@ -16329,6 +16522,14 @@ TEST(WebSocketTest, InvalidURL) {
// Missing host
ws::WebSocketClient ws4("ws://:8080/path");
EXPECT_FALSE(ws4.is_valid());
// Port number overflow — should not crash
ws::WebSocketClient ws5("ws://localhost:99999999999999999999/path");
EXPECT_FALSE(ws5.is_valid());
// Port out of range
ws::WebSocketClient ws6("ws://localhost:99999/path");
EXPECT_FALSE(ws6.is_valid());
}
TEST(WebSocketTest, UnsupportedScheme) {
+9
View File
@@ -0,0 +1,9 @@
#include "../httplib.h"
int main() {
httplib::Server svr;
httplib::Client cli("localhost", 8080);
(void)svr;
(void)cli;
return 0;
}
+49
View File
@@ -109,6 +109,55 @@ TEST(RedirectTest, YouTubeSSLDigest) {
// ----------------------------------------------------------------------------
#ifdef CPPHTTPLIB_SSL_ENABLED
TEST(RedirectTest, TLSVerificationOnProxyRedirect) {
// Untrusted HTTPS server with self-signed cert
SSLServer untrusted_svr("cert.pem", "key.pem");
untrusted_svr.Get("/", [](const Request &, Response &res) {
res.set_content("MITM'd", "text/plain");
});
auto untrusted_port = untrusted_svr.bind_to_any_port("0.0.0.0");
auto t1 = thread([&]() { untrusted_svr.listen_after_bind(); });
auto se1 = detail::scope_exit([&] {
untrusted_svr.stop();
t1.join();
});
// HTTP server that redirects to the untrusted HTTPS server
// Use host.docker.internal so the proxy container can reach the host
Server redirect_svr;
redirect_svr.Get("/", [&](const Request &, Response &res) {
res.set_redirect(
"https://host.docker.internal:" + to_string(untrusted_port) + "/");
});
auto redirect_port = redirect_svr.bind_to_any_port("0.0.0.0");
auto t2 = thread([&]() { redirect_svr.listen_after_bind(); });
auto se2 = detail::scope_exit([&] {
redirect_svr.stop();
t2.join();
});
// Wait until servers are up
untrusted_svr.wait_until_ready();
redirect_svr.wait_until_ready();
// Client with proxy + follow_location, verification ON (default)
Client cli("host.docker.internal", redirect_port);
cli.set_proxy("localhost", 3128);
cli.set_proxy_basic_auth("hello", "world");
cli.set_follow_location(true);
auto res = cli.Get("/");
// Self-signed cert must be rejected
ASSERT_TRUE(res == nullptr);
}
#endif
// ----------------------------------------------------------------------------
template <typename T> void BaseAuthTestFromHTTPWatch(T &cli) {
cli.set_proxy("localhost", 3128);
cli.set_proxy_basic_auth("hello", "world");