mirror of
https://github.com/usual2970/certimate.git
synced 2025-10-04 21:44:54 +00:00
feat: new deployment provider: baotawaf site
This commit is contained in:
@@ -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{}
|
||||
|
@@ -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"`
|
||||
|
@@ -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)
|
||||
|
@@ -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
|
||||
}
|
@@ -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)
|
||||
})
|
||||
}
|
19
internal/pkg/sdk3rd/btwaf/api.go
Normal file
19
internal/pkg/sdk3rd/btwaf/api.go
Normal 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
|
||||
}
|
77
internal/pkg/sdk3rd/btwaf/client.go
Normal file
77
internal/pkg/sdk3rd/btwaf/client.go
Normal 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
|
||||
}
|
67
internal/pkg/sdk3rd/btwaf/models.go
Normal file
67
internal/pkg/sdk3rd/btwaf/models.go
Normal 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
|
||||
}
|
Reference in New Issue
Block a user