feat: support GitHub token for update checks

This commit is contained in:
wucm667
2026-07-19 11:02:56 +08:00
parent d4b9797ff7
commit 510ee451bd
9 changed files with 298 additions and 10 deletions
+4
View File
@@ -23,6 +23,10 @@ SERVER_PORT=8080
# Server mode: release or debug
SERVER_MODE=release
# Optional token used only for GitHub Release API update checks. Release asset
# downloads remain anonymous. GITHUB_TOKEN and GH_TOKEN are not used.
UPDATE_GITHUB_TOKEN=
# Return Server-Timing for authenticated requests made by the Admin web UI
ENABLE_SERVER_TIMING=false
+1
View File
@@ -238,6 +238,7 @@ docker compose down -v
| `ADMIN_EMAIL` | No | `admin@sub2api.local` | Admin email |
| `ADMIN_PASSWORD` | No | *(auto-generated)* | Admin password |
| `TZ` | No | `Asia/Shanghai` | Timezone |
| `UPDATE_GITHUB_TOKEN` | No | *(empty)* | Token for `api.github.com` release checks only; asset downloads remain anonymous. |
| `GEMINI_OAUTH_CLIENT_ID` | No | *(builtin)* | Google OAuth client ID (Gemini OAuth). Leave empty to use the built-in Gemini CLI client. |
| `GEMINI_OAUTH_CLIENT_SECRET` | No | *(builtin)* | Google OAuth client secret (Gemini OAuth). Leave empty to use the built-in Gemini CLI client. |
| `GEMINI_OAUTH_SCOPES` | No | *(default)* | OAuth scopes (Gemini OAuth) |
+1
View File
@@ -53,6 +53,7 @@ services:
- SERVER_MODE=${SERVER_MODE:-release}
- ENABLE_SERVER_TIMING=${ENABLE_SERVER_TIMING:-false}
- RUN_MODE=${RUN_MODE:-standard}
- UPDATE_GITHUB_TOKEN=${UPDATE_GITHUB_TOKEN:-}
# =======================================================================
# Database Configuration (PostgreSQL)
+1
View File
@@ -39,6 +39,7 @@ services:
- SERVER_MODE=${SERVER_MODE:-release}
- ENABLE_SERVER_TIMING=${ENABLE_SERVER_TIMING:-false}
- RUN_MODE=${RUN_MODE:-standard}
- UPDATE_GITHUB_TOKEN=${UPDATE_GITHUB_TOKEN:-}
# =======================================================================
# Database Configuration (PostgreSQL) - Required
+1
View File
@@ -49,6 +49,7 @@ services:
- SERVER_MODE=${SERVER_MODE:-release}
- ENABLE_SERVER_TIMING=${ENABLE_SERVER_TIMING:-false}
- RUN_MODE=${RUN_MODE:-standard}
- UPDATE_GITHUB_TOKEN=${UPDATE_GITHUB_TOKEN:-}
# =======================================================================
# Database Configuration (PostgreSQL)
+51 -3
View File
@@ -480,10 +480,58 @@ check_dependencies() {
fi
}
# Authenticate only GitHub REST API requests. Release asset downloads must stay anonymous.
github_api_curl() {
local arg
local expect_value=false
local url
if [ "$#" -lt 1 ]; then
echo "github_api_curl requires exactly one GitHub API URL" >&2
return 2
fi
url="${!#}"
# Keep authenticated invocations constrained to the options used below. In
# particular, curl config, --url, and --next could add another destination.
for arg in "${@:1:$#-1}"; do
if [ "$expect_value" = true ]; then
expect_value=false
continue
fi
case "$arg" in
-s|--silent)
;;
--connect-timeout|--max-time|-o|--output|-w|--write-out)
expect_value=true
;;
*)
echo "Unsafe github_api_curl argument: $arg" >&2
return 2
;;
esac
done
if [ "$expect_value" = true ] || [[ "$url" != https://api.github.com/* ]]; then
echo "github_api_curl requires exactly one GitHub API URL" >&2
return 2
fi
if [ -n "${UPDATE_GITHUB_TOKEN:-}" ]; then
if [[ "$UPDATE_GITHUB_TOKEN" == *$'\n'* || "$UPDATE_GITHUB_TOKEN" == *$'\r'* || "$UPDATE_GITHUB_TOKEN" == *'"'* || "$UPDATE_GITHUB_TOKEN" == *'\'* ]]; then
echo "UPDATE_GITHUB_TOKEN contains unsupported characters" >&2
return 2
fi
printf 'header = "Authorization: Bearer %s"\n' "$UPDATE_GITHUB_TOKEN" | UPDATE_GITHUB_TOKEN= GITHUB_TOKEN= GH_TOKEN= curl -q --globoff --config - "$@"
else
UPDATE_GITHUB_TOKEN= GITHUB_TOKEN= GH_TOKEN= curl -q --globoff "$@"
fi
}
# Get latest release version
get_latest_version() {
print_info "$(msg 'fetching_version')"
LATEST_VERSION=$(curl -s --connect-timeout 10 --max-time 30 "https://api.github.com/repos/${GITHUB_REPO}/releases/latest" 2>/dev/null | grep '"tag_name"' | sed -E 's/.*"([^"]+)".*/\1/')
LATEST_VERSION=$(github_api_curl -s --connect-timeout 10 --max-time 30 "https://api.github.com/repos/${GITHUB_REPO}/releases/latest" 2>/dev/null | grep '"tag_name"' | sed -E 's/.*"([^"]+)".*/\1/')
if [ -z "$LATEST_VERSION" ]; then
print_error "$(msg 'failed_get_version')"
@@ -499,7 +547,7 @@ list_versions() {
print_info "$(msg 'fetching_versions')"
local versions
versions=$(curl -s --connect-timeout 10 --max-time 30 "https://api.github.com/repos/${GITHUB_REPO}/releases" 2>/dev/null | grep '"tag_name"' | sed -E 's/.*"([^"]+)".*/\1/' | head -20)
versions=$(github_api_curl -s --connect-timeout 10 --max-time 30 "https://api.github.com/repos/${GITHUB_REPO}/releases" 2>/dev/null | grep '"tag_name"' | sed -E 's/.*"([^"]+)".*/\1/' | head -20)
if [ -z "$versions" ]; then
print_error "$(msg 'failed_get_version')"
@@ -536,7 +584,7 @@ validate_version() {
# Check if the release exists
local http_code
http_code=$(curl -s -o /dev/null -w "%{http_code}" --connect-timeout 10 --max-time 30 "https://api.github.com/repos/${GITHUB_REPO}/releases/tags/${version}" 2>/dev/null)
http_code=$(github_api_curl -s -o /dev/null -w "%{http_code}" --connect-timeout 10 --max-time 30 "https://api.github.com/repos/${GITHUB_REPO}/releases/tags/${version}" 2>/dev/null)
# Check for network errors (empty or non-numeric response)
if [ -z "$http_code" ] || ! [[ "$http_code" =~ ^[0-9]+$ ]]; then
+103
View File
@@ -0,0 +1,103 @@
#!/bin/bash
set -euo pipefail
ROOT_DIR=$(cd "$(dirname "${BASH_SOURCE[0]}")/../.." && pwd)
TEMP_DIR=$(mktemp -d)
trap 'rm -rf "$TEMP_DIR"' EXIT
cat > "$TEMP_DIR/curl" <<'EOF'
#!/bin/bash
printf '%s\n' "$@" > "$CURL_ARGS_LOG"
env > "${CURL_ARGS_LOG}.env"
cat > "${CURL_ARGS_LOG}.stdin"
EOF
chmod +x "$TEMP_DIR/curl"
mkdir "$TEMP_DIR/home"
cat > "$TEMP_DIR/home/.curlrc" <<'EOF'
url = "https://example.com/collect"
header = "X-Leaked-From-Curlrc: yes"
EOF
run_api_curl() {
CURL_ARGS_LOG="$1" HOME="$TEMP_DIR/home" PATH="$TEMP_DIR:$PATH" UPDATE_GITHUB_TOKEN="${2:-}" \
GITHUB_TOKEN="github-fallback" GH_TOKEN="gh-fallback" \
bash -c 'source <(head -n -1 "$1"); github_api_curl -s "$2"' bash \
"$ROOT_DIR/deploy/install.sh" "https://api.github.com/repos/Wei-Shaw/sub2api/releases/latest"
}
run_api_curl "$TEMP_DIR/authenticated" "update-secret"
test "$(head -n 1 "$TEMP_DIR/authenticated")" = '-q'
grep -Fxq -- '--config' "$TEMP_DIR/authenticated"
grep -Fxq -- '-' "$TEMP_DIR/authenticated"
grep -Fxq -- '--globoff' "$TEMP_DIR/authenticated"
grep -Fxq 'header = "Authorization: Bearer update-secret"' "$TEMP_DIR/authenticated.stdin"
if grep -Fq 'update-secret' "$TEMP_DIR/authenticated"; then
echo "installer exposed the update token in curl argv" >&2
exit 1
fi
if grep -Eq 'update-secret|github-fallback|gh-fallback' "$TEMP_DIR/authenticated.env"; then
echo "installer exposed a token in curl environment" >&2
exit 1
fi
test "$(grep -Fxc 'https://api.github.com/repos/Wei-Shaw/sub2api/releases/latest' "$TEMP_DIR/authenticated")" -eq 1
if grep -Fq 'example.com/collect' "$TEMP_DIR/authenticated" || grep -Fq 'X-Leaked-From-Curlrc' "$TEMP_DIR/authenticated" ||
grep -Fq 'example.com/collect' "$TEMP_DIR/authenticated.stdin" || grep -Fq 'X-Leaked-From-Curlrc' "$TEMP_DIR/authenticated.stdin"; then
echo "installer allowed hostile curl config into authenticated invocation" >&2
exit 1
fi
run_api_curl "$TEMP_DIR/anonymous"
test "$(head -n 1 "$TEMP_DIR/anonymous")" = '-q'
if grep -Eq 'github-fallback|gh-fallback' "$TEMP_DIR/anonymous.env"; then
echo "installer exposed a fallback token in anonymous curl environment" >&2
exit 1
fi
if grep -Fq 'Authorization:' "$TEMP_DIR/anonymous"; then
echo "installer unexpectedly used a fallback token" >&2
exit 1
fi
test ! -s "$TEMP_DIR/anonymous.stdin"
test "$(grep -Fxc 'https://api.github.com/repos/Wei-Shaw/sub2api/releases/latest' "$TEMP_DIR/anonymous")" -eq 1
if grep -Fq 'example.com/collect' "$TEMP_DIR/anonymous" || grep -Fq 'X-Leaked-From-Curlrc' "$TEMP_DIR/anonymous"; then
echo "installer allowed hostile curl config into anonymous invocation" >&2
exit 1
fi
assert_unsafe_invocation_rejected() {
local name=$1
shift
rm -f "$TEMP_DIR/$name" "$TEMP_DIR/$name.stdin"
if CURL_ARGS_LOG="$TEMP_DIR/$name" PATH="$TEMP_DIR:$PATH" UPDATE_GITHUB_TOKEN="update-secret" \
bash -c 'source <(head -n -1 "$1"); shift; github_api_curl "$@"' bash \
"$ROOT_DIR/deploy/install.sh" "$@" 2>/dev/null; then
echo "installer accepted unsafe curl invocation: $name" >&2
exit 1
fi
if [ -e "$TEMP_DIR/$name" ]; then
echo "installer invoked curl for unsafe request: $name" >&2
exit 1
fi
}
assert_unsafe_invocation_rejected non-api -s \
"https://github.com/Wei-Shaw/sub2api/releases/download/v1/asset"
assert_unsafe_invocation_rejected mixed-host -s \
"https://api.github.com/repos/Wei-Shaw/sub2api/releases/latest" \
"https://example.com/collect"
assert_unsafe_invocation_rejected multiple-api -s \
"https://api.github.com/repos/Wei-Shaw/sub2api/releases/latest" \
"https://api.github.com/repos/Wei-Shaw/sub2api/releases"
assert_unsafe_invocation_rejected url-option -s --url \
"https://example.com/collect" \
"https://api.github.com/repos/Wei-Shaw/sub2api/releases/latest"
# Every installer release API request must use the scoped helper.
test "$(grep -c 'github_api_curl .*https://api.github.com/' "$ROOT_DIR/deploy/install.sh")" -eq 3
# Asset and checksum downloads must continue to call curl directly.
grep -Fq 'curl -sL "$download_url"' "$ROOT_DIR/deploy/install.sh"
grep -Fq 'curl -sL "$checksum_url"' "$ROOT_DIR/deploy/install.sh"
echo "install GitHub token checks passed"