From 797758a7420bf6c20fbcd86823d891ce4aeee692 Mon Sep 17 00:00:00 2001 From: yhirose Date: Sat, 28 Feb 2026 14:45:40 -0500 Subject: [PATCH] Documentation Site on GitHub Pages (#2376) * Add initial documentations * Update documentation for Basic Client and add WebSocket section * feat: add a static site generator with multi-language support - Introduced a new Rust-based static site generator in the `docs-gen` directory. - Implemented core functionality for building sites from markdown files, including: - Configuration loading from `config.toml`. - Markdown rendering with frontmatter support. - Navigation generation based on page structure. - Static file copying and output directory management. - Added templates for base layout, pages, and portal. - Created a CSS file for styling and a JavaScript file for interactive features like language selection and theme toggling. - Updated documentation source with new configuration and example pages in English and Japanese. - Added a `justfile` target for building the documentation site. * Add language/theme toggle functionality - Created a new Japanese tour index page at docs/ja/tour/index.html - Implemented navigation links for various sections of the cpp-httplib tutorial - Added a language selector to switch between English and Japanese - Introduced theme toggle functionality to switch between light and dark modes - Added mobile sidebar toggle for better navigation on smaller screens --- .gitignore | 1 + docs-gen/Cargo.lock | 1354 +++++++++++++++++ docs-gen/Cargo.toml | 16 + docs-gen/README.md | 148 ++ docs-gen/src/builder.rs | 339 +++++ docs-gen/src/config.rs | 53 + docs-gen/src/main.rs | 23 + docs-gen/src/markdown.rs | 140 ++ docs-src/config.toml | 12 + docs-src/pages/en/cookbook/index.md | 8 + docs-src/pages/en/index.md | 21 + docs-src/pages/en/tour/01-getting-started.md | 88 ++ docs-src/pages/en/tour/02-basic-client.md | 266 ++++ docs-src/pages/en/tour/03-basic-server.md | 280 ++++ .../pages/en/tour/04-static-file-server.md | 134 ++ docs-src/pages/en/tour/05-tls-setup.md | 88 ++ docs-src/pages/en/tour/06-https-client.md | 122 ++ docs-src/pages/en/tour/07-https-server.md | 124 ++ docs-src/pages/en/tour/08-websocket.md | 139 ++ docs-src/pages/en/tour/09-whats-next.md | 228 +++ docs-src/pages/en/tour/index.md | 16 + docs-src/pages/ja/cookbook/index.md | 8 + docs-src/pages/ja/index.md | 21 + docs-src/pages/ja/tour/01-getting-started.md | 88 ++ docs-src/pages/ja/tour/02-basic-client.md | 266 ++++ docs-src/pages/ja/tour/03-basic-server.md | 280 ++++ .../pages/ja/tour/04-static-file-server.md | 134 ++ docs-src/pages/ja/tour/05-tls-setup.md | 88 ++ docs-src/pages/ja/tour/06-https-client.md | 122 ++ docs-src/pages/ja/tour/07-https-server.md | 124 ++ docs-src/pages/ja/tour/08-websocket.md | 139 ++ docs-src/pages/ja/tour/09-whats-next.md | 228 +++ docs-src/pages/ja/tour/index.md | 16 + docs-src/static/css/main.css | 438 ++++++ docs-src/static/js/main.js | 73 + docs-src/templates/base.html | 54 + docs-src/templates/page.html | 30 + docs-src/templates/portal.html | 12 + docs/css/main.css | 438 ++++++ docs/en/cookbook/index.html | 73 + docs/en/index.html | 72 + docs/en/tour/01-getting-started/index.html | 198 +++ docs/en/tour/02-basic-client/index.html | 491 ++++++ docs/en/tour/03-basic-server/index.html | 446 ++++++ docs/en/tour/04-static-file-server/index.html | 269 ++++ docs/en/tour/05-tls-setup/index.html | 193 +++ docs/en/tour/06-https-client/index.html | 251 +++ docs/en/tour/07-https-server/index.html | 240 +++ docs/en/tour/08-websocket/index.html | 292 ++++ docs/en/tour/09-whats-next/index.html | 424 ++++++ docs/en/tour/index.html | 105 ++ docs/index.html | 17 + docs/ja/cookbook/index.html | 73 + docs/ja/index.html | 72 + docs/ja/tour/01-getting-started/index.html | 198 +++ docs/ja/tour/02-basic-client/index.html | 491 ++++++ docs/ja/tour/03-basic-server/index.html | 446 ++++++ docs/ja/tour/04-static-file-server/index.html | 269 ++++ docs/ja/tour/05-tls-setup/index.html | 193 +++ docs/ja/tour/06-https-client/index.html | 251 +++ docs/ja/tour/07-https-server/index.html | 240 +++ docs/ja/tour/08-websocket/index.html | 292 ++++ docs/ja/tour/09-whats-next/index.html | 424 ++++++ docs/ja/tour/index.html | 105 ++ docs/js/main.js | 73 + justfile | 4 + 66 files changed, 12361 insertions(+) create mode 100644 docs-gen/Cargo.lock create mode 100644 docs-gen/Cargo.toml create mode 100644 docs-gen/README.md create mode 100644 docs-gen/src/builder.rs create mode 100644 docs-gen/src/config.rs create mode 100644 docs-gen/src/main.rs create mode 100644 docs-gen/src/markdown.rs create mode 100644 docs-src/config.toml create mode 100644 docs-src/pages/en/cookbook/index.md create mode 100644 docs-src/pages/en/index.md create mode 100644 docs-src/pages/en/tour/01-getting-started.md create mode 100644 docs-src/pages/en/tour/02-basic-client.md create mode 100644 docs-src/pages/en/tour/03-basic-server.md create mode 100644 docs-src/pages/en/tour/04-static-file-server.md create mode 100644 docs-src/pages/en/tour/05-tls-setup.md create mode 100644 docs-src/pages/en/tour/06-https-client.md create mode 100644 docs-src/pages/en/tour/07-https-server.md create mode 100644 docs-src/pages/en/tour/08-websocket.md create mode 100644 docs-src/pages/en/tour/09-whats-next.md create mode 100644 docs-src/pages/en/tour/index.md create mode 100644 docs-src/pages/ja/cookbook/index.md create mode 100644 docs-src/pages/ja/index.md create mode 100644 docs-src/pages/ja/tour/01-getting-started.md create mode 100644 docs-src/pages/ja/tour/02-basic-client.md create mode 100644 docs-src/pages/ja/tour/03-basic-server.md create mode 100644 docs-src/pages/ja/tour/04-static-file-server.md create mode 100644 docs-src/pages/ja/tour/05-tls-setup.md create mode 100644 docs-src/pages/ja/tour/06-https-client.md create mode 100644 docs-src/pages/ja/tour/07-https-server.md create mode 100644 docs-src/pages/ja/tour/08-websocket.md create mode 100644 docs-src/pages/ja/tour/09-whats-next.md create mode 100644 docs-src/pages/ja/tour/index.md create mode 100644 docs-src/static/css/main.css create mode 100644 docs-src/static/js/main.js create mode 100644 docs-src/templates/base.html create mode 100644 docs-src/templates/page.html create mode 100644 docs-src/templates/portal.html create mode 100644 docs/css/main.css create mode 100644 docs/en/cookbook/index.html create mode 100644 docs/en/index.html create mode 100644 docs/en/tour/01-getting-started/index.html create mode 100644 docs/en/tour/02-basic-client/index.html create mode 100644 docs/en/tour/03-basic-server/index.html create mode 100644 docs/en/tour/04-static-file-server/index.html create mode 100644 docs/en/tour/05-tls-setup/index.html create mode 100644 docs/en/tour/06-https-client/index.html create mode 100644 docs/en/tour/07-https-server/index.html create mode 100644 docs/en/tour/08-websocket/index.html create mode 100644 docs/en/tour/09-whats-next/index.html create mode 100644 docs/en/tour/index.html create mode 100644 docs/index.html create mode 100644 docs/ja/cookbook/index.html create mode 100644 docs/ja/index.html create mode 100644 docs/ja/tour/01-getting-started/index.html create mode 100644 docs/ja/tour/02-basic-client/index.html create mode 100644 docs/ja/tour/03-basic-server/index.html create mode 100644 docs/ja/tour/04-static-file-server/index.html create mode 100644 docs/ja/tour/05-tls-setup/index.html create mode 100644 docs/ja/tour/06-https-client/index.html create mode 100644 docs/ja/tour/07-https-server/index.html create mode 100644 docs/ja/tour/08-websocket/index.html create mode 100644 docs/ja/tour/09-whats-next/index.html create mode 100644 docs/ja/tour/index.html create mode 100644 docs/js/main.js diff --git a/.gitignore b/.gitignore index 1ea2142..4093094 100644 --- a/.gitignore +++ b/.gitignore @@ -57,6 +57,7 @@ test/*.log test/_build_* work/ benchmark/server* +docs-gen/target/ *.swp diff --git a/docs-gen/Cargo.lock b/docs-gen/Cargo.lock new file mode 100644 index 0000000..6d12b67 --- /dev/null +++ b/docs-gen/Cargo.lock @@ -0,0 +1,1354 @@ +# This file is automatically @generated by Cargo. +# It is not intended for manual editing. +version = 4 + +[[package]] +name = "adler2" +version = "2.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "320119579fcad9c21884f5c4861d16174d0e06250625266f50fe6898340abefa" + +[[package]] +name = "aho-corasick" +version = "1.1.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ddd31a130427c27518df266943a5308ed92d4b226cc639f5a8f1002816174301" +dependencies = [ + "memchr", +] + +[[package]] +name = "android_system_properties" +version = "0.1.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "819e7219dbd41043ac279b19830f2efc897156490d7fd6ea916720117ee66311" +dependencies = [ + "libc", +] + +[[package]] +name = "anstream" +version = "0.6.21" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "43d5b281e737544384e969a5ccad3f1cdd24b48086a0fc1b2a5262a26b8f4f4a" +dependencies = [ + "anstyle", + "anstyle-parse", + "anstyle-query", + "anstyle-wincon", + "colorchoice", + "is_terminal_polyfill", + "utf8parse", +] + +[[package]] +name = "anstyle" +version = "1.0.13" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5192cca8006f1fd4f7237516f40fa183bb07f8fbdfedaa0036de5ea9b0b45e78" + +[[package]] +name = "anstyle-parse" +version = "0.2.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4e7644824f0aa2c7b9384579234ef10eb7efb6a0deb83f9630a49594dd9c15c2" +dependencies = [ + "utf8parse", +] + +[[package]] +name = "anstyle-query" +version = "1.1.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "40c48f72fd53cd289104fc64099abca73db4166ad86ea0b4341abe65af83dadc" +dependencies = [ + "windows-sys", +] + +[[package]] +name = "anstyle-wincon" +version = "3.0.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "291e6a250ff86cd4a820112fb8898808a366d8f9f58ce16d1f538353ad55747d" +dependencies = [ + "anstyle", + "once_cell_polyfill", + "windows-sys", +] + +[[package]] +name = "anyhow" +version = "1.0.102" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7f202df86484c868dbad7eaa557ef785d5c66295e41b460ef922eca0723b842c" + +[[package]] +name = "autocfg" +version = "1.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c08606f8c3cbf4ce6ec8e28fb0014a2c086708fe954eaa885384a6165172e7e8" + +[[package]] +name = "base64" +version = "0.22.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "72b3254f16251a8381aa12e40e3c4d2f0199f8c6508fbecb9d91f575e0fbb8c6" + +[[package]] +name = "bincode" +version = "1.3.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b1f45e9417d87227c7a56d22e471c6206462cba514c7590c09aff4cf6d1ddcad" +dependencies = [ + "serde", +] + +[[package]] +name = "bitflags" +version = "2.11.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "843867be96c8daad0d758b57df9392b6d8d271134fce549de6ce169ff98a92af" + +[[package]] +name = "block-buffer" +version = "0.10.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3078c7629b62d3f0439517fa394996acacc5cbc91c5a20d8c658e77abd503a71" +dependencies = [ + "generic-array", +] + +[[package]] +name = "bstr" +version = "1.12.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "63044e1ae8e69f3b5a92c736ca6269b8d12fa7efe39bf34ddb06d102cf0e2cab" +dependencies = [ + "memchr", + "serde", +] + +[[package]] +name = "bumpalo" +version = "3.20.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5d20789868f4b01b2f2caec9f5c4e0213b41e3e5702a50157d699ae31ced2fcb" + +[[package]] +name = "cc" +version = "1.2.56" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "aebf35691d1bfb0ac386a69bac2fde4dd276fb618cf8bf4f5318fe285e821bb2" +dependencies = [ + "find-msvc-tools", + "shlex", +] + +[[package]] +name = "cfg-if" +version = "1.0.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9330f8b2ff13f34540b44e946ef35111825727b38d33286ef986142615121801" + +[[package]] +name = "chrono" +version = "0.4.44" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c673075a2e0e5f4a1dde27ce9dee1ea4558c7ffe648f576438a20ca1d2acc4b0" +dependencies = [ + "iana-time-zone", + "num-traits", + "windows-link", +] + +[[package]] +name = "chrono-tz" +version = "0.9.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "93698b29de5e97ad0ae26447b344c482a7284c737d9ddc5f9e52b74a336671bb" +dependencies = [ + "chrono", + "chrono-tz-build", + "phf", +] + +[[package]] +name = "chrono-tz-build" +version = "0.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0c088aee841df9c3041febbb73934cfc39708749bf96dc827e3359cd39ef11b1" +dependencies = [ + "parse-zoneinfo", + "phf", + "phf_codegen", +] + +[[package]] +name = "clap" +version = "4.5.60" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2797f34da339ce31042b27d23607e051786132987f595b02ba4f6a6dffb7030a" +dependencies = [ + "clap_builder", + "clap_derive", +] + +[[package]] +name = "clap_builder" +version = "4.5.60" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "24a241312cea5059b13574bb9b3861cabf758b879c15190b37b6d6fd63ab6876" +dependencies = [ + "anstream", + "anstyle", + "clap_lex", + "strsim", +] + +[[package]] +name = "clap_derive" +version = "4.5.55" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a92793da1a46a5f2a02a6f4c46c6496b28c43638adea8306fcb0caa1634f24e5" +dependencies = [ + "heck", + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "clap_lex" +version = "1.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3a822ea5bc7590f9d40f1ba12c0dc3c2760f3482c6984db1573ad11031420831" + +[[package]] +name = "colorchoice" +version = "1.0.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b05b61dc5112cbb17e4b6cd61790d9845d13888356391624cbe7e41efeac1e75" + +[[package]] +name = "core-foundation-sys" +version = "0.8.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "773648b94d0e5d620f64f280777445740e61fe701025087ec8b57f45c791888b" + +[[package]] +name = "cpufeatures" +version = "0.2.17" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "59ed5838eebb26a2bb2e58f6d5b5316989ae9d08bab10e0e6d103e656d1b0280" +dependencies = [ + "libc", +] + +[[package]] +name = "crc32fast" +version = "1.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9481c1c90cbf2ac953f07c8d4a58aa3945c425b7185c9154d67a65e4230da511" +dependencies = [ + "cfg-if", +] + +[[package]] +name = "crossbeam-deque" +version = "0.8.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9dd111b7b7f7d55b72c0a6ae361660ee5853c9af73f70c3c2ef6858b950e2e51" +dependencies = [ + "crossbeam-epoch", + "crossbeam-utils", +] + +[[package]] +name = "crossbeam-epoch" +version = "0.9.18" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5b82ac4a3c2ca9c3460964f020e1402edd5753411d7737aa39c3714ad1b5420e" +dependencies = [ + "crossbeam-utils", +] + +[[package]] +name = "crossbeam-utils" +version = "0.8.21" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d0a5c400df2834b80a4c3327b3aad3a4c4cd4de0629063962b03235697506a28" + +[[package]] +name = "crypto-common" +version = "0.1.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "78c8292055d1c1df0cce5d180393dc8cce0abec0a7102adb6c7b1eef6016d60a" +dependencies = [ + "generic-array", + "typenum", +] + +[[package]] +name = "deranged" +version = "0.5.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7cd812cc2bc1d69d4764bd80df88b4317eaef9e773c75226407d9bc0876b211c" +dependencies = [ + "powerfmt", +] + +[[package]] +name = "deunicode" +version = "1.6.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "abd57806937c9cc163efc8ea3910e00a62e2aeb0b8119f1793a978088f8f6b04" + +[[package]] +name = "digest" +version = "0.10.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9ed9a281f7bc9b7576e61468ba615a66a5c8cfdff42420a70aa82701a3b1e292" +dependencies = [ + "block-buffer", + "crypto-common", +] + +[[package]] +name = "docs-gen" +version = "0.1.0" +dependencies = [ + "anyhow", + "clap", + "pulldown-cmark", + "serde", + "serde_json", + "serde_yml", + "syntect", + "tera", + "toml", + "walkdir", +] + +[[package]] +name = "equivalent" +version = "1.0.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "877a4ace8713b0bcf2a4e7eec82529c029f1d0619886d18145fea96c3ffe5c0f" + +[[package]] +name = "find-msvc-tools" +version = "0.1.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5baebc0774151f905a1a2cc41989300b1e6fbb29aff0ceffa1064fdd3088d582" + +[[package]] +name = "flate2" +version = "1.1.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "843fba2746e448b37e26a819579957415c8cef339bf08564fe8b7ddbd959573c" +dependencies = [ + "crc32fast", + "miniz_oxide", +] + +[[package]] +name = "fnv" +version = "1.0.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3f9eec918d3f24069decb9af1554cad7c880e2da24a9afd88aca000531ab82c1" + +[[package]] +name = "generic-array" +version = "0.14.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "85649ca51fd72272d7821adaf274ad91c288277713d9c18820d8499a7ff69e9a" +dependencies = [ + "typenum", + "version_check", +] + +[[package]] +name = "getopts" +version = "0.2.24" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cfe4fbac503b8d1f88e6676011885f34b7174f46e59956bba534ba83abded4df" +dependencies = [ + "unicode-width", +] + +[[package]] +name = "getrandom" +version = "0.2.17" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ff2abc00be7fca6ebc474524697ae276ad847ad0a6b3faa4bcb027e9a4614ad0" +dependencies = [ + "cfg-if", + "libc", + "wasi", +] + +[[package]] +name = "globset" +version = "0.4.18" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "52dfc19153a48bde0cbd630453615c8151bce3a5adfac7a0aebfbf0a1e1f57e3" +dependencies = [ + "aho-corasick", + "bstr", + "log", + "regex-automata", + "regex-syntax", +] + +[[package]] +name = "globwalk" +version = "0.9.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0bf760ebf69878d9fd8f110c89703d90ce35095324d1f1edcb595c63945ee757" +dependencies = [ + "bitflags", + "ignore", + "walkdir", +] + +[[package]] +name = "hashbrown" +version = "0.16.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "841d1cc9bed7f9236f321df977030373f4a4163ae1a7dbfe1a51a2c1a51d9100" + +[[package]] +name = "heck" +version = "0.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2304e00983f87ffb38b55b444b5e3b60a884b5d30c0fca7d82fe33449bbe55ea" + +[[package]] +name = "humansize" +version = "2.1.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6cb51c9a029ddc91b07a787f1d86b53ccfa49b0e86688c946ebe8d3555685dd7" +dependencies = [ + "libm", +] + +[[package]] +name = "iana-time-zone" +version = "0.1.65" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e31bc9ad994ba00e440a8aa5c9ef0ec67d5cb5e5cb0cc7f8b744a35b389cc470" +dependencies = [ + "android_system_properties", + "core-foundation-sys", + "iana-time-zone-haiku", + "js-sys", + "log", + "wasm-bindgen", + "windows-core", +] + +[[package]] +name = "iana-time-zone-haiku" +version = "0.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f31827a206f56af32e590ba56d5d2d085f558508192593743f16b2306495269f" +dependencies = [ + "cc", +] + +[[package]] +name = "ignore" +version = "0.4.25" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d3d782a365a015e0f5c04902246139249abf769125006fbe7649e2ee88169b4a" +dependencies = [ + "crossbeam-deque", + "globset", + "log", + "memchr", + "regex-automata", + "same-file", + "walkdir", + "winapi-util", +] + +[[package]] +name = "indexmap" +version = "2.13.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7714e70437a7dc3ac8eb7e6f8df75fd8eb422675fc7678aff7364301092b1017" +dependencies = [ + "equivalent", + "hashbrown", +] + +[[package]] +name = "is_terminal_polyfill" +version = "1.70.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a6cb138bb79a146c1bd460005623e142ef0181e3d0219cb493e02f7d08a35695" + +[[package]] +name = "itoa" +version = "1.0.17" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "92ecc6618181def0457392ccd0ee51198e065e016d1d527a7ac1b6dc7c1f09d2" + +[[package]] +name = "js-sys" +version = "0.3.91" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b49715b7073f385ba4bc528e5747d02e66cb39c6146efb66b781f131f0fb399c" +dependencies = [ + "once_cell", + "wasm-bindgen", +] + +[[package]] +name = "lazy_static" +version = "1.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bbd2bcb4c963f2ddae06a2efc7e9f3591312473c50c6685e1f298068316e66fe" + +[[package]] +name = "libc" +version = "0.2.182" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6800badb6cb2082ffd7b6a67e6125bb39f18782f793520caee8cb8846be06112" + +[[package]] +name = "libm" +version = "0.2.16" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b6d2cec3eae94f9f509c767b45932f1ada8350c4bdb85af2fcab4a3c14807981" + +[[package]] +name = "libyml" +version = "0.0.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3302702afa434ffa30847a83305f0a69d6abd74293b6554c18ec85c7ef30c980" +dependencies = [ + "anyhow", + "version_check", +] + +[[package]] +name = "linked-hash-map" +version = "0.5.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0717cef1bc8b636c6e1c1bbdefc09e6322da8a9321966e8928ef80d20f7f770f" + +[[package]] +name = "log" +version = "0.4.29" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5e5032e24019045c762d3c0f28f5b6b8bbf38563a65908389bf7978758920897" + +[[package]] +name = "memchr" +version = "2.8.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f8ca58f447f06ed17d5fc4043ce1b10dd205e060fb3ce5b979b8ed8e59ff3f79" + +[[package]] +name = "miniz_oxide" +version = "0.8.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1fa76a2c86f704bdb222d66965fb3d63269ce38518b83cb0575fca855ebb6316" +dependencies = [ + "adler2", + "simd-adler32", +] + +[[package]] +name = "num-conv" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cf97ec579c3c42f953ef76dbf8d55ac91fb219dde70e49aa4a6b7d74e9919050" + +[[package]] +name = "num-traits" +version = "0.2.19" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "071dfc062690e90b734c0b2273ce72ad0ffa95f0c74596bc250dcfd960262841" +dependencies = [ + "autocfg", +] + +[[package]] +name = "once_cell" +version = "1.21.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "42f5e15c9953c5e4ccceeb2e7382a716482c34515315f7b03532b8b4e8393d2d" + +[[package]] +name = "once_cell_polyfill" +version = "1.70.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "384b8ab6d37215f3c5301a95a4accb5d64aa607f1fcb26a11b5303878451b4fe" + +[[package]] +name = "onig" +version = "6.5.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "336b9c63443aceef14bea841b899035ae3abe89b7c486aaf4c5bd8aafedac3f0" +dependencies = [ + "bitflags", + "libc", + "once_cell", + "onig_sys", +] + +[[package]] +name = "onig_sys" +version = "69.9.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c7f86c6eef3d6df15f23bcfb6af487cbd2fed4e5581d58d5bf1f5f8b7f6727dc" +dependencies = [ + "cc", + "pkg-config", +] + +[[package]] +name = "parse-zoneinfo" +version = "0.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1f2a05b18d44e2957b88f96ba460715e295bc1d7510468a2f3d3b44535d26c24" +dependencies = [ + "regex", +] + +[[package]] +name = "percent-encoding" +version = "2.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9b4f627cb1b25917193a259e49bdad08f671f8d9708acfd5fe0a8c1455d87220" + +[[package]] +name = "pest" +version = "2.8.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e0848c601009d37dfa3430c4666e147e49cdcf1b92ecd3e63657d8a5f19da662" +dependencies = [ + "memchr", + "ucd-trie", +] + +[[package]] +name = "pest_derive" +version = "2.8.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "11f486f1ea21e6c10ed15d5a7c77165d0ee443402f0780849d1768e7d9d6fe77" +dependencies = [ + "pest", + "pest_generator", +] + +[[package]] +name = "pest_generator" +version = "2.8.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8040c4647b13b210a963c1ed407c1ff4fdfa01c31d6d2a098218702e6664f94f" +dependencies = [ + "pest", + "pest_meta", + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "pest_meta" +version = "2.8.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "89815c69d36021a140146f26659a81d6c2afa33d216d736dd4be5381a7362220" +dependencies = [ + "pest", + "sha2", +] + +[[package]] +name = "phf" +version = "0.11.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1fd6780a80ae0c52cc120a26a1a42c1ae51b247a253e4e06113d23d2c2edd078" +dependencies = [ + "phf_shared", +] + +[[package]] +name = "phf_codegen" +version = "0.11.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "aef8048c789fa5e851558d709946d6d79a8ff88c0440c587967f8e94bfb1216a" +dependencies = [ + "phf_generator", + "phf_shared", +] + +[[package]] +name = "phf_generator" +version = "0.11.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3c80231409c20246a13fddb31776fb942c38553c51e871f8cbd687a4cfb5843d" +dependencies = [ + "phf_shared", + "rand", +] + +[[package]] +name = "phf_shared" +version = "0.11.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "67eabc2ef2a60eb7faa00097bd1ffdb5bd28e62bf39990626a582201b7a754e5" +dependencies = [ + "siphasher", +] + +[[package]] +name = "pkg-config" +version = "0.3.32" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7edddbd0b52d732b21ad9a5fab5c704c14cd949e5e9a1ec5929a24fded1b904c" + +[[package]] +name = "plist" +version = "1.8.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "740ebea15c5d1428f910cd1a5f52cebf8d25006245ed8ade92702f4943d91e07" +dependencies = [ + "base64", + "indexmap", + "quick-xml", + "serde", + "time", +] + +[[package]] +name = "powerfmt" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "439ee305def115ba05938db6eb1644ff94165c5ab5e9420d1c1bcedbba909391" + +[[package]] +name = "ppv-lite86" +version = "0.2.21" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "85eae3c4ed2f50dcfe72643da4befc30deadb458a9b590d720cde2f2b1e97da9" +dependencies = [ + "zerocopy", +] + +[[package]] +name = "proc-macro2" +version = "1.0.106" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8fd00f0bb2e90d81d1044c2b32617f68fcb9fa3bb7640c23e9c748e53fb30934" +dependencies = [ + "unicode-ident", +] + +[[package]] +name = "pulldown-cmark" +version = "0.12.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f86ba2052aebccc42cbbb3ed234b8b13ce76f75c3551a303cb2bcffcff12bb14" +dependencies = [ + "bitflags", + "getopts", + "memchr", + "pulldown-cmark-escape", + "unicase", +] + +[[package]] +name = "pulldown-cmark-escape" +version = "0.11.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "007d8adb5ddab6f8e3f491ac63566a7d5002cc7ed73901f72057943fa71ae1ae" + +[[package]] +name = "quick-xml" +version = "0.38.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b66c2058c55a409d601666cffe35f04333cf1013010882cec174a7467cd4e21c" +dependencies = [ + "memchr", +] + +[[package]] +name = "quote" +version = "1.0.44" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "21b2ebcf727b7760c461f091f9f0f539b77b8e87f2fd88131e7f1b433b3cece4" +dependencies = [ + "proc-macro2", +] + +[[package]] +name = "rand" +version = "0.8.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "34af8d1a0e25924bc5b7c43c079c942339d8f0a8b57c39049bef581b46327404" +dependencies = [ + "libc", + "rand_chacha", + "rand_core", +] + +[[package]] +name = "rand_chacha" +version = "0.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e6c10a63a0fa32252be49d21e7709d4d4baf8d231c2dbce1eaa8141b9b127d88" +dependencies = [ + "ppv-lite86", + "rand_core", +] + +[[package]] +name = "rand_core" +version = "0.6.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ec0be4795e2f6a28069bec0b5ff3e2ac9bafc99e6a9a7dc3547996c5c816922c" +dependencies = [ + "getrandom", +] + +[[package]] +name = "regex" +version = "1.12.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e10754a14b9137dd7b1e3e5b0493cc9171fdd105e0ab477f51b72e7f3ac0e276" +dependencies = [ + "aho-corasick", + "memchr", + "regex-automata", + "regex-syntax", +] + +[[package]] +name = "regex-automata" +version = "0.4.14" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6e1dd4122fc1595e8162618945476892eefca7b88c52820e74af6262213cae8f" +dependencies = [ + "aho-corasick", + "memchr", + "regex-syntax", +] + +[[package]] +name = "regex-syntax" +version = "0.8.10" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dc897dd8d9e8bd1ed8cdad82b5966c3e0ecae09fb1907d58efaa013543185d0a" + +[[package]] +name = "rustversion" +version = "1.0.22" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b39cdef0fa800fc44525c84ccb54a029961a8215f9619753635a9c0d2538d46d" + +[[package]] +name = "ryu" +version = "1.0.23" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9774ba4a74de5f7b1c1451ed6cd5285a32eddb5cccb8cc655a4e50009e06477f" + +[[package]] +name = "same-file" +version = "1.0.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "93fc1dc3aaa9bfed95e02e6eadabb4baf7e3078b0bd1b4d7b6b0b68378900502" +dependencies = [ + "winapi-util", +] + +[[package]] +name = "serde" +version = "1.0.228" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9a8e94ea7f378bd32cbbd37198a4a91436180c5bb472411e48b5ec2e2124ae9e" +dependencies = [ + "serde_core", + "serde_derive", +] + +[[package]] +name = "serde_core" +version = "1.0.228" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "41d385c7d4ca58e59fc732af25c3983b67ac852c1a25000afe1175de458b67ad" +dependencies = [ + "serde_derive", +] + +[[package]] +name = "serde_derive" +version = "1.0.228" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d540f220d3187173da220f885ab66608367b6574e925011a9353e4badda91d79" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "serde_json" +version = "1.0.149" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "83fc039473c5595ace860d8c4fafa220ff474b3fc6bfdb4293327f1a37e94d86" +dependencies = [ + "itoa", + "memchr", + "serde", + "serde_core", + "zmij", +] + +[[package]] +name = "serde_spanned" +version = "0.6.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bf41e0cfaf7226dca15e8197172c295a782857fcb97fad1808a166870dee75a3" +dependencies = [ + "serde", +] + +[[package]] +name = "serde_yml" +version = "0.0.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "59e2dd588bf1597a252c3b920e0143eb99b0f76e4e082f4c92ce34fbc9e71ddd" +dependencies = [ + "indexmap", + "itoa", + "libyml", + "memchr", + "ryu", + "serde", + "version_check", +] + +[[package]] +name = "sha2" +version = "0.10.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a7507d819769d01a365ab707794a4084392c824f54a7a6a7862f8c3d0892b283" +dependencies = [ + "cfg-if", + "cpufeatures", + "digest", +] + +[[package]] +name = "shlex" +version = "1.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0fda2ff0d084019ba4d7c6f371c95d8fd75ce3524c3cb8fb653a3023f6323e64" + +[[package]] +name = "simd-adler32" +version = "0.3.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e320a6c5ad31d271ad523dcf3ad13e2767ad8b1cb8f047f75a8aeaf8da139da2" + +[[package]] +name = "siphasher" +version = "1.0.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b2aa850e253778c88a04c3d7323b043aeda9d3e30d5971937c1855769763678e" + +[[package]] +name = "slug" +version = "0.1.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "882a80f72ee45de3cc9a5afeb2da0331d58df69e4e7d8eeb5d3c7784ae67e724" +dependencies = [ + "deunicode", + "wasm-bindgen", +] + +[[package]] +name = "strsim" +version = "0.11.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7da8b5736845d9f2fcb837ea5d9e2628564b3b043a70948a3f0b778838c5fb4f" + +[[package]] +name = "syn" +version = "2.0.117" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e665b8803e7b1d2a727f4023456bbbbe74da67099c585258af0ad9c5013b9b99" +dependencies = [ + "proc-macro2", + "quote", + "unicode-ident", +] + +[[package]] +name = "syntect" +version = "5.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "656b45c05d95a5704399aeef6bd0ddec7b2b3531b7c9e900abbf7c4d2190c925" +dependencies = [ + "bincode", + "flate2", + "fnv", + "once_cell", + "onig", + "plist", + "regex-syntax", + "serde", + "serde_derive", + "serde_json", + "thiserror", + "walkdir", + "yaml-rust", +] + +[[package]] +name = "tera" +version = "1.20.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e8004bca281f2d32df3bacd59bc67b312cb4c70cea46cbd79dbe8ac5ed206722" +dependencies = [ + "chrono", + "chrono-tz", + "globwalk", + "humansize", + "lazy_static", + "percent-encoding", + "pest", + "pest_derive", + "rand", + "regex", + "serde", + "serde_json", + "slug", + "unicode-segmentation", +] + +[[package]] +name = "thiserror" +version = "2.0.18" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4288b5bcbc7920c07a1149a35cf9590a2aa808e0bc1eafaade0b80947865fbc4" +dependencies = [ + "thiserror-impl", +] + +[[package]] +name = "thiserror-impl" +version = "2.0.18" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ebc4ee7f67670e9b64d05fa4253e753e016c6c95ff35b89b7941d6b856dec1d5" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "time" +version = "0.3.47" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "743bd48c283afc0388f9b8827b976905fb217ad9e647fae3a379a9283c4def2c" +dependencies = [ + "deranged", + "itoa", + "num-conv", + "powerfmt", + "serde_core", + "time-core", + "time-macros", +] + +[[package]] +name = "time-core" +version = "0.1.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7694e1cfe791f8d31026952abf09c69ca6f6fa4e1a1229e18988f06a04a12dca" + +[[package]] +name = "time-macros" +version = "0.2.27" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2e70e4c5a0e0a8a4823ad65dfe1a6930e4f4d756dcd9dd7939022b5e8c501215" +dependencies = [ + "num-conv", + "time-core", +] + +[[package]] +name = "toml" +version = "0.8.23" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dc1beb996b9d83529a9e75c17a1686767d148d70663143c7854d8b4a09ced362" +dependencies = [ + "serde", + "serde_spanned", + "toml_datetime", + "toml_edit", +] + +[[package]] +name = "toml_datetime" +version = "0.6.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "22cddaf88f4fbc13c51aebbf5f8eceb5c7c5a9da2ac40a13519eb5b0a0e8f11c" +dependencies = [ + "serde", +] + +[[package]] +name = "toml_edit" +version = "0.22.27" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "41fe8c660ae4257887cf66394862d21dbca4a6ddd26f04a3560410406a2f819a" +dependencies = [ + "indexmap", + "serde", + "serde_spanned", + "toml_datetime", + "toml_write", + "winnow", +] + +[[package]] +name = "toml_write" +version = "0.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5d99f8c9a7727884afe522e9bd5edbfc91a3312b36a77b5fb8926e4c31a41801" + +[[package]] +name = "typenum" +version = "1.19.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "562d481066bde0658276a35467c4af00bdc6ee726305698a55b86e61d7ad82bb" + +[[package]] +name = "ucd-trie" +version = "0.1.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2896d95c02a80c6d6a5d6e953d479f5ddf2dfdb6a244441010e373ac0fb88971" + +[[package]] +name = "unicase" +version = "2.9.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dbc4bc3a9f746d862c45cb89d705aa10f187bb96c76001afab07a0d35ce60142" + +[[package]] +name = "unicode-ident" +version = "1.0.24" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e6e4313cd5fcd3dad5cafa179702e2b244f760991f45397d14d4ebf38247da75" + +[[package]] +name = "unicode-segmentation" +version = "1.12.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f6ccf251212114b54433ec949fd6a7841275f9ada20dddd2f29e9ceea4501493" + +[[package]] +name = "unicode-width" +version = "0.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b4ac048d71ede7ee76d585517add45da530660ef4390e49b098733c6e897f254" + +[[package]] +name = "utf8parse" +version = "0.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "06abde3611657adf66d383f00b093d7faecc7fa57071cce2578660c9f1010821" + +[[package]] +name = "version_check" +version = "0.9.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0b928f33d975fc6ad9f86c8f283853ad26bdd5b10b7f1542aa2fa15e2289105a" + +[[package]] +name = "walkdir" +version = "2.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "29790946404f91d9c5d06f9874efddea1dc06c5efe94541a7d6863108e3a5e4b" +dependencies = [ + "same-file", + "winapi-util", +] + +[[package]] +name = "wasi" +version = "0.11.1+wasi-snapshot-preview1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ccf3ec651a847eb01de73ccad15eb7d99f80485de043efb2f370cd654f4ea44b" + +[[package]] +name = "wasm-bindgen" +version = "0.2.114" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6532f9a5c1ece3798cb1c2cfdba640b9b3ba884f5db45973a6f442510a87d38e" +dependencies = [ + "cfg-if", + "once_cell", + "rustversion", + "wasm-bindgen-macro", + "wasm-bindgen-shared", +] + +[[package]] +name = "wasm-bindgen-macro" +version = "0.2.114" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "18a2d50fcf105fb33bb15f00e7a77b772945a2ee45dcf454961fd843e74c18e6" +dependencies = [ + "quote", + "wasm-bindgen-macro-support", +] + +[[package]] +name = "wasm-bindgen-macro-support" +version = "0.2.114" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "03ce4caeaac547cdf713d280eda22a730824dd11e6b8c3ca9e42247b25c631e3" +dependencies = [ + "bumpalo", + "proc-macro2", + "quote", + "syn", + "wasm-bindgen-shared", +] + +[[package]] +name = "wasm-bindgen-shared" +version = "0.2.114" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "75a326b8c223ee17883a4251907455a2431acc2791c98c26279376490c378c16" +dependencies = [ + "unicode-ident", +] + +[[package]] +name = "winapi-util" +version = "0.1.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c2a7b1c03c876122aa43f3020e6c3c3ee5c05081c9a00739faf7503aeba10d22" +dependencies = [ + "windows-sys", +] + +[[package]] +name = "windows-core" +version = "0.62.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b8e83a14d34d0623b51dce9581199302a221863196a1dde71a7663a4c2be9deb" +dependencies = [ + "windows-implement", + "windows-interface", + "windows-link", + "windows-result", + "windows-strings", +] + +[[package]] +name = "windows-implement" +version = "0.60.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "053e2e040ab57b9dc951b72c264860db7eb3b0200ba345b4e4c3b14f67855ddf" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "windows-interface" +version = "0.59.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3f316c4a2570ba26bbec722032c4099d8c8bc095efccdc15688708623367e358" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "windows-link" +version = "0.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f0805222e57f7521d6a62e36fa9163bc891acd422f971defe97d64e70d0a4fe5" + +[[package]] +name = "windows-result" +version = "0.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7781fa89eaf60850ac3d2da7af8e5242a5ea78d1a11c49bf2910bb5a73853eb5" +dependencies = [ + "windows-link", +] + +[[package]] +name = "windows-strings" +version = "0.5.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7837d08f69c77cf6b07689544538e017c1bfcf57e34b4c0ff58e6c2cd3b37091" +dependencies = [ + "windows-link", +] + +[[package]] +name = "windows-sys" +version = "0.61.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ae137229bcbd6cdf0f7b80a31df61766145077ddf49416a728b02cb3921ff3fc" +dependencies = [ + "windows-link", +] + +[[package]] +name = "winnow" +version = "0.7.14" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5a5364e9d77fcdeeaa6062ced926ee3381faa2ee02d3eb83a5c27a8825540829" +dependencies = [ + "memchr", +] + +[[package]] +name = "yaml-rust" +version = "0.4.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "56c1936c4cc7a1c9ab21a1ebb602eb942ba868cbd44a99cb7cdc5892335e1c85" +dependencies = [ + "linked-hash-map", +] + +[[package]] +name = "zerocopy" +version = "0.8.40" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a789c6e490b576db9f7e6b6d661bcc9799f7c0ac8352f56ea20193b2681532e5" +dependencies = [ + "zerocopy-derive", +] + +[[package]] +name = "zerocopy-derive" +version = "0.8.40" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f65c489a7071a749c849713807783f70672b28094011623e200cb86dcb835953" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "zmij" +version = "1.0.21" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b8848ee67ecc8aedbaf3e4122217aff892639231befc6a1b58d29fff4c2cabaa" diff --git a/docs-gen/Cargo.toml b/docs-gen/Cargo.toml new file mode 100644 index 0000000..2e9cd03 --- /dev/null +++ b/docs-gen/Cargo.toml @@ -0,0 +1,16 @@ +[package] +name = "docs-gen" +version = "0.1.0" +edition = "2021" + +[dependencies] +pulldown-cmark = "0.12" +tera = "1" +walkdir = "2" +serde = { version = "1", features = ["derive"] } +serde_json = "1" +serde_yml = "0.0.12" +toml = "0.8" +syntect = "5" +anyhow = "1" +clap = { version = "4", features = ["derive"] } diff --git a/docs-gen/README.md b/docs-gen/README.md new file mode 100644 index 0000000..ddec2b0 --- /dev/null +++ b/docs-gen/README.md @@ -0,0 +1,148 @@ +# docs-gen + +A simple static site generator written in Rust. Designed for multi-language documentation sites with Markdown content, Tera templates, and syntax highlighting. + +## Build + +``` +cargo build --release --manifest-path docs-gen/Cargo.toml +``` + +## Usage + +``` +docs-gen [SRC] [--out OUT] +``` + +- `SRC` — Source directory containing `config.toml` (default: `.`) +- `--out OUT` — Output directory (default: `docs`) + +Example: + +``` +./docs-gen/target/release/docs-gen docs-src --out docs +``` + +## Source Directory Structure + +``` +docs-src/ +├── config.toml # Site configuration +├── pages/ # Markdown content (one subdirectory per language) +│ ├── en/ +│ │ ├── index.md # Portal page (no sidebar) +│ │ ├── tour/ +│ │ │ ├── index.md # Section index +│ │ │ ├── 01-getting-started.md +│ │ │ └── ... +│ │ └── cookbook/ +│ │ └── index.md +│ └── ja/ +│ └── ... # Same structure as en/ +├── templates/ # Tera HTML templates +│ ├── base.html # Base layout (header, scripts) +│ ├── page.html # Content page with sidebar navigation +│ └── portal.html # Portal page without sidebar +└── static/ # Static assets (copied as-is to output root) + ├── css/ + └── js/ +``` + +## config.toml + +```toml +[site] +title = "My Project" +base_url = "https://example.github.io/my-project" + +[i18n] +default_lang = "en" +langs = ["en", "ja"] + +[highlight] +theme = "base16-eighties.dark" # Dark mode syntax theme (syntect built-in) +theme_light = "base16-ocean.light" # Light mode syntax theme (optional) +``` + +When `theme_light` is set, code blocks are rendered twice (dark and light) and toggled via CSS classes `.code-dark` / `.code-light`. + +Available themes: `base16-ocean.dark`, `base16-ocean.light`, `base16-eighties.dark`, `base16-mocha.dark`, `InspiredGitHub`, `Solarized (dark)`, `Solarized (light)`. + +## Markdown Frontmatter + +Every `.md` file requires YAML frontmatter: + +```yaml +--- +title: "Page Title" +order: 1 +--- +``` + +| Field | Required | Description | +|----------|----------|-------------| +| `title` | yes | Page title shown in heading and browser tab | +| `order` | no | Sort order within the section (default: `0`) | +| `status` | no | Set to `"draft"` to show a DRAFT banner | + +## URL Routing + +Markdown files are mapped to URLs as follows: + +| File path | URL | Output file | +|-----------------------|-----------------|--------------------------| +| `en/index.md` | `/en/` | `en/index.html` | +| `en/tour/index.md` | `/en/tour/` | `en/tour/index.html` | +| `en/tour/01-foo.md` | `/en/tour/01-foo/` | `en/tour/01-foo/index.html` | + +A root `index.html` is generated automatically, redirecting `/` to `//` (respecting `localStorage` preference). + +## Navigation + +Navigation is generated automatically from the directory structure: + +- Each subdirectory under a language becomes a **section** +- The section's `index.md` title is used as the section heading +- Pages within a section are sorted by `order`, then by filename +- `portal.html` template is used for root `index.md` (no sidebar) +- `page.html` template is used for all other pages (with sidebar) + +## Template Variables + +Templates use [Tera](https://keats.github.io/tera/) syntax. Available variables: + +### All templates + +| Variable | Type | Description | +|---------------|--------|-------------| +| `page.title` | string | Page title from frontmatter | +| `page.url` | string | Page URL path | +| `page.status` | string? | `"draft"` or null | +| `content` | string | Rendered HTML content (use `{{ content \| safe }}`) | +| `lang` | string | Current language code | +| `site.title` | string | Site title from config | +| `site.base_url` | string | Base URL from config | +| `site.langs` | list | Available language codes | + +### page.html only + +| Variable | Type | Description | +|--------------------|--------|-------------| +| `nav` | list | Navigation sections | +| `nav[].title` | string | Section title | +| `nav[].url` | string | Section URL | +| `nav[].active` | bool | Whether this section contains the current page | +| `nav[].children` | list | Child pages | +| `nav[].children[].title` | string | Page title | +| `nav[].children[].url` | string | Page URL | +| `nav[].children[].active` | bool | Whether this is the current page | + +## Dependencies + +- [pulldown-cmark](https://crates.io/crates/pulldown-cmark) — Markdown parsing +- [tera](https://crates.io/crates/tera) — Template engine +- [syntect](https://crates.io/crates/syntect) — Syntax highlighting +- [walkdir](https://crates.io/crates/walkdir) — Directory traversal +- [serde](https://crates.io/crates/serde) / [serde_yml](https://crates.io/crates/serde_yml) / [toml](https://crates.io/crates/toml) — Serialization +- [clap](https://crates.io/crates/clap) — CLI argument parsing +- [anyhow](https://crates.io/crates/anyhow) — Error handling diff --git a/docs-gen/src/builder.rs b/docs-gen/src/builder.rs new file mode 100644 index 0000000..74c24f7 --- /dev/null +++ b/docs-gen/src/builder.rs @@ -0,0 +1,339 @@ +use crate::config::SiteConfig; +use crate::markdown::{Frontmatter, MarkdownRenderer}; +use anyhow::{Context, Result}; +use serde::Serialize; +use std::fs; +use std::path::{Path, PathBuf}; +use tera::Tera; +use walkdir::WalkDir; + +#[derive(Debug, Serialize)] +struct PageContext { + title: String, + url: String, + status: Option, +} + +#[derive(Debug, Serialize, Clone)] +struct NavItem { + title: String, + url: String, + children: Vec, + active: bool, +} + +#[derive(Debug, Serialize)] +struct SiteContext { + title: String, + version: Option, + base_url: String, + langs: Vec, +} + +struct Page { + frontmatter: Frontmatter, + html_content: String, + url: String, + out_path: PathBuf, + rel_path: String, + section: String, +} + +pub fn build(src: &Path, out: &Path) -> Result<()> { + let config = SiteConfig::load(src)?; + let renderer = MarkdownRenderer::new(config.highlight_theme(), config.highlight_theme_light()); + + let templates_dir = src.join("templates"); + let template_glob = format!("{}/**/*.html", templates_dir.display()); + let tera = Tera::new(&template_glob).context("Failed to load templates")?; + + // Clean output directory + if out.exists() { + fs::remove_dir_all(out).context("Failed to clean output directory")?; + } + fs::create_dir_all(out)?; + + // Copy static files + let static_dir = src.join("static"); + if static_dir.exists() { + copy_dir_recursive(&static_dir, out)?; + } + + // Build each language + for lang in &config.i18n.langs { + let pages_dir = src.join("pages").join(lang); + if !pages_dir.exists() { + eprintln!("Warning: pages directory not found for lang '{}', skipping", lang); + continue; + } + + let pages = collect_pages(&pages_dir, lang, out, &renderer)?; + let nav = build_nav(&pages); + + for page in &pages { + let template_name = if page.section.is_empty() { + "portal.html" + } else { + "page.html" + }; + + // Filter nav to only the current section + let section_nav: Vec<&NavItem> = nav + .iter() + .filter(|item| { + let item_section = extract_section(&item.url); + item_section == page.section + }) + .collect(); + + let mut ctx = tera::Context::new(); + ctx.insert("page", &PageContext { + title: page.frontmatter.title.clone(), + url: page.url.clone(), + status: page.frontmatter.status.clone(), + }); + ctx.insert("content", &page.html_content); + ctx.insert("lang", lang); + ctx.insert("site", &SiteContext { + title: config.site.title.clone(), + version: config.site.version.clone(), + base_url: config.site.base_url.clone(), + langs: config.i18n.langs.clone(), + }); + + // Set active state and pass nav + let mut nav_with_active: Vec = section_nav + .into_iter() + .cloned() + .map(|mut item| { + set_active(&mut item, &page.url); + item + }) + .collect(); + + // If we're on a section index page, expand its children + if let Some(item) = nav_with_active.first_mut() { + if item.url == page.url { + item.active = true; + } + } + + ctx.insert("nav", &nav_with_active); + + let html = tera + .render(template_name, &ctx) + .with_context(|| format!("Failed to render template for {}", page.url))?; + + if let Some(parent) = page.out_path.parent() { + fs::create_dir_all(parent)?; + } + fs::write(&page.out_path, html)?; + } + } + + // Generate root redirect + generate_root_redirect(out, &config)?; + + println!( + "Site generated: {} languages, output at {}", + config.i18n.langs.len(), + out.display() + ); + + Ok(()) +} + +fn collect_pages( + pages_dir: &Path, + lang: &str, + out: &Path, + renderer: &MarkdownRenderer, +) -> Result> { + let mut pages = Vec::new(); + + for entry in WalkDir::new(pages_dir) + .into_iter() + .filter_map(|e| e.ok()) + .filter(|e| { + e.path().extension().map_or(false, |ext| ext == "md") + }) + { + let path = entry.path(); + let content = fs::read_to_string(path) + .with_context(|| format!("Failed to read {}", path.display()))?; + + let (frontmatter, body) = MarkdownRenderer::parse_frontmatter(&content) + .with_context(|| format!("Failed to parse frontmatter in {}", path.display()))?; + + let html_content = renderer.render(body); + + let rel = path.strip_prefix(pages_dir)?; + let rel_str = rel.to_string_lossy().to_string(); + + // Compute URL and output path + let (url, out_path) = if rel.file_name().map_or(false, |f| f == "index.md") { + // index.md -> //dir/ + let parent = rel.parent().unwrap_or(Path::new("")); + if parent.as_os_str().is_empty() { + // Root index.md + ( + format!("/{}/", lang), + out.join(lang).join("index.html"), + ) + } else { + ( + format!("/{}/{}/", lang, parent.display()), + out.join(lang).join(parent).join("index.html"), + ) + } + } else { + // foo.md -> //foo/ + let stem = rel.with_extension(""); + ( + format!("/{}/{}/", lang, stem.display()), + out.join(lang).join(&stem).join("index.html"), + ) + }; + + let section = extract_section(&url); + + pages.push(Page { + frontmatter, + html_content, + url, + out_path, + rel_path: rel_str, + section, + }); + } + + Ok(pages) +} + +fn extract_section(url: &str) -> String { + // URL format: // or //section/... + let parts: Vec<&str> = url.trim_matches('/').split('/').collect(); + if parts.len() >= 2 { + parts[1].to_string() + } else { + String::new() + } +} + +fn build_nav(pages: &[Page]) -> Vec { + // Group pages by section (top-level directory) + let mut sections: std::collections::BTreeMap> = + std::collections::BTreeMap::new(); + + for page in pages { + if page.section.is_empty() { + continue; // Skip root index (portal) + } + sections + .entry(page.section.clone()) + .or_default() + .push(page); + } + + let mut nav = Vec::new(); + + for (section, mut section_pages) in sections { + // Sort by order, then by filename + section_pages.sort_by(|a, b| { + a.frontmatter + .order + .cmp(&b.frontmatter.order) + .then_with(|| a.rel_path.cmp(&b.rel_path)) + }); + + // Find the section index page + let index_page = section_pages + .iter() + .find(|p| p.rel_path.ends_with("index.md") && extract_section(&p.url) == section); + + let section_title = index_page + .map(|p| p.frontmatter.title.clone()) + .unwrap_or_else(|| section.clone()); + let section_url = index_page + .map(|p| p.url.clone()) + .unwrap_or_default(); + + let children: Vec = section_pages + .iter() + .filter(|p| !p.rel_path.ends_with("index.md") || extract_section(&p.url) != section) + .map(|p| NavItem { + title: p.frontmatter.title.clone(), + url: p.url.clone(), + children: Vec::new(), + active: false, + }) + .collect(); + + nav.push(NavItem { + title: section_title, + url: section_url, + children, + active: false, + }); + } + + // Sort nav sections by order of their index pages + nav +} + +fn set_active(item: &mut NavItem, current_url: &str) { + if item.url == current_url { + item.active = true; + } + for child in &mut item.children { + set_active(child, current_url); + if child.active { + item.active = true; + } + } +} + +fn generate_root_redirect(out: &Path, config: &SiteConfig) -> Result<()> { + let html = format!( + r#" + + + + + +Redirecting... + + +

Redirecting to /{default_lang}/...

+ +"#, + config.i18n.default_lang, + default_lang = config.i18n.default_lang, + ); + + fs::write(out.join("index.html"), html)?; + Ok(()) +} + +fn copy_dir_recursive(src: &Path, dst: &Path) -> Result<()> { + for entry in WalkDir::new(src).into_iter().filter_map(|e| e.ok()) { + let path = entry.path(); + let rel = path.strip_prefix(src)?; + let target = dst.join(rel); + + if path.is_dir() { + fs::create_dir_all(&target)?; + } else { + if let Some(parent) = target.parent() { + fs::create_dir_all(parent)?; + } + fs::copy(path, &target)?; + } + } + Ok(()) +} diff --git a/docs-gen/src/config.rs b/docs-gen/src/config.rs new file mode 100644 index 0000000..007ee47 --- /dev/null +++ b/docs-gen/src/config.rs @@ -0,0 +1,53 @@ +use anyhow::{Context, Result}; +use serde::Deserialize; +use std::path::Path; + +#[derive(Debug, Deserialize)] +pub struct SiteConfig { + pub site: Site, + pub i18n: I18n, + pub highlight: Option, +} + +#[derive(Debug, Deserialize)] +pub struct Site { + pub title: String, + pub version: Option, + pub base_url: String, +} + +#[derive(Debug, Deserialize)] +pub struct I18n { + pub default_lang: String, + pub langs: Vec, +} + +#[derive(Debug, Deserialize)] +pub struct Highlight { + pub theme: Option, + pub theme_light: Option, +} + +impl SiteConfig { + pub fn load(src_dir: &Path) -> Result { + let path = src_dir.join("config.toml"); + let content = + std::fs::read_to_string(&path).with_context(|| format!("Failed to read {}", path.display()))?; + let config: SiteConfig = + toml::from_str(&content).with_context(|| format!("Failed to parse {}", path.display()))?; + Ok(config) + } + + pub fn highlight_theme(&self) -> &str { + self.highlight + .as_ref() + .and_then(|h| h.theme.as_deref()) + .unwrap_or("base16-ocean.dark") + } + + pub fn highlight_theme_light(&self) -> Option<&str> { + self.highlight + .as_ref() + .and_then(|h| h.theme_light.as_deref()) + } +} diff --git a/docs-gen/src/main.rs b/docs-gen/src/main.rs new file mode 100644 index 0000000..b1ac82f --- /dev/null +++ b/docs-gen/src/main.rs @@ -0,0 +1,23 @@ +mod builder; +mod config; +mod markdown; + +use clap::Parser; +use std::path::PathBuf; + +#[derive(Parser)] +#[command(version, about = "A simple static site generator")] +struct Cli { + /// Source directory containing config.toml + #[arg(default_value = ".")] + src: PathBuf, + + /// Output directory + #[arg(long, default_value = "docs")] + out: PathBuf, +} + +fn main() -> anyhow::Result<()> { + let cli = Cli::parse(); + builder::build(&cli.src, &cli.out) +} diff --git a/docs-gen/src/markdown.rs b/docs-gen/src/markdown.rs new file mode 100644 index 0000000..bc32684 --- /dev/null +++ b/docs-gen/src/markdown.rs @@ -0,0 +1,140 @@ +use anyhow::{Context, Result}; +use pulldown_cmark::{CodeBlockKind, Event, Options, Parser, Tag, TagEnd}; +use serde::Deserialize; +use syntect::highlighting::ThemeSet; +use syntect::html::highlighted_html_for_string; +use syntect::parsing::SyntaxSet; + +#[derive(Debug, Deserialize)] +pub struct Frontmatter { + pub title: String, + #[serde(default)] + pub order: i32, + pub status: Option, +} + +pub struct MarkdownRenderer { + syntax_set: SyntaxSet, + theme_set: ThemeSet, + theme_name: String, + theme_light_name: Option, +} + +impl MarkdownRenderer { + pub fn new(theme_name: &str, theme_light_name: Option<&str>) -> Self { + Self { + syntax_set: SyntaxSet::load_defaults_newlines(), + theme_set: ThemeSet::load_defaults(), + theme_name: theme_name.to_string(), + theme_light_name: theme_light_name.map(|s| s.to_string()), + } + } + + pub fn parse_frontmatter(content: &str) -> Result<(Frontmatter, &str)> { + let content = content.trim_start(); + if !content.starts_with("---") { + anyhow::bail!("Missing frontmatter delimiter"); + } + let after_first = &content[3..]; + let end = after_first + .find("\n---") + .context("Missing closing frontmatter delimiter")?; + let yaml = &after_first[..end]; + let body = &after_first[end + 4..]; + let fm: Frontmatter = + serde_yml::from_str(yaml).context("Failed to parse frontmatter YAML")?; + Ok((fm, body)) + } + + pub fn render(&self, markdown: &str) -> String { + let options = Options::ENABLE_TABLES + | Options::ENABLE_STRIKETHROUGH + | Options::ENABLE_TASKLISTS; + + let parser = Parser::new_ext(markdown, options); + + let mut in_code_block = false; + let mut code_lang = String::new(); + let mut code_buf = String::new(); + let mut events: Vec = Vec::new(); + + for event in parser { + match event { + Event::Start(Tag::CodeBlock(kind)) => { + in_code_block = true; + code_buf.clear(); + code_lang = match kind { + CodeBlockKind::Fenced(lang) => lang.to_string(), + CodeBlockKind::Indented => String::new(), + }; + } + Event::End(TagEnd::CodeBlock) => { + in_code_block = false; + let html = self.highlight_code(&code_buf, &code_lang); + events.push(Event::Html(html.into())); + } + Event::Text(text) if in_code_block => { + code_buf.push_str(&text); + } + other => events.push(other), + } + } + + let mut html_output = String::new(); + pulldown_cmark::html::push_html(&mut html_output, events.into_iter()); + html_output + } + + fn highlight_code(&self, code: &str, lang: &str) -> String { + if lang.is_empty() { + return format!("
{}
", escape_html(code)); + } + + let syntax = self + .syntax_set + .find_syntax_by_token(lang) + .unwrap_or_else(|| self.syntax_set.find_syntax_plain_text()); + + let dark_html = self.highlight_with_theme(code, syntax, &self.theme_name); + + if let Some(ref light_name) = self.theme_light_name { + let light_html = self.highlight_with_theme(code, syntax, light_name); + format!( + "
{}
{}
", + dark_html, light_html + ) + } else { + dark_html + } + } + + fn highlight_with_theme( + &self, + code: &str, + syntax: &syntect::parsing::SyntaxReference, + theme_name: &str, + ) -> String { + let theme = self + .theme_set + .themes + .get(theme_name) + .unwrap_or_else(|| { + self.theme_set + .themes + .values() + .next() + .expect("No themes available") + }); + + match highlighted_html_for_string(code, &self.syntax_set, syntax, theme) { + Ok(html) => html, + Err(_) => format!("
{}
", escape_html(code)), + } + } +} + +fn escape_html(s: &str) -> String { + s.replace('&', "&") + .replace('<', "<") + .replace('>', ">") +} diff --git a/docs-src/config.toml b/docs-src/config.toml new file mode 100644 index 0000000..faeaa5b --- /dev/null +++ b/docs-src/config.toml @@ -0,0 +1,12 @@ +[site] +title = "cpp-httplib" +version = "0.35.0" +base_url = "https://yhirose.github.io/cpp-httplib" + +[i18n] +default_lang = "en" +langs = ["en", "ja"] + +[highlight] +theme = "base16-eighties.dark" +theme_light = "base16-ocean.light" diff --git a/docs-src/pages/en/cookbook/index.md b/docs-src/pages/en/cookbook/index.md new file mode 100644 index 0000000..700d6ff --- /dev/null +++ b/docs-src/pages/en/cookbook/index.md @@ -0,0 +1,8 @@ +--- +title: "Cookbook" +order: 1 +--- + +This section is under construction. + +Check back soon for a collection of recipes organized by topic. diff --git a/docs-src/pages/en/index.md b/docs-src/pages/en/index.md new file mode 100644 index 0000000..6e40db8 --- /dev/null +++ b/docs-src/pages/en/index.md @@ -0,0 +1,21 @@ +--- +title: "cpp-httplib" +order: 0 +--- + +[cpp-httplib](https://github.com/yhirose/cpp-httplib) is an HTTP/HTTPS library for C++. Just copy a single header file, [`httplib.h`](https://github.com/yhirose/cpp-httplib/raw/refs/tags/latest/httplib.h), and you're ready to go. + +When you need a quick HTTP server or client in C++, you want something that just works. That's exactly why I built cpp-httplib. You can start writing both servers and clients in just a few lines of code. + +The API uses a lambda-based design that feels natural. It runs anywhere you have a C++11 or later compiler. Windows, macOS, Linux — use whatever environment you already have. + +HTTPS works too. Just link OpenSSL or mbedTLS, and both server and client gain TLS support. Content-Encoding (gzip, Brotli, etc.), file uploads, and other features you actually need in real-world development are all included. WebSocket is also supported. + +Under the hood, it uses blocking I/O with a thread pool. It's not built for handling massive numbers of simultaneous connections. But for API servers, embedded HTTP in tools, mock servers for testing, and many other use cases, it delivers solid performance. + +"Solve today's problem, today." That's the kind of simplicity cpp-httplib aims for. + +## Documentation + +- [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 diff --git a/docs-src/pages/en/tour/01-getting-started.md b/docs-src/pages/en/tour/01-getting-started.md new file mode 100644 index 0000000..f6c82ba --- /dev/null +++ b/docs-src/pages/en/tour/01-getting-started.md @@ -0,0 +1,88 @@ +--- +title: "Getting Started" +order: 1 +--- + +All you need to get started with cpp-httplib is `httplib.h` and a C++ compiler. Let's download the file and get a Hello World server running. + +## Getting httplib.h + +You can download it directly from GitHub. Always use the latest version. + +```sh +curl -LO https://github.com/yhirose/cpp-httplib/raw/refs/tags/latest/httplib.h +``` + +Place the downloaded `httplib.h` in your project directory and you're good to go. + +## Setting Up Your Compiler + +| OS | Development Environment | Setup | +| -- | ----------------------- | ----- | +| macOS | Apple Clang | Xcode Command Line Tools (`xcode-select --install`) | +| Ubuntu | clang++ or g++ | `apt install clang` or `apt install g++` | +| Windows | MSVC | Visual Studio 2022 or later (install with C++ components) | + +## Hello World Server + +Save the following code as `server.cpp`. + +```cpp +#include "httplib.h" + +int main() { + httplib::Server svr; + + svr.Get("/", [](const httplib::Request&, httplib::Response& res) { + res.set_content("Hello, World!", "text/plain"); + }); + + svr.listen("0.0.0.0", 8080); +} +``` + +In just a few lines, you have a server that responds to HTTP requests. + +## Compiling and Running + +The sample code in this tutorial is written in C++17 for cleaner, more concise code. cpp-httplib itself can compile with C++11 as well. + +```sh +# macOS +clang++ -std=c++17 -o server server.cpp + +# Linux +# `-pthread`: cpp-httplib uses threads internally +clang++ -std=c++17 -pthread -o server server.cpp + +# Windows (Developer Command Prompt) +# `/EHsc`: Enable C++ exception handling +cl /EHsc /std:c++17 server.cpp +``` + +Once it compiles, run it. + +```sh +# macOS / Linux +./server + +# Windows +server.exe +``` + +Open `http://localhost:8080` in your browser. If you see "Hello, World!", you're all set. + +You can also verify with `curl`. + +```sh +curl http://localhost:8080/ +# Hello, World! +``` + +To stop the server, press `Ctrl+C` in your terminal. + +## Next Steps + +Now you know the basics of running a server. Next, let's look at the client side. cpp-httplib also comes with HTTP client functionality. + +**Next:** [Basic Client](../02-basic-client) diff --git a/docs-src/pages/en/tour/02-basic-client.md b/docs-src/pages/en/tour/02-basic-client.md new file mode 100644 index 0000000..9a909f7 --- /dev/null +++ b/docs-src/pages/en/tour/02-basic-client.md @@ -0,0 +1,266 @@ +--- +title: "Basic Client" +order: 2 +--- + +cpp-httplib isn't just for servers -- it also comes with a full HTTP client. Let's use `httplib::Client` to send GET and POST requests. + +## Preparing a Test Server + +To try out the client, you need a server that accepts requests. Save the following code, then compile and run it the same way you did in the previous chapter. We'll cover the server details in the next chapter. + +```cpp +#include "httplib.h" +#include + +int main() { + httplib::Server svr; + + svr.Get("/hi", [](const auto &, auto &res) { + res.set_content("Hello!", "text/plain"); + }); + + svr.Get("/search", [](const auto &req, auto &res) { + auto q = req.get_param_value("q"); + res.set_content("Query: " + q, "text/plain"); + }); + + svr.Post("/post", [](const auto &req, auto &res) { + res.set_content(req.body, "text/plain"); + }); + + svr.Post("/submit", [](const auto &req, auto &res) { + std::string result; + for (auto &[key, val] : req.params) { + result += key + " = " + val + "\n"; + } + res.set_content(result, "text/plain"); + }); + + svr.Post("/upload", [](const auto &req, auto &res) { + auto f = req.form.get_file("file"); + auto content = f.filename + " (" + std::to_string(f.content.size()) + " bytes)"; + res.set_content(content, "text/plain"); + }); + + svr.Get("/users/:id", [](const auto &req, auto &res) { + auto id = req.path_params.at("id"); + res.set_content("User ID: " + id, "text/plain"); + }); + + svr.Get(R"(/files/(\d+))", [](const auto &req, auto &res) { + auto id = req.matches[1]; + res.set_content("File ID: " + std::string(id), "text/plain"); + }); + + std::cout << "Listening on port 8080..." << std::endl; + svr.listen("0.0.0.0", 8080); +} +``` + +## GET Request + +Once the server is running, open a separate terminal and give it a try. Let's start with the simplest GET request. + +```cpp +#include "httplib.h" +#include + +int main() { + httplib::Client cli("http://localhost:8080"); + + auto res = cli.Get("/hi"); + if (res) { + std::cout << res->status << std::endl; // 200 + std::cout << res->body << std::endl; // Hello! + } +} +``` + +Pass the server address to the `httplib::Client` constructor, then call `Get()` to send a request. You can retrieve the status code and body from the returned `res`. + +Here's the equivalent `curl` command. + +```sh +curl http://localhost:8080/hi +# Hello! +``` + +## Checking the Response + +A response contains header information in addition to the status code and body. + +```cpp +auto res = cli.Get("/hi"); +if (res) { + // Status code + std::cout << res->status << std::endl; // 200 + + // Body + std::cout << res->body << std::endl; // Hello! + + // Headers + std::cout << res->get_header_value("Content-Type") << std::endl; // text/plain +} +``` + +`res->body` is a `std::string`, so if you want to parse a JSON response, you can pass it directly to a JSON library like [nlohmann/json](https://github.com/nlohmann/json). + +## Query Parameters + +To add query parameters to a GET request, you can either write them directly in the URL or use `httplib::Params`. + +```cpp +auto res = cli.Get("/search", httplib::Params{{"q", "cpp-httplib"}}); +if (res) { + std::cout << res->body << std::endl; // Query: cpp-httplib +} +``` + +`httplib::Params` automatically URL-encodes special characters for you. + +```sh +curl "http://localhost:8080/search?q=cpp-httplib" +# Query: cpp-httplib +``` + +## Path Parameters + +When values are embedded directly in the URL path, no special client API is needed. Just pass the path to `Get()` as-is. + +```cpp +auto res = cli.Get("/users/42"); +if (res) { + std::cout << res->body << std::endl; // User ID: 42 +} +``` + +```sh +curl http://localhost:8080/users/42 +# User ID: 42 +``` + +The test server also has a `/files/(\d+)` route that uses a regex to accept numeric IDs only. + +```cpp +auto res = cli.Get("/files/42"); +if (res) { + std::cout << res->body << std::endl; // File ID: 42 +} +``` + +```sh +curl http://localhost:8080/files/42 +# File ID: 42 +``` + +Pass a non-numeric ID like `/files/abc` and you'll get a 404. We'll cover how that works in the next chapter. + +## Request Headers + +To add custom HTTP headers, pass an `httplib::Headers` object. This works with both `Get()` and `Post()`. + +```cpp +auto res = cli.Get("/hi", httplib::Headers{ + {"Authorization", "Bearer my-token"} +}); +``` + +```sh +curl -H "Authorization: Bearer my-token" http://localhost:8080/hi +``` + +## POST Request + +Let's POST some text data. Pass the body as the second argument to `Post()` and the Content-Type as the third. + +```cpp +auto res = cli.Post("/post", "Hello, Server!", "text/plain"); +if (res) { + std::cout << res->status << std::endl; // 200 + std::cout << res->body << std::endl; // Hello, Server! +} +``` + +The test server's `/post` endpoint echoes the body back, so you get the same string you sent. + +```sh +curl -X POST -H "Content-Type: text/plain" -d "Hello, Server!" http://localhost:8080/post +# Hello, Server! +``` + +## Sending Form Data + +You can send key-value pairs just like an HTML form. Use `httplib::Params` for this. + +```cpp +auto res = cli.Post("/submit", httplib::Params{ + {"name", "Alice"}, + {"age", "30"} +}); +if (res) { + std::cout << res->body << std::endl; + // age = 30 + // name = Alice +} +``` + +This sends the data in `application/x-www-form-urlencoded` format. + +```sh +curl -X POST -d "name=Alice&age=30" http://localhost:8080/submit +``` + +## POSTing a File + +To upload a file, use `httplib::UploadFormDataItems` to send it as multipart form data. + +```cpp +auto res = cli.Post("/upload", httplib::UploadFormDataItems{ + {"file", "Hello, File!", "hello.txt", "text/plain"} +}); +if (res) { + std::cout << res->body << std::endl; // hello.txt (12 bytes) +} +``` + +Each element in `UploadFormDataItems` has four fields: `{name, content, filename, content_type}`. + +```sh +curl -F "file=Hello, File!;filename=hello.txt;type=text/plain" http://localhost:8080/upload +``` + +## Error Handling + +Network communication can fail -- the server might not be reachable. Always check whether `res` is valid. + +```cpp +httplib::Client cli("http://localhost:9999"); // Non-existent port +auto res = cli.Get("/hi"); + +if (!res) { + // Connection error + std::cout << "Error: " << httplib::to_string(res.error()) << std::endl; + // Error: Connection + return 1; +} + +// If we reach here, we have a response +if (res->status != 200) { + std::cout << "HTTP Error: " << res->status << std::endl; + return 1; +} + +std::cout << res->body << std::endl; +``` + +There are two levels of errors. + +- **Connection error**: The client couldn't reach the server. `res` evaluates to false, and you can call `res.error()` to find out what went wrong. +- **HTTP error**: The server returned an error status (404, 500, etc.). `res` evaluates to true, but you need to check `res->status`. + +## Next Steps + +Now you know how to send requests from a client. Next, let's take a closer look at the server side. We'll dig into routing, path parameters, and more. + +**Next:** [Basic Server](../03-basic-server) diff --git a/docs-src/pages/en/tour/03-basic-server.md b/docs-src/pages/en/tour/03-basic-server.md new file mode 100644 index 0000000..a63a0ab --- /dev/null +++ b/docs-src/pages/en/tour/03-basic-server.md @@ -0,0 +1,280 @@ +--- +title: "Basic Server" +order: 3 +--- + +In the previous chapter, you sent requests from a client to a test server. Now let's walk through how that server actually works. + +## Starting the Server + +Once you've registered your routes, call `svr.listen()` to start the server. + +```cpp +svr.listen("0.0.0.0", 8080); +``` + +The first argument is the host, and the second is the port. `"0.0.0.0"` listens on all network interfaces. Use `"127.0.0.1"` if you want to accept connections from your own machine only. + +`listen()` is a blocking call. It won't return until the server stops. The server keeps running until you press `Ctrl+C` in your terminal or call `svr.stop()` from another thread. + +## Routing + +Routing is the heart of any server. It's how you tell cpp-httplib: when a request comes in for this URL with this HTTP method, run this code. + +```cpp +httplib::Server svr; + +svr.Get("/hi", [](const httplib::Request &req, httplib::Response &res) { + res.set_content("Hello!", "text/plain"); +}); +``` + +`svr.Get()` registers a handler for GET requests. The first argument is the path, the second is the handler function. When a GET request arrives at `/hi`, your lambda runs. + +There's a method for each HTTP verb. + +```cpp +svr.Get("/path", handler); // GET +svr.Post("/path", handler); // POST +svr.Put("/path", handler); // PUT +svr.Delete("/path", handler); // DELETE +``` + +The handler signature is `(const httplib::Request &req, httplib::Response &res)`. You can use `auto` to keep it short. + +```cpp +svr.Get("/hi", [](const auto &req, auto &res) { + res.set_content("Hello!", "text/plain"); +}); +``` + +The handler only runs when the path matches. Requests to unregistered paths automatically return 404. + +## The Request Object + +The first parameter `req` gives you everything the client sent. + +### Body + +`req.body` holds the request body as a `std::string`. + +```cpp +svr.Post("/post", [](const auto &req, auto &res) { + // Echo the body back to the client + res.set_content(req.body, "text/plain"); +}); +``` + +### Headers + +Use `req.get_header_value()` to read a request header. + +```cpp +svr.Get("/check", [](const auto &req, auto &res) { + auto auth = req.get_header_value("Authorization"); + res.set_content("Auth: " + auth, "text/plain"); +}); +``` + +### Query Parameters and Form Data + +`req.get_param_value()` retrieves a parameter by name. It works for both GET query parameters and POST form data. + +```cpp +svr.Get("/search", [](const auto &req, auto &res) { + auto q = req.get_param_value("q"); + res.set_content("Query: " + q, "text/plain"); +}); +``` + +A request to `/search?q=cpp-httplib` gives you `"cpp-httplib"` for `q`. + +To loop over all parameters, use `req.params`. + +```cpp +svr.Post("/submit", [](const auto &req, auto &res) { + std::string result; + for (auto &[key, val] : req.params) { + result += key + " = " + val + "\n"; + } + res.set_content(result, "text/plain"); +}); +``` + +### File Uploads + +Files uploaded via multipart form data are available through `req.form.get_file()`. + +```cpp +svr.Post("/upload", [](const auto &req, auto &res) { + auto f = req.form.get_file("file"); + auto content = f.filename + " (" + std::to_string(f.content.size()) + " bytes)"; + res.set_content(content, "text/plain"); +}); +``` + +`f.filename` gives you the filename, and `f.content` gives you the file data. + +## Path Parameters + +Sometimes you want to capture part of the URL as a variable -- for example, the `42` in `/users/42`. Use the `:param` syntax to do that. + +```cpp +svr.Get("/users/:id", [](const auto &req, auto &res) { + auto id = req.path_params.at("id"); + res.set_content("User ID: " + id, "text/plain"); +}); +``` + +A request to `/users/42` gives you `"42"` from `req.path_params.at("id")`. `/users/100` gives you `"100"`. + +You can capture multiple segments at once. + +```cpp +svr.Get("/users/:user_id/posts/:post_id", [](const auto &req, auto &res) { + auto user_id = req.path_params.at("user_id"); + auto post_id = req.path_params.at("post_id"); + res.set_content("User: " + user_id + ", Post: " + post_id, "text/plain"); +}); +``` + +### Regex Patterns + +You can also write a regular expression directly in the path instead of `:param`. Capture group values are available via `req.matches`, which is a `std::smatch`. + +```cpp +// Only accept numeric IDs +svr.Get(R"(/files/(\d+))", [](const auto &req, auto &res) { + auto id = req.matches[1]; // First capture group + res.set_content("File ID: " + std::string(id), "text/plain"); +}); +``` + +`/files/42` matches, but `/files/abc` doesn't. This is handy when you want to constrain what values are accepted. + +## Building a Response + +The second parameter `res` is how you send data back to the client. + +### Body and Content-Type + +`res.set_content()` sets the body and Content-Type. That's all you need for a 200 response. + +```cpp +svr.Get("/hi", [](const auto &req, auto &res) { + res.set_content("Hello!", "text/plain"); +}); +``` + +### Status Code + +To return a different status code, assign to `res.status`. + +```cpp +svr.Get("/not-found", [](const auto &req, auto &res) { + res.status = 404; + res.set_content("Not found", "text/plain"); +}); +``` + +### Response Headers + +Add response headers with `res.set_header()`. + +```cpp +svr.Get("/with-header", [](const auto &req, auto &res) { + res.set_header("X-Custom", "my-value"); + res.set_content("Hello!", "text/plain"); +}); +``` + +## Walking Through the Test Server + +Now let's use what we've learned to read through the test server from the previous chapter. + +### GET /hi + +```cpp +svr.Get("/hi", [](const auto &, auto &res) { + res.set_content("Hello!", "text/plain"); +}); +``` + +The simplest possible handler. We don't need any information from the request, so the `req` parameter is left unnamed. It just returns `"Hello!"`. + +### GET /search + +```cpp +svr.Get("/search", [](const auto &req, auto &res) { + auto q = req.get_param_value("q"); + res.set_content("Query: " + q, "text/plain"); +}); +``` + +`req.get_param_value("q")` pulls out the query parameter `q`. A request to `/search?q=cpp-httplib` returns `"Query: cpp-httplib"`. + +### POST /post + +```cpp +svr.Post("/post", [](const auto &req, auto &res) { + res.set_content(req.body, "text/plain"); +}); +``` + +An echo server. Whatever body the client sends, `req.body` holds it, and we send it straight back. + +### POST /submit + +```cpp +svr.Post("/submit", [](const auto &req, auto &res) { + std::string result; + for (auto &[key, val] : req.params) { + result += key + " = " + val + "\n"; + } + res.set_content(result, "text/plain"); +}); +``` + +Loops over the form data in `req.params` using structured bindings (`auto &[key, val]`) to unpack each key-value pair. + +### POST /upload + +```cpp +svr.Post("/upload", [](const auto &req, auto &res) { + auto f = req.form.get_file("file"); + auto content = f.filename + " (" + std::to_string(f.content.size()) + " bytes)"; + res.set_content(content, "text/plain"); +}); +``` + +Receives a file uploaded via multipart form data. `req.form.get_file("file")` fetches the field named `"file"`, and we respond with the filename and size. + +### GET /users/:id + +```cpp +svr.Get("/users/:id", [](const auto &req, auto &res) { + auto id = req.path_params.at("id"); + res.set_content("User ID: " + id, "text/plain"); +}); +``` + +`:id` is the path parameter. `req.path_params.at("id")` retrieves its value. `/users/42` gives you `"42"`, `/users/alice` gives you `"alice"`. + +### GET /files/(\d+) + +```cpp +svr.Get(R"(/files/(\d+))", [](const auto &req, auto &res) { + auto id = req.matches[1]; + res.set_content("File ID: " + std::string(id), "text/plain"); +}); +``` + +The regex `(\d+)` matches numeric IDs only. `/files/42` hits this handler, but `/files/abc` returns 404. `req.matches[1]` retrieves the first capture group. + +## Next Steps + +You now have the full picture of how a server works. Routing, reading requests, building responses -- that's enough to build a real API server. + +Next, let's look at serving static files. We'll build a server that delivers HTML and CSS. + +**Next:** [Static File Server](../04-static-file-server) diff --git a/docs-src/pages/en/tour/04-static-file-server.md b/docs-src/pages/en/tour/04-static-file-server.md new file mode 100644 index 0000000..6760086 --- /dev/null +++ b/docs-src/pages/en/tour/04-static-file-server.md @@ -0,0 +1,134 @@ +--- +title: "Static File Server" +order: 4 +--- + +cpp-httplib can serve static files too — HTML, CSS, images, you name it. No complicated configuration required. One call to `set_mount_point()` is all it takes. + +## The basics of set_mount_point + +Let's jump right in. `set_mount_point()` maps a URL path to a local directory. + +```cpp +#include "httplib.h" +#include + +int main() { + httplib::Server svr; + + svr.set_mount_point("/", "./html"); + + std::cout << "Listening on port 8080..." << std::endl; + svr.listen("0.0.0.0", 8080); +} +``` + +The first argument is the URL mount point. The second is the local directory path. In this example, requests to `/` are served from the `./html` directory. + +Let's try it out. First, create an `html` directory and add an `index.html` file. + +```sh +mkdir html +``` + +```html + + +My Page + +

Hello from cpp-httplib!

+

This is a static file.

+ + +``` + +Compile and start the server. + +```sh +g++ -std=c++17 -o server server.cpp -pthread +./server +``` + +Open `http://localhost:8080` in your browser. You should see the contents of `html/index.html`. Visiting `http://localhost:8080/index.html` returns the same page. + +You can also access it with the client code from the previous chapter, or with `curl`. + +```cpp +httplib::Client cli("http://localhost:8080"); +auto res = cli.Get("/"); +if (res) { + std::cout << res->body << std::endl; // HTML is displayed +} +``` + +```sh +curl http://localhost:8080 +``` + +## Multiple mount points + +You can call `set_mount_point()` as many times as you like. Each URL path gets its own directory. + +```cpp +svr.set_mount_point("/", "./public"); +svr.set_mount_point("/assets", "./static/assets"); +svr.set_mount_point("/docs", "./documentation"); +``` + +A request to `/assets/style.css` serves `./static/assets/style.css`. A request to `/docs/guide.html` serves `./documentation/guide.html`. + +## Combining with handlers + +Static file serving and routing handlers — the kind you learned about in the previous chapter — work side by side. + +```cpp +httplib::Server svr; + +// API endpoint +svr.Get("/api/hello", [](const auto &, auto &res) { + res.set_content(R"({"message":"Hello!"})", "application/json"); +}); + +// Static file serving +svr.set_mount_point("/", "./public"); + +svr.listen("0.0.0.0", 8080); +``` + +Handlers take priority. The handler responds to `/api/hello`. For every other path, the server looks for a file in `./public`. + +## Adding response headers + +Pass headers as the third argument to `set_mount_point()` and they get attached to every static file response. This is great for cache control. + +```cpp +svr.set_mount_point("/", "./public", { + {"Cache-Control", "max-age=3600"} +}); +``` + +With this in place, the browser caches served files for one hour. + +## A Dockerfile for your static file server + +The cpp-httplib repository includes a `Dockerfile` built for static file serving. We also publish a pre-built image on Docker Hub, so you can get up and running with a single command. + +```sh +> docker run -p 8080:80 -v ./my-site:/html yhirose4dockerhub/cpp-httplib-server +Serving HTTP on 0.0.0.0:80 +Mount point: / -> ./html +Press Ctrl+C to shutdown gracefully... +192.168.65.1 - - [22/Feb/2026:12:00:00 +0000] "GET / HTTP/1.1" 200 256 "-" "Mozilla/5.0 ..." +192.168.65.1 - - [22/Feb/2026:12:00:00 +0000] "GET /style.css HTTP/1.1" 200 1024 "-" "Mozilla/5.0 ..." +192.168.65.1 - - [22/Feb/2026:12:00:01 +0000] "GET /favicon.ico HTTP/1.1" 404 152 "-" "Mozilla/5.0 ..." +``` + +Everything in your `./my-site` directory gets served on port 8080. The access log follows the same format as NGINX, so you can see exactly what's happening. + +## What's next + +You can now serve static files. A web server that delivers HTML, CSS, and JavaScript — built with this little code. + +Next, let's encrypt your connections with HTTPS. We'll start by setting up a TLS library. + +**Next:** [TLS Setup](../05-tls-setup) diff --git a/docs-src/pages/en/tour/05-tls-setup.md b/docs-src/pages/en/tour/05-tls-setup.md new file mode 100644 index 0000000..40eee3c --- /dev/null +++ b/docs-src/pages/en/tour/05-tls-setup.md @@ -0,0 +1,88 @@ +--- +title: "TLS Setup" +order: 5 +--- + +So far we've been using plain HTTP, but in the real world, HTTPS is the norm. To use HTTPS with cpp-httplib, you need a TLS library. + +In this tour, we'll use OpenSSL. It's the most widely used option, and you'll find plenty of resources online. + +## Installing OpenSSL + +Install it for your OS. + +| OS | How to install | +| -- | -------------- | +| macOS | [Homebrew](https://brew.sh/) (`brew install openssl`) | +| Ubuntu / Debian | `sudo apt install libssl-dev` | +| Windows | [vcpkg](https://vcpkg.io/) (`vcpkg install openssl`) | + +## Compile Options + +To enable TLS, define the `CPPHTTPLIB_OPENSSL_SUPPORT` macro when compiling. You'll need a few extra options compared to the previous chapters. + +```sh +# macOS (Homebrew) +clang++ -std=c++17 -DCPPHTTPLIB_OPENSSL_SUPPORT \ + -I$(brew --prefix openssl)/include \ + -L$(brew --prefix openssl)/lib \ + -lssl -lcrypto \ + -framework CoreFoundation -framework Security \ + -o server server.cpp + +# Linux +clang++ -std=c++17 -pthread -DCPPHTTPLIB_OPENSSL_SUPPORT \ + -lssl -lcrypto \ + -o server server.cpp + +# Windows (Developer Command Prompt) +cl /EHsc /std:c++17 /DCPPHTTPLIB_OPENSSL_SUPPORT server.cpp libssl.lib libcrypto.lib +``` + +Let's look at what each option does. + +- **`-DCPPHTTPLIB_OPENSSL_SUPPORT`** — Defines the macro that enables TLS support +- **`-lssl -lcrypto`** — Links the OpenSSL libraries +- **`-I` / `-L`** (macOS only) — Points to the Homebrew OpenSSL paths +- **`-framework CoreFoundation -framework Security`** (macOS only) — Needed to automatically load system certificates from the Keychain + +## Verifying the Setup + +Let's make sure everything works. Here's a simple program that passes an HTTPS URL to `httplib::Client`. + +```cpp +#define CPPHTTPLIB_OPENSSL_SUPPORT +#include "httplib.h" +#include + +int main() { + httplib::Client cli("https://www.google.com"); + + auto res = cli.Get("/"); + if (res) { + std::cout << "Status: " << res->status << std::endl; + } else { + std::cout << "Error: " << httplib::to_string(res.error()) << std::endl; + } +} +``` + +Compile and run it. If you see `Status: 200`, your setup is complete. + +## Other TLS Backends + +cpp-httplib also supports Mbed TLS and wolfSSL in addition to OpenSSL. You can switch between them just by changing the macro definition and linked libraries. + +| Backend | Macro | Libraries to link | +| :--- | :--- | :--- | +| OpenSSL | `CPPHTTPLIB_OPENSSL_SUPPORT` | `libssl`, `libcrypto` | +| Mbed TLS | `CPPHTTPLIB_MBEDTLS_SUPPORT` | `libmbedtls`, `libmbedx509`, `libmbedcrypto` | +| wolfSSL | `CPPHTTPLIB_WOLFSSL_SUPPORT` | `libwolfssl` | + +This tour assumes OpenSSL, but the API is the same regardless of which backend you choose. + +## Next Step + +You're all set with TLS. Next, let's send a request to an HTTPS site. + +**Next:** [HTTPS Client](../06-https-client) diff --git a/docs-src/pages/en/tour/06-https-client.md b/docs-src/pages/en/tour/06-https-client.md new file mode 100644 index 0000000..3faabd9 --- /dev/null +++ b/docs-src/pages/en/tour/06-https-client.md @@ -0,0 +1,122 @@ +--- +title: "HTTPS Client" +order: 6 +--- + +In the previous chapter, you set up OpenSSL. Now let's put it to use with an HTTPS client. You can use the same `httplib::Client` from Chapter 2. Just pass a URL with the `https://` scheme to the constructor. + +## GET Request + +Let's try accessing a real HTTPS site. + +```cpp +#define CPPHTTPLIB_OPENSSL_SUPPORT +#include "httplib.h" +#include + +int main() { + httplib::Client cli("https://nghttp2.org"); + + auto res = cli.Get("/"); + if (res) { + std::cout << res->status << std::endl; // 200 + std::cout << res->body.substr(0, 100) << std::endl; // First 100 chars of the HTML + } else { + std::cout << "Error: " << httplib::to_string(res.error()) << std::endl; + } +} +``` + +In Chapter 2, you wrote `httplib::Client cli("http://localhost:8080")`. All you need to change is the scheme to `https://`. Every API you learned in Chapter 2 -- `Get()`, `Post()`, and so on -- works exactly the same way. + +```sh +curl https://nghttp2.org/ +``` + +## Specifying a Port + +The default port for HTTPS is 443. If you need a different port, include it in the URL. + +```cpp +httplib::Client cli("https://localhost:8443"); +``` + +## CA Certificate Verification + +When connecting over HTTPS, `httplib::Client` verifies the server certificate by default. It only connects to servers whose certificate was issued by a trusted CA (Certificate Authority). + +CA certificates are loaded automatically from the Keychain on macOS, the system CA certificate store on Linux, and the Windows certificate store on Windows. In most cases, no extra configuration is needed. + +### Specifying a CA Certificate File + +On some environments, the system CA certificates may not be found. In that case, use `set_ca_cert_path()` to specify the path directly. + +```cpp +httplib::Client cli("https://nghttp2.org"); +cli.set_ca_cert_path("/etc/ssl/certs/ca-certificates.crt"); + +auto res = cli.Get("/"); +``` + +```sh +curl --cacert /etc/ssl/certs/ca-certificates.crt https://nghttp2.org/ +``` + +### Disabling Certificate Verification + +During development, you might want to connect to a server with a self-signed certificate. You can disable verification for that. + +```cpp +httplib::Client cli("https://localhost:8443"); +cli.enable_server_certificate_verification(false); + +auto res = cli.Get("/"); +``` + +```sh +curl -k https://localhost:8443/ +``` + +Never disable this in production. It opens you up to man-in-the-middle attacks. + +## Following Redirects + +When accessing HTTPS sites, you'll often encounter redirects. For example, `http://` to `https://`, or a bare domain to `www`. + +By default, redirects are not followed. You can check the redirect target in the `Location` header. + +```cpp +httplib::Client cli("https://nghttp2.org"); + +auto res = cli.Get("/httpbin/redirect/3"); +if (res) { + std::cout << res->status << std::endl; // 302 + std::cout << res->get_header_value("Location") << std::endl; +} +``` + +```sh +curl https://nghttp2.org/httpbin/redirect/3 +``` + +Call `set_follow_location(true)` to automatically follow redirects and get the final response. + +```cpp +httplib::Client cli("https://nghttp2.org"); +cli.set_follow_location(true); + +auto res = cli.Get("/httpbin/redirect/3"); +if (res) { + std::cout << res->status << std::endl; // 200 (the final response) +} +``` + +```sh +curl -L https://nghttp2.org/httpbin/redirect/3 +``` + +## Next Steps + +Now you know how to use the HTTPS client. Next, let's set up your own HTTPS server. We'll start with creating a self-signed certificate. + +**Next:** [HTTPS Server](../07-https-server) diff --git a/docs-src/pages/en/tour/07-https-server.md b/docs-src/pages/en/tour/07-https-server.md new file mode 100644 index 0000000..e6857e6 --- /dev/null +++ b/docs-src/pages/en/tour/07-https-server.md @@ -0,0 +1,124 @@ +--- +title: "HTTPS Server" +order: 7 +--- + +In the previous chapter, you used an HTTPS client. Now let's set up your own HTTPS server. Just swap `httplib::Server` from Chapter 3 with `httplib::SSLServer`. + +A TLS server needs a server certificate and a private key, though. Let's get those ready first. + +## Creating a Self-Signed Certificate + +For development and testing, a self-signed certificate works just fine. You can generate one quickly with an OpenSSL command. + +```sh +openssl req -x509 -noenc -keyout key.pem -out cert.pem -subj /CN=localhost +``` + +This creates two files: + +- **`cert.pem`** — Server certificate +- **`key.pem`** — Private key + +## A Minimal HTTPS Server + +Once you have your certificate, let's write the server. + +```cpp +#define CPPHTTPLIB_OPENSSL_SUPPORT +#include "httplib.h" +#include + +int main() { + httplib::SSLServer svr("cert.pem", "key.pem"); + + svr.Get("/", [](const auto &, auto &res) { + res.set_content("Hello, HTTPS!", "text/plain"); + }); + + std::cout << "Listening on https://localhost:8443" << std::endl; + svr.listen("0.0.0.0", 8443); +} +``` + +Just pass the certificate and private key paths to the `httplib::SSLServer` constructor. The routing API is exactly the same as `httplib::Server` from Chapter 3. + +Compile and start it up. + +## Testing It Out + +With the server running, try accessing it with `curl`. Since we're using a self-signed certificate, add the `-k` option to skip certificate verification. + +```sh +curl -k https://localhost:8443/ +# Hello, HTTPS! +``` + +If you open `https://localhost:8443` in a browser, you'll see a "This connection is not secure" warning. That's expected with a self-signed certificate. Just proceed past it. + +## Connecting from a Client + +Let's connect using `httplib::Client` from the previous chapter. There are two ways to connect to a server with a self-signed certificate. + +### Option 1: Disable Certificate Verification + +This is the quick and easy approach for development. + +```cpp +#define CPPHTTPLIB_OPENSSL_SUPPORT +#include "httplib.h" +#include + +int main() { + httplib::Client cli("https://localhost:8443"); + cli.enable_server_certificate_verification(false); + + auto res = cli.Get("/"); + if (res) { + std::cout << res->body << std::endl; // Hello, HTTPS! + } +} +``` + +### Option 2: Specify the Self-Signed Certificate as a CA Certificate + +This is the safer approach. You tell the client to trust `cert.pem` as a CA certificate. + +```cpp +#define CPPHTTPLIB_OPENSSL_SUPPORT +#include "httplib.h" +#include + +int main() { + httplib::Client cli("https://localhost:8443"); + cli.set_ca_cert_path("cert.pem"); + + auto res = cli.Get("/"); + if (res) { + std::cout << res->body << std::endl; // Hello, HTTPS! + } +} +``` + +This way, only connections to the server with that specific certificate are allowed, preventing impersonation. Use this approach whenever possible, even in test environments. + +## Comparing Server and SSLServer + +The `httplib::Server` API you learned in Chapter 3 works exactly the same with `httplib::SSLServer`. The only difference is the constructor. + +| | `httplib::Server` | `httplib::SSLServer` | +| -- | ------------------ | -------------------- | +| Constructor | No arguments | Certificate and private key paths | +| Protocol | HTTP | HTTPS | +| Port (convention) | 8080 | 8443 | +| Routing | Same | Same | + +To switch an HTTP server to HTTPS, just change the constructor. + +## Next Steps + +Your HTTPS server is up and running. You now have the basics of both HTTP/HTTPS clients and servers covered. + +Next, let's look at the WebSocket support that was recently added to cpp-httplib. + +**Next:** [WebSocket](../08-websocket) diff --git a/docs-src/pages/en/tour/08-websocket.md b/docs-src/pages/en/tour/08-websocket.md new file mode 100644 index 0000000..edb03c4 --- /dev/null +++ b/docs-src/pages/en/tour/08-websocket.md @@ -0,0 +1,139 @@ +--- +title: "WebSocket" +order: 8 +--- + +cpp-httplib supports WebSocket as well. Unlike HTTP request/response, WebSocket lets the server and client exchange messages in both directions. It's great for chat apps and real-time notifications. + +Let's build an echo server and client right away. + +## Echo Server + +Here's an echo server that sends back whatever message it receives. + +```cpp +#include "httplib.h" +#include + +int main() { + httplib::Server svr; + + svr.WebSocket("/ws", [](const httplib::Request &, httplib::ws::WebSocket &ws) { + std::string msg; + while (ws.read(msg)) { + ws.send(msg); // Send back the received message as-is + } + }); + + std::cout << "Listening on port 8080..." << std::endl; + svr.listen("0.0.0.0", 8080); +} +``` + +You register a WebSocket handler with `svr.WebSocket()`. It works just like `svr.Get()` and `svr.Post()` from Chapter 3. + +Inside the handler, `ws.read(msg)` waits for a message. When the connection closes, `read()` returns `false`, so the loop exits. `ws.send(msg)` sends a message back. + +## Connecting from a Client + +Let's connect to the server using `httplib::ws::WebSocketClient`. + +```cpp +#include "httplib.h" +#include + +int main() { + httplib::ws::WebSocketClient client("ws://localhost:8080/ws"); + + if (!client.connect()) { + std::cout << "Connection failed" << std::endl; + return 1; + } + + // Send a message + client.send("Hello, WebSocket!"); + + // Receive a response from the server + std::string msg; + if (client.read(msg)) { + std::cout << msg << std::endl; // Hello, WebSocket! + } + + client.close(); +} +``` + +Pass a URL in `ws://host:port/path` format to the constructor. Call `connect()` to start the connection, then use `send()` and `read()` to exchange messages. + +## Text and Binary + +WebSocket has two types of messages: text and binary. You can tell them apart by the return value of `read()`. + +```cpp +svr.WebSocket("/ws", [](const httplib::Request &, httplib::ws::WebSocket &ws) { + std::string msg; + httplib::ws::ReadResult ret; + while ((ret = ws.read(msg))) { + if (ret == httplib::ws::Binary) { + ws.send(msg.data(), msg.size()); // Send as binary + } else { + ws.send(msg); // Send as text + } + } +}); +``` + +- `ws.send(const std::string &)` — sends as a text message +- `ws.send(const char *, size_t)` — sends as a binary message + +The client-side API is the same. + +## Accessing Request Information + +You can read HTTP request information from the handshake through the first argument `req` in the handler. This is handy for checking authentication tokens. + +```cpp +svr.WebSocket("/ws", [](const httplib::Request &req, httplib::ws::WebSocket &ws) { + auto token = req.get_header_value("Authorization"); + if (token.empty()) { + ws.close(httplib::ws::CloseStatus::PolicyViolation, "unauthorized"); + return; + } + + std::string msg; + while (ws.read(msg)) { + ws.send(msg); + } +}); +``` + +## Using WSS + +WebSocket over HTTPS (WSS) is also supported. On the server side, just register a WebSocket handler on `httplib::SSLServer`. + +```cpp +httplib::SSLServer svr("cert.pem", "key.pem"); + +svr.WebSocket("/ws", [](const httplib::Request &, httplib::ws::WebSocket &ws) { + std::string msg; + while (ws.read(msg)) { + ws.send(msg); + } +}); + +svr.listen("0.0.0.0", 8443); +``` + +On the client side, use the `wss://` scheme. + +```cpp +httplib::ws::WebSocketClient client("wss://localhost:8443/ws"); +``` + +## Next Steps + +Now you know the basics of WebSocket. This wraps up the Tour. + +The next page gives you a summary of features we didn't cover in the Tour. + +**Next:** [What's Next](../09-whats-next) diff --git a/docs-src/pages/en/tour/09-whats-next.md b/docs-src/pages/en/tour/09-whats-next.md new file mode 100644 index 0000000..d5003d0 --- /dev/null +++ b/docs-src/pages/en/tour/09-whats-next.md @@ -0,0 +1,228 @@ +--- +title: "What's Next" +order: 9 +--- + +Great job finishing the Tour! You now have a solid grasp of the cpp-httplib basics. But there's a lot more to explore. Here's a quick overview of features we didn't cover in the Tour, organized by category. + +## Streaming API + +When you're working with LLM streaming responses or downloading large files, you don't want to load the entire response into memory. Use `stream::Get()` to process data chunk by chunk. + +```cpp +httplib::Client cli("http://localhost:11434"); + +auto result = httplib::stream::Get(cli, "/api/generate"); + +if (result) { + while (result.next()) { + std::cout.write(result.data(), result.size()); + } +} +``` + +You can also pass a `content_receiver` callback to `Get()`. This approach works with Keep-Alive. + +```cpp +httplib::Client cli("http://localhost:8080"); + +cli.Get("/stream", [](const char *data, size_t len) { + std::cout.write(data, len); + return true; +}); +``` + +On the server side, you have `set_content_provider()` and `set_chunked_content_provider()`. Use the former when you know the size, and the latter when you don't. + +```cpp +// With known size (sets Content-Length) +svr.Get("/file", [](const auto &, auto &res) { + auto size = get_file_size("large.bin"); + res.set_content_provider(size, "application/octet-stream", + [](size_t offset, size_t length, httplib::DataSink &sink) { + // Send 'length' bytes starting from 'offset' + return true; + }); +}); + +// Unknown size (Chunked Transfer Encoding) +svr.Get("/stream", [](const auto &, auto &res) { + res.set_chunked_content_provider("text/plain", + [](size_t offset, httplib::DataSink &sink) { + sink.write("chunk\n", 6); + return true; // Return false to finish + }); +}); +``` + +For uploading large files, `make_file_provider()` comes in handy. It streams the file instead of loading it all into memory. + +```cpp +httplib::Client cli("http://localhost:8080"); + +auto res = cli.Post("/upload", {}, { + httplib::make_file_provider("file", "/path/to/large-file.zip") +}); +``` + +## Server-Sent Events (SSE) + +We provide an SSE client as well. It supports automatic reconnection and resuming via `Last-Event-ID`. + +```cpp +httplib::Client cli("http://localhost:8080"); +httplib::sse::SSEClient sse(cli, "/events"); + +sse.on_message([](const httplib::sse::SSEMessage &msg) { + std::cout << msg.event << ": " << msg.data << std::endl; +}); + +sse.start(); // Blocking, with auto-reconnection +``` + +You can also set separate handlers for each event type. + +```cpp +sse.on_event("update", [](const httplib::sse::SSEMessage &msg) { + // Only handles "update" events +}); +``` + +## Authentication + +The client has helpers for Basic auth, Bearer Token auth, and Digest auth. + +```cpp +httplib::Client cli("https://api.example.com"); +cli.set_basic_auth("user", "password"); +cli.set_bearer_token_auth("my-token"); +``` + +## Compression + +We support compression and decompression with gzip, Brotli, and Zstandard. Define the corresponding macro when you compile. + +| Method | Macro | +| -- | -- | +| gzip | `CPPHTTPLIB_ZLIB_SUPPORT` | +| Brotli | `CPPHTTPLIB_BROTLI_SUPPORT` | +| Zstandard | `CPPHTTPLIB_ZSTD_SUPPORT` | + +```cpp +httplib::Client cli("https://example.com"); +cli.set_compress(true); // Compress request body +cli.set_decompress(true); // Decompress response body +``` + +## Proxy + +You can connect through an HTTP proxy. + +```cpp +httplib::Client cli("https://example.com"); +cli.set_proxy("proxy.example.com", 8080); +cli.set_proxy_basic_auth("user", "password"); +``` + +## Timeouts + +You can set connection, read, and write timeouts individually. + +```cpp +httplib::Client cli("https://example.com"); +cli.set_connection_timeout(5, 0); // 5 seconds +cli.set_read_timeout(10, 0); // 10 seconds +cli.set_write_timeout(10, 0); // 10 seconds +``` + +## Keep-Alive + +If you're making multiple requests to the same server, enable Keep-Alive. It reuses the TCP connection, which is much more efficient. + +```cpp +httplib::Client cli("https://example.com"); +cli.set_keep_alive(true); +``` + +## Server Middleware + +You can hook into request processing before and after handlers run. + +```cpp +svr.set_pre_routing_handler([](const auto &req, auto &res) { + // Runs before every request + return httplib::Server::HandlerResponse::Unhandled; // Continue to normal routing +}); + +svr.set_post_routing_handler([](const auto &req, auto &res) { + // Runs after the response is sent + res.set_header("X-Server", "cpp-httplib"); +}); +``` + +Use `req.user_data` to pass data from middleware to handlers. This is useful for sharing things like decoded auth tokens. + +```cpp +svr.set_pre_routing_handler([](const auto &req, auto &res) { + req.user_data["auth_user"] = std::string("alice"); + return httplib::Server::HandlerResponse::Unhandled; +}); + +svr.Get("/me", [](const auto &req, auto &res) { + auto user = std::any_cast(req.user_data.at("auth_user")); + res.set_content("Hello, " + user, "text/plain"); +}); +``` + +You can also customize error and exception handlers. + +```cpp +svr.set_error_handler([](const auto &req, auto &res) { + res.set_content("Custom Error Page", "text/html"); +}); + +svr.set_exception_handler([](const auto &req, auto &res, std::exception_ptr ep) { + res.status = 500; + res.set_content("Internal Server Error", "text/plain"); +}); +``` + +## Logging + +You can set a logger on both the server and the client. + +```cpp +svr.set_logger([](const auto &req, const auto &res) { + std::cout << req.method << " " << req.path << " " << res.status << std::endl; +}); +``` + +## Unix Domain Socket + +In addition to TCP, we support Unix Domain Sockets. You can use them for inter-process communication on the same machine. + +```cpp +// Server +httplib::Server svr; +svr.set_address_family(AF_UNIX); +svr.listen("/tmp/httplib.sock", 0); +``` + +```cpp +// Client +httplib::Client cli("http://localhost"); +cli.set_address_family(AF_UNIX); +cli.set_hostname_addr_map({{"localhost", "/tmp/httplib.sock"}}); + +auto res = cli.Get("/"); +``` + +## Learn More + +Want to dig deeper? Check out these resources. + +- Cookbook — A collection of recipes for common use cases +- [README](https://github.com/yhirose/cpp-httplib/blob/master/README.md) — Full API reference +- [README-sse](https://github.com/yhirose/cpp-httplib/blob/master/README-sse.md) — How to use Server-Sent Events +- [README-stream](https://github.com/yhirose/cpp-httplib/blob/master/README-stream.md) — How to use the Streaming API +- [README-websocket](https://github.com/yhirose/cpp-httplib/blob/master/README-websocket.md) — How to use the WebSocket server diff --git a/docs-src/pages/en/tour/index.md b/docs-src/pages/en/tour/index.md new file mode 100644 index 0000000..572834b --- /dev/null +++ b/docs-src/pages/en/tour/index.md @@ -0,0 +1,16 @@ +--- +title: "A Tour of cpp-httplib" +order: 1 +--- + +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. [Getting Started](01-getting-started) — Get httplib.h and build a Hello World server +2. [Basic Client](02-basic-client) — Send GET/POST requests and use path parameters +3. [Basic Server](03-basic-server) — Routing, path parameters, and building responses +4. [Static File Server](04-static-file-server) — Serve static files +5. [TLS Setup](05-tls-setup) — Set up OpenSSL / mbedTLS +6. [HTTPS Client](06-https-client) — Make requests to HTTPS sites +7. [HTTPS Server](07-https-server) — Build an HTTPS server +8. [WebSocket](08-websocket) — Learn the basics of WebSocket communication +9. [What's Next](09-whats-next) — Explore more features diff --git a/docs-src/pages/ja/cookbook/index.md b/docs-src/pages/ja/cookbook/index.md new file mode 100644 index 0000000..700d6ff --- /dev/null +++ b/docs-src/pages/ja/cookbook/index.md @@ -0,0 +1,8 @@ +--- +title: "Cookbook" +order: 1 +--- + +This section is under construction. + +Check back soon for a collection of recipes organized by topic. diff --git a/docs-src/pages/ja/index.md b/docs-src/pages/ja/index.md new file mode 100644 index 0000000..b301f2f --- /dev/null +++ b/docs-src/pages/ja/index.md @@ -0,0 +1,21 @@ +--- +title: "cpp-httplib" +order: 0 +--- + +[cpp-httplib](https://github.com/yhirose/cpp-httplib)は、C++用のHTTP/HTTPSライブラリです。[`httplib.h`](https://github.com/yhirose/cpp-httplib/raw/refs/tags/latest/httplib.h) というヘッダーファイルを1枚コピーするだけで使えます。 + +C++でちょっとしたHTTPサーバーやクライアントが必要になったとき、すぐに動くものが欲しいですよね。cpp-httplibはまさにそのために作られました。サーバーもクライアントも、数行のコードで書き始められます。 + +APIはラムダ式をベースにした直感的な設計で、C++11以降のコンパイラーがあればどこでも動きます。Windows、macOS、Linux — お使いの環境をそのまま使えます。 + +HTTPSも使えます。OpenSSLやmbedTLSをリンクするだけで、サーバー・クライアントの両方がTLSに対応します。Content-Encoding(gzip, brotli等)、ファイルアップロードなど、実際の開発で必要になる機能もひと通り揃っています。WebSocketもサポートしています。 + +内部的にはブロッキングI/Oとスレッドプールを使っています。大量の同時接続を捌くような用途には向きませんが、APIサーバーやツールの組み込みHTTP、テスト用のモックサーバーなど、多くのユースケースで十分な性能を発揮します。 + +「今日の課題を、今日中に解決する」— cpp-httplibが目指しているのは、そういうシンプルさです。 + +## ドキュメント + +- [A Tour of cpp-httplib](tour/) — 基本を順を追って学べるチュートリアル。初めての方はここから +- [Cookbook](cookbook/) — 目的別のレシピ集。必要なトピックから読めます diff --git a/docs-src/pages/ja/tour/01-getting-started.md b/docs-src/pages/ja/tour/01-getting-started.md new file mode 100644 index 0000000..1ebf501 --- /dev/null +++ b/docs-src/pages/ja/tour/01-getting-started.md @@ -0,0 +1,88 @@ +--- +title: "Getting Started" +order: 1 +--- + +cpp-httplibを始めるのに必要なのは、`httplib.h`とC++コンパイラーだけです。ファイルをダウンロードして、Hello Worldサーバーを動かすところまでやってみましょう。 + +## httplib.h の入手 + +GitHubから直接ダウンロードできます。常に最新版を使ってください。 + +```sh +curl -LO https://github.com/yhirose/cpp-httplib/raw/refs/tags/latest/httplib.h +``` + +ダウンロードした `httplib.h` をプロジェクトのディレクトリに置けば、準備完了です。 + +## コンパイラーの準備 + +| OS | 開発環境 | セットアップ | +| -- | -------- | ------------ | +| macOS | Apple Clang | Xcode Command Line Tools (`xcode-select --install`) | +| Ubuntu | clang++ または g++ | `apt install clang` または `apt install g++` | +| Windows | MSVC | Visual Studio 2022 以降(C++ コンポーネントを含めてインストール) | + +## Hello World サーバー + +次のコードを `server.cpp` として保存しましょう。 + +```cpp +#include "httplib.h" + +int main() { + httplib::Server svr; + + svr.Get("/", [](const httplib::Request&, httplib::Response& res) { + res.set_content("Hello, World!", "text/plain"); + }); + + svr.listen("0.0.0.0", 8080); +} +``` + +たった数行で、HTTPリクエストに応答するサーバーが書けます。 + +## コンパイルと実行 + +このチュートリアルのサンプルコードは、コードを簡潔に書けるC++17で書いています。cpp-httplib自体はC++11でもコンパイルできます。 + +```sh +# macOS +clang++ -std=c++17 -o server server.cpp + +# Linux +# `-pthread`: cpp-httplibは内部でスレッドを使用 +clang++ -std=c++17 -pthread -o server server.cpp + +# Windows (Developer Command Prompt) +# `/EHsc`: C++例外処理を有効化 +cl /EHsc /std:c++17 server.cpp +``` + +コンパイルできたら実行します。 + +```sh +# macOS / Linux +./server + +# Windows +server.exe +``` + +ブラウザで `http://localhost:8080` を開いてください。"Hello, World!" と表示されれば成功です。 + +`curl` でも確認できます。 + +```sh +curl http://localhost:8080/ +# Hello, World! +``` + +サーバーを停止するには、ターミナルで `Ctrl+C` を押します。 + +## 次のステップ + +サーバーの基本がわかりましたね。次は、クライアント側を見てみましょう。cpp-httplibはHTTPクライアント機能も備えています。 + +**次:** [Basic Client](../02-basic-client) diff --git a/docs-src/pages/ja/tour/02-basic-client.md b/docs-src/pages/ja/tour/02-basic-client.md new file mode 100644 index 0000000..f262b08 --- /dev/null +++ b/docs-src/pages/ja/tour/02-basic-client.md @@ -0,0 +1,266 @@ +--- +title: "Basic Client" +order: 2 +--- + +cpp-httplibはサーバーだけでなく、HTTPクライアント機能も備えています。`httplib::Client` を使って、GETやPOSTリクエストを送ってみましょう。 + +## テスト用サーバーの準備 + +クライアントの動作を確認するために、リクエストを受け付けるサーバーを用意します。次のコードを保存し、前章と同じ手順でコンパイル・実行してください。サーバーの詳しい解説は次章で行います。 + +```cpp +#include "httplib.h" +#include + +int main() { + httplib::Server svr; + + svr.Get("/hi", [](const auto &, auto &res) { + res.set_content("Hello!", "text/plain"); + }); + + svr.Get("/search", [](const auto &req, auto &res) { + auto q = req.get_param_value("q"); + res.set_content("Query: " + q, "text/plain"); + }); + + svr.Post("/post", [](const auto &req, auto &res) { + res.set_content(req.body, "text/plain"); + }); + + svr.Post("/submit", [](const auto &req, auto &res) { + std::string result; + for (auto &[key, val] : req.params) { + result += key + " = " + val + "\n"; + } + res.set_content(result, "text/plain"); + }); + + svr.Post("/upload", [](const auto &req, auto &res) { + auto f = req.form.get_file("file"); + auto content = f.filename + " (" + std::to_string(f.content.size()) + " bytes)"; + res.set_content(content, "text/plain"); + }); + + svr.Get("/users/:id", [](const auto &req, auto &res) { + auto id = req.path_params.at("id"); + res.set_content("User ID: " + id, "text/plain"); + }); + + svr.Get(R"(/files/(\d+))", [](const auto &req, auto &res) { + auto id = req.matches[1]; + res.set_content("File ID: " + std::string(id), "text/plain"); + }); + + std::cout << "Listening on port 8080..." << std::endl; + svr.listen("0.0.0.0", 8080); +} +``` + +## GETリクエスト + +サーバーが起動したら、別のターミナルを開いて試してみましょう。まず、最もシンプルなGETリクエストです。 + +```cpp +#include "httplib.h" +#include + +int main() { + httplib::Client cli("http://localhost:8080"); + + auto res = cli.Get("/hi"); + if (res) { + std::cout << res->status << std::endl; // 200 + std::cout << res->body << std::endl; // Hello! + } +} +``` + +`httplib::Client` のコンストラクターにサーバーのアドレスを渡し、`Get()` でリクエストを送ります。戻り値の `res` からステータスコードやボディを取得できます。 + +対応する `curl` コマンドはこうなります。 + +```sh +curl http://localhost:8080/hi +# Hello! +``` + +## レスポンスの確認 + +レスポンスには、ステータスコードとボディ以外にもヘッダー情報が含まれています。 + +```cpp +auto res = cli.Get("/hi"); +if (res) { + // ステータスコード + std::cout << res->status << std::endl; // 200 + + // ボディ + std::cout << res->body << std::endl; // Hello! + + // ヘッダー + std::cout << res->get_header_value("Content-Type") << std::endl; // text/plain +} +``` + +`res->body` は `std::string` なので、JSON レスポンスをパースしたい場合は [nlohmann/json](https://github.com/nlohmann/json) などの JSON ライブラリにそのまま渡せます。 + +## クエリパラメーター + +GETリクエストにクエリパラメーターを付けるには、URLに直接書くか、`httplib::Params` を使います。 + +```cpp +auto res = cli.Get("/search", httplib::Params{{"q", "cpp-httplib"}}); +if (res) { + std::cout << res->body << std::endl; // Query: cpp-httplib +} +``` + +`httplib::Params` を使うと、特殊文字のURLエンコードを自動で行ってくれます。 + +```sh +curl "http://localhost:8080/search?q=cpp-httplib" +# Query: cpp-httplib +``` + +## パスパラメーター + +URLのパスに値を直接埋め込む場合も、クライアント側は特別なAPIは不要です。パスをそのまま `Get()` に渡すだけです。 + +```cpp +auto res = cli.Get("/users/42"); +if (res) { + std::cout << res->body << std::endl; // User ID: 42 +} +``` + +```sh +curl http://localhost:8080/users/42 +# User ID: 42 +``` + +テスト用サーバーには、正規表現でIDを数字のみに絞った `/files/(\d+)` もあります。 + +```cpp +auto res = cli.Get("/files/42"); +if (res) { + std::cout << res->body << std::endl; // File ID: 42 +} +``` + +```sh +curl http://localhost:8080/files/42 +# File ID: 42 +``` + +`/files/abc` のように数字以外を渡すと404が返ります。仕組みは次章で解説します。 + +## リクエストヘッダー + +カスタムHTTPヘッダーを付けるには、`httplib::Headers` を渡します。`Get()` や `Post()` のどちらでも使えます。 + +```cpp +auto res = cli.Get("/hi", httplib::Headers{ + {"Authorization", "Bearer my-token"} +}); +``` + +```sh +curl -H "Authorization: Bearer my-token" http://localhost:8080/hi +``` + +## POSTリクエスト + +テキストデータをPOSTしてみましょう。`Post()` の第2引数にボディ、第3引数にContent-Typeを指定します。 + +```cpp +auto res = cli.Post("/post", "Hello, Server!", "text/plain"); +if (res) { + std::cout << res->status << std::endl; // 200 + std::cout << res->body << std::endl; // Hello, Server! +} +``` + +テスト用サーバーの `/post` はボディをそのまま返すので、送った文字列がそのまま返ってきます。 + +```sh +curl -X POST -H "Content-Type: text/plain" -d "Hello, Server!" http://localhost:8080/post +# Hello, Server! +``` + +## フォームデータの送信 + +HTMLフォームのように、キーと値のペアを送ることもできます。`httplib::Params` を使います。 + +```cpp +auto res = cli.Post("/submit", httplib::Params{ + {"name", "Alice"}, + {"age", "30"} +}); +if (res) { + std::cout << res->body << std::endl; + // age = 30 + // name = Alice +} +``` + +これは `application/x-www-form-urlencoded` 形式で送信されます。 + +```sh +curl -X POST -d "name=Alice&age=30" http://localhost:8080/submit +``` + +## ファイルのPOST + +ファイルをアップロードするには、`httplib::UploadFormDataItems` を使ってマルチパートフォームデータとして送信します。 + +```cpp +auto res = cli.Post("/upload", httplib::UploadFormDataItems{ + {"file", "Hello, File!", "hello.txt", "text/plain"} +}); +if (res) { + std::cout << res->body << std::endl; // hello.txt (12 bytes) +} +``` + +`UploadFormDataItems` の各要素は `{name, content, filename, content_type}` の4つのフィールドで構成されます。 + +```sh +curl -F "file=Hello, File!;filename=hello.txt;type=text/plain" http://localhost:8080/upload +``` + +## エラーハンドリング + +ネットワーク通信では、サーバーに接続できない場合があります。`res` が有効かどうかを必ず確認しましょう。 + +```cpp +httplib::Client cli("http://localhost:9999"); // 存在しないポート +auto res = cli.Get("/hi"); + +if (!res) { + // 接続エラー + std::cout << "Error: " << httplib::to_string(res.error()) << std::endl; + // Error: Connection + return 1; +} + +// ここに到達すればレスポンスを受信できている +if (res->status != 200) { + std::cout << "HTTP Error: " << res->status << std::endl; + return 1; +} + +std::cout << res->body << std::endl; +``` + +エラーには2つのレベルがあります。 + +- **接続エラー**: サーバーに到達できなかった場合。`res` が偽になり、`res.error()` でエラーの種類を取得できます +- **HTTPエラー**: サーバーからエラーステータス(404、500など)が返ってきた場合。`res` は真ですが、`res->status` を確認する必要があります + +## 次のステップ + +クライアントからリクエストを送る方法がわかりました。次は、サーバー側をもっと詳しく見てみましょう。ルーティングやパスパラメータなど、サーバーの機能を掘り下げます。 + +**次:** [Basic Server](../03-basic-server) diff --git a/docs-src/pages/ja/tour/03-basic-server.md b/docs-src/pages/ja/tour/03-basic-server.md new file mode 100644 index 0000000..0b31f53 --- /dev/null +++ b/docs-src/pages/ja/tour/03-basic-server.md @@ -0,0 +1,280 @@ +--- +title: "Basic Server" +order: 3 +--- + +前章ではクライアントからリクエストを送りました。そのとき、テスト用サーバーを用意しましたね。この章では、あのサーバーの仕組みをひとつずつ紐解いていきます。 + +## サーバーの起動 + +ルーティングを登録したら、最後に `svr.listen()` を呼んでサーバーを起動します。 + +```cpp +svr.listen("0.0.0.0", 8080); +``` + +第1引数はホスト、第2引数はポート番号です。`"0.0.0.0"` を指定すると、すべてのネットワークインターフェースでリクエストを受け付けます。自分のマシンからのアクセスだけに限定したいときは `"127.0.0.1"` を使います。 + +`listen()` はブロッキング呼び出しです。サーバーが停止するまで、この行から先には進みません。ターミナルで `Ctrl+C` を押すか、別スレッドから `svr.stop()` を呼ぶまでサーバーは動き続けます。 + +## ルーティング + +サーバーの核になるのは「ルーティング」です。どのURLに、どのHTTPメソッドでアクセスされたら、何をするか。それを登録する仕組みです。 + +```cpp +httplib::Server svr; + +svr.Get("/hi", [](const httplib::Request &req, httplib::Response &res) { + res.set_content("Hello!", "text/plain"); +}); +``` + +`svr.Get()` は、GETリクエストに対するハンドラーを登録します。第1引数がパス、第2引数がハンドラー関数です。`/hi` にGETリクエストが来たら、このラムダが呼ばれます。 + +HTTPメソッドごとにメソッドが用意されています。 + +```cpp +svr.Get("/path", handler); // GET +svr.Post("/path", handler); // POST +svr.Put("/path", handler); // PUT +svr.Delete("/path", handler); // DELETE +``` + +ハンドラーのシグネチャは `(const httplib::Request &req, httplib::Response &res)` です。`auto` を使って短く書くこともできます。 + +```cpp +svr.Get("/hi", [](const auto &req, auto &res) { + res.set_content("Hello!", "text/plain"); +}); +``` + +パスが一致したときだけハンドラーが呼ばれます。登録されていないパスにアクセスすると、自動的に404が返ります。 + +## リクエストオブジェクト + +ハンドラーの第1引数 `req` から、クライアントが送ってきた情報を読み取れます。 + +### ボディ + +`req.body` でリクエストボディを取得できます。型は `std::string` です。 + +```cpp +svr.Post("/post", [](const auto &req, auto &res) { + // クライアントが送ったボディをそのまま返す + res.set_content(req.body, "text/plain"); +}); +``` + +### ヘッダー + +`req.get_header_value()` でリクエストヘッダーの値を取得できます。 + +```cpp +svr.Get("/check", [](const auto &req, auto &res) { + auto auth = req.get_header_value("Authorization"); + res.set_content("Auth: " + auth, "text/plain"); +}); +``` + +### クエリパラメーターとフォームデータ + +`req.get_param_value()` でパラメーターを取得できます。GETのクエリパラメーターと、POSTのフォームデータの両方に使えます。 + +```cpp +svr.Get("/search", [](const auto &req, auto &res) { + auto q = req.get_param_value("q"); + res.set_content("Query: " + q, "text/plain"); +}); +``` + +`/search?q=cpp-httplib` にアクセスすると、`q` の値は `"cpp-httplib"` になります。 + +すべてのパラメーターをループで処理したいときは、`req.params` を使います。 + +```cpp +svr.Post("/submit", [](const auto &req, auto &res) { + std::string result; + for (auto &[key, val] : req.params) { + result += key + " = " + val + "\n"; + } + res.set_content(result, "text/plain"); +}); +``` + +### ファイルアップロード + +マルチパートフォームでアップロードされたファイルは、`req.form.get_file()` で取得します。 + +```cpp +svr.Post("/upload", [](const auto &req, auto &res) { + auto f = req.form.get_file("file"); + auto content = f.filename + " (" + std::to_string(f.content.size()) + " bytes)"; + res.set_content(content, "text/plain"); +}); +``` + +`f.filename` でファイル名、`f.content` でファイルの中身にアクセスできます。 + +## パスパラメーター + +URLの一部を変数として受け取りたいことがあります。たとえば `/users/42` の `42` を取得したい場合です。`:param` 記法を使うと、URLの一部をキャプチャできます。 + +```cpp +svr.Get("/users/:id", [](const auto &req, auto &res) { + auto id = req.path_params.at("id"); + res.set_content("User ID: " + id, "text/plain"); +}); +``` + +`/users/42` にアクセスすると、`req.path_params.at("id")` は `"42"` を返します。`/users/100` なら `"100"` です。 + +複数のパスパラメーターも使えます。 + +```cpp +svr.Get("/users/:user_id/posts/:post_id", [](const auto &req, auto &res) { + auto user_id = req.path_params.at("user_id"); + auto post_id = req.path_params.at("post_id"); + res.set_content("User: " + user_id + ", Post: " + post_id, "text/plain"); +}); +``` + +### 正規表現パターン + +`:param` の代わりに正規表現をパスに書くこともできます。キャプチャグループの値は `req.matches` で取得します。型は `std::smatch` です。 + +```cpp +// 数字のみのIDを受け付ける +svr.Get(R"(/files/(\d+))", [](const auto &req, auto &res) { + auto id = req.matches[1]; // 最初のキャプチャグループ + res.set_content("File ID: " + std::string(id), "text/plain"); +}); +``` + +`/files/42` にはマッチしますが、`/files/abc` にはマッチしません。入力値を絞り込みたいときに便利です。 + +## レスポンスの組み立て + +ハンドラーの第2引数 `res` を使って、クライアントに返すレスポンスを組み立てます。 + +### ボディとContent-Type + +`res.set_content()` でボディとContent-Typeを設定します。これだけでステータスコード200のレスポンスが返ります。 + +```cpp +svr.Get("/hi", [](const auto &req, auto &res) { + res.set_content("Hello!", "text/plain"); +}); +``` + +### ステータスコード + +ステータスコードを変えたいときは、`res.status` に代入します。 + +```cpp +svr.Get("/not-found", [](const auto &req, auto &res) { + res.status = 404; + res.set_content("Not found", "text/plain"); +}); +``` + +### レスポンスヘッダー + +`res.set_header()` でレスポンスヘッダーを追加できます。 + +```cpp +svr.Get("/with-header", [](const auto &req, auto &res) { + res.set_header("X-Custom", "my-value"); + res.set_content("Hello!", "text/plain"); +}); +``` + +## 前章のサーバーを読み解く + +ここまでの知識を使って、前章で用意したテスト用サーバーを改めて見てみましょう。 + +### GET /hi + +```cpp +svr.Get("/hi", [](const auto &, auto &res) { + res.set_content("Hello!", "text/plain"); +}); +``` + +最もシンプルなハンドラーです。リクエストの情報は使わないので、`req` の変数名を省略しています。`"Hello!"` というテキストをそのまま返します。 + +### GET /search + +```cpp +svr.Get("/search", [](const auto &req, auto &res) { + auto q = req.get_param_value("q"); + res.set_content("Query: " + q, "text/plain"); +}); +``` + +`req.get_param_value("q")` でクエリパラメーター `q` の値を取り出します。`/search?q=cpp-httplib` なら、レスポンスは `"Query: cpp-httplib"` になります。 + +### POST /post + +```cpp +svr.Post("/post", [](const auto &req, auto &res) { + res.set_content(req.body, "text/plain"); +}); +``` + +クライアントが送ったリクエストボディを、そのままレスポンスとして返すエコーサーバーです。`req.body` にボディが丸ごと入っています。 + +### POST /submit + +```cpp +svr.Post("/submit", [](const auto &req, auto &res) { + std::string result; + for (auto &[key, val] : req.params) { + result += key + " = " + val + "\n"; + } + res.set_content(result, "text/plain"); +}); +``` + +フォームデータとして送られたキーと値のペアを、`req.params` でループ処理しています。構造化束縛 `auto &[key, val]` を使って、各ペアを取り出しています。 + +### POST /upload + +```cpp +svr.Post("/upload", [](const auto &req, auto &res) { + auto f = req.form.get_file("file"); + auto content = f.filename + " (" + std::to_string(f.content.size()) + " bytes)"; + res.set_content(content, "text/plain"); +}); +``` + +マルチパートフォームで送られたファイルを受け取ります。`req.form.get_file("file")` で `"file"` という名前のフィールドを取得し、`f.filename` と `f.content.size()` でファイル名とサイズを返しています。 + +### GET /users/:id + +```cpp +svr.Get("/users/:id", [](const auto &req, auto &res) { + auto id = req.path_params.at("id"); + res.set_content("User ID: " + id, "text/plain"); +}); +``` + +`:id` の部分がパスパラメーターです。`req.path_params.at("id")` で値を取り出しています。`/users/42` なら `"42"`、`/users/alice` なら `"alice"` が得られます。 + +### GET /files/(\d+) + +```cpp +svr.Get(R"(/files/(\d+))", [](const auto &req, auto &res) { + auto id = req.matches[1]; + res.set_content("File ID: " + std::string(id), "text/plain"); +}); +``` + +正規表現 `(\d+)` で数字だけのIDにマッチします。`/files/42` にはマッチしますが、`/files/abc` は404になります。`req.matches[1]` で最初のキャプチャグループの値を取得しています。 + +## 次のステップ + +サーバーの基本がわかりましたね。ルーティング、リクエストの読み取り、レスポンスの組み立て。これだけで、十分に実用的なAPIサーバーが作れます。 + +次は、静的ファイルの配信を見てみましょう。HTMLやCSSを配信するサーバーを作ります。 + +**次:** [Static File Server](../04-static-file-server) diff --git a/docs-src/pages/ja/tour/04-static-file-server.md b/docs-src/pages/ja/tour/04-static-file-server.md new file mode 100644 index 0000000..95ac897 --- /dev/null +++ b/docs-src/pages/ja/tour/04-static-file-server.md @@ -0,0 +1,134 @@ +--- +title: "Static File Server" +order: 4 +--- + +cpp-httplibは、HTMLやCSS、画像ファイルなどの静的ファイルも配信できます。面倒な設定は要りません。`set_mount_point()` を1行呼ぶだけです。 + +## set_mount_point の基本 + +さっそくやってみましょう。`set_mount_point()` は、URLのパスとローカルディレクトリを紐づけます。 + +```cpp +#include "httplib.h" +#include + +int main() { + httplib::Server svr; + + svr.set_mount_point("/", "./html"); + + std::cout << "Listening on port 8080..." << std::endl; + svr.listen("0.0.0.0", 8080); +} +``` + +第1引数がURLのマウントポイント、第2引数がローカルのディレクトリパスです。この例だと、`/` へのリクエストを `./html` ディレクトリから配信します。 + +試してみましょう。まず `html` ディレクトリを作って、`index.html` を置きます。 + +```sh +mkdir html +``` + +```html + + +My Page + +

Hello from cpp-httplib!

+

This is a static file.

+ + +``` + +コンパイルして起動します。 + +```sh +g++ -std=c++17 -o server server.cpp -pthread +./server +``` + +ブラウザで `http://localhost:8080` を開いてみてください。`html/index.html` の内容が表示されるはずです。`http://localhost:8080/index.html` でも同じページが返ります。 + +もちろん、前章のクライアントコードや `curl` でもアクセスできますよ。 + +```cpp +httplib::Client cli("http://localhost:8080"); +auto res = cli.Get("/"); +if (res) { + std::cout << res->body << std::endl; // HTMLが表示される +} +``` + +```sh +curl http://localhost:8080 +``` + +## 複数のマウントポイント + +`set_mount_point()` は何回でも呼べます。URLのパスごとに、別々のディレクトリを割り当てられます。 + +```cpp +svr.set_mount_point("/", "./public"); +svr.set_mount_point("/assets", "./static/assets"); +svr.set_mount_point("/docs", "./documentation"); +``` + +`/assets/style.css` なら `./static/assets/style.css` を、`/docs/guide.html` なら `./documentation/guide.html` を配信します。 + +## ハンドラーとの組み合わせ + +静的ファイルの配信と、前章で学んだルーティングハンドラーは共存できます。 + +```cpp +httplib::Server svr; + +// APIエンドポイント +svr.Get("/api/hello", [](const auto &, auto &res) { + res.set_content(R"({"message":"Hello!"})", "application/json"); +}); + +// 静的ファイル配信 +svr.set_mount_point("/", "./public"); + +svr.listen("0.0.0.0", 8080); +``` + +ハンドラーが先に評価されます。`/api/hello` にはハンドラーが応答し、それ以外のパスは `./public` ディレクトリからファイルを探します。 + +## レスポンスヘッダーの追加 + +`set_mount_point()` の第3引数にヘッダーを渡すと、静的ファイルのレスポンスにカスタムヘッダーを付けられます。キャッシュ制御に便利です。 + +```cpp +svr.set_mount_point("/", "./public", { + {"Cache-Control", "max-age=3600"} +}); +``` + +こうすると、ブラウザは配信されたファイルを1時間キャッシュします。 + +## 静的ファイルサーバー用のDockerファイル + +cpp-httplibのリポジトリには、静的ファイルサーバー用の `Dockerfile` が含まれています。Docker Hubにビルド済みイメージも公開しているので、1コマンドで起動できます。 + +```sh +> docker run -p 8080:80 -v ./my-site:/html yhirose4dockerhub/cpp-httplib-server +Serving HTTP on 0.0.0.0:80 +Mount point: / -> ./html +Press Ctrl+C to shutdown gracefully... +192.168.65.1 - - [22/Feb/2026:12:00:00 +0000] "GET / HTTP/1.1" 200 256 "-" "Mozilla/5.0 ..." +192.168.65.1 - - [22/Feb/2026:12:00:00 +0000] "GET /style.css HTTP/1.1" 200 1024 "-" "Mozilla/5.0 ..." +192.168.65.1 - - [22/Feb/2026:12:00:01 +0000] "GET /favicon.ico HTTP/1.1" 404 152 "-" "Mozilla/5.0 ..." +``` + +`./my-site` ディレクトリの中身が、そのままポート8080で配信されます。NGINXと同じログ形式で、アクセスの様子を確認できますよ。 + +## 次のステップ + +静的ファイルの配信ができるようになりましたね。HTMLやCSS、JavaScriptを配信するWebサーバーが、これだけのコードで作れます。 + +次は、HTTPSで暗号化通信をしてみましょう。まずはTLSライブラリのセットアップからです。 + +**次:** [TLS Setup](../05-tls-setup) diff --git a/docs-src/pages/ja/tour/05-tls-setup.md b/docs-src/pages/ja/tour/05-tls-setup.md new file mode 100644 index 0000000..ab6b8d1 --- /dev/null +++ b/docs-src/pages/ja/tour/05-tls-setup.md @@ -0,0 +1,88 @@ +--- +title: "TLS Setup" +order: 5 +--- + +ここまではHTTP(平文)でやってきましたが、実際のWebではHTTPS(暗号化通信)が当たり前ですよね。cpp-httplibでHTTPSを使うには、TLSライブラリが必要です。 + +このTourではOpenSSLを使います。最も広く使われていて、情報も豊富です。 + +## OpenSSLのインストール + +お使いのOSに合わせてインストールしましょう。 + +| OS | インストール方法 | +| -- | ---------------- | +| macOS | [Homebrew](https://brew.sh/) (`brew install openssl`) | +| Ubuntu / Debian | `sudo apt install libssl-dev` | +| Windows | [vcpkg](https://vcpkg.io/) (`vcpkg install openssl`) | + +## コンパイルオプション + +TLS機能を有効にするには、`CPPHTTPLIB_OPENSSL_SUPPORT` マクロを定義してコンパイルします。前章までのコンパイルコマンドに、いくつかオプションが増えます。 + +```sh +# macOS (Homebrew) +clang++ -std=c++17 -DCPPHTTPLIB_OPENSSL_SUPPORT \ + -I$(brew --prefix openssl)/include \ + -L$(brew --prefix openssl)/lib \ + -lssl -lcrypto \ + -framework CoreFoundation -framework Security \ + -o server server.cpp + +# Linux +clang++ -std=c++17 -pthread -DCPPHTTPLIB_OPENSSL_SUPPORT \ + -lssl -lcrypto \ + -o server server.cpp + +# Windows (Developer Command Prompt) +cl /EHsc /std:c++17 /DCPPHTTPLIB_OPENSSL_SUPPORT server.cpp libssl.lib libcrypto.lib +``` + +それぞれのオプションの役割を見てみましょう。 + +- **`-DCPPHTTPLIB_OPENSSL_SUPPORT`** — TLS機能を有効にするマクロ定義 +- **`-lssl -lcrypto`** — OpenSSLのライブラリをリンク +- **`-I` / `-L`**(macOSのみ)— Homebrew版OpenSSLのパスを指定 +- **`-framework CoreFoundation -framework Security`**(macOSのみ)— Keychainからシステム証明書を自動で読み込むために必要です + +## 動作確認 + +ちゃんと動くか確認してみましょう。`httplib::Client` にHTTPSのURLを渡してアクセスするだけのプログラムです。 + +```cpp +#define CPPHTTPLIB_OPENSSL_SUPPORT +#include "httplib.h" +#include + +int main() { + httplib::Client cli("https://www.google.com"); + + auto res = cli.Get("/"); + if (res) { + std::cout << "Status: " << res->status << std::endl; + } else { + std::cout << "Error: " << httplib::to_string(res.error()) << std::endl; + } +} +``` + +コンパイルして実行してみてください。`Status: 200` と表示されれば、セットアップ完了です。 + +## 他のTLSバックエンド + +cpp-httplibはOpenSSL以外にも、Mbed TLSとwolfSSLに対応しています。マクロ定義とリンクするライブラリを変えるだけで切り替えられます。 + +| バックエンド | マクロ定義 | リンクするライブラリ | +| :--- | :--- | :--- | +| OpenSSL | `CPPHTTPLIB_OPENSSL_SUPPORT` | `libssl`, `libcrypto` | +| Mbed TLS | `CPPHTTPLIB_MBEDTLS_SUPPORT` | `libmbedtls`, `libmbedx509`, `libmbedcrypto` | +| wolfSSL | `CPPHTTPLIB_WOLFSSL_SUPPORT` | `libwolfssl` | + +このTourではOpenSSLを前提に進めますが、APIはどのバックエンドでも共通です。 + +## 次のステップ + +TLSの準備ができましたね。次は、HTTPSサイトにリクエストを送ってみましょう。 + +**次:** [HTTPS Client](../06-https-client) diff --git a/docs-src/pages/ja/tour/06-https-client.md b/docs-src/pages/ja/tour/06-https-client.md new file mode 100644 index 0000000..36b3306 --- /dev/null +++ b/docs-src/pages/ja/tour/06-https-client.md @@ -0,0 +1,122 @@ +--- +title: "HTTPS Client" +order: 6 +--- + +前章でOpenSSLのセットアップが済んだので、さっそくHTTPSクライアントを使ってみましょう。2章で使った `httplib::Client` がそのまま使えます。コンストラクタに `https://` 付きのURLを渡すだけです。 + +## GETリクエスト + +実在するHTTPSサイトにアクセスしてみましょう。 + +```cpp +#define CPPHTTPLIB_OPENSSL_SUPPORT +#include "httplib.h" +#include + +int main() { + httplib::Client cli("https://nghttp2.org"); + + auto res = cli.Get("/"); + if (res) { + std::cout << res->status << std::endl; // 200 + std::cout << res->body.substr(0, 100) << std::endl; // HTMLの先頭部分 + } else { + std::cout << "Error: " << httplib::to_string(res.error()) << std::endl; + } +} +``` + +2章では `httplib::Client cli("http://localhost:8080")` と書きましたよね。スキームを `https://` に変えるだけです。`Get()` や `Post()` など、2章で学んだAPIはすべてそのまま使えます。 + +```sh +curl https://nghttp2.org/ +``` + +## ポートの指定 + +HTTPSのデフォルトポートは443です。別のポートを使いたい場合は、URLにポートを含めます。 + +```cpp +httplib::Client cli("https://localhost:8443"); +``` + +## CA証明書の検証 + +`httplib::Client` はHTTPS接続時、デフォルトでサーバー証明書を検証します。信頼できるCA(認証局)が発行した証明書を持つサーバーにしか接続しません。 + +CA証明書は、macOSならKeychain、LinuxならシステムのCA証明書ストア、WindowsならWindowsの証明書ストアから自動で読み込みます。ほとんどの場合、追加の設定は要りません。 + +### CA証明書ファイルの指定 + +環境によってはシステムのCA証明書が見つからないこともあります。そのときは `set_ca_cert_path()` でパスを直接指定してください。 + +```cpp +httplib::Client cli("https://nghttp2.org"); +cli.set_ca_cert_path("/etc/ssl/certs/ca-certificates.crt"); + +auto res = cli.Get("/"); +``` + +```sh +curl --cacert /etc/ssl/certs/ca-certificates.crt https://nghttp2.org/ +``` + +### 証明書検証の無効化 + +開発中、自己署名証明書のサーバーに接続したいときは、検証を無効にできます。 + +```cpp +httplib::Client cli("https://localhost:8443"); +cli.enable_server_certificate_verification(false); + +auto res = cli.Get("/"); +``` + +```sh +curl -k https://localhost:8443/ +``` + +本番では絶対に無効にしないでください。中間者攻撃のリスクがあります。 + +## リダイレクトの追跡 + +HTTPSサイトへのアクセスでは、リダイレクトに遭遇することがよくあります。たとえば `http://` から `https://` へ、あるいは `www` なしから `www` ありへ転送されるケースです。 + +デフォルトではリダイレクトを追跡しません。リダイレクト先は `Location` ヘッダーで確認できます。 + +```cpp +httplib::Client cli("https://nghttp2.org"); + +auto res = cli.Get("/httpbin/redirect/3"); +if (res) { + std::cout << res->status << std::endl; // 302 + std::cout << res->get_header_value("Location") << std::endl; +} +``` + +```sh +curl https://nghttp2.org/httpbin/redirect/3 +``` + +`set_follow_location(true)` を設定すると、リダイレクトを自動で追跡して、最終的なレスポンスを返してくれます。 + +```cpp +httplib::Client cli("https://nghttp2.org"); +cli.set_follow_location(true); + +auto res = cli.Get("/httpbin/redirect/3"); +if (res) { + std::cout << res->status << std::endl; // 200(最終的なレスポンス) +} +``` + +```sh +curl -L https://nghttp2.org/httpbin/redirect/3 +``` + +## 次のステップ + +HTTPSクライアントの使い方がわかりましたね。次は自分でHTTPSサーバーを立ててみましょう。自己署名証明書の作り方から始めます。 + +**次:** [HTTPS Server](../07-https-server) diff --git a/docs-src/pages/ja/tour/07-https-server.md b/docs-src/pages/ja/tour/07-https-server.md new file mode 100644 index 0000000..eba9dcf --- /dev/null +++ b/docs-src/pages/ja/tour/07-https-server.md @@ -0,0 +1,124 @@ +--- +title: "HTTPS Server" +order: 7 +--- + +前章ではHTTPSクライアントを使いました。今度は自分でHTTPSサーバーを立ててみましょう。3章の `httplib::Server` を `httplib::SSLServer` に置き換えるだけです。 + +ただし、TLSサーバーにはサーバー証明書と秘密鍵が必要です。まずはそこから準備しましょう。 + +## 自己署名証明書の作成 + +開発やテスト用なら、自己署名証明書(いわゆるオレオレ証明書)で十分です。OpenSSLのコマンドでサクッと作れます。 + +```sh +openssl req -x509 -noenc -keyout key.pem -out cert.pem -subj /CN=localhost +``` + +これで2つのファイルができます。 + +- **`cert.pem`** — サーバー証明書 +- **`key.pem`** — 秘密鍵 + +## 最小のHTTPSサーバー + +証明書ができたら、さっそくサーバーを書いてみましょう。 + +```cpp +#define CPPHTTPLIB_OPENSSL_SUPPORT +#include "httplib.h" +#include + +int main() { + httplib::SSLServer svr("cert.pem", "key.pem"); + + svr.Get("/", [](const auto &, auto &res) { + res.set_content("Hello, HTTPS!", "text/plain"); + }); + + std::cout << "Listening on https://localhost:8443" << std::endl; + svr.listen("0.0.0.0", 8443); +} +``` + +`httplib::SSLServer` のコンストラクタに証明書と秘密鍵のパスを渡すだけです。ルーティングの書き方は3章の `httplib::Server` とまったく同じですよ。 + +コンパイルして起動しましょう。 + +## 動作確認 + +サーバーが起動したら、`curl` でアクセスしてみましょう。自己署名証明書なので、`-k` オプションで証明書検証をスキップします。 + +```sh +curl -k https://localhost:8443/ +# Hello, HTTPS! +``` + +ブラウザで `https://localhost:8443` を開くと、「この接続は安全ではありません」と警告が出ます。自己署名証明書なので正常です。気にせず進めてください。 + +## クライアントからの接続 + +前章の `httplib::Client` で接続してみましょう。自己署名証明書のサーバーに接続するには、2つの方法があります。 + +### 方法1: 証明書検証を無効にする + +開発時の手軽な方法です。 + +```cpp +#define CPPHTTPLIB_OPENSSL_SUPPORT +#include "httplib.h" +#include + +int main() { + httplib::Client cli("https://localhost:8443"); + cli.enable_server_certificate_verification(false); + + auto res = cli.Get("/"); + if (res) { + std::cout << res->body << std::endl; // Hello, HTTPS! + } +} +``` + +### 方法2: 自己署名証明書をCA証明書として指定する + +こちらのほうが安全です。`cert.pem` をCA証明書として信頼するよう指定します。 + +```cpp +#define CPPHTTPLIB_OPENSSL_SUPPORT +#include "httplib.h" +#include + +int main() { + httplib::Client cli("https://localhost:8443"); + cli.set_ca_cert_path("cert.pem"); + + auto res = cli.Get("/"); + if (res) { + std::cout << res->body << std::endl; // Hello, HTTPS! + } +} +``` + +この方法なら、指定した証明書のサーバーにだけ接続を許可して、なりすましを防げます。テスト環境でもなるべくこちらを使いましょう。 + +## Server と SSLServer の比較 + +3章で学んだ `httplib::Server` のAPIは、`httplib::SSLServer` でもそのまま使えます。違いはコンストラクタだけです。 + +| | `httplib::Server` | `httplib::SSLServer` | +| -- | ------------------ | -------------------- | +| コンストラクタ | 引数なし | 証明書と秘密鍵のパス | +| プロトコル | HTTP | HTTPS | +| ポート(慣例) | 8080 | 8443 | +| ルーティング | 共通 | 共通 | + +HTTPサーバーをHTTPSに切り替えるには、コンストラクタを変えるだけです。 + +## 次のステップ + +HTTPSサーバーが動きましたね。これでHTTP/HTTPSのクライアントとサーバー、両方の基本がそろいました。 + +次は、cpp-httplibに新しく加わったWebSocket機能を見てみましょう。 + +**次:** [WebSocket](../08-websocket) diff --git a/docs-src/pages/ja/tour/08-websocket.md b/docs-src/pages/ja/tour/08-websocket.md new file mode 100644 index 0000000..e42bf3d --- /dev/null +++ b/docs-src/pages/ja/tour/08-websocket.md @@ -0,0 +1,139 @@ +--- +title: "WebSocket" +order: 8 +--- + +cpp-httplibはWebSocketにも対応しています。HTTPのリクエスト/レスポンスと違い、WebSocketはサーバーとクライアントが双方向にメッセージをやり取りできます。チャットやリアルタイム通知に便利です。 + +さっそく、エコーサーバーとクライアントを作ってみましょう。 + +## エコーサーバー + +受け取ったメッセージをそのまま返すエコーサーバーです。 + +```cpp +#include "httplib.h" +#include + +int main() { + httplib::Server svr; + + svr.WebSocket("/ws", [](const httplib::Request &, httplib::ws::WebSocket &ws) { + std::string msg; + while (ws.read(msg)) { + ws.send(msg); // 受け取ったメッセージをそのまま返す + } + }); + + std::cout << "Listening on port 8080..." << std::endl; + svr.listen("0.0.0.0", 8080); +} +``` + +`svr.WebSocket()` でWebSocketハンドラーを登録します。3章の `svr.Get()` や `svr.Post()` と同じ感覚ですね。 + +ハンドラーの中では、`ws.read(msg)` でメッセージを待ちます。接続が閉じられると `read()` が `false` を返すので、ループを抜けます。`ws.send(msg)` でメッセージを送り返します。 + +## クライアントからの接続 + +`httplib::ws::WebSocketClient` を使ってサーバーに接続してみましょう。 + +```cpp +#include "httplib.h" +#include + +int main() { + httplib::ws::WebSocketClient client("ws://localhost:8080/ws"); + + if (!client.connect()) { + std::cout << "Connection failed" << std::endl; + return 1; + } + + // メッセージを送信 + client.send("Hello, WebSocket!"); + + // サーバーからの応答を受信 + std::string msg; + if (client.read(msg)) { + std::cout << msg << std::endl; // Hello, WebSocket! + } + + client.close(); +} +``` + +コンストラクタには `ws://host:port/path` 形式のURLを渡します。`connect()` で接続を開始し、`send()` と `read()` でメッセージをやり取りします。 + +## テキストとバイナリ + +WebSocketにはテキストとバイナリの2種類のメッセージがあります。`read()` の戻り値で区別できます。 + +```cpp +svr.WebSocket("/ws", [](const httplib::Request &, httplib::ws::WebSocket &ws) { + std::string msg; + httplib::ws::ReadResult ret; + while ((ret = ws.read(msg))) { + if (ret == httplib::ws::Binary) { + ws.send(msg.data(), msg.size()); // バイナリとして送信 + } else { + ws.send(msg); // テキストとして送信 + } + } +}); +``` + +- `ws.send(const std::string &)` — テキストメッセージとして送信 +- `ws.send(const char *, size_t)` — バイナリメッセージとして送信 + +クライアント側も同じAPIです。 + +## リクエスト情報へのアクセス + +ハンドラーの第1引数 `req` から、ハンドシェイク時のHTTPリクエスト情報を読み取れます。認証トークンの確認などに便利です。 + +```cpp +svr.WebSocket("/ws", [](const httplib::Request &req, httplib::ws::WebSocket &ws) { + auto token = req.get_header_value("Authorization"); + if (token.empty()) { + ws.close(httplib::ws::CloseStatus::PolicyViolation, "unauthorized"); + return; + } + + std::string msg; + while (ws.read(msg)) { + ws.send(msg); + } +}); +``` + +## WSSで使う + +HTTPS上のWebSocket(WSS)にも対応しています。サーバー側は `httplib::SSLServer` にWebSocketハンドラーを登録するだけです。 + +```cpp +httplib::SSLServer svr("cert.pem", "key.pem"); + +svr.WebSocket("/ws", [](const httplib::Request &, httplib::ws::WebSocket &ws) { + std::string msg; + while (ws.read(msg)) { + ws.send(msg); + } +}); + +svr.listen("0.0.0.0", 8443); +``` + +クライアント側は `wss://` スキームを使います。 + +```cpp +httplib::ws::WebSocketClient client("wss://localhost:8443/ws"); +``` + +## 次のステップ + +WebSocketの基本がわかりましたね。ここまでで Tourは終わりです。 + +次のページでは、Tourで取り上げなかった機能をまとめて紹介します。 + +**次:** [What's Next](../09-whats-next) diff --git a/docs-src/pages/ja/tour/09-whats-next.md b/docs-src/pages/ja/tour/09-whats-next.md new file mode 100644 index 0000000..cfbb6da --- /dev/null +++ b/docs-src/pages/ja/tour/09-whats-next.md @@ -0,0 +1,228 @@ +--- +title: "What's Next" +order: 9 +--- + +Tourお疲れさまでした! cpp-httplibの基本はひと通り押さえましたね。でも、まだまだ便利な機能があります。Tourで取り上げなかった機能をカテゴリー別に紹介します。 + +## Streaming API + +LLMのストリーミング応答や大きなファイルのダウンロードでは、レスポンス全体をメモリに載せたくないですよね。`stream::Get()` を使えば、データをチャンクごとに処理できます。 + +```cpp +httplib::Client cli("http://localhost:11434"); + +auto result = httplib::stream::Get(cli, "/api/generate"); + +if (result) { + while (result.next()) { + std::cout.write(result.data(), result.size()); + } +} +``` + +`Get()` に `content_receiver` コールバックを渡す方法もあります。こちらはKeep-Aliveと併用できます。 + +```cpp +httplib::Client cli("http://localhost:8080"); + +cli.Get("/stream", [](const char *data, size_t len) { + std::cout.write(data, len); + return true; +}); +``` + +サーバー側には `set_content_provider()` と `set_chunked_content_provider()` があります。サイズがわかっているなら前者、不明なら後者を使ってください。 + +```cpp +// サイズ指定あり(Content-Length が設定される) +svr.Get("/file", [](const auto &, auto &res) { + auto size = get_file_size("large.bin"); + res.set_content_provider(size, "application/octet-stream", + [](size_t offset, size_t length, httplib::DataSink &sink) { + // offset から length バイト分を送る + return true; + }); +}); + +// サイズ不明(Chunked Transfer Encoding) +svr.Get("/stream", [](const auto &, auto &res) { + res.set_chunked_content_provider("text/plain", + [](size_t offset, httplib::DataSink &sink) { + sink.write("chunk\n", 6); + return true; // falseを返すと終了 + }); +}); +``` + +大きなファイルのアップロードには `make_file_provider()` が便利です。ファイルを全部メモリに読み込まず、ストリーミングで送れます。 + +```cpp +httplib::Client cli("http://localhost:8080"); + +auto res = cli.Post("/upload", {}, { + httplib::make_file_provider("file", "/path/to/large-file.zip") +}); +``` + +## Server-Sent Events (SSE) + +SSEクライアントも用意しています。自動再接続や `Last-Event-ID` による再開にも対応しています。 + +```cpp +httplib::Client cli("http://localhost:8080"); +httplib::sse::SSEClient sse(cli, "/events"); + +sse.on_message([](const httplib::sse::SSEMessage &msg) { + std::cout << msg.event << ": " << msg.data << std::endl; +}); + +sse.start(); // ブロッキング、自動再接続あり +``` + +イベントタイプごとにハンドラーを分けることもできますよ。 + +```cpp +sse.on_event("update", [](const httplib::sse::SSEMessage &msg) { + // "update" イベントだけ処理 +}); +``` + +## 認証 + +クライアントにはBasic認証、Bearer Token認証、Digest認証のヘルパーを用意しています。 + +```cpp +httplib::Client cli("https://api.example.com"); +cli.set_basic_auth("user", "password"); +cli.set_bearer_token_auth("my-token"); +``` + +## 圧縮 + +gzip、Brotli、Zstandardによる圧縮・展開に対応しています。使いたい方式のマクロを定義してコンパイルしましょう。 + +| 圧縮方式 | マクロ定義 | +| -- | -- | +| gzip | `CPPHTTPLIB_ZLIB_SUPPORT` | +| Brotli | `CPPHTTPLIB_BROTLI_SUPPORT` | +| Zstandard | `CPPHTTPLIB_ZSTD_SUPPORT` | + +```cpp +httplib::Client cli("https://example.com"); +cli.set_compress(true); // リクエストボディを圧縮 +cli.set_decompress(true); // レスポンスボディを展開 +``` + +## プロキシ + +HTTPプロキシ経由で接続できます。 + +```cpp +httplib::Client cli("https://example.com"); +cli.set_proxy("proxy.example.com", 8080); +cli.set_proxy_basic_auth("user", "password"); +``` + +## タイムアウト + +接続・読み取り・書き込みのタイムアウトを個別に設定できます。 + +```cpp +httplib::Client cli("https://example.com"); +cli.set_connection_timeout(5, 0); // 5秒 +cli.set_read_timeout(10, 0); // 10秒 +cli.set_write_timeout(10, 0); // 10秒 +``` + +## Keep-Alive + +同じサーバーに何度もリクエストするなら、Keep-Aliveを有効にしましょう。TCP接続を再利用するので効率的です。 + +```cpp +httplib::Client cli("https://example.com"); +cli.set_keep_alive(true); +``` + +## サーバーのミドルウェア + +リクエスト処理の前後にフックを挟めます。 + +```cpp +svr.set_pre_routing_handler([](const auto &req, auto &res) { + // すべてのリクエストの前に実行される + return httplib::Server::HandlerResponse::Unhandled; // 通常のルーティングに進む +}); + +svr.set_post_routing_handler([](const auto &req, auto &res) { + // レスポンスが返された後に実行される + res.set_header("X-Server", "cpp-httplib"); +}); +``` + +`req.user_data` を使うと、ミドルウェアからハンドラーにデータを渡せます。認証トークンのデコード結果を共有するときに便利です。 + +```cpp +svr.set_pre_routing_handler([](const auto &req, auto &res) { + req.user_data["auth_user"] = std::string("alice"); + return httplib::Server::HandlerResponse::Unhandled; +}); + +svr.Get("/me", [](const auto &req, auto &res) { + auto user = std::any_cast(req.user_data.at("auth_user")); + res.set_content("Hello, " + user, "text/plain"); +}); +``` + +エラーや例外のハンドラーもカスタマイズできますよ。 + +```cpp +svr.set_error_handler([](const auto &req, auto &res) { + res.set_content("Custom Error Page", "text/html"); +}); + +svr.set_exception_handler([](const auto &req, auto &res, std::exception_ptr ep) { + res.status = 500; + res.set_content("Internal Server Error", "text/plain"); +}); +``` + +## ロギング + +サーバーでもクライアントでもロガーを設定できます。 + +```cpp +svr.set_logger([](const auto &req, const auto &res) { + std::cout << req.method << " " << req.path << " " << res.status << std::endl; +}); +``` + +## Unix Domain Socket + +TCP以外に、Unix Domain Socketでの通信にも対応しています。同じマシン上のプロセス間通信に使えます。 + +```cpp +// サーバー +httplib::Server svr; +svr.set_address_family(AF_UNIX); +svr.listen("/tmp/httplib.sock", 0); +``` + +```cpp +// クライアント +httplib::Client cli("http://localhost"); +cli.set_address_family(AF_UNIX); +cli.set_hostname_addr_map({{"localhost", "/tmp/httplib.sock"}}); + +auto res = cli.Get("/"); +``` + +## さらに詳しく + +もっと詳しく知りたいときは、以下を参照してください。 + +- Cookbook — よくあるユースケースのレシピ集 +- [README](https://github.com/yhirose/cpp-httplib/blob/master/README.md) — 全APIのリファレンス +- [README-sse](https://github.com/yhirose/cpp-httplib/blob/master/README-sse.md) — Server-Sent Eventsの使い方 +- [README-stream](https://github.com/yhirose/cpp-httplib/blob/master/README-stream.md) — Streaming APIの使い方 +- [README-websocket](https://github.com/yhirose/cpp-httplib/blob/master/README-websocket.md) — WebSocketサーバーの使い方 diff --git a/docs-src/pages/ja/tour/index.md b/docs-src/pages/ja/tour/index.md new file mode 100644 index 0000000..29fc328 --- /dev/null +++ b/docs-src/pages/ja/tour/index.md @@ -0,0 +1,16 @@ +--- +title: "A Tour of cpp-httplib" +order: 1 +--- + +cpp-httplibの基本を、順番に学んでいくチュートリアルです。各章は前の章の内容を踏まえて進む構成なので、1章から順に読んでください。 + +1. [Getting Started](01-getting-started) — httplib.h の入手とHello Worldサーバー +2. [Basic Client](02-basic-client) — GET/POST・パスパラメーターのリクエスト送信 +3. [Basic Server](03-basic-server) — ルーティング、パスパラメーター、レスポンスの組み立て +4. [Static File Server](04-static-file-server) — 静的ファイルの配信 +5. [TLS Setup](05-tls-setup) — OpenSSL / mbedTLS のセットアップ +6. [HTTPS Client](06-https-client) — HTTPSサイトへのリクエスト +7. [HTTPS Server](07-https-server) — HTTPSサーバーの構築 +8. [WebSocket](08-websocket) — WebSocket通信の基本 +9. [What's Next](09-whats-next) — さらなる機能の紹介 diff --git a/docs-src/static/css/main.css b/docs-src/static/css/main.css new file mode 100644 index 0000000..702b809 --- /dev/null +++ b/docs-src/static/css/main.css @@ -0,0 +1,438 @@ +:root { + --bg: #333; + --bg-secondary: #3c3c3c; + --bg-code: #2a2a2a; + --text: #ccc; + --text-bright: white; + --text-muted: #999; + --text-code: #b0b0b0; + --text-inline-code: plum; + --border: #555; + --border-code: #3a3a3a; + --link: palegoldenrod; + --heading: lightskyblue; + --heading-link: #f0c090; + --header-nav-link: pink; + --emphasis: pink; + --nav-section: #bbb; + --nav-section-active: #ddd; + --content-width: 900px; + --sidebar-width: 280px; + --header-height: 48px; + --line-height: 1.6; +} + +* { + margin: 0; + padding: 0; + box-sizing: border-box; +} + +body { + background-color: var(--bg); + color: var(--text); + font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, sans-serif; + line-height: var(--line-height); +} + +a { + color: var(--link); + text-decoration: none; +} + +a:hover { + text-decoration: underline; +} + +/* Header */ +.header { + position: fixed; + top: 0; + left: 0; + right: 0; + height: var(--header-height); + background-color: var(--bg-secondary); + border-bottom: 1px solid var(--border); + z-index: 100; +} + +.header-inner { + height: 100%; + display: flex; + align-items: center; + padding: 0 16px; + gap: 24px; +} + +.header-title { + color: var(--text); + font-weight: bold; + font-size: 1.1rem; + white-space: nowrap; +} + +.header-title:hover { + text-decoration: none; + color: var(--text-bright); +} + +.header-spacer { + flex: 1; +} + +.header-nav { + display: flex; + gap: 16px; +} + +.header-nav a { + color: var(--header-nav-link); + font-size: 0.9rem; +} + +.header-tools { + display: flex; + align-items: center; + gap: 8px; +} + +.lang-selector { + position: relative; +} + +.lang-btn { + background: none; + border: 1px solid var(--text-muted); + color: var(--text); + padding: 4px 10px; + border-radius: 4px; + cursor: pointer; + font-size: 0.85rem; +} + +.lang-btn:hover { + border-color: var(--text); +} + +.lang-popup { + display: none; + position: absolute; + right: 0; + top: 100%; + margin-top: 4px; + background: var(--bg-secondary); + border: 1px solid var(--border); + border-radius: 4px; + list-style: none; + min-width: 60px; + z-index: 200; +} + +.lang-popup.open { + display: block; +} + +.lang-popup li a { + display: block; + padding: 6px 12px; + color: var(--text); + font-size: 0.85rem; +} + +.lang-popup li a:hover { + background: var(--bg); + text-decoration: none; +} + +.sidebar-toggle { + display: none; + background: none; + border: none; + color: var(--text); + font-size: 1.2rem; + cursor: pointer; + padding: 4px 8px; +} + +/* Draft banner */ +.draft-banner { + position: fixed; + top: var(--header-height); + right: 0; + background: #c44; + color: white; + padding: 4px 16px; + font-size: 0.75rem; + font-weight: bold; + letter-spacing: 0.1em; + z-index: 99; +} + +/* Layout */ +.layout { + margin-top: var(--header-height); + display: grid; + grid-template-columns: var(--sidebar-width) minmax(0, 1fr); + min-height: calc(100vh - var(--header-height)); +} + +.layout.no-sidebar { + grid-template-columns: 1fr; +} + +/* Sidebar */ +.sidebar { + width: var(--sidebar-width); + flex-shrink: 0; + padding: 24px 16px; + border-right: 1px solid var(--bg-secondary); + position: sticky; + top: var(--header-height); + height: calc(100vh - var(--header-height)); + overflow-y: auto; +} + +.nav-section { + margin-bottom: 16px; +} + +.nav-section-title { + color: var(--nav-section); + font-weight: bold; + font-size: 1.0rem; + display: block; + margin-bottom: 8px; +} + +.nav-section-title.active { + color: var(--nav-section-active); +} + +.nav-list { + list-style: none; + padding-left: 8px; +} + +.nav-list li { + margin-bottom: 4px; +} + +.nav-list li a { + color: var(--text-muted); + font-size: 0.85rem; +} + +.nav-list li a:hover { + color: var(--text); +} + +.nav-list li a.active { + color: var(--emphasis); + font-weight: bold; +} + +/* Content */ +.content { + min-width: 0; + max-width: var(--content-width); + padding: 32px 24px; + overflow-wrap: break-word; +} + +.content.portal { + max-width: var(--content-width); + padding: 48px 24px; + margin: 0 auto; +} + +.content article h1 { + font-size: 1.8rem; + margin-bottom: 24px; + color: var(--heading); +} + +.content article h2 { + font-size: 1.4rem; + margin-top: 32px; + margin-bottom: 16px; + color: var(--heading-link); +} + +.content article h3 { + font-size: 1.1rem; + margin-top: 24px; + margin-bottom: 12px; + color: var(--text); +} + +.content article p { + margin-bottom: 12px; +} + +.content article ul, +.content article ol { + margin-bottom: 12px; + padding-left: 24px; +} + +.content article li { + margin-bottom: 4px; +} + +.content article strong { + color: var(--emphasis); +} + +.content article code { + background: var(--bg-code); + color: var(--text-inline-code); + padding: 2px 6px; + border-radius: 3px; + font-size: 0.9em; +} + +.content article pre { + background: var(--bg-code) !important; + color: var(--text-code); + padding: 16px; + border-radius: 4px; + overflow-x: auto; + margin-bottom: 16px; + border: 1px solid var(--border-code); +} + +.content article pre code { + background: none; + padding: 0; +} + +.content article table { + width: 100%; + border-collapse: collapse; + margin-bottom: 16px; +} + +.content article th, +.content article td { + border: 1px solid var(--bg-secondary); + padding: 8px 12px; + text-align: left; +} + +.content article th { + background: var(--bg-secondary); +} + +.content article blockquote { + border-left: 3px solid var(--text-muted); + padding-left: 16px; + margin-bottom: 12px; + color: var(--text-muted); +} + +/* Footer */ +.footer { + padding: 12px 16px; + text-align: center; + color: var(--text-muted); + font-size: 0.8rem; + border-top: 1px solid var(--bg-secondary); +} + +/* Responsive */ +@media (max-width: 768px) { + .layout { + grid-template-columns: minmax(0, 1fr); + } + + .sidebar { + position: fixed; + left: calc(-1 * var(--sidebar-width)); + width: var(--sidebar-width); + top: var(--header-height); + height: calc(100vh - var(--header-height)); + background: var(--bg); + z-index: 50; + transition: left 0.2s ease; + border-right: 1px solid var(--border); + } + + .sidebar.open { + left: 0; + } + + .sidebar-toggle { + display: block; + } + + .content { + padding: 24px 16px; + } +} + +@media (max-width: 480px) { + :root { + --header-height: 44px; + } + + .header-inner { + padding: 0 12px; + gap: 12px; + } + + .header-nav a { + font-size: 0.8rem; + } + + .content article h1 { + font-size: 1.4rem; + } + + .content article h2 { + font-size: 1.2rem; + } +} + +/* Light mode */ +[data-theme="light"] { + --bg: #f5f5f5; + --bg-secondary: #e8e8e8; + --bg-code: #eee; + --text: #333; + --text-bright: #000; + --text-muted: #666; + --text-code: #333; + --text-inline-code: #8b5ca0; + --border: #ccc; + --border-code: #ddd; + --link: #b8860b; + --heading: #2a6496; + --heading-link: #c06020; + --header-nav-link: #c04060; + --emphasis: #c04060; + --nav-section: #666; + --nav-section-active: #333; +} + +/* Code block theme switching */ +.code-light { display: none; } +.code-dark { display: block; } + +[data-theme="light"] .code-light { display: block; } +[data-theme="light"] .code-dark { display: none; } + +/* Theme toggle */ +.theme-toggle { + background: none; + border: 1px solid var(--text-muted); + color: var(--text); + padding: 4px 8px; + border-radius: 4px; + cursor: pointer; + font-size: 1rem; + line-height: 1; +} + +.theme-toggle:hover { + border-color: var(--text); +} diff --git a/docs-src/static/js/main.js b/docs-src/static/js/main.js new file mode 100644 index 0000000..3bdb6bb --- /dev/null +++ b/docs-src/static/js/main.js @@ -0,0 +1,73 @@ +// Language selector +(function () { + var btn = document.querySelector('.lang-btn'); + var popup = document.querySelector('.lang-popup'); + if (!btn || !popup) return; + + btn.addEventListener('click', function (e) { + e.stopPropagation(); + popup.classList.toggle('open'); + }); + + document.addEventListener('click', function () { + popup.classList.remove('open'); + }); + + popup.addEventListener('click', function (e) { + var link = e.target.closest('[data-lang]'); + if (!link) return; + e.preventDefault(); + var lang = link.getAttribute('data-lang'); + localStorage.setItem('preferred-lang', lang); + var path = window.location.pathname; + var newPath = path.replace(/^\/[a-z]{2}\//, '/' + lang + '/'); + window.location.href = newPath; + }); +})(); + +// Theme toggle +(function () { + var btn = document.querySelector('.theme-toggle'); + if (!btn) return; + + function getTheme() { + var stored = localStorage.getItem('preferred-theme'); + if (stored) return stored; + return window.matchMedia('(prefers-color-scheme: light)').matches ? 'light' : 'dark'; + } + + function applyTheme(theme) { + if (theme === 'light') { + document.documentElement.setAttribute('data-theme', 'light'); + } else { + document.documentElement.removeAttribute('data-theme'); + } + btn.textContent = theme === 'light' ? '\u2600\uFE0F' : '\uD83C\uDF19'; + } + + applyTheme(getTheme()); + + btn.addEventListener('click', function () { + var current = getTheme(); + var next = current === 'dark' ? 'light' : 'dark'; + localStorage.setItem('preferred-theme', next); + applyTheme(next); + }); +})(); + +// Mobile sidebar toggle +(function () { + var toggle = document.querySelector('.sidebar-toggle'); + var sidebar = document.querySelector('.sidebar'); + if (!toggle || !sidebar) return; + + toggle.addEventListener('click', function () { + sidebar.classList.toggle('open'); + }); + + document.addEventListener('click', function (e) { + if (!sidebar.contains(e.target) && e.target !== toggle) { + sidebar.classList.remove('open'); + } + }); +})(); diff --git a/docs-src/templates/base.html b/docs-src/templates/base.html new file mode 100644 index 0000000..305587b --- /dev/null +++ b/docs-src/templates/base.html @@ -0,0 +1,54 @@ + + + + + + {{ page.title }} - {{ site.title }} + + + + +
+
+ {{ site.title }}{% if site.version %} v{{ site.version }}{% endif %} +
+ +
+ +
+ + +
+
+ {% block sidebar_toggle %}{% endblock %} +
+
+ + {% if page.status == "draft" %} +
DRAFT
+ {% endif %} + +
+ {% block body %}{% endblock %} +
+ +
+ © 2026 yhirose. All rights reserved. +
+ + + + diff --git a/docs-src/templates/page.html b/docs-src/templates/page.html new file mode 100644 index 0000000..089bd73 --- /dev/null +++ b/docs-src/templates/page.html @@ -0,0 +1,30 @@ +{% extends "base.html" %} + +{% block layout_class %}has-sidebar{% endblock %} + +{% block sidebar_toggle %}{% endblock %} + +{% block body %} + +
+
+

{{ page.title }}

+ {{ content | safe }} +
+
+{% endblock %} diff --git a/docs-src/templates/portal.html b/docs-src/templates/portal.html new file mode 100644 index 0000000..cadbd06 --- /dev/null +++ b/docs-src/templates/portal.html @@ -0,0 +1,12 @@ +{% extends "base.html" %} + +{% block layout_class %}no-sidebar{% endblock %} + +{% block body %} +
+
+

{{ page.title }}

+ {{ content | safe }} +
+
+{% endblock %} diff --git a/docs/css/main.css b/docs/css/main.css new file mode 100644 index 0000000..702b809 --- /dev/null +++ b/docs/css/main.css @@ -0,0 +1,438 @@ +:root { + --bg: #333; + --bg-secondary: #3c3c3c; + --bg-code: #2a2a2a; + --text: #ccc; + --text-bright: white; + --text-muted: #999; + --text-code: #b0b0b0; + --text-inline-code: plum; + --border: #555; + --border-code: #3a3a3a; + --link: palegoldenrod; + --heading: lightskyblue; + --heading-link: #f0c090; + --header-nav-link: pink; + --emphasis: pink; + --nav-section: #bbb; + --nav-section-active: #ddd; + --content-width: 900px; + --sidebar-width: 280px; + --header-height: 48px; + --line-height: 1.6; +} + +* { + margin: 0; + padding: 0; + box-sizing: border-box; +} + +body { + background-color: var(--bg); + color: var(--text); + font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, sans-serif; + line-height: var(--line-height); +} + +a { + color: var(--link); + text-decoration: none; +} + +a:hover { + text-decoration: underline; +} + +/* Header */ +.header { + position: fixed; + top: 0; + left: 0; + right: 0; + height: var(--header-height); + background-color: var(--bg-secondary); + border-bottom: 1px solid var(--border); + z-index: 100; +} + +.header-inner { + height: 100%; + display: flex; + align-items: center; + padding: 0 16px; + gap: 24px; +} + +.header-title { + color: var(--text); + font-weight: bold; + font-size: 1.1rem; + white-space: nowrap; +} + +.header-title:hover { + text-decoration: none; + color: var(--text-bright); +} + +.header-spacer { + flex: 1; +} + +.header-nav { + display: flex; + gap: 16px; +} + +.header-nav a { + color: var(--header-nav-link); + font-size: 0.9rem; +} + +.header-tools { + display: flex; + align-items: center; + gap: 8px; +} + +.lang-selector { + position: relative; +} + +.lang-btn { + background: none; + border: 1px solid var(--text-muted); + color: var(--text); + padding: 4px 10px; + border-radius: 4px; + cursor: pointer; + font-size: 0.85rem; +} + +.lang-btn:hover { + border-color: var(--text); +} + +.lang-popup { + display: none; + position: absolute; + right: 0; + top: 100%; + margin-top: 4px; + background: var(--bg-secondary); + border: 1px solid var(--border); + border-radius: 4px; + list-style: none; + min-width: 60px; + z-index: 200; +} + +.lang-popup.open { + display: block; +} + +.lang-popup li a { + display: block; + padding: 6px 12px; + color: var(--text); + font-size: 0.85rem; +} + +.lang-popup li a:hover { + background: var(--bg); + text-decoration: none; +} + +.sidebar-toggle { + display: none; + background: none; + border: none; + color: var(--text); + font-size: 1.2rem; + cursor: pointer; + padding: 4px 8px; +} + +/* Draft banner */ +.draft-banner { + position: fixed; + top: var(--header-height); + right: 0; + background: #c44; + color: white; + padding: 4px 16px; + font-size: 0.75rem; + font-weight: bold; + letter-spacing: 0.1em; + z-index: 99; +} + +/* Layout */ +.layout { + margin-top: var(--header-height); + display: grid; + grid-template-columns: var(--sidebar-width) minmax(0, 1fr); + min-height: calc(100vh - var(--header-height)); +} + +.layout.no-sidebar { + grid-template-columns: 1fr; +} + +/* Sidebar */ +.sidebar { + width: var(--sidebar-width); + flex-shrink: 0; + padding: 24px 16px; + border-right: 1px solid var(--bg-secondary); + position: sticky; + top: var(--header-height); + height: calc(100vh - var(--header-height)); + overflow-y: auto; +} + +.nav-section { + margin-bottom: 16px; +} + +.nav-section-title { + color: var(--nav-section); + font-weight: bold; + font-size: 1.0rem; + display: block; + margin-bottom: 8px; +} + +.nav-section-title.active { + color: var(--nav-section-active); +} + +.nav-list { + list-style: none; + padding-left: 8px; +} + +.nav-list li { + margin-bottom: 4px; +} + +.nav-list li a { + color: var(--text-muted); + font-size: 0.85rem; +} + +.nav-list li a:hover { + color: var(--text); +} + +.nav-list li a.active { + color: var(--emphasis); + font-weight: bold; +} + +/* Content */ +.content { + min-width: 0; + max-width: var(--content-width); + padding: 32px 24px; + overflow-wrap: break-word; +} + +.content.portal { + max-width: var(--content-width); + padding: 48px 24px; + margin: 0 auto; +} + +.content article h1 { + font-size: 1.8rem; + margin-bottom: 24px; + color: var(--heading); +} + +.content article h2 { + font-size: 1.4rem; + margin-top: 32px; + margin-bottom: 16px; + color: var(--heading-link); +} + +.content article h3 { + font-size: 1.1rem; + margin-top: 24px; + margin-bottom: 12px; + color: var(--text); +} + +.content article p { + margin-bottom: 12px; +} + +.content article ul, +.content article ol { + margin-bottom: 12px; + padding-left: 24px; +} + +.content article li { + margin-bottom: 4px; +} + +.content article strong { + color: var(--emphasis); +} + +.content article code { + background: var(--bg-code); + color: var(--text-inline-code); + padding: 2px 6px; + border-radius: 3px; + font-size: 0.9em; +} + +.content article pre { + background: var(--bg-code) !important; + color: var(--text-code); + padding: 16px; + border-radius: 4px; + overflow-x: auto; + margin-bottom: 16px; + border: 1px solid var(--border-code); +} + +.content article pre code { + background: none; + padding: 0; +} + +.content article table { + width: 100%; + border-collapse: collapse; + margin-bottom: 16px; +} + +.content article th, +.content article td { + border: 1px solid var(--bg-secondary); + padding: 8px 12px; + text-align: left; +} + +.content article th { + background: var(--bg-secondary); +} + +.content article blockquote { + border-left: 3px solid var(--text-muted); + padding-left: 16px; + margin-bottom: 12px; + color: var(--text-muted); +} + +/* Footer */ +.footer { + padding: 12px 16px; + text-align: center; + color: var(--text-muted); + font-size: 0.8rem; + border-top: 1px solid var(--bg-secondary); +} + +/* Responsive */ +@media (max-width: 768px) { + .layout { + grid-template-columns: minmax(0, 1fr); + } + + .sidebar { + position: fixed; + left: calc(-1 * var(--sidebar-width)); + width: var(--sidebar-width); + top: var(--header-height); + height: calc(100vh - var(--header-height)); + background: var(--bg); + z-index: 50; + transition: left 0.2s ease; + border-right: 1px solid var(--border); + } + + .sidebar.open { + left: 0; + } + + .sidebar-toggle { + display: block; + } + + .content { + padding: 24px 16px; + } +} + +@media (max-width: 480px) { + :root { + --header-height: 44px; + } + + .header-inner { + padding: 0 12px; + gap: 12px; + } + + .header-nav a { + font-size: 0.8rem; + } + + .content article h1 { + font-size: 1.4rem; + } + + .content article h2 { + font-size: 1.2rem; + } +} + +/* Light mode */ +[data-theme="light"] { + --bg: #f5f5f5; + --bg-secondary: #e8e8e8; + --bg-code: #eee; + --text: #333; + --text-bright: #000; + --text-muted: #666; + --text-code: #333; + --text-inline-code: #8b5ca0; + --border: #ccc; + --border-code: #ddd; + --link: #b8860b; + --heading: #2a6496; + --heading-link: #c06020; + --header-nav-link: #c04060; + --emphasis: #c04060; + --nav-section: #666; + --nav-section-active: #333; +} + +/* Code block theme switching */ +.code-light { display: none; } +.code-dark { display: block; } + +[data-theme="light"] .code-light { display: block; } +[data-theme="light"] .code-dark { display: none; } + +/* Theme toggle */ +.theme-toggle { + background: none; + border: 1px solid var(--text-muted); + color: var(--text); + padding: 4px 8px; + border-radius: 4px; + cursor: pointer; + font-size: 1rem; + line-height: 1; +} + +.theme-toggle:hover { + border-color: var(--text); +} diff --git a/docs/en/cookbook/index.html b/docs/en/cookbook/index.html new file mode 100644 index 0000000..2ea7169 --- /dev/null +++ b/docs/en/cookbook/index.html @@ -0,0 +1,73 @@ + + + + + + Cookbook - cpp-httplib + + + + +
+
+ cpp-httplib v0.35.0 +
+ +
+ +
+ + +
+
+ +
+
+ + + +
+ + +
+
+

Cookbook

+

This section is under construction.

+

Check back soon for a collection of recipes organized by topic.

+ +
+
+ +
+ +
+ © 2025 yhirose. All rights reserved. +
+ + + + diff --git a/docs/en/index.html b/docs/en/index.html new file mode 100644 index 0000000..1513096 --- /dev/null +++ b/docs/en/index.html @@ -0,0 +1,72 @@ + + + + + + cpp-httplib - cpp-httplib + + + + +
+
+ cpp-httplib v0.35.0 +
+ +
+ +
+ + +
+
+ +
+
+ + + +
+ +
+
+

cpp-httplib

+

cpp-httplib is an HTTP/HTTPS library for C++. Just copy a single header file, httplib.h, and you're ready to go.

+

When you need a quick HTTP server or client in C++, you want something that just works. That's exactly why I built cpp-httplib. You can start writing both servers and clients in just a few lines of code.

+

The API uses a lambda-based design that feels natural. It runs anywhere you have a C++11 or later compiler. Windows, macOS, Linux — use whatever environment you already have.

+

HTTPS works too. Just link OpenSSL or mbedTLS, and both server and client gain TLS support. Content-Encoding (gzip, Brotli, etc.), file uploads, and other features you actually need in real-world development are all included. WebSocket is also supported.

+

Under the hood, it uses blocking I/O with a thread pool. It's not built for handling massive numbers of simultaneous connections. But for API servers, embedded HTTP in tools, mock servers for testing, and many other use cases, it delivers solid performance.

+

"Solve today's problem, today." That's the kind of simplicity cpp-httplib aims for.

+

Documentation

+
    +
  • A Tour of cpp-httplib — A step-by-step tutorial covering the basics. Start here if you're new
  • +
  • Cookbook — A collection of recipes organized by topic. Jump to whatever you need
  • +
+ +
+
+ +
+ +
+ © 2025 yhirose. All rights reserved. +
+ + + + diff --git a/docs/en/tour/01-getting-started/index.html b/docs/en/tour/01-getting-started/index.html new file mode 100644 index 0000000..947de1a --- /dev/null +++ b/docs/en/tour/01-getting-started/index.html @@ -0,0 +1,198 @@ + + + + + + Getting Started - cpp-httplib + + + + +
+
+ cpp-httplib v0.35.0 +
+ +
+ +
+ + +
+
+ +
+
+ + + +
+ + +
+
+

Getting Started

+

All you need to get started with cpp-httplib is httplib.h and a C++ compiler. Let's download the file and get a Hello World server running.

+

Getting httplib.h

+

You can download it directly from GitHub. Always use the latest version.

+
+curl -LO https://github.com/yhirose/cpp-httplib/raw/refs/tags/latest/httplib.h
+
+
+curl -LO https://github.com/yhirose/cpp-httplib/raw/refs/tags/latest/httplib.h
+
+
+

Place the downloaded httplib.h in your project directory and you're good to go.

+

Setting Up Your Compiler

+ + + + +
OSDevelopment EnvironmentSetup
macOSApple ClangXcode Command Line Tools (xcode-select --install)
Ubuntuclang++ or g++apt install clang or apt install g++
WindowsMSVCVisual Studio 2022 or later (install with C++ components)
+

Hello World Server

+

Save the following code as server.cpp.

+
+#include "httplib.h"
+
+int main() {
+    httplib::Server svr;
+
+    svr.Get("/", [](const httplib::Request&, httplib::Response& res) {
+        res.set_content("Hello, World!", "text/plain");
+    });
+
+    svr.listen("0.0.0.0", 8080);
+}
+
+
+#include "httplib.h"
+
+int main() {
+    httplib::Server svr;
+
+    svr.Get("/", [](const httplib::Request&, httplib::Response& res) {
+        res.set_content("Hello, World!", "text/plain");
+    });
+
+    svr.listen("0.0.0.0", 8080);
+}
+
+
+

In just a few lines, you have a server that responds to HTTP requests.

+

Compiling and Running

+

The sample code in this tutorial is written in C++17 for cleaner, more concise code. cpp-httplib itself can compile with C++11 as well.

+
+# macOS
+clang++ -std=c++17 -o server server.cpp
+
+# Linux
+# `-pthread`: cpp-httplib uses threads internally
+clang++ -std=c++17 -pthread -o server server.cpp
+
+# Windows (Developer Command Prompt)
+# `/EHsc`: Enable C++ exception handling
+cl /EHsc /std:c++17 server.cpp
+
+
+# macOS
+clang++ -std=c++17 -o server server.cpp
+
+# Linux
+# `-pthread`: cpp-httplib uses threads internally
+clang++ -std=c++17 -pthread -o server server.cpp
+
+# Windows (Developer Command Prompt)
+# `/EHsc`: Enable C++ exception handling
+cl /EHsc /std:c++17 server.cpp
+
+
+

Once it compiles, run it.

+
+# macOS / Linux
+./server
+
+# Windows
+server.exe
+
+
+# macOS / Linux
+./server
+
+# Windows
+server.exe
+
+
+

Open http://localhost:8080 in your browser. If you see "Hello, World!", you're all set.

+

You can also verify with curl.

+
+curl http://localhost:8080/
+# Hello, World!
+
+
+curl http://localhost:8080/
+# Hello, World!
+
+
+

To stop the server, press Ctrl+C in your terminal.

+

Next Steps

+

Now you know the basics of running a server. Next, let's look at the client side. cpp-httplib also comes with HTTP client functionality.

+

Next: Basic Client

+ +
+
+ +
+ +
+ © 2025 yhirose. All rights reserved. +
+ + + + diff --git a/docs/en/tour/02-basic-client/index.html b/docs/en/tour/02-basic-client/index.html new file mode 100644 index 0000000..57f07ae --- /dev/null +++ b/docs/en/tour/02-basic-client/index.html @@ -0,0 +1,491 @@ + + + + + + Basic Client - cpp-httplib + + + + +
+
+ cpp-httplib v0.35.0 +
+ +
+ +
+ + +
+
+ +
+
+ + + +
+ + +
+
+

Basic Client

+

cpp-httplib isn't just for servers -- it also comes with a full HTTP client. Let's use httplib::Client to send GET and POST requests.

+

Preparing a Test Server

+

To try out the client, you need a server that accepts requests. Save the following code, then compile and run it the same way you did in the previous chapter. We'll cover the server details in the next chapter.

+
+#include "httplib.h"
+#include <iostream>
+
+int main() {
+    httplib::Server svr;
+
+    svr.Get("/hi", [](const auto &, auto &res) {
+        res.set_content("Hello!", "text/plain");
+    });
+
+    svr.Get("/search", [](const auto &req, auto &res) {
+        auto q = req.get_param_value("q");
+        res.set_content("Query: " + q, "text/plain");
+    });
+
+    svr.Post("/post", [](const auto &req, auto &res) {
+        res.set_content(req.body, "text/plain");
+    });
+
+    svr.Post("/submit", [](const auto &req, auto &res) {
+        std::string result;
+        for (auto &[key, val] : req.params) {
+            result += key + " = " + val + "\n";
+        }
+        res.set_content(result, "text/plain");
+    });
+
+    svr.Post("/upload", [](const auto &req, auto &res) {
+        auto f = req.form.get_file("file");
+        auto content = f.filename + " (" + std::to_string(f.content.size()) + " bytes)";
+        res.set_content(content, "text/plain");
+    });
+
+    svr.Get("/users/:id", [](const auto &req, auto &res) {
+        auto id = req.path_params.at("id");
+        res.set_content("User ID: " + id, "text/plain");
+    });
+
+    svr.Get(R"(/files/(\d+))", [](const auto &req, auto &res) {
+        auto id = req.matches[1];
+        res.set_content("File ID: " + std::string(id), "text/plain");
+    });
+
+    std::cout << "Listening on port 8080..." << std::endl;
+    svr.listen("0.0.0.0", 8080);
+}
+
+
+#include "httplib.h"
+#include <iostream>
+
+int main() {
+    httplib::Server svr;
+
+    svr.Get("/hi", [](const auto &, auto &res) {
+        res.set_content("Hello!", "text/plain");
+    });
+
+    svr.Get("/search", [](const auto &req, auto &res) {
+        auto q = req.get_param_value("q");
+        res.set_content("Query: " + q, "text/plain");
+    });
+
+    svr.Post("/post", [](const auto &req, auto &res) {
+        res.set_content(req.body, "text/plain");
+    });
+
+    svr.Post("/submit", [](const auto &req, auto &res) {
+        std::string result;
+        for (auto &[key, val] : req.params) {
+            result += key + " = " + val + "\n";
+        }
+        res.set_content(result, "text/plain");
+    });
+
+    svr.Post("/upload", [](const auto &req, auto &res) {
+        auto f = req.form.get_file("file");
+        auto content = f.filename + " (" + std::to_string(f.content.size()) + " bytes)";
+        res.set_content(content, "text/plain");
+    });
+
+    svr.Get("/users/:id", [](const auto &req, auto &res) {
+        auto id = req.path_params.at("id");
+        res.set_content("User ID: " + id, "text/plain");
+    });
+
+    svr.Get(R"(/files/(\d+))", [](const auto &req, auto &res) {
+        auto id = req.matches[1];
+        res.set_content("File ID: " + std::string(id), "text/plain");
+    });
+
+    std::cout << "Listening on port 8080..." << std::endl;
+    svr.listen("0.0.0.0", 8080);
+}
+
+
+

GET Request

+

Once the server is running, open a separate terminal and give it a try. Let's start with the simplest GET request.

+
+#include "httplib.h"
+#include <iostream>
+
+int main() {
+    httplib::Client cli("http://localhost:8080");
+
+    auto res = cli.Get("/hi");
+    if (res) {
+        std::cout << res->status << std::endl;  // 200
+        std::cout << res->body << std::endl;    // Hello!
+    }
+}
+
+
+#include "httplib.h"
+#include <iostream>
+
+int main() {
+    httplib::Client cli("http://localhost:8080");
+
+    auto res = cli.Get("/hi");
+    if (res) {
+        std::cout << res->status << std::endl;  // 200
+        std::cout << res->body << std::endl;    // Hello!
+    }
+}
+
+
+

Pass the server address to the httplib::Client constructor, then call Get() to send a request. You can retrieve the status code and body from the returned res.

+

Here's the equivalent curl command.

+
+curl http://localhost:8080/hi
+# Hello!
+
+
+curl http://localhost:8080/hi
+# Hello!
+
+
+

Checking the Response

+

A response contains header information in addition to the status code and body.

+
+auto res = cli.Get("/hi");
+if (res) {
+    // Status code
+    std::cout << res->status << std::endl;  // 200
+
+    // Body
+    std::cout << res->body << std::endl;  // Hello!
+
+    // Headers
+    std::cout << res->get_header_value("Content-Type") << std::endl;  // text/plain
+}
+
+
+auto res = cli.Get("/hi");
+if (res) {
+    // Status code
+    std::cout << res->status << std::endl;  // 200
+
+    // Body
+    std::cout << res->body << std::endl;  // Hello!
+
+    // Headers
+    std::cout << res->get_header_value("Content-Type") << std::endl;  // text/plain
+}
+
+
+

res->body is a std::string, so if you want to parse a JSON response, you can pass it directly to a JSON library like nlohmann/json.

+

Query Parameters

+

To add query parameters to a GET request, you can either write them directly in the URL or use httplib::Params.

+
+auto res = cli.Get("/search", httplib::Params{{"q", "cpp-httplib"}});
+if (res) {
+    std::cout << res->body << std::endl;  // Query: cpp-httplib
+}
+
+
+auto res = cli.Get("/search", httplib::Params{{"q", "cpp-httplib"}});
+if (res) {
+    std::cout << res->body << std::endl;  // Query: cpp-httplib
+}
+
+
+

httplib::Params automatically URL-encodes special characters for you.

+
+curl "http://localhost:8080/search?q=cpp-httplib"
+# Query: cpp-httplib
+
+
+curl "http://localhost:8080/search?q=cpp-httplib"
+# Query: cpp-httplib
+
+
+

Path Parameters

+

When values are embedded directly in the URL path, no special client API is needed. Just pass the path to Get() as-is.

+
+auto res = cli.Get("/users/42");
+if (res) {
+    std::cout << res->body << std::endl;  // User ID: 42
+}
+
+
+auto res = cli.Get("/users/42");
+if (res) {
+    std::cout << res->body << std::endl;  // User ID: 42
+}
+
+
+curl http://localhost:8080/users/42
+# User ID: 42
+
+
+curl http://localhost:8080/users/42
+# User ID: 42
+
+
+

The test server also has a /files/(\d+) route that uses a regex to accept numeric IDs only.

+
+auto res = cli.Get("/files/42");
+if (res) {
+    std::cout << res->body << std::endl;  // File ID: 42
+}
+
+
+auto res = cli.Get("/files/42");
+if (res) {
+    std::cout << res->body << std::endl;  // File ID: 42
+}
+
+
+curl http://localhost:8080/files/42
+# File ID: 42
+
+
+curl http://localhost:8080/files/42
+# File ID: 42
+
+
+

Pass a non-numeric ID like /files/abc and you'll get a 404. We'll cover how that works in the next chapter.

+

Request Headers

+

To add custom HTTP headers, pass an httplib::Headers object. This works with both Get() and Post().

+
+auto res = cli.Get("/hi", httplib::Headers{
+    {"Authorization", "Bearer my-token"}
+});
+
+
+auto res = cli.Get("/hi", httplib::Headers{
+    {"Authorization", "Bearer my-token"}
+});
+
+
+curl -H "Authorization: Bearer my-token" http://localhost:8080/hi
+
+
+curl -H "Authorization: Bearer my-token" http://localhost:8080/hi
+
+
+

POST Request

+

Let's POST some text data. Pass the body as the second argument to Post() and the Content-Type as the third.

+
+auto res = cli.Post("/post", "Hello, Server!", "text/plain");
+if (res) {
+    std::cout << res->status << std::endl;  // 200
+    std::cout << res->body << std::endl;    // Hello, Server!
+}
+
+
+auto res = cli.Post("/post", "Hello, Server!", "text/plain");
+if (res) {
+    std::cout << res->status << std::endl;  // 200
+    std::cout << res->body << std::endl;    // Hello, Server!
+}
+
+
+

The test server's /post endpoint echoes the body back, so you get the same string you sent.

+
+curl -X POST -H "Content-Type: text/plain" -d "Hello, Server!" http://localhost:8080/post
+# Hello, Server!
+
+
+curl -X POST -H "Content-Type: text/plain" -d "Hello, Server!" http://localhost:8080/post
+# Hello, Server!
+
+
+

Sending Form Data

+

You can send key-value pairs just like an HTML form. Use httplib::Params for this.

+
+auto res = cli.Post("/submit", httplib::Params{
+    {"name", "Alice"},
+    {"age", "30"}
+});
+if (res) {
+    std::cout << res->body << std::endl;
+    // age = 30
+    // name = Alice
+}
+
+
+auto res = cli.Post("/submit", httplib::Params{
+    {"name", "Alice"},
+    {"age", "30"}
+});
+if (res) {
+    std::cout << res->body << std::endl;
+    // age = 30
+    // name = Alice
+}
+
+
+

This sends the data in application/x-www-form-urlencoded format.

+
+curl -X POST -d "name=Alice&age=30" http://localhost:8080/submit
+
+
+curl -X POST -d "name=Alice&age=30" http://localhost:8080/submit
+
+
+

POSTing a File

+

To upload a file, use httplib::UploadFormDataItems to send it as multipart form data.

+
+auto res = cli.Post("/upload", httplib::UploadFormDataItems{
+    {"file", "Hello, File!", "hello.txt", "text/plain"}
+});
+if (res) {
+    std::cout << res->body << std::endl;  // hello.txt (12 bytes)
+}
+
+
+auto res = cli.Post("/upload", httplib::UploadFormDataItems{
+    {"file", "Hello, File!", "hello.txt", "text/plain"}
+});
+if (res) {
+    std::cout << res->body << std::endl;  // hello.txt (12 bytes)
+}
+
+
+

Each element in UploadFormDataItems has four fields: {name, content, filename, content_type}.

+
+curl -F "file=Hello, File!;filename=hello.txt;type=text/plain" http://localhost:8080/upload
+
+
+curl -F "file=Hello, File!;filename=hello.txt;type=text/plain" http://localhost:8080/upload
+
+
+

Error Handling

+

Network communication can fail -- the server might not be reachable. Always check whether res is valid.

+
+httplib::Client cli("http://localhost:9999");  // Non-existent port
+auto res = cli.Get("/hi");
+
+if (!res) {
+    // Connection error
+    std::cout << "Error: " << httplib::to_string(res.error()) << std::endl;
+    // Error: Connection
+    return 1;
+}
+
+// If we reach here, we have a response
+if (res->status != 200) {
+    std::cout << "HTTP Error: " << res->status << std::endl;
+    return 1;
+}
+
+std::cout << res->body << std::endl;
+
+
+httplib::Client cli("http://localhost:9999");  // Non-existent port
+auto res = cli.Get("/hi");
+
+if (!res) {
+    // Connection error
+    std::cout << "Error: " << httplib::to_string(res.error()) << std::endl;
+    // Error: Connection
+    return 1;
+}
+
+// If we reach here, we have a response
+if (res->status != 200) {
+    std::cout << "HTTP Error: " << res->status << std::endl;
+    return 1;
+}
+
+std::cout << res->body << std::endl;
+
+
+

There are two levels of errors.

+
    +
  • Connection error: The client couldn't reach the server. res evaluates to false, and you can call res.error() to find out what went wrong.
  • +
  • HTTP error: The server returned an error status (404, 500, etc.). res evaluates to true, but you need to check res->status.
  • +
+

Next Steps

+

Now you know how to send requests from a client. Next, let's take a closer look at the server side. We'll dig into routing, path parameters, and more.

+

Next: Basic Server

+ +
+
+ +
+ +
+ © 2025 yhirose. All rights reserved. +
+ + + + diff --git a/docs/en/tour/03-basic-server/index.html b/docs/en/tour/03-basic-server/index.html new file mode 100644 index 0000000..a4d9dcf --- /dev/null +++ b/docs/en/tour/03-basic-server/index.html @@ -0,0 +1,446 @@ + + + + + + Basic Server - cpp-httplib + + + + +
+
+ cpp-httplib v0.35.0 +
+ +
+ +
+ + +
+
+ +
+
+ + + +
+ + +
+
+

Basic Server

+

In the previous chapter, you sent requests from a client to a test server. Now let's walk through how that server actually works.

+

Starting the Server

+

Once you've registered your routes, call svr.listen() to start the server.

+
+svr.listen("0.0.0.0", 8080);
+
+
+svr.listen("0.0.0.0", 8080);
+
+
+

The first argument is the host, and the second is the port. "0.0.0.0" listens on all network interfaces. Use "127.0.0.1" if you want to accept connections from your own machine only.

+

listen() is a blocking call. It won't return until the server stops. The server keeps running until you press Ctrl+C in your terminal or call svr.stop() from another thread.

+

Routing

+

Routing is the heart of any server. It's how you tell cpp-httplib: when a request comes in for this URL with this HTTP method, run this code.

+
+httplib::Server svr;
+
+svr.Get("/hi", [](const httplib::Request &req, httplib::Response &res) {
+    res.set_content("Hello!", "text/plain");
+});
+
+
+httplib::Server svr;
+
+svr.Get("/hi", [](const httplib::Request &req, httplib::Response &res) {
+    res.set_content("Hello!", "text/plain");
+});
+
+
+

svr.Get() registers a handler for GET requests. The first argument is the path, the second is the handler function. When a GET request arrives at /hi, your lambda runs.

+

There's a method for each HTTP verb.

+
+svr.Get("/path",    handler);  // GET
+svr.Post("/path",   handler);  // POST
+svr.Put("/path",    handler);  // PUT
+svr.Delete("/path", handler);  // DELETE
+
+
+svr.Get("/path",    handler);  // GET
+svr.Post("/path",   handler);  // POST
+svr.Put("/path",    handler);  // PUT
+svr.Delete("/path", handler);  // DELETE
+
+
+

The handler signature is (const httplib::Request &req, httplib::Response &res). You can use auto to keep it short.

+
+svr.Get("/hi", [](const auto &req, auto &res) {
+    res.set_content("Hello!", "text/plain");
+});
+
+
+svr.Get("/hi", [](const auto &req, auto &res) {
+    res.set_content("Hello!", "text/plain");
+});
+
+
+

The handler only runs when the path matches. Requests to unregistered paths automatically return 404.

+

The Request Object

+

The first parameter req gives you everything the client sent.

+

Body

+

req.body holds the request body as a std::string.

+
+svr.Post("/post", [](const auto &req, auto &res) {
+    // Echo the body back to the client
+    res.set_content(req.body, "text/plain");
+});
+
+
+svr.Post("/post", [](const auto &req, auto &res) {
+    // Echo the body back to the client
+    res.set_content(req.body, "text/plain");
+});
+
+
+

Headers

+

Use req.get_header_value() to read a request header.

+
+svr.Get("/check", [](const auto &req, auto &res) {
+    auto auth = req.get_header_value("Authorization");
+    res.set_content("Auth: " + auth, "text/plain");
+});
+
+
+svr.Get("/check", [](const auto &req, auto &res) {
+    auto auth = req.get_header_value("Authorization");
+    res.set_content("Auth: " + auth, "text/plain");
+});
+
+
+

Query Parameters and Form Data

+

req.get_param_value() retrieves a parameter by name. It works for both GET query parameters and POST form data.

+
+svr.Get("/search", [](const auto &req, auto &res) {
+    auto q = req.get_param_value("q");
+    res.set_content("Query: " + q, "text/plain");
+});
+
+
+svr.Get("/search", [](const auto &req, auto &res) {
+    auto q = req.get_param_value("q");
+    res.set_content("Query: " + q, "text/plain");
+});
+
+
+

A request to /search?q=cpp-httplib gives you "cpp-httplib" for q.

+

To loop over all parameters, use req.params.

+
+svr.Post("/submit", [](const auto &req, auto &res) {
+    std::string result;
+    for (auto &[key, val] : req.params) {
+        result += key + " = " + val + "\n";
+    }
+    res.set_content(result, "text/plain");
+});
+
+
+svr.Post("/submit", [](const auto &req, auto &res) {
+    std::string result;
+    for (auto &[key, val] : req.params) {
+        result += key + " = " + val + "\n";
+    }
+    res.set_content(result, "text/plain");
+});
+
+
+

File Uploads

+

Files uploaded via multipart form data are available through req.form.get_file().

+
+svr.Post("/upload", [](const auto &req, auto &res) {
+    auto f = req.form.get_file("file");
+    auto content = f.filename + " (" + std::to_string(f.content.size()) + " bytes)";
+    res.set_content(content, "text/plain");
+});
+
+
+svr.Post("/upload", [](const auto &req, auto &res) {
+    auto f = req.form.get_file("file");
+    auto content = f.filename + " (" + std::to_string(f.content.size()) + " bytes)";
+    res.set_content(content, "text/plain");
+});
+
+
+

f.filename gives you the filename, and f.content gives you the file data.

+

Path Parameters

+

Sometimes you want to capture part of the URL as a variable -- for example, the 42 in /users/42. Use the :param syntax to do that.

+
+svr.Get("/users/:id", [](const auto &req, auto &res) {
+    auto id = req.path_params.at("id");
+    res.set_content("User ID: " + id, "text/plain");
+});
+
+
+svr.Get("/users/:id", [](const auto &req, auto &res) {
+    auto id = req.path_params.at("id");
+    res.set_content("User ID: " + id, "text/plain");
+});
+
+
+

A request to /users/42 gives you "42" from req.path_params.at("id"). /users/100 gives you "100".

+

You can capture multiple segments at once.

+
+svr.Get("/users/:user_id/posts/:post_id", [](const auto &req, auto &res) {
+    auto user_id = req.path_params.at("user_id");
+    auto post_id = req.path_params.at("post_id");
+    res.set_content("User: " + user_id + ", Post: " + post_id, "text/plain");
+});
+
+
+svr.Get("/users/:user_id/posts/:post_id", [](const auto &req, auto &res) {
+    auto user_id = req.path_params.at("user_id");
+    auto post_id = req.path_params.at("post_id");
+    res.set_content("User: " + user_id + ", Post: " + post_id, "text/plain");
+});
+
+
+

Regex Patterns

+

You can also write a regular expression directly in the path instead of :param. Capture group values are available via req.matches, which is a std::smatch.

+
+// Only accept numeric IDs
+svr.Get(R"(/files/(\d+))", [](const auto &req, auto &res) {
+    auto id = req.matches[1];  // First capture group
+    res.set_content("File ID: " + std::string(id), "text/plain");
+});
+
+
+// Only accept numeric IDs
+svr.Get(R"(/files/(\d+))", [](const auto &req, auto &res) {
+    auto id = req.matches[1];  // First capture group
+    res.set_content("File ID: " + std::string(id), "text/plain");
+});
+
+
+

/files/42 matches, but /files/abc doesn't. This is handy when you want to constrain what values are accepted.

+

Building a Response

+

The second parameter res is how you send data back to the client.

+

Body and Content-Type

+

res.set_content() sets the body and Content-Type. That's all you need for a 200 response.

+
+svr.Get("/hi", [](const auto &req, auto &res) {
+    res.set_content("Hello!", "text/plain");
+});
+
+
+svr.Get("/hi", [](const auto &req, auto &res) {
+    res.set_content("Hello!", "text/plain");
+});
+
+
+

Status Code

+

To return a different status code, assign to res.status.

+
+svr.Get("/not-found", [](const auto &req, auto &res) {
+    res.status = 404;
+    res.set_content("Not found", "text/plain");
+});
+
+
+svr.Get("/not-found", [](const auto &req, auto &res) {
+    res.status = 404;
+    res.set_content("Not found", "text/plain");
+});
+
+
+

Response Headers

+

Add response headers with res.set_header().

+
+svr.Get("/with-header", [](const auto &req, auto &res) {
+    res.set_header("X-Custom", "my-value");
+    res.set_content("Hello!", "text/plain");
+});
+
+
+svr.Get("/with-header", [](const auto &req, auto &res) {
+    res.set_header("X-Custom", "my-value");
+    res.set_content("Hello!", "text/plain");
+});
+
+
+

Walking Through the Test Server

+

Now let's use what we've learned to read through the test server from the previous chapter.

+

GET /hi

+
+svr.Get("/hi", [](const auto &, auto &res) {
+    res.set_content("Hello!", "text/plain");
+});
+
+
+svr.Get("/hi", [](const auto &, auto &res) {
+    res.set_content("Hello!", "text/plain");
+});
+
+
+

The simplest possible handler. We don't need any information from the request, so the req parameter is left unnamed. It just returns "Hello!".

+

GET /search

+
+svr.Get("/search", [](const auto &req, auto &res) {
+    auto q = req.get_param_value("q");
+    res.set_content("Query: " + q, "text/plain");
+});
+
+
+svr.Get("/search", [](const auto &req, auto &res) {
+    auto q = req.get_param_value("q");
+    res.set_content("Query: " + q, "text/plain");
+});
+
+
+

req.get_param_value("q") pulls out the query parameter q. A request to /search?q=cpp-httplib returns "Query: cpp-httplib".

+

POST /post

+
+svr.Post("/post", [](const auto &req, auto &res) {
+    res.set_content(req.body, "text/plain");
+});
+
+
+svr.Post("/post", [](const auto &req, auto &res) {
+    res.set_content(req.body, "text/plain");
+});
+
+
+

An echo server. Whatever body the client sends, req.body holds it, and we send it straight back.

+

POST /submit

+
+svr.Post("/submit", [](const auto &req, auto &res) {
+    std::string result;
+    for (auto &[key, val] : req.params) {
+        result += key + " = " + val + "\n";
+    }
+    res.set_content(result, "text/plain");
+});
+
+
+svr.Post("/submit", [](const auto &req, auto &res) {
+    std::string result;
+    for (auto &[key, val] : req.params) {
+        result += key + " = " + val + "\n";
+    }
+    res.set_content(result, "text/plain");
+});
+
+
+

Loops over the form data in req.params using structured bindings (auto &[key, val]) to unpack each key-value pair.

+

POST /upload

+
+svr.Post("/upload", [](const auto &req, auto &res) {
+    auto f = req.form.get_file("file");
+    auto content = f.filename + " (" + std::to_string(f.content.size()) + " bytes)";
+    res.set_content(content, "text/plain");
+});
+
+
+svr.Post("/upload", [](const auto &req, auto &res) {
+    auto f = req.form.get_file("file");
+    auto content = f.filename + " (" + std::to_string(f.content.size()) + " bytes)";
+    res.set_content(content, "text/plain");
+});
+
+
+

Receives a file uploaded via multipart form data. req.form.get_file("file") fetches the field named "file", and we respond with the filename and size.

+

GET /users/:id

+
+svr.Get("/users/:id", [](const auto &req, auto &res) {
+    auto id = req.path_params.at("id");
+    res.set_content("User ID: " + id, "text/plain");
+});
+
+
+svr.Get("/users/:id", [](const auto &req, auto &res) {
+    auto id = req.path_params.at("id");
+    res.set_content("User ID: " + id, "text/plain");
+});
+
+
+

:id is the path parameter. req.path_params.at("id") retrieves its value. /users/42 gives you "42", /users/alice gives you "alice".

+

GET /files/(\d+)

+
+svr.Get(R"(/files/(\d+))", [](const auto &req, auto &res) {
+    auto id = req.matches[1];
+    res.set_content("File ID: " + std::string(id), "text/plain");
+});
+
+
+svr.Get(R"(/files/(\d+))", [](const auto &req, auto &res) {
+    auto id = req.matches[1];
+    res.set_content("File ID: " + std::string(id), "text/plain");
+});
+
+
+

The regex (\d+) matches numeric IDs only. /files/42 hits this handler, but /files/abc returns 404. req.matches[1] retrieves the first capture group.

+

Next Steps

+

You now have the full picture of how a server works. Routing, reading requests, building responses -- that's enough to build a real API server.

+

Next, let's look at serving static files. We'll build a server that delivers HTML and CSS.

+

Next: Static File Server

+ +
+
+ +
+ +
+ © 2025 yhirose. All rights reserved. +
+ + + + diff --git a/docs/en/tour/04-static-file-server/index.html b/docs/en/tour/04-static-file-server/index.html new file mode 100644 index 0000000..3f1d5b6 --- /dev/null +++ b/docs/en/tour/04-static-file-server/index.html @@ -0,0 +1,269 @@ + + + + + + Static File Server - cpp-httplib + + + + +
+
+ cpp-httplib v0.35.0 +
+ +
+ +
+ + +
+
+ +
+
+ + + +
+ + +
+
+

Static File Server

+

cpp-httplib can serve static files too — HTML, CSS, images, you name it. No complicated configuration required. One call to set_mount_point() is all it takes.

+

The basics of set_mount_point

+

Let's jump right in. set_mount_point() maps a URL path to a local directory.

+
+#include "httplib.h"
+#include <iostream>
+
+int main() {
+    httplib::Server svr;
+
+    svr.set_mount_point("/", "./html");
+
+    std::cout << "Listening on port 8080..." << std::endl;
+    svr.listen("0.0.0.0", 8080);
+}
+
+
+#include "httplib.h"
+#include <iostream>
+
+int main() {
+    httplib::Server svr;
+
+    svr.set_mount_point("/", "./html");
+
+    std::cout << "Listening on port 8080..." << std::endl;
+    svr.listen("0.0.0.0", 8080);
+}
+
+
+

The first argument is the URL mount point. The second is the local directory path. In this example, requests to / are served from the ./html directory.

+

Let's try it out. First, create an html directory and add an index.html file.

+
+mkdir html
+
+
+mkdir html
+
+
+<!DOCTYPE html>
+<html>
+<head><title>My Page</title></head>
+<body>
+    <h1>Hello from cpp-httplib!</h1>
+    <p>This is a static file.</p>
+</body>
+</html>
+
+
+<!DOCTYPE html>
+<html>
+<head><title>My Page</title></head>
+<body>
+    <h1>Hello from cpp-httplib!</h1>
+    <p>This is a static file.</p>
+</body>
+</html>
+
+
+

Compile and start the server.

+
+g++ -std=c++17 -o server server.cpp -pthread
+./server
+
+
+g++ -std=c++17 -o server server.cpp -pthread
+./server
+
+
+

Open http://localhost:8080 in your browser. You should see the contents of html/index.html. Visiting http://localhost:8080/index.html returns the same page.

+

You can also access it with the client code from the previous chapter, or with curl.

+
+httplib::Client cli("http://localhost:8080");
+auto res = cli.Get("/");
+if (res) {
+    std::cout << res->body << std::endl;  // HTML is displayed
+}
+
+
+httplib::Client cli("http://localhost:8080");
+auto res = cli.Get("/");
+if (res) {
+    std::cout << res->body << std::endl;  // HTML is displayed
+}
+
+
+curl http://localhost:8080
+
+
+curl http://localhost:8080
+
+
+

Multiple mount points

+

You can call set_mount_point() as many times as you like. Each URL path gets its own directory.

+
+svr.set_mount_point("/", "./public");
+svr.set_mount_point("/assets", "./static/assets");
+svr.set_mount_point("/docs", "./documentation");
+
+
+svr.set_mount_point("/", "./public");
+svr.set_mount_point("/assets", "./static/assets");
+svr.set_mount_point("/docs", "./documentation");
+
+
+

A request to /assets/style.css serves ./static/assets/style.css. A request to /docs/guide.html serves ./documentation/guide.html.

+

Combining with handlers

+

Static file serving and routing handlers — the kind you learned about in the previous chapter — work side by side.

+
+httplib::Server svr;
+
+// API endpoint
+svr.Get("/api/hello", [](const auto &, auto &res) {
+    res.set_content(R"({"message":"Hello!"})", "application/json");
+});
+
+// Static file serving
+svr.set_mount_point("/", "./public");
+
+svr.listen("0.0.0.0", 8080);
+
+
+httplib::Server svr;
+
+// API endpoint
+svr.Get("/api/hello", [](const auto &, auto &res) {
+    res.set_content(R"({"message":"Hello!"})", "application/json");
+});
+
+// Static file serving
+svr.set_mount_point("/", "./public");
+
+svr.listen("0.0.0.0", 8080);
+
+
+

Handlers take priority. The handler responds to /api/hello. For every other path, the server looks for a file in ./public.

+

Adding response headers

+

Pass headers as the third argument to set_mount_point() and they get attached to every static file response. This is great for cache control.

+
+svr.set_mount_point("/", "./public", {
+    {"Cache-Control", "max-age=3600"}
+});
+
+
+svr.set_mount_point("/", "./public", {
+    {"Cache-Control", "max-age=3600"}
+});
+
+
+

With this in place, the browser caches served files for one hour.

+

A Dockerfile for your static file server

+

The cpp-httplib repository includes a Dockerfile built for static file serving. We also publish a pre-built image on Docker Hub, so you can get up and running with a single command.

+
+> docker run -p 8080:80 -v ./my-site:/html yhirose4dockerhub/cpp-httplib-server
+Serving HTTP on 0.0.0.0:80
+Mount point: / -> ./html
+Press Ctrl+C to shutdown gracefully...
+192.168.65.1 - - [22/Feb/2026:12:00:00 +0000] "GET / HTTP/1.1" 200 256 "-" "Mozilla/5.0 ..."
+192.168.65.1 - - [22/Feb/2026:12:00:00 +0000] "GET /style.css HTTP/1.1" 200 1024 "-" "Mozilla/5.0 ..."
+192.168.65.1 - - [22/Feb/2026:12:00:01 +0000] "GET /favicon.ico HTTP/1.1" 404 152 "-" "Mozilla/5.0 ..."
+
+
+> docker run -p 8080:80 -v ./my-site:/html yhirose4dockerhub/cpp-httplib-server
+Serving HTTP on 0.0.0.0:80
+Mount point: / -> ./html
+Press Ctrl+C to shutdown gracefully...
+192.168.65.1 - - [22/Feb/2026:12:00:00 +0000] "GET / HTTP/1.1" 200 256 "-" "Mozilla/5.0 ..."
+192.168.65.1 - - [22/Feb/2026:12:00:00 +0000] "GET /style.css HTTP/1.1" 200 1024 "-" "Mozilla/5.0 ..."
+192.168.65.1 - - [22/Feb/2026:12:00:01 +0000] "GET /favicon.ico HTTP/1.1" 404 152 "-" "Mozilla/5.0 ..."
+
+
+

Everything in your ./my-site directory gets served on port 8080. The access log follows the same format as NGINX, so you can see exactly what's happening.

+

What's next

+

You can now serve static files. A web server that delivers HTML, CSS, and JavaScript — built with this little code.

+

Next, let's encrypt your connections with HTTPS. We'll start by setting up a TLS library.

+

Next: TLS Setup

+ +
+
+ +
+ +
+ © 2025 yhirose. All rights reserved. +
+ + + + diff --git a/docs/en/tour/05-tls-setup/index.html b/docs/en/tour/05-tls-setup/index.html new file mode 100644 index 0000000..0910368 --- /dev/null +++ b/docs/en/tour/05-tls-setup/index.html @@ -0,0 +1,193 @@ + + + + + + TLS Setup - cpp-httplib + + + + +
+
+ cpp-httplib v0.35.0 +
+ +
+ +
+ + +
+
+ +
+
+ + + +
+ + +
+
+

TLS Setup

+

So far we've been using plain HTTP, but in the real world, HTTPS is the norm. To use HTTPS with cpp-httplib, you need a TLS library.

+

In this tour, we'll use OpenSSL. It's the most widely used option, and you'll find plenty of resources online.

+

Installing OpenSSL

+

Install it for your OS.

+ + + + +
OSHow to install
macOSHomebrew (brew install openssl)
Ubuntu / Debiansudo apt install libssl-dev
Windowsvcpkg (vcpkg install openssl)
+

Compile Options

+

To enable TLS, define the CPPHTTPLIB_OPENSSL_SUPPORT macro when compiling. You'll need a few extra options compared to the previous chapters.

+
+# macOS (Homebrew)
+clang++ -std=c++17 -DCPPHTTPLIB_OPENSSL_SUPPORT \
+    -I$(brew --prefix openssl)/include \
+    -L$(brew --prefix openssl)/lib \
+    -lssl -lcrypto \
+    -framework CoreFoundation -framework Security \
+    -o server server.cpp
+
+# Linux
+clang++ -std=c++17 -pthread -DCPPHTTPLIB_OPENSSL_SUPPORT \
+    -lssl -lcrypto \
+    -o server server.cpp
+
+# Windows (Developer Command Prompt)
+cl /EHsc /std:c++17 /DCPPHTTPLIB_OPENSSL_SUPPORT server.cpp libssl.lib libcrypto.lib
+
+
+# macOS (Homebrew)
+clang++ -std=c++17 -DCPPHTTPLIB_OPENSSL_SUPPORT \
+    -I$(brew --prefix openssl)/include \
+    -L$(brew --prefix openssl)/lib \
+    -lssl -lcrypto \
+    -framework CoreFoundation -framework Security \
+    -o server server.cpp
+
+# Linux
+clang++ -std=c++17 -pthread -DCPPHTTPLIB_OPENSSL_SUPPORT \
+    -lssl -lcrypto \
+    -o server server.cpp
+
+# Windows (Developer Command Prompt)
+cl /EHsc /std:c++17 /DCPPHTTPLIB_OPENSSL_SUPPORT server.cpp libssl.lib libcrypto.lib
+
+
+

Let's look at what each option does.

+
    +
  • -DCPPHTTPLIB_OPENSSL_SUPPORT — Defines the macro that enables TLS support
  • +
  • -lssl -lcrypto — Links the OpenSSL libraries
  • +
  • -I / -L (macOS only) — Points to the Homebrew OpenSSL paths
  • +
  • -framework CoreFoundation -framework Security (macOS only) — Needed to automatically load system certificates from the Keychain
  • +
+

Verifying the Setup

+

Let's make sure everything works. Here's a simple program that passes an HTTPS URL to httplib::Client.

+
+#define CPPHTTPLIB_OPENSSL_SUPPORT
+#include "httplib.h"
+#include <iostream>
+
+int main() {
+    httplib::Client cli("https://www.google.com");
+
+    auto res = cli.Get("/");
+    if (res) {
+        std::cout << "Status: " << res->status << std::endl;
+    } else {
+        std::cout << "Error: " << httplib::to_string(res.error()) << std::endl;
+    }
+}
+
+
+#define CPPHTTPLIB_OPENSSL_SUPPORT
+#include "httplib.h"
+#include <iostream>
+
+int main() {
+    httplib::Client cli("https://www.google.com");
+
+    auto res = cli.Get("/");
+    if (res) {
+        std::cout << "Status: " << res->status << std::endl;
+    } else {
+        std::cout << "Error: " << httplib::to_string(res.error()) << std::endl;
+    }
+}
+
+
+

Compile and run it. If you see Status: 200, your setup is complete.

+

Other TLS Backends

+

cpp-httplib also supports Mbed TLS and wolfSSL in addition to OpenSSL. You can switch between them just by changing the macro definition and linked libraries.

+ + + + +
BackendMacroLibraries to link
OpenSSLCPPHTTPLIB_OPENSSL_SUPPORTlibssl, libcrypto
Mbed TLSCPPHTTPLIB_MBEDTLS_SUPPORTlibmbedtls, libmbedx509, libmbedcrypto
wolfSSLCPPHTTPLIB_WOLFSSL_SUPPORTlibwolfssl
+

This tour assumes OpenSSL, but the API is the same regardless of which backend you choose.

+

Next Step

+

You're all set with TLS. Next, let's send a request to an HTTPS site.

+

Next: HTTPS Client

+ +
+
+ +
+ +
+ © 2025 yhirose. All rights reserved. +
+ + + + diff --git a/docs/en/tour/06-https-client/index.html b/docs/en/tour/06-https-client/index.html new file mode 100644 index 0000000..8d68feb --- /dev/null +++ b/docs/en/tour/06-https-client/index.html @@ -0,0 +1,251 @@ + + + + + + HTTPS Client - cpp-httplib + + + + +
+
+ cpp-httplib v0.35.0 +
+ +
+ +
+ + +
+
+ +
+
+ + + +
+ + +
+
+

HTTPS Client

+

In the previous chapter, you set up OpenSSL. Now let's put it to use with an HTTPS client. You can use the same httplib::Client from Chapter 2. Just pass a URL with the https:// scheme to the constructor.

+

GET Request

+

Let's try accessing a real HTTPS site.

+
+#define CPPHTTPLIB_OPENSSL_SUPPORT
+#include "httplib.h"
+#include <iostream>
+
+int main() {
+    httplib::Client cli("https://nghttp2.org");
+
+    auto res = cli.Get("/");
+    if (res) {
+        std::cout << res->status << std::endl;           // 200
+        std::cout << res->body.substr(0, 100) << std::endl;  // First 100 chars of the HTML
+    } else {
+        std::cout << "Error: " << httplib::to_string(res.error()) << std::endl;
+    }
+}
+
+
+#define CPPHTTPLIB_OPENSSL_SUPPORT
+#include "httplib.h"
+#include <iostream>
+
+int main() {
+    httplib::Client cli("https://nghttp2.org");
+
+    auto res = cli.Get("/");
+    if (res) {
+        std::cout << res->status << std::endl;           // 200
+        std::cout << res->body.substr(0, 100) << std::endl;  // First 100 chars of the HTML
+    } else {
+        std::cout << "Error: " << httplib::to_string(res.error()) << std::endl;
+    }
+}
+
+
+

In Chapter 2, you wrote httplib::Client cli("http://localhost:8080"). All you need to change is the scheme to https://. Every API you learned in Chapter 2 -- Get(), Post(), and so on -- works exactly the same way.

+
+curl https://nghttp2.org/
+
+
+curl https://nghttp2.org/
+
+
+

Specifying a Port

+

The default port for HTTPS is 443. If you need a different port, include it in the URL.

+
+httplib::Client cli("https://localhost:8443");
+
+
+httplib::Client cli("https://localhost:8443");
+
+
+

CA Certificate Verification

+

When connecting over HTTPS, httplib::Client verifies the server certificate by default. It only connects to servers whose certificate was issued by a trusted CA (Certificate Authority).

+

CA certificates are loaded automatically from the Keychain on macOS, the system CA certificate store on Linux, and the Windows certificate store on Windows. In most cases, no extra configuration is needed.

+

Specifying a CA Certificate File

+

On some environments, the system CA certificates may not be found. In that case, use set_ca_cert_path() to specify the path directly.

+
+httplib::Client cli("https://nghttp2.org");
+cli.set_ca_cert_path("/etc/ssl/certs/ca-certificates.crt");
+
+auto res = cli.Get("/");
+
+
+httplib::Client cli("https://nghttp2.org");
+cli.set_ca_cert_path("/etc/ssl/certs/ca-certificates.crt");
+
+auto res = cli.Get("/");
+
+
+curl --cacert /etc/ssl/certs/ca-certificates.crt https://nghttp2.org/
+
+
+curl --cacert /etc/ssl/certs/ca-certificates.crt https://nghttp2.org/
+
+
+

Disabling Certificate Verification

+

During development, you might want to connect to a server with a self-signed certificate. You can disable verification for that.

+
+httplib::Client cli("https://localhost:8443");
+cli.enable_server_certificate_verification(false);
+
+auto res = cli.Get("/");
+
+
+httplib::Client cli("https://localhost:8443");
+cli.enable_server_certificate_verification(false);
+
+auto res = cli.Get("/");
+
+
+curl -k https://localhost:8443/
+
+
+curl -k https://localhost:8443/
+
+
+

Never disable this in production. It opens you up to man-in-the-middle attacks.

+

Following Redirects

+

When accessing HTTPS sites, you'll often encounter redirects. For example, http:// to https://, or a bare domain to www.

+

By default, redirects are not followed. You can check the redirect target in the Location header.

+
+httplib::Client cli("https://nghttp2.org");
+
+auto res = cli.Get("/httpbin/redirect/3");
+if (res) {
+    std::cout << res->status << std::endl;  // 302
+    std::cout << res->get_header_value("Location") << std::endl;
+}
+
+
+httplib::Client cli("https://nghttp2.org");
+
+auto res = cli.Get("/httpbin/redirect/3");
+if (res) {
+    std::cout << res->status << std::endl;  // 302
+    std::cout << res->get_header_value("Location") << std::endl;
+}
+
+
+curl https://nghttp2.org/httpbin/redirect/3
+
+
+curl https://nghttp2.org/httpbin/redirect/3
+
+
+

Call set_follow_location(true) to automatically follow redirects and get the final response.

+
+httplib::Client cli("https://nghttp2.org");
+cli.set_follow_location(true);
+
+auto res = cli.Get("/httpbin/redirect/3");
+if (res) {
+    std::cout << res->status << std::endl;  // 200 (the final response)
+}
+
+
+httplib::Client cli("https://nghttp2.org");
+cli.set_follow_location(true);
+
+auto res = cli.Get("/httpbin/redirect/3");
+if (res) {
+    std::cout << res->status << std::endl;  // 200 (the final response)
+}
+
+
+curl -L https://nghttp2.org/httpbin/redirect/3
+
+
+curl -L https://nghttp2.org/httpbin/redirect/3
+
+
+

Next Steps

+

Now you know how to use the HTTPS client. Next, let's set up your own HTTPS server. We'll start with creating a self-signed certificate.

+

Next: HTTPS Server

+ +
+
+ +
+ +
+ © 2025 yhirose. All rights reserved. +
+ + + + diff --git a/docs/en/tour/07-https-server/index.html b/docs/en/tour/07-https-server/index.html new file mode 100644 index 0000000..1819647 --- /dev/null +++ b/docs/en/tour/07-https-server/index.html @@ -0,0 +1,240 @@ + + + + + + HTTPS Server - cpp-httplib + + + + +
+
+ cpp-httplib v0.35.0 +
+ +
+ +
+ + +
+
+ +
+
+ + + +
+ + +
+
+

HTTPS Server

+

In the previous chapter, you used an HTTPS client. Now let's set up your own HTTPS server. Just swap httplib::Server from Chapter 3 with httplib::SSLServer.

+

A TLS server needs a server certificate and a private key, though. Let's get those ready first.

+

Creating a Self-Signed Certificate

+

For development and testing, a self-signed certificate works just fine. You can generate one quickly with an OpenSSL command.

+
+openssl req -x509 -noenc -keyout key.pem -out cert.pem -subj /CN=localhost
+
+
+openssl req -x509 -noenc -keyout key.pem -out cert.pem -subj /CN=localhost
+
+
+

This creates two files:

+
    +
  • cert.pem — Server certificate
  • +
  • key.pem — Private key
  • +
+

A Minimal HTTPS Server

+

Once you have your certificate, let's write the server.

+
+#define CPPHTTPLIB_OPENSSL_SUPPORT
+#include "httplib.h"
+#include <iostream>
+
+int main() {
+    httplib::SSLServer svr("cert.pem", "key.pem");
+
+    svr.Get("/", [](const auto &, auto &res) {
+        res.set_content("Hello, HTTPS!", "text/plain");
+    });
+
+    std::cout << "Listening on https://localhost:8443" << std::endl;
+    svr.listen("0.0.0.0", 8443);
+}
+
+
+#define CPPHTTPLIB_OPENSSL_SUPPORT
+#include "httplib.h"
+#include <iostream>
+
+int main() {
+    httplib::SSLServer svr("cert.pem", "key.pem");
+
+    svr.Get("/", [](const auto &, auto &res) {
+        res.set_content("Hello, HTTPS!", "text/plain");
+    });
+
+    std::cout << "Listening on https://localhost:8443" << std::endl;
+    svr.listen("0.0.0.0", 8443);
+}
+
+
+

Just pass the certificate and private key paths to the httplib::SSLServer constructor. The routing API is exactly the same as httplib::Server from Chapter 3.

+

Compile and start it up.

+

Testing It Out

+

With the server running, try accessing it with curl. Since we're using a self-signed certificate, add the -k option to skip certificate verification.

+
+curl -k https://localhost:8443/
+# Hello, HTTPS!
+
+
+curl -k https://localhost:8443/
+# Hello, HTTPS!
+
+
+

If you open https://localhost:8443 in a browser, you'll see a "This connection is not secure" warning. That's expected with a self-signed certificate. Just proceed past it.

+

Connecting from a Client

+

Let's connect using httplib::Client from the previous chapter. There are two ways to connect to a server with a self-signed certificate.

+

Option 1: Disable Certificate Verification

+

This is the quick and easy approach for development.

+
+#define CPPHTTPLIB_OPENSSL_SUPPORT
+#include "httplib.h"
+#include <iostream>
+
+int main() {
+    httplib::Client cli("https://localhost:8443");
+    cli.enable_server_certificate_verification(false);
+
+    auto res = cli.Get("/");
+    if (res) {
+        std::cout << res->body << std::endl;  // Hello, HTTPS!
+    }
+}
+
+
+#define CPPHTTPLIB_OPENSSL_SUPPORT
+#include "httplib.h"
+#include <iostream>
+
+int main() {
+    httplib::Client cli("https://localhost:8443");
+    cli.enable_server_certificate_verification(false);
+
+    auto res = cli.Get("/");
+    if (res) {
+        std::cout << res->body << std::endl;  // Hello, HTTPS!
+    }
+}
+
+
+

Option 2: Specify the Self-Signed Certificate as a CA Certificate

+

This is the safer approach. You tell the client to trust cert.pem as a CA certificate.

+
+#define CPPHTTPLIB_OPENSSL_SUPPORT
+#include "httplib.h"
+#include <iostream>
+
+int main() {
+    httplib::Client cli("https://localhost:8443");
+    cli.set_ca_cert_path("cert.pem");
+
+    auto res = cli.Get("/");
+    if (res) {
+        std::cout << res->body << std::endl;  // Hello, HTTPS!
+    }
+}
+
+
+#define CPPHTTPLIB_OPENSSL_SUPPORT
+#include "httplib.h"
+#include <iostream>
+
+int main() {
+    httplib::Client cli("https://localhost:8443");
+    cli.set_ca_cert_path("cert.pem");
+
+    auto res = cli.Get("/");
+    if (res) {
+        std::cout << res->body << std::endl;  // Hello, HTTPS!
+    }
+}
+
+
+

This way, only connections to the server with that specific certificate are allowed, preventing impersonation. Use this approach whenever possible, even in test environments.

+

Comparing Server and SSLServer

+

The httplib::Server API you learned in Chapter 3 works exactly the same with httplib::SSLServer. The only difference is the constructor.

+ + + + + +
httplib::Serverhttplib::SSLServer
ConstructorNo argumentsCertificate and private key paths
ProtocolHTTPHTTPS
Port (convention)80808443
RoutingSameSame
+

To switch an HTTP server to HTTPS, just change the constructor.

+

Next Steps

+

Your HTTPS server is up and running. You now have the basics of both HTTP/HTTPS clients and servers covered.

+

Next, let's look at the WebSocket support that was recently added to cpp-httplib.

+

Next: WebSocket

+ +
+
+ +
+ +
+ © 2025 yhirose. All rights reserved. +
+ + + + diff --git a/docs/en/tour/08-websocket/index.html b/docs/en/tour/08-websocket/index.html new file mode 100644 index 0000000..57854c4 --- /dev/null +++ b/docs/en/tour/08-websocket/index.html @@ -0,0 +1,292 @@ + + + + + + WebSocket - cpp-httplib + + + + +
+
+ cpp-httplib v0.35.0 +
+ +
+ +
+ + +
+
+ +
+
+ + + +
+ + +
+
+

WebSocket

+

cpp-httplib supports WebSocket as well. Unlike HTTP request/response, WebSocket lets the server and client exchange messages in both directions. It's great for chat apps and real-time notifications.

+

Let's build an echo server and client right away.

+

Echo Server

+

Here's an echo server that sends back whatever message it receives.

+
+#include "httplib.h"
+#include <iostream>
+
+int main() {
+    httplib::Server svr;
+
+    svr.WebSocket("/ws", [](const httplib::Request &, httplib::ws::WebSocket &ws) {
+        std::string msg;
+        while (ws.read(msg)) {
+            ws.send(msg);  // Send back the received message as-is
+        }
+    });
+
+    std::cout << "Listening on port 8080..." << std::endl;
+    svr.listen("0.0.0.0", 8080);
+}
+
+
+#include "httplib.h"
+#include <iostream>
+
+int main() {
+    httplib::Server svr;
+
+    svr.WebSocket("/ws", [](const httplib::Request &, httplib::ws::WebSocket &ws) {
+        std::string msg;
+        while (ws.read(msg)) {
+            ws.send(msg);  // Send back the received message as-is
+        }
+    });
+
+    std::cout << "Listening on port 8080..." << std::endl;
+    svr.listen("0.0.0.0", 8080);
+}
+
+
+

You register a WebSocket handler with svr.WebSocket(). It works just like svr.Get() and svr.Post() from Chapter 3.

+

Inside the handler, ws.read(msg) waits for a message. When the connection closes, read() returns false, so the loop exits. ws.send(msg) sends a message back.

+

Connecting from a Client

+

Let's connect to the server using httplib::ws::WebSocketClient.

+
+#include "httplib.h"
+#include <iostream>
+
+int main() {
+    httplib::ws::WebSocketClient client("ws://localhost:8080/ws");
+
+    if (!client.connect()) {
+        std::cout << "Connection failed" << std::endl;
+        return 1;
+    }
+
+    // Send a message
+    client.send("Hello, WebSocket!");
+
+    // Receive a response from the server
+    std::string msg;
+    if (client.read(msg)) {
+        std::cout << msg << std::endl;  // Hello, WebSocket!
+    }
+
+    client.close();
+}
+
+
+#include "httplib.h"
+#include <iostream>
+
+int main() {
+    httplib::ws::WebSocketClient client("ws://localhost:8080/ws");
+
+    if (!client.connect()) {
+        std::cout << "Connection failed" << std::endl;
+        return 1;
+    }
+
+    // Send a message
+    client.send("Hello, WebSocket!");
+
+    // Receive a response from the server
+    std::string msg;
+    if (client.read(msg)) {
+        std::cout << msg << std::endl;  // Hello, WebSocket!
+    }
+
+    client.close();
+}
+
+
+

Pass a URL in ws://host:port/path format to the constructor. Call connect() to start the connection, then use send() and read() to exchange messages.

+

Text and Binary

+

WebSocket has two types of messages: text and binary. You can tell them apart by the return value of read().

+
+svr.WebSocket("/ws", [](const httplib::Request &, httplib::ws::WebSocket &ws) {
+    std::string msg;
+    httplib::ws::ReadResult ret;
+    while ((ret = ws.read(msg))) {
+        if (ret == httplib::ws::Binary) {
+            ws.send(msg.data(), msg.size());  // Send as binary
+        } else {
+            ws.send(msg);  // Send as text
+        }
+    }
+});
+
+
+svr.WebSocket("/ws", [](const httplib::Request &, httplib::ws::WebSocket &ws) {
+    std::string msg;
+    httplib::ws::ReadResult ret;
+    while ((ret = ws.read(msg))) {
+        if (ret == httplib::ws::Binary) {
+            ws.send(msg.data(), msg.size());  // Send as binary
+        } else {
+            ws.send(msg);  // Send as text
+        }
+    }
+});
+
+
+
    +
  • ws.send(const std::string &) — sends as a text message
  • +
  • ws.send(const char *, size_t) — sends as a binary message
  • +
+

The client-side API is the same.

+

Accessing Request Information

+

You can read HTTP request information from the handshake through the first argument req in the handler. This is handy for checking authentication tokens.

+
+svr.WebSocket("/ws", [](const httplib::Request &req, httplib::ws::WebSocket &ws) {
+    auto token = req.get_header_value("Authorization");
+    if (token.empty()) {
+        ws.close(httplib::ws::CloseStatus::PolicyViolation, "unauthorized");
+        return;
+    }
+
+    std::string msg;
+    while (ws.read(msg)) {
+        ws.send(msg);
+    }
+});
+
+
+svr.WebSocket("/ws", [](const httplib::Request &req, httplib::ws::WebSocket &ws) {
+    auto token = req.get_header_value("Authorization");
+    if (token.empty()) {
+        ws.close(httplib::ws::CloseStatus::PolicyViolation, "unauthorized");
+        return;
+    }
+
+    std::string msg;
+    while (ws.read(msg)) {
+        ws.send(msg);
+    }
+});
+
+
+

Using WSS

+

WebSocket over HTTPS (WSS) is also supported. On the server side, just register a WebSocket handler on httplib::SSLServer.

+
+httplib::SSLServer svr("cert.pem", "key.pem");
+
+svr.WebSocket("/ws", [](const httplib::Request &, httplib::ws::WebSocket &ws) {
+    std::string msg;
+    while (ws.read(msg)) {
+        ws.send(msg);
+    }
+});
+
+svr.listen("0.0.0.0", 8443);
+
+
+httplib::SSLServer svr("cert.pem", "key.pem");
+
+svr.WebSocket("/ws", [](const httplib::Request &, httplib::ws::WebSocket &ws) {
+    std::string msg;
+    while (ws.read(msg)) {
+        ws.send(msg);
+    }
+});
+
+svr.listen("0.0.0.0", 8443);
+
+
+

On the client side, use the wss:// scheme.

+
+httplib::ws::WebSocketClient client("wss://localhost:8443/ws");
+
+
+httplib::ws::WebSocketClient client("wss://localhost:8443/ws");
+
+
+

Next Steps

+

Now you know the basics of WebSocket. This wraps up the Tour.

+

The next page gives you a summary of features we didn't cover in the Tour.

+

Next: What's Next

+ +
+
+ +
+ +
+ © 2025 yhirose. All rights reserved. +
+ + + + diff --git a/docs/en/tour/09-whats-next/index.html b/docs/en/tour/09-whats-next/index.html new file mode 100644 index 0000000..9519f9f --- /dev/null +++ b/docs/en/tour/09-whats-next/index.html @@ -0,0 +1,424 @@ + + + + + + What's Next - cpp-httplib + + + + +
+
+ cpp-httplib v0.35.0 +
+ +
+ +
+ + +
+
+ +
+
+ + + +
+ + +
+
+

What's Next

+

Great job finishing the Tour! You now have a solid grasp of the cpp-httplib basics. But there's a lot more to explore. Here's a quick overview of features we didn't cover in the Tour, organized by category.

+

Streaming API

+

When you're working with LLM streaming responses or downloading large files, you don't want to load the entire response into memory. Use stream::Get() to process data chunk by chunk.

+
+httplib::Client cli("http://localhost:11434");
+
+auto result = httplib::stream::Get(cli, "/api/generate");
+
+if (result) {
+    while (result.next()) {
+        std::cout.write(result.data(), result.size());
+    }
+}
+
+
+httplib::Client cli("http://localhost:11434");
+
+auto result = httplib::stream::Get(cli, "/api/generate");
+
+if (result) {
+    while (result.next()) {
+        std::cout.write(result.data(), result.size());
+    }
+}
+
+
+

You can also pass a content_receiver callback to Get(). This approach works with Keep-Alive.

+
+httplib::Client cli("http://localhost:8080");
+
+cli.Get("/stream", [](const char *data, size_t len) {
+    std::cout.write(data, len);
+    return true;
+});
+
+
+httplib::Client cli("http://localhost:8080");
+
+cli.Get("/stream", [](const char *data, size_t len) {
+    std::cout.write(data, len);
+    return true;
+});
+
+
+

On the server side, you have set_content_provider() and set_chunked_content_provider(). Use the former when you know the size, and the latter when you don't.

+
+// With known size (sets Content-Length)
+svr.Get("/file", [](const auto &, auto &res) {
+    auto size = get_file_size("large.bin");
+    res.set_content_provider(size, "application/octet-stream",
+        [](size_t offset, size_t length, httplib::DataSink &sink) {
+            // Send 'length' bytes starting from 'offset'
+            return true;
+        });
+});
+
+// Unknown size (Chunked Transfer Encoding)
+svr.Get("/stream", [](const auto &, auto &res) {
+    res.set_chunked_content_provider("text/plain",
+        [](size_t offset, httplib::DataSink &sink) {
+            sink.write("chunk\n", 6);
+            return true;  // Return false to finish
+        });
+});
+
+
+// With known size (sets Content-Length)
+svr.Get("/file", [](const auto &, auto &res) {
+    auto size = get_file_size("large.bin");
+    res.set_content_provider(size, "application/octet-stream",
+        [](size_t offset, size_t length, httplib::DataSink &sink) {
+            // Send 'length' bytes starting from 'offset'
+            return true;
+        });
+});
+
+// Unknown size (Chunked Transfer Encoding)
+svr.Get("/stream", [](const auto &, auto &res) {
+    res.set_chunked_content_provider("text/plain",
+        [](size_t offset, httplib::DataSink &sink) {
+            sink.write("chunk\n", 6);
+            return true;  // Return false to finish
+        });
+});
+
+
+

For uploading large files, make_file_provider() comes in handy. It streams the file instead of loading it all into memory.

+
+httplib::Client cli("http://localhost:8080");
+
+auto res = cli.Post("/upload", {}, {
+    httplib::make_file_provider("file", "/path/to/large-file.zip")
+});
+
+
+httplib::Client cli("http://localhost:8080");
+
+auto res = cli.Post("/upload", {}, {
+    httplib::make_file_provider("file", "/path/to/large-file.zip")
+});
+
+
+

Server-Sent Events (SSE)

+

We provide an SSE client as well. It supports automatic reconnection and resuming via Last-Event-ID.

+
+httplib::Client cli("http://localhost:8080");
+httplib::sse::SSEClient sse(cli, "/events");
+
+sse.on_message([](const httplib::sse::SSEMessage &msg) {
+    std::cout << msg.event << ": " << msg.data << std::endl;
+});
+
+sse.start();  // Blocking, with auto-reconnection
+
+
+httplib::Client cli("http://localhost:8080");
+httplib::sse::SSEClient sse(cli, "/events");
+
+sse.on_message([](const httplib::sse::SSEMessage &msg) {
+    std::cout << msg.event << ": " << msg.data << std::endl;
+});
+
+sse.start();  // Blocking, with auto-reconnection
+
+
+

You can also set separate handlers for each event type.

+
+sse.on_event("update", [](const httplib::sse::SSEMessage &msg) {
+    // Only handles "update" events
+});
+
+
+sse.on_event("update", [](const httplib::sse::SSEMessage &msg) {
+    // Only handles "update" events
+});
+
+
+

Authentication

+

The client has helpers for Basic auth, Bearer Token auth, and Digest auth.

+
+httplib::Client cli("https://api.example.com");
+cli.set_basic_auth("user", "password");
+cli.set_bearer_token_auth("my-token");
+
+
+httplib::Client cli("https://api.example.com");
+cli.set_basic_auth("user", "password");
+cli.set_bearer_token_auth("my-token");
+
+
+

Compression

+

We support compression and decompression with gzip, Brotli, and Zstandard. Define the corresponding macro when you compile.

+ + + + +
MethodMacro
gzipCPPHTTPLIB_ZLIB_SUPPORT
BrotliCPPHTTPLIB_BROTLI_SUPPORT
ZstandardCPPHTTPLIB_ZSTD_SUPPORT
+
+httplib::Client cli("https://example.com");
+cli.set_compress(true);    // Compress request body
+cli.set_decompress(true);  // Decompress response body
+
+
+httplib::Client cli("https://example.com");
+cli.set_compress(true);    // Compress request body
+cli.set_decompress(true);  // Decompress response body
+
+
+

Proxy

+

You can connect through an HTTP proxy.

+
+httplib::Client cli("https://example.com");
+cli.set_proxy("proxy.example.com", 8080);
+cli.set_proxy_basic_auth("user", "password");
+
+
+httplib::Client cli("https://example.com");
+cli.set_proxy("proxy.example.com", 8080);
+cli.set_proxy_basic_auth("user", "password");
+
+
+

Timeouts

+

You can set connection, read, and write timeouts individually.

+
+httplib::Client cli("https://example.com");
+cli.set_connection_timeout(5, 0);  // 5 seconds
+cli.set_read_timeout(10, 0);       // 10 seconds
+cli.set_write_timeout(10, 0);      // 10 seconds
+
+
+httplib::Client cli("https://example.com");
+cli.set_connection_timeout(5, 0);  // 5 seconds
+cli.set_read_timeout(10, 0);       // 10 seconds
+cli.set_write_timeout(10, 0);      // 10 seconds
+
+
+

Keep-Alive

+

If you're making multiple requests to the same server, enable Keep-Alive. It reuses the TCP connection, which is much more efficient.

+
+httplib::Client cli("https://example.com");
+cli.set_keep_alive(true);
+
+
+httplib::Client cli("https://example.com");
+cli.set_keep_alive(true);
+
+
+

Server Middleware

+

You can hook into request processing before and after handlers run.

+
+svr.set_pre_routing_handler([](const auto &req, auto &res) {
+    // Runs before every request
+    return httplib::Server::HandlerResponse::Unhandled;  // Continue to normal routing
+});
+
+svr.set_post_routing_handler([](const auto &req, auto &res) {
+    // Runs after the response is sent
+    res.set_header("X-Server", "cpp-httplib");
+});
+
+
+svr.set_pre_routing_handler([](const auto &req, auto &res) {
+    // Runs before every request
+    return httplib::Server::HandlerResponse::Unhandled;  // Continue to normal routing
+});
+
+svr.set_post_routing_handler([](const auto &req, auto &res) {
+    // Runs after the response is sent
+    res.set_header("X-Server", "cpp-httplib");
+});
+
+
+

Use req.user_data to pass data from middleware to handlers. This is useful for sharing things like decoded auth tokens.

+
+svr.set_pre_routing_handler([](const auto &req, auto &res) {
+    req.user_data["auth_user"] = std::string("alice");
+    return httplib::Server::HandlerResponse::Unhandled;
+});
+
+svr.Get("/me", [](const auto &req, auto &res) {
+    auto user = std::any_cast<std::string>(req.user_data.at("auth_user"));
+    res.set_content("Hello, " + user, "text/plain");
+});
+
+
+svr.set_pre_routing_handler([](const auto &req, auto &res) {
+    req.user_data["auth_user"] = std::string("alice");
+    return httplib::Server::HandlerResponse::Unhandled;
+});
+
+svr.Get("/me", [](const auto &req, auto &res) {
+    auto user = std::any_cast<std::string>(req.user_data.at("auth_user"));
+    res.set_content("Hello, " + user, "text/plain");
+});
+
+
+

You can also customize error and exception handlers.

+
+svr.set_error_handler([](const auto &req, auto &res) {
+    res.set_content("Custom Error Page", "text/html");
+});
+
+svr.set_exception_handler([](const auto &req, auto &res, std::exception_ptr ep) {
+    res.status = 500;
+    res.set_content("Internal Server Error", "text/plain");
+});
+
+
+svr.set_error_handler([](const auto &req, auto &res) {
+    res.set_content("Custom Error Page", "text/html");
+});
+
+svr.set_exception_handler([](const auto &req, auto &res, std::exception_ptr ep) {
+    res.status = 500;
+    res.set_content("Internal Server Error", "text/plain");
+});
+
+
+

Logging

+

You can set a logger on both the server and the client.

+
+svr.set_logger([](const auto &req, const auto &res) {
+    std::cout << req.method << " " << req.path << " " << res.status << std::endl;
+});
+
+
+svr.set_logger([](const auto &req, const auto &res) {
+    std::cout << req.method << " " << req.path << " " << res.status << std::endl;
+});
+
+
+

Unix Domain Socket

+

In addition to TCP, we support Unix Domain Sockets. You can use them for inter-process communication on the same machine.

+
+// Server
+httplib::Server svr;
+svr.set_address_family(AF_UNIX);
+svr.listen("/tmp/httplib.sock", 0);
+
+
+// Server
+httplib::Server svr;
+svr.set_address_family(AF_UNIX);
+svr.listen("/tmp/httplib.sock", 0);
+
+
+// Client
+httplib::Client cli("http://localhost");
+cli.set_address_family(AF_UNIX);
+cli.set_hostname_addr_map({{"localhost", "/tmp/httplib.sock"}});
+
+auto res = cli.Get("/");
+
+
+// Client
+httplib::Client cli("http://localhost");
+cli.set_address_family(AF_UNIX);
+cli.set_hostname_addr_map({{"localhost", "/tmp/httplib.sock"}});
+
+auto res = cli.Get("/");
+
+
+

Learn More

+

Want to dig deeper? Check out these resources.

+
    +
  • Cookbook — A collection of recipes for common use cases
  • +
  • README — Full API reference
  • +
  • README-sse — How to use Server-Sent Events
  • +
  • README-stream — How to use the Streaming API
  • +
  • README-websocket — How to use the WebSocket server
  • +
+ +
+
+ +
+ +
+ © 2025 yhirose. All rights reserved. +
+ + + + diff --git a/docs/en/tour/index.html b/docs/en/tour/index.html new file mode 100644 index 0000000..ce8502e --- /dev/null +++ b/docs/en/tour/index.html @@ -0,0 +1,105 @@ + + + + + + A Tour of cpp-httplib - cpp-httplib + + + + +
+
+ cpp-httplib v0.35.0 +
+ +
+ +
+ + +
+
+ +
+
+ + + +
+ + +
+
+

A Tour of cpp-httplib

+

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. Getting Started — Get httplib.h and build a Hello World server
  2. +
  3. Basic Client — Send GET/POST requests and use path parameters
  4. +
  5. Basic Server — Routing, path parameters, and building responses
  6. +
  7. Static File Server — Serve static files
  8. +
  9. TLS Setup — Set up OpenSSL / mbedTLS
  10. +
  11. HTTPS Client — Make requests to HTTPS sites
  12. +
  13. HTTPS Server — Build an HTTPS server
  14. +
  15. WebSocket — Learn the basics of WebSocket communication
  16. +
  17. What's Next — Explore more features
  18. +
+ +
+
+ +
+ +
+ © 2025 yhirose. All rights reserved. +
+ + + + diff --git a/docs/index.html b/docs/index.html new file mode 100644 index 0000000..7362714 --- /dev/null +++ b/docs/index.html @@ -0,0 +1,17 @@ + + + + + + +Redirecting... + + +

Redirecting to /en/...

+ + \ No newline at end of file diff --git a/docs/ja/cookbook/index.html b/docs/ja/cookbook/index.html new file mode 100644 index 0000000..5fd34c2 --- /dev/null +++ b/docs/ja/cookbook/index.html @@ -0,0 +1,73 @@ + + + + + + Cookbook - cpp-httplib + + + + +
+
+ cpp-httplib v0.35.0 +
+ +
+ +
+ + +
+
+ +
+
+ + + +
+ + +
+
+

Cookbook

+

This section is under construction.

+

Check back soon for a collection of recipes organized by topic.

+ +
+
+ +
+ +
+ © 2025 yhirose. All rights reserved. +
+ + + + diff --git a/docs/ja/index.html b/docs/ja/index.html new file mode 100644 index 0000000..d4e61d2 --- /dev/null +++ b/docs/ja/index.html @@ -0,0 +1,72 @@ + + + + + + cpp-httplib - cpp-httplib + + + + +
+
+ cpp-httplib v0.35.0 +
+ +
+ +
+ + +
+
+ +
+
+ + + +
+ +
+
+

cpp-httplib

+

cpp-httplibは、C++用のHTTP/HTTPSライブラリです。httplib.h というヘッダーファイルを1枚コピーするだけで使えます。

+

C++でちょっとしたHTTPサーバーやクライアントが必要になったとき、すぐに動くものが欲しいですよね。cpp-httplibはまさにそのために作られました。サーバーもクライアントも、数行のコードで書き始められます。

+

APIはラムダ式をベースにした直感的な設計で、C++11以降のコンパイラーがあればどこでも動きます。Windows、macOS、Linux — お使いの環境をそのまま使えます。

+

HTTPSも使えます。OpenSSLやmbedTLSをリンクするだけで、サーバー・クライアントの両方がTLSに対応します。Content-Encoding(gzip, brotli等)、ファイルアップロードなど、実際の開発で必要になる機能もひと通り揃っています。WebSocketもサポートしています。

+

内部的にはブロッキングI/Oとスレッドプールを使っています。大量の同時接続を捌くような用途には向きませんが、APIサーバーやツールの組み込みHTTP、テスト用のモックサーバーなど、多くのユースケースで十分な性能を発揮します。

+

「今日の課題を、今日中に解決する」— cpp-httplibが目指しているのは、そういうシンプルさです。

+

ドキュメント

+
    +
  • A Tour of cpp-httplib — 基本を順を追って学べるチュートリアル。初めての方はここから
  • +
  • Cookbook — 目的別のレシピ集。必要なトピックから読めます
  • +
+ +
+
+ +
+ +
+ © 2025 yhirose. All rights reserved. +
+ + + + diff --git a/docs/ja/tour/01-getting-started/index.html b/docs/ja/tour/01-getting-started/index.html new file mode 100644 index 0000000..0ba405f --- /dev/null +++ b/docs/ja/tour/01-getting-started/index.html @@ -0,0 +1,198 @@ + + + + + + Getting Started - cpp-httplib + + + + +
+
+ cpp-httplib v0.35.0 +
+ +
+ +
+ + +
+
+ +
+
+ + + +
+ + +
+
+

Getting Started

+

cpp-httplibを始めるのに必要なのは、httplib.hとC++コンパイラーだけです。ファイルをダウンロードして、Hello Worldサーバーを動かすところまでやってみましょう。

+

httplib.h の入手

+

GitHubから直接ダウンロードできます。常に最新版を使ってください。

+
+curl -LO https://github.com/yhirose/cpp-httplib/raw/refs/tags/latest/httplib.h
+
+
+curl -LO https://github.com/yhirose/cpp-httplib/raw/refs/tags/latest/httplib.h
+
+
+

ダウンロードした httplib.h をプロジェクトのディレクトリに置けば、準備完了です。

+

コンパイラーの準備

+ + + + +
OS開発環境セットアップ
macOSApple ClangXcode Command Line Tools (xcode-select --install)
Ubuntuclang++ または g++apt install clang または apt install g++
WindowsMSVCVisual Studio 2022 以降(C++ コンポーネントを含めてインストール)
+

Hello World サーバー

+

次のコードを server.cpp として保存しましょう。

+
+#include "httplib.h"
+
+int main() {
+    httplib::Server svr;
+
+    svr.Get("/", [](const httplib::Request&, httplib::Response& res) {
+        res.set_content("Hello, World!", "text/plain");
+    });
+
+    svr.listen("0.0.0.0", 8080);
+}
+
+
+#include "httplib.h"
+
+int main() {
+    httplib::Server svr;
+
+    svr.Get("/", [](const httplib::Request&, httplib::Response& res) {
+        res.set_content("Hello, World!", "text/plain");
+    });
+
+    svr.listen("0.0.0.0", 8080);
+}
+
+
+

たった数行で、HTTPリクエストに応答するサーバーが書けます。

+

コンパイルと実行

+

このチュートリアルのサンプルコードは、コードを簡潔に書けるC++17で書いています。cpp-httplib自体はC++11でもコンパイルできます。

+
+# macOS
+clang++ -std=c++17 -o server server.cpp
+
+# Linux
+# `-pthread`: cpp-httplibは内部でスレッドを使用
+clang++ -std=c++17 -pthread -o server server.cpp
+
+# Windows (Developer Command Prompt)
+# `/EHsc`: C++例外処理を有効化
+cl /EHsc /std:c++17 server.cpp
+
+
+# macOS
+clang++ -std=c++17 -o server server.cpp
+
+# Linux
+# `-pthread`: cpp-httplibは内部でスレッドを使用
+clang++ -std=c++17 -pthread -o server server.cpp
+
+# Windows (Developer Command Prompt)
+# `/EHsc`: C++例外処理を有効化
+cl /EHsc /std:c++17 server.cpp
+
+
+

コンパイルできたら実行します。

+
+# macOS / Linux
+./server
+
+# Windows
+server.exe
+
+
+# macOS / Linux
+./server
+
+# Windows
+server.exe
+
+
+

ブラウザで http://localhost:8080 を開いてください。"Hello, World!" と表示されれば成功です。

+

curl でも確認できます。

+
+curl http://localhost:8080/
+# Hello, World!
+
+
+curl http://localhost:8080/
+# Hello, World!
+
+
+

サーバーを停止するには、ターミナルで Ctrl+C を押します。

+

次のステップ

+

サーバーの基本がわかりましたね。次は、クライアント側を見てみましょう。cpp-httplibはHTTPクライアント機能も備えています。

+

次: Basic Client

+ +
+
+ +
+ +
+ © 2025 yhirose. All rights reserved. +
+ + + + diff --git a/docs/ja/tour/02-basic-client/index.html b/docs/ja/tour/02-basic-client/index.html new file mode 100644 index 0000000..aa243df --- /dev/null +++ b/docs/ja/tour/02-basic-client/index.html @@ -0,0 +1,491 @@ + + + + + + Basic Client - cpp-httplib + + + + +
+
+ cpp-httplib v0.35.0 +
+ +
+ +
+ + +
+
+ +
+
+ + + +
+ + +
+
+

Basic Client

+

cpp-httplibはサーバーだけでなく、HTTPクライアント機能も備えています。httplib::Client を使って、GETやPOSTリクエストを送ってみましょう。

+

テスト用サーバーの準備

+

クライアントの動作を確認するために、リクエストを受け付けるサーバーを用意します。次のコードを保存し、前章と同じ手順でコンパイル・実行してください。サーバーの詳しい解説は次章で行います。

+
+#include "httplib.h"
+#include <iostream>
+
+int main() {
+    httplib::Server svr;
+
+    svr.Get("/hi", [](const auto &, auto &res) {
+        res.set_content("Hello!", "text/plain");
+    });
+
+    svr.Get("/search", [](const auto &req, auto &res) {
+        auto q = req.get_param_value("q");
+        res.set_content("Query: " + q, "text/plain");
+    });
+
+    svr.Post("/post", [](const auto &req, auto &res) {
+        res.set_content(req.body, "text/plain");
+    });
+
+    svr.Post("/submit", [](const auto &req, auto &res) {
+        std::string result;
+        for (auto &[key, val] : req.params) {
+            result += key + " = " + val + "\n";
+        }
+        res.set_content(result, "text/plain");
+    });
+
+    svr.Post("/upload", [](const auto &req, auto &res) {
+        auto f = req.form.get_file("file");
+        auto content = f.filename + " (" + std::to_string(f.content.size()) + " bytes)";
+        res.set_content(content, "text/plain");
+    });
+
+    svr.Get("/users/:id", [](const auto &req, auto &res) {
+        auto id = req.path_params.at("id");
+        res.set_content("User ID: " + id, "text/plain");
+    });
+
+    svr.Get(R"(/files/(\d+))", [](const auto &req, auto &res) {
+        auto id = req.matches[1];
+        res.set_content("File ID: " + std::string(id), "text/plain");
+    });
+
+    std::cout << "Listening on port 8080..." << std::endl;
+    svr.listen("0.0.0.0", 8080);
+}
+
+
+#include "httplib.h"
+#include <iostream>
+
+int main() {
+    httplib::Server svr;
+
+    svr.Get("/hi", [](const auto &, auto &res) {
+        res.set_content("Hello!", "text/plain");
+    });
+
+    svr.Get("/search", [](const auto &req, auto &res) {
+        auto q = req.get_param_value("q");
+        res.set_content("Query: " + q, "text/plain");
+    });
+
+    svr.Post("/post", [](const auto &req, auto &res) {
+        res.set_content(req.body, "text/plain");
+    });
+
+    svr.Post("/submit", [](const auto &req, auto &res) {
+        std::string result;
+        for (auto &[key, val] : req.params) {
+            result += key + " = " + val + "\n";
+        }
+        res.set_content(result, "text/plain");
+    });
+
+    svr.Post("/upload", [](const auto &req, auto &res) {
+        auto f = req.form.get_file("file");
+        auto content = f.filename + " (" + std::to_string(f.content.size()) + " bytes)";
+        res.set_content(content, "text/plain");
+    });
+
+    svr.Get("/users/:id", [](const auto &req, auto &res) {
+        auto id = req.path_params.at("id");
+        res.set_content("User ID: " + id, "text/plain");
+    });
+
+    svr.Get(R"(/files/(\d+))", [](const auto &req, auto &res) {
+        auto id = req.matches[1];
+        res.set_content("File ID: " + std::string(id), "text/plain");
+    });
+
+    std::cout << "Listening on port 8080..." << std::endl;
+    svr.listen("0.0.0.0", 8080);
+}
+
+
+

GETリクエスト

+

サーバーが起動したら、別のターミナルを開いて試してみましょう。まず、最もシンプルなGETリクエストです。

+
+#include "httplib.h"
+#include <iostream>
+
+int main() {
+    httplib::Client cli("http://localhost:8080");
+
+    auto res = cli.Get("/hi");
+    if (res) {
+        std::cout << res->status << std::endl;  // 200
+        std::cout << res->body << std::endl;    // Hello!
+    }
+}
+
+
+#include "httplib.h"
+#include <iostream>
+
+int main() {
+    httplib::Client cli("http://localhost:8080");
+
+    auto res = cli.Get("/hi");
+    if (res) {
+        std::cout << res->status << std::endl;  // 200
+        std::cout << res->body << std::endl;    // Hello!
+    }
+}
+
+
+

httplib::Client のコンストラクターにサーバーのアドレスを渡し、Get() でリクエストを送ります。戻り値の res からステータスコードやボディを取得できます。

+

対応する curl コマンドはこうなります。

+
+curl http://localhost:8080/hi
+# Hello!
+
+
+curl http://localhost:8080/hi
+# Hello!
+
+
+

レスポンスの確認

+

レスポンスには、ステータスコードとボディ以外にもヘッダー情報が含まれています。

+
+auto res = cli.Get("/hi");
+if (res) {
+    // ステータスコード
+    std::cout << res->status << std::endl;  // 200
+
+    // ボディ
+    std::cout << res->body << std::endl;  // Hello!
+
+    // ヘッダー
+    std::cout << res->get_header_value("Content-Type") << std::endl;  // text/plain
+}
+
+
+auto res = cli.Get("/hi");
+if (res) {
+    // ステータスコード
+    std::cout << res->status << std::endl;  // 200
+
+    // ボディ
+    std::cout << res->body << std::endl;  // Hello!
+
+    // ヘッダー
+    std::cout << res->get_header_value("Content-Type") << std::endl;  // text/plain
+}
+
+
+

res->bodystd::string なので、JSON レスポンスをパースしたい場合は nlohmann/json などの JSON ライブラリにそのまま渡せます。

+

クエリパラメーター

+

GETリクエストにクエリパラメーターを付けるには、URLに直接書くか、httplib::Params を使います。

+
+auto res = cli.Get("/search", httplib::Params{{"q", "cpp-httplib"}});
+if (res) {
+    std::cout << res->body << std::endl;  // Query: cpp-httplib
+}
+
+
+auto res = cli.Get("/search", httplib::Params{{"q", "cpp-httplib"}});
+if (res) {
+    std::cout << res->body << std::endl;  // Query: cpp-httplib
+}
+
+
+

httplib::Params を使うと、特殊文字のURLエンコードを自動で行ってくれます。

+
+curl "http://localhost:8080/search?q=cpp-httplib"
+# Query: cpp-httplib
+
+
+curl "http://localhost:8080/search?q=cpp-httplib"
+# Query: cpp-httplib
+
+
+

パスパラメーター

+

URLのパスに値を直接埋め込む場合も、クライアント側は特別なAPIは不要です。パスをそのまま Get() に渡すだけです。

+
+auto res = cli.Get("/users/42");
+if (res) {
+    std::cout << res->body << std::endl;  // User ID: 42
+}
+
+
+auto res = cli.Get("/users/42");
+if (res) {
+    std::cout << res->body << std::endl;  // User ID: 42
+}
+
+
+curl http://localhost:8080/users/42
+# User ID: 42
+
+
+curl http://localhost:8080/users/42
+# User ID: 42
+
+
+

テスト用サーバーには、正規表現でIDを数字のみに絞った /files/(\d+) もあります。

+
+auto res = cli.Get("/files/42");
+if (res) {
+    std::cout << res->body << std::endl;  // File ID: 42
+}
+
+
+auto res = cli.Get("/files/42");
+if (res) {
+    std::cout << res->body << std::endl;  // File ID: 42
+}
+
+
+curl http://localhost:8080/files/42
+# File ID: 42
+
+
+curl http://localhost:8080/files/42
+# File ID: 42
+
+
+

/files/abc のように数字以外を渡すと404が返ります。仕組みは次章で解説します。

+

リクエストヘッダー

+

カスタムHTTPヘッダーを付けるには、httplib::Headers を渡します。Get()Post() のどちらでも使えます。

+
+auto res = cli.Get("/hi", httplib::Headers{
+    {"Authorization", "Bearer my-token"}
+});
+
+
+auto res = cli.Get("/hi", httplib::Headers{
+    {"Authorization", "Bearer my-token"}
+});
+
+
+curl -H "Authorization: Bearer my-token" http://localhost:8080/hi
+
+
+curl -H "Authorization: Bearer my-token" http://localhost:8080/hi
+
+
+

POSTリクエスト

+

テキストデータをPOSTしてみましょう。Post() の第2引数にボディ、第3引数にContent-Typeを指定します。

+
+auto res = cli.Post("/post", "Hello, Server!", "text/plain");
+if (res) {
+    std::cout << res->status << std::endl;  // 200
+    std::cout << res->body << std::endl;    // Hello, Server!
+}
+
+
+auto res = cli.Post("/post", "Hello, Server!", "text/plain");
+if (res) {
+    std::cout << res->status << std::endl;  // 200
+    std::cout << res->body << std::endl;    // Hello, Server!
+}
+
+
+

テスト用サーバーの /post はボディをそのまま返すので、送った文字列がそのまま返ってきます。

+
+curl -X POST -H "Content-Type: text/plain" -d "Hello, Server!" http://localhost:8080/post
+# Hello, Server!
+
+
+curl -X POST -H "Content-Type: text/plain" -d "Hello, Server!" http://localhost:8080/post
+# Hello, Server!
+
+
+

フォームデータの送信

+

HTMLフォームのように、キーと値のペアを送ることもできます。httplib::Params を使います。

+
+auto res = cli.Post("/submit", httplib::Params{
+    {"name", "Alice"},
+    {"age", "30"}
+});
+if (res) {
+    std::cout << res->body << std::endl;
+    // age = 30
+    // name = Alice
+}
+
+
+auto res = cli.Post("/submit", httplib::Params{
+    {"name", "Alice"},
+    {"age", "30"}
+});
+if (res) {
+    std::cout << res->body << std::endl;
+    // age = 30
+    // name = Alice
+}
+
+
+

これは application/x-www-form-urlencoded 形式で送信されます。

+
+curl -X POST -d "name=Alice&age=30" http://localhost:8080/submit
+
+
+curl -X POST -d "name=Alice&age=30" http://localhost:8080/submit
+
+
+

ファイルのPOST

+

ファイルをアップロードするには、httplib::UploadFormDataItems を使ってマルチパートフォームデータとして送信します。

+
+auto res = cli.Post("/upload", httplib::UploadFormDataItems{
+    {"file", "Hello, File!", "hello.txt", "text/plain"}
+});
+if (res) {
+    std::cout << res->body << std::endl;  // hello.txt (12 bytes)
+}
+
+
+auto res = cli.Post("/upload", httplib::UploadFormDataItems{
+    {"file", "Hello, File!", "hello.txt", "text/plain"}
+});
+if (res) {
+    std::cout << res->body << std::endl;  // hello.txt (12 bytes)
+}
+
+
+

UploadFormDataItems の各要素は {name, content, filename, content_type} の4つのフィールドで構成されます。

+
+curl -F "file=Hello, File!;filename=hello.txt;type=text/plain" http://localhost:8080/upload
+
+
+curl -F "file=Hello, File!;filename=hello.txt;type=text/plain" http://localhost:8080/upload
+
+
+

エラーハンドリング

+

ネットワーク通信では、サーバーに接続できない場合があります。res が有効かどうかを必ず確認しましょう。

+
+httplib::Client cli("http://localhost:9999");  // 存在しないポート
+auto res = cli.Get("/hi");
+
+if (!res) {
+    // 接続エラー
+    std::cout << "Error: " << httplib::to_string(res.error()) << std::endl;
+    // Error: Connection
+    return 1;
+}
+
+// ここに到達すればレスポンスを受信できている
+if (res->status != 200) {
+    std::cout << "HTTP Error: " << res->status << std::endl;
+    return 1;
+}
+
+std::cout << res->body << std::endl;
+
+
+httplib::Client cli("http://localhost:9999");  // 存在しないポート
+auto res = cli.Get("/hi");
+
+if (!res) {
+    // 接続エラー
+    std::cout << "Error: " << httplib::to_string(res.error()) << std::endl;
+    // Error: Connection
+    return 1;
+}
+
+// ここに到達すればレスポンスを受信できている
+if (res->status != 200) {
+    std::cout << "HTTP Error: " << res->status << std::endl;
+    return 1;
+}
+
+std::cout << res->body << std::endl;
+
+
+

エラーには2つのレベルがあります。

+
    +
  • 接続エラー: サーバーに到達できなかった場合。res が偽になり、res.error() でエラーの種類を取得できます
  • +
  • HTTPエラー: サーバーからエラーステータス(404、500など)が返ってきた場合。res は真ですが、res->status を確認する必要があります
  • +
+

次のステップ

+

クライアントからリクエストを送る方法がわかりました。次は、サーバー側をもっと詳しく見てみましょう。ルーティングやパスパラメータなど、サーバーの機能を掘り下げます。

+

次: Basic Server

+ +
+
+ +
+ +
+ © 2025 yhirose. All rights reserved. +
+ + + + diff --git a/docs/ja/tour/03-basic-server/index.html b/docs/ja/tour/03-basic-server/index.html new file mode 100644 index 0000000..9c0493b --- /dev/null +++ b/docs/ja/tour/03-basic-server/index.html @@ -0,0 +1,446 @@ + + + + + + Basic Server - cpp-httplib + + + + +
+
+ cpp-httplib v0.35.0 +
+ +
+ +
+ + +
+
+ +
+
+ + + +
+ + +
+
+

Basic Server

+

前章ではクライアントからリクエストを送りました。そのとき、テスト用サーバーを用意しましたね。この章では、あのサーバーの仕組みをひとつずつ紐解いていきます。

+

サーバーの起動

+

ルーティングを登録したら、最後に svr.listen() を呼んでサーバーを起動します。

+
+svr.listen("0.0.0.0", 8080);
+
+
+svr.listen("0.0.0.0", 8080);
+
+
+

第1引数はホスト、第2引数はポート番号です。"0.0.0.0" を指定すると、すべてのネットワークインターフェースでリクエストを受け付けます。自分のマシンからのアクセスだけに限定したいときは "127.0.0.1" を使います。

+

listen() はブロッキング呼び出しです。サーバーが停止するまで、この行から先には進みません。ターミナルで Ctrl+C を押すか、別スレッドから svr.stop() を呼ぶまでサーバーは動き続けます。

+

ルーティング

+

サーバーの核になるのは「ルーティング」です。どのURLに、どのHTTPメソッドでアクセスされたら、何をするか。それを登録する仕組みです。

+
+httplib::Server svr;
+
+svr.Get("/hi", [](const httplib::Request &req, httplib::Response &res) {
+    res.set_content("Hello!", "text/plain");
+});
+
+
+httplib::Server svr;
+
+svr.Get("/hi", [](const httplib::Request &req, httplib::Response &res) {
+    res.set_content("Hello!", "text/plain");
+});
+
+
+

svr.Get() は、GETリクエストに対するハンドラーを登録します。第1引数がパス、第2引数がハンドラー関数です。/hi にGETリクエストが来たら、このラムダが呼ばれます。

+

HTTPメソッドごとにメソッドが用意されています。

+
+svr.Get("/path",    handler);  // GET
+svr.Post("/path",   handler);  // POST
+svr.Put("/path",    handler);  // PUT
+svr.Delete("/path", handler);  // DELETE
+
+
+svr.Get("/path",    handler);  // GET
+svr.Post("/path",   handler);  // POST
+svr.Put("/path",    handler);  // PUT
+svr.Delete("/path", handler);  // DELETE
+
+
+

ハンドラーのシグネチャは (const httplib::Request &req, httplib::Response &res) です。auto を使って短く書くこともできます。

+
+svr.Get("/hi", [](const auto &req, auto &res) {
+    res.set_content("Hello!", "text/plain");
+});
+
+
+svr.Get("/hi", [](const auto &req, auto &res) {
+    res.set_content("Hello!", "text/plain");
+});
+
+
+

パスが一致したときだけハンドラーが呼ばれます。登録されていないパスにアクセスすると、自動的に404が返ります。

+

リクエストオブジェクト

+

ハンドラーの第1引数 req から、クライアントが送ってきた情報を読み取れます。

+

ボディ

+

req.body でリクエストボディを取得できます。型は std::string です。

+
+svr.Post("/post", [](const auto &req, auto &res) {
+    // クライアントが送ったボディをそのまま返す
+    res.set_content(req.body, "text/plain");
+});
+
+
+svr.Post("/post", [](const auto &req, auto &res) {
+    // クライアントが送ったボディをそのまま返す
+    res.set_content(req.body, "text/plain");
+});
+
+
+

ヘッダー

+

req.get_header_value() でリクエストヘッダーの値を取得できます。

+
+svr.Get("/check", [](const auto &req, auto &res) {
+    auto auth = req.get_header_value("Authorization");
+    res.set_content("Auth: " + auth, "text/plain");
+});
+
+
+svr.Get("/check", [](const auto &req, auto &res) {
+    auto auth = req.get_header_value("Authorization");
+    res.set_content("Auth: " + auth, "text/plain");
+});
+
+
+

クエリパラメーターとフォームデータ

+

req.get_param_value() でパラメーターを取得できます。GETのクエリパラメーターと、POSTのフォームデータの両方に使えます。

+
+svr.Get("/search", [](const auto &req, auto &res) {
+    auto q = req.get_param_value("q");
+    res.set_content("Query: " + q, "text/plain");
+});
+
+
+svr.Get("/search", [](const auto &req, auto &res) {
+    auto q = req.get_param_value("q");
+    res.set_content("Query: " + q, "text/plain");
+});
+
+
+

/search?q=cpp-httplib にアクセスすると、q の値は "cpp-httplib" になります。

+

すべてのパラメーターをループで処理したいときは、req.params を使います。

+
+svr.Post("/submit", [](const auto &req, auto &res) {
+    std::string result;
+    for (auto &[key, val] : req.params) {
+        result += key + " = " + val + "\n";
+    }
+    res.set_content(result, "text/plain");
+});
+
+
+svr.Post("/submit", [](const auto &req, auto &res) {
+    std::string result;
+    for (auto &[key, val] : req.params) {
+        result += key + " = " + val + "\n";
+    }
+    res.set_content(result, "text/plain");
+});
+
+
+

ファイルアップロード

+

マルチパートフォームでアップロードされたファイルは、req.form.get_file() で取得します。

+
+svr.Post("/upload", [](const auto &req, auto &res) {
+    auto f = req.form.get_file("file");
+    auto content = f.filename + " (" + std::to_string(f.content.size()) + " bytes)";
+    res.set_content(content, "text/plain");
+});
+
+
+svr.Post("/upload", [](const auto &req, auto &res) {
+    auto f = req.form.get_file("file");
+    auto content = f.filename + " (" + std::to_string(f.content.size()) + " bytes)";
+    res.set_content(content, "text/plain");
+});
+
+
+

f.filename でファイル名、f.content でファイルの中身にアクセスできます。

+

パスパラメーター

+

URLの一部を変数として受け取りたいことがあります。たとえば /users/4242 を取得したい場合です。:param 記法を使うと、URLの一部をキャプチャできます。

+
+svr.Get("/users/:id", [](const auto &req, auto &res) {
+    auto id = req.path_params.at("id");
+    res.set_content("User ID: " + id, "text/plain");
+});
+
+
+svr.Get("/users/:id", [](const auto &req, auto &res) {
+    auto id = req.path_params.at("id");
+    res.set_content("User ID: " + id, "text/plain");
+});
+
+
+

/users/42 にアクセスすると、req.path_params.at("id")"42" を返します。/users/100 なら "100" です。

+

複数のパスパラメーターも使えます。

+
+svr.Get("/users/:user_id/posts/:post_id", [](const auto &req, auto &res) {
+    auto user_id = req.path_params.at("user_id");
+    auto post_id = req.path_params.at("post_id");
+    res.set_content("User: " + user_id + ", Post: " + post_id, "text/plain");
+});
+
+
+svr.Get("/users/:user_id/posts/:post_id", [](const auto &req, auto &res) {
+    auto user_id = req.path_params.at("user_id");
+    auto post_id = req.path_params.at("post_id");
+    res.set_content("User: " + user_id + ", Post: " + post_id, "text/plain");
+});
+
+
+

正規表現パターン

+

:param の代わりに正規表現をパスに書くこともできます。キャプチャグループの値は req.matches で取得します。型は std::smatch です。

+
+// 数字のみのIDを受け付ける
+svr.Get(R"(/files/(\d+))", [](const auto &req, auto &res) {
+    auto id = req.matches[1];  // 最初のキャプチャグループ
+    res.set_content("File ID: " + std::string(id), "text/plain");
+});
+
+
+// 数字のみのIDを受け付ける
+svr.Get(R"(/files/(\d+))", [](const auto &req, auto &res) {
+    auto id = req.matches[1];  // 最初のキャプチャグループ
+    res.set_content("File ID: " + std::string(id), "text/plain");
+});
+
+
+

/files/42 にはマッチしますが、/files/abc にはマッチしません。入力値を絞り込みたいときに便利です。

+

レスポンスの組み立て

+

ハンドラーの第2引数 res を使って、クライアントに返すレスポンスを組み立てます。

+

ボディとContent-Type

+

res.set_content() でボディとContent-Typeを設定します。これだけでステータスコード200のレスポンスが返ります。

+
+svr.Get("/hi", [](const auto &req, auto &res) {
+    res.set_content("Hello!", "text/plain");
+});
+
+
+svr.Get("/hi", [](const auto &req, auto &res) {
+    res.set_content("Hello!", "text/plain");
+});
+
+
+

ステータスコード

+

ステータスコードを変えたいときは、res.status に代入します。

+
+svr.Get("/not-found", [](const auto &req, auto &res) {
+    res.status = 404;
+    res.set_content("Not found", "text/plain");
+});
+
+
+svr.Get("/not-found", [](const auto &req, auto &res) {
+    res.status = 404;
+    res.set_content("Not found", "text/plain");
+});
+
+
+

レスポンスヘッダー

+

res.set_header() でレスポンスヘッダーを追加できます。

+
+svr.Get("/with-header", [](const auto &req, auto &res) {
+    res.set_header("X-Custom", "my-value");
+    res.set_content("Hello!", "text/plain");
+});
+
+
+svr.Get("/with-header", [](const auto &req, auto &res) {
+    res.set_header("X-Custom", "my-value");
+    res.set_content("Hello!", "text/plain");
+});
+
+
+

前章のサーバーを読み解く

+

ここまでの知識を使って、前章で用意したテスト用サーバーを改めて見てみましょう。

+

GET /hi

+
+svr.Get("/hi", [](const auto &, auto &res) {
+    res.set_content("Hello!", "text/plain");
+});
+
+
+svr.Get("/hi", [](const auto &, auto &res) {
+    res.set_content("Hello!", "text/plain");
+});
+
+
+

最もシンプルなハンドラーです。リクエストの情報は使わないので、req の変数名を省略しています。"Hello!" というテキストをそのまま返します。

+

GET /search

+
+svr.Get("/search", [](const auto &req, auto &res) {
+    auto q = req.get_param_value("q");
+    res.set_content("Query: " + q, "text/plain");
+});
+
+
+svr.Get("/search", [](const auto &req, auto &res) {
+    auto q = req.get_param_value("q");
+    res.set_content("Query: " + q, "text/plain");
+});
+
+
+

req.get_param_value("q") でクエリパラメーター q の値を取り出します。/search?q=cpp-httplib なら、レスポンスは "Query: cpp-httplib" になります。

+

POST /post

+
+svr.Post("/post", [](const auto &req, auto &res) {
+    res.set_content(req.body, "text/plain");
+});
+
+
+svr.Post("/post", [](const auto &req, auto &res) {
+    res.set_content(req.body, "text/plain");
+});
+
+
+

クライアントが送ったリクエストボディを、そのままレスポンスとして返すエコーサーバーです。req.body にボディが丸ごと入っています。

+

POST /submit

+
+svr.Post("/submit", [](const auto &req, auto &res) {
+    std::string result;
+    for (auto &[key, val] : req.params) {
+        result += key + " = " + val + "\n";
+    }
+    res.set_content(result, "text/plain");
+});
+
+
+svr.Post("/submit", [](const auto &req, auto &res) {
+    std::string result;
+    for (auto &[key, val] : req.params) {
+        result += key + " = " + val + "\n";
+    }
+    res.set_content(result, "text/plain");
+});
+
+
+

フォームデータとして送られたキーと値のペアを、req.params でループ処理しています。構造化束縛 auto &[key, val] を使って、各ペアを取り出しています。

+

POST /upload

+
+svr.Post("/upload", [](const auto &req, auto &res) {
+    auto f = req.form.get_file("file");
+    auto content = f.filename + " (" + std::to_string(f.content.size()) + " bytes)";
+    res.set_content(content, "text/plain");
+});
+
+
+svr.Post("/upload", [](const auto &req, auto &res) {
+    auto f = req.form.get_file("file");
+    auto content = f.filename + " (" + std::to_string(f.content.size()) + " bytes)";
+    res.set_content(content, "text/plain");
+});
+
+
+

マルチパートフォームで送られたファイルを受け取ります。req.form.get_file("file")"file" という名前のフィールドを取得し、f.filenamef.content.size() でファイル名とサイズを返しています。

+

GET /users/:id

+
+svr.Get("/users/:id", [](const auto &req, auto &res) {
+    auto id = req.path_params.at("id");
+    res.set_content("User ID: " + id, "text/plain");
+});
+
+
+svr.Get("/users/:id", [](const auto &req, auto &res) {
+    auto id = req.path_params.at("id");
+    res.set_content("User ID: " + id, "text/plain");
+});
+
+
+

:id の部分がパスパラメーターです。req.path_params.at("id") で値を取り出しています。/users/42 なら "42"/users/alice なら "alice" が得られます。

+

GET /files/(\d+)

+
+svr.Get(R"(/files/(\d+))", [](const auto &req, auto &res) {
+    auto id = req.matches[1];
+    res.set_content("File ID: " + std::string(id), "text/plain");
+});
+
+
+svr.Get(R"(/files/(\d+))", [](const auto &req, auto &res) {
+    auto id = req.matches[1];
+    res.set_content("File ID: " + std::string(id), "text/plain");
+});
+
+
+

正規表現 (\d+) で数字だけのIDにマッチします。/files/42 にはマッチしますが、/files/abc は404になります。req.matches[1] で最初のキャプチャグループの値を取得しています。

+

次のステップ

+

サーバーの基本がわかりましたね。ルーティング、リクエストの読み取り、レスポンスの組み立て。これだけで、十分に実用的なAPIサーバーが作れます。

+

次は、静的ファイルの配信を見てみましょう。HTMLやCSSを配信するサーバーを作ります。

+

次: Static File Server

+ +
+
+ +
+ +
+ © 2025 yhirose. All rights reserved. +
+ + + + diff --git a/docs/ja/tour/04-static-file-server/index.html b/docs/ja/tour/04-static-file-server/index.html new file mode 100644 index 0000000..206f1d0 --- /dev/null +++ b/docs/ja/tour/04-static-file-server/index.html @@ -0,0 +1,269 @@ + + + + + + Static File Server - cpp-httplib + + + + +
+
+ cpp-httplib v0.35.0 +
+ +
+ +
+ + +
+
+ +
+
+ + + +
+ + +
+
+

Static File Server

+

cpp-httplibは、HTMLやCSS、画像ファイルなどの静的ファイルも配信できます。面倒な設定は要りません。set_mount_point() を1行呼ぶだけです。

+

set_mount_point の基本

+

さっそくやってみましょう。set_mount_point() は、URLのパスとローカルディレクトリを紐づけます。

+
+#include "httplib.h"
+#include <iostream>
+
+int main() {
+    httplib::Server svr;
+
+    svr.set_mount_point("/", "./html");
+
+    std::cout << "Listening on port 8080..." << std::endl;
+    svr.listen("0.0.0.0", 8080);
+}
+
+
+#include "httplib.h"
+#include <iostream>
+
+int main() {
+    httplib::Server svr;
+
+    svr.set_mount_point("/", "./html");
+
+    std::cout << "Listening on port 8080..." << std::endl;
+    svr.listen("0.0.0.0", 8080);
+}
+
+
+

第1引数がURLのマウントポイント、第2引数がローカルのディレクトリパスです。この例だと、/ へのリクエストを ./html ディレクトリから配信します。

+

試してみましょう。まず html ディレクトリを作って、index.html を置きます。

+
+mkdir html
+
+
+mkdir html
+
+
+<!DOCTYPE html>
+<html>
+<head><title>My Page</title></head>
+<body>
+    <h1>Hello from cpp-httplib!</h1>
+    <p>This is a static file.</p>
+</body>
+</html>
+
+
+<!DOCTYPE html>
+<html>
+<head><title>My Page</title></head>
+<body>
+    <h1>Hello from cpp-httplib!</h1>
+    <p>This is a static file.</p>
+</body>
+</html>
+
+
+

コンパイルして起動します。

+
+g++ -std=c++17 -o server server.cpp -pthread
+./server
+
+
+g++ -std=c++17 -o server server.cpp -pthread
+./server
+
+
+

ブラウザで http://localhost:8080 を開いてみてください。html/index.html の内容が表示されるはずです。http://localhost:8080/index.html でも同じページが返ります。

+

もちろん、前章のクライアントコードや curl でもアクセスできますよ。

+
+httplib::Client cli("http://localhost:8080");
+auto res = cli.Get("/");
+if (res) {
+    std::cout << res->body << std::endl;  // HTMLが表示される
+}
+
+
+httplib::Client cli("http://localhost:8080");
+auto res = cli.Get("/");
+if (res) {
+    std::cout << res->body << std::endl;  // HTMLが表示される
+}
+
+
+curl http://localhost:8080
+
+
+curl http://localhost:8080
+
+
+

複数のマウントポイント

+

set_mount_point() は何回でも呼べます。URLのパスごとに、別々のディレクトリを割り当てられます。

+
+svr.set_mount_point("/", "./public");
+svr.set_mount_point("/assets", "./static/assets");
+svr.set_mount_point("/docs", "./documentation");
+
+
+svr.set_mount_point("/", "./public");
+svr.set_mount_point("/assets", "./static/assets");
+svr.set_mount_point("/docs", "./documentation");
+
+
+

/assets/style.css なら ./static/assets/style.css を、/docs/guide.html なら ./documentation/guide.html を配信します。

+

ハンドラーとの組み合わせ

+

静的ファイルの配信と、前章で学んだルーティングハンドラーは共存できます。

+
+httplib::Server svr;
+
+// APIエンドポイント
+svr.Get("/api/hello", [](const auto &, auto &res) {
+    res.set_content(R"({"message":"Hello!"})", "application/json");
+});
+
+// 静的ファイル配信
+svr.set_mount_point("/", "./public");
+
+svr.listen("0.0.0.0", 8080);
+
+
+httplib::Server svr;
+
+// APIエンドポイント
+svr.Get("/api/hello", [](const auto &, auto &res) {
+    res.set_content(R"({"message":"Hello!"})", "application/json");
+});
+
+// 静的ファイル配信
+svr.set_mount_point("/", "./public");
+
+svr.listen("0.0.0.0", 8080);
+
+
+

ハンドラーが先に評価されます。/api/hello にはハンドラーが応答し、それ以外のパスは ./public ディレクトリからファイルを探します。

+

レスポンスヘッダーの追加

+

set_mount_point() の第3引数にヘッダーを渡すと、静的ファイルのレスポンスにカスタムヘッダーを付けられます。キャッシュ制御に便利です。

+
+svr.set_mount_point("/", "./public", {
+    {"Cache-Control", "max-age=3600"}
+});
+
+
+svr.set_mount_point("/", "./public", {
+    {"Cache-Control", "max-age=3600"}
+});
+
+
+

こうすると、ブラウザは配信されたファイルを1時間キャッシュします。

+

静的ファイルサーバー用のDockerファイル

+

cpp-httplibのリポジトリには、静的ファイルサーバー用の Dockerfile が含まれています。Docker Hubにビルド済みイメージも公開しているので、1コマンドで起動できます。

+
+> docker run -p 8080:80 -v ./my-site:/html yhirose4dockerhub/cpp-httplib-server
+Serving HTTP on 0.0.0.0:80
+Mount point: / -> ./html
+Press Ctrl+C to shutdown gracefully...
+192.168.65.1 - - [22/Feb/2026:12:00:00 +0000] "GET / HTTP/1.1" 200 256 "-" "Mozilla/5.0 ..."
+192.168.65.1 - - [22/Feb/2026:12:00:00 +0000] "GET /style.css HTTP/1.1" 200 1024 "-" "Mozilla/5.0 ..."
+192.168.65.1 - - [22/Feb/2026:12:00:01 +0000] "GET /favicon.ico HTTP/1.1" 404 152 "-" "Mozilla/5.0 ..."
+
+
+> docker run -p 8080:80 -v ./my-site:/html yhirose4dockerhub/cpp-httplib-server
+Serving HTTP on 0.0.0.0:80
+Mount point: / -> ./html
+Press Ctrl+C to shutdown gracefully...
+192.168.65.1 - - [22/Feb/2026:12:00:00 +0000] "GET / HTTP/1.1" 200 256 "-" "Mozilla/5.0 ..."
+192.168.65.1 - - [22/Feb/2026:12:00:00 +0000] "GET /style.css HTTP/1.1" 200 1024 "-" "Mozilla/5.0 ..."
+192.168.65.1 - - [22/Feb/2026:12:00:01 +0000] "GET /favicon.ico HTTP/1.1" 404 152 "-" "Mozilla/5.0 ..."
+
+
+

./my-site ディレクトリの中身が、そのままポート8080で配信されます。NGINXと同じログ形式で、アクセスの様子を確認できますよ。

+

次のステップ

+

静的ファイルの配信ができるようになりましたね。HTMLやCSS、JavaScriptを配信するWebサーバーが、これだけのコードで作れます。

+

次は、HTTPSで暗号化通信をしてみましょう。まずはTLSライブラリのセットアップからです。

+

次: TLS Setup

+ +
+
+ +
+ +
+ © 2025 yhirose. All rights reserved. +
+ + + + diff --git a/docs/ja/tour/05-tls-setup/index.html b/docs/ja/tour/05-tls-setup/index.html new file mode 100644 index 0000000..908bba9 --- /dev/null +++ b/docs/ja/tour/05-tls-setup/index.html @@ -0,0 +1,193 @@ + + + + + + TLS Setup - cpp-httplib + + + + +
+
+ cpp-httplib v0.35.0 +
+ +
+ +
+ + +
+
+ +
+
+ + + +
+ + +
+
+

TLS Setup

+

ここまではHTTP(平文)でやってきましたが、実際のWebではHTTPS(暗号化通信)が当たり前ですよね。cpp-httplibでHTTPSを使うには、TLSライブラリが必要です。

+

このTourではOpenSSLを使います。最も広く使われていて、情報も豊富です。

+

OpenSSLのインストール

+

お使いのOSに合わせてインストールしましょう。

+ + + + +
OSインストール方法
macOSHomebrew (brew install openssl)
Ubuntu / Debiansudo apt install libssl-dev
Windowsvcpkg (vcpkg install openssl)
+

コンパイルオプション

+

TLS機能を有効にするには、CPPHTTPLIB_OPENSSL_SUPPORT マクロを定義してコンパイルします。前章までのコンパイルコマンドに、いくつかオプションが増えます。

+
+# macOS (Homebrew)
+clang++ -std=c++17 -DCPPHTTPLIB_OPENSSL_SUPPORT \
+    -I$(brew --prefix openssl)/include \
+    -L$(brew --prefix openssl)/lib \
+    -lssl -lcrypto \
+    -framework CoreFoundation -framework Security \
+    -o server server.cpp
+
+# Linux
+clang++ -std=c++17 -pthread -DCPPHTTPLIB_OPENSSL_SUPPORT \
+    -lssl -lcrypto \
+    -o server server.cpp
+
+# Windows (Developer Command Prompt)
+cl /EHsc /std:c++17 /DCPPHTTPLIB_OPENSSL_SUPPORT server.cpp libssl.lib libcrypto.lib
+
+
+# macOS (Homebrew)
+clang++ -std=c++17 -DCPPHTTPLIB_OPENSSL_SUPPORT \
+    -I$(brew --prefix openssl)/include \
+    -L$(brew --prefix openssl)/lib \
+    -lssl -lcrypto \
+    -framework CoreFoundation -framework Security \
+    -o server server.cpp
+
+# Linux
+clang++ -std=c++17 -pthread -DCPPHTTPLIB_OPENSSL_SUPPORT \
+    -lssl -lcrypto \
+    -o server server.cpp
+
+# Windows (Developer Command Prompt)
+cl /EHsc /std:c++17 /DCPPHTTPLIB_OPENSSL_SUPPORT server.cpp libssl.lib libcrypto.lib
+
+
+

それぞれのオプションの役割を見てみましょう。

+
    +
  • -DCPPHTTPLIB_OPENSSL_SUPPORT — TLS機能を有効にするマクロ定義
  • +
  • -lssl -lcrypto — OpenSSLのライブラリをリンク
  • +
  • -I / -L(macOSのみ)— Homebrew版OpenSSLのパスを指定
  • +
  • -framework CoreFoundation -framework Security(macOSのみ)— Keychainからシステム証明書を自動で読み込むために必要です
  • +
+

動作確認

+

ちゃんと動くか確認してみましょう。httplib::Client にHTTPSのURLを渡してアクセスするだけのプログラムです。

+
+#define CPPHTTPLIB_OPENSSL_SUPPORT
+#include "httplib.h"
+#include <iostream>
+
+int main() {
+    httplib::Client cli("https://www.google.com");
+
+    auto res = cli.Get("/");
+    if (res) {
+        std::cout << "Status: " << res->status << std::endl;
+    } else {
+        std::cout << "Error: " << httplib::to_string(res.error()) << std::endl;
+    }
+}
+
+
+#define CPPHTTPLIB_OPENSSL_SUPPORT
+#include "httplib.h"
+#include <iostream>
+
+int main() {
+    httplib::Client cli("https://www.google.com");
+
+    auto res = cli.Get("/");
+    if (res) {
+        std::cout << "Status: " << res->status << std::endl;
+    } else {
+        std::cout << "Error: " << httplib::to_string(res.error()) << std::endl;
+    }
+}
+
+
+

コンパイルして実行してみてください。Status: 200 と表示されれば、セットアップ完了です。

+

他のTLSバックエンド

+

cpp-httplibはOpenSSL以外にも、Mbed TLSとwolfSSLに対応しています。マクロ定義とリンクするライブラリを変えるだけで切り替えられます。

+ + + + +
バックエンドマクロ定義リンクするライブラリ
OpenSSLCPPHTTPLIB_OPENSSL_SUPPORTlibssl, libcrypto
Mbed TLSCPPHTTPLIB_MBEDTLS_SUPPORTlibmbedtls, libmbedx509, libmbedcrypto
wolfSSLCPPHTTPLIB_WOLFSSL_SUPPORTlibwolfssl
+

このTourではOpenSSLを前提に進めますが、APIはどのバックエンドでも共通です。

+

次のステップ

+

TLSの準備ができましたね。次は、HTTPSサイトにリクエストを送ってみましょう。

+

次: HTTPS Client

+ +
+
+ +
+ +
+ © 2025 yhirose. All rights reserved. +
+ + + + diff --git a/docs/ja/tour/06-https-client/index.html b/docs/ja/tour/06-https-client/index.html new file mode 100644 index 0000000..fbbfd3d --- /dev/null +++ b/docs/ja/tour/06-https-client/index.html @@ -0,0 +1,251 @@ + + + + + + HTTPS Client - cpp-httplib + + + + +
+
+ cpp-httplib v0.35.0 +
+ +
+ +
+ + +
+
+ +
+
+ + + +
+ + +
+
+

HTTPS Client

+

前章でOpenSSLのセットアップが済んだので、さっそくHTTPSクライアントを使ってみましょう。2章で使った httplib::Client がそのまま使えます。コンストラクタに https:// 付きのURLを渡すだけです。

+

GETリクエスト

+

実在するHTTPSサイトにアクセスしてみましょう。

+
+#define CPPHTTPLIB_OPENSSL_SUPPORT
+#include "httplib.h"
+#include <iostream>
+
+int main() {
+    httplib::Client cli("https://nghttp2.org");
+
+    auto res = cli.Get("/");
+    if (res) {
+        std::cout << res->status << std::endl;           // 200
+        std::cout << res->body.substr(0, 100) << std::endl;  // HTMLの先頭部分
+    } else {
+        std::cout << "Error: " << httplib::to_string(res.error()) << std::endl;
+    }
+}
+
+
+#define CPPHTTPLIB_OPENSSL_SUPPORT
+#include "httplib.h"
+#include <iostream>
+
+int main() {
+    httplib::Client cli("https://nghttp2.org");
+
+    auto res = cli.Get("/");
+    if (res) {
+        std::cout << res->status << std::endl;           // 200
+        std::cout << res->body.substr(0, 100) << std::endl;  // HTMLの先頭部分
+    } else {
+        std::cout << "Error: " << httplib::to_string(res.error()) << std::endl;
+    }
+}
+
+
+

2章では httplib::Client cli("http://localhost:8080") と書きましたよね。スキームを https:// に変えるだけです。Get()Post() など、2章で学んだAPIはすべてそのまま使えます。

+
+curl https://nghttp2.org/
+
+
+curl https://nghttp2.org/
+
+
+

ポートの指定

+

HTTPSのデフォルトポートは443です。別のポートを使いたい場合は、URLにポートを含めます。

+
+httplib::Client cli("https://localhost:8443");
+
+
+httplib::Client cli("https://localhost:8443");
+
+
+

CA証明書の検証

+

httplib::Client はHTTPS接続時、デフォルトでサーバー証明書を検証します。信頼できるCA(認証局)が発行した証明書を持つサーバーにしか接続しません。

+

CA証明書は、macOSならKeychain、LinuxならシステムのCA証明書ストア、WindowsならWindowsの証明書ストアから自動で読み込みます。ほとんどの場合、追加の設定は要りません。

+

CA証明書ファイルの指定

+

環境によってはシステムのCA証明書が見つからないこともあります。そのときは set_ca_cert_path() でパスを直接指定してください。

+
+httplib::Client cli("https://nghttp2.org");
+cli.set_ca_cert_path("/etc/ssl/certs/ca-certificates.crt");
+
+auto res = cli.Get("/");
+
+
+httplib::Client cli("https://nghttp2.org");
+cli.set_ca_cert_path("/etc/ssl/certs/ca-certificates.crt");
+
+auto res = cli.Get("/");
+
+
+curl --cacert /etc/ssl/certs/ca-certificates.crt https://nghttp2.org/
+
+
+curl --cacert /etc/ssl/certs/ca-certificates.crt https://nghttp2.org/
+
+
+

証明書検証の無効化

+

開発中、自己署名証明書のサーバーに接続したいときは、検証を無効にできます。

+
+httplib::Client cli("https://localhost:8443");
+cli.enable_server_certificate_verification(false);
+
+auto res = cli.Get("/");
+
+
+httplib::Client cli("https://localhost:8443");
+cli.enable_server_certificate_verification(false);
+
+auto res = cli.Get("/");
+
+
+curl -k https://localhost:8443/
+
+
+curl -k https://localhost:8443/
+
+
+

本番では絶対に無効にしないでください。中間者攻撃のリスクがあります。

+

リダイレクトの追跡

+

HTTPSサイトへのアクセスでは、リダイレクトに遭遇することがよくあります。たとえば http:// から https:// へ、あるいは www なしから www ありへ転送されるケースです。

+

デフォルトではリダイレクトを追跡しません。リダイレクト先は Location ヘッダーで確認できます。

+
+httplib::Client cli("https://nghttp2.org");
+
+auto res = cli.Get("/httpbin/redirect/3");
+if (res) {
+    std::cout << res->status << std::endl;  // 302
+    std::cout << res->get_header_value("Location") << std::endl;
+}
+
+
+httplib::Client cli("https://nghttp2.org");
+
+auto res = cli.Get("/httpbin/redirect/3");
+if (res) {
+    std::cout << res->status << std::endl;  // 302
+    std::cout << res->get_header_value("Location") << std::endl;
+}
+
+
+curl https://nghttp2.org/httpbin/redirect/3
+
+
+curl https://nghttp2.org/httpbin/redirect/3
+
+
+

set_follow_location(true) を設定すると、リダイレクトを自動で追跡して、最終的なレスポンスを返してくれます。

+
+httplib::Client cli("https://nghttp2.org");
+cli.set_follow_location(true);
+
+auto res = cli.Get("/httpbin/redirect/3");
+if (res) {
+    std::cout << res->status << std::endl;  // 200(最終的なレスポンス)
+}
+
+
+httplib::Client cli("https://nghttp2.org");
+cli.set_follow_location(true);
+
+auto res = cli.Get("/httpbin/redirect/3");
+if (res) {
+    std::cout << res->status << std::endl;  // 200(最終的なレスポンス)
+}
+
+
+curl -L https://nghttp2.org/httpbin/redirect/3
+
+
+curl -L https://nghttp2.org/httpbin/redirect/3
+
+
+

次のステップ

+

HTTPSクライアントの使い方がわかりましたね。次は自分でHTTPSサーバーを立ててみましょう。自己署名証明書の作り方から始めます。

+

次: HTTPS Server

+ +
+
+ +
+ +
+ © 2025 yhirose. All rights reserved. +
+ + + + diff --git a/docs/ja/tour/07-https-server/index.html b/docs/ja/tour/07-https-server/index.html new file mode 100644 index 0000000..2ed95e2 --- /dev/null +++ b/docs/ja/tour/07-https-server/index.html @@ -0,0 +1,240 @@ + + + + + + HTTPS Server - cpp-httplib + + + + +
+
+ cpp-httplib v0.35.0 +
+ +
+ +
+ + +
+
+ +
+
+ + + +
+ + +
+
+

HTTPS Server

+

前章ではHTTPSクライアントを使いました。今度は自分でHTTPSサーバーを立ててみましょう。3章の httplib::Serverhttplib::SSLServer に置き換えるだけです。

+

ただし、TLSサーバーにはサーバー証明書と秘密鍵が必要です。まずはそこから準備しましょう。

+

自己署名証明書の作成

+

開発やテスト用なら、自己署名証明書(いわゆるオレオレ証明書)で十分です。OpenSSLのコマンドでサクッと作れます。

+
+openssl req -x509 -noenc -keyout key.pem -out cert.pem -subj /CN=localhost
+
+
+openssl req -x509 -noenc -keyout key.pem -out cert.pem -subj /CN=localhost
+
+
+

これで2つのファイルができます。

+
    +
  • cert.pem — サーバー証明書
  • +
  • key.pem — 秘密鍵
  • +
+

最小のHTTPSサーバー

+

証明書ができたら、さっそくサーバーを書いてみましょう。

+
+#define CPPHTTPLIB_OPENSSL_SUPPORT
+#include "httplib.h"
+#include <iostream>
+
+int main() {
+    httplib::SSLServer svr("cert.pem", "key.pem");
+
+    svr.Get("/", [](const auto &, auto &res) {
+        res.set_content("Hello, HTTPS!", "text/plain");
+    });
+
+    std::cout << "Listening on https://localhost:8443" << std::endl;
+    svr.listen("0.0.0.0", 8443);
+}
+
+
+#define CPPHTTPLIB_OPENSSL_SUPPORT
+#include "httplib.h"
+#include <iostream>
+
+int main() {
+    httplib::SSLServer svr("cert.pem", "key.pem");
+
+    svr.Get("/", [](const auto &, auto &res) {
+        res.set_content("Hello, HTTPS!", "text/plain");
+    });
+
+    std::cout << "Listening on https://localhost:8443" << std::endl;
+    svr.listen("0.0.0.0", 8443);
+}
+
+
+

httplib::SSLServer のコンストラクタに証明書と秘密鍵のパスを渡すだけです。ルーティングの書き方は3章の httplib::Server とまったく同じですよ。

+

コンパイルして起動しましょう。

+

動作確認

+

サーバーが起動したら、curl でアクセスしてみましょう。自己署名証明書なので、-k オプションで証明書検証をスキップします。

+
+curl -k https://localhost:8443/
+# Hello, HTTPS!
+
+
+curl -k https://localhost:8443/
+# Hello, HTTPS!
+
+
+

ブラウザで https://localhost:8443 を開くと、「この接続は安全ではありません」と警告が出ます。自己署名証明書なので正常です。気にせず進めてください。

+

クライアントからの接続

+

前章の httplib::Client で接続してみましょう。自己署名証明書のサーバーに接続するには、2つの方法があります。

+

方法1: 証明書検証を無効にする

+

開発時の手軽な方法です。

+
+#define CPPHTTPLIB_OPENSSL_SUPPORT
+#include "httplib.h"
+#include <iostream>
+
+int main() {
+    httplib::Client cli("https://localhost:8443");
+    cli.enable_server_certificate_verification(false);
+
+    auto res = cli.Get("/");
+    if (res) {
+        std::cout << res->body << std::endl;  // Hello, HTTPS!
+    }
+}
+
+
+#define CPPHTTPLIB_OPENSSL_SUPPORT
+#include "httplib.h"
+#include <iostream>
+
+int main() {
+    httplib::Client cli("https://localhost:8443");
+    cli.enable_server_certificate_verification(false);
+
+    auto res = cli.Get("/");
+    if (res) {
+        std::cout << res->body << std::endl;  // Hello, HTTPS!
+    }
+}
+
+
+

方法2: 自己署名証明書をCA証明書として指定する

+

こちらのほうが安全です。cert.pem をCA証明書として信頼するよう指定します。

+
+#define CPPHTTPLIB_OPENSSL_SUPPORT
+#include "httplib.h"
+#include <iostream>
+
+int main() {
+    httplib::Client cli("https://localhost:8443");
+    cli.set_ca_cert_path("cert.pem");
+
+    auto res = cli.Get("/");
+    if (res) {
+        std::cout << res->body << std::endl;  // Hello, HTTPS!
+    }
+}
+
+
+#define CPPHTTPLIB_OPENSSL_SUPPORT
+#include "httplib.h"
+#include <iostream>
+
+int main() {
+    httplib::Client cli("https://localhost:8443");
+    cli.set_ca_cert_path("cert.pem");
+
+    auto res = cli.Get("/");
+    if (res) {
+        std::cout << res->body << std::endl;  // Hello, HTTPS!
+    }
+}
+
+
+

この方法なら、指定した証明書のサーバーにだけ接続を許可して、なりすましを防げます。テスト環境でもなるべくこちらを使いましょう。

+

Server と SSLServer の比較

+

3章で学んだ httplib::Server のAPIは、httplib::SSLServer でもそのまま使えます。違いはコンストラクタだけです。

+ + + + + +
httplib::Serverhttplib::SSLServer
コンストラクタ引数なし証明書と秘密鍵のパス
プロトコルHTTPHTTPS
ポート(慣例)80808443
ルーティング共通共通
+

HTTPサーバーをHTTPSに切り替えるには、コンストラクタを変えるだけです。

+

次のステップ

+

HTTPSサーバーが動きましたね。これでHTTP/HTTPSのクライアントとサーバー、両方の基本がそろいました。

+

次は、cpp-httplibに新しく加わったWebSocket機能を見てみましょう。

+

次: WebSocket

+ +
+
+ +
+ +
+ © 2025 yhirose. All rights reserved. +
+ + + + diff --git a/docs/ja/tour/08-websocket/index.html b/docs/ja/tour/08-websocket/index.html new file mode 100644 index 0000000..85903d2 --- /dev/null +++ b/docs/ja/tour/08-websocket/index.html @@ -0,0 +1,292 @@ + + + + + + WebSocket - cpp-httplib + + + + +
+
+ cpp-httplib v0.35.0 +
+ +
+ +
+ + +
+
+ +
+
+ + + +
+ + +
+
+

WebSocket

+

cpp-httplibはWebSocketにも対応しています。HTTPのリクエスト/レスポンスと違い、WebSocketはサーバーとクライアントが双方向にメッセージをやり取りできます。チャットやリアルタイム通知に便利です。

+

さっそく、エコーサーバーとクライアントを作ってみましょう。

+

エコーサーバー

+

受け取ったメッセージをそのまま返すエコーサーバーです。

+
+#include "httplib.h"
+#include <iostream>
+
+int main() {
+    httplib::Server svr;
+
+    svr.WebSocket("/ws", [](const httplib::Request &, httplib::ws::WebSocket &ws) {
+        std::string msg;
+        while (ws.read(msg)) {
+            ws.send(msg);  // 受け取ったメッセージをそのまま返す
+        }
+    });
+
+    std::cout << "Listening on port 8080..." << std::endl;
+    svr.listen("0.0.0.0", 8080);
+}
+
+
+#include "httplib.h"
+#include <iostream>
+
+int main() {
+    httplib::Server svr;
+
+    svr.WebSocket("/ws", [](const httplib::Request &, httplib::ws::WebSocket &ws) {
+        std::string msg;
+        while (ws.read(msg)) {
+            ws.send(msg);  // 受け取ったメッセージをそのまま返す
+        }
+    });
+
+    std::cout << "Listening on port 8080..." << std::endl;
+    svr.listen("0.0.0.0", 8080);
+}
+
+
+

svr.WebSocket() でWebSocketハンドラーを登録します。3章の svr.Get()svr.Post() と同じ感覚ですね。

+

ハンドラーの中では、ws.read(msg) でメッセージを待ちます。接続が閉じられると read()false を返すので、ループを抜けます。ws.send(msg) でメッセージを送り返します。

+

クライアントからの接続

+

httplib::ws::WebSocketClient を使ってサーバーに接続してみましょう。

+
+#include "httplib.h"
+#include <iostream>
+
+int main() {
+    httplib::ws::WebSocketClient client("ws://localhost:8080/ws");
+
+    if (!client.connect()) {
+        std::cout << "Connection failed" << std::endl;
+        return 1;
+    }
+
+    // メッセージを送信
+    client.send("Hello, WebSocket!");
+
+    // サーバーからの応答を受信
+    std::string msg;
+    if (client.read(msg)) {
+        std::cout << msg << std::endl;  // Hello, WebSocket!
+    }
+
+    client.close();
+}
+
+
+#include "httplib.h"
+#include <iostream>
+
+int main() {
+    httplib::ws::WebSocketClient client("ws://localhost:8080/ws");
+
+    if (!client.connect()) {
+        std::cout << "Connection failed" << std::endl;
+        return 1;
+    }
+
+    // メッセージを送信
+    client.send("Hello, WebSocket!");
+
+    // サーバーからの応答を受信
+    std::string msg;
+    if (client.read(msg)) {
+        std::cout << msg << std::endl;  // Hello, WebSocket!
+    }
+
+    client.close();
+}
+
+
+

コンストラクタには ws://host:port/path 形式のURLを渡します。connect() で接続を開始し、send()read() でメッセージをやり取りします。

+

テキストとバイナリ

+

WebSocketにはテキストとバイナリの2種類のメッセージがあります。read() の戻り値で区別できます。

+
+svr.WebSocket("/ws", [](const httplib::Request &, httplib::ws::WebSocket &ws) {
+    std::string msg;
+    httplib::ws::ReadResult ret;
+    while ((ret = ws.read(msg))) {
+        if (ret == httplib::ws::Binary) {
+            ws.send(msg.data(), msg.size());  // バイナリとして送信
+        } else {
+            ws.send(msg);  // テキストとして送信
+        }
+    }
+});
+
+
+svr.WebSocket("/ws", [](const httplib::Request &, httplib::ws::WebSocket &ws) {
+    std::string msg;
+    httplib::ws::ReadResult ret;
+    while ((ret = ws.read(msg))) {
+        if (ret == httplib::ws::Binary) {
+            ws.send(msg.data(), msg.size());  // バイナリとして送信
+        } else {
+            ws.send(msg);  // テキストとして送信
+        }
+    }
+});
+
+
+
    +
  • ws.send(const std::string &) — テキストメッセージとして送信
  • +
  • ws.send(const char *, size_t) — バイナリメッセージとして送信
  • +
+

クライアント側も同じAPIです。

+

リクエスト情報へのアクセス

+

ハンドラーの第1引数 req から、ハンドシェイク時のHTTPリクエスト情報を読み取れます。認証トークンの確認などに便利です。

+
+svr.WebSocket("/ws", [](const httplib::Request &req, httplib::ws::WebSocket &ws) {
+    auto token = req.get_header_value("Authorization");
+    if (token.empty()) {
+        ws.close(httplib::ws::CloseStatus::PolicyViolation, "unauthorized");
+        return;
+    }
+
+    std::string msg;
+    while (ws.read(msg)) {
+        ws.send(msg);
+    }
+});
+
+
+svr.WebSocket("/ws", [](const httplib::Request &req, httplib::ws::WebSocket &ws) {
+    auto token = req.get_header_value("Authorization");
+    if (token.empty()) {
+        ws.close(httplib::ws::CloseStatus::PolicyViolation, "unauthorized");
+        return;
+    }
+
+    std::string msg;
+    while (ws.read(msg)) {
+        ws.send(msg);
+    }
+});
+
+
+

WSSで使う

+

HTTPS上のWebSocket(WSS)にも対応しています。サーバー側は httplib::SSLServer にWebSocketハンドラーを登録するだけです。

+
+httplib::SSLServer svr("cert.pem", "key.pem");
+
+svr.WebSocket("/ws", [](const httplib::Request &, httplib::ws::WebSocket &ws) {
+    std::string msg;
+    while (ws.read(msg)) {
+        ws.send(msg);
+    }
+});
+
+svr.listen("0.0.0.0", 8443);
+
+
+httplib::SSLServer svr("cert.pem", "key.pem");
+
+svr.WebSocket("/ws", [](const httplib::Request &, httplib::ws::WebSocket &ws) {
+    std::string msg;
+    while (ws.read(msg)) {
+        ws.send(msg);
+    }
+});
+
+svr.listen("0.0.0.0", 8443);
+
+
+

クライアント側は wss:// スキームを使います。

+
+httplib::ws::WebSocketClient client("wss://localhost:8443/ws");
+
+
+httplib::ws::WebSocketClient client("wss://localhost:8443/ws");
+
+
+

次のステップ

+

WebSocketの基本がわかりましたね。ここまでで Tourは終わりです。

+

次のページでは、Tourで取り上げなかった機能をまとめて紹介します。

+

次: What's Next

+ +
+
+ +
+ +
+ © 2025 yhirose. All rights reserved. +
+ + + + diff --git a/docs/ja/tour/09-whats-next/index.html b/docs/ja/tour/09-whats-next/index.html new file mode 100644 index 0000000..c3b22f4 --- /dev/null +++ b/docs/ja/tour/09-whats-next/index.html @@ -0,0 +1,424 @@ + + + + + + What's Next - cpp-httplib + + + + +
+
+ cpp-httplib v0.35.0 +
+ +
+ +
+ + +
+
+ +
+
+ + + +
+ + +
+
+

What's Next

+

Tourお疲れさまでした! cpp-httplibの基本はひと通り押さえましたね。でも、まだまだ便利な機能があります。Tourで取り上げなかった機能をカテゴリー別に紹介します。

+

Streaming API

+

LLMのストリーミング応答や大きなファイルのダウンロードでは、レスポンス全体をメモリに載せたくないですよね。stream::Get() を使えば、データをチャンクごとに処理できます。

+
+httplib::Client cli("http://localhost:11434");
+
+auto result = httplib::stream::Get(cli, "/api/generate");
+
+if (result) {
+    while (result.next()) {
+        std::cout.write(result.data(), result.size());
+    }
+}
+
+
+httplib::Client cli("http://localhost:11434");
+
+auto result = httplib::stream::Get(cli, "/api/generate");
+
+if (result) {
+    while (result.next()) {
+        std::cout.write(result.data(), result.size());
+    }
+}
+
+
+

Get()content_receiver コールバックを渡す方法もあります。こちらはKeep-Aliveと併用できます。

+
+httplib::Client cli("http://localhost:8080");
+
+cli.Get("/stream", [](const char *data, size_t len) {
+    std::cout.write(data, len);
+    return true;
+});
+
+
+httplib::Client cli("http://localhost:8080");
+
+cli.Get("/stream", [](const char *data, size_t len) {
+    std::cout.write(data, len);
+    return true;
+});
+
+
+

サーバー側には set_content_provider()set_chunked_content_provider() があります。サイズがわかっているなら前者、不明なら後者を使ってください。

+
+// サイズ指定あり(Content-Length が設定される)
+svr.Get("/file", [](const auto &, auto &res) {
+    auto size = get_file_size("large.bin");
+    res.set_content_provider(size, "application/octet-stream",
+        [](size_t offset, size_t length, httplib::DataSink &sink) {
+            // offset から length バイト分を送る
+            return true;
+        });
+});
+
+// サイズ不明(Chunked Transfer Encoding)
+svr.Get("/stream", [](const auto &, auto &res) {
+    res.set_chunked_content_provider("text/plain",
+        [](size_t offset, httplib::DataSink &sink) {
+            sink.write("chunk\n", 6);
+            return true;  // falseを返すと終了
+        });
+});
+
+
+// サイズ指定あり(Content-Length が設定される)
+svr.Get("/file", [](const auto &, auto &res) {
+    auto size = get_file_size("large.bin");
+    res.set_content_provider(size, "application/octet-stream",
+        [](size_t offset, size_t length, httplib::DataSink &sink) {
+            // offset から length バイト分を送る
+            return true;
+        });
+});
+
+// サイズ不明(Chunked Transfer Encoding)
+svr.Get("/stream", [](const auto &, auto &res) {
+    res.set_chunked_content_provider("text/plain",
+        [](size_t offset, httplib::DataSink &sink) {
+            sink.write("chunk\n", 6);
+            return true;  // falseを返すと終了
+        });
+});
+
+
+

大きなファイルのアップロードには make_file_provider() が便利です。ファイルを全部メモリに読み込まず、ストリーミングで送れます。

+
+httplib::Client cli("http://localhost:8080");
+
+auto res = cli.Post("/upload", {}, {
+    httplib::make_file_provider("file", "/path/to/large-file.zip")
+});
+
+
+httplib::Client cli("http://localhost:8080");
+
+auto res = cli.Post("/upload", {}, {
+    httplib::make_file_provider("file", "/path/to/large-file.zip")
+});
+
+
+

Server-Sent Events (SSE)

+

SSEクライアントも用意しています。自動再接続や Last-Event-ID による再開にも対応しています。

+
+httplib::Client cli("http://localhost:8080");
+httplib::sse::SSEClient sse(cli, "/events");
+
+sse.on_message([](const httplib::sse::SSEMessage &msg) {
+    std::cout << msg.event << ": " << msg.data << std::endl;
+});
+
+sse.start();  // ブロッキング、自動再接続あり
+
+
+httplib::Client cli("http://localhost:8080");
+httplib::sse::SSEClient sse(cli, "/events");
+
+sse.on_message([](const httplib::sse::SSEMessage &msg) {
+    std::cout << msg.event << ": " << msg.data << std::endl;
+});
+
+sse.start();  // ブロッキング、自動再接続あり
+
+
+

イベントタイプごとにハンドラーを分けることもできますよ。

+
+sse.on_event("update", [](const httplib::sse::SSEMessage &msg) {
+    // "update" イベントだけ処理
+});
+
+
+sse.on_event("update", [](const httplib::sse::SSEMessage &msg) {
+    // "update" イベントだけ処理
+});
+
+
+

認証

+

クライアントにはBasic認証、Bearer Token認証、Digest認証のヘルパーを用意しています。

+
+httplib::Client cli("https://api.example.com");
+cli.set_basic_auth("user", "password");
+cli.set_bearer_token_auth("my-token");
+
+
+httplib::Client cli("https://api.example.com");
+cli.set_basic_auth("user", "password");
+cli.set_bearer_token_auth("my-token");
+
+
+

圧縮

+

gzip、Brotli、Zstandardによる圧縮・展開に対応しています。使いたい方式のマクロを定義してコンパイルしましょう。

+ + + + +
圧縮方式マクロ定義
gzipCPPHTTPLIB_ZLIB_SUPPORT
BrotliCPPHTTPLIB_BROTLI_SUPPORT
ZstandardCPPHTTPLIB_ZSTD_SUPPORT
+
+httplib::Client cli("https://example.com");
+cli.set_compress(true);    // リクエストボディを圧縮
+cli.set_decompress(true);  // レスポンスボディを展開
+
+
+httplib::Client cli("https://example.com");
+cli.set_compress(true);    // リクエストボディを圧縮
+cli.set_decompress(true);  // レスポンスボディを展開
+
+
+

プロキシ

+

HTTPプロキシ経由で接続できます。

+
+httplib::Client cli("https://example.com");
+cli.set_proxy("proxy.example.com", 8080);
+cli.set_proxy_basic_auth("user", "password");
+
+
+httplib::Client cli("https://example.com");
+cli.set_proxy("proxy.example.com", 8080);
+cli.set_proxy_basic_auth("user", "password");
+
+
+

タイムアウト

+

接続・読み取り・書き込みのタイムアウトを個別に設定できます。

+
+httplib::Client cli("https://example.com");
+cli.set_connection_timeout(5, 0);  // 5秒
+cli.set_read_timeout(10, 0);       // 10秒
+cli.set_write_timeout(10, 0);      // 10秒
+
+
+httplib::Client cli("https://example.com");
+cli.set_connection_timeout(5, 0);  // 5秒
+cli.set_read_timeout(10, 0);       // 10秒
+cli.set_write_timeout(10, 0);      // 10秒
+
+
+

Keep-Alive

+

同じサーバーに何度もリクエストするなら、Keep-Aliveを有効にしましょう。TCP接続を再利用するので効率的です。

+
+httplib::Client cli("https://example.com");
+cli.set_keep_alive(true);
+
+
+httplib::Client cli("https://example.com");
+cli.set_keep_alive(true);
+
+
+

サーバーのミドルウェア

+

リクエスト処理の前後にフックを挟めます。

+
+svr.set_pre_routing_handler([](const auto &req, auto &res) {
+    // すべてのリクエストの前に実行される
+    return httplib::Server::HandlerResponse::Unhandled;  // 通常のルーティングに進む
+});
+
+svr.set_post_routing_handler([](const auto &req, auto &res) {
+    // レスポンスが返された後に実行される
+    res.set_header("X-Server", "cpp-httplib");
+});
+
+
+svr.set_pre_routing_handler([](const auto &req, auto &res) {
+    // すべてのリクエストの前に実行される
+    return httplib::Server::HandlerResponse::Unhandled;  // 通常のルーティングに進む
+});
+
+svr.set_post_routing_handler([](const auto &req, auto &res) {
+    // レスポンスが返された後に実行される
+    res.set_header("X-Server", "cpp-httplib");
+});
+
+
+

req.user_data を使うと、ミドルウェアからハンドラーにデータを渡せます。認証トークンのデコード結果を共有するときに便利です。

+
+svr.set_pre_routing_handler([](const auto &req, auto &res) {
+    req.user_data["auth_user"] = std::string("alice");
+    return httplib::Server::HandlerResponse::Unhandled;
+});
+
+svr.Get("/me", [](const auto &req, auto &res) {
+    auto user = std::any_cast<std::string>(req.user_data.at("auth_user"));
+    res.set_content("Hello, " + user, "text/plain");
+});
+
+
+svr.set_pre_routing_handler([](const auto &req, auto &res) {
+    req.user_data["auth_user"] = std::string("alice");
+    return httplib::Server::HandlerResponse::Unhandled;
+});
+
+svr.Get("/me", [](const auto &req, auto &res) {
+    auto user = std::any_cast<std::string>(req.user_data.at("auth_user"));
+    res.set_content("Hello, " + user, "text/plain");
+});
+
+
+

エラーや例外のハンドラーもカスタマイズできますよ。

+
+svr.set_error_handler([](const auto &req, auto &res) {
+    res.set_content("Custom Error Page", "text/html");
+});
+
+svr.set_exception_handler([](const auto &req, auto &res, std::exception_ptr ep) {
+    res.status = 500;
+    res.set_content("Internal Server Error", "text/plain");
+});
+
+
+svr.set_error_handler([](const auto &req, auto &res) {
+    res.set_content("Custom Error Page", "text/html");
+});
+
+svr.set_exception_handler([](const auto &req, auto &res, std::exception_ptr ep) {
+    res.status = 500;
+    res.set_content("Internal Server Error", "text/plain");
+});
+
+
+

ロギング

+

サーバーでもクライアントでもロガーを設定できます。

+
+svr.set_logger([](const auto &req, const auto &res) {
+    std::cout << req.method << " " << req.path << " " << res.status << std::endl;
+});
+
+
+svr.set_logger([](const auto &req, const auto &res) {
+    std::cout << req.method << " " << req.path << " " << res.status << std::endl;
+});
+
+
+

Unix Domain Socket

+

TCP以外に、Unix Domain Socketでの通信にも対応しています。同じマシン上のプロセス間通信に使えます。

+
+// サーバー
+httplib::Server svr;
+svr.set_address_family(AF_UNIX);
+svr.listen("/tmp/httplib.sock", 0);
+
+
+// サーバー
+httplib::Server svr;
+svr.set_address_family(AF_UNIX);
+svr.listen("/tmp/httplib.sock", 0);
+
+
+// クライアント
+httplib::Client cli("http://localhost");
+cli.set_address_family(AF_UNIX);
+cli.set_hostname_addr_map({{"localhost", "/tmp/httplib.sock"}});
+
+auto res = cli.Get("/");
+
+
+// クライアント
+httplib::Client cli("http://localhost");
+cli.set_address_family(AF_UNIX);
+cli.set_hostname_addr_map({{"localhost", "/tmp/httplib.sock"}});
+
+auto res = cli.Get("/");
+
+
+

さらに詳しく

+

もっと詳しく知りたいときは、以下を参照してください。

+
    +
  • Cookbook — よくあるユースケースのレシピ集
  • +
  • README — 全APIのリファレンス
  • +
  • README-sse — Server-Sent Eventsの使い方
  • +
  • README-stream — Streaming APIの使い方
  • +
  • README-websocket — WebSocketサーバーの使い方
  • +
+ +
+
+ +
+ +
+ © 2025 yhirose. All rights reserved. +
+ + + + diff --git a/docs/ja/tour/index.html b/docs/ja/tour/index.html new file mode 100644 index 0000000..5024677 --- /dev/null +++ b/docs/ja/tour/index.html @@ -0,0 +1,105 @@ + + + + + + A Tour of cpp-httplib - cpp-httplib + + + + +
+
+ cpp-httplib v0.35.0 +
+ +
+ +
+ + +
+
+ +
+
+ + + +
+ + +
+
+

A Tour of cpp-httplib

+

cpp-httplibの基本を、順番に学んでいくチュートリアルです。各章は前の章の内容を踏まえて進む構成なので、1章から順に読んでください。

+
    +
  1. Getting Started — httplib.h の入手とHello Worldサーバー
  2. +
  3. Basic Client — GET/POST・パスパラメーターのリクエスト送信
  4. +
  5. Basic Server — ルーティング、パスパラメーター、レスポンスの組み立て
  6. +
  7. Static File Server — 静的ファイルの配信
  8. +
  9. TLS Setup — OpenSSL / mbedTLS のセットアップ
  10. +
  11. HTTPS Client — HTTPSサイトへのリクエスト
  12. +
  13. HTTPS Server — HTTPSサーバーの構築
  14. +
  15. WebSocket — WebSocket通信の基本
  16. +
  17. What's Next — さらなる機能の紹介
  18. +
+ +
+
+ +
+ +
+ © 2025 yhirose. All rights reserved. +
+ + + + diff --git a/docs/js/main.js b/docs/js/main.js new file mode 100644 index 0000000..3bdb6bb --- /dev/null +++ b/docs/js/main.js @@ -0,0 +1,73 @@ +// Language selector +(function () { + var btn = document.querySelector('.lang-btn'); + var popup = document.querySelector('.lang-popup'); + if (!btn || !popup) return; + + btn.addEventListener('click', function (e) { + e.stopPropagation(); + popup.classList.toggle('open'); + }); + + document.addEventListener('click', function () { + popup.classList.remove('open'); + }); + + popup.addEventListener('click', function (e) { + var link = e.target.closest('[data-lang]'); + if (!link) return; + e.preventDefault(); + var lang = link.getAttribute('data-lang'); + localStorage.setItem('preferred-lang', lang); + var path = window.location.pathname; + var newPath = path.replace(/^\/[a-z]{2}\//, '/' + lang + '/'); + window.location.href = newPath; + }); +})(); + +// Theme toggle +(function () { + var btn = document.querySelector('.theme-toggle'); + if (!btn) return; + + function getTheme() { + var stored = localStorage.getItem('preferred-theme'); + if (stored) return stored; + return window.matchMedia('(prefers-color-scheme: light)').matches ? 'light' : 'dark'; + } + + function applyTheme(theme) { + if (theme === 'light') { + document.documentElement.setAttribute('data-theme', 'light'); + } else { + document.documentElement.removeAttribute('data-theme'); + } + btn.textContent = theme === 'light' ? '\u2600\uFE0F' : '\uD83C\uDF19'; + } + + applyTheme(getTheme()); + + btn.addEventListener('click', function () { + var current = getTheme(); + var next = current === 'dark' ? 'light' : 'dark'; + localStorage.setItem('preferred-theme', next); + applyTheme(next); + }); +})(); + +// Mobile sidebar toggle +(function () { + var toggle = document.querySelector('.sidebar-toggle'); + var sidebar = document.querySelector('.sidebar'); + if (!toggle || !sidebar) return; + + toggle.addEventListener('click', function () { + sidebar.classList.toggle('open'); + }); + + document.addEventListener('click', function (e) { + if (!sidebar.contains(e.target) && e.target !== toggle) { + sidebar.classList.remove('open'); + } + }); +})(); diff --git a/justfile b/justfile index c6fe58e..7d50f18 100644 --- a/justfile +++ b/justfile @@ -44,3 +44,7 @@ build: bench: @(cd benchmark && make bench-all) + +docs: + cargo build --release --manifest-path docs-gen/Cargo.toml + ./docs-gen/target/release/docs-gen docs-src --out docs