mirror of
https://github.com/certimate-go/certimate.git
synced 2026-09-01 15:39:35 +08:00
Add Synology DSM certificate deployer integration
This commit is contained in:
@@ -58,6 +58,7 @@ require (
|
||||
github.com/pocketbase/dbx v1.11.0
|
||||
github.com/pocketbase/pocketbase v0.35.0
|
||||
github.com/povsister/scp v0.0.0-20250701154629-777cf82de5df
|
||||
github.com/pquerna/otp v1.5.0
|
||||
github.com/qiniu/go-sdk/v7 v7.25.5
|
||||
github.com/samber/lo v1.52.0
|
||||
github.com/spf13/cobra v1.10.2
|
||||
@@ -194,6 +195,7 @@ require (
|
||||
github.com/aws/aws-sdk-go-v2/service/ssooidc v1.35.12 // indirect
|
||||
github.com/aws/aws-sdk-go-v2/service/sts v1.41.5 // indirect
|
||||
github.com/aws/smithy-go v1.24.0 // indirect
|
||||
github.com/boombuler/barcode v1.0.1-0.20190219062509-6c824513bacc // indirect
|
||||
github.com/cenkalti/backoff/v4 v4.3.0 // indirect
|
||||
github.com/clbanning/mxj/v2 v2.7.0 // indirect
|
||||
github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc // indirect
|
||||
|
||||
@@ -247,6 +247,8 @@ github.com/beorn7/perks v0.0.0-20180321164747-3a771d992973/go.mod h1:Dwedo/Wpr24
|
||||
github.com/beorn7/perks v1.0.0/go.mod h1:KWe93zE9D1o94FZ5RNwFwVgaQK1VOXiVxmqh+CedLV8=
|
||||
github.com/beorn7/perks v1.0.1/go.mod h1:G2ZrVWU2WbWT9wwq4/hrbKbnv/1ERSJQ0ibhJ6rlkpw=
|
||||
github.com/bgentry/speakeasy v0.1.0/go.mod h1:+zsyZBPWlz7T6j88CTgSN5bM796AkVf0kBD4zp0CCIs=
|
||||
github.com/boombuler/barcode v1.0.1-0.20190219062509-6c824513bacc h1:biVzkmvwrH8WK8raXaxBx6fRVTlJILwEwQGL1I/ByEI=
|
||||
github.com/boombuler/barcode v1.0.1-0.20190219062509-6c824513bacc/go.mod h1:paBWMcWSl3LHKBqUq+rly7CNSldXjb2rDl3JlRe0mD8=
|
||||
github.com/buger/goterm v1.0.4 h1:Z9YvGmOih81P0FbVtEYTFF6YsSgxSUKEhf/f9bTMXbY=
|
||||
github.com/buger/goterm v1.0.4/go.mod h1:HiFWV3xnkolgrBV3mY8m0X0Pumt4zg4QhbdOzQtB8tE=
|
||||
github.com/byteplus-sdk/byteplus-sdk-golang v1.0.61 h1:IJHXAInb/ALRosopHPzXH/x2mt7LyfaawILEHYZHRRo=
|
||||
@@ -764,6 +766,8 @@ github.com/posener/complete v1.1.1/go.mod h1:em0nMJCgc9GFtwrmVmEMR/ZL6WyhyjMBndr
|
||||
github.com/posener/complete v1.2.3/go.mod h1:WZIdtGGp+qx0sLrYKtIRAruyNpv6hFCicSgv7Sy7s/s=
|
||||
github.com/povsister/scp v0.0.0-20250701154629-777cf82de5df h1:zEgSHrxo8f6hGG1xCaqunfBq8hlfDmFd1JM0QXiQi7o=
|
||||
github.com/povsister/scp v0.0.0-20250701154629-777cf82de5df/go.mod h1:CiJNEeV6v0tUCNul/+gTjl+FgjfImoiuptJB9AEzqjE=
|
||||
github.com/pquerna/otp v1.5.0 h1:NMMR+WrmaqXU4EzdGJEE1aUUI0AMRzsp96fFFWNPwxs=
|
||||
github.com/pquerna/otp v1.5.0/go.mod h1:dkJfzwRKNiegxyNb54X/3fLwhCynbMspSyWKnvi1AEg=
|
||||
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=
|
||||
|
||||
@@ -0,0 +1,30 @@
|
||||
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/synologydsm"
|
||||
xmaps "github.com/certimate-go/certimate/pkg/utils/maps"
|
||||
)
|
||||
|
||||
func init() {
|
||||
Registries.MustRegister(domain.DeploymentProviderTypeSynologyDSM, func(options *ProviderFactoryOptions) (deployer.Provider, error) {
|
||||
credentials := domain.AccessConfigForSynologyDSM{}
|
||||
if err := xmaps.Populate(options.ProviderAccessConfig, &credentials); err != nil {
|
||||
return nil, fmt.Errorf("failed to populate provider access config: %w", err)
|
||||
}
|
||||
|
||||
provider, err := synologydsm.NewDeployer(&synologydsm.DeployerConfig{
|
||||
ServerUrl: credentials.ServerUrl,
|
||||
Username: credentials.Username,
|
||||
Password: credentials.Password,
|
||||
TotpSecret: credentials.TotpSecret,
|
||||
AllowInsecureConnections: credentials.AllowInsecureConnections,
|
||||
CertificateIdOrDescription: xmaps.GetString(options.ProviderExtendedConfig, "certificateIdOrDesc"),
|
||||
IsDefault: xmaps.GetBool(options.ProviderExtendedConfig, "isDefault"),
|
||||
})
|
||||
return provider, err
|
||||
})
|
||||
}
|
||||
@@ -516,6 +516,14 @@ type AccessConfigForSSLCom struct {
|
||||
AccessConfigForACMEExternalAccountBinding
|
||||
}
|
||||
|
||||
type AccessConfigForSynologyDSM struct {
|
||||
ServerUrl string `json:"serverUrl"`
|
||||
Username string `json:"username"`
|
||||
Password string `json:"password"`
|
||||
TotpSecret string `json:"totpSecret,omitempty"`
|
||||
AllowInsecureConnections bool `json:"allowInsecureConnections,omitempty"`
|
||||
}
|
||||
|
||||
type AccessConfigForTechnitiumDNS struct {
|
||||
ServerUrl string `json:"serverUrl"`
|
||||
ApiToken string `json:"apiToken"`
|
||||
|
||||
@@ -103,6 +103,7 @@ const (
|
||||
AccessProviderTypeSpaceship = AccessProviderType("spaceship")
|
||||
AccessProviderTypeSSH = AccessProviderType("ssh")
|
||||
AccessProviderTypeSSLCOM = AccessProviderType("sslcom")
|
||||
AccessProviderTypeSynologyDSM = AccessProviderType("synologydsm")
|
||||
AccessProviderTypeTechnitiumDNS = AccessProviderType("technitiumdns")
|
||||
AccessProviderTypeTelegramBot = AccessProviderType("telegrambot")
|
||||
AccessProviderTypeTencentCloud = AccessProviderType("tencentcloud")
|
||||
@@ -330,6 +331,7 @@ const (
|
||||
DeploymentProviderTypeRatPanelConsole = DeploymentProviderType(AccessProviderTypeRatPanel + "-console")
|
||||
DeploymentProviderTypeSafeLine = DeploymentProviderType(AccessProviderTypeSafeLine)
|
||||
DeploymentProviderTypeSSH = DeploymentProviderType(AccessProviderTypeSSH)
|
||||
DeploymentProviderTypeSynologyDSM = DeploymentProviderType(AccessProviderTypeSynologyDSM)
|
||||
DeploymentProviderTypeTencentCloudCDN = DeploymentProviderType(AccessProviderTypeTencentCloud + "-cdn")
|
||||
DeploymentProviderTypeTencentCloudCLB = DeploymentProviderType(AccessProviderTypeTencentCloud + "-clb")
|
||||
DeploymentProviderTypeTencentCloudCOS = DeploymentProviderType(AccessProviderTypeTencentCloud + "-cos")
|
||||
|
||||
@@ -0,0 +1,232 @@
|
||||
package synologydsm
|
||||
|
||||
import (
|
||||
"context"
|
||||
"crypto/tls"
|
||||
"errors"
|
||||
"fmt"
|
||||
"log/slog"
|
||||
"time"
|
||||
|
||||
"github.com/pquerna/otp/totp"
|
||||
"github.com/samber/lo"
|
||||
|
||||
"github.com/certimate-go/certimate/pkg/core/deployer"
|
||||
dsmsdk "github.com/certimate-go/certimate/pkg/sdk3rd/synologydsm"
|
||||
xcert "github.com/certimate-go/certimate/pkg/utils/cert"
|
||||
xwait "github.com/certimate-go/certimate/pkg/utils/wait"
|
||||
)
|
||||
|
||||
type DeployerConfig struct {
|
||||
// 群晖 DSM 服务地址。
|
||||
ServerUrl string `json:"serverUrl"`
|
||||
// 群晖 DSM 用户名。
|
||||
Username string `json:"username"`
|
||||
// 群晖 DSM 用户密码。
|
||||
Password string `json:"password"`
|
||||
// 群晖 DSM 2FA TOTP 密钥。
|
||||
TotpSecret string `json:"totpSecret,omitempty"`
|
||||
// 是否允许不安全的连接。
|
||||
AllowInsecureConnections bool `json:"allowInsecureConnections,omitempty"`
|
||||
// 证书 ID 或描述。
|
||||
// 选填。零值时表示新建证书;否则表示更新证书。
|
||||
CertificateIdOrDescription string `json:"certificateIdOrDesc,omitempty"`
|
||||
// 是否设为默认证书。
|
||||
IsDefault bool `json:"isDefault,omitempty"`
|
||||
}
|
||||
|
||||
type Deployer struct {
|
||||
config *DeployerConfig
|
||||
logger *slog.Logger
|
||||
sdkClient *dsmsdk.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 := createSDKClient(config.ServerUrl, config.AllowInsecureConnections)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("could not create client: %w", err)
|
||||
}
|
||||
|
||||
return &Deployer{
|
||||
config: config,
|
||||
logger: slog.Default(),
|
||||
sdkClient: client,
|
||||
}, nil
|
||||
}
|
||||
|
||||
func (d *Deployer) SetLogger(logger *slog.Logger) {
|
||||
if logger == nil {
|
||||
d.logger = slog.Default()
|
||||
} else {
|
||||
d.logger = logger
|
||||
}
|
||||
}
|
||||
|
||||
func (d *Deployer) Deploy(ctx context.Context, certPEM string, privkeyPEM string) (*deployer.DeployResult, error) {
|
||||
// 提取服务器证书和中间证书
|
||||
serverCertPEM, intermediateCertPEM, err := xcert.ExtractCertificatesFromPEM(certPEM)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to extract certs: %w", err)
|
||||
}
|
||||
|
||||
// 如果启用了 TOTP,则等到下一个时间窗口后生成 OTP 动态密码
|
||||
var otpCode string
|
||||
if d.config.TotpSecret != "" {
|
||||
now := time.Now()
|
||||
wait := time.Duration(30-now.Unix()%30) * time.Second
|
||||
if wait > 0 {
|
||||
wait = wait + 1*time.Second
|
||||
d.logger.Info("waiting for the next TOTP time step ...", slog.Int("wait", int(wait.Seconds())))
|
||||
xwait.DelayWithContext(ctx, wait)
|
||||
}
|
||||
|
||||
now = time.Now()
|
||||
otpCodeStr, err := totp.GenerateCode(d.config.TotpSecret, now)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to generate TOTP code: %w", err)
|
||||
}
|
||||
|
||||
otpCode = otpCodeStr
|
||||
}
|
||||
|
||||
// 登录到群晖 DSM
|
||||
loginReq := &dsmsdk.LoginRequest{
|
||||
Account: d.config.Username,
|
||||
Password: d.config.Password,
|
||||
OtpCode: otpCode,
|
||||
}
|
||||
loginResp, err := d.sdkClient.Login(loginReq)
|
||||
d.logger.Debug("sdk request 'SYNO.API.Auth:login'", slog.Any("request", loginReq), slog.Any("response", loginResp))
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to execute sdk request 'SYNO.API.Auth:login': %w", err)
|
||||
}
|
||||
defer func() {
|
||||
logoutResp, _ := d.sdkClient.Logout()
|
||||
d.logger.Debug("sdk request 'SYNO.API.Auth:logout'", slog.Any("response", logoutResp))
|
||||
}()
|
||||
|
||||
// 如果原证书 ID 或描述为空,则创建证书;否则更新证书。
|
||||
if d.config.CertificateIdOrDescription == "" {
|
||||
// 导入证书
|
||||
importCertificateReq := &dsmsdk.ImportCertificateRequest{
|
||||
ID: "",
|
||||
Description: fmt.Sprintf("certimate-%d", time.Now().UnixMilli()),
|
||||
Key: privkeyPEM,
|
||||
Cert: serverCertPEM,
|
||||
InterCert: intermediateCertPEM,
|
||||
AsDefault: d.config.IsDefault,
|
||||
}
|
||||
importCertificateResp, err := d.sdkClient.ImportCertificate(importCertificateReq)
|
||||
d.logger.Debug("sdk request 'SYNO.Core.Certificate:import'", slog.Any("request", importCertificateReq), slog.Any("response", importCertificateResp))
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to execute sdk request 'SYNO.Core.Certificate:import': %w", err)
|
||||
}
|
||||
} else {
|
||||
// 查找证书列表,找到已有证书
|
||||
var certInfo *dsmsdk.CertificateInfo
|
||||
listCertificatesResp, err := d.sdkClient.ListCertificates()
|
||||
d.logger.Debug("sdk request 'SYNO.Core.Certificate.CRT:list'", slog.Any("response", listCertificatesResp))
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to execute sdk request 'SYNO.Core.Certificate.CRT:list': %w", err)
|
||||
} else {
|
||||
matchedCerts := lo.Filter(listCertificatesResp.Data.Certificates, func(certItem *dsmsdk.CertificateInfo, _ int) bool {
|
||||
return certItem.ID == d.config.CertificateIdOrDescription
|
||||
})
|
||||
if len(matchedCerts) == 0 {
|
||||
matchedCerts = lo.Filter(listCertificatesResp.Data.Certificates, func(certItem *dsmsdk.CertificateInfo, _ int) bool {
|
||||
return certItem.Description == d.config.CertificateIdOrDescription
|
||||
})
|
||||
}
|
||||
if len(matchedCerts) == 0 {
|
||||
return nil, fmt.Errorf("could not find certificate '%s'", d.config.CertificateIdOrDescription)
|
||||
} else {
|
||||
if len(matchedCerts) > 1 {
|
||||
d.logger.Warn("found several certificates matched '%s', using the first one")
|
||||
}
|
||||
certInfo = matchedCerts[0]
|
||||
}
|
||||
}
|
||||
|
||||
// 导入证书
|
||||
importCertificateReq := &dsmsdk.ImportCertificateRequest{
|
||||
ID: certInfo.ID,
|
||||
Description: certInfo.Description,
|
||||
Key: privkeyPEM,
|
||||
Cert: serverCertPEM,
|
||||
InterCert: intermediateCertPEM,
|
||||
AsDefault: d.config.IsDefault || certInfo.IsDefault,
|
||||
}
|
||||
importCertificateResp, err := d.sdkClient.ImportCertificate(importCertificateReq)
|
||||
d.logger.Debug("sdk request 'SYNO.Core.Certificate:import'", slog.Any("request", importCertificateReq), slog.Any("response", importCertificateResp))
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to execute sdk request 'SYNO.Core.Certificate:import': %w", err)
|
||||
}
|
||||
}
|
||||
|
||||
if d.config.IsDefault {
|
||||
// 查找证书列表,找到默认证书
|
||||
listCertificatesResp, err := d.sdkClient.ListCertificates()
|
||||
d.logger.Debug("sdk request 'SYNO.Core.Certificate.CRT:list'", slog.Any("response", listCertificatesResp))
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to execute sdk request 'SYNO.Core.Certificate.CRT:list': %w", err)
|
||||
} else {
|
||||
var defaultCertId string
|
||||
for _, certItem := range listCertificatesResp.Data.Certificates {
|
||||
if certItem.IsDefault {
|
||||
defaultCertId = certItem.ID
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
if defaultCertId != "" {
|
||||
settings := make([]*dsmsdk.ServiceCertificateSetting, 0)
|
||||
for _, certItem := range listCertificatesResp.Data.Certificates {
|
||||
if certItem.ID == defaultCertId {
|
||||
continue
|
||||
}
|
||||
|
||||
for _, service := range certItem.Services {
|
||||
settings = append(settings, &dsmsdk.ServiceCertificateSetting{
|
||||
Service: service,
|
||||
CertID: defaultCertId,
|
||||
OldCertID: certItem.ID,
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
// 应用到所有服务并重启
|
||||
if len(settings) > 0 {
|
||||
setServiceCertificateReq := &dsmsdk.SetServiceCertificateRequest{
|
||||
Settings: settings,
|
||||
}
|
||||
setServiceCertificateResp, err := d.sdkClient.SetServiceCertificate(setServiceCertificateReq)
|
||||
d.logger.Debug("sdk request 'SYNO.Core.Certificate.Service:set'", slog.Any("request", setServiceCertificateReq), slog.Any("response", setServiceCertificateResp))
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to execute sdk request 'SYNO.Core.Certificate.Service:set': %w", err)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return &deployer.DeployResult{}, nil
|
||||
}
|
||||
|
||||
func createSDKClient(serverUrl string, skipTlsVerify bool) (*dsmsdk.Client, error) {
|
||||
client, err := dsmsdk.NewClient(serverUrl)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
if skipTlsVerify {
|
||||
client.SetTLSConfig(&tls.Config{InsecureSkipVerify: true})
|
||||
}
|
||||
|
||||
return client, nil
|
||||
}
|
||||
@@ -0,0 +1,90 @@
|
||||
package synologydsm_test
|
||||
|
||||
import (
|
||||
"context"
|
||||
"flag"
|
||||
"fmt"
|
||||
"os"
|
||||
"strings"
|
||||
"testing"
|
||||
|
||||
provider "github.com/certimate-go/certimate/pkg/core/deployer/providers/synologydsm"
|
||||
)
|
||||
|
||||
var (
|
||||
fInputCertPath string
|
||||
fInputKeyPath string
|
||||
fServerUrl string
|
||||
fUsername string
|
||||
fPassword string
|
||||
fTotpSecret string
|
||||
fCertificateIdOrDesc string
|
||||
fIsDefault bool
|
||||
)
|
||||
|
||||
func init() {
|
||||
argsPrefix := "SYNOLOGYDSM_"
|
||||
|
||||
flag.StringVar(&fInputCertPath, argsPrefix+"INPUTCERTPATH", "", "")
|
||||
flag.StringVar(&fInputKeyPath, argsPrefix+"INPUTKEYPATH", "", "")
|
||||
flag.StringVar(&fServerUrl, argsPrefix+"SERVERURL", "", "")
|
||||
flag.StringVar(&fUsername, argsPrefix+"USERNAME", "", "")
|
||||
flag.StringVar(&fPassword, argsPrefix+"PASSWORD", "", "")
|
||||
flag.StringVar(&fTotpSecret, argsPrefix+"TOTPSECRET", "", "")
|
||||
flag.StringVar(&fCertificateIdOrDesc, argsPrefix+"CERTIFICATEIDORDESC", "", "")
|
||||
flag.BoolVar(&fIsDefault, argsPrefix+"ISDEFAULT", false, "")
|
||||
}
|
||||
|
||||
/*
|
||||
Shell command to run this test:
|
||||
|
||||
go test -v ./synology_dsm_test.go -args \
|
||||
--SYNOLOGYDSM_INPUTCERTPATH="/path/to/your-input-cert.pem" \
|
||||
--SYNOLOGYDSM_INPUTKEYPATH="/path/to/your-input-key.pem" \
|
||||
--SYNOLOGYDSM_SERVERURL="http://127.0.0.1:5000/" \
|
||||
--SYNOLOGYDSM_USERNAME="admin" \
|
||||
--SYNOLOGYDSM_PASSWORD="password" \
|
||||
--SYNOLOGYDSM_CERTIFICATEIDORDESC="your-certificate-id-or-desc" \
|
||||
--SYNOLOGYDSM_ISDEFAULT=true
|
||||
*/
|
||||
func TestDeploy(t *testing.T) {
|
||||
flag.Parse()
|
||||
|
||||
t.Run("Deploy", func(t *testing.T) {
|
||||
t.Log(strings.Join([]string{
|
||||
"args:",
|
||||
fmt.Sprintf("INPUTCERTPATH: %v", fInputCertPath),
|
||||
fmt.Sprintf("INPUTKEYPATH: %v", fInputKeyPath),
|
||||
fmt.Sprintf("SERVERURL: %v", fServerUrl),
|
||||
fmt.Sprintf("USERNAME: %v", fUsername),
|
||||
fmt.Sprintf("PASSWORD: %v", fPassword),
|
||||
fmt.Sprintf("TOTPSECRET: %v", fTotpSecret),
|
||||
fmt.Sprintf("CERTIFICATEIDORDESC: %v", fCertificateIdOrDesc),
|
||||
fmt.Sprintf("ISDEFAULT: %v", fIsDefault),
|
||||
}, "\n"))
|
||||
|
||||
deployer, err := provider.NewDeployer(&provider.DeployerConfig{
|
||||
ServerUrl: fServerUrl,
|
||||
Username: fUsername,
|
||||
Password: fPassword,
|
||||
TotpSecret: fTotpSecret,
|
||||
AllowInsecureConnections: true,
|
||||
CertificateIdOrDescription: fCertificateIdOrDesc,
|
||||
IsDefault: fIsDefault,
|
||||
})
|
||||
if err != nil {
|
||||
t.Errorf("err: %+v", err)
|
||||
return
|
||||
}
|
||||
|
||||
fInputCertData, _ := os.ReadFile(fInputCertPath)
|
||||
fInputKeyData, _ := os.ReadFile(fInputKeyPath)
|
||||
res, err := deployer.Deploy(context.Background(), string(fInputCertData), string(fInputKeyData))
|
||||
if err != nil {
|
||||
t.Errorf("err: %+v", err)
|
||||
return
|
||||
}
|
||||
|
||||
t.Logf("ok: %v", res)
|
||||
})
|
||||
}
|
||||
@@ -0,0 +1,91 @@
|
||||
package synologydsm
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"net/http"
|
||||
"net/url"
|
||||
"strconv"
|
||||
|
||||
qs "github.com/google/go-querystring/query"
|
||||
)
|
||||
|
||||
type LoginRequest struct {
|
||||
Account string `json:"account" url:"account"`
|
||||
Password string `json:"passwd" url:"passwd"`
|
||||
OtpCode string `json:"otp_code,omitempty" url:"otp_code,omitempty"`
|
||||
}
|
||||
|
||||
type LoginResponse struct {
|
||||
sdkResponseBase
|
||||
Data *struct {
|
||||
Sid string `json:"sid"`
|
||||
SynoToken string `json:"synotoken"`
|
||||
DeviceId string `json:"device_id,omitempty"`
|
||||
Did string `json:"did,omitempty"`
|
||||
} `json:"data,omitempty"`
|
||||
}
|
||||
|
||||
func (c *Client) Login(req *LoginRequest) (*LoginResponse, error) {
|
||||
const AUTH_API_NAME = "SYNO.API.Auth"
|
||||
if c.authApiPath == "" || c.authApiVersion == 0 {
|
||||
queryInfoReq := &QueryAPIInfoRequest{
|
||||
Query: AUTH_API_NAME,
|
||||
}
|
||||
queryInfoResp, err := c.QueryAPIInfo(queryInfoReq)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("sdkerr: failed to query API info: %w", err)
|
||||
} else {
|
||||
authApiInfo, ok := queryInfoResp.Data[AUTH_API_NAME]
|
||||
if !ok {
|
||||
return nil, fmt.Errorf("sdkerr: failed to query API info: \"%s\" not found", AUTH_API_NAME)
|
||||
}
|
||||
|
||||
c.authApiPath = authApiInfo.Path
|
||||
c.authApiVersion = authApiInfo.MaxVersion
|
||||
}
|
||||
}
|
||||
|
||||
params := url.Values{
|
||||
"api": {AUTH_API_NAME},
|
||||
"version": {strconv.Itoa(c.authApiVersion)},
|
||||
"method": {"login"},
|
||||
"format": {"sid"},
|
||||
"enable_syno_token": {"yes"},
|
||||
"enable_device_token": {"yes"},
|
||||
"device_name": {"Certimate"},
|
||||
}
|
||||
|
||||
values, err := qs.Values(req)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
for k := range values {
|
||||
params.Set(k, values.Get(k))
|
||||
}
|
||||
|
||||
httpreq, err := c.newRequest(http.MethodGet, fmt.Sprintf("/webapi/%s?%s", c.authApiPath, params.Encode()))
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
result := &LoginResponse{}
|
||||
if _, err := c.doRequestWithResult(httpreq, result); err != nil {
|
||||
if result != nil && result.GetErrorCode() > 0 {
|
||||
errcode := result.GetErrorCode()
|
||||
errdesc := getAuthErrorDescription(errcode)
|
||||
return result, fmt.Errorf("sdkerr: code='%d', desc='%s'", errcode, errdesc)
|
||||
}
|
||||
return result, err
|
||||
}
|
||||
|
||||
if result.Data.Sid == "" || result.Data.SynoToken == "" {
|
||||
return result, fmt.Errorf("sdkerr: login succeeded but the sid or synotoken is empty")
|
||||
}
|
||||
|
||||
c.synoTokenMtx.Lock()
|
||||
defer c.synoTokenMtx.Unlock()
|
||||
c.sid = result.Data.Sid
|
||||
c.synoToken = result.Data.SynoToken
|
||||
|
||||
return result, nil
|
||||
}
|
||||
@@ -0,0 +1,44 @@
|
||||
package synologydsm
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"net/http"
|
||||
"net/url"
|
||||
"strconv"
|
||||
)
|
||||
|
||||
type LogoutResponse struct {
|
||||
sdkResponseBase
|
||||
}
|
||||
|
||||
func (c *Client) Logout() (*LogoutResponse, error) {
|
||||
if c.sid == "" {
|
||||
result := &LogoutResponse{}
|
||||
result.Success = true
|
||||
return result, nil
|
||||
}
|
||||
|
||||
params := url.Values{
|
||||
"api": {"SYNO.API.Auth"},
|
||||
"version": {strconv.Itoa(c.authApiVersion)},
|
||||
"method": {"logout"},
|
||||
"_sid": {c.sid},
|
||||
}
|
||||
|
||||
httpreq, err := c.newRequest(http.MethodGet, fmt.Sprintf("/webapi/%s?%s", c.authApiPath, params.Encode()))
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
result := &LogoutResponse{}
|
||||
if _, err := c.doRequestWithResult(httpreq, result); err != nil {
|
||||
return result, err
|
||||
}
|
||||
|
||||
c.synoTokenMtx.Lock()
|
||||
defer c.synoTokenMtx.Unlock()
|
||||
c.sid = ""
|
||||
c.synoToken = ""
|
||||
|
||||
return result, nil
|
||||
}
|
||||
@@ -0,0 +1,37 @@
|
||||
package synologydsm
|
||||
|
||||
import (
|
||||
"net/http"
|
||||
"net/url"
|
||||
)
|
||||
|
||||
type ListCertificatesResponse struct {
|
||||
sdkResponseBase
|
||||
Data *struct {
|
||||
Certificates []*CertificateInfo `json:"certificates"`
|
||||
} `json:"data,omitempty"`
|
||||
}
|
||||
|
||||
func (c *Client) ListCertificates() (*ListCertificatesResponse, error) {
|
||||
params := url.Values{
|
||||
"api": {"SYNO.Core.Certificate.CRT"},
|
||||
"method": {"list"},
|
||||
"version": {"1"},
|
||||
"_sid": {c.sid},
|
||||
}
|
||||
|
||||
httpreq, err := c.newRequest(http.MethodPost, "/webapi/entry.cgi")
|
||||
if err != nil {
|
||||
return nil, err
|
||||
} else {
|
||||
httpreq.SetHeader("Content-Type", "application/x-www-form-urlencoded")
|
||||
httpreq.SetFormDataFromValues(params)
|
||||
}
|
||||
|
||||
result := &ListCertificatesResponse{}
|
||||
if _, err := c.doRequestWithResult(httpreq, result); err != nil {
|
||||
return result, err
|
||||
}
|
||||
|
||||
return result, nil
|
||||
}
|
||||
@@ -0,0 +1,55 @@
|
||||
package synologydsm
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"net/http"
|
||||
"net/url"
|
||||
"strings"
|
||||
)
|
||||
|
||||
type ImportCertificateRequest struct {
|
||||
ID string `json:"id" url:"id"`
|
||||
Description string `json:"desc" url:"desc"`
|
||||
Key string `json:"key" url:"key"`
|
||||
Cert string `json:"cert" url:"cert"`
|
||||
InterCert string `json:"inter_cert" url:"inter_cert"`
|
||||
AsDefault bool `json:"as_default" url:"as_default"`
|
||||
}
|
||||
|
||||
type ImportCertificateResponse struct {
|
||||
sdkResponseBase
|
||||
Data *struct {
|
||||
RestartHttpd bool `json:"restart_httpd"`
|
||||
} `json:"data,omitempty"`
|
||||
}
|
||||
|
||||
func (c *Client) ImportCertificate(req *ImportCertificateRequest) (*ImportCertificateResponse, error) {
|
||||
params := url.Values{
|
||||
"api": {"SYNO.Core.Certificate"},
|
||||
"method": {"import"},
|
||||
"version": {"1"},
|
||||
"_sid": {c.sid},
|
||||
"SynoToken": {c.synoToken},
|
||||
}
|
||||
|
||||
httpreq, err := c.newRequest(http.MethodPost, fmt.Sprintf("/webapi/entry.cgi?%s", params.Encode()))
|
||||
if err != nil {
|
||||
return nil, err
|
||||
} else {
|
||||
httpreq.SetMultipartField("key", "key.pem", "text/plain", strings.NewReader(req.Key))
|
||||
httpreq.SetMultipartField("cert", "cert.pem", "text/plain", strings.NewReader(req.Cert))
|
||||
httpreq.SetMultipartField("inter_cert", "chain.pem", "text/plain", strings.NewReader(req.InterCert))
|
||||
httpreq.SetMultipartField("id", "", "", strings.NewReader(req.ID))
|
||||
httpreq.SetMultipartField("desc", "", "", strings.NewReader(req.Description))
|
||||
if req.AsDefault {
|
||||
httpreq.SetMultipartField("as_default", "", "", strings.NewReader("true"))
|
||||
}
|
||||
}
|
||||
|
||||
result := &ImportCertificateResponse{}
|
||||
if _, err := c.doRequestWithResult(httpreq, result); err != nil {
|
||||
return result, err
|
||||
}
|
||||
|
||||
return result, nil
|
||||
}
|
||||
@@ -0,0 +1,47 @@
|
||||
package synologydsm
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"net/http"
|
||||
"net/url"
|
||||
)
|
||||
|
||||
type ServiceCertificateSetting struct {
|
||||
Service *CertificateService `json:"service"`
|
||||
OldCertID string `json:"old_id"`
|
||||
CertID string `json:"id"`
|
||||
}
|
||||
|
||||
type SetServiceCertificateRequest struct {
|
||||
Settings []*ServiceCertificateSetting `json:"settings"`
|
||||
}
|
||||
|
||||
type SetServiceCertificateResponse struct {
|
||||
sdkResponseBase
|
||||
}
|
||||
|
||||
func (c *Client) SetServiceCertificate(req *SetServiceCertificateRequest) (*SetServiceCertificateResponse, error) {
|
||||
bsettings, _ := json.Marshal(req.Settings)
|
||||
params := url.Values{
|
||||
"api": {"SYNO.Core.Certificate.Service"},
|
||||
"method": {"set"},
|
||||
"version": {"1"},
|
||||
"settings": {string(bsettings)},
|
||||
}
|
||||
|
||||
httpreq, err := c.newRequest(http.MethodPost, fmt.Sprintf("/webapi/entry.cgi?_sid=%s", c.sid))
|
||||
if err != nil {
|
||||
return nil, err
|
||||
} else {
|
||||
httpreq.SetHeader("Content-Type", "application/x-www-form-urlencoded")
|
||||
httpreq.SetFormDataFromValues(params)
|
||||
}
|
||||
|
||||
result := &SetServiceCertificateResponse{}
|
||||
if _, err := c.doRequestWithResult(httpreq, result); err != nil {
|
||||
return result, err
|
||||
}
|
||||
|
||||
return result, nil
|
||||
}
|
||||
@@ -0,0 +1,46 @@
|
||||
package synologydsm
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"net/http"
|
||||
"net/url"
|
||||
|
||||
qs "github.com/google/go-querystring/query"
|
||||
)
|
||||
|
||||
type QueryAPIInfoRequest struct {
|
||||
Query string `json:"query" url:"query"`
|
||||
}
|
||||
|
||||
type QueryAPIInfoResponse struct {
|
||||
sdkResponseBase
|
||||
Data map[string]APIInfo `json:"data,omitempty"`
|
||||
}
|
||||
|
||||
func (c *Client) QueryAPIInfo(req *QueryAPIInfoRequest) (*QueryAPIInfoResponse, error) {
|
||||
params := url.Values{
|
||||
"api": {"SYNO.API.Info"},
|
||||
"version": {"1"},
|
||||
"method": {"query"},
|
||||
}
|
||||
|
||||
values, err := qs.Values(req)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
for k := range values {
|
||||
params.Set(k, values.Get(k))
|
||||
}
|
||||
|
||||
httpreq, err := c.newRequest(http.MethodGet, fmt.Sprintf("/webapi/query.cgi?%s", params.Encode()))
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
result := &QueryAPIInfoResponse{}
|
||||
if _, err := c.doRequestWithResult(httpreq, result); err != nil {
|
||||
return result, err
|
||||
}
|
||||
|
||||
return result, nil
|
||||
}
|
||||
@@ -0,0 +1,116 @@
|
||||
package synologydsm
|
||||
|
||||
import (
|
||||
"crypto/tls"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"net/http"
|
||||
"net/url"
|
||||
"strings"
|
||||
"sync"
|
||||
"time"
|
||||
|
||||
"github.com/go-resty/resty/v2"
|
||||
|
||||
"github.com/certimate-go/certimate/internal/app"
|
||||
)
|
||||
|
||||
type Client struct {
|
||||
authApiPath string
|
||||
authApiVersion int
|
||||
|
||||
sid string
|
||||
synoToken string
|
||||
synoTokenMtx sync.Mutex
|
||||
|
||||
client *resty.Client
|
||||
}
|
||||
|
||||
func NewClient(serverUrl string) (*Client, error) {
|
||||
if serverUrl == "" {
|
||||
return nil, fmt.Errorf("sdkerr: unset serverUrl")
|
||||
}
|
||||
if _, err := url.Parse(serverUrl); err != nil {
|
||||
return nil, fmt.Errorf("sdkerr: invalid serverUrl: %w", err)
|
||||
}
|
||||
|
||||
client := &Client{}
|
||||
client.client = resty.New().
|
||||
SetBaseURL(strings.TrimRight(serverUrl, "/")).
|
||||
SetHeader("User-Agent", app.AppUserAgent).
|
||||
SetPreRequestHook(func(c *resty.Client, req *http.Request) error {
|
||||
if client.synoToken != "" {
|
||||
req.Header.Set("X-SYNO-TOKEN", client.synoToken)
|
||||
}
|
||||
|
||||
return nil
|
||||
})
|
||||
|
||||
return client, nil
|
||||
}
|
||||
|
||||
func (c *Client) SetTimeout(timeout time.Duration) *Client {
|
||||
c.client.SetTimeout(timeout)
|
||||
return c
|
||||
}
|
||||
|
||||
func (c *Client) SetTLSConfig(config *tls.Config) *Client {
|
||||
c.client.SetTLSClientConfig(config)
|
||||
return c
|
||||
}
|
||||
|
||||
func (c *Client) newRequest(method string, path string) (*resty.Request, error) {
|
||||
if method == "" {
|
||||
return nil, fmt.Errorf("sdkerr: unset method")
|
||||
}
|
||||
if path == "" {
|
||||
return nil, fmt.Errorf("sdkerr: unset path")
|
||||
}
|
||||
|
||||
req := c.client.R()
|
||||
req.Method = method
|
||||
req.URL = path
|
||||
return req, nil
|
||||
}
|
||||
|
||||
func (c *Client) doRequest(req *resty.Request) (*resty.Response, error) {
|
||||
if req == nil {
|
||||
return nil, fmt.Errorf("sdkerr: nil request")
|
||||
}
|
||||
|
||||
// WARN:
|
||||
// PLEASE DO NOT USE `req.SetResult` or `req.SetError` HERE! USE `doRequestWithResult` INSTEAD.
|
||||
|
||||
resp, err := req.Send()
|
||||
if err != nil {
|
||||
return resp, fmt.Errorf("sdkerr: failed to send request: %w", err)
|
||||
} else if resp.IsError() {
|
||||
return resp, fmt.Errorf("sdkerr: unexpected status code: %d (resp: %s)", resp.StatusCode(), resp.String())
|
||||
}
|
||||
|
||||
return resp, nil
|
||||
}
|
||||
|
||||
func (c *Client) doRequestWithResult(req *resty.Request, res sdkResponse) (*resty.Response, error) {
|
||||
if req == nil {
|
||||
return nil, fmt.Errorf("sdkerr: nil request")
|
||||
}
|
||||
|
||||
resp, err := c.doRequest(req)
|
||||
if err != nil {
|
||||
if resp != nil {
|
||||
json.Unmarshal(resp.Body(), &res)
|
||||
}
|
||||
return resp, err
|
||||
}
|
||||
|
||||
if err := json.Unmarshal(resp.Body(), &res); err != nil {
|
||||
return resp, fmt.Errorf("sdkerr: failed to unmarshal response: %w (resp: %s)", err, resp.String())
|
||||
} else {
|
||||
if tsuccess := res.GetSuccess(); !tsuccess {
|
||||
return resp, fmt.Errorf("sdkerr: code='%d'", res.GetErrorCode())
|
||||
}
|
||||
}
|
||||
|
||||
return resp, nil
|
||||
}
|
||||
@@ -0,0 +1,70 @@
|
||||
package synologydsm
|
||||
|
||||
type sdkResponse interface {
|
||||
GetSuccess() bool
|
||||
GetErrorCode() int
|
||||
}
|
||||
|
||||
type sdkResponseBase struct {
|
||||
Success bool `json:"success"`
|
||||
Error *APIError `json:"error,omitempty"`
|
||||
}
|
||||
|
||||
func (r *sdkResponseBase) GetSuccess() bool {
|
||||
return r.Success
|
||||
}
|
||||
|
||||
func (r *sdkResponseBase) GetErrorCode() int {
|
||||
if r.Error == nil {
|
||||
if r.Success {
|
||||
return 0
|
||||
}
|
||||
return -1
|
||||
}
|
||||
|
||||
return r.Error.Code
|
||||
}
|
||||
|
||||
var _ sdkResponse = (*sdkResponseBase)(nil)
|
||||
|
||||
type APIError struct {
|
||||
Code int `json:"code,omitempty"`
|
||||
}
|
||||
|
||||
type APIInfo struct {
|
||||
Path string `json:"path"`
|
||||
MinVersion int `json:"minVersion"`
|
||||
MaxVersion int `json:"maxVersion"`
|
||||
}
|
||||
|
||||
type CertificateInfo struct {
|
||||
ID string `json:"id"`
|
||||
Description string `json:"desc"`
|
||||
IsDefault bool `json:"is_default"`
|
||||
IsBroken bool `json:"is_broken"`
|
||||
Issuer struct {
|
||||
CommonName string `json:"common_name"`
|
||||
Country string `json:"country"`
|
||||
Organization string `json:"organization"`
|
||||
} `json:"issuer"`
|
||||
Subject struct {
|
||||
CommonName string `json:"common_name"`
|
||||
Country string `json:"country"`
|
||||
Organization string `json:"organization"`
|
||||
SAN []string `json:"sub_alt_name"`
|
||||
} `json:"subject"`
|
||||
ValidFrom string `json:"valid_from"`
|
||||
ValidTill string `json:"valid_till"`
|
||||
SignatureAlgorithm string `json:"signature_algorithm"`
|
||||
Renewable bool `json:"renewable"`
|
||||
Services []*CertificateService `json:"services"`
|
||||
}
|
||||
|
||||
type CertificateService struct {
|
||||
DisplayName string `json:"display_name"`
|
||||
DisplayNameI18N string `json:"display_name_i18n,omitempty"`
|
||||
IsPkg bool `json:"isPkg"`
|
||||
Owner string `json:"owner"`
|
||||
Service string `json:"service"`
|
||||
Subscriber string `json:"subscriber"`
|
||||
}
|
||||
@@ -0,0 +1,46 @@
|
||||
package synologydsm
|
||||
|
||||
func getAuthErrorDescription(code int) string {
|
||||
switch code {
|
||||
case 100:
|
||||
return "Unknown error"
|
||||
case 101:
|
||||
return "Invalid parameters"
|
||||
case 102:
|
||||
return "API does not exist"
|
||||
case 103:
|
||||
return "Method does not exist"
|
||||
case 104:
|
||||
return "This API version is not supported"
|
||||
case 105:
|
||||
return "Insufficient user privilege"
|
||||
case 106:
|
||||
return "Connection time out"
|
||||
case 107:
|
||||
return "Multiple login detected"
|
||||
case 400:
|
||||
return "Invalid password or account does not exist"
|
||||
case 401:
|
||||
return "Guest or disabled account"
|
||||
case 402:
|
||||
return "Permission denied"
|
||||
case 403:
|
||||
return "2-factor authentication code required (OTP)"
|
||||
case 404:
|
||||
return "Failed to authenticate 2-factor authentication code"
|
||||
case 405:
|
||||
return "Server version is too low or not supported"
|
||||
case 406:
|
||||
return "2-factor authentication code expired"
|
||||
case 407:
|
||||
return "Login failed: IP has been blocked"
|
||||
case 408:
|
||||
return "Expired password"
|
||||
case 409:
|
||||
return "Password must be changed (password policy)"
|
||||
case 410:
|
||||
return "Account locked (too many failed login attempts)"
|
||||
default:
|
||||
return "Unknown authentication error"
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,88 @@
|
||||
package wait
|
||||
|
||||
import (
|
||||
"context"
|
||||
"time"
|
||||
)
|
||||
|
||||
// 等待,直到指定时间。
|
||||
//
|
||||
// 入参:
|
||||
// - wait: 等待时间。
|
||||
//
|
||||
// 出参:
|
||||
// - err: 错误。
|
||||
func Delay(wait time.Duration) error {
|
||||
return DelayWithContext(context.Background(), wait)
|
||||
}
|
||||
|
||||
// 等待,直到指定时间,或上下文被取消。
|
||||
//
|
||||
// 入参:
|
||||
// - ctx: 上下文。
|
||||
// - wait: 等待时间。
|
||||
//
|
||||
// 出参:
|
||||
// - err: 错误。
|
||||
func DelayWithContext(ctx context.Context, wait time.Duration) error {
|
||||
ticker := time.NewTimer(wait)
|
||||
defer ticker.Stop()
|
||||
|
||||
select {
|
||||
case <-ctx.Done():
|
||||
return ctx.Err()
|
||||
case <-ticker.C:
|
||||
return nil
|
||||
}
|
||||
}
|
||||
|
||||
// 等待,直到条件满足或超时。
|
||||
//
|
||||
// 入参:
|
||||
// - condition: 条件函数,接收尝试次数作为参数,返回是否满足条件和错误。
|
||||
// - timeout: 超时时间。
|
||||
// - interval: 执行条件函数的间隔时间。
|
||||
//
|
||||
// 出参:
|
||||
// - ret: 是否满足条件。
|
||||
// - err: 错误。
|
||||
func For(condition func(int) (bool, error), timeout time.Duration, interval time.Duration) (bool, error) {
|
||||
conditionWithContext := func(_ context.Context, i int) (bool, error) {
|
||||
return condition(i)
|
||||
}
|
||||
return ForWithContext(context.Background(), conditionWithContext, timeout, interval)
|
||||
}
|
||||
|
||||
// 等待,直到条件满足或超时,或上下文被取消。
|
||||
//
|
||||
// 入参:
|
||||
// - ctx: 上下文。
|
||||
// - condition: 条件函数,接收上下文和尝试次数作为参数,返回是否满足条件和错误。
|
||||
// - timeout: 超时时间。
|
||||
// - interval: 执行条件函数的间隔时间。
|
||||
//
|
||||
// 出参:
|
||||
// - ret: 是否满足条件。
|
||||
// - err: 错误。
|
||||
func ForWithContext(ctx context.Context, condition func(context.Context, int) (bool, error), timeout time.Duration, interval time.Duration) (bool, error) {
|
||||
ctxWithTimeout, cancel := context.WithTimeout(ctx, timeout)
|
||||
defer cancel()
|
||||
|
||||
ticker := time.NewTicker(interval)
|
||||
defer ticker.Stop()
|
||||
|
||||
attempt := 0
|
||||
for {
|
||||
select {
|
||||
case <-ctxWithTimeout.Done():
|
||||
return false, ctxWithTimeout.Err()
|
||||
|
||||
case <-ticker.C:
|
||||
attempt++
|
||||
ret, err := condition(ctxWithTimeout, attempt)
|
||||
if ret || err != nil {
|
||||
return ret, err
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
Binary file not shown.
|
After Width: | Height: | Size: 3.7 KiB |
@@ -91,6 +91,7 @@ import AccessConfigFieldsProviderSlackBot from "./AccessConfigFieldsProviderSlac
|
||||
import AccessConfigFieldsProviderSpaceship from "./AccessConfigFieldsProviderSpaceship";
|
||||
import AccessConfigFieldsProviderSSH from "./AccessConfigFieldsProviderSSH";
|
||||
import AccessConfigFieldsProviderSSLCom from "./AccessConfigFieldsProviderSSLCom";
|
||||
import AccessConfigFieldsProviderSynologyDSM from "./AccessConfigFieldsProviderSynologyDSM";
|
||||
import AccessConfigFieldsProviderTechnitiumDNS from "./AccessConfigFieldsProviderTechnitiumDNS";
|
||||
import AccessConfigFieldsProviderTelegramBot from "./AccessConfigFieldsProviderTelegramBot";
|
||||
import AccessConfigFieldsProviderTencentCloud from "./AccessConfigFieldsProviderTencentCloud";
|
||||
@@ -201,6 +202,7 @@ const providerComponentMap: Partial<Record<AccessProviderType, React.ComponentTy
|
||||
[ACCESS_PROVIDERS.SPACESHIP]: AccessConfigFieldsProviderSpaceship,
|
||||
[ACCESS_PROVIDERS.SSLCOM]: AccessConfigFieldsProviderSSLCom,
|
||||
[ACCESS_PROVIDERS.SSH]: AccessConfigFieldsProviderSSH,
|
||||
[ACCESS_PROVIDERS.SYNOLOGYDSM]: AccessConfigFieldsProviderSynologyDSM,
|
||||
[ACCESS_PROVIDERS.TECHNITIUMDNS]: AccessConfigFieldsProviderTechnitiumDNS,
|
||||
[ACCESS_PROVIDERS.TELEGRAMBOT]: AccessConfigFieldsProviderTelegramBot,
|
||||
[ACCESS_PROVIDERS.TENCENTCLOUD]: AccessConfigFieldsProviderTencentCloud,
|
||||
|
||||
@@ -0,0 +1,101 @@
|
||||
import { getI18n, useTranslation } from "react-i18next";
|
||||
import { Form, Input, Switch } from "antd";
|
||||
import { createSchemaFieldRule } from "antd-zod";
|
||||
import { z } from "zod";
|
||||
|
||||
import { useFormNestedFieldsContext } from "./_context";
|
||||
|
||||
const AccessConfigFieldsProviderSynologyDSM = () => {
|
||||
const { i18n, t } = useTranslation();
|
||||
|
||||
const { parentNamePath } = useFormNestedFieldsContext();
|
||||
const formSchema = z.object({
|
||||
[parentNamePath]: getSchema({ i18n }),
|
||||
});
|
||||
const formRule = createSchemaFieldRule(formSchema);
|
||||
const initialValues = getInitialValues();
|
||||
|
||||
return (
|
||||
<>
|
||||
<Form.Item
|
||||
name={[parentNamePath, "serverUrl"]}
|
||||
initialValue={initialValues.serverUrl}
|
||||
label={t("access.form.synologydsm_server_url.label")}
|
||||
rules={[formRule]}
|
||||
>
|
||||
<Input type="url" placeholder={t("access.form.synologydsm_server_url.placeholder")} />
|
||||
</Form.Item>
|
||||
|
||||
<Form.Item
|
||||
name={[parentNamePath, "username"]}
|
||||
initialValue={initialValues.username}
|
||||
label={t("access.form.synologydsm_username.label")}
|
||||
rules={[formRule]}
|
||||
>
|
||||
<Input autoComplete="new-password" placeholder={t("access.form.synologydsm_username.placeholder")} />
|
||||
</Form.Item>
|
||||
|
||||
<Form.Item
|
||||
name={[parentNamePath, "password"]}
|
||||
initialValue={initialValues.password}
|
||||
label={t("access.form.synologydsm_password.label")}
|
||||
rules={[formRule]}
|
||||
>
|
||||
<Input.Password autoComplete="new-password" placeholder={t("access.form.synologydsm_password.placeholder")} />
|
||||
</Form.Item>
|
||||
|
||||
<Form.Item
|
||||
name={[parentNamePath, "totpSecret"]}
|
||||
initialValue={initialValues.totpSecret}
|
||||
label={t("access.form.synologydsm_totp_secret.label")}
|
||||
extra={t("access.form.synologydsm_totp_secret.help")}
|
||||
rules={[formRule]}
|
||||
tooltip={<span dangerouslySetInnerHTML={{ __html: t("access.form.synologydsm_totp_secret.tooltip") }}></span>}
|
||||
>
|
||||
<Input.Password autoComplete="new-password" placeholder={t("access.form.synologydsm_totp_secret.placeholder")} />
|
||||
</Form.Item>
|
||||
|
||||
<Form.Item
|
||||
name={[parentNamePath, "allowInsecureConnections"]}
|
||||
initialValue={initialValues.allowInsecureConnections}
|
||||
rules={[formRule]}
|
||||
label={t("access.form.shared_allow_insecure_conns.label")}
|
||||
>
|
||||
<Switch />
|
||||
</Form.Item>
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
const getInitialValues = (): Nullish<z.infer<ReturnType<typeof getSchema>>> => {
|
||||
return {
|
||||
serverUrl: "http://<your-host-addr>:5000/",
|
||||
username: "",
|
||||
password: "",
|
||||
};
|
||||
};
|
||||
|
||||
const getSchema = ({ i18n = getI18n() }: { i18n: ReturnType<typeof getI18n> }) => {
|
||||
const { t } = i18n;
|
||||
|
||||
return z.object({
|
||||
serverUrl: z.url(t("common.errmsg.url_invalid")),
|
||||
username: z.string().nonempty(t("access.form.synologydsm_username.placeholder")),
|
||||
password: z.string().nonempty(t("access.form.synologydsm_password.placeholder")),
|
||||
totpSecret: z
|
||||
.string()
|
||||
.nullish()
|
||||
.refine((v) => {
|
||||
if (!v) return true;
|
||||
return /^[A-Z2-7]{16,32}$/.test(v);
|
||||
}),
|
||||
allowInsecureConnections: z.boolean().nullish(),
|
||||
});
|
||||
};
|
||||
|
||||
const _default = Object.assign(AccessConfigFieldsProviderSynologyDSM, {
|
||||
getInitialValues,
|
||||
getSchema,
|
||||
});
|
||||
|
||||
export default _default;
|
||||
@@ -72,6 +72,7 @@ import BizDeployNodeConfigFieldsProviderRainYunRCDN from "./BizDeployNodeConfigF
|
||||
import BizDeployNodeConfigFieldsProviderRatPanel from "./BizDeployNodeConfigFieldsProviderRatPanel.tsx";
|
||||
import BizDeployNodeConfigFieldsProviderSafeLine from "./BizDeployNodeConfigFieldsProviderSafeLine.tsx";
|
||||
import BizDeployNodeConfigFieldsProviderSSH from "./BizDeployNodeConfigFieldsProviderSSH";
|
||||
import BizDeployNodeConfigFieldsProviderSynologyDSM from "./BizDeployNodeConfigFieldsProviderSynologyDSM";
|
||||
import BizDeployNodeConfigFieldsProviderTencentCloudCDN from "./BizDeployNodeConfigFieldsProviderTencentCloudCDN";
|
||||
import BizDeployNodeConfigFieldsProviderTencentCloudCLB from "./BizDeployNodeConfigFieldsProviderTencentCloudCLB";
|
||||
import BizDeployNodeConfigFieldsProviderTencentCloudCOS from "./BizDeployNodeConfigFieldsProviderTencentCloudCOS";
|
||||
@@ -184,6 +185,7 @@ const providerComponentMap: Partial<Record<DeploymentProviderType, React.Compone
|
||||
[DEPLOYMENT_PROVIDERS.RATPANEL]: BizDeployNodeConfigFieldsProviderRatPanel,
|
||||
[DEPLOYMENT_PROVIDERS.SAFELINE]: BizDeployNodeConfigFieldsProviderSafeLine,
|
||||
[DEPLOYMENT_PROVIDERS.SSH]: BizDeployNodeConfigFieldsProviderSSH,
|
||||
[DEPLOYMENT_PROVIDERS.SYNOLOGYDSM]: BizDeployNodeConfigFieldsProviderSynologyDSM,
|
||||
[DEPLOYMENT_PROVIDERS.TENCENTCLOUD_CDN]: BizDeployNodeConfigFieldsProviderTencentCloudCDN,
|
||||
[DEPLOYMENT_PROVIDERS.TENCENTCLOUD_CLB]: BizDeployNodeConfigFieldsProviderTencentCloudCLB,
|
||||
[DEPLOYMENT_PROVIDERS.TENCENTCLOUD_COS]: BizDeployNodeConfigFieldsProviderTencentCloudCOS,
|
||||
|
||||
+61
@@ -0,0 +1,61 @@
|
||||
import { getI18n, useTranslation } from "react-i18next";
|
||||
import { Form, Input, Switch } from "antd";
|
||||
import { createSchemaFieldRule } from "antd-zod";
|
||||
import { z } from "zod";
|
||||
|
||||
import { useFormNestedFieldsContext } from "./_context";
|
||||
|
||||
const BizDeployNodeConfigFieldsProviderSynologyDSM = () => {
|
||||
const { i18n, t } = useTranslation();
|
||||
|
||||
const { parentNamePath } = useFormNestedFieldsContext();
|
||||
const formSchema = z.object({
|
||||
[parentNamePath]: getSchema({ i18n }),
|
||||
});
|
||||
const formRule = createSchemaFieldRule(formSchema);
|
||||
const initialValues = getInitialValues();
|
||||
|
||||
return (
|
||||
<>
|
||||
<Form.Item
|
||||
name={[parentNamePath, "certificateIdOrDesc"]}
|
||||
initialValue={initialValues.certificateIdOrDesc}
|
||||
label={t("workflow_node.deploy.form.synologydsm_certificate_id_or_desc.label")}
|
||||
extra={t("workflow_node.deploy.form.synologydsm_certificate_id_or_desc.help")}
|
||||
tooltip={<span dangerouslySetInnerHTML={{ __html: t("workflow_node.deploy.form.synologydsm_certificate_id_or_desc.tooltip") }}></span>}
|
||||
rules={[formRule]}
|
||||
>
|
||||
<Input placeholder={t("workflow_node.deploy.form.synologydsm_certificate_id_or_desc.placeholder")} />
|
||||
</Form.Item>
|
||||
|
||||
<Form.Item
|
||||
name={[parentNamePath, "isDefault"]}
|
||||
initialValue={initialValues.isDefault}
|
||||
label={t("workflow_node.deploy.form.synologydsm_is_default.label")}
|
||||
rules={[formRule]}
|
||||
>
|
||||
<Switch />
|
||||
</Form.Item>
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
const getInitialValues = (): Nullish<z.infer<ReturnType<typeof getSchema>>> => {
|
||||
return {
|
||||
isDefault: true,
|
||||
};
|
||||
};
|
||||
|
||||
const getSchema = ({ i18n: _i18n = getI18n() }: { i18n?: ReturnType<typeof getI18n> }) => {
|
||||
return z.object({
|
||||
certificateIdOrDesc: z.string().nullish(),
|
||||
isDefault: z.boolean().nullish(),
|
||||
});
|
||||
};
|
||||
|
||||
const _default = Object.assign(BizDeployNodeConfigFieldsProviderSynologyDSM, {
|
||||
getInitialValues,
|
||||
getSchema,
|
||||
});
|
||||
|
||||
export default _default;
|
||||
@@ -107,6 +107,7 @@ export const ACCESS_PROVIDERS = Object.freeze({
|
||||
SPACESHIP: "spaceship",
|
||||
SSH: "ssh",
|
||||
SSLCOM: "sslcom",
|
||||
SYNOLOGYDSM: "synologydsm",
|
||||
TECHNITIUMDNS: "technitiumdns",
|
||||
TELEGRAMBOT: "telegrambot",
|
||||
TENCENTCLOUD: "tencentcloud",
|
||||
@@ -193,6 +194,7 @@ export const accessProvidersMap: Map<AccessProvider["type"] | string, AccessProv
|
||||
[ACCESS_PROVIDERS.PROXMOXVE, "provider.proxmoxve", "/imgs/providers/proxmoxve.svg", [ACCESS_USAGES.HOSTING]],
|
||||
[ACCESS_PROVIDERS.RATPANEL, "provider.ratpanel", "/imgs/providers/ratpanel.png", [ACCESS_USAGES.HOSTING]],
|
||||
[ACCESS_PROVIDERS.SAFELINE, "provider.safeline", "/imgs/providers/safeline.svg", [ACCESS_USAGES.HOSTING]],
|
||||
[ACCESS_PROVIDERS.SYNOLOGYDSM, "provider.synologydsm", "/imgs/providers/synologydsm.png", [ACCESS_USAGES.HOSTING]],
|
||||
|
||||
[ACCESS_PROVIDERS.AKAMAI, "provider.akamai", "/imgs/providers/akamai.svg", [ACCESS_USAGES.DNS]],
|
||||
[ACCESS_PROVIDERS.ARVANCLOUD, "provider.arvancloud", "/imgs/providers/arvancloud.svg", [ACCESS_USAGES.DNS]],
|
||||
@@ -615,6 +617,7 @@ export const DEPLOYMENT_PROVIDERS = Object.freeze({
|
||||
RATPANEL_CONSOLE: `${ACCESS_PROVIDERS.RATPANEL}-console`,
|
||||
SAFELINE: `${ACCESS_PROVIDERS.SAFELINE}`,
|
||||
SSH: `${ACCESS_PROVIDERS.SSH}`,
|
||||
SYNOLOGYDSM: `${ACCESS_PROVIDERS.SYNOLOGYDSM}`,
|
||||
TENCENTCLOUD_CDN: `${ACCESS_PROVIDERS.TENCENTCLOUD}-cdn`,
|
||||
TENCENTCLOUD_CLB: `${ACCESS_PROVIDERS.TENCENTCLOUD}-clb`,
|
||||
TENCENTCLOUD_COS: `${ACCESS_PROVIDERS.TENCENTCLOUD}-cos`,
|
||||
@@ -796,6 +799,7 @@ export const deploymentProvidersMap: Map<DeploymentProvider["type"] | string, De
|
||||
[DEPLOYMENT_PROVIDERS.DOKPLOY, "provider.dokploy", DEPLOYMENT_CATEGORIES.WEBSITE],
|
||||
[DEPLOYMENT_PROVIDERS.NGINXPROXYMANAGER, "provider.nginxproxymanager", DEPLOYMENT_CATEGORIES.WEBSITE],
|
||||
[DEPLOYMENT_PROVIDERS.PROXMOXVE, "provider.proxmoxve", DEPLOYMENT_CATEGORIES.OTHER],
|
||||
[DEPLOYMENT_PROVIDERS.SYNOLOGYDSM, "provider.synologydsm", DEPLOYMENT_CATEGORIES.OTHER],
|
||||
] satisfies Array<[DeploymentProviderType, string, DeploymentCategoryType, "builtin"] | [DeploymentProviderType, string, DeploymentCategoryType]>
|
||||
).map(([type, name, category, builtin]) => [
|
||||
type,
|
||||
|
||||
@@ -517,6 +517,17 @@
|
||||
"access.form.proxmoxve_api_token_secret.label": "Proxmox VE API token secret (Optional)",
|
||||
"access.form.proxmoxve_api_token_secret.placeholder": "Please enter Proxmox VE API token secret",
|
||||
"access.form.proxmoxve_api_token_secret.tooltip": "For more information, see <a href=\"https://pve.proxmox.com/pve-docs/pve-admin-guide.html#pveum_tokens\" target=\"_blank\">https://pve.proxmox.com/pve-docs/pve-admin-guide.html#pveum_tokens</a>",
|
||||
"access.form.synologydsm_server_url.label": "Synology DSM server URL",
|
||||
"access.form.synologydsm_server_url.placeholder": "http://192.168.1.100:5000",
|
||||
"access.form.synologydsm_server_url.tooltip": "Format: http(s)://IP-or-hostname:port. Default port is 5000 for HTTP, 5001 for HTTPS. Example: http://192.168.1.100:5000 or https://nas.example.com:5001",
|
||||
"access.form.synologydsm_username.label": "Synology DSM username",
|
||||
"access.form.synologydsm_username.placeholder": "Please enter Synology DSM username",
|
||||
"access.form.synologydsm_password.label": "Synology DSM password",
|
||||
"access.form.synologydsm_password.placeholder": "Please enter Synology DSM password",
|
||||
"access.form.synologydsm_totp_secret.label": "Synology DSM 2FA TOTP secret key (Optional)",
|
||||
"access.form.synologydsm_totp_secret.placeholder": "Please enter Synology DSM 2FA TOTP secret key",
|
||||
"access.form.synologydsm_totp_secret.help": "Notes: Only required when you set up 2FA sign-in to Synology DSM.",
|
||||
"access.form.synologydsm_totp_secret.tooltip": "How to get it? The secret key can be found by clicking \"Can't scan it\" when setting up 2FA, or by using a QRCode scanning tool.",
|
||||
"access.form.qingcloud_access_key_id.label": "QingCloud AccessKeyID",
|
||||
"access.form.qingcloud_access_key_id.placeholder": "Please enter QingCloud AccessKeyID",
|
||||
"access.form.qingcloud_access_key_id.tooltip": "For more information, see <a href=\"https://console.qingcloud.com/access_keys/\" target=\"_blank\">https://console.qingcloud.com/access_keys/</a>",
|
||||
|
||||
@@ -165,6 +165,7 @@
|
||||
"provider.spaceship": "Spaceship",
|
||||
"provider.ssh": "Remote host (SSH)",
|
||||
"provider.sslcom": "SSL.com",
|
||||
"provider.synologydsm": "Synology DSM",
|
||||
"provider.technitiumdns": "Technitium DNS",
|
||||
"provider.telegrambot": "Telegram Bot",
|
||||
"provider.tencentcloud": "Tencent Cloud",
|
||||
|
||||
@@ -750,6 +750,11 @@
|
||||
"workflow_node.deploy.form.proxmoxve_node_name.label": "Proxmox VE cluster node name",
|
||||
"workflow_node.deploy.form.proxmoxve_node_name.placeholder": "Please enter Proxmox VE cluster node name",
|
||||
"workflow_node.deploy.form.proxmoxve_auto_restart.label": "Auto restart Proxmox VE after deployment",
|
||||
"workflow_node.deploy.form.synologydsm_certificate_id_or_desc.label": "Synology DSM certificate ID or description (Optional)",
|
||||
"workflow_node.deploy.form.synologydsm_certificate_id_or_desc.placeholder": "Please enter Synology DSM certificate ID or description",
|
||||
"workflow_node.deploy.form.synologydsm_certificate_id_or_desc.help": "Notes: Leave it blank to import a new certificate; otherwise, to replace the existing one.",
|
||||
"workflow_node.deploy.form.synologydsm_certificate_id_or_desc.tooltip": "You can find it on Synology DSM dashboard. <br><br>(Please open the browser devtools, and visit the \"Control Panel -> Security -> Certificate\" page to capture network requests, then you can get the certificate ID.)",
|
||||
"workflow_node.deploy.form.synologydsm_is_default.label": "Set as default certificate and applied to all DSM services",
|
||||
"workflow_node.deploy.form.qiniu_cdn_domain.label": "Qiniu CDN domain",
|
||||
"workflow_node.deploy.form.qiniu_cdn_domain.placeholder": "Please enter Qiniu CDN domain name",
|
||||
"workflow_node.deploy.form.qiniu_kodo_bucket.label": "Qiniu Kodo bucket",
|
||||
|
||||
@@ -517,6 +517,16 @@
|
||||
"access.form.proxmoxve_api_token_secret.label": "Proxmox VE API Token Secret(可选)",
|
||||
"access.form.proxmoxve_api_token_secret.placeholder": "请输入 Proxmox VE API Token Secret",
|
||||
"access.form.proxmoxve_api_token_secret.tooltip": "这是什么?请参阅 <a href=\"https://pve.proxmox.com/pve-docs/pve-admin-guide.html#pveum_tokens\" target=\"_blank\">https://pve.proxmox.com/pve-docs/pve-admin-guide.html#pveum_tokens</a>",
|
||||
"access.form.synologydsm_server_url.label": "群晖 DSM 服务地址",
|
||||
"access.form.synologydsm_server_url.placeholder": "请输入群晖 DSM 服务地址",
|
||||
"access.form.synologydsm_username.label": "群晖 DSM 用户名",
|
||||
"access.form.synologydsm_username.placeholder": "请输入群晖 DSM 用户名",
|
||||
"access.form.synologydsm_password.label": "群晖 DSM 用户密码",
|
||||
"access.form.synologydsm_password.placeholder": "请输入群晖 DSM 用户密码",
|
||||
"access.form.synologydsm_totp_secret.label": "群晖 DSM 双重验证 TOTP 密钥(可选)",
|
||||
"access.form.synologydsm_totp_secret.placeholder": "请输入群晖 DSM 双重验证 TOTP 密钥",
|
||||
"access.form.synologydsm_totp_secret.help": "提示:仅当群晖开启双重验证登录时需要填写,用于生成动态登录验证码。",
|
||||
"access.form.synologydsm_totp_secret.tooltip": "如何获取此参数?可在设置双重验证时点击「无法扫描?」查看,或将二维码识别为文本后、提取其中的 <em>secret</em> 部分。注意,这不是 6 位动态 OTP 码。",
|
||||
"access.form.qingcloud_access_key_id.label": "青云 AccessKeyID",
|
||||
"access.form.qingcloud_access_key_id.placeholder": "请输入青云 AccessKeyID",
|
||||
"access.form.qingcloud_access_key_id.tooltip": "这是什么?请参阅 <a href=\"https://console.qingcloud.com/access_keys/\" target=\"_blank\">https://console.qingcloud.com/access_keys/</a>",
|
||||
|
||||
@@ -165,6 +165,7 @@
|
||||
"provider.spaceship": "Spaceship",
|
||||
"provider.ssh": "远程主机(SSH)",
|
||||
"provider.sslcom": "SSL.com",
|
||||
"provider.synologydsm": "群晖 DSM",
|
||||
"provider.technitiumdns": "Technitium DNS",
|
||||
"provider.telegrambot": "Telegram 机器人",
|
||||
"provider.tencentcloud": "腾讯云",
|
||||
|
||||
@@ -748,6 +748,11 @@
|
||||
"workflow_node.deploy.form.proxmoxve_node_name.label": "Proxmox VE 集群节点名称",
|
||||
"workflow_node.deploy.form.proxmoxve_node_name.placeholder": "请输入 Proxmox VE 集群节点名称",
|
||||
"workflow_node.deploy.form.proxmoxve_auto_restart.label": "部署后自动重启 Proxmox VE 服务",
|
||||
"workflow_node.deploy.form.synologydsm_certificate_id_or_desc.label": "群晖 DSM 原证书 ID 或描述(可选)",
|
||||
"workflow_node.deploy.form.synologydsm_certificate_id_or_desc.placeholder": "请输入群晖 DSM 原证书 ID 或描述",
|
||||
"workflow_node.deploy.form.synologydsm_certificate_id_or_desc.help": "提示:不填写时,将上传新证书;否则,将替换原证书。",
|
||||
"workflow_node.deploy.form.synologydsm_certificate_id_or_desc.tooltip": "请登录群晖 DSM 面板查看<br><br>(目前需要在「控制面板 -> 安全性 -> 证书」页面,并打开浏览器开发者工具抓取网络请求后得到证书 ID)",
|
||||
"workflow_node.deploy.form.synologydsm_is_default.label": "设为默认证书并应用到所有 DSM 服务",
|
||||
"workflow_node.deploy.form.qiniu_cdn_domain.label": "七牛云 CDN 加速域名",
|
||||
"workflow_node.deploy.form.qiniu_cdn_domain.placeholder": "请输入七牛云 CDN 加速域名",
|
||||
"workflow_node.deploy.form.qiniu_kodo_bucket.label": "七牛云对象存储桶名",
|
||||
|
||||
Reference in New Issue
Block a user