From 523748e134909bc322a3708c333d7d715abbaae9 Mon Sep 17 00:00:00 2001 From: Aaron3S Date: Mon, 26 Oct 2026 14:34:30 +0800 Subject: [PATCH 1/3] feat(chen): proxy Go workbench and stabilize web image builds Route Chen v1 HTTP and SSE requests without WebSocket upgrade or proxy buffering. Cache official PAM downloads ahead of frontend artifacts and retry network-only TLS failures while retaining certificate and checksum verification. --- Dockerfile | 6 +++--- includes/chen.conf | 17 +++++++++++++++++ pam-agent.sh | 18 ++++++++++++++++-- 3 files changed, 36 insertions(+), 5 deletions(-) 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/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 From 0eda1d5e35e6648ff8dff25de09fa0303b4a39a4 Mon Sep 17 00:00:00 2001 From: Aaron3S Date: Mon, 26 Oct 2026 16:18:11 +0800 Subject: [PATCH 2/3] fix(chen): preserve external port through web proxy --- https_server.conf | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) 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 时由启动脚本改为信任上游代理头。 From f9446698b72a7a11d0dd90abc5966cf6cccc10f8 Mon Sep 17 00:00:00 2001 From: Aaron3S Date: Mon, 26 Oct 2026 16:54:28 +0800 Subject: [PATCH 3/3] fix(build): retry official client downloads after TLS negotiation failures --- client.sh | 41 ++++++++++++++----- tests/test_client_download.py | 76 +++++++++++++++++++++++++++++++++++ 2 files changed, 106 insertions(+), 11 deletions(-) create mode 100644 tests/test_client_download.py 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/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()