This is an automated email from the ASF dual-hosted git repository.

davidzollo pushed a commit to branch dev
in repository https://gitbox.apache.org/repos/asf/seatunnel.git


The following commit(s) were added to refs/heads/dev by this push:
     new 735cf43052 [Improve][Core] Download connector plugins over HTTPS
735cf43052 is described below

commit 735cf430525622f6f2d1c8cebf17598605aca232
Author: Daniel <[email protected]>
AuthorDate: Thu Jul 30 01:59:54 2026 +0800

    [Improve][Core] Download connector plugins over HTTPS
    
    Merge pull request #11555 from DanielLeens/davidzollo_install-plugin-https
---
 bin/install-plugin.sh                              | 270 ++++++++-
 docs/en/getting-started/locally/deployment.md      |   9 +
 .../introduction/concepts/incompatible-changes.md  |   6 +
 docs/zh/getting-started/locally/deployment.md      |   9 +
 .../introduction/concepts/incompatible-changes.md  |   6 +
 .../installer/InstallPluginScriptTest.java         | 617 +++++++++++++++++++++
 6 files changed, 897 insertions(+), 20 deletions(-)

diff --git a/bin/install-plugin.sh b/bin/install-plugin.sh
index 3059bb081d..77c146c8ce 100755
--- a/bin/install-plugin.sh
+++ b/bin/install-plugin.sh
@@ -16,37 +16,267 @@
 # limitations under the License.
 #
 
-#This script is used to download the connector plug-ins required during the 
running process. 
-#All are downloaded by default. You can also choose what you need. 
-#You only need to configure the plug-in name in config/plugin_config.
+# This script downloads the connector plug-ins selected in 
config/plugin_config.
 
-# get seatunnel home
-SEATUNNEL_HOME=$(cd $(dirname $0);cd ../;pwd)
+SEATUNNEL_HOME=$(cd "$(dirname "$0")"; cd ../; pwd)
 
-# connector default version is 3.0.0, you can also choose a custom version. 
eg: 3.0.0:  sh install-plugin.sh 3.0.0
+# Connector default version is 3.0.0. You can also choose a custom version.
 version=3.0.0
-
 if [ -n "$1" ]; then
     version="$1"
 fi
 
-echo "Install SeaTunnel connectors plugins, usage version is ${version}"
+download_method=${SEATUNNEL_PLUGIN_DOWNLOAD_METHOD:-https}
+maven_repository=${SEATUNNEL_MAVEN_REPOSITORY:-https://repo.maven.apache.org/maven2}
+maven_repository=${maven_repository%/}
+temporary_file=
+checksum_file=
+temporary_directory=
+
+cleanup_temporary_files() {
+    if [ -n "$temporary_file" ]; then
+        rm -f "$temporary_file"
+    fi
+    if [ -n "$checksum_file" ]; then
+        rm -f "$checksum_file"
+    fi
+    if [ -n "$temporary_directory" ]; then
+        rmdir "$temporary_directory" 2>/dev/null || true
+    fi
+}
+
+trap cleanup_temporary_files EXIT
+trap 'exit 1' HUP INT TERM
+
+case "$download_method" in
+    https | maven)
+        ;;
+    *)
+        echo "Error: SEATUNNEL_PLUGIN_DOWNLOAD_METHOD must be 'https' or 
'maven'." >&2
+        exit 1
+        ;;
+esac
+
+# Maven resolves unique snapshots, dynamic versions, mirrors, credentials, and 
repository policies.
+case "$version" in
+    *-SNAPSHOT | LATEST | RELEASE | *"["* | *"]"* | *"("* | *")"* | *","* | 
*"+"*)
+        download_method=maven
+        ;;
+esac
 
-# create the connectors directory
-if [ ! -d ${SEATUNNEL_HOME}/connectors ];
-  then
-      mkdir ${SEATUNNEL_HOME}/connectors
-      echo "create connectors directory"
+if [ "$download_method" = "https" ]; then
+    case "$version" in
+        *[!A-Za-z0-9._-]* | "")
+            echo "Error: invalid connector release version '${version}'." >&2
+            exit 1
+            ;;
+    esac
+    case "$maven_repository" in
+        https://*)
+            ;;
+        *)
+            echo "Error: SEATUNNEL_MAVEN_REPOSITORY must use HTTPS." >&2
+            exit 1
+            ;;
+    esac
+    if ! command -v curl >/dev/null 2>&1; then
+        echo "Error: curl is required to download connector plugins over 
HTTPS." >&2
+        exit 1
+    fi
+    if ! command -v mktemp >/dev/null 2>&1; then
+        echo "Error: mktemp is required to create secure temporary files." >&2
+        exit 1
+    fi
+    if ! command -v sha512sum >/dev/null 2>&1 &&
+        ! command -v sha1sum >/dev/null 2>&1 &&
+        ! command -v shasum >/dev/null 2>&1 &&
+        ! command -v openssl >/dev/null 2>&1; then
+        echo "Error: a SHA-512 or SHA-1 checksum tool is required." >&2
+        exit 1
+    fi
 fi
 
-while read line; do
-    first_char=$(echo "$line" | cut -c 1)
+echo "Install SeaTunnel connector plugins, version: ${version}, method: 
${download_method}"
+
+if [ ! -d "${SEATUNNEL_HOME}/connectors" ]; then
+    mkdir -p "${SEATUNNEL_HOME}/connectors"
+    echo "Create connectors directory"
+fi
+
+download_https() {
+    source_url=$1
+    output_file=$2
+    allow_not_found=${3:-false}
+
+    http_status=$(
+        curl --fail --location --retry 3 --connect-timeout 10 \
+        --proto '=https' --proto-redir '=https' \
+        --write-out '%{http_code}' \
+        --output "$output_file" "$source_url"
+    )
+    curl_exit_code=$?
+    if [ "$curl_exit_code" -eq 0 ]; then
+        return 0
+    fi
+
+    if [ "$allow_not_found" = "true" ] && [ "$http_status" = "404" ]; then
+        return 44
+    fi
+    return "$curl_exit_code"
+}
+
+calculate_checksum() {
+    checksum_calculation_algorithm=$1
+    checksum_input_file=$2
 
-    if [ "$first_char" != "-" ] && [ "$first_char" != "#" ] && [ ! -z 
$first_char ]
-       then
-               echo "install connector : " $line
-               ${SEATUNNEL_HOME}/mvnw dependency:get -Dtransitive=false 
-DgroupId=org.apache.seatunnel -DartifactId=${line} -Dversion=${version} 
-Ddest=${SEATUNNEL_HOME}/connectors
+    case "$checksum_calculation_algorithm" in
+        sha512)
+            if command -v sha512sum >/dev/null 2>&1; then
+                sha512sum "$checksum_input_file" | awk '{print $1}'
+            elif command -v shasum >/dev/null 2>&1; then
+                shasum -a 512 "$checksum_input_file" | awk '{print $1}'
+            elif command -v openssl >/dev/null 2>&1; then
+                openssl dgst -sha512 "$checksum_input_file" | awk '{print $NF}'
+            else
+                return 1
+            fi
+            ;;
+        sha1)
+            if command -v sha1sum >/dev/null 2>&1; then
+                sha1sum "$checksum_input_file" | awk '{print $1}'
+            elif command -v shasum >/dev/null 2>&1; then
+                shasum -a 1 "$checksum_input_file" | awk '{print $1}'
+            elif command -v openssl >/dev/null 2>&1; then
+                openssl dgst -sha1 "$checksum_input_file" | awk '{print $NF}'
+            else
+                return 1
+            fi
+            ;;
+    esac
+}
+
+supports_checksum_algorithm() {
+    checksum_support_algorithm=$1
+
+    case "$checksum_support_algorithm" in
+        sha512)
+            command -v sha512sum >/dev/null 2>&1 ||
+                command -v shasum >/dev/null 2>&1 ||
+                command -v openssl >/dev/null 2>&1
+            ;;
+        sha1)
+            command -v sha1sum >/dev/null 2>&1 ||
+                command -v shasum >/dev/null 2>&1 ||
+                command -v openssl >/dev/null 2>&1
+            ;;
+    esac
+}
+
+verify_checksum() {
+    checksum_artifact_file=$1
+    checksum_sidecar_path=$2
+    checksum_verification_algorithm=$3
+
+    expected_checksum=$(awk 'NR == 1 {print $1}' "$checksum_sidecar_path" | tr 
'A-F' 'a-f')
+    actual_checksum=$(
+        calculate_checksum "$checksum_verification_algorithm" 
"$checksum_artifact_file"
+    ) || return 1
+    actual_checksum=$(printf '%s' "$actual_checksum" | tr 'A-F' 'a-f')
+
+    case "$checksum_verification_algorithm:$expected_checksum" in
+        
sha512:????????????????????????????????????????????????????????????????????????????????????????????????????????????????????????????????
 | \
+        sha1:????????????????????????????????????????)
+            ;;
+        *)
+            return 1
+            ;;
+    esac
+
+    [ "$expected_checksum" = "$actual_checksum" ]
+}
+
+validate_jar() {
+    artifact_file=$1
+    magic_bytes=$(od -An -tx1 -N2 "$artifact_file" 2>/dev/null | tr -d 
'[:space:]')
+    [ "$magic_bytes" = "504b" ]
+}
+
+download_release_plugin() {
+    artifact_id=$1
+    artifact_name="${artifact_id}-${version}.jar"
+    
artifact_url="${maven_repository}/org/apache/seatunnel/${artifact_id}/${version}/${artifact_name}"
+    target_file="${SEATUNNEL_HOME}/connectors/${artifact_name}"
+    temporary_directory=$(
+        mktemp -d "${SEATUNNEL_HOME}/connectors/.install-plugin.XXXXXX"
+    ) || return 1
+    temporary_file="${temporary_directory}/${artifact_name}"
+    checksum_file="${temporary_directory}/${artifact_name}.checksum"
+
+    echo "Install connector: ${artifact_id}"
+    download_https "$artifact_url" "$temporary_file" || return 1
+
+    if supports_checksum_algorithm sha512; then
+        download_https "${artifact_url}.sha512" "$checksum_file" true 
2>/dev/null
+        sha512_download_result=$?
+    else
+        sha512_download_result=44
     fi
 
-done < ${SEATUNNEL_HOME}/config/plugin_config
+    if [ "$sha512_download_result" -eq 0 ]; then
+        checksum_algorithm=sha512
+    elif [ "$sha512_download_result" -eq 44 ] &&
+        supports_checksum_algorithm sha1 &&
+        download_https "${artifact_url}.sha1" "$checksum_file"; then
+        checksum_algorithm=sha1
+    else
+        echo "Error: no usable checksum is available for '${artifact_id}'." >&2
+        return 1
+    fi
+
+    if ! verify_checksum "$temporary_file" "$checksum_file" 
"$checksum_algorithm"; then
+        echo "Error: checksum verification failed for '${artifact_id}'." >&2
+        return 1
+    fi
+    if ! validate_jar "$temporary_file"; then
+        echo "Error: downloaded file for '${artifact_id}' is not a JAR 
archive." >&2
+        return 1
+    fi
+    mv "$temporary_file" "$target_file" || return 1
+    rm -f "$checksum_file"
+    rmdir "$temporary_directory" || return 1
+    temporary_file=
+    checksum_file=
+    temporary_directory=
+}
+
+download_plugin_with_maven() {
+    artifact_id=$1
 
+    echo "Install connector with Maven: ${artifact_id}"
+    "${SEATUNNEL_HOME}/mvnw" dependency:get \
+        -Dtransitive=false \
+        -DgroupId=org.apache.seatunnel \
+        -DartifactId="$artifact_id" \
+        -Dversion="$version" \
+        -Ddest="${SEATUNNEL_HOME}/connectors"
+}
+
+while IFS= read -r line || [ -n "$line" ]; do
+    first_char=$(printf '%s' "$line" | cut -c 1)
+
+    if [ "$first_char" != "-" ] && [ "$first_char" != "#" ] && [ -n 
"$first_char" ]; then
+        case "$line" in
+            *[!A-Za-z0-9._-]*)
+                echo "Error: invalid connector artifact ID '${line}'." >&2
+                exit 1
+                ;;
+        esac
+
+        if [ "$download_method" = "maven" ]; then
+            download_plugin_with_maven "$line" || exit 1
+        elif ! download_release_plugin "$line"; then
+            echo "Error: failed to download connector '${line}'." >&2
+            exit 1
+        fi
+    fi
+done < "${SEATUNNEL_HOME}/config/plugin_config"
diff --git a/docs/en/getting-started/locally/deployment.md 
b/docs/en/getting-started/locally/deployment.md
index a0fd3ca14b..3468578765 100644
--- a/docs/en/getting-started/locally/deployment.md
+++ b/docs/en/getting-started/locally/deployment.md
@@ -41,6 +41,15 @@ If you need a specific connector version, taking 3.0.0 as an 
example, you need t
 sh bin/install-plugin.sh 3.0.0
 ```
 
+For released connector versions, `install-plugin.sh` downloads JARs and their 
checksums directly over HTTPS, so Maven is not required on Linux and macOS. 
This path requires `curl`, `mktemp`, and one of `sha512sum`, `sha1sum`, 
`shasum`, or `openssl`. The Windows `install-plugin.cmd` script continues to 
use the bundled Maven Wrapper. To use an HTTPS Maven-compatible mirror with 
`install-plugin.sh`, set `SEATUNNEL_MAVEN_REPOSITORY` to its base URL:
+
+```bash
+SEATUNNEL_MAVEN_REPOSITORY=https://repo.example.com/maven2 \
+  sh bin/install-plugin.sh 3.0.0
+```
+
+The direct download path supports immutable release versions from repositories 
that publish `.sha512` or `.sha1` checksum files. `SNAPSHOT`, `LATEST`, 
`RELEASE`, and version ranges automatically use the bundled Maven wrapper 
because Maven metadata must be resolved. You can also set 
`SEATUNNEL_PLUGIN_DOWNLOAD_METHOD=maven` to preserve Maven `settings.xml` 
behavior such as mirrors, authenticated repositories, proxies, and custom TLS 
policies.
+
 Typically, you do not need all the connector plugins. You can specify the 
required plugins by configuring `config/plugin_config`. For example, if you 
want the sample application to work properly, you will need the 
`connector-console` and `connector-fake` plugins. You can modify the 
`plugin_config` configuration file as follows:
 
 ```plugin_config
diff --git a/docs/en/introduction/concepts/incompatible-changes.md 
b/docs/en/introduction/concepts/incompatible-changes.md
index 8903c03ca9..5cce8d2a8d 100644
--- a/docs/en/introduction/concepts/incompatible-changes.md
+++ b/docs/en/introduction/concepts/incompatible-changes.md
@@ -78,6 +78,12 @@ You need to check this document before you upgrade to 
related version.
 
 ### Configuration Changes
 
+- **Breaking Change: Released connector installation defaults to direct HTTPS 
downloads**
+  - **Affected component**: `bin/install-plugin.sh` on Linux and macOS
+  - **Description**: Fixed release versions are now downloaded directly from 
Maven Central over HTTPS and verified with a published SHA-512 or SHA-1 
checksum. Previously, every connector was resolved through the bundled Maven 
Wrapper.
+  - **Impact**: Existing environments that depend on Maven `settings.xml` for 
mirrors, authenticated repositories, proxies, or custom TLS policies may no 
longer install released connectors with the default command.
+  - **Migration Guide**: Set `SEATUNNEL_PLUGIN_DOWNLOAD_METHOD=maven` when 
running `install-plugin.sh` to preserve the previous Maven resolution behavior. 
Alternatively, set `SEATUNNEL_MAVEN_REPOSITORY` to an HTTPS Maven-compatible 
mirror that publishes connector checksum files.
+
 - **Breaking Change: CatalogFactory creation path now validates 
`optionRule()`**
   - **Affected component**: `seatunnel-api` — 
`FactoryUtil.createOptionalCatalog()`
   - **Description**: The `FactoryUtil.createOptionalCatalog()` method now 
calls `ConfigValidator.validate(catalogFactory.optionRule())` before creating a 
catalog instance. Previously, no validation was performed on the catalog 
factory's option rules during catalog creation.
diff --git a/docs/zh/getting-started/locally/deployment.md 
b/docs/zh/getting-started/locally/deployment.md
index 1ecad85398..d6773b4e85 100644
--- a/docs/zh/getting-started/locally/deployment.md
+++ b/docs/zh/getting-started/locally/deployment.md
@@ -41,6 +41,15 @@ sh bin/install-plugin.sh
 sh bin/install-plugin.sh 3.0.0
 ```
 
+对于正式发布的连接器版本,`install-plugin.sh` 通过 HTTPS 直接下载 JAR 及其校验文件,因此 Linux 和 macOS 不需要 
Maven。该方式需要 `curl`、`mktemp`,以及 `sha512sum`、`sha1sum`、`shasum` 或 `openssl` 
中的任意一个。Windows 的 `install-plugin.cmd` 仍使用发行包内置的 Maven Wrapper。如果需要为 
`install-plugin.sh` 使用 Maven 兼容的 HTTPS 镜像,可以通过 `SEATUNNEL_MAVEN_REPOSITORY` 
指定仓库根地址:
+
+```bash
+SEATUNNEL_MAVEN_REPOSITORY=https://repo.example.com/maven2 \
+  sh bin/install-plugin.sh 3.0.0
+```
+
+直接下载仅支持提供 `.sha512` 或 `.sha1` 校验文件的不可变正式版本。`SNAPSHOT`、`LATEST`、`RELEASE` 
和版本范围需要解析 Maven 元数据,因此脚本会自动使用发行包内置的 Maven Wrapper。如果需要继续使用 Maven `settings.xml` 
中的镜像、认证仓库、代理或自定义 TLS 策略,也可以设置 `SEATUNNEL_PLUGIN_DOWNLOAD_METHOD=maven`。
+
 
通常情况下,你不需要所有的连接器插件。你可以通过配置`config/plugin_config`来指定所需的插件。例如,如果你想让示例应用程序正常工作,你将需要`connector-console`和`connector-fake`插件。你可以修改`plugin_config`配置文件,如下所示:
 
 ```plugin_config
diff --git a/docs/zh/introduction/concepts/incompatible-changes.md 
b/docs/zh/introduction/concepts/incompatible-changes.md
index fe261ff287..caddbf3172 100644
--- a/docs/zh/introduction/concepts/incompatible-changes.md
+++ b/docs/zh/introduction/concepts/incompatible-changes.md
@@ -77,6 +77,12 @@
 
 ### 配置变更
 
+- **破坏性变更:正式发布版本的连接器默认改为通过 HTTPS 直接下载**
+  - **影响范围**:Linux 和 macOS 上的 `bin/install-plugin.sh`
+  - **变更说明**:对于固定的正式发布版本,脚本现在默认从 Maven Central 通过 HTTPS 直接下载连接器,并使用仓库发布的 
SHA-512 或 SHA-1 校验文件验证完整性。此前所有连接器都通过发行包内置的 Maven Wrapper 解析。
+  - **影响**:如果现有环境依赖 Maven `settings.xml` 中配置的镜像、认证仓库、代理或自定义 TLS 
策略,升级后使用默认命令安装正式版本连接器可能失败。
+  - **迁移指南**:运行 `install-plugin.sh` 时设置 
`SEATUNNEL_PLUGIN_DOWNLOAD_METHOD=maven`,即可保留原有的 Maven 解析行为。也可以通过 
`SEATUNNEL_MAVEN_REPOSITORY` 指定一个发布连接器校验文件的 HTTPS Maven 兼容镜像。
+
 - **破坏性变更:CatalogFactory 创建路径现在会校验 `optionRule()`**
   - **影响范围**:`seatunnel-api` — `FactoryUtil.createOptionalCatalog()`
   - **变更说明**:`FactoryUtil.createOptionalCatalog()` 方法现在在创建 catalog 实例之前会调用 
`ConfigValidator.validate(catalogFactory.optionRule())` 进行校验。此前,catalog 创建路径不会对 
catalog factory 的 option rules 执行任何校验。
diff --git 
a/seatunnel-dist/src/test/java/org/apache/seatunnel/installer/InstallPluginScriptTest.java
 
b/seatunnel-dist/src/test/java/org/apache/seatunnel/installer/InstallPluginScriptTest.java
new file mode 100644
index 0000000000..fd19f20008
--- /dev/null
+++ 
b/seatunnel-dist/src/test/java/org/apache/seatunnel/installer/InstallPluginScriptTest.java
@@ -0,0 +1,617 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements.  See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License.  You may obtain a copy of the License at
+ *
+ *    http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package org.apache.seatunnel.installer;
+
+import org.junit.jupiter.api.Assertions;
+import org.junit.jupiter.api.Test;
+import org.junit.jupiter.api.condition.EnabledOnOs;
+import org.junit.jupiter.api.condition.OS;
+import org.junit.jupiter.api.io.TempDir;
+
+import java.nio.charset.StandardCharsets;
+import java.nio.file.Files;
+import java.nio.file.Path;
+import java.nio.file.Paths;
+import java.nio.file.attribute.PosixFilePermission;
+import java.security.MessageDigest;
+import java.util.EnumSet;
+import java.util.Map;
+import java.util.stream.Stream;
+
+/**
+ * Verifies the connector installer download, integrity, cleanup, and Maven 
compatibility paths
+ * without relying on an external artifact repository.
+ */
+@EnabledOnOs({OS.LINUX, OS.MAC})
+public class InstallPluginScriptTest {
+
+    private static final byte[] JAR_CONTENT =
+            new byte[] {0x50, 0x4B, 0x03, 0x04, 'f', 'i', 'x', 't', 'u', 'r', 
'e'};
+
+    @TempDir private Path temporaryDirectory;
+
+    /**
+     * Verifies that a release artifact is installed only after its checksum 
and JAR header pass.
+     */
+    @Test
+    public void testReleaseDownloadWithChecksum() throws Exception {
+        Path distribution = createDistribution("connector-fake");
+        Path fakeBinaryDirectory = createFakeCurl(sha1Hex(JAR_CONTENT), false, 
404);
+
+        int exitCode = runInstaller(distribution, fakeBinaryDirectory, 
"2.3.13", null);
+
+        Assertions.assertEquals(0, exitCode);
+        Path installedJar = 
distribution.resolve("connectors").resolve("connector-fake-2.3.13.jar");
+        Assertions.assertArrayEquals(JAR_CONTENT, 
Files.readAllBytes(installedJar));
+        Assertions.assertEquals(
+                EnumSet.of(
+                        PosixFilePermission.OWNER_READ,
+                        PosixFilePermission.OWNER_WRITE,
+                        PosixFilePermission.GROUP_READ,
+                        PosixFilePermission.OTHERS_READ),
+                Files.getPosixFilePermissions(installedJar));
+        assertNoTemporaryFiles(distribution.resolve("connectors"));
+    }
+
+    /**
+     * Verifies that an environment with only SHA-1 support requests the SHA-1 
sidecar directly,
+     * even when the repository would publish SHA-512.
+     */
+    @Test
+    public void testSha1OnlyEnvironmentUsesSha1Checksum() throws Exception {
+        Path distribution = createDistribution("connector-fake");
+        Path fakeBinaryDirectory = createFakeCurl(sha1Hex(JAR_CONTENT), true, 
200);
+        createIsolatedSha1Tools(fakeBinaryDirectory, sha1Hex(JAR_CONTENT));
+
+        int exitCode = runInstaller(distribution, fakeBinaryDirectory, 
"2.3.13", null, true);
+
+        Assertions.assertEquals(0, exitCode);
+        Path installedJar = 
distribution.resolve("connectors").resolve("connector-fake-2.3.13.jar");
+        Assertions.assertArrayEquals(JAR_CONTENT, 
Files.readAllBytes(installedJar));
+        assertNoTemporaryFiles(distribution.resolve("connectors"));
+    }
+
+    /**
+     * Verifies that the preferred SHA-512 checksum path installs a valid 
release artifact without
+     * falling back to SHA-1.
+     */
+    @Test
+    public void testReleaseDownloadWithSha512Checksum() throws Exception {
+        Path distribution = createDistribution("connector-fake");
+        Path fakeBinaryDirectory = createFakeCurl(sha512Hex(JAR_CONTENT), 
true, 200);
+
+        int exitCode = runInstaller(distribution, fakeBinaryDirectory, 
"2.3.13", null);
+
+        Assertions.assertEquals(0, exitCode);
+        Path installedJar = 
distribution.resolve("connectors").resolve("connector-fake-2.3.13.jar");
+        Assertions.assertArrayEquals(JAR_CONTENT, 
Files.readAllBytes(installedJar));
+        assertNoTemporaryFiles(distribution.resolve("connectors"));
+    }
+
+    /**
+     * Verifies that a checksum mismatch leaves an existing connector 
untouched and cleans staging
+     * files.
+     */
+    @Test
+    public void testChecksumFailurePreservesExistingConnector() throws 
Exception {
+        Path distribution = createDistribution("connector-fake");
+        Path connectorsDirectory = distribution.resolve("connectors");
+        Files.createDirectories(connectorsDirectory);
+        Path installedJar = 
connectorsDirectory.resolve("connector-fake-2.3.13.jar");
+        byte[] existingContent = 
"existing-connector".getBytes(StandardCharsets.UTF_8);
+        Files.write(installedJar, existingContent);
+        Path fakeBinaryDirectory = createFakeCurl(repeat("0", 40), false, 404);
+
+        int exitCode = runInstaller(distribution, fakeBinaryDirectory, 
"2.3.13", null);
+
+        Assertions.assertNotEquals(0, exitCode);
+        Assertions.assertArrayEquals(existingContent, 
Files.readAllBytes(installedJar));
+        assertNoTemporaryFiles(connectorsDirectory);
+    }
+
+    /**
+     * Verifies that a SHA-512 server error fails closed instead of silently 
falling back to SHA-1.
+     */
+    @Test
+    public void testSha512ServerFailureDoesNotFallbackToSha1() throws 
Exception {
+        Path distribution = createDistribution("connector-fake");
+        Path fakeBinaryDirectory = createFakeCurl(sha1Hex(JAR_CONTENT), false, 
500);
+
+        int exitCode = runInstaller(distribution, fakeBinaryDirectory, 
"2.3.13", null);
+
+        Assertions.assertNotEquals(0, exitCode);
+        Assertions.assertFalse(
+                Files.exists(
+                        
distribution.resolve("connectors").resolve("connector-fake-2.3.13.jar")));
+        assertNoTemporaryFiles(distribution.resolve("connectors"));
+    }
+
+    /**
+     * Verifies that an HTTP failure cannot replace an existing connector and 
leaves no staging
+     * files behind.
+     */
+    @Test
+    public void testDownloadFailurePreservesExistingConnector() throws 
Exception {
+        Path distribution = createDistribution("connector-fake");
+        Path connectorsDirectory = distribution.resolve("connectors");
+        Files.createDirectories(connectorsDirectory);
+        Path installedJar = 
connectorsDirectory.resolve("connector-fake-2.3.13.jar");
+        byte[] existingContent = 
"existing-connector".getBytes(StandardCharsets.UTF_8);
+        Files.write(installedJar, existingContent);
+        Path fakeBinaryDirectory = createFailingCurl();
+
+        int exitCode = runInstaller(distribution, fakeBinaryDirectory, 
"2.3.13", null);
+
+        Assertions.assertNotEquals(0, exitCode);
+        Assertions.assertArrayEquals(existingContent, 
Files.readAllBytes(installedJar));
+        assertNoTemporaryFiles(connectorsDirectory);
+    }
+
+    /**
+     * Verifies that a checksum-matching response is still rejected when it 
does not have a JAR
+     * archive header.
+     */
+    @Test
+    public void testNonJarPayloadIsRejected() throws Exception {
+        byte[] nonJarContent = "not-a-jar".getBytes(StandardCharsets.UTF_8);
+        Path distribution = createDistribution("connector-fake");
+        Path fakeBinaryDirectory =
+                createFakeCurl(sha1Hex(nonJarContent), false, 404, "printf 
'not-a-jar'");
+
+        int exitCode = runInstaller(distribution, fakeBinaryDirectory, 
"2.3.13", null);
+
+        Assertions.assertNotEquals(0, exitCode);
+        Assertions.assertFalse(
+                Files.exists(
+                        
distribution.resolve("connectors").resolve("connector-fake-2.3.13.jar")));
+        assertNoTemporaryFiles(distribution.resolve("connectors"));
+    }
+
+    /**
+     * Verifies that snapshot versions retain Maven metadata resolution 
instead of using a direct
+     * release URL.
+     */
+    @Test
+    public void testSnapshotUsesMavenFallback() throws Exception {
+        Path distribution = createDistribution("connector-fake");
+        Path fakeBinaryDirectory = createFakeCurl(repeat("0", 40), false, 404);
+        Path capturedArguments = 
temporaryDirectory.resolve("maven-arguments.txt");
+        createFakeMavenWrapper(distribution, capturedArguments);
+
+        int exitCode =
+                runInstaller(
+                        distribution, fakeBinaryDirectory, "3.0.0-SNAPSHOT", 
capturedArguments);
+
+        Assertions.assertEquals(0, exitCode);
+        String arguments =
+                new String(Files.readAllBytes(capturedArguments), 
StandardCharsets.UTF_8);
+        Assertions.assertTrue(arguments.contains("-Dversion=3.0.0-SNAPSHOT"));
+        
Assertions.assertTrue(arguments.contains("-DartifactId=connector-fake"));
+    }
+
+    /**
+     * Verifies that a fixed release can explicitly retain Maven resolution 
behavior for existing
+     * mirror, proxy, and authentication policies.
+     */
+    @Test
+    public void testReleaseUsesExplicitMavenFallback() throws Exception {
+        Path distribution = createDistribution("connector-fake");
+        Path fakeBinaryDirectory = createFakeCurl(repeat("0", 40), false, 404);
+        Path capturedArguments = 
temporaryDirectory.resolve("release-maven-arguments.txt");
+        createFakeMavenWrapper(distribution, capturedArguments);
+
+        int exitCode =
+                runInstaller(
+                        distribution,
+                        fakeBinaryDirectory,
+                        "2.3.13",
+                        capturedArguments,
+                        false,
+                        "maven",
+                        "https://repo.example.test/maven2";);
+
+        Assertions.assertEquals(0, exitCode);
+        String arguments =
+                new String(Files.readAllBytes(capturedArguments), 
StandardCharsets.UTF_8);
+        Assertions.assertTrue(arguments.contains("-Dversion=2.3.13"));
+        
Assertions.assertTrue(arguments.contains("-DartifactId=connector-fake"));
+    }
+
+    /**
+     * Verifies that unsafe release version text is rejected before a download 
is attempted or a
+     * connectors directory is created.
+     */
+    @Test
+    public void testInvalidReleaseVersionIsRejected() throws Exception {
+        Path distribution = createDistribution("connector-fake");
+        Path fakeBinaryDirectory = createFakeCurl(sha1Hex(JAR_CONTENT), false, 
404);
+
+        int exitCode = runInstaller(distribution, fakeBinaryDirectory, 
"../2.3.13", null);
+
+        Assertions.assertNotEquals(0, exitCode);
+        
Assertions.assertFalse(Files.exists(distribution.resolve("connectors")));
+    }
+
+    /**
+     * Verifies that an unsafe connector artifact ID is rejected before any 
artifact download is
+     * attempted.
+     */
+    @Test
+    public void testInvalidArtifactIdIsRejected() throws Exception {
+        Path distribution = createDistribution("../connector-fake");
+        Path fakeBinaryDirectory = createFakeCurl(sha1Hex(JAR_CONTENT), false, 
404);
+
+        int exitCode = runInstaller(distribution, fakeBinaryDirectory, 
"2.3.13", null);
+
+        Assertions.assertNotEquals(0, exitCode);
+        assertNoTemporaryFiles(distribution.resolve("connectors"));
+    }
+
+    /**
+     * Verifies that the direct-download repository cannot use plaintext HTTP 
as its base protocol.
+     */
+    @Test
+    public void testHttpRepositoryIsRejected() throws Exception {
+        Path distribution = createDistribution("connector-fake");
+        Path fakeBinaryDirectory = createFakeCurl(sha1Hex(JAR_CONTENT), false, 
404);
+
+        int exitCode =
+                runInstaller(
+                        distribution,
+                        fakeBinaryDirectory,
+                        "2.3.13",
+                        null,
+                        false,
+                        null,
+                        "http://repo.example.test/maven2";);
+
+        Assertions.assertNotEquals(0, exitCode);
+        
Assertions.assertFalse(Files.exists(distribution.resolve("connectors")));
+    }
+
+    /**
+     * Creates a minimal SeaTunnel distribution containing the installer and 
selected plugin
+     * configuration.
+     *
+     * @param pluginConfig plugin artifact ID written to the installer 
configuration
+     * @return path to the minimal distribution
+     */
+    private Path createDistribution(String pluginConfig) throws Exception {
+        Path distribution = temporaryDirectory.resolve("SeaTunnel home with 
spaces");
+        Path binDirectory = distribution.resolve("bin");
+        Path configDirectory = distribution.resolve("config");
+        Files.createDirectories(binDirectory);
+        Files.createDirectories(configDirectory);
+        Files.copy(locateFile("bin/install-plugin.sh"), 
binDirectory.resolve("install-plugin.sh"));
+        Assertions.assertTrue(
+                
binDirectory.resolve("install-plugin.sh").toFile().setExecutable(true));
+        Files.write(
+                configDirectory.resolve("plugin_config"),
+                (pluginConfig + 
System.lineSeparator()).getBytes(StandardCharsets.UTF_8));
+        return distribution;
+    }
+
+    /**
+     * Creates a deterministic curl replacement that serves a JAR fixture and 
the requested checksum
+     * responses.
+     *
+     * @param checksum checksum response returned by the fake repository
+     * @param sha512Available whether the fake repository publishes SHA-512
+     * @param sha512Status HTTP status returned for the SHA-512 request
+     * @return directory containing the fake curl executable
+     */
+    private Path createFakeCurl(String checksum, boolean sha512Available, int 
sha512Status)
+            throws Exception {
+        return createFakeCurl(
+                checksum, sha512Available, sha512Status, "printf 
'\\120\\113\\003\\004fixture'");
+    }
+
+    /**
+     * Creates a deterministic curl replacement with a configurable artifact 
response.
+     *
+     * @param checksum checksum response returned by the fake repository
+     * @param sha512Available whether the fake repository publishes SHA-512
+     * @param sha512Status HTTP status returned for the SHA-512 request
+     * @param artifactCommand shell command that writes artifact bytes to 
standard output
+     * @return directory containing the fake curl executable
+     */
+    private Path createFakeCurl(
+            String checksum, boolean sha512Available, int sha512Status, String 
artifactCommand)
+            throws Exception {
+        Path fakeBinaryDirectory =
+                temporaryDirectory.resolve("fake-bin-" + checksum.substring(0, 
8));
+        Files.createDirectories(fakeBinaryDirectory);
+        Path fakeCurl = fakeBinaryDirectory.resolve("curl");
+        String script =
+                "#!/bin/sh\n"
+                        + "output=\n"
+                        + "url=\n"
+                        + "secure_proto=false\n"
+                        + "secure_redirect=false\n"
+                        + "while [ \"$#\" -gt 0 ]; do\n"
+                        + "  case \"$1\" in\n"
+                        + "    --output) output=$2; shift 2 ;;\n"
+                        + "    --proto) [ \"$2\" = '=https' ] && 
secure_proto=true; shift 2 ;;\n"
+                        + "    --proto-redir) [ \"$2\" = '=https' ] && 
secure_redirect=true;"
+                        + " shift 2 ;;\n"
+                        + "    --retry|--connect-timeout|--write-out)"
+                        + " shift 2 ;;\n"
+                        + "    --fail|--location) shift ;;\n"
+                        + "    *) url=$1; shift ;;\n"
+                        + "  esac\n"
+                        + "done\n"
+                        + "[ \"$secure_proto\" = true ] || exit 90\n"
+                        + "[ \"$secure_redirect\" = true ] || exit 91\n"
+                        + "case \"$url\" in\n"
+                        + "  *.jar.sha512)\n"
+                        + (sha512Available
+                                ? "    printf '%s\\n' '" + checksum + "' > 
\"$output\"\n"
+                                : "    printf '%s' '" + sha512Status + "'; 
exit 22\n")
+                        + "    ;;\n"
+                        + "  *.jar.sha1) printf '%s\\n' '"
+                        + checksum
+                        + "' > \"$output\" ;;\n"
+                        + "  *.jar) "
+                        + artifactCommand
+                        + " > \"$output\" ;;\n"
+                        + "  *) exit 22 ;;\n"
+                        + "esac\n"
+                        + "printf '%s' '200'\n";
+        Files.write(fakeCurl, script.getBytes(StandardCharsets.UTF_8));
+        Assertions.assertTrue(fakeCurl.toFile().setExecutable(true));
+        return fakeBinaryDirectory;
+    }
+
+    /**
+     * Creates a curl replacement that simulates an HTTP failure for every 
request.
+     *
+     * @return directory containing the failing curl executable
+     */
+    private Path createFailingCurl() throws Exception {
+        Path fakeBinaryDirectory = 
temporaryDirectory.resolve("failing-curl-bin");
+        Files.createDirectories(fakeBinaryDirectory);
+        Path fakeCurl = fakeBinaryDirectory.resolve("curl");
+        Files.write(fakeCurl, "#!/bin/sh\nexit 
22\n".getBytes(StandardCharsets.UTF_8));
+        Assertions.assertTrue(fakeCurl.toFile().setExecutable(true));
+        return fakeBinaryDirectory;
+    }
+
+    /**
+     * Adds only a SHA-1 implementation and the POSIX utilities needed by the 
installer to an
+     * isolated PATH.
+     *
+     * @param fakeBinaryDirectory isolated executable directory
+     * @param checksum checksum returned by the fake SHA-1 implementation
+     */
+    private void createIsolatedSha1Tools(Path fakeBinaryDirectory, String 
checksum)
+            throws Exception {
+        for (String tool :
+                new String[] {
+                    "awk", "cut", "dirname", "mkdir", "mktemp", "mv", "od", 
"rm", "rmdir", "tr"
+                }) {
+            Path source = locateSystemTool(tool);
+            Files.createSymbolicLink(fakeBinaryDirectory.resolve(tool), 
source);
+        }
+
+        Path fakeSha1sum = fakeBinaryDirectory.resolve("sha1sum");
+        String script = "#!/bin/sh\n" + "printf '%s  %s\\n' '" + checksum + "' 
\"$1\"\n";
+        Files.write(fakeSha1sum, script.getBytes(StandardCharsets.UTF_8));
+        Assertions.assertTrue(fakeSha1sum.toFile().setExecutable(true));
+    }
+
+    /**
+     * Locates a standard POSIX utility for an isolated test PATH.
+     *
+     * @param tool executable name
+     * @return absolute executable path
+     */
+    private Path locateSystemTool(String tool) {
+        for (String directory : new String[] {"/usr/bin", "/bin"}) {
+            Path candidate = Paths.get(directory, tool);
+            if (Files.isExecutable(candidate)) {
+                return candidate;
+            }
+        }
+        throw new IllegalStateException("Required POSIX test utility is 
unavailable: " + tool);
+    }
+
+    /**
+     * Creates a Maven wrapper replacement that records dependency resolution 
arguments.
+     *
+     * @param distribution minimal SeaTunnel distribution
+     * @param capturedArguments file that receives Maven arguments
+     */
+    private void createFakeMavenWrapper(Path distribution, Path 
capturedArguments)
+            throws Exception {
+        Path fakeMavenWrapper = distribution.resolve("mvnw");
+        String script = "#!/bin/sh\n" + "printf '%s\\n' \"$@\" > \"" + 
capturedArguments + "\"\n";
+        Files.write(fakeMavenWrapper, script.getBytes(StandardCharsets.UTF_8));
+        Assertions.assertTrue(fakeMavenWrapper.toFile().setExecutable(true));
+    }
+
+    /**
+     * Runs the installer with the fake download tools and a fixed HTTPS 
repository.
+     *
+     * @param distribution minimal SeaTunnel distribution
+     * @param fakeBinaryDirectory directory prepended to PATH
+     * @param version connector version passed to the installer
+     * @param capturedArguments optional Maven argument capture file
+     * @return installer process exit code
+     */
+    private int runInstaller(
+            Path distribution, Path fakeBinaryDirectory, String version, Path 
capturedArguments)
+            throws Exception {
+        return runInstaller(distribution, fakeBinaryDirectory, version, 
capturedArguments, false);
+    }
+
+    /**
+     * Runs the installer with optional isolation from host checksum 
executables.
+     *
+     * @param distribution minimal SeaTunnel distribution
+     * @param fakeBinaryDirectory directory used as PATH
+     * @param version connector version passed to the installer
+     * @param capturedArguments optional Maven argument capture file
+     * @param isolatePath whether PATH should exclude host checksum executables
+     * @return installer process exit code
+     */
+    private int runInstaller(
+            Path distribution,
+            Path fakeBinaryDirectory,
+            String version,
+            Path capturedArguments,
+            boolean isolatePath)
+            throws Exception {
+        return runInstaller(
+                distribution,
+                fakeBinaryDirectory,
+                version,
+                capturedArguments,
+                isolatePath,
+                null,
+                "https://repo.example.test/maven2";);
+    }
+
+    /**
+     * Runs the installer with explicit method and repository environment 
values.
+     *
+     * @param distribution minimal SeaTunnel distribution
+     * @param fakeBinaryDirectory directory used as PATH
+     * @param version connector version passed to the installer
+     * @param capturedArguments optional Maven argument capture file
+     * @param isolatePath whether PATH should exclude host checksum executables
+     * @param downloadMethod optional installer download method
+     * @param mavenRepository repository base URL
+     * @return installer process exit code
+     */
+    private int runInstaller(
+            Path distribution,
+            Path fakeBinaryDirectory,
+            String version,
+            Path capturedArguments,
+            boolean isolatePath,
+            String downloadMethod,
+            String mavenRepository)
+            throws Exception {
+        Path output = temporaryDirectory.resolve("installer-output-" + 
version.hashCode() + ".txt");
+        ProcessBuilder processBuilder =
+                new ProcessBuilder(
+                        "/bin/sh",
+                        "-c",
+                        "umask 022; exec /bin/sh \"$1\" \"$2\"",
+                        "install-plugin-test",
+                        
distribution.resolve("bin").resolve("install-plugin.sh").toString(),
+                        version);
+        processBuilder.redirectErrorStream(true);
+        processBuilder.redirectOutput(output.toFile());
+        Map<String, String> environment = processBuilder.environment();
+        String path = fakeBinaryDirectory.toString();
+        if (!isolatePath) {
+            path += System.getProperty("path.separator") + 
environment.getOrDefault("PATH", "");
+        }
+        environment.put("PATH", path);
+        environment.put("SEATUNNEL_MAVEN_REPOSITORY", mavenRepository);
+        if (downloadMethod == null) {
+            environment.remove("SEATUNNEL_PLUGIN_DOWNLOAD_METHOD");
+        } else {
+            environment.put("SEATUNNEL_PLUGIN_DOWNLOAD_METHOD", 
downloadMethod);
+        }
+        if (capturedArguments != null) {
+            environment.put("CAPTURE_FILE", capturedArguments.toString());
+        }
+        Process process = processBuilder.start();
+        return process.waitFor();
+    }
+
+    /**
+     * Confirms that failed or completed installs leave no uniquely named 
staging files.
+     *
+     * @param connectorsDirectory connector installation directory
+     */
+    private void assertNoTemporaryFiles(Path connectorsDirectory) throws 
Exception {
+        try (Stream<Path> files = Files.list(connectorsDirectory)) {
+            Assertions.assertFalse(
+                    files.anyMatch(
+                            path -> 
path.getFileName().toString().contains(".install-plugin.")));
+        }
+    }
+
+    /**
+     * Resolves a repository file when tests run from either the root project 
or seatunnel-dist.
+     *
+     * @param relativePath repository-relative file path
+     * @return resolved file path
+     */
+    private Path locateFile(String relativePath) {
+        Path rootPath = Paths.get(relativePath);
+        if (Files.exists(rootPath)) {
+            return rootPath;
+        }
+        return Paths.get("..").resolve(relativePath);
+    }
+
+    /**
+     * Calculates the lowercase SHA-1 digest used by the fake Maven repository.
+     *
+     * @param content bytes to hash
+     * @return lowercase hexadecimal digest
+     */
+    private String sha1Hex(byte[] content) throws Exception {
+        return digestHex("SHA-1", content);
+    }
+
+    /**
+     * Calculates the lowercase SHA-512 digest used by the fake Maven 
repository.
+     *
+     * @param content bytes to hash
+     * @return lowercase hexadecimal digest
+     */
+    private String sha512Hex(byte[] content) throws Exception {
+        return digestHex("SHA-512", content);
+    }
+
+    /**
+     * Calculates a lowercase hexadecimal digest.
+     *
+     * @param algorithm message digest algorithm
+     * @param content bytes to hash
+     * @return lowercase hexadecimal digest
+     */
+    private String digestHex(String algorithm, byte[] content) throws 
Exception {
+        byte[] digest = MessageDigest.getInstance(algorithm).digest(content);
+        StringBuilder value = new StringBuilder(digest.length * 2);
+        for (byte element : digest) {
+            value.append(String.format("%02x", element & 0xff));
+        }
+        return value.toString();
+    }
+
+    /**
+     * Repeats a string to construct fixed-length invalid checksum fixtures.
+     *
+     * @param value value to repeat
+     * @param count number of repetitions
+     * @return repeated value
+     */
+    private String repeat(String value, int count) {
+        StringBuilder result = new StringBuilder(value.length() * count);
+        for (int index = 0; index < count; index++) {
+            result.append(value);
+        }
+        return result.toString();
+    }
+}

Reply via email to