diff --git a/Dockerfile b/Dockerfile index 3571be0..9c91f7f 100644 --- a/Dockerfile +++ b/Dockerfile @@ -34,13 +34,13 @@ RUN set -ex \ WORKDIR /opt -COPY --from=lina /opt/lina /opt/lina -COPY --from=luna /opt/luna /opt/luna -ARG VERSION ARG PAM_AGENT_VERSION COPY pam-agent.sh pam-agent-version.txt /tmp/ RUN PAM_AGENT_VERSION="${PAM_AGENT_VERSION}" sh /tmp/pam-agent.sh \ && rm -f /tmp/pam-agent.sh /tmp/pam-agent-version.txt +COPY --from=lina /opt/lina /opt/lina +COPY --from=luna /opt/luna /opt/luna +ARG VERSION COPY versions.txt /opt/download/versions.txt COPY client-version.txt /opt/download/client-version.txt COPY nginx.conf /etc/nginx/nginx.conf diff --git a/client.sh b/client.sh index 8c9bf3b..5fcd38e 100755 --- a/client.sh +++ b/client.sh @@ -9,8 +9,10 @@ PROJECT_DIR=$(CDPATH= cd -- "$(dirname -- "$0")" && pwd) : "${CLIENT_NAME:?CLIENT_NAME is required}" : "${WEBLITE_VERSION:?WEBLITE_VERSION is required}" -RELEASE_URL=${CLIENT_RELEASE_URL:-https://github.com/jumpserver/luna/releases/download/client-v${CLIENT_VERSION}} -WEBLITE_RELEASE_URL=${WEBLITE_RELEASE_URL:-https://github.com/jumpserver/luna/releases/download/weblite-v${WEBLITE_VERSION}} +OFFICIAL_RELEASE_URL=https://github.com/jumpserver/luna/releases/download/client-v${CLIENT_VERSION} +RELEASE_URL=${CLIENT_RELEASE_URL:-${OFFICIAL_RELEASE_URL}} +OFFICIAL_WEBLITE_RELEASE_URL=https://github.com/jumpserver/luna/releases/download/weblite-v${WEBLITE_VERSION} +WEBLITE_RELEASE_URL=${WEBLITE_RELEASE_URL:-${OFFICIAL_WEBLITE_RELEASE_URL}} DOWNLOAD_DIR=${CLIENT_DOWNLOAD_DIR:-/opt/download/public} WEBLITE_DOWNLOAD_DIR=${WEBLITE_DOWNLOAD_DIR:-/opt/download/applets} @@ -21,15 +23,32 @@ windows_file="${CLIENT_NAME}Client_${CLIENT_VERSION}_x64-setup.exe" macos_arm64_file="${CLIENT_NAME}Client_${CLIENT_VERSION}_aarch64.dmg" weblite_file="JumpServer-WebLite-${WEBLITE_VERSION}-x64.msi" -wget --https-only \ - --output-document="${windows_file}" \ - "${RELEASE_URL}/${CLIENT_NAME}-${CLIENT_VERSION}-Setup.exe" -wget --https-only \ - --output-document="${macos_arm64_file}" \ - "${RELEASE_URL}/${CLIENT_NAME}-${CLIENT_VERSION}-arm64.dmg" -wget --https-only \ - --output-document="${WEBLITE_DOWNLOAD_DIR}/${weblite_file}" \ - "${WEBLITE_RELEASE_URL}/${weblite_file}" +download_release() { + file=$1 url=$2 official_url=$3 + if wget --https-only --timeout=30 --tries=1 --output-document="${file}" "${url}"; then + return 0 + else + status=$? + fi + # Match PAM downloads: retry only a network failure against the same official + # URL. Certificate, authentication and HTTP failures retain their error status. + if [ "${status}" -ne 4 ] || [ "${url}" != "${official_url}" ]; then + return "${status}" + fi + printf '%s\n' "Client download network failure; retrying once with TLS 1.2" >&2 + wget --https-only --timeout=30 --tries=1 --secure-protocol=TLSv1_2 \ + --output-document="${file}" "${url}" +} + +download_release "${windows_file}" \ + "${RELEASE_URL}/${CLIENT_NAME}-${CLIENT_VERSION}-Setup.exe" \ + "${OFFICIAL_RELEASE_URL}/${CLIENT_NAME}-${CLIENT_VERSION}-Setup.exe" +download_release "${macos_arm64_file}" \ + "${RELEASE_URL}/${CLIENT_NAME}-${CLIENT_VERSION}-arm64.dmg" \ + "${OFFICIAL_RELEASE_URL}/${CLIENT_NAME}-${CLIENT_VERSION}-arm64.dmg" +download_release "${WEBLITE_DOWNLOAD_DIR}/${weblite_file}" \ + "${WEBLITE_RELEASE_URL}/${weblite_file}" \ + "${OFFICIAL_WEBLITE_RELEASE_URL}/${weblite_file}" ln -s "${windows_file}" "Client_${CLIENT_VERSION}_x64-setup.exe" ln -s "${macos_arm64_file}" "Client_${CLIENT_VERSION}_aarch64.dmg" diff --git a/https_server.conf b/https_server.conf index 3107cbe..f55a6dd 100644 --- a/https_server.conf +++ b/https_server.conf @@ -17,7 +17,7 @@ server { proxy_buffering off; proxy_request_buffering off; proxy_http_version 1.1; - proxy_set_header Host $host; + proxy_set_header Host $http_host; proxy_set_header Upgrade $http_upgrade; proxy_set_header Connection $http_connection; # 默认作为第一层;仅显式 USE_LB=0 时由启动脚本改为信任上游代理头。 @@ -55,7 +55,7 @@ server { proxy_buffering off; proxy_request_buffering off; proxy_http_version 1.1; - proxy_set_header Host $host; + proxy_set_header Host $http_host; proxy_set_header Upgrade $http_upgrade; proxy_set_header Connection $http_connection; # 默认作为第一层;仅显式 USE_LB=0 时由启动脚本改为信任上游代理头。 diff --git a/includes/chen.conf b/includes/chen.conf index 28f8289..a551ae8 100644 --- a/includes/chen.conf +++ b/includes/chen.conf @@ -1,3 +1,20 @@ +# Go SQL workbench: request/response over HTTP and one session event stream. +# Keep this prefix separate from legacy Chen WebSocket routes. +location /chen/api/v1/ { + proxy_pass http://chen:8082; + proxy_http_version 1.1; + proxy_buffering off; + proxy_cache off; + proxy_request_buffering off; + proxy_set_header Connection ""; + proxy_set_header Host $http_host; + proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; + proxy_connect_timeout 30; + proxy_send_timeout 600; + proxy_read_timeout 600; + send_timeout 600; +} + location /chen/ { proxy_pass http://chen:8082; proxy_buffering off; diff --git a/pam-agent.sh b/pam-agent.sh index b9d0d10..ef9394b 100755 --- a/pam-agent.sh +++ b/pam-agent.sh @@ -6,13 +6,27 @@ PROJECT_DIR=$(CDPATH= cd -- "$(dirname -- "$0")" && pwd) : "${PAM_AGENT_VERSION:?PAM_AGENT_VERSION must name a published JumpServer release}" RELEASE_TAG="v${PAM_AGENT_VERSION#v}" -RELEASE_URL=${PAM_AGENT_RELEASE_URL:-https://github.com/jumpserver/pam-clients/releases/download/${RELEASE_TAG}} +OFFICIAL_RELEASE_URL=https://github.com/jumpserver/pam-clients/releases/download/${RELEASE_TAG} +RELEASE_URL=${PAM_AGENT_RELEASE_URL:-${OFFICIAL_RELEASE_URL}} DOWNLOAD_DIR=${PAM_AGENT_DOWNLOAD_DIR:-/opt/download/pam} mkdir -p "${DOWNLOAD_DIR}" cd "${DOWNLOAD_DIR}" for filename in jms-pam-agent-linux-amd64 jms-pam-agent-linux-arm64 SHA256SUMS; do - wget --https-only --output-document="${filename}" "${RELEASE_URL}/${filename}" + if wget --https-only --timeout=30 --tries=1 --output-document="${filename}" "${RELEASE_URL}/${filename}"; then + continue + else + status=$? + fi + # Some GnuTLS/proxy combinations fail with automatic TLS negotiation. Retry + # network errors once with TLS 1.2, only for the same official release URL. + # Certificate, authentication, HTTP and checksum errors must still fail. + if [ "${status}" -ne 4 ] || [ "${RELEASE_URL}" != "${OFFICIAL_RELEASE_URL}" ]; then + exit "${status}" + fi + printf '%s\n' "PAM download network failure; retrying ${filename} once with TLS 1.2" >&2 + wget --https-only --timeout=30 --tries=1 --secure-protocol=TLSv1_2 \ + --output-document="${filename}" "${RELEASE_URL}/${filename}" done sha256sum --check SHA256SUMS chmod 0755 jms-pam-agent-linux-amd64 jms-pam-agent-linux-arm64 diff --git a/tests/test_client_download.py b/tests/test_client_download.py new file mode 100644 index 0000000..be76868 --- /dev/null +++ b/tests/test_client_download.py @@ -0,0 +1,76 @@ +import json +import os +from pathlib import Path +import shutil +import subprocess +import tempfile +import unittest + + +ROOT = Path(__file__).resolve().parents[1] + + +class ClientDownloadTest(unittest.TestCase): + def run_download(self, failure=0, custom_url=False): + with tempfile.TemporaryDirectory() as directory: + root = Path(directory) + for name in ("client.sh", "client-version.txt"): + shutil.copyfile(ROOT / name, root / name) + executable = root / "wget" + executable.write_text( + "#!/usr/bin/env python3\n" + "import json, os, sys\n" + "from pathlib import Path\n" + "log = Path(os.environ['DOWNLOAD_TEST_LOG'])\n" + "calls = json.loads(log.read_text()) if log.exists() else []\n" + "calls.append(sys.argv[1:]); log.write_text(json.dumps(calls))\n" + "if len(calls) == 1: sys.exit(int(os.environ['DOWNLOAD_TEST_FAILURE']))\n" + ) + executable.chmod(0o755) + env = dict(os.environ) + env.update( + PATH=str(root) + os.pathsep + env["PATH"], + DOWNLOAD_TEST_LOG=str(root / "calls.json"), + DOWNLOAD_TEST_FAILURE=str(failure), + CLIENT_DOWNLOAD_DIR=str(root / "public"), + WEBLITE_DOWNLOAD_DIR=str(root / "applets"), + ) + env.pop("CLIENT_RELEASE_URL", None) + env.pop("WEBLITE_RELEASE_URL", None) + if custom_url: + env["CLIENT_RELEASE_URL"] = "https://mirror.example.invalid/releases" + process = subprocess.run(["sh", str(root / "client.sh")], env=env, capture_output=True) + return process.returncode, json.loads((root / "calls.json").read_text()) + + def test_normal_downloads_have_no_tls_override(self): + code, calls = self.run_download() + self.assertEqual(code, 0) + self.assertEqual(len(calls), 3) + self.assertTrue(all("--https-only" in call for call in calls)) + self.assertFalse(any("--secure-protocol=TLSv1_2" in call for call in calls)) + + def test_network_retry_keeps_exact_official_url_and_target(self): + code, calls = self.run_download(4) + self.assertEqual(code, 0) + self.assertEqual(len(calls), 4) + self.assertIn("--secure-protocol=TLSv1_2", calls[1]) + self.assertEqual(calls[0][-1], calls[1][-1]) + target = lambda call: next(arg for arg in call if arg.startswith("--output-document=")) + self.assertEqual(target(calls[0]), target(calls[1])) + self.assertNotIn("--no-check-certificate", calls[1]) + + def test_certificate_authentication_and_http_failures_are_not_retried(self): + for failure in (5, 6, 8): + with self.subTest(failure=failure): + code, calls = self.run_download(failure) + self.assertEqual(code, failure) + self.assertEqual(len(calls), 1) + + def test_custom_mirror_does_not_get_tls_fallback(self): + code, calls = self.run_download(4, custom_url=True) + self.assertEqual(code, 4) + self.assertEqual(len(calls), 1) + + +if __name__ == "__main__": + unittest.main()