diff --git a/internal/certificate/service.go b/internal/certificate/service.go index 3387c273..0ec9624c 100644 --- a/internal/certificate/service.go +++ b/internal/certificate/service.go @@ -59,7 +59,7 @@ func (s *CertificateService) DownloadArchivedFile(ctx context.Context, req *dtos var bytes []byte switch strings.ToUpper(req.CertificateFormat) { - case "", "PEM": + case "", string(domain.CertificateFormatTypePEM): { serverCertPEM, intermediaCertPEM, err := xcert.ExtractCertificatesFromPEM(certificate.Certificate) if err != nil { @@ -114,7 +114,7 @@ func (s *CertificateService) DownloadArchivedFile(ctx context.Context, req *dtos bytes = buf.Bytes() } - case "PFX": + case string(domain.CertificateFormatTypePFX): { const pfxPassword = "certimate" @@ -151,7 +151,7 @@ func (s *CertificateService) DownloadArchivedFile(ctx context.Context, req *dtos bytes = buf.Bytes() } - case "JKS": + case string(domain.CertificateFormatTypeJKS): { const jksPassword = "certimate" diff --git a/internal/certmgmt/deployers/sp_s3.go b/internal/certmgmt/deployers/sp_s3.go new file mode 100644 index 00000000..c83d0339 --- /dev/null +++ b/internal/certmgmt/deployers/sp_s3.go @@ -0,0 +1,40 @@ +package deployers + +import ( + "fmt" + + "github.com/certimate-go/certimate/internal/domain" + "github.com/certimate-go/certimate/pkg/core/deployer" + "github.com/certimate-go/certimate/pkg/core/deployer/providers/s3" + xmaps "github.com/certimate-go/certimate/pkg/utils/maps" +) + +func init() { + Registries.MustRegister(domain.DeploymentProviderTypeS3, func(options *ProviderFactoryOptions) (deployer.Provider, error) { + credentials := domain.AccessConfigForS3{} + if err := xmaps.Populate(options.ProviderAccessConfig, &credentials); err != nil { + return nil, fmt.Errorf("failed to populate provider access config: %w", err) + } + + provider, err := s3.NewDeployer(&s3.DeployerConfig{ + Endpoint: credentials.Endpoint, + AccessKey: credentials.AccessKey, + SecretKey: credentials.SecretKey, + SignatureVersion: credentials.SignatureVersion, + UsePathStyle: credentials.UsePathStyle, + AllowInsecureConnections: credentials.AllowInsecureConnections, + Region: xmaps.GetString(options.ProviderExtendedConfig, "region"), + Bucket: xmaps.GetString(options.ProviderExtendedConfig, "bucket"), + OutputFormat: xmaps.GetOrDefaultString(options.ProviderExtendedConfig, "format", s3.OUTPUT_FORMAT_PEM), + OutputCertObjectKey: xmaps.GetString(options.ProviderExtendedConfig, "certObjectKey"), + OutputServerCertObjectKey: xmaps.GetString(options.ProviderExtendedConfig, "certObjectKeyForServerOnly"), + OutputIntermediaCertObjectKey: xmaps.GetString(options.ProviderExtendedConfig, "certObjectKeyForIntermediaOnly"), + OutputKeyObjectKey: xmaps.GetString(options.ProviderExtendedConfig, "keyObjectKey"), + PfxPassword: xmaps.GetString(options.ProviderExtendedConfig, "pfxPassword"), + JksAlias: xmaps.GetString(options.ProviderExtendedConfig, "jksAlias"), + JksKeypass: xmaps.GetString(options.ProviderExtendedConfig, "jksKeypass"), + JksStorepass: xmaps.GetString(options.ProviderExtendedConfig, "jksStorepass"), + }) + return provider, err + }) +} diff --git a/internal/domain/certificate.go b/internal/domain/certificate.go index e4b1ed0b..f29d2e1a 100644 --- a/internal/domain/certificate.go +++ b/internal/domain/certificate.go @@ -112,3 +112,11 @@ func (t CertificateKeyAlgorithmType) KeyType() (certcrypto.KeyType, error) { return certcrypto.RSA2048, fmt.Errorf("unsupported key algorithm type: '%s'", t) } + +type CertificateFormatType string + +const ( + CertificateFormatTypePEM CertificateFormatType = "PEM" + CertificateFormatTypePFX CertificateFormatType = "PFX" + CertificateFormatTypeJKS CertificateFormatType = "JKS" +) diff --git a/internal/domain/provider.go b/internal/domain/provider.go index dd798e22..2d6d8043 100644 --- a/internal/domain/provider.go +++ b/internal/domain/provider.go @@ -329,6 +329,7 @@ const ( DeploymentProviderTypeRainYunRCDN = DeploymentProviderType(AccessProviderTypeRainYun + "-rcdn") DeploymentProviderTypeRatPanel = DeploymentProviderType(AccessProviderTypeRatPanel) DeploymentProviderTypeRatPanelConsole = DeploymentProviderType(AccessProviderTypeRatPanel + "-console") + DeploymentProviderTypeS3 = DeploymentProviderType(AccessProviderTypeS3) DeploymentProviderTypeSafeLine = DeploymentProviderType(AccessProviderTypeSafeLine) DeploymentProviderTypeSSH = DeploymentProviderType(AccessProviderTypeSSH) DeploymentProviderTypeSynologyDSM = DeploymentProviderType(AccessProviderTypeSynologyDSM) diff --git a/internal/tools/s3/client.go b/internal/tools/s3/client.go index 7054459b..25e06dc2 100644 --- a/internal/tools/s3/client.go +++ b/internal/tools/s3/client.go @@ -1,6 +1,7 @@ package s3 import ( + "bytes" "context" "errors" "fmt" @@ -87,11 +88,11 @@ func NewClient(config *Config) (*Client, error) { }, nil } -func (c *Client) PutObject(ctx context.Context, bucket, key string, reader io.Reader) error { +func (c *Client) PutObject(ctx context.Context, bucket, key string, reader io.Reader, size int64) error { putOpts := minio.PutObjectOptions{ DisableMultipart: true, } - _, err := c.client.PutObject(ctx, bucket, key, reader, 1024*1024*16, putOpts) + _, err := c.client.PutObject(ctx, bucket, key, reader, size, putOpts) if err != nil { return err } @@ -99,6 +100,16 @@ func (c *Client) PutObject(ctx context.Context, bucket, key string, reader io.Re return nil } +func (c *Client) PutObjectString(ctx context.Context, bucket, key string, data string) error { + reader := strings.NewReader(data) + return c.PutObject(ctx, bucket, key, reader, reader.Size()) +} + +func (c *Client) PutObjectBytes(ctx context.Context, bucket, key string, data []byte) error { + reader := bytes.NewReader(data) + return c.PutObject(ctx, bucket, key, reader, reader.Size()) +} + func (c *Client) RemoveObject(ctx context.Context, bucket, key string) error { removeOpts := minio.RemoveObjectOptions{} err := c.client.RemoveObject(ctx, bucket, key, removeOpts) diff --git a/pkg/core/certifier/challengers/http01/s3/s3.go b/pkg/core/certifier/challengers/http01/s3/s3.go index e022d394..79abd2fd 100644 --- a/pkg/core/certifier/challengers/http01/s3/s3.go +++ b/pkg/core/certifier/challengers/http01/s3/s3.go @@ -62,8 +62,7 @@ type provider struct { func (p *provider) Present(domain, token, keyAuth string) error { objectKey := strings.Trim(http01.ChallengePath(token), "/") - reader := strings.NewReader(keyAuth) - if err := p.client.PutObject(context.Background(), p.bucket, objectKey, reader); err != nil { + if err := p.client.PutObjectString(context.Background(), p.bucket, objectKey, keyAuth); err != nil { return fmt.Errorf("s3: failed to upload token to s3: %w", err) } diff --git a/pkg/core/deployer/providers/local/consts.go b/pkg/core/deployer/providers/local/consts.go index 01a05949..0d0ab86e 100644 --- a/pkg/core/deployer/providers/local/consts.go +++ b/pkg/core/deployer/providers/local/consts.go @@ -1,9 +1,7 @@ package local -const ( - OUTPUT_FORMAT_PEM = "PEM" - OUTPUT_FORMAT_PFX = "PFX" - OUTPUT_FORMAT_JKS = "JKS" +import ( + "github.com/certimate-go/certimate/internal/domain" ) const ( @@ -11,3 +9,9 @@ const ( SHELL_ENV_CMD = "cmd" SHELL_ENV_POWERSHELL = "powershell" ) + +const ( + OUTPUT_FORMAT_PEM = string(domain.CertificateFormatTypePEM) + OUTPUT_FORMAT_PFX = string(domain.CertificateFormatTypePFX) + OUTPUT_FORMAT_JKS = string(domain.CertificateFormatTypeJKS) +) diff --git a/pkg/core/deployer/providers/local/local.go b/pkg/core/deployer/providers/local/local.go index f25ab21c..0ce64535 100644 --- a/pkg/core/deployer/providers/local/local.go +++ b/pkg/core/deployer/providers/local/local.go @@ -104,53 +104,59 @@ func (d *Deployer) Deploy(ctx context.Context, certPEM, privkeyPEM string) (*dep // 写入证书和私钥文件 switch d.config.OutputFormat { case OUTPUT_FORMAT_PEM: - if err := xfile.WriteString(d.config.OutputCertPath, certPEM); err != nil { - return nil, fmt.Errorf("failed to save certificate file: %w", err) - } - d.logger.Info("ssl certificate file saved", slog.String("path", d.config.OutputCertPath)) - - if d.config.OutputServerCertPath != "" { - if err := xfile.WriteString(d.config.OutputServerCertPath, serverCertPEM); err != nil { - return nil, fmt.Errorf("failed to save server certificate file: %w", err) + { + if err := xfile.WriteString(d.config.OutputCertPath, certPEM); err != nil { + return nil, fmt.Errorf("failed to save certificate file: %w", err) } - d.logger.Info("ssl server certificate file saved", slog.String("path", d.config.OutputServerCertPath)) - } + d.logger.Info("ssl certificate file saved", slog.String("path", d.config.OutputCertPath)) - if d.config.OutputIntermediaCertPath != "" { - if err := xfile.WriteString(d.config.OutputIntermediaCertPath, intermediaCertPEM); err != nil { - return nil, fmt.Errorf("failed to save intermedia certificate file: %w", err) + if d.config.OutputServerCertPath != "" { + if err := xfile.WriteString(d.config.OutputServerCertPath, serverCertPEM); err != nil { + return nil, fmt.Errorf("failed to save server certificate file: %w", err) + } + d.logger.Info("ssl server certificate file saved", slog.String("path", d.config.OutputServerCertPath)) } - d.logger.Info("ssl intermedia certificate file saved", slog.String("path", d.config.OutputIntermediaCertPath)) - } - if err := xfile.WriteString(d.config.OutputKeyPath, privkeyPEM); err != nil { - return nil, fmt.Errorf("failed to save private key file: %w", err) + if d.config.OutputIntermediaCertPath != "" { + if err := xfile.WriteString(d.config.OutputIntermediaCertPath, intermediaCertPEM); err != nil { + return nil, fmt.Errorf("failed to save intermedia certificate file: %w", err) + } + d.logger.Info("ssl intermedia certificate file saved", slog.String("path", d.config.OutputIntermediaCertPath)) + } + + if err := xfile.WriteString(d.config.OutputKeyPath, privkeyPEM); err != nil { + return nil, fmt.Errorf("failed to save private key file: %w", err) + } + d.logger.Info("ssl private key file saved", slog.String("path", d.config.OutputKeyPath)) } - d.logger.Info("ssl private key file saved", slog.String("path", d.config.OutputKeyPath)) case OUTPUT_FORMAT_PFX: - pfxData, err := xcert.TransformCertificateFromPEMToPFX(certPEM, privkeyPEM, d.config.PfxPassword) - if err != nil { - return nil, fmt.Errorf("failed to transform certificate to PFX: %w", err) - } - d.logger.Info("ssl certificate transformed to pfx") + { + pfxData, err := xcert.TransformCertificateFromPEMToPFX(certPEM, privkeyPEM, d.config.PfxPassword) + if err != nil { + return nil, fmt.Errorf("failed to transform certificate to PFX: %w", err) + } + d.logger.Info("ssl certificate transformed to pfx") - if err := xfile.Write(d.config.OutputCertPath, pfxData); err != nil { - return nil, fmt.Errorf("failed to save certificate file: %w", err) + if err := xfile.Write(d.config.OutputCertPath, pfxData); err != nil { + return nil, fmt.Errorf("failed to save certificate file: %w", err) + } + d.logger.Info("ssl certificate file saved", slog.String("path", d.config.OutputCertPath)) } - d.logger.Info("ssl certificate file saved", slog.String("path", d.config.OutputCertPath)) case OUTPUT_FORMAT_JKS: - jksData, err := xcert.TransformCertificateFromPEMToJKS(certPEM, privkeyPEM, d.config.JksAlias, d.config.JksKeypass, d.config.JksStorepass) - if err != nil { - return nil, fmt.Errorf("failed to transform certificate to JKS: %w", err) - } - d.logger.Info("ssl certificate transformed to jks") + { + jksData, err := xcert.TransformCertificateFromPEMToJKS(certPEM, privkeyPEM, d.config.JksAlias, d.config.JksKeypass, d.config.JksStorepass) + if err != nil { + return nil, fmt.Errorf("failed to transform certificate to JKS: %w", err) + } + d.logger.Info("ssl certificate transformed to jks") - if err := xfile.Write(d.config.OutputCertPath, jksData); err != nil { - return nil, fmt.Errorf("failed to save certificate file: %w", err) + if err := xfile.Write(d.config.OutputCertPath, jksData); err != nil { + return nil, fmt.Errorf("failed to save certificate file: %w", err) + } + d.logger.Info("ssl certificate file saved", slog.String("path", d.config.OutputCertPath)) } - d.logger.Info("ssl certificate file saved", slog.String("path", d.config.OutputCertPath)) default: return nil, fmt.Errorf("unsupported output format '%s'", d.config.OutputFormat) diff --git a/pkg/core/deployer/providers/s3/consts.go b/pkg/core/deployer/providers/s3/consts.go new file mode 100644 index 00000000..4caf33f9 --- /dev/null +++ b/pkg/core/deployer/providers/s3/consts.go @@ -0,0 +1,11 @@ +package s3 + +import ( + "github.com/certimate-go/certimate/internal/domain" +) + +const ( + OUTPUT_FORMAT_PEM = string(domain.CertificateFormatTypePEM) + OUTPUT_FORMAT_PFX = string(domain.CertificateFormatTypePFX) + OUTPUT_FORMAT_JKS = string(domain.CertificateFormatTypeJKS) +) diff --git a/pkg/core/deployer/providers/s3/s3.go b/pkg/core/deployer/providers/s3/s3.go new file mode 100644 index 00000000..1760f8ce --- /dev/null +++ b/pkg/core/deployer/providers/s3/s3.go @@ -0,0 +1,169 @@ +package s3 + +import ( + "context" + "errors" + "fmt" + "log/slog" + + "github.com/certimate-go/certimate/internal/tools/s3" + "github.com/certimate-go/certimate/pkg/core/deployer" + xcert "github.com/certimate-go/certimate/pkg/utils/cert" +) + +type DeployerConfig struct { + // S3 Endpoint。 + Endpoint string `json:"endpoint"` + // S3 AccessKey。 + AccessKey string `json:"accessKey"` + // S3 SecretKey。 + SecretKey string `json:"secretKey"` + // S3 签名版本。 + // 可取值 "v2"、"v4"。 + // 零值时默认值 "v4"。 + SignatureVersion string `json:"signatureVersion,omitempty"` + // 是否使用路径风格。 + UsePathStyle bool `json:"usePathStyle,omitempty"` + // 存储区域。 + Region string `json:"region"` + // 存储桶名。 + Bucket string `json:"bucket"` + // 是否允许不安全的连接。 + AllowInsecureConnections bool `json:"allowInsecureConnections,omitempty"` + // 输出证书格式。 + OutputFormat string `json:"outputFormat,omitempty"` + // 输出证书文件路径。 + OutputCertObjectKey string `json:"outputCertObjectKey,omitempty"` + // 输出服务器证书文件路径。 + // 选填。 + OutputServerCertObjectKey string `json:"outputServerCertObjectKey,omitempty"` + // 输出中间证书文件路径。 + // 选填。 + OutputIntermediaCertObjectKey string `json:"outputIntermediaCertObjectKey,omitempty"` + // 输出私钥文件路径。 + OutputKeyObjectKey string `json:"outputKeyObjectKey,omitempty"` + // PFX 导出密码。 + // 证书格式为 PFX 时必填。 + PfxPassword string `json:"pfxPassword,omitempty"` + // JKS 别名。 + // 证书格式为 JKS 时必填。 + JksAlias string `json:"jksAlias,omitempty"` + // JKS 密钥密码。 + // 证书格式为 JKS 时必填。 + JksKeypass string `json:"jksKeypass,omitempty"` + // JKS 存储密码。 + // 证书格式为 JKS 时必填。 + JksStorepass string `json:"jksStorepass,omitempty"` +} + +type Deployer struct { + config *DeployerConfig + logger *slog.Logger + s3Client *s3.Client +} + +var _ deployer.Provider = (*Deployer)(nil) + +func NewDeployer(config *DeployerConfig) (*Deployer, error) { + if config == nil { + return nil, errors.New("the configuration of the deployer provider is nil") + } + + client, err := s3.NewClient(&s3.Config{ + Endpoint: config.Endpoint, + AccessKey: config.AccessKey, + SecretKey: config.SecretKey, + SignatureVersion: config.SignatureVersion, + UsePathStyle: config.UsePathStyle, + Region: config.Region, + SkipTlsVerify: config.AllowInsecureConnections, + }) + if err != nil { + return nil, fmt.Errorf("s3: failed to create s3 client: %w", err) + } + + return &Deployer{ + config: config, + logger: slog.Default(), + s3Client: client, + }, nil +} + +func (d *Deployer) SetLogger(logger *slog.Logger) { + if logger == nil { + d.logger = slog.New(slog.DiscardHandler) + } else { + d.logger = logger + } +} + +func (d *Deployer) Deploy(ctx context.Context, certPEM, privkeyPEM string) (*deployer.DeployResult, error) { + // 提取服务器证书和中间证书 + serverCertPEM, intermediaCertPEM, err := xcert.ExtractCertificatesFromPEM(certPEM) + if err != nil { + return nil, fmt.Errorf("failed to extract certs: %w", err) + } + + // 写入证书和私钥文件 + switch d.config.OutputFormat { + case OUTPUT_FORMAT_PEM: + { + if err := d.s3Client.PutObjectString(ctx, d.config.Bucket, d.config.OutputCertObjectKey, certPEM); err != nil { + return nil, fmt.Errorf("failed to upload certificate file: %w", err) + } + d.logger.Info("ssl certificate file uploaded", slog.String("bucket", d.config.Bucket), slog.String("object", d.config.OutputCertObjectKey)) + + if d.config.OutputServerCertObjectKey != "" { + if err := d.s3Client.PutObjectString(ctx, d.config.Bucket, d.config.OutputServerCertObjectKey, serverCertPEM); err != nil { + return nil, fmt.Errorf("failed to upload server certificate file: %w", err) + } + d.logger.Info("ssl server certificate file uploaded", slog.String("bucket", d.config.Bucket), slog.String("object", d.config.OutputServerCertObjectKey)) + } + + if d.config.OutputIntermediaCertObjectKey != "" { + if err := d.s3Client.PutObjectString(ctx, d.config.Bucket, d.config.OutputIntermediaCertObjectKey, intermediaCertPEM); err != nil { + return nil, fmt.Errorf("failed to upload intermedia certificate file: %w", err) + } + d.logger.Info("ssl intermedia certificate file uploaded", slog.String("bucket", d.config.Bucket), slog.String("object", d.config.OutputIntermediaCertObjectKey)) + } + + if err := d.s3Client.PutObjectString(ctx, d.config.Bucket, d.config.OutputKeyObjectKey, privkeyPEM); err != nil { + return nil, fmt.Errorf("failed to upload private key file: %w", err) + } + d.logger.Info("ssl private key file uploaded", slog.String("bucket", d.config.Bucket), slog.String("object", d.config.OutputKeyObjectKey)) + } + + case OUTPUT_FORMAT_PFX: + { + pfxData, err := xcert.TransformCertificateFromPEMToPFX(certPEM, privkeyPEM, d.config.PfxPassword) + if err != nil { + return nil, fmt.Errorf("failed to transform certificate to PFX: %w", err) + } + d.logger.Info("ssl certificate transformed to pfx") + + if err := d.s3Client.PutObjectBytes(ctx, d.config.Bucket, d.config.OutputCertObjectKey, pfxData); err != nil { + return nil, fmt.Errorf("failed to upload certificate file: %w", err) + } + d.logger.Info("ssl certificate file uploaded", slog.String("bucket", d.config.Bucket), slog.String("object", d.config.OutputCertObjectKey)) + } + + case OUTPUT_FORMAT_JKS: + { + jksData, err := xcert.TransformCertificateFromPEMToJKS(certPEM, privkeyPEM, d.config.JksAlias, d.config.JksKeypass, d.config.JksStorepass) + if err != nil { + return nil, fmt.Errorf("failed to transform certificate to JKS: %w", err) + } + d.logger.Info("ssl certificate transformed to jks") + + if err := d.s3Client.PutObjectBytes(ctx, d.config.Bucket, d.config.OutputCertObjectKey, jksData); err != nil { + return nil, fmt.Errorf("failed to upload certificate file: %w", err) + } + d.logger.Info("ssl certificate file uploaded", slog.String("bucket", d.config.Bucket), slog.String("object", d.config.OutputCertObjectKey)) + } + + default: + return nil, fmt.Errorf("unsupported output format '%s'", d.config.OutputFormat) + } + + return &deployer.DeployResult{}, nil +} diff --git a/pkg/core/deployer/providers/ssh/consts.go b/pkg/core/deployer/providers/ssh/consts.go index ce43ebd3..0022a0fb 100644 --- a/pkg/core/deployer/providers/ssh/consts.go +++ b/pkg/core/deployer/providers/ssh/consts.go @@ -1,5 +1,9 @@ package ssh +import ( + "github.com/certimate-go/certimate/internal/domain" +) + const ( AUTH_METHOD_NONE = "none" AUTH_METHOD_PASSWORD = "password" @@ -7,7 +11,8 @@ const ( ) const ( - OUTPUT_FORMAT_PEM = "PEM" - OUTPUT_FORMAT_PFX = "PFX" - OUTPUT_FORMAT_JKS = "JKS" + OUTPUT_FORMAT_PEM = string(domain.CertificateFormatTypePEM) + OUTPUT_FORMAT_PFX = string(domain.CertificateFormatTypePFX) + OUTPUT_FORMAT_JKS = string(domain.CertificateFormatTypeJKS) ) + diff --git a/pkg/core/deployer/providers/ssh/ssh.go b/pkg/core/deployer/providers/ssh/ssh.go index 05323290..e2146a76 100644 --- a/pkg/core/deployer/providers/ssh/ssh.go +++ b/pkg/core/deployer/providers/ssh/ssh.go @@ -202,53 +202,59 @@ func (d *Deployer) Deploy(ctx context.Context, certPEM, privkeyPEM string) (*dep // 上传证书和私钥文件 switch d.config.OutputFormat { case OUTPUT_FORMAT_PEM: - if err := xssh.WriteRemoteString(client, d.config.OutputKeyPath, privkeyPEM, d.config.UseSCP); err != nil { - return nil, fmt.Errorf("failed to upload private key file: %w", err) - } - d.logger.Info("ssl private key file uploaded", slog.String("path", d.config.OutputKeyPath)) - - if err := xssh.WriteRemoteString(client, d.config.OutputCertPath, certPEM, d.config.UseSCP); err != nil { - return nil, fmt.Errorf("failed to upload certificate file: %w", err) - } - d.logger.Info("ssl certificate file uploaded", slog.String("path", d.config.OutputCertPath)) - - if d.config.OutputServerCertPath != "" { - if err := xssh.WriteRemoteString(client, d.config.OutputServerCertPath, serverCertPEM, d.config.UseSCP); err != nil { - return nil, fmt.Errorf("failed to save server certificate file: %w", err) + { + if err := xssh.WriteRemoteString(client, d.config.OutputCertPath, certPEM, d.config.UseSCP); err != nil { + return nil, fmt.Errorf("failed to upload certificate file: %w", err) } - d.logger.Info("ssl server certificate file uploaded", slog.String("path", d.config.OutputServerCertPath)) - } + d.logger.Info("ssl certificate file uploaded", slog.String("path", d.config.OutputCertPath)) - if d.config.OutputIntermediaCertPath != "" { - if err := xssh.WriteRemoteString(client, d.config.OutputIntermediaCertPath, intermediaCertPEM, d.config.UseSCP); err != nil { - return nil, fmt.Errorf("failed to save intermedia certificate file: %w", err) + if d.config.OutputServerCertPath != "" { + if err := xssh.WriteRemoteString(client, d.config.OutputServerCertPath, serverCertPEM, d.config.UseSCP); err != nil { + return nil, fmt.Errorf("failed to save server certificate file: %w", err) + } + d.logger.Info("ssl server certificate file uploaded", slog.String("path", d.config.OutputServerCertPath)) } - d.logger.Info("ssl intermedia certificate file uploaded", slog.String("path", d.config.OutputIntermediaCertPath)) + + if d.config.OutputIntermediaCertPath != "" { + if err := xssh.WriteRemoteString(client, d.config.OutputIntermediaCertPath, intermediaCertPEM, d.config.UseSCP); err != nil { + return nil, fmt.Errorf("failed to save intermedia certificate file: %w", err) + } + d.logger.Info("ssl intermedia certificate file uploaded", slog.String("path", d.config.OutputIntermediaCertPath)) + } + + if err := xssh.WriteRemoteString(client, d.config.OutputKeyPath, privkeyPEM, d.config.UseSCP); err != nil { + return nil, fmt.Errorf("failed to upload private key file: %w", err) + } + d.logger.Info("ssl private key file uploaded", slog.String("path", d.config.OutputKeyPath)) } case OUTPUT_FORMAT_PFX: - pfxData, err := xcert.TransformCertificateFromPEMToPFX(certPEM, privkeyPEM, d.config.PfxPassword) - if err != nil { - return nil, fmt.Errorf("failed to transform certificate to PFX: %w", err) - } - d.logger.Info("ssl certificate transformed to pfx") + { + pfxData, err := xcert.TransformCertificateFromPEMToPFX(certPEM, privkeyPEM, d.config.PfxPassword) + if err != nil { + return nil, fmt.Errorf("failed to transform certificate to PFX: %w", err) + } + d.logger.Info("ssl certificate transformed to pfx") - if err := xssh.WriteRemote(client, d.config.OutputCertPath, pfxData, d.config.UseSCP); err != nil { - return nil, fmt.Errorf("failed to upload certificate file: %w", err) + if err := xssh.WriteRemote(client, d.config.OutputCertPath, pfxData, d.config.UseSCP); err != nil { + return nil, fmt.Errorf("failed to upload certificate file: %w", err) + } + d.logger.Info("ssl certificate file uploaded", slog.String("path", d.config.OutputCertPath)) } - d.logger.Info("ssl certificate file uploaded", slog.String("path", d.config.OutputCertPath)) case OUTPUT_FORMAT_JKS: - jksData, err := xcert.TransformCertificateFromPEMToJKS(certPEM, privkeyPEM, d.config.JksAlias, d.config.JksKeypass, d.config.JksStorepass) - if err != nil { - return nil, fmt.Errorf("failed to transform certificate to JKS: %w", err) - } - d.logger.Info("ssl certificate transformed to jks") + { + jksData, err := xcert.TransformCertificateFromPEMToJKS(certPEM, privkeyPEM, d.config.JksAlias, d.config.JksKeypass, d.config.JksStorepass) + if err != nil { + return nil, fmt.Errorf("failed to transform certificate to JKS: %w", err) + } + d.logger.Info("ssl certificate transformed to jks") - if err := xssh.WriteRemote(client, d.config.OutputCertPath, jksData, d.config.UseSCP); err != nil { - return nil, fmt.Errorf("failed to upload certificate file: %w", err) + if err := xssh.WriteRemote(client, d.config.OutputCertPath, jksData, d.config.UseSCP); err != nil { + return nil, fmt.Errorf("failed to upload certificate file: %w", err) + } + d.logger.Info("ssl certificate file uploaded", slog.String("path", d.config.OutputCertPath)) } - d.logger.Info("ssl certificate file uploaded", slog.String("path", d.config.OutputCertPath)) default: return nil, fmt.Errorf("unsupported output format '%s'", d.config.OutputFormat) diff --git a/ui/src/components/workflow/designer/forms/BizDeployNodeConfigFieldsProvider.tsx b/ui/src/components/workflow/designer/forms/BizDeployNodeConfigFieldsProvider.tsx index 8637a43b..e789c538 100644 --- a/ui/src/components/workflow/designer/forms/BizDeployNodeConfigFieldsProvider.tsx +++ b/ui/src/components/workflow/designer/forms/BizDeployNodeConfigFieldsProvider.tsx @@ -70,6 +70,7 @@ import BizDeployNodeConfigFieldsProviderQiniuKodo from "./BizDeployNodeConfigFie import BizDeployNodeConfigFieldsProviderQiniuPili from "./BizDeployNodeConfigFieldsProviderQiniuPili"; import BizDeployNodeConfigFieldsProviderRainYunRCDN from "./BizDeployNodeConfigFieldsProviderRainYunRCDN"; import BizDeployNodeConfigFieldsProviderRatPanel from "./BizDeployNodeConfigFieldsProviderRatPanel.tsx"; +import BizDeployNodeConfigFieldsProviderS3 from "./BizDeployNodeConfigFieldsProviderS3"; import BizDeployNodeConfigFieldsProviderSafeLine from "./BizDeployNodeConfigFieldsProviderSafeLine.tsx"; import BizDeployNodeConfigFieldsProviderSSH from "./BizDeployNodeConfigFieldsProviderSSH"; import BizDeployNodeConfigFieldsProviderSynologyDSM from "./BizDeployNodeConfigFieldsProviderSynologyDSM"; @@ -183,6 +184,7 @@ const providerComponentMap: Partial { const getInitialValues = (): Nullish>> => { return { format: FORMAT_PEM, - certPath: "/etc/ssl/certimate/cert.crt", keyPath: "/etc/ssl/certimate/cert.key", + certPath: "/etc/ssl/certimate/cert.crt", shellEnv: SHELLENV_SH, }; }; diff --git a/ui/src/components/workflow/designer/forms/BizDeployNodeConfigFieldsProviderS3.tsx b/ui/src/components/workflow/designer/forms/BizDeployNodeConfigFieldsProviderS3.tsx new file mode 100644 index 00000000..d75a168f --- /dev/null +++ b/ui/src/components/workflow/designer/forms/BizDeployNodeConfigFieldsProviderS3.tsx @@ -0,0 +1,286 @@ +import { getI18n, useTranslation } from "react-i18next"; +import { Form, Input, Select } from "antd"; +import { createSchemaFieldRule } from "antd-zod"; +import { z } from "zod"; + +import Show from "@/components/Show"; +import { CERTIFICATE_FORMATS } from "@/domain/certificate"; + +import { useFormNestedFieldsContext } from "./_context"; + +const FORMAT_PEM = CERTIFICATE_FORMATS.PEM; +const FORMAT_PFX = CERTIFICATE_FORMATS.PFX; +const FORMAT_JKS = CERTIFICATE_FORMATS.JKS; + +const BizDeployNodeConfigFieldsProviderS3 = () => { + const { i18n, t } = useTranslation(); + + const { parentNamePath } = useFormNestedFieldsContext(); + const formSchema = z.object({ + [parentNamePath]: getSchema({ i18n }), + }); + const formRule = createSchemaFieldRule(formSchema); + const formInst = Form.useFormInstance(); + const initialValues = getInitialValues(); + + const fieldFormat = Form.useWatch([parentNamePath, "format"], formInst); + const fieldCertPath = Form.useWatch([parentNamePath, "certObjectKey"], formInst); + + const handleFormatSelect = (value: string) => { + if (fieldFormat === value) return; + + switch (value) { + case FORMAT_PEM: + { + if (/(.pfx|.jks)$/.test(fieldCertPath)) { + formInst.setFieldValue([parentNamePath, "certObjectKey"], fieldCertPath.replace(/(.pfx|.jks)$/, ".crt")); + } + } + break; + + case FORMAT_PFX: + { + if (/(.crt|.jks)$/.test(fieldCertPath)) { + formInst.setFieldValue([parentNamePath, "certObjectKey"], fieldCertPath.replace(/(.crt|.jks)$/, ".pfx")); + } + } + break; + + case FORMAT_JKS: + { + if (/(.crt|.pfx)$/.test(fieldCertPath)) { + formInst.setFieldValue([parentNamePath, "certObjectKey"], fieldCertPath.replace(/(.crt|.pfx)$/, ".jks")); + } + } + break; + } + }; + + return ( + <> + + + + + + + + + + + + + + + + + + + + + + + + + + + + + } + > + + + + + + } + > + + + + } + > + + + + } + > + + + + + ); +}; + +const getInitialValues = (): Nullish>> => { + return { + region: "", + bucket: "", + format: FORMAT_PEM, + keyObjectKey: ".certimate/cert.key", + certObjectKey: ".certimate/cert.crt", + }; +}; + +const getSchema = ({ i18n = getI18n() }: { i18n?: ReturnType }) => { + const { t } = i18n; + + return z + .object({ + region: z.string().nonempty(t("workflow_node.deploy.form.s3_region.placeholder")), + bucket: z.string().nonempty(t("workflow_node.deploy.form.s3_bucket.placeholder")), + format: z.literal([FORMAT_PEM, FORMAT_PFX, FORMAT_JKS], t("workflow_node.deploy.form.s3_format.placeholder")), + keyObjectKey: z + .string() + .max(256, t("common.errmsg.string_max", { max: 256 })) + .nullish(), + certObjectKey: z + .string() + .min(1, t("workflow_node.deploy.form.s3_cert_object_key.placeholder")) + .max(256, t("common.errmsg.string_max", { max: 256 })), + certObjectKeyForServerOnly: z + .string() + .max(256, t("common.errmsg.string_max", { max: 256 })) + .nullish(), + certObjectKeyForIntermediaOnly: z + .string() + .max(256, t("common.errmsg.string_max", { max: 256 })) + .nullish(), + pfxPassword: z.string().nullish(), + jksAlias: z.string().nullish(), + jksKeypass: z.string().nullish(), + jksStorepass: z.string().nullish(), + }) + .superRefine((values, ctx) => { + switch (values.format) { + case FORMAT_PEM: + { + if (!values.keyObjectKey?.trim()) { + ctx.addIssue({ + code: "custom", + message: t("workflow_node.deploy.form.s3_key_object_key.placeholder"), + path: ["keyObjectKey"], + }); + } + } + break; + + case FORMAT_PFX: + { + if (!values.pfxPassword?.trim()) { + ctx.addIssue({ + code: "custom", + message: t("workflow_node.deploy.form.s3_pfx_password.placeholder"), + path: ["pfxPassword"], + }); + } + } + break; + + case FORMAT_JKS: + { + if (!values.jksAlias?.trim()) { + ctx.addIssue({ + code: "custom", + message: t("workflow_node.deploy.form.s3_jks_alias.placeholder"), + path: ["jksAlias"], + }); + } + + if (!values.jksKeypass?.trim()) { + ctx.addIssue({ + code: "custom", + message: t("workflow_node.deploy.form.s3_jks_keypass.placeholder"), + path: ["jksKeypass"], + }); + } + + if (!values.jksStorepass?.trim()) { + ctx.addIssue({ + code: "custom", + message: t("workflow_node.deploy.form.s3_jks_storepass.placeholder"), + path: ["jksStorepass"], + }); + } + } + break; + } + }); +}; + +const _default = Object.assign(BizDeployNodeConfigFieldsProviderS3, { + getInitialValues, + getSchema, +}); + +export default _default; diff --git a/ui/src/components/workflow/designer/forms/BizDeployNodeConfigFieldsProviderSSH.tsx b/ui/src/components/workflow/designer/forms/BizDeployNodeConfigFieldsProviderSSH.tsx index 6ae0ccfd..58b63632 100644 --- a/ui/src/components/workflow/designer/forms/BizDeployNodeConfigFieldsProviderSSH.tsx +++ b/ui/src/components/workflow/designer/forms/BizDeployNodeConfigFieldsProviderSSH.tsx @@ -460,8 +460,8 @@ const BizDeployNodeConfigFieldsProviderSSH = () => { const getInitialValues = (): Nullish>> => { return { format: FORMAT_PEM, - certPath: "/etc/ssl/certimate/cert.crt", keyPath: "/etc/ssl/certimate/cert.key", + certPath: "/etc/ssl/certimate/cert.crt", }; }; diff --git a/ui/src/domain/provider.ts b/ui/src/domain/provider.ts index 04c634c3..e87d74a3 100644 --- a/ui/src/domain/provider.ts +++ b/ui/src/domain/provider.ts @@ -615,6 +615,7 @@ export const DEPLOYMENT_PROVIDERS = Object.freeze({ RAINYUN_RCDN: `${ACCESS_PROVIDERS.RAINYUN}-rcdn`, RATPANEL: `${ACCESS_PROVIDERS.RATPANEL}`, RATPANEL_CONSOLE: `${ACCESS_PROVIDERS.RATPANEL}-console`, + S3: `${ACCESS_PROVIDERS.S3}`, SAFELINE: `${ACCESS_PROVIDERS.SAFELINE}`, SSH: `${ACCESS_PROVIDERS.SSH}`, SYNOLOGYDSM: `${ACCESS_PROVIDERS.SYNOLOGYDSM}`, @@ -690,6 +691,7 @@ export const deploymentProvidersMap: Maphttps://learn.microsoft.com/en-us/windows-hardware/drivers/install/personal-information-exchange---pfx--files", + "workflow_node.deploy.form.s3_jks_alias.label": "JKS alias", + "workflow_node.deploy.form.s3_jks_alias.placeholder": "Please enter JKS alias", + "workflow_node.deploy.form.s3_jks_alias.tooltip": "For more information, see https://docs.oracle.com/cd/E19509-01/820-3503/ggfen/index.html", + "workflow_node.deploy.form.s3_jks_keypass.label": "JKS key password", + "workflow_node.deploy.form.s3_jks_keypass.placeholder": "Please enter JKS key password", + "workflow_node.deploy.form.s3_jks_keypass.tooltip": "For more information, see https://docs.oracle.com/cd/E19509-01/820-3503/ggfen/index.html", + "workflow_node.deploy.form.s3_jks_storepass.label": "JKS store password", + "workflow_node.deploy.form.s3_jks_storepass.placeholder": "Please enter JKS store password", + "workflow_node.deploy.form.s3_jks_storepass.tooltip": "For more information, see https://docs.oracle.com/cd/E19509-01/820-3503/ggfen/index.html", "workflow_node.deploy.form.safeline_resource_type.option.certificate.label": "Certificate", "workflow_node.deploy.form.safeline_certificate_id.label": "SafeLine certificate ID", "workflow_node.deploy.form.safeline_certificate_id.placeholder": "Please enter SafeLine certificate ID", diff --git a/ui/src/i18n/locales/zh/nls.provider.json b/ui/src/i18n/locales/zh/nls.provider.json index 1b03e79b..86693383 100644 --- a/ui/src/i18n/locales/zh/nls.provider.json +++ b/ui/src/i18n/locales/zh/nls.provider.json @@ -159,6 +159,7 @@ "provider.ratpanel_console": "耗子面板 - 面板自身", "provider.rfc2136": "RFC 2136: Dynamic DNS Updates", "provider.s3": "对象存储(S3 兼容)", + "provider.s3_upload": "上传到 S3 兼容的对象存储", "provider.safeline": "雷池", "provider.sectigo": "Sectigo", "provider.slackbot": "Slack 机器人", diff --git a/ui/src/i18n/locales/zh/nls.workflow.nodes.json b/ui/src/i18n/locales/zh/nls.workflow.nodes.json index da6f020f..51299cf4 100644 --- a/ui/src/i18n/locales/zh/nls.workflow.nodes.json +++ b/ui/src/i18n/locales/zh/nls.workflow.nodes.json @@ -777,6 +777,39 @@ "workflow_node.deploy.form.ratpanel_site_names.tooltip": "请登录耗子面板查看", "workflow_node.deploy.form.ratpanel_site_names.multiple_input_modal.title": "修改耗子面板网站名称", "workflow_node.deploy.form.ratpanel_site_names.multiple_input_modal.placeholder": "请输入耗子面板网站名称", + "workflow_node.deploy.form.s3_region.label": "对象存储区域", + "workflow_node.deploy.form.s3_region.placeholder": "请输入对象存储区域", + "workflow_node.deploy.form.s3_bucket.label": "对象存储桶名", + "workflow_node.deploy.form.s3_bucket.placeholder": "请输入对象存储桶名", + "workflow_node.deploy.form.s3_format.label": "文件格式", + "workflow_node.deploy.form.s3_format.placeholder": "请选择文件格式", + "workflow_node.deploy.form.s3_format.option.pem.label": "PEM 格式(*.pem, *.crt, *.key)", + "workflow_node.deploy.form.s3_format.option.pfx.label": "PFX 格式(*.pfx, *.p12)", + "workflow_node.deploy.form.s3_format.option.jks.label": "JKS 格式(*.jks)", + "workflow_node.deploy.form.s3_key_object_key.label": "私钥文件对象键", + "workflow_node.deploy.form.s3_key_object_key.placeholder": "请输入私钥文件对象键", + "workflow_node.deploy.form.s3_cert_object_key.label": "证书文件对象键", + "workflow_node.deploy.form.s3_cert_object_key.placeholder": "请输入证书文件对象键", + "workflow_node.deploy.form.s3_fullchaincert_object_key.label": "证书链文件对象键", + "workflow_node.deploy.form.s3_fullchaincert_object_key.placeholder": "请输入证书链文件对象键", + "workflow_node.deploy.form.s3_servercert_object_key.label": "服务器证书文件对象键(可选)", + "workflow_node.deploy.form.s3_servercert_object_key.placeholder": "请输入服务器证书文件对象键", + "workflow_node.deploy.form.s3_servercert_object_key.help": "注意:不填写时将不会上传服务器证书。", + "workflow_node.deploy.form.s3_intermediacert_object_key.label": "中间证书文件对象键(可选)", + "workflow_node.deploy.form.s3_intermediacert_object_key.placeholder": "请输入中间证书文件对象键", + "workflow_node.deploy.form.s3_intermediacert_object_key.help": "注意:不填写时将不会上传中间证书。", + "workflow_node.deploy.form.s3_pfx_password.label": "PFX 导出密码", + "workflow_node.deploy.form.s3_pfx_password.placeholder": "请输入 PFX 导出密码", + "workflow_node.deploy.form.s3_pfx_password.tooltip": "这是什么?请参阅 https://learn.microsoft.com/zh-cn/windows-hardware/drivers/install/personal-information-exchange---pfx--files", + "workflow_node.deploy.form.s3_jks_alias.label": "JKS 别名", + "workflow_node.deploy.form.s3_jks_alias.placeholder": "请输入 JKS 别名", + "workflow_node.deploy.form.s3_jks_alias.tooltip": "这是什么?请参阅 https://docs.oracle.com/cd/E19509-01/820-3503/ggfen/index.html", + "workflow_node.deploy.form.s3_jks_keypass.label": "JKS 私钥访问口令", + "workflow_node.deploy.form.s3_jks_keypass.placeholder": "请输入 JKS 私钥访问口令", + "workflow_node.deploy.form.s3_jks_keypass.tooltip": "这是什么?请参阅 https://docs.oracle.com/cd/E19509-01/820-3503/ggfen/index.html", + "workflow_node.deploy.form.s3_jks_storepass.label": "JKS 密钥库存储口令", + "workflow_node.deploy.form.s3_jks_storepass.placeholder": "请输入 JKS 密钥库存储口令", + "workflow_node.deploy.form.s3_jks_storepass.tooltip": "这是什么?请参阅 https://docs.oracle.com/cd/E19509-01/820-3503/ggfen/index.html", "workflow_node.deploy.form.safeline_resource_type.option.certificate.label": "替换指定证书", "workflow_node.deploy.form.safeline_certificate_id.label": "雷池证书 ID", "workflow_node.deploy.form.safeline_certificate_id.placeholder": "请输入雷池证书 ID",