3 커밋 cb8365349f ... a77284a634

작성자 SHA1 메시지 날짜
  yhirose a77284a634 Release v0.39.0 1 주 전
  yhirose 315a87520d Add release script and update .gitignore for work directory 1 주 전
  yhirose 703abbb53b Prevent forwarding of authentication credentials during cross-host redirects as per RFC 9110. Add tests for basic auth and bearer token scenarios. 1 주 전
6개의 변경된 파일228개의 추가작업 그리고 17개의 파일을 삭제
  1. 1 2
      .gitignore
  2. 1 1
      docs-src/config.toml
  3. 7 14
      httplib.h
  4. 3 0
      justfile
  5. 157 0
      scripts/release.sh
  6. 59 0
      test/test.cc

+ 1 - 2
.gitignore

@@ -2,6 +2,7 @@ tags
 AGENTS.md
 docs-src/pages/AGENTS.md
 plans/
+work/
 
 # Ignore executables (no extension) but not source files
 example/server
@@ -59,9 +60,7 @@ test/*.pem
 test/*.srl
 test/*.log
 test/_build_*
-work/
 benchmark/server*
-docs-gen/target/
 
 *.swp
 

+ 1 - 1
docs-src/config.toml

@@ -4,7 +4,7 @@ langs = ["en", "ja"]
 
 [site]
 title = "cpp-httplib"
-version = "0.38.0"
+version = "0.39.0"
 hostname = "https://yhirose.github.io"
 base_path = "/cpp-httplib"
 footer_message = "© 2026 Yuji Hirose. All rights reserved."

+ 7 - 14
httplib.h

@@ -8,8 +8,8 @@
 #ifndef CPPHTTPLIB_HTTPLIB_H
 #define CPPHTTPLIB_HTTPLIB_H
 
-#define CPPHTTPLIB_VERSION "0.38.0"
-#define CPPHTTPLIB_VERSION_NUM "0x002600"
+#define CPPHTTPLIB_VERSION "0.39.0"
+#define CPPHTTPLIB_VERSION_NUM "0x002700"
 
 #ifdef _WIN32
 #if defined(_WIN32_WINNT) && _WIN32_WINNT < 0x0A00
@@ -13048,18 +13048,11 @@ inline void ClientImpl::setup_redirect_client(ClientType &client) {
   client.set_compress(compress_);
   client.set_decompress(decompress_);
 
-  // Copy authentication settings BEFORE proxy setup
-  if (!basic_auth_username_.empty()) {
-    client.set_basic_auth(basic_auth_username_, basic_auth_password_);
-  }
-  if (!bearer_token_auth_token_.empty()) {
-    client.set_bearer_token_auth(bearer_token_auth_token_);
-  }
-#ifdef CPPHTTPLIB_SSL_ENABLED
-  if (!digest_auth_username_.empty()) {
-    client.set_digest_auth(digest_auth_username_, digest_auth_password_);
-  }
-#endif
+  // NOTE: Authentication credentials (basic auth, bearer token, digest auth)
+  // are intentionally NOT copied to the redirect client. Per RFC 9110 Section
+  // 15.4, credentials must not be forwarded when redirecting to a different
+  // host. This function is only called for cross-host redirects; same-host
+  // redirects are handled directly in ClientImpl::redirect().
 
   // Setup proxy configuration (CRITICAL ORDER - proxy must be set
   // before proxy auth)

+ 3 - 0
justfile

@@ -51,3 +51,6 @@ docs-serve:
 
 docs-check:
     @docs-gen check docs-src
+
+release *args:
+    @./scripts/release.sh {{args}}

+ 157 - 0
scripts/release.sh

@@ -0,0 +1,157 @@
+#!/usr/bin/env bash
+#
+# Release a new version of cpp-httplib.
+#
+# Usage: ./release.sh [--run]
+#
+# By default, runs in dry-run mode (no changes made).
+# Pass --run to actually update files, commit, tag, and push.
+#
+# This script:
+#   1. Reads the current version from httplib.h
+#   2. Checks that the working directory is clean
+#   3. Verifies CI status of the latest commit (all must pass except abidiff)
+#   4. Determines the next version automatically:
+#        - abidiff passed  → patch bump (e.g., 0.38.0 → 0.38.1)
+#        - abidiff failed  → minor bump (e.g., 0.38.1 → 0.39.0)
+#   5. Updates httplib.h and docs-src/config.toml
+#   6. Commits, tags (vX.Y.Z), and pushes
+
+set -euo pipefail
+
+DRY_RUN=1
+if [ "${1:-}" = "--run" ]; then
+  DRY_RUN=0
+  shift
+fi
+
+if [ $# -ne 0 ]; then
+  echo "Usage: $0 [--run]"
+  exit 1
+fi
+
+# --- Step 1: Read current version from httplib.h ---
+CURRENT_VERSION=$(sed -n 's/^#define CPPHTTPLIB_VERSION "\([^"]*\)"/\1/p' httplib.h)
+IFS='.' read -r V_MAJOR V_MINOR V_PATCH <<< "$CURRENT_VERSION"
+
+echo "==> Current version: $CURRENT_VERSION"
+
+# --- Step 2: Check working directory is clean ---
+if [ -n "$(git status --porcelain)" ]; then
+  echo "Error: working directory is not clean"
+  exit 1
+fi
+
+# --- Step 3: Check CI status of the latest commit ---
+echo ""
+echo "==> Checking CI status of the latest commit..."
+
+HEAD_SHA=$(git rev-parse HEAD)
+HEAD_SHORT=$(git rev-parse --short HEAD)
+echo "    Latest commit: $HEAD_SHORT"
+
+# Fetch all workflow runs for the HEAD commit
+RUNS=$(gh run list --json name,conclusion,headSha \
+  --jq "[.[] | select(.headSha == \"$HEAD_SHA\")]")
+
+NUM_RUNS=$(echo "$RUNS" | jq 'length')
+
+if [ "$NUM_RUNS" -eq 0 ]; then
+  echo "Error: No CI runs found for commit $HEAD_SHORT."
+  echo "       Wait for CI to complete before releasing."
+  exit 1
+fi
+
+echo "    Found $NUM_RUNS workflow run(s):"
+
+FAILED=0
+ABIDIFF_PASSED=0
+while IFS=$'\t' read -r name conclusion; do
+  if [[ "$name" == *abidiff* ]] || [[ "$name" == *abi* && "$name" != *stability* ]]; then
+    if [ "$conclusion" = "success" ]; then
+      echo "      [ OK ] $name"
+      ABIDIFF_PASSED=1
+    else
+      echo "      [FAIL] $name ($conclusion) → ABI break detected, minor bump"
+      ABIDIFF_PASSED=0
+    fi
+    continue
+  fi
+
+  if [ "$conclusion" = "success" ]; then
+    echo "      [ OK ] $name"
+  else
+    echo "      [FAIL] $name ($conclusion)"
+    FAILED=1
+  fi
+done < <(echo "$RUNS" | jq -r '.[] | [.name, .conclusion] | @tsv')
+
+if [ "$FAILED" -eq 1 ]; then
+  echo ""
+  echo "Error: Some CI checks failed. Fix them before releasing."
+  exit 1
+fi
+
+echo "    All non-abidiff CI checks passed."
+
+# --- Step 4: Determine new version ---
+if [ "$ABIDIFF_PASSED" -eq 1 ]; then
+  NEW_PATCH=$((V_PATCH + 1))
+  NEW_VERSION="$V_MAJOR.$V_MINOR.$NEW_PATCH"
+  echo ""
+  echo "==> abidiff passed → patch bump"
+else
+  NEW_MINOR=$((V_MINOR + 1))
+  NEW_VERSION="$V_MAJOR.$NEW_MINOR.0"
+  echo ""
+  echo "==> abidiff failed → minor bump"
+fi
+
+VERSION_HEX=$(printf "0x%02x%02x%02x" "${NEW_VERSION%%.*}" "$(echo "$NEW_VERSION" | cut -d. -f2)" "${NEW_VERSION##*.}")
+
+if [ "$DRY_RUN" -eq 1 ]; then
+  echo "==> [DRY RUN] New version: $NEW_VERSION ($VERSION_HEX)"
+else
+  echo "==> New version: $NEW_VERSION ($VERSION_HEX)"
+fi
+
+# --- Step 5: Update files ---
+echo ""
+if [ "$DRY_RUN" -eq 1 ]; then
+  echo "==> [DRY RUN] Would update httplib.h:"
+  echo "    CPPHTTPLIB_VERSION     = \"$NEW_VERSION\""
+  echo "    CPPHTTPLIB_VERSION_NUM = \"$VERSION_HEX\""
+  echo ""
+  echo "==> [DRY RUN] Would update docs-src/config.toml:"
+  echo "    version = \"$NEW_VERSION\""
+  echo ""
+  echo "==> [DRY RUN] Would commit, tag v$NEW_VERSION and latest, and push."
+  echo ""
+  echo "==> Dry run complete. No changes were made."
+else
+  echo "==> Updating httplib.h..."
+  sed -i '' "s/#define CPPHTTPLIB_VERSION \"[^\"]*\"/#define CPPHTTPLIB_VERSION \"$NEW_VERSION\"/" httplib.h
+  sed -i '' "s/#define CPPHTTPLIB_VERSION_NUM \"0x[0-9a-fA-F]*\"/#define CPPHTTPLIB_VERSION_NUM \"$VERSION_HEX\"/" httplib.h
+  echo "    CPPHTTPLIB_VERSION     = \"$NEW_VERSION\""
+  echo "    CPPHTTPLIB_VERSION_NUM = \"$VERSION_HEX\""
+
+  echo ""
+  echo "==> Updating docs-src/config.toml..."
+  sed -i '' "s/^version = \"[^\"]*\"/version = \"$NEW_VERSION\"/" docs-src/config.toml
+  echo "    version = \"$NEW_VERSION\""
+
+  # --- Step 6: Commit, tag, and push ---
+  echo ""
+  echo "==> Committing and tagging..."
+  git add httplib.h docs-src/config.toml
+  git commit -m "Release v$NEW_VERSION"
+  git tag "v$NEW_VERSION"
+  git tag -f "latest"
+
+  echo ""
+  echo "==> Pushing..."
+  git push && git push --tags --force
+
+  echo ""
+  echo "==> Released v$NEW_VERSION"
+fi

+ 59 - 0
test/test.cc

@@ -2382,6 +2382,65 @@ TEST(RedirectToDifferentPort, Redirect) {
   EXPECT_EQ("Hello World!", res->body);
 }
 
+static void
+TestDoNotForwardCredentialsOnRedirect(std::function<void(Client &)> set_auth) {
+  Server svr1;
+  std::string captured_authorization;
+  svr1.Get("/target", [&](const Request &req, Response &res) {
+    captured_authorization = req.get_header_value("Authorization");
+    res.set_content("OK", "text/plain");
+  });
+
+  int svr1_port = 0;
+  auto thread1 = std::thread([&]() {
+    svr1_port = svr1.bind_to_any_port(HOST);
+    svr1.listen_after_bind();
+  });
+
+  Server svr2;
+  svr2.Get("/redir", [&](const Request & /*req*/, Response &res) {
+    res.set_redirect(
+        "http://localhost:" + std::to_string(svr1_port) + "/target", 302);
+  });
+
+  int svr2_port = 0;
+  auto thread2 = std::thread([&]() {
+    svr2_port = svr2.bind_to_any_port(HOST);
+    svr2.listen_after_bind();
+  });
+  auto se = detail::scope_exit([&] {
+    svr2.stop();
+    thread2.join();
+    svr1.stop();
+    thread1.join();
+    ASSERT_FALSE(svr2.is_running());
+    ASSERT_FALSE(svr1.is_running());
+  });
+
+  svr1.wait_until_ready();
+  svr2.wait_until_ready();
+
+  Client cli("localhost", svr2_port);
+  cli.set_follow_location(true);
+  set_auth(cli);
+
+  auto res = cli.Get("/redir");
+  ASSERT_TRUE(res);
+  EXPECT_EQ(StatusCode::OK_200, res->status);
+  // RFC 9110: credentials MUST NOT be forwarded to a different host
+  EXPECT_TRUE(captured_authorization.empty());
+}
+
+TEST(RedirectToDifferentPort, DoNotForwardCredentialsBasicAuth) {
+  TestDoNotForwardCredentialsOnRedirect(
+      [](Client &cli) { cli.set_basic_auth("admin", "secret"); });
+}
+
+TEST(RedirectToDifferentPort, DoNotForwardCredentialsBearerToken) {
+  TestDoNotForwardCredentialsOnRedirect(
+      [](Client &cli) { cli.set_bearer_token_auth("my-secret-token"); });
+}
+
 TEST(RedirectToDifferentPort, OverflowPortNumber) {
   Server svr;
   svr.Get("/redir", [&](const Request & /*req*/, Response &res) {