mirror of https://github.com/allinssl/allinssl
修改插件部署、新增多吉云cdn部署
parent
0c53a7eb3f
commit
aca870ad30
|
@ -1,6 +1,8 @@
|
|||
package deploy
|
||||
|
||||
import (
|
||||
"ALLinSSL/backend/internal/cert/deploy/doge"
|
||||
"ALLinSSL/backend/internal/cert/deploy/plugin"
|
||||
"ALLinSSL/backend/public"
|
||||
"fmt"
|
||||
)
|
||||
|
@ -87,9 +89,12 @@ func Deploy(cfg map[string]any, logger *public.Logger) error {
|
|||
case "volcengine-dcdn":
|
||||
logger.Debug("部署到火山DCDN...")
|
||||
return DeployVolcEngineDCdn(cfg)
|
||||
// case "plugin":
|
||||
// logger.Debug("使用插件部署...")
|
||||
// return DeployPlugin(cfg)
|
||||
case "doge-cdn":
|
||||
logger.Debug("部署到多吉云CDN...")
|
||||
return doge.DeployCdn(cfg)
|
||||
case "plugin":
|
||||
logger.Debug("使用插件部署...")
|
||||
return plugin.Deploy(cfg, logger)
|
||||
default:
|
||||
return fmt.Errorf("不支持的部署: %s", providerName)
|
||||
}
|
||||
|
|
|
@ -0,0 +1,221 @@
|
|||
package doge
|
||||
|
||||
import (
|
||||
"ALLinSSL/backend/public"
|
||||
"crypto/hmac"
|
||||
"crypto/sha1"
|
||||
"encoding/hex"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"io"
|
||||
"net/http"
|
||||
"net/url"
|
||||
"strings"
|
||||
)
|
||||
|
||||
type Auth struct {
|
||||
AccessKey string `json:"access_key"`
|
||||
SecretKey string `json:"secret_key"`
|
||||
}
|
||||
|
||||
func NewAuth(accessKey, secretKey string) *Auth {
|
||||
return &Auth{
|
||||
AccessKey: accessKey,
|
||||
SecretKey: secretKey,
|
||||
}
|
||||
}
|
||||
|
||||
func DeployCdn(cfg map[string]any) error {
|
||||
if cfg == nil {
|
||||
return fmt.Errorf("config cannot be nil")
|
||||
}
|
||||
certStr, ok := cfg["cert"].(string)
|
||||
if !ok || certStr == "" {
|
||||
return fmt.Errorf("cert is required and must be a string")
|
||||
}
|
||||
keyStr, ok := cfg["key"].(string)
|
||||
if !ok || keyStr == "" {
|
||||
return fmt.Errorf("key is required and must be a string")
|
||||
}
|
||||
accessKey, ok := cfg["access_key"].(string)
|
||||
if !ok || accessKey == "" {
|
||||
return fmt.Errorf("access_key is required and must be a string")
|
||||
}
|
||||
secretKey, ok := cfg["secret_key"].(string)
|
||||
if !ok || secretKey == "" {
|
||||
return fmt.Errorf("secret_key is required and must be a string")
|
||||
}
|
||||
domain, ok := cfg["domain"].(string)
|
||||
if !ok || domain == "" {
|
||||
return fmt.Errorf("domain is required and must be a string")
|
||||
}
|
||||
sha256, err := public.GetSHA256(certStr)
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to get SHA256 of cert: %w", err)
|
||||
}
|
||||
note := fmt.Sprintf("allinssl-%s", sha256)
|
||||
|
||||
a := NewAuth(accessKey, secretKey)
|
||||
// 检查证书是否已存在于 CDN
|
||||
certList, err := a.listCertFromCdn()
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to list certs from CDN: %w", err)
|
||||
}
|
||||
var certID float64
|
||||
for _, cert := range certList {
|
||||
if cert["note"] == note {
|
||||
certID, ok = cert["id"].(float64)
|
||||
if !ok {
|
||||
certID = 0
|
||||
}
|
||||
}
|
||||
}
|
||||
// 如果证书不存在,则上传证书到 CDN
|
||||
if certID == 0 {
|
||||
certID, err = a.uploadCertToCdn(certStr, keyStr, note)
|
||||
if err != nil || certID == 0 {
|
||||
return fmt.Errorf("failed to upload to CDN: %w", err)
|
||||
}
|
||||
}
|
||||
// 绑定证书到域名
|
||||
_, err = a.bindCertToCdn(certID, domain)
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to bind cert to CDN: %w", err)
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (a Auth) uploadCertToCdn(cert, key, note string) (float64, error) {
|
||||
params := map[string]any{
|
||||
"cert": cert,
|
||||
"private": key,
|
||||
"note": note,
|
||||
}
|
||||
|
||||
res, err := a.DogeCloudAPI("/cdn/cert/upload.json", params, true)
|
||||
if err != nil {
|
||||
return 0, fmt.Errorf("failed to call DogeCloud API: %w", err)
|
||||
}
|
||||
code, ok := res["code"].(float64)
|
||||
if !ok {
|
||||
return 0, fmt.Errorf("invalid response format: code not found")
|
||||
}
|
||||
if code != 200 {
|
||||
return 0, fmt.Errorf("DogeCloud API error: %s", res["msg"])
|
||||
}
|
||||
data, ok := res["data"].(map[string]any)
|
||||
if !ok {
|
||||
return 0, fmt.Errorf("invalid response format: data not found")
|
||||
}
|
||||
certID, ok := data["id"].(float64)
|
||||
if !ok {
|
||||
return 0, fmt.Errorf("invalid response format: id not found")
|
||||
}
|
||||
return certID, nil
|
||||
}
|
||||
|
||||
func (a Auth) listCertFromCdn() ([]map[string]any, error) {
|
||||
res, err := a.DogeCloudAPI("/cdn/cert/list.json", map[string]interface{}{}, true)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to call DogeCloud API: %w", err)
|
||||
}
|
||||
code, ok := res["code"].(float64)
|
||||
if !ok {
|
||||
return nil, fmt.Errorf("invalid response format: code not found")
|
||||
}
|
||||
if code != 200 {
|
||||
return nil, fmt.Errorf("DogeCloud API error: %s", res["msg"])
|
||||
}
|
||||
data, ok := res["data"].(map[string]any)
|
||||
if !ok {
|
||||
return nil, fmt.Errorf("invalid response format: data not found")
|
||||
}
|
||||
certList, ok := data["certs"].([]any)
|
||||
if !ok {
|
||||
return nil, fmt.Errorf("invalid response format: certs not found")
|
||||
}
|
||||
certs := make([]map[string]any, 0, len(certList))
|
||||
for _, cert := range certList {
|
||||
certMap, ok := cert.(map[string]any)
|
||||
if !ok {
|
||||
return nil, fmt.Errorf("invalid response format: cert item is not a map")
|
||||
}
|
||||
certs = append(certs, certMap)
|
||||
}
|
||||
return certs, nil
|
||||
}
|
||||
|
||||
func (a Auth) bindCertToCdn(certID float64, domain string) (map[string]interface{}, error) {
|
||||
params := map[string]interface{}{
|
||||
"id": certID,
|
||||
"domain": domain,
|
||||
}
|
||||
res, err := a.DogeCloudAPI("/cdn/cert/bind.json", params, true)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to call DogeCloud API: %w", err)
|
||||
}
|
||||
code, ok := res["code"].(float64)
|
||||
if !ok {
|
||||
return nil, fmt.Errorf("invalid response format: code not found")
|
||||
}
|
||||
if code != 200 {
|
||||
return nil, fmt.Errorf("DogeCloud API error: %s", res["msg"])
|
||||
}
|
||||
return res, nil
|
||||
|
||||
}
|
||||
|
||||
// DogeCloudAPI 调用多吉云的 API 根据多吉云官网示例修改
|
||||
func (a Auth) DogeCloudAPI(apiPath string, data map[string]interface{}, jsonMode bool) (map[string]interface{}, error) {
|
||||
AccessKey := a.AccessKey
|
||||
SecretKey := a.SecretKey
|
||||
|
||||
body := ""
|
||||
mime := ""
|
||||
if jsonMode {
|
||||
_body, err := json.Marshal(data)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
body = string(_body)
|
||||
mime = "application/json"
|
||||
} else {
|
||||
values := url.Values{}
|
||||
for k, v := range data {
|
||||
values.Set(k, v.(string))
|
||||
}
|
||||
body = values.Encode()
|
||||
mime = "application/x-www-form-urlencoded"
|
||||
}
|
||||
|
||||
signStr := apiPath + "\n" + body
|
||||
hmacObj := hmac.New(sha1.New, []byte(SecretKey))
|
||||
hmacObj.Write([]byte(signStr))
|
||||
sign := hex.EncodeToString(hmacObj.Sum(nil))
|
||||
Authorization := "TOKEN " + AccessKey + ":" + sign
|
||||
|
||||
req, err := http.NewRequest("POST", "https://api.dogecloud.com"+apiPath, strings.NewReader(body))
|
||||
if err != nil {
|
||||
return nil, err // 创建请求错误
|
||||
}
|
||||
req.Header.Add("Content-Type", mime)
|
||||
req.Header.Add("Authorization", Authorization)
|
||||
client := http.Client{}
|
||||
resp, err := client.Do(req)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
} // 网络错误
|
||||
defer resp.Body.Close()
|
||||
r, err := io.ReadAll(resp.Body)
|
||||
if err != nil {
|
||||
return nil, err // 读取响应错误
|
||||
}
|
||||
var result map[string]interface{}
|
||||
|
||||
err = json.Unmarshal(r, &result)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return result, nil
|
||||
}
|
|
@ -0,0 +1,21 @@
|
|||
package doge
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"os"
|
||||
"testing"
|
||||
)
|
||||
|
||||
func TestPlugin(t *testing.T) {
|
||||
err := DeployCdn(map[string]interface{}{
|
||||
"access_key": "xxxxxx",
|
||||
"secret_key": "xxxxx",
|
||||
"key": "xxxxx",
|
||||
"cert": "xxxxx",
|
||||
"domain": "xx.com",
|
||||
})
|
||||
if err != nil {
|
||||
fmt.Fprintf(os.Stderr, "调用插件失败: %v\n", err)
|
||||
os.Exit(1)
|
||||
}
|
||||
}
|
|
@ -2,6 +2,7 @@ package plugin
|
|||
|
||||
import (
|
||||
"ALLinSSL/backend/internal/access"
|
||||
"ALLinSSL/backend/public"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"strconv"
|
||||
|
@ -13,7 +14,7 @@ type CertDeployPlugin struct {
|
|||
Cert string
|
||||
}
|
||||
|
||||
func Deploy(cfg map[string]any) error {
|
||||
func Deploy(cfg map[string]any, logger *public.Logger) error {
|
||||
cert, ok := cfg["certificate"].(map[string]any)
|
||||
if !ok {
|
||||
return fmt.Errorf("证书不存在")
|
||||
|
@ -52,9 +53,9 @@ func Deploy(cfg map[string]any) error {
|
|||
if !ok {
|
||||
return fmt.Errorf("插件配置错误")
|
||||
}
|
||||
pluginParams, ok := pluginConfig["params"].(string)
|
||||
pluginParams, ok := cfg["params"].(string)
|
||||
if !ok {
|
||||
return fmt.Errorf("插件参数错误:")
|
||||
return fmt.Errorf("插件参数错误:params")
|
||||
}
|
||||
var paramsMap map[string]any
|
||||
err = json.Unmarshal([]byte(pluginParams), ¶msMap)
|
||||
|
@ -76,12 +77,13 @@ func Deploy(cfg map[string]any) error {
|
|||
return fmt.Errorf("证书错误:cert")
|
||||
}
|
||||
|
||||
params := map[string]any{
|
||||
"config": pluginConfig,
|
||||
"key": keyPem,
|
||||
"cert": certPem,
|
||||
}
|
||||
rep, err := CallPlugin(pluginName, action, params)
|
||||
pluginConfig["key"] = keyPem
|
||||
pluginConfig["cert"] = certPem
|
||||
|
||||
// 调用插件
|
||||
logger.Debug(fmt.Sprintf("调用插件%s:%s", pluginName, action))
|
||||
|
||||
rep, err := CallPlugin(pluginName, action, pluginConfig, logger)
|
||||
if err != nil {
|
||||
return fmt.Errorf("调用插件失败:%v", err)
|
||||
}
|
||||
|
|
|
@ -97,28 +97,29 @@ func getMetadata(path string) (PluginMetadata, error) {
|
|||
return meta, nil
|
||||
}
|
||||
|
||||
func CallPlugin(name, action string, params map[string]interface{}) (*Response, error) {
|
||||
func CallPlugin(name, action string, params map[string]interface{}, logger *public.Logger) (*Response, error) {
|
||||
// 第一次尝试
|
||||
resp, err := tryCallPlugin(name, action, params)
|
||||
resp, err := tryCallPlugin(name, action, params, logger)
|
||||
if err == nil {
|
||||
return resp, nil
|
||||
}
|
||||
|
||||
// 如果是插件或 action 不存在,则刷新插件列表并再试一次
|
||||
if errors.Is(err, ErrPluginNotFound) || errors.Is(err, ErrActionNotFound) {
|
||||
fmt.Println("🔄 尝试刷新插件列表...")
|
||||
logger.Debug("插件或插件内方法不存在,尝试刷新插件列表...")
|
||||
_, scanErr := scanPlugins("plugins")
|
||||
if scanErr != nil {
|
||||
logger.Error("插件刷新失败", scanErr)
|
||||
return nil, fmt.Errorf("插件刷新失败: %v", scanErr)
|
||||
}
|
||||
return tryCallPlugin(name, action, params)
|
||||
return tryCallPlugin(name, action, params, logger)
|
||||
}
|
||||
|
||||
// 其他错误直接返回
|
||||
return nil, err
|
||||
}
|
||||
|
||||
func tryCallPlugin(name, action string, params map[string]interface{}) (*Response, error) {
|
||||
func tryCallPlugin(name, action string, params map[string]interface{}, logger *public.Logger) (*Response, error) {
|
||||
plugin, ok := pluginRegistry[name]
|
||||
if !ok {
|
||||
return nil, ErrPluginNotFound
|
||||
|
@ -133,6 +134,7 @@ func tryCallPlugin(name, action string, params map[string]interface{}) (*Respons
|
|||
}
|
||||
}
|
||||
if !found {
|
||||
logger.Debug("插件不支持该 action", "plugin", name, "action", action)
|
||||
return nil, ErrActionNotFound
|
||||
}
|
||||
|
||||
|
@ -148,30 +150,37 @@ func tryCallPlugin(name, action string, params map[string]interface{}) (*Respons
|
|||
|
||||
stdin, err := cmd.StdinPipe()
|
||||
if err != nil {
|
||||
logger.Error("开启标准输入管道失败", err)
|
||||
return nil, err
|
||||
}
|
||||
stdout, err := cmd.StdoutPipe()
|
||||
if err != nil {
|
||||
logger.Error("开启标准输出管道失败", err)
|
||||
return nil, err
|
||||
}
|
||||
if err := cmd.Start(); err != nil {
|
||||
logger.Error("启动插件失败", err)
|
||||
return nil, err
|
||||
}
|
||||
|
||||
if err := json.NewEncoder(stdin).Encode(req); err != nil {
|
||||
logger.Error("发送插件请求失败", err)
|
||||
return nil, err
|
||||
}
|
||||
stdin.Close()
|
||||
|
||||
respBytes, err := io.ReadAll(stdout)
|
||||
if err != nil {
|
||||
logger.Error("读取插件响应失败", err)
|
||||
return nil, err
|
||||
}
|
||||
var resp Response
|
||||
if err := json.Unmarshal(respBytes, &resp); err != nil {
|
||||
logger.Error("解析插件响应失败", err, "内容", string(respBytes))
|
||||
return nil, fmt.Errorf("解析插件响应失败: %v\n内容: %s", err, respBytes)
|
||||
}
|
||||
cmd.Wait()
|
||||
logger.Debug("插件响应", "plugin", name, "action", action, "response", resp)
|
||||
|
||||
return &resp, nil
|
||||
}
|
||||
|
|
|
@ -1,9 +1,11 @@
|
|||
package public
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"log"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"strings"
|
||||
"sync"
|
||||
"time"
|
||||
)
|
||||
|
@ -89,27 +91,33 @@ func (l *Logger) Close() {
|
|||
}
|
||||
|
||||
// write 写日志,内部使用锁保证线程安全
|
||||
func (l *Logger) write(level string, msg string) {
|
||||
func (l *Logger) write(level string, args ...interface{}) {
|
||||
l.mutex.Lock()
|
||||
defer l.mutex.Unlock()
|
||||
|
||||
timestamp := time.Now().Format("2006-01-02 15:04:05")
|
||||
logLine := "[" + level + "] " + timestamp + " - " + msg
|
||||
message := fmt.Sprintln(args...) // 自动拼接参数并换行
|
||||
logLine := "[" + level + "] " + timestamp + " - " + message
|
||||
logLine = strings.TrimRight(logLine, "\n") // 去掉 Sprintln 自动加的换行
|
||||
l.logger.Println(logLine)
|
||||
}
|
||||
|
||||
// Info 输出 info 级别日志
|
||||
func (l *Logger) Info(msg string) {
|
||||
l.write("INFO", msg)
|
||||
func (l *Logger) Info(args ...interface{}) {
|
||||
l.write("INFO", args...)
|
||||
}
|
||||
|
||||
// Error 输出 error 级别日志
|
||||
func (l *Logger) Error(msg string) {
|
||||
l.write("ERROR", msg)
|
||||
func (l *Logger) Error(args ...interface{}) {
|
||||
l.write("ERROR", args...)
|
||||
}
|
||||
func (l *Logger) Debug(msg string) {
|
||||
l.write("Debug", msg)
|
||||
|
||||
// Debug 输出 debug 级别日志
|
||||
func (l *Logger) Debug(args ...interface{}) {
|
||||
l.write("DEBUG", args...)
|
||||
}
|
||||
|
||||
// 获取底层 logger 实例
|
||||
func (l *Logger) GetLogger() *log.Logger {
|
||||
return l.logger
|
||||
}
|
||||
|
|
Loading…
Reference in New Issue