mirror of https://github.com/allinssl/allinssl
插件示例-阿里云
parent
b9f457e7ba
commit
c18d4accfb
|
@ -25,12 +25,13 @@ type ActionInfo struct {
|
||||||
}
|
}
|
||||||
|
|
||||||
type PluginMetadata struct {
|
type PluginMetadata struct {
|
||||||
Name string `json:"name"`
|
Name string `json:"name"`
|
||||||
Description string `json:"description"`
|
Description string `json:"description"`
|
||||||
Version string `json:"version"`
|
Version string `json:"version"`
|
||||||
Author string `json:"author"`
|
Author string `json:"author"`
|
||||||
Actions []ActionInfo `json:"actions"`
|
Actions []ActionInfo `json:"actions"`
|
||||||
Path string // 插件路径
|
Config map[string]any `json:"config,omitempty"` // 可选配置
|
||||||
|
Path string // 插件路径
|
||||||
}
|
}
|
||||||
|
|
||||||
type Request struct {
|
type Request struct {
|
||||||
|
|
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