feat: new deployment provider: baotawaf site

This commit is contained in:
Fu Diwei
2025-05-20 21:16:28 +08:00
parent 4ad08d983a
commit 591df58992
22 changed files with 628 additions and 8 deletions

View File

@@ -35,6 +35,7 @@ import (
pBaishanCDN "github.com/usual2970/certimate/internal/pkg/core/deployer/providers/baishan-cdn"
pBaotaPanelConsole "github.com/usual2970/certimate/internal/pkg/core/deployer/providers/baotapanel-console"
pBaotaPanelSite "github.com/usual2970/certimate/internal/pkg/core/deployer/providers/baotapanel-site"
pBaotaWAFSite "github.com/usual2970/certimate/internal/pkg/core/deployer/providers/baotawaf-site"
pBunnyCDN "github.com/usual2970/certimate/internal/pkg/core/deployer/providers/bunny-cdn"
pBytePlusCDN "github.com/usual2970/certimate/internal/pkg/core/deployer/providers/byteplus-cdn"
pCacheFly "github.com/usual2970/certimate/internal/pkg/core/deployer/providers/cachefly"
@@ -475,6 +476,29 @@ func createDeployerProvider(options *deployerProviderOptions) (deployer.Deployer
}
}
case domain.DeploymentProviderTypeBaotaWAFSite:
{
access := domain.AccessConfigForBaotaWAF{}
if err := maputil.Populate(options.ProviderAccessConfig, &access); err != nil {
return nil, fmt.Errorf("failed to populate provider access config: %w", err)
}
switch options.Provider {
case domain.DeploymentProviderTypeBaotaWAFSite:
deployer, err := pBaotaWAFSite.NewDeployer(&pBaotaWAFSite.DeployerConfig{
ApiUrl: access.ApiUrl,
ApiKey: access.ApiKey,
AllowInsecureConnections: access.AllowInsecureConnections,
SiteName: maputil.GetString(options.ProviderServiceConfig, "siteName"),
SitePort: maputil.GetOrDefaultInt32(options.ProviderServiceConfig, "sitePort", 443),
})
return deployer, err
default:
break
}
}
case domain.DeploymentProviderTypeBunnyCDN:
{
access := domain.AccessConfigForBunny{}

View File

@@ -67,6 +67,12 @@ type AccessConfigForBaotaPanel struct {
AllowInsecureConnections bool `json:"allowInsecureConnections,omitempty"`
}
type AccessConfigForBaotaWAF struct {
ApiUrl string `json:"apiUrl"`
ApiKey string `json:"apiKey"`
AllowInsecureConnections bool `json:"allowInsecureConnections,omitempty"`
}
type AccessConfigForBytePlus struct {
AccessKey string `json:"accessKey"`
SecretKey string `json:"secretKey"`

View File

@@ -19,6 +19,7 @@ const (
AccessProviderTypeBaiduCloud = AccessProviderType("baiducloud")
AccessProviderTypeBaishan = AccessProviderType("baishan")
AccessProviderTypeBaotaPanel = AccessProviderType("baotapanel")
AccessProviderTypeBaotaWAF = AccessProviderType("baotawaf")
AccessProviderTypeBytePlus = AccessProviderType("byteplus")
AccessProviderTypeBunny = AccessProviderType("bunny")
AccessProviderTypeBuypass = AccessProviderType("buypass")
@@ -190,6 +191,7 @@ const (
DeploymentProviderTypeBaishanCDN = DeploymentProviderType(AccessProviderTypeBaishan + "-cdn")
DeploymentProviderTypeBaotaPanelConsole = DeploymentProviderType(AccessProviderTypeBaotaPanel + "-console")
DeploymentProviderTypeBaotaPanelSite = DeploymentProviderType(AccessProviderTypeBaotaPanel + "-site")
DeploymentProviderTypeBaotaWAFSite = DeploymentProviderType(AccessProviderTypeBaotaWAF + "-site")
DeploymentProviderTypeBunnyCDN = DeploymentProviderType(AccessProviderTypeBunny + "-cdn")
DeploymentProviderTypeBytePlusCDN = DeploymentProviderType(AccessProviderTypeBytePlus + "-cdn")
DeploymentProviderTypeCacheFly = DeploymentProviderType(AccessProviderTypeCacheFly)

View File

@@ -0,0 +1,151 @@
package baotapanelwaf
import (
"context"
"crypto/tls"
"errors"
"fmt"
"log/slog"
"net/url"
"github.com/usual2970/certimate/internal/pkg/core/deployer"
btsdk "github.com/usual2970/certimate/internal/pkg/sdk3rd/btwaf"
typeutil "github.com/usual2970/certimate/internal/pkg/utils/type"
)
type DeployerConfig struct {
// 堡塔云 WAF 地址。
ApiUrl string `json:"apiUrl"`
// 堡塔云 WAF 接口密钥。
ApiKey string `json:"apiKey"`
// 是否允许不安全的连接。
AllowInsecureConnections bool `json:"allowInsecureConnections,omitempty"`
// 网站名称。
SiteName string `json:"siteName"`
// 网站 SSL 端口。
// 零值时默认为 443。
SitePort int32 `json:"sitePort,omitempty"`
}
type DeployerProvider struct {
config *DeployerConfig
logger *slog.Logger
sdkClient *btsdk.Client
}
var _ deployer.Deployer = (*DeployerProvider)(nil)
func NewDeployer(config *DeployerConfig) (*DeployerProvider, error) {
if config == nil {
panic("config is nil")
}
client, err := createSdkClient(config.ApiUrl, config.ApiKey, config.AllowInsecureConnections)
if err != nil {
return nil, fmt.Errorf("failed to create sdk client: %w", err)
}
return &DeployerProvider{
config: config,
logger: slog.Default(),
sdkClient: client,
}, nil
}
func (d *DeployerProvider) WithLogger(logger *slog.Logger) deployer.Deployer {
if logger == nil {
d.logger = slog.Default()
} else {
d.logger = logger
}
return d
}
func (d *DeployerProvider) Deploy(ctx context.Context, certPEM string, privkeyPEM string) (*deployer.DeployResult, error) {
if d.config.SiteName == "" {
return nil, errors.New("config `siteName` is required")
}
if d.config.SitePort == 0 {
d.config.SitePort = 443
}
// 遍历获取网站列表,获取网站 ID
// REF: https://support.huaweicloud.com/api-waf/ListHost.html
siteId := ""
getSitListPage := int32(1)
getSitListPageSize := int32(100)
for {
select {
case <-ctx.Done():
return nil, ctx.Err()
default:
}
getSiteListReq := &btsdk.GetSiteListRequest{
SiteName: typeutil.ToPtr(d.config.SiteName),
Page: typeutil.ToPtr(getSitListPage),
PageSize: typeutil.ToPtr(getSitListPageSize),
}
getSiteListResp, err := d.sdkClient.GetSiteList(getSiteListReq)
d.logger.Debug("sdk request 'bt.GetSiteList'", slog.Any("request", getSiteListReq), slog.Any("response", getSiteListResp))
if err != nil {
return nil, fmt.Errorf("failed to execute sdk request 'bt.GetSiteList': %w", err)
}
if getSiteListResp.Result != nil && getSiteListResp.Result.List != nil {
for _, siteItem := range getSiteListResp.Result.List {
if siteItem.SiteName == d.config.SiteName {
siteId = siteItem.SiteId
break
}
}
}
if getSiteListResp.Result == nil || len(getSiteListResp.Result.List) < int(getSitListPageSize) {
break
} else {
getSitListPage++
}
}
if siteId == "" {
return nil, errors.New("site not found")
}
// 修改站点配置
modifySiteReq := &btsdk.ModifySiteRequest{
SiteId: siteId,
Type: typeutil.ToPtr("openCert"),
Server: &btsdk.SiteServerInfo{
ListenSSLPort: typeutil.ToPtr(d.config.SitePort),
SSL: &btsdk.SiteServerSSLInfo{
IsSSL: typeutil.ToPtr(int32(1)),
FullChain: typeutil.ToPtr(certPEM),
PrivateKey: typeutil.ToPtr(privkeyPEM),
},
},
}
modifySiteResp, err := d.sdkClient.ModifySite(modifySiteReq)
d.logger.Debug("sdk request 'bt.ModifySite'", slog.Any("request", modifySiteReq), slog.Any("response", modifySiteResp))
if err != nil {
return nil, fmt.Errorf("failed to execute sdk request 'bt.ModifySite': %w", err)
}
return &deployer.DeployResult{}, nil
}
func createSdkClient(apiUrl, apiKey string, skipTlsVerify bool) (*btsdk.Client, error) {
if _, err := url.Parse(apiUrl); err != nil {
return nil, errors.New("invalid baota api url")
}
if apiKey == "" {
return nil, errors.New("invalid baota api key")
}
client := btsdk.NewClient(apiUrl, apiKey)
if skipTlsVerify {
client.WithTLSConfig(&tls.Config{InsecureSkipVerify: true})
}
return client, nil
}

View File

@@ -0,0 +1,81 @@
package baotapanelwaf_test
import (
"context"
"flag"
"fmt"
"os"
"strings"
"testing"
provider "github.com/usual2970/certimate/internal/pkg/core/deployer/providers/baotawaf-site"
)
var (
fInputCertPath string
fInputKeyPath string
fApiUrl string
fApiKey string
fSiteName string
fSitePort int64
)
func init() {
argsPrefix := "CERTIMATE_DEPLOYER_BAOTAWAFSITE_"
flag.StringVar(&fInputCertPath, argsPrefix+"INPUTCERTPATH", "", "")
flag.StringVar(&fInputKeyPath, argsPrefix+"INPUTKEYPATH", "", "")
flag.StringVar(&fApiUrl, argsPrefix+"APIURL", "", "")
flag.StringVar(&fApiKey, argsPrefix+"APIKEY", "", "")
flag.StringVar(&fSiteName, argsPrefix+"SITENAME", "", "")
flag.Int64Var(&fSitePort, argsPrefix+"SITEPORT", 0, "")
}
/*
Shell command to run this test:
go test -v ./baotawaf_site_test.go -args \
--CERTIMATE_DEPLOYER_BAOTAWAFSITE_INPUTCERTPATH="/path/to/your-input-cert.pem" \
--CERTIMATE_DEPLOYER_BAOTAWAFSITE_INPUTKEYPATH="/path/to/your-input-key.pem" \
--CERTIMATE_DEPLOYER_BAOTAWAFSITE_APIURL="http://127.0.0.1:8888" \
--CERTIMATE_DEPLOYER_BAOTAWAFSITE_APIKEY="your-api-key" \
--CERTIMATE_DEPLOYER_BAOTAWAFSITE_SITENAME="your-site-name"\
--CERTIMATE_DEPLOYER_BAOTAWAFSITE_SITEPORT=443
*/
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("APIURL: %v", fApiUrl),
fmt.Sprintf("APIKEY: %v", fApiKey),
fmt.Sprintf("SITENAME: %v", fSiteName),
fmt.Sprintf("SITEPORT: %v", fSitePort),
}, "\n"))
deployer, err := provider.NewDeployer(&provider.DeployerConfig{
ApiUrl: fApiUrl,
ApiKey: fApiKey,
AllowInsecureConnections: true,
SiteName: fSiteName,
SitePort: int32(fSitePort),
})
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)
})
}

View File

@@ -0,0 +1,19 @@
package btwaf
func (c *Client) GetSiteList(req *GetSiteListRequest) (*GetSiteListResponse, error) {
resp := &GetSiteListResponse{}
err := c.sendRequestWithResult("/wafmastersite/get_site_list", req, resp)
return resp, err
}
func (c *Client) ModifySite(req *ModifySiteRequest) (*ModifySiteResponse, error) {
resp := &ModifySiteResponse{}
err := c.sendRequestWithResult("/wafmastersite/modify_site", req, resp)
return resp, err
}
func (c *Client) ConfigSetSSL(req *ConfigSetSSLRequest) (*ConfigSetSSLResponse, error) {
resp := &ConfigSetSSLResponse{}
err := c.sendRequestWithResult("/config/set_cert", req, resp)
return resp, err
}

View File

@@ -0,0 +1,77 @@
package btwaf
import (
"crypto/md5"
"crypto/tls"
"encoding/hex"
"encoding/json"
"fmt"
"net/http"
"strings"
"time"
"github.com/go-resty/resty/v2"
)
type Client struct {
client *resty.Client
}
func NewClient(apiHost, apiKey string) *Client {
client := resty.New().
SetBaseURL(strings.TrimRight(apiHost, "/") + "/api").
SetPreRequestHook(func(c *resty.Client, req *http.Request) error {
timestamp := fmt.Sprintf("%d", time.Now().Unix())
keyMd5 := md5.Sum([]byte(apiKey))
keyMd5Hex := strings.ToLower(hex.EncodeToString(keyMd5[:]))
signMd5 := md5.Sum([]byte(timestamp + keyMd5Hex))
signMd5Hex := strings.ToLower(hex.EncodeToString(signMd5[:]))
req.Header.Set("waf_request_time", timestamp)
req.Header.Set("waf_request_token", signMd5Hex)
return nil
})
return &Client{
client: client,
}
}
func (c *Client) WithTimeout(timeout time.Duration) *Client {
c.client.SetTimeout(timeout)
return c
}
func (c *Client) WithTLSConfig(config *tls.Config) *Client {
c.client.SetTLSClientConfig(config)
return c
}
func (c *Client) sendRequest(path string, params interface{}) (*resty.Response, error) {
req := c.client.R().
SetHeader("Content-Type", "application/json").
SetBody(params)
resp, err := req.Post(path)
if err != nil {
return resp, fmt.Errorf("baota api error: failed to send request: %w", err)
} else if resp.IsError() {
return resp, fmt.Errorf("baota api error: unexpected status code: %d, resp: %s", resp.StatusCode(), resp.String())
}
return resp, nil
}
func (c *Client) sendRequestWithResult(path string, params interface{}, result BaseResponse) error {
resp, err := c.sendRequest(path, params)
if err != nil {
return err
}
if err := json.Unmarshal(resp.Body(), &result); err != nil {
return fmt.Errorf("baota api error: failed to unmarshal response: %w", err)
} else if errcode := result.GetCode(); errcode != 0 {
return fmt.Errorf("baota api error: code='%d'", errcode)
}
return nil
}

View File

@@ -0,0 +1,67 @@
package btwaf
type BaseResponse interface {
GetCode() int32
}
type baseResponse struct {
Code *int32 `json:"code,omitempty"`
}
func (r *baseResponse) GetCode() int32 {
if r.Code != nil {
return *r.Code
}
return 0
}
type GetSiteListRequest struct {
Page *int32 `json:"p,omitempty"`
PageSize *int32 `json:"p_size,omitempty"`
SiteName *string `json:"site_name,omitempty"`
}
type GetSiteListResponse struct {
baseResponse
Result *struct {
List []*struct {
SiteId string `json:"site_id"`
SiteName string `json:"site_name"`
Type string `json:"types"`
Status int32 `json:"status"`
CreateTime int64 `json:"create_time"`
UpdateTime int64 `json:"update_time"`
} `json:"list"`
Total int32 `json:"total"`
} `json:"res,omitempty"`
}
type SiteServerInfo struct {
ListenSSLPort *int32 `json:"listen_ssl_port,omitempty"`
SSL *SiteServerSSLInfo `json:"ssl,omitempty"`
}
type SiteServerSSLInfo struct {
IsSSL *int32 `json:"is_ssl,omitempty"`
FullChain *string `json:"full_chain,omitempty"`
PrivateKey *string `json:"private_key,omitempty"`
}
type ModifySiteRequest struct {
SiteId string `json:"site_id"`
Type *string `json:"types,omitempty"`
Server *SiteServerInfo `json:"server,omitempty"`
}
type ModifySiteResponse struct {
baseResponse
}
type ConfigSetSSLRequest struct {
CertContent string `json:"certContent"`
KeyContent string `json:"keyContent"`
}
type ConfigSetSSLResponse struct {
baseResponse
}