mirror of
https://github.com/yhirose/cpp-httplib
synced 2026-06-08 18:30:49 +00:00
Compare commits
13 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 4d7c9a788d | |||
| 1cd0347ace | |||
| b3a8af80b9 | |||
| 1e97c28e36 | |||
| d279eff4db | |||
| 188035fb6d | |||
| 125272f34b | |||
| 9ced2f614d | |||
| 68fa9bce0f | |||
| 1f34c541b0 | |||
| e41ec36274 | |||
| 7489fd3a8b | |||
| ab3098f18b |
@@ -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
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
@@ -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})
|
||||
|
||||
@@ -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
@@ -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"
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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,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.
|
||||
|
||||
@@ -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`)
|
||||
- 接続クローズをハンドリングする
|
||||
- バイナリフレームを送受信する
|
||||
|
||||
@@ -19,3 +19,4 @@ HTTPSも使えます。OpenSSLやmbedTLSをリンクするだけで、サーバ
|
||||
|
||||
- [A Tour of cpp-httplib](tour/) — 基本を順を追って学べるチュートリアル。初めての方はここから
|
||||
- [Cookbook](cookbook/) — 目的別のレシピ集。必要なトピックから読めます
|
||||
- [Building a Desktop LLM App](llm-app/) — llama.cpp を組み込んだデスクトップアプリを段階的に構築する実践ガイド
|
||||
|
||||
@@ -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,6 +1,6 @@
|
||||
---
|
||||
title: "A Tour of cpp-httplib"
|
||||
order: 1
|
||||
order: 0
|
||||
---
|
||||
|
||||
cpp-httplibの基本を、順番に学んでいくチュートリアルです。各章は前の章の内容を踏まえて進む構成なので、1章から順に読んでください。
|
||||
|
||||
@@ -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();
|
||||
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -0,0 +1,8 @@
|
||||
services:
|
||||
squid_basic:
|
||||
extra_hosts:
|
||||
- "host.docker.internal:host-gateway"
|
||||
|
||||
squid_digest:
|
||||
extra_hosts:
|
||||
- "host.docker.internal:host-gateway"
|
||||
+201
@@ -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) {
|
||||
|
||||
@@ -0,0 +1,9 @@
|
||||
#include "../httplib.h"
|
||||
|
||||
int main() {
|
||||
httplib::Server svr;
|
||||
httplib::Client cli("localhost", 8080);
|
||||
(void)svr;
|
||||
(void)cli;
|
||||
return 0;
|
||||
}
|
||||
@@ -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");
|
||||
|
||||
Reference in New Issue
Block a user