diff --git a/backend/app/api/access.go b/backend/app/api/access.go index 78ffe0c..9cd08dd 100644 --- a/backend/app/api/access.go +++ b/backend/app/api/access.go @@ -6,8 +6,9 @@ import ( "ALLinSSL/backend/internal/cert/deploy" "ALLinSSL/backend/internal/cert/deploy/plugin" "ALLinSSL/backend/public" - "github.com/gin-gonic/gin" "strings" + + "github.com/gin-gonic/gin" ) func GetAccessList(c *gin.Context) { @@ -323,6 +324,8 @@ func TestAccess(c *gin.Context) { result = deploy.TencentCloudAPITest(form.ID) case "aliyun": result = deploy.AliyunCdnAPITest(form.ID) + case "rainyun": + result = deploy.RainyunApiTest(form.ID) case "qiniu": result = deploy.QiniuAPITest(form.ID) case "baidu": diff --git a/backend/internal/cert/deploy/deploy.go b/backend/internal/cert/deploy/deploy.go index 49ad7e1..e1bbaeb 100644 --- a/backend/internal/cert/deploy/deploy.go +++ b/backend/internal/cert/deploy/deploy.go @@ -110,6 +110,9 @@ func Deploy(cfg map[string]any, logger *public.Logger) error { case "webhook": logger.Debug("通过Webhook推送证书...") return webhook.Deploy(cfg) + case "rainyun-sslcenter": + logger.Debug("通过Webhook推送证书...") + return DeployRainyunSSLCenter(cfg) default: return fmt.Errorf("不支持的部署: %s", providerName) } diff --git a/backend/internal/cert/deploy/rainyun.go b/backend/internal/cert/deploy/rainyun.go new file mode 100644 index 0000000..0a665b3 --- /dev/null +++ b/backend/internal/cert/deploy/rainyun.go @@ -0,0 +1,130 @@ +package deploy + +import ( + "ALLinSSL/backend/internal/access" + "bytes" + "context" + "encoding/json" + "errors" + "fmt" + "io" + "net/http" + "strconv" + "time" + + "github.com/tidwall/gjson" +) + +var rainyunApi = "https://api.v2.rainyun.com" +var httpClient = &http.Client{} + +func RainyunApiTest(providerID string) error { + providerData, err := access.GetAccess(providerID) + if err != nil { + return err + } + providerConfigStr, ok := providerData["config"].(string) + if !ok { + return fmt.Errorf("api配置错误") + } + // 解析 JSON 配置 + var providerConfig map[string]string + err = json.Unmarshal([]byte(providerConfigStr), &providerConfig) + if err != nil { + return err + } + + resp, err := requestRainyunApi("/user/", providerConfig["api_key"], http.MethodGet, nil) + if err != nil { + return err + } + + if gjson.Get(resp, "code").Int() != 200 { + return errors.New(gjson.Get(resp, "message").String()) + } + return nil +} + +func DeployRainyunSSLCenter(cfg map[string]any) error { + // 获取证书 + cert, ok := cfg["certificate"].(map[string]any) + if !ok { + return fmt.Errorf("证书不存在") + } + + // 获取ApiKey + var providerID string + switch v := cfg["provider_id"].(type) { + case float64: + providerID = strconv.Itoa(int(v)) + case string: + providerID = v + default: + return fmt.Errorf("参数错误:provider_id") + } + providerData, err := access.GetAccess(providerID) + if err != nil { + return err + } + providerConfigStr, _ := providerData["config"].(string) + var providerConfig map[string]string + err = json.Unmarshal([]byte(providerConfigStr), &providerConfig) + if err != nil { + return err + } + apiKey := providerConfig["api_key"] + + // 校验参数 + certId, ok := cfg["cert_id"].(string) + if !ok { + return fmt.Errorf("参数错误:cert_id") + } + _, ok = cert["key"].(string) + if !ok { + return fmt.Errorf("证书错误:key") + } + _, ok = cert["cert"].(string) + if !ok { + return fmt.Errorf("证书错误:cert") + } + + // 更新证书中心 + reqPath := fmt.Sprintf("/product/sslcenter/%s", certId) + resp, err := requestRainyunApi(reqPath, apiKey, http.MethodPost, cert) + if err != nil { + return err + } + if gjson.Get(resp, "code").Int() != 200 { + return errors.New(gjson.Get(resp, "message").String()) + } + + return nil +} + +func requestRainyunApi(path, apikey, method string, data interface{}) (string, error) { + ctx, cancel := context.WithTimeout(context.Background(), 10*time.Second) + defer cancel() + + reqBody, err := json.Marshal(data) + if err != nil || data == nil { + reqBody = nil + } + req, err := http.NewRequestWithContext(ctx, method, rainyunApi+path, bytes.NewBuffer(reqBody)) + if err != nil { + return "", fmt.Errorf("build request: %w", err) + } + req.Header.Set("X-Api-Key", apikey) + + resp, err := httpClient.Do(req) + if err != nil { + return "", fmt.Errorf("http get error: %w", err) + } + defer resp.Body.Close() + + body, err := io.ReadAll(io.LimitReader(resp.Body, 50*1024*1024)) + if err != nil { + return "", fmt.Errorf("read body: %w", err) + } + + return string(body), nil +} diff --git a/backend/migrations/init.go b/backend/migrations/init.go index 6d1da8b..823b42b 100644 --- a/backend/migrations/init.go +++ b/backend/migrations/init.go @@ -156,6 +156,7 @@ func init() { InsertIfNotExists(db, "access_type", map[string]any{"name": "cloudflare", "type": "host"}, []string{"name", "type"}, []any{"cloudflare", "host"}) InsertIfNotExists(db, "access_type", map[string]any{"name": "rainyun", "type": "dns"}, []string{"name", "type"}, []any{"rainyun", "dns"}) + InsertIfNotExists(db, "access_type", map[string]any{"name": "rainyun", "type": "host"}, []string{"name", "type"}, []any{"rainyun", "host"}) InsertIfNotExists(db, "access_type", map[string]any{"name": "cloudflare", "type": "dns"}, []string{"name", "type"}, []any{"cloudflare", "dns"}) InsertIfNotExists(db, "access_type", map[string]any{"name": "huaweicloud", "type": "host"}, []string{"name", "type"}, []any{"huaweicloud", "host"}) InsertIfNotExists(db, "access_type", map[string]any{"name": "huaweicloud", "type": "dns"}, []string{"name", "type"}, []any{"huaweicloud", "dns"}) diff --git a/frontend/apps/allin-ssl/src/config/data.tsx b/frontend/apps/allin-ssl/src/config/data.tsx index a1f2834..f7bb44b 100644 --- a/frontend/apps/allin-ssl/src/config/data.tsx +++ b/frontend/apps/allin-ssl/src/config/data.tsx @@ -2,37 +2,37 @@ import { $t } from '@locales/index' // 消息推送类型 export interface MessagePushType { - name: string - type: string + name: string + type: string } // 定义ApiProject接口,包含可选的notApi属性 export interface ApiProjectType { - name: string - icon: string - type?: string[] - notApi?: boolean - hostRelated?: Record - sort?: number + name: string + icon: string + type?: string[] + notApi?: boolean + hostRelated?: Record + sort?: number } // $t('t_0_1747886301644') export const MessagePushConfig = { - mail: { name: $t('t_68_1745289354676'), type: 'mail' }, - workwx: { name: $t('t_33_1746773350932'), type: 'workwx' }, - dingtalk: { name: $t('t_32_1746773348993'), type: 'dingtalk' }, - feishu: { name: $t('t_34_1746773350153'), type: 'feishu' }, - webhook: { name: 'WebHook', type: 'webhook' }, + mail: { name: $t('t_68_1745289354676'), type: 'mail' }, + workwx: { name: $t('t_33_1746773350932'), type: 'workwx' }, + dingtalk: { name: $t('t_32_1746773348993'), type: 'dingtalk' }, + feishu: { name: $t('t_34_1746773350153'), type: 'feishu' }, + webhook: { name: 'WebHook', type: 'webhook' }, } // CA证书授权 export const CACertificateAuthorization = { - zerossl: { name: 'ZeroSSL', type: 'zerossl' }, - google: { name: 'Google', type: 'google' }, - sslcom: { name: 'SSL.COM', type: 'sslcom' }, - buypass: { name: 'Buypass', type: 'buypass' }, - letsencrypt: { name: "Let's Encrypt", type: 'letsencrypt' }, - custom: { name: '自定义', type: 'custom' }, + zerossl: { name: 'ZeroSSL', type: 'zerossl' }, + google: { name: 'Google', type: 'google' }, + sslcom: { name: 'SSL.COM', type: 'sslcom' }, + buypass: { name: 'Buypass', type: 'buypass' }, + letsencrypt: { name: "Let's Encrypt", type: 'letsencrypt' }, + custom: { name: '自定义', type: 'custom' }, } // 授权API管理 @@ -282,6 +282,9 @@ export const ApiProjectConfig: Record = { name: "雨云", icon: "rainyun", type: ["dns"], + hostRelated: { + sslcenter: { name: "证书中心" } + }, sort: 33, }, plugin: { diff --git a/go.mod b/go.mod index dfc7e33..272585c 100644 --- a/go.mod +++ b/go.mod @@ -119,6 +119,9 @@ require ( github.com/pkg/errors v0.9.1 // indirect github.com/quasoft/memstore v0.0.0-20191010062613-2bce066d2b0b // indirect github.com/remyoudompheng/bigfft v0.0.0-20230129092748-24d4a6f8daec // indirect + github.com/tidwall/gjson v1.18.0 // indirect + github.com/tidwall/match v1.1.1 // indirect + github.com/tidwall/pretty v1.2.0 // indirect github.com/twitchyliquid64/golang-asm v0.15.1 // indirect github.com/ugorji/go/codec v1.2.12 // indirect github.com/volcengine/volc-sdk-golang v1.0.216 // indirect diff --git a/go.sum b/go.sum index 8ac1075..fa19858 100644 --- a/go.sum +++ b/go.sum @@ -711,6 +711,12 @@ github.com/tencentcloud/tencentcloud-sdk-go/tencentcloud/common v1.0.1210 h1:waS github.com/tencentcloud/tencentcloud-sdk-go/tencentcloud/common v1.0.1210/go.mod h1:r5r4xbfxSaeR04b166HGsBa/R4U3SueirEUpXGuw+Q0= github.com/tencentcloud/tencentcloud-sdk-go/tencentcloud/ssl v1.0.1124 h1:LQKAlxFb0sYiE8ojK5h9+seuFzogoJtYnXmiRF+4F4Q= github.com/tencentcloud/tencentcloud-sdk-go/tencentcloud/ssl v1.0.1124/go.mod h1:tYbK0FbHVG+78od7eZpzczE8qk0JWKO/osTQWuiJ3Fo= +github.com/tidwall/gjson v1.18.0 h1:FIDeeyB800efLX89e5a8Y0BNH+LOngJyGrIWxG2FKQY= +github.com/tidwall/gjson v1.18.0/go.mod h1:/wbyibRr2FHMks5tjHJ5F8dMZh3AcwJEMf5vlfC0lxk= +github.com/tidwall/match v1.1.1 h1:+Ho715JplO36QYgwN9PGYNhgZvoUSc9X2c80KVTi+GA= +github.com/tidwall/match v1.1.1/go.mod h1:eRSPERbgtNPcGhD8UCthc6PmLEQXEWd3PRB5JTxsfmM= +github.com/tidwall/pretty v1.2.0 h1:RWIZEg2iJ8/g6fDDYzMpobmaoGh5OLl4AXtGUGPcqCs= +github.com/tidwall/pretty v1.2.0/go.mod h1:ITEVvHYasfjBbM0u2Pg8T2nJnzm8xPwvNhhsoaGGjNU= github.com/tjfoc/gmsm v1.3.2/go.mod h1:HaUcFuY0auTiaHB9MHFGCPx5IaLhTUd2atbCFBQXn9w= github.com/tjfoc/gmsm v1.4.1 h1:aMe1GlZb+0bLjn+cKTPEvvn9oUEBlJitaZiiBwsbgho= github.com/tjfoc/gmsm v1.4.1/go.mod h1:j4INPkHWMrhJb38G+J6W4Tw0AbuN8Thu3PbdVYhVcTE=