feat: new acme dns-01 provider: statecloud smartdns

This commit is contained in:
Fu Diwei
2025-06-12 23:05:18 +08:00
parent fb62f1e105
commit 9c8ab98efb
23 changed files with 837 additions and 65 deletions

View File

@@ -0,0 +1,46 @@
package dns
import (
"context"
"net/http"
)
type AddRecordRequest struct {
Domain *string `json:"domain,omitempty"`
Host *string `json:"host,omitempty"`
Type *string `json:"type,omitempty"`
LineCode *string `json:"lineCode,omitempty"`
Value *string `json:"value,omitempty"`
TTL *int32 `json:"ttl,omitempty"`
State *int32 `json:"state,omitempty"`
Remark *string `json:"remark"`
}
type AddRecordResponse struct {
baseResult
ReturnObj *struct {
RecordId int32 `json:"recordId"`
} `json:"returnObj,omitempty"`
}
func (c *Client) AddRecord(req *AddRecordRequest) (*AddRecordResponse, error) {
return c.AddRecordWithContext(context.Background(), req)
}
func (c *Client) AddRecordWithContext(ctx context.Context, req *AddRecordRequest) (*AddRecordResponse, error) {
request, err := c.newRequest(http.MethodPost, "/v2/addRecord")
if err != nil {
return nil, err
} else {
request.SetContext(ctx)
request.SetBody(req)
}
result := &AddRecordResponse{}
if _, err := c.doRequestWithResult(request, result); err != nil {
return result, err
}
return result, nil
}

View File

@@ -0,0 +1,35 @@
package dns
import (
"context"
"net/http"
)
type DeleteRecordRequest struct {
RecordId *int32 `json:"recordId,omitempty"`
}
type DeleteRecordResponse struct {
baseResult
}
func (c *Client) DeleteRecord(req *DeleteRecordRequest) (*DeleteRecordResponse, error) {
return c.DeleteRecordWithContext(context.Background(), req)
}
func (c *Client) DeleteRecordWithContext(ctx context.Context, req *DeleteRecordRequest) (*DeleteRecordResponse, error) {
request, err := c.newRequest(http.MethodPost, "/v2/deleteRecord")
if err != nil {
return nil, err
} else {
request.SetContext(ctx)
request.SetBody(req)
}
result := &DeleteRecordResponse{}
if _, err := c.doRequestWithResult(request, result); err != nil {
return result, err
}
return result, nil
}

View File

@@ -0,0 +1,53 @@
package dns
import (
"context"
"net/http"
)
type QueryRecordListRequest struct {
Domain *string `json:"domain,omitempty"`
Host *string `json:"host,omitempty"`
Type *string `json:"type,omitempty"`
LineCode *string `json:"lineCode,omitempty"`
Value *string `json:"value,omitempty"`
State *int32 `json:"state,omitempty"`
}
type QueryRecordListResponse struct {
baseResult
ReturnObj *struct {
Records []*struct {
RecordId int32 `json:"recordId"`
Host string `json:"host"`
Type string `json:"type"`
LineCode string `json:"lineCode"`
Value string `json:"value"`
TTL int32 `json:"ttl"`
State int32 `json:"state"`
Remark string `json:"remark"`
} `json:"records,omitempty"`
} `json:"returnObj,omitempty"`
}
func (c *Client) QueryRecordList(req *QueryRecordListRequest) (*QueryRecordListResponse, error) {
return c.QueryRecordListWithContext(context.Background(), req)
}
func (c *Client) QueryRecordListWithContext(ctx context.Context, req *QueryRecordListRequest) (*QueryRecordListResponse, error) {
request, err := c.newRequest(http.MethodGet, "/v2/queryRecordList")
if err != nil {
return nil, err
} else {
request.SetContext(ctx)
request.SetBody(req)
}
result := &QueryRecordListResponse{}
if _, err := c.doRequestWithResult(request, result); err != nil {
return result, err
}
return result, nil
}

View File

@@ -0,0 +1,47 @@
package dns
import (
"context"
"net/http"
)
type UpdateRecordRequest struct {
RecordId *int32 `json:"recordId,omitempty"`
Domain *string `json:"domain,omitempty"`
Host *string `json:"host,omitempty"`
Type *string `json:"type,omitempty"`
LineCode *string `json:"lineCode,omitempty"`
Value *string `json:"value,omitempty"`
TTL *int32 `json:"ttl,omitempty"`
State *int32 `json:"state,omitempty"`
Remark *string `json:"remark"`
}
type UpdateRecordResponse struct {
baseResult
ReturnObj *struct {
RecordId int32 `json:"recordId"`
} `json:"returnObj,omitempty"`
}
func (c *Client) UpdateRecord(req *UpdateRecordRequest) (*UpdateRecordResponse, error) {
return c.UpdateRecordWithContext(context.Background(), req)
}
func (c *Client) UpdateRecordWithContext(ctx context.Context, req *UpdateRecordRequest) (*UpdateRecordResponse, error) {
request, err := c.newRequest(http.MethodPost, "/v2/updateRecord")
if err != nil {
return nil, err
} else {
request.SetContext(ctx)
request.SetBody(req)
}
result := &UpdateRecordResponse{}
if _, err := c.doRequestWithResult(request, result); err != nil {
return result, err
}
return result, nil
}

View File

@@ -0,0 +1,40 @@
package dns
import (
"time"
"github.com/go-resty/resty/v2"
"github.com/usual2970/certimate/internal/pkg/sdk3rd/ctyun/openapi"
)
const endpoint = "https://smartdns-global.ctapi.ctyun.cn"
type Client struct {
client *openapi.Client
}
func NewClient(accessKeyId, secretAccessKey string) (*Client, error) {
client, err := openapi.NewClient(endpoint, accessKeyId, secretAccessKey)
if err != nil {
return nil, err
}
return &Client{client: client}, nil
}
func (c *Client) SetTimeout(timeout time.Duration) *Client {
c.client.SetTimeout(timeout)
return c
}
func (c *Client) newRequest(method string, path string) (*resty.Request, error) {
return c.client.NewRequest(method, path)
}
func (c *Client) doRequest(request *resty.Request) (*resty.Response, error) {
return c.client.DoRequest(request)
}
func (c *Client) doRequestWithResult(request *resty.Request, result any) (*resty.Response, error) {
return c.client.DoRequestWithResult(request, result)
}

View File

@@ -0,0 +1,11 @@
package dns
import "encoding/json"
type baseResult struct {
StatusCode json.RawMessage `json:"statusCode,omitempty"`
Message *string `json:"message,omitempty"`
Error *string `json:"error,omitempty"`
ErrorMessage *string `json:"errorMessage,omitempty"`
RequestId *string `json:"requestId,omitempty"`
}

View File

@@ -0,0 +1,167 @@
package openapi
import (
"crypto/hmac"
"crypto/sha256"
"encoding/base64"
"encoding/hex"
"encoding/json"
"fmt"
"io"
"net/http"
"net/url"
"time"
"github.com/go-resty/resty/v2"
"github.com/google/uuid"
)
type Client struct {
client *resty.Client
}
func NewClient(endpoint, accessKeyId, secretAccessKey string) (*Client, error) {
if endpoint == "" {
return nil, fmt.Errorf("sdk error: unset endpoint")
}
if _, err := url.Parse(endpoint); err != nil {
return nil, fmt.Errorf("sdk error: invalid endpoint: %w", err)
}
if accessKeyId == "" {
return nil, fmt.Errorf("sdk error: unset accessKey")
}
if secretAccessKey == "" {
return nil, fmt.Errorf("sdk error: unset secretKey")
}
client := resty.New().
SetBaseURL(endpoint).
SetHeader("Accept", "application/json").
SetHeader("Content-Type", "application/json").
SetHeader("User-Agent", "certimate").
SetPreRequestHook(func(c *resty.Client, req *http.Request) error {
// 生成时间戳及流水号
now := time.Now()
eopDate := now.Format("20060102T150405Z")
eopReqId := uuid.New().String()
// 获取查询参数
queryStr := ""
if req.URL != nil {
queryStr = req.URL.Query().Encode()
}
// 获取请求正文
payloadStr := ""
if req.Body != nil {
reader, err := req.GetBody()
if err != nil {
return err
}
defer reader.Close()
payload, err := io.ReadAll(reader)
if err != nil {
return err
}
payloadStr = string(payload)
}
// 构造代签字符串
payloadHash := sha256.Sum256([]byte(payloadStr))
payloadHashHex := hex.EncodeToString(payloadHash[:])
dataToSign := fmt.Sprintf("ctyun-eop-request-id:%s\neop-date:%s\n\n%s\n%s", eopReqId, eopDate, queryStr, payloadHashHex)
// 生成 ktime
hasher := hmac.New(sha256.New, []byte(secretAccessKey))
hasher.Write([]byte(eopDate))
ktime := hasher.Sum(nil)
// 生成 kak
hasher = hmac.New(sha256.New, ktime)
hasher.Write([]byte(accessKeyId))
kak := hasher.Sum(nil)
// 生成 kdata
hasher = hmac.New(sha256.New, kak)
hasher.Write([]byte(now.Format("20060102")))
kdate := hasher.Sum(nil)
// 构造签名
hasher = hmac.New(sha256.New, kdate)
hasher.Write([]byte(dataToSign))
sign := hasher.Sum(nil)
signStr := base64.StdEncoding.EncodeToString(sign)
// 设置请求头
req.Header.Set("ctyun-eop-request-id", eopReqId)
req.Header.Set("eop-date", eopDate)
req.Header.Set("eop-authorization", fmt.Sprintf("%s Headers=ctyun-eop-request-id;eop-date Signature=%s", accessKeyId, signStr))
return nil
})
return &Client{
client: client,
}, nil
}
func (c *Client) SetTimeout(timeout time.Duration) *Client {
c.client.SetTimeout(timeout)
return c
}
func (c *Client) NewRequest(method string, path string) (*resty.Request, error) {
if method == "" {
return nil, fmt.Errorf("sdk error: unset method")
}
if path == "" {
return nil, fmt.Errorf("sdk error: unset path")
}
req := c.client.R()
req.Method = method
req.URL = path
return req, nil
}
func (c *Client) DoRequest(request *resty.Request) (*resty.Response, error) {
if request == nil {
return nil, fmt.Errorf("sdk error: nil request")
}
// WARN:
// PLEASE DO NOT USE `req.SetResult` or `req.SetError` here.
resp, err := request.Send()
if err != nil {
return resp, fmt.Errorf("sdk error: failed to send request: %w", err)
} else if resp.IsError() {
return resp, fmt.Errorf("sdk error: unexpected status code: %d, resp: %s", resp.StatusCode(), resp.String())
}
return resp, nil
}
func (c *Client) DoRequestWithResult(request *resty.Request, result any) (*resty.Response, error) {
if request == nil {
return nil, fmt.Errorf("sdk error: nil request")
}
response, err := c.DoRequest(request)
if err != nil {
if response != nil {
json.Unmarshal(response.Body(), &result)
}
return response, err
}
if len(response.Body()) != 0 {
if err := json.Unmarshal(response.Body(), &result); err != nil {
return response, fmt.Errorf("sdk error: failed to unmarshal response: %w", err)
}
}
return response, nil
}