diff --git a/README.md b/README.md
index 28438c76..97e851ea 100644
--- a/README.md
+++ b/README.md
@@ -118,7 +118,7 @@ make local.run
| 提供商 | 备注 |
| :-------------------------------------- | :------------------------------------------------------------------ |
| 本地部署 | 可部署到本地服务器 |
-| SSH 部署 | 可部署到远程服务器(通过 SSH+SFTP) |
+| SSH 部署 | 可部署到远程服务器(通过 SSH+SFTP/SCP) |
| Webhook 回调 | 可部署到 Webhook |
| [Kubernetes](https://kubernetes.io/) | 可部署到 Kubernetes Secret |
| [阿里云](https://www.aliyun.com/) | 可部署到阿里云 OSS、CDN、DCDN、SLB(CLB/ALB/NLB)、WAF、Live 等服务 |
diff --git a/README_EN.md b/README_EN.md
index 5b3e67ff..b5c71b3d 100644
--- a/README_EN.md
+++ b/README_EN.md
@@ -117,7 +117,7 @@ The following hosting providers are supported:
| Provider | Remarks |
| :---------------------------------------------- | :------------------------------------------------------------------------------- |
| Local | Supports deployment to local servers |
-| SSH | Supports deployment to remote servers (via SSH+SFTP) |
+| SSH | Supports deployment to remote servers (via SSH+SFTP/SCP) |
| Webhook | Supports deployment to Webhook |
| [Kubernetes](https://kubernetes.io/) | Supports deployment to Kubernetes Secret |
| [Alibaba Cloud](https://www.alibabacloud.com/) | Supports deployment to Alibaba Cloud OSS, CDN, DCDN, SLB(CLB/ALB/NLB), WAF, Live |
diff --git a/go.mod b/go.mod
index 3bb9f1d9..6cd082bf 100644
--- a/go.mod
+++ b/go.mod
@@ -29,6 +29,7 @@ require (
github.com/pkg/sftp v1.13.7
github.com/pocketbase/dbx v1.11.0
github.com/pocketbase/pocketbase v0.24.4
+ github.com/povsister/scp v0.0.0-20240802064259-28781e87b246
github.com/qiniu/go-sdk/v7 v7.25.2
github.com/tencentcloud/tencentcloud-sdk-go/tencentcloud/cdn v1.0.1084
github.com/tencentcloud/tencentcloud-sdk-go/tencentcloud/clb v1.0.1084
diff --git a/go.sum b/go.sum
index 2111474e..7dde5dc8 100644
--- a/go.sum
+++ b/go.sum
@@ -743,6 +743,8 @@ github.com/pocketbase/pocketbase v0.24.4 h1:kw/c23HccoxMV/19U9QlDcvNJgQ66vlUrxGQ
github.com/pocketbase/pocketbase v0.24.4/go.mod h1:EfXV/8RUY76jA6g1RPNHjOuW7wTd2bz0QlvAI/RU8YY=
github.com/posener/complete v1.1.1/go.mod h1:em0nMJCgc9GFtwrmVmEMR/ZL6WyhyjMBndrE9hABlRI=
github.com/posener/complete v1.2.3/go.mod h1:WZIdtGGp+qx0sLrYKtIRAruyNpv6hFCicSgv7Sy7s/s=
+github.com/povsister/scp v0.0.0-20240802064259-28781e87b246 h1:c4D8BPWLOxxdaxQLfLKQXH2YXY/E9yo3jrDSL54XrTw=
+github.com/povsister/scp v0.0.0-20240802064259-28781e87b246/go.mod h1:i1Au86ZXK0ZalQNyBp2njCcyhSCR/QP/AMfILip+zNI=
github.com/prometheus/client_golang v0.9.1/go.mod h1:7SWBe2y4D6OKWSNQJUaRYU/AaXPKyh/dDVn+NZz0KFw=
github.com/prometheus/client_golang v1.0.0/go.mod h1:db9x61etRT2tGnBNRi70OPL5FsnadC4Ky3P0J6CfImo=
github.com/prometheus/client_golang v1.4.0/go.mod h1:e9GMxYsXl05ICDXkRhurwBS4Q3OK1iX/F2sw+iXX5zU=
@@ -933,6 +935,7 @@ golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9/go.mod h1:LzIPMQfyMNhhGPh
golang.org/x/crypto v0.0.0-20201012173705-84dcc777aaee/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto=
golang.org/x/crypto v0.0.0-20201112155050-0c6587e931a9/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto=
golang.org/x/crypto v0.0.0-20210314154223-e6e6c4f2bb5b/go.mod h1:T9bdIzuCu7OtxOm1hfPfRQxPLYneinmdGuTeoZ9dtd4=
+golang.org/x/crypto v0.0.0-20210322153248-0c34fe9e7dc2/go.mod h1:T9bdIzuCu7OtxOm1hfPfRQxPLYneinmdGuTeoZ9dtd4=
golang.org/x/crypto v0.0.0-20210616213533-5ff15b29337e/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc=
golang.org/x/crypto v0.0.0-20210915214749-c084706c2272/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc=
golang.org/x/crypto v0.0.0-20210920023735-84f357641f63/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc=
diff --git a/internal/deployer/providers.go b/internal/deployer/providers.go
index 28e4108c..344c78e6 100644
--- a/internal/deployer/providers.go
+++ b/internal/deployer/providers.go
@@ -350,6 +350,7 @@ func createDeployer(options *deployerOptions) (deployer.Deployer, logger.Logger,
SshPassword: access.Password,
SshKey: access.Key,
SshKeyPassphrase: access.KeyPassphrase,
+ UseSCP: maps.GetValueAsBool(options.ProviderDeployConfig, "useSCP"),
PreCommand: maps.GetValueAsString(options.ProviderDeployConfig, "preCommand"),
PostCommand: maps.GetValueAsString(options.ProviderDeployConfig, "postCommand"),
OutputFormat: providerSSH.OutputFormatType(maps.GetValueOrDefaultAsString(options.ProviderDeployConfig, "format", string(providerSSH.OUTPUT_FORMAT_PEM))),
diff --git a/internal/pkg/core/deployer/providers/ssh/ssh.go b/internal/pkg/core/deployer/providers/ssh/ssh.go
index 4fffce74..20ea348a 100644
--- a/internal/pkg/core/deployer/providers/ssh/ssh.go
+++ b/internal/pkg/core/deployer/providers/ssh/ssh.go
@@ -10,6 +10,7 @@ import (
xerrors "github.com/pkg/errors"
"github.com/pkg/sftp"
+ "github.com/povsister/scp"
"golang.org/x/crypto/ssh"
"github.com/usual2970/certimate/internal/pkg/core/deployer"
@@ -32,6 +33,8 @@ type SshDeployerConfig struct {
SshKey string `json:"sshKey,omitempty"`
// SSH 登录私钥口令。
SshKeyPassphrase string `json:"sshKeyPassphrase,omitempty"`
+ // 是否回退使用 SCP。
+ UseSCP bool `json:"useSCP,omitempty"`
// 前置命令。
PreCommand string `json:"preCommand,omitempty"`
// 后置命令。
@@ -112,13 +115,13 @@ func (d *SshDeployer) Deploy(ctx context.Context, certPem string, privkeyPem str
// 上传证书和私钥文件
switch d.config.OutputFormat {
case OUTPUT_FORMAT_PEM:
- if err := writeSftpFileString(client, d.config.OutputCertPath, certPem); err != nil {
+ if err := writeFileString(client, d.config.UseSCP, d.config.OutputCertPath, certPem); err != nil {
return nil, xerrors.Wrap(err, "failed to upload certificate file")
}
d.logger.Logt("certificate file uploaded")
- if err := writeSftpFileString(client, d.config.OutputKeyPath, privkeyPem); err != nil {
+ if err := writeFileString(client, d.config.UseSCP, d.config.OutputKeyPath, privkeyPem); err != nil {
return nil, xerrors.Wrap(err, "failed to upload private key file")
}
@@ -132,7 +135,7 @@ func (d *SshDeployer) Deploy(ctx context.Context, certPem string, privkeyPem str
d.logger.Logt("certificate transformed to PFX")
- if err := writeSftpFile(client, d.config.OutputCertPath, pfxData); err != nil {
+ if err := writeFile(client, d.config.UseSCP, d.config.OutputCertPath, pfxData); err != nil {
return nil, xerrors.Wrap(err, "failed to upload certificate file")
}
@@ -146,7 +149,7 @@ func (d *SshDeployer) Deploy(ctx context.Context, certPem string, privkeyPem str
d.logger.Logt("certificate transformed to JKS")
- if err := writeSftpFile(client, d.config.OutputCertPath, jksData); err != nil {
+ if err := writeFile(client, d.config.UseSCP, d.config.OutputCertPath, jksData); err != nil {
return nil, xerrors.Wrap(err, "failed to upload certificate file")
}
@@ -223,11 +226,47 @@ func execSshCommand(sshCli *ssh.Client, command string) (string, string, error)
return stdoutBuf.String(), stderrBuf.String(), nil
}
-func writeSftpFileString(sshCli *ssh.Client, path string, content string) error {
- return writeSftpFile(sshCli, path, []byte(content))
+func writeFileString(sshCli *ssh.Client, useSCP bool, path string, content string) error {
+ if useSCP {
+ return writeFileStringWithSCP(sshCli, path, content)
+ }
+
+ return writeFileStringWithSFTP(sshCli, path, content)
}
-func writeSftpFile(sshCli *ssh.Client, path string, data []byte) error {
+func writeFile(sshCli *ssh.Client, useSCP bool, path string, data []byte) error {
+ if useSCP {
+ return writeFileWithSCP(sshCli, path, data)
+ }
+
+ return writeFileWithSFTP(sshCli, path, data)
+}
+
+func writeFileStringWithSCP(sshCli *ssh.Client, path string, content string) error {
+ return writeFileWithSCP(sshCli, path, []byte(content))
+}
+
+func writeFileWithSCP(sshCli *ssh.Client, path string, data []byte) error {
+ scpCli, err := scp.NewClientFromExistingSSH(sshCli, &scp.ClientOption{})
+ if err != nil {
+ return xerrors.Wrap(err, "failed to create scp client")
+ }
+ defer scpCli.Close()
+
+ reader := bytes.NewReader(data)
+ err = scpCli.CopyToRemote(reader, path, &scp.FileTransferOption{})
+ if err != nil {
+ return xerrors.Wrap(err, "failed to write to remote file")
+ }
+
+ return nil
+}
+
+func writeFileStringWithSFTP(sshCli *ssh.Client, path string, content string) error {
+ return writeFileWithSFTP(sshCli, path, []byte(content))
+}
+
+func writeFileWithSFTP(sshCli *ssh.Client, path string, data []byte) error {
sftpCli, err := sftp.NewClient(sshCli)
if err != nil {
return xerrors.Wrap(err, "failed to create sftp client")
diff --git a/ui/src/components/workflow/node/DeployNodeConfigFormSSHConfig.tsx b/ui/src/components/workflow/node/DeployNodeConfigFormSSHConfig.tsx
index 976ca079..1e176d7b 100644
--- a/ui/src/components/workflow/node/DeployNodeConfigFormSSHConfig.tsx
+++ b/ui/src/components/workflow/node/DeployNodeConfigFormSSHConfig.tsx
@@ -1,6 +1,6 @@
import { useTranslation } from "react-i18next";
import { DownOutlined as DownOutlinedIcon } from "@ant-design/icons";
-import { Button, Dropdown, Form, type FormInstance, Input, Select } from "antd";
+import { Button, Dropdown, Form, type FormInstance, Input, Select, Switch } from "antd";
import { createSchemaFieldRule } from "antd-zod";
import { z } from "zod";
@@ -17,6 +17,7 @@ type DeployNodeConfigFormSSHConfigFieldValues = Nullish<{
jksStorepass?: string | null;
preCommand?: string | null;
postCommand?: string | null;
+ useSCP?: boolean;
}>;
export type DeployNodeConfigFormSSHConfigProps = {
@@ -89,6 +90,7 @@ const DeployNodeConfigFormSSHConfig = ({ form: formInst, formName, disabled, ini
.string()
.max(20480, t("common.errmsg.string_max", { max: 20480 }))
.nullish(),
+ useSCP: z.boolean().nullish(),
});
const formRule = createSchemaFieldRule(formSchema);
@@ -261,6 +263,15 @@ const DeployNodeConfigFormSSHConfig = ({ form: formInst, formName, disabled, ini