mirror of https://github.com/allinssl/allinssl
				
				
				
			插件示例-阿里云
							parent
							
								
									b9f457e7ba
								
							
						
					
					
						commit
						c18d4accfb
					
				| 
						 | 
				
			
			@ -30,6 +30,7 @@ type PluginMetadata struct {
 | 
			
		|||
	Version     string         `json:"version"`
 | 
			
		||||
	Author      string         `json:"author"`
 | 
			
		||||
	Actions     []ActionInfo   `json:"actions"`
 | 
			
		||||
	Config      map[string]any `json:"config,omitempty"` // 可选配置
 | 
			
		||||
	Path        string         // 插件路径
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
										
											Binary file not shown.
										
									
								
							| 
						 | 
				
			
			@ -0,0 +1,156 @@
 | 
			
		|||
package main
 | 
			
		||||
 | 
			
		||||
import (
 | 
			
		||||
	"ALLinSSL/plugins/aliyun/cas"
 | 
			
		||||
	"ALLinSSL/plugins/aliyun/esa"
 | 
			
		||||
	"fmt"
 | 
			
		||||
	"strconv"
 | 
			
		||||
	"strings"
 | 
			
		||||
)
 | 
			
		||||
 | 
			
		||||
func uploadToCAS(cfg map[string]any) (*Response, error) {
 | 
			
		||||
	if cfg == nil {
 | 
			
		||||
		return nil, fmt.Errorf("config cannot be nil")
 | 
			
		||||
	}
 | 
			
		||||
	certStr, ok := cfg["cert"].(string)
 | 
			
		||||
	if !ok || certStr == "" {
 | 
			
		||||
		return nil, fmt.Errorf("cert is required and must be a string")
 | 
			
		||||
	}
 | 
			
		||||
	keyStr, ok := cfg["key"].(string)
 | 
			
		||||
	if !ok || keyStr == "" {
 | 
			
		||||
		return nil, fmt.Errorf("key is required and must be a string")
 | 
			
		||||
	}
 | 
			
		||||
	accessKey, ok := cfg["access_key"].(string)
 | 
			
		||||
	if !ok || accessKey == "" {
 | 
			
		||||
		return nil, fmt.Errorf("access_key is required and must be a string")
 | 
			
		||||
	}
 | 
			
		||||
	secretKey, ok := cfg["secret_key"].(string)
 | 
			
		||||
	if !ok || secretKey == "" {
 | 
			
		||||
		return nil, fmt.Errorf("secret_key is required and must be a string")
 | 
			
		||||
	}
 | 
			
		||||
	endpoint, ok := cfg["endpoint"].(string)
 | 
			
		||||
	if !ok || endpoint == "" {
 | 
			
		||||
		endpoint = "cas.ap-southeast-1.aliyuncs.com" // 默认值
 | 
			
		||||
	}
 | 
			
		||||
	name, ok := cfg["name"].(string)
 | 
			
		||||
	if !ok || name == "" {
 | 
			
		||||
		name = "allinssl-certificate" // 默认名称
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	client, err := cas.CreateClient(accessKey, secretKey, endpoint)
 | 
			
		||||
	if err != nil {
 | 
			
		||||
		return nil, fmt.Errorf("failed to create CAS client: %w", err)
 | 
			
		||||
	}
 | 
			
		||||
	// 上传证书到 CAS
 | 
			
		||||
	err = cas.UploadToCas(client, certStr, keyStr, name)
 | 
			
		||||
	if err != nil {
 | 
			
		||||
		return nil, fmt.Errorf("failed to upload certificate to CAS: %w", err)
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	return &Response{
 | 
			
		||||
		Status:  "success",
 | 
			
		||||
		Message: "CAS upload successful",
 | 
			
		||||
		Result:  nil,
 | 
			
		||||
	}, nil
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
func deployToESA(cfg map[string]any) (*Response, error) {
 | 
			
		||||
	if cfg == nil {
 | 
			
		||||
		return nil, fmt.Errorf("config cannot be nil")
 | 
			
		||||
	}
 | 
			
		||||
	certPEM, ok := cfg["cert"].(string)
 | 
			
		||||
	if !ok || certPEM == "" {
 | 
			
		||||
		return nil, fmt.Errorf("cert is required and must be a string")
 | 
			
		||||
	}
 | 
			
		||||
	privkeyPEM, ok := cfg["key"].(string)
 | 
			
		||||
	if !ok || privkeyPEM == "" {
 | 
			
		||||
		return nil, fmt.Errorf("key is required and must be a string")
 | 
			
		||||
	}
 | 
			
		||||
	accessKey, ok := cfg["access_key"].(string)
 | 
			
		||||
	if !ok || accessKey == "" {
 | 
			
		||||
		return nil, fmt.Errorf("access_key is required and must be a string")
 | 
			
		||||
	}
 | 
			
		||||
	secretKey, ok := cfg["secret_key"].(string)
 | 
			
		||||
	if !ok || secretKey == "" {
 | 
			
		||||
		return nil, fmt.Errorf("secret_key is required and must be a string")
 | 
			
		||||
	}
 | 
			
		||||
	var siteID int64
 | 
			
		||||
	switch v := cfg["site_id"].(type) {
 | 
			
		||||
	case float64:
 | 
			
		||||
		siteID = int64(v)
 | 
			
		||||
	case string:
 | 
			
		||||
		var err error
 | 
			
		||||
		siteID, err = strconv.ParseInt(v, 10, 64)
 | 
			
		||||
		if err != nil {
 | 
			
		||||
			return nil, fmt.Errorf("site_id format error: %w", err)
 | 
			
		||||
		}
 | 
			
		||||
	case int:
 | 
			
		||||
		siteID = int64(v)
 | 
			
		||||
	default:
 | 
			
		||||
		return nil, fmt.Errorf("site_id format error")
 | 
			
		||||
	}
 | 
			
		||||
	var delRepeatDomainCert bool
 | 
			
		||||
	switch v := cfg["del_repeat_domain_cert"].(type) {
 | 
			
		||||
	case bool:
 | 
			
		||||
		delRepeatDomainCert = v
 | 
			
		||||
	case string:
 | 
			
		||||
		if v == "true" {
 | 
			
		||||
			delRepeatDomainCert = true
 | 
			
		||||
		}
 | 
			
		||||
	case nil:
 | 
			
		||||
		delRepeatDomainCert = false
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	client, err := esa.CreateEsaClient(accessKey, secretKey)
 | 
			
		||||
	if err != nil {
 | 
			
		||||
		return nil, fmt.Errorf("failed to create ESA client: %w", err)
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	// 检查是否需要删除重复的域名证书
 | 
			
		||||
	if delRepeatDomainCert {
 | 
			
		||||
		// 解析现有证书的域名
 | 
			
		||||
		certObj, err := ParseCertificate([]byte(certPEM))
 | 
			
		||||
		if err != nil {
 | 
			
		||||
			return nil, fmt.Errorf("failed to parse certificate: %w", err)
 | 
			
		||||
		}
 | 
			
		||||
		domainSet := make(map[string]bool)
 | 
			
		||||
 | 
			
		||||
		if certObj.Subject.CommonName != "" {
 | 
			
		||||
			domainSet[certObj.Subject.CommonName] = true
 | 
			
		||||
		}
 | 
			
		||||
		for _, dns := range certObj.DNSNames {
 | 
			
		||||
			domainSet[dns] = true
 | 
			
		||||
		}
 | 
			
		||||
 | 
			
		||||
		// 转成切片并拼接成逗号分隔的字符串
 | 
			
		||||
		var domains []string
 | 
			
		||||
		for domain := range domainSet {
 | 
			
		||||
			domains = append(domains, domain)
 | 
			
		||||
		}
 | 
			
		||||
		domainList := strings.Join(domains, ",")
 | 
			
		||||
 | 
			
		||||
		certList, err := esa.ListCertFromESA(client, siteID)
 | 
			
		||||
		if err != nil {
 | 
			
		||||
			return nil, fmt.Errorf("failed to list certificates from ESA: %w", err)
 | 
			
		||||
		}
 | 
			
		||||
		for _, cert := range certList {
 | 
			
		||||
			if *cert.SAN == domainList {
 | 
			
		||||
				err = esa.DeleteEsaCert(client, siteID, *cert.Id)
 | 
			
		||||
				if err != nil {
 | 
			
		||||
					return nil, fmt.Errorf("failed to delete existing certificate: %w", err)
 | 
			
		||||
				}
 | 
			
		||||
			}
 | 
			
		||||
		}
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	err = esa.UploadCertToESA(client, siteID, certPEM, privkeyPEM)
 | 
			
		||||
	if err != nil {
 | 
			
		||||
		return nil, fmt.Errorf("failed to upload certificate to ESA: %w", err)
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	return &Response{
 | 
			
		||||
		Status:  "success",
 | 
			
		||||
		Message: "ESA deployment successful",
 | 
			
		||||
		Result:  nil,
 | 
			
		||||
	}, nil
 | 
			
		||||
}
 | 
			
		||||
| 
						 | 
				
			
			@ -0,0 +1,31 @@
 | 
			
		|||
package cas
 | 
			
		||||
 | 
			
		||||
import (
 | 
			
		||||
	cas "github.com/alibabacloud-go/cas-20200407/v4/client"
 | 
			
		||||
	openapi "github.com/alibabacloud-go/darabonba-openapi/v2/client"
 | 
			
		||||
	util "github.com/alibabacloud-go/tea-utils/v2/service"
 | 
			
		||||
	"github.com/alibabacloud-go/tea/tea"
 | 
			
		||||
)
 | 
			
		||||
 | 
			
		||||
func CreateClient(accessKey, accessSecret, endpoint string) (*cas.Client, error) {
 | 
			
		||||
	if endpoint == "" {
 | 
			
		||||
		endpoint = "cas.ap-southeast-1.aliyuncs.com"
 | 
			
		||||
	}
 | 
			
		||||
	config := &openapi.Config{
 | 
			
		||||
		AccessKeyId:     tea.String(accessKey),
 | 
			
		||||
		AccessKeySecret: tea.String(accessSecret),
 | 
			
		||||
		Endpoint:        tea.String(endpoint),
 | 
			
		||||
	}
 | 
			
		||||
	return cas.NewClient(config)
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
func UploadToCas(client *cas.Client, cert, key, name string) error {
 | 
			
		||||
	uploadUserCertificateRequest := &cas.UploadUserCertificateRequest{
 | 
			
		||||
		Name: tea.String(name),
 | 
			
		||||
		Cert: tea.String(cert),
 | 
			
		||||
		Key:  tea.String(key),
 | 
			
		||||
	}
 | 
			
		||||
	runtime := &util.RuntimeOptions{}
 | 
			
		||||
	_, err := client.UploadUserCertificateWithOptions(uploadUserCertificateRequest, runtime)
 | 
			
		||||
	return err
 | 
			
		||||
}
 | 
			
		||||
| 
						 | 
				
			
			@ -0,0 +1,63 @@
 | 
			
		|||
package esa
 | 
			
		||||
 | 
			
		||||
import (
 | 
			
		||||
	openapi "github.com/alibabacloud-go/darabonba-openapi/v2/client"
 | 
			
		||||
	esa "github.com/alibabacloud-go/esa-20240910/v2/client"
 | 
			
		||||
	util "github.com/alibabacloud-go/tea-utils/v2/service"
 | 
			
		||||
	"github.com/alibabacloud-go/tea/tea"
 | 
			
		||||
)
 | 
			
		||||
 | 
			
		||||
// CreateEsaClient creates a new ESA client with the provided access key and secret.
 | 
			
		||||
func CreateEsaClient(accessKey, accessSecret string) (*esa.Client, error) {
 | 
			
		||||
	config := &openapi.Config{
 | 
			
		||||
		AccessKeyId:     tea.String(accessKey),
 | 
			
		||||
		AccessKeySecret: tea.String(accessSecret),
 | 
			
		||||
		Endpoint:        tea.String("esa.ap-southeast-1.aliyuncs.com"),
 | 
			
		||||
	}
 | 
			
		||||
	return esa.NewClient(config)
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
// UploadCertToESA uploads the certificate and private key to Alibaba Cloud ESA.
 | 
			
		||||
func UploadCertToESA(client *esa.Client, id int64, certPEM, privkeyPEM string) error {
 | 
			
		||||
	req := esa.SetCertificateRequest{
 | 
			
		||||
		SiteId:      tea.Int64(id),
 | 
			
		||||
		Type:        tea.String("upload"),
 | 
			
		||||
		Certificate: tea.String(certPEM),
 | 
			
		||||
		PrivateKey:  tea.String(privkeyPEM),
 | 
			
		||||
	}
 | 
			
		||||
	runtime := &util.RuntimeOptions{}
 | 
			
		||||
 | 
			
		||||
	_, err := client.SetCertificateWithOptions(&req, runtime)
 | 
			
		||||
	if err != nil {
 | 
			
		||||
		return err
 | 
			
		||||
	}
 | 
			
		||||
	return nil
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
// ListCertFromESA retrieves the list of certificates from Alibaba Cloud ESA for a given site ID.
 | 
			
		||||
func ListCertFromESA(client *esa.Client, id int64) ([]*esa.ListCertificatesResponseBodyResult, error) {
 | 
			
		||||
	req := esa.ListCertificatesRequest{
 | 
			
		||||
		SiteId: tea.Int64(id),
 | 
			
		||||
	}
 | 
			
		||||
	runtime := &util.RuntimeOptions{}
 | 
			
		||||
	resp, err := client.ListCertificatesWithOptions(&req, runtime)
 | 
			
		||||
	if err != nil {
 | 
			
		||||
		return nil, err
 | 
			
		||||
	}
 | 
			
		||||
	return resp.Body.Result, nil
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
// DeleteEsaCert deletes a certificate from Alibaba Cloud ESA by its ID.
 | 
			
		||||
func DeleteEsaCert(client *esa.Client, id int64, certID string) error {
 | 
			
		||||
	req := esa.DeleteCertificateRequest{
 | 
			
		||||
		SiteId: tea.Int64(id),
 | 
			
		||||
		Id:     tea.String(certID),
 | 
			
		||||
	}
 | 
			
		||||
	runtime := &util.RuntimeOptions{}
 | 
			
		||||
 | 
			
		||||
	_, err := client.DeleteCertificateWithOptions(&req, runtime)
 | 
			
		||||
	if err != nil {
 | 
			
		||||
		return err
 | 
			
		||||
	}
 | 
			
		||||
	return nil
 | 
			
		||||
}
 | 
			
		||||
| 
						 | 
				
			
			@ -0,0 +1,123 @@
 | 
			
		|||
package main
 | 
			
		||||
 | 
			
		||||
import (
 | 
			
		||||
	"crypto/x509"
 | 
			
		||||
	"encoding/json"
 | 
			
		||||
	"encoding/pem"
 | 
			
		||||
	"fmt"
 | 
			
		||||
	"io"
 | 
			
		||||
	"os"
 | 
			
		||||
)
 | 
			
		||||
 | 
			
		||||
type ActionInfo struct {
 | 
			
		||||
	Name        string         `json:"name"`
 | 
			
		||||
	Description string         `json:"description"`
 | 
			
		||||
	Params      map[string]any `json:"params,omitempty"` // 可选参数
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
type Request struct {
 | 
			
		||||
	Action string                 `json:"action"`
 | 
			
		||||
	Params map[string]interface{} `json:"params"`
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
type Response struct {
 | 
			
		||||
	Status  string                 `json:"status"`
 | 
			
		||||
	Message string                 `json:"message"`
 | 
			
		||||
	Result  map[string]interface{} `json:"result"`
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
var pluginMeta = map[string]interface{}{
 | 
			
		||||
	"name":        "aliyun",
 | 
			
		||||
	"description": "部署到阿里云",
 | 
			
		||||
	"version":     "1.0.0",
 | 
			
		||||
	"author":      "主包",
 | 
			
		||||
	"config": map[string]interface{}{
 | 
			
		||||
		"access_key": "阿里云 AccessKey",
 | 
			
		||||
		"secret_key": "阿里云 SecretKey",
 | 
			
		||||
	},
 | 
			
		||||
	"actions": []ActionInfo{
 | 
			
		||||
		{
 | 
			
		||||
			Name:        "deployToESA",
 | 
			
		||||
			Description: "部署到阿里云esa",
 | 
			
		||||
			Params: map[string]any{
 | 
			
		||||
				"site_id":                "站点 ID",
 | 
			
		||||
				"del_repeat_domain_cert": "是否删除重复的域名证书,默认 false",
 | 
			
		||||
			},
 | 
			
		||||
		},
 | 
			
		||||
		{
 | 
			
		||||
			Name:        "uploadToCAS",
 | 
			
		||||
			Description: "上传到阿里云cas",
 | 
			
		||||
			Params: map[string]any{
 | 
			
		||||
				"name": "证书名称",
 | 
			
		||||
			},
 | 
			
		||||
		},
 | 
			
		||||
	},
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
// **解析 PEM 格式的证书**
 | 
			
		||||
func ParseCertificate(certPEM []byte) (*x509.Certificate, error) {
 | 
			
		||||
	block, _ := pem.Decode(certPEM)
 | 
			
		||||
	if block == nil {
 | 
			
		||||
		return nil, fmt.Errorf("无法解析证书 PEM")
 | 
			
		||||
	}
 | 
			
		||||
	return x509.ParseCertificate(block.Bytes)
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
func outputJSON(resp *Response) {
 | 
			
		||||
	_ = json.NewEncoder(os.Stdout).Encode(resp)
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
func outputError(msg string, err error) {
 | 
			
		||||
	outputJSON(&Response{
 | 
			
		||||
		Status:  "error",
 | 
			
		||||
		Message: fmt.Sprintf("%s: %v", msg, err),
 | 
			
		||||
	})
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
func main() {
 | 
			
		||||
	var req Request
 | 
			
		||||
	input, err := io.ReadAll(os.Stdin)
 | 
			
		||||
	if err != nil {
 | 
			
		||||
		outputError("读取输入失败", err)
 | 
			
		||||
		return
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	if err := json.Unmarshal(input, &req); err != nil {
 | 
			
		||||
		outputError("解析请求失败", err)
 | 
			
		||||
		return
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	switch req.Action {
 | 
			
		||||
	case "get_metadata":
 | 
			
		||||
		outputJSON(&Response{
 | 
			
		||||
			Status:  "success",
 | 
			
		||||
			Message: "插件信息",
 | 
			
		||||
			Result:  pluginMeta,
 | 
			
		||||
		})
 | 
			
		||||
	case "list_actions":
 | 
			
		||||
		outputJSON(&Response{
 | 
			
		||||
			Status:  "success",
 | 
			
		||||
			Message: "支持的动作",
 | 
			
		||||
			Result:  map[string]interface{}{"actions": pluginMeta["actions"]},
 | 
			
		||||
		})
 | 
			
		||||
	case "deployToESA":
 | 
			
		||||
		rep, err := deployToESA(req.Params)
 | 
			
		||||
		if err != nil {
 | 
			
		||||
			outputError("ESA 部署失败", err)
 | 
			
		||||
			return
 | 
			
		||||
		}
 | 
			
		||||
		outputJSON(rep)
 | 
			
		||||
	case "uploadToCAS":
 | 
			
		||||
		rep, err := uploadToCAS(req.Params)
 | 
			
		||||
		if err != nil {
 | 
			
		||||
			outputError("CAS 上传失败", err)
 | 
			
		||||
			return
 | 
			
		||||
		}
 | 
			
		||||
		outputJSON(rep)
 | 
			
		||||
	default:
 | 
			
		||||
		outputJSON(&Response{
 | 
			
		||||
			Status:  "error",
 | 
			
		||||
			Message: "未知 action: " + req.Action,
 | 
			
		||||
		})
 | 
			
		||||
	}
 | 
			
		||||
}
 | 
			
		||||
		Loading…
	
		Reference in New Issue