【修复】长期持有tcp连接未关闭

【新增】支持通过webhook调用自己的服务解析dns记录
【新增】支持通过webhook推送证书和密钥
【新增】导入导出工作流、通知、证书、api授权数据
【新增】支持自定义插件目录
pull/348/head
v-me-50 2025-08-14 16:41:29 +08:00
parent e939724f37
commit 6d0732fc31
16 changed files with 377 additions and 42 deletions

View File

@ -4,6 +4,8 @@ import (
"ALLinSSL/backend/internal/setting"
"ALLinSSL/backend/public"
"github.com/gin-gonic/gin"
"os"
"path/filepath"
)
func GetSetting(c *gin.Context) {
@ -47,3 +49,40 @@ func GetVersion(c *gin.Context) {
}
public.SuccessData(c, data, 0)
}
func DownloadData(c *gin.Context) {
dbPath := "data/data.db"
dbName := filepath.Base(dbPath)
// 设置响应头,让浏览器下载文件
c.Header("Content-Type", "application/octet-stream")
c.Header("Content-Disposition", "attachment; filename=\""+dbName+"\"")
c.File(dbPath)
}
func UploadData(c *gin.Context) {
file, err := c.FormFile("file")
if err != nil {
public.FailMsg(c, "文件上传失败: "+err.Error())
return
}
// 检查文件类型
if filepath.Ext(file.Filename) != ".db" {
public.FailMsg(c, "只允许上传 .db 文件")
return
}
// 备份源文件
// 修改源文件名为 data.db.bak
err = os.Rename("data/data.db", "data/data.db.bak")
if err != nil {
public.FailMsg(c, "备份源文件失败: "+err.Error())
return
}
if err := c.SaveUploadedFile(file, "data/data.db"); err != nil {
public.FailMsg(c, "保存文件失败: "+err.Error())
return
}
public.SuccessMsg(c, "数据上传成功")
}

View File

@ -4,7 +4,9 @@ import (
"ALLinSSL/backend/internal/access"
"ALLinSSL/backend/internal/cert"
"ALLinSSL/backend/internal/cert/apply/lego/jdcloud"
"ALLinSSL/backend/internal/cert/apply/lego/webhook"
"ALLinSSL/backend/public"
"crypto/tls"
"encoding/json"
"fmt"
azcorecloud "github.com/Azure/azure-sdk-for-go/sdk/azcore/cloud"
@ -202,6 +204,10 @@ func GetDNSProvider(providerName string, creds map[string]string, httpClient *ht
config.SecretKey = creds["secret_key"]
config.PropagationTimeout = maxWait
return constellix.NewDNSProviderConfig(config)
case "webhook":
config := webhook.NewConfig(creds)
config.PropagationTimeout = maxWait
return webhook.NewDNSProviderConfig(config)
default:
return nil, fmt.Errorf("不支持的 DNS Provider: %s", providerName)
@ -495,7 +501,9 @@ func Apply(cfg map[string]any, logger *public.Logger) (map[string]any, error) {
}
httpClient = &http.Client{
Transport: &http.Transport{
Proxy: http.ProxyURL(proxyURL),
Proxy: http.ProxyURL(proxyURL),
TLSClientConfig: &tls.Config{InsecureSkipVerify: true},
DisableKeepAlives: true,
},
Timeout: 30 * time.Second,
}

View File

@ -0,0 +1,77 @@
package webhook
import (
"ALLinSSL/backend/public"
"fmt"
"github.com/go-acme/lego/v4/challenge/dns01"
"time"
)
var configData string
type Config struct {
WebhookConfig *public.WebhookConfig
PropagationTimeout time.Duration
PollingInterval time.Duration
TTL int
HTTPTimeout time.Duration
}
type DNSProvider struct {
config *Config
}
func NewConfig(WebhookConfigStr map[string]string) *Config {
fmt.Println(WebhookConfigStr)
WebhookConfig := &public.WebhookConfig{
Url: WebhookConfigStr["url"],
Data: WebhookConfigStr["data"],
Method: WebhookConfigStr["method"],
Headers: WebhookConfigStr["headers"],
IgnoreSSL: WebhookConfigStr["ignore_ssl"] == "true",
}
fmt.Println(WebhookConfig.Url)
return &Config{
WebhookConfig: WebhookConfig,
TTL: 600,
PropagationTimeout: dns01.DefaultPropagationTimeout,
PollingInterval: dns01.DefaultPollingInterval,
HTTPTimeout: 30 * time.Second,
}
}
func NewDNSProviderConfig(config *Config) (*DNSProvider, error) {
if config == nil {
return nil, fmt.Errorf("配置不能为空")
}
return &DNSProvider{config: config}, nil
}
func (d *DNSProvider) Timeout() (timeout, interval time.Duration) {
return d.config.PropagationTimeout, d.config.PollingInterval
}
func (d *DNSProvider) Present(domain, token, keyAuth string) error {
fmt.Println(d.config.WebhookConfig.Url)
configData = d.config.WebhookConfig.Data
return d.send(domain, token, keyAuth, "present")
}
func (d *DNSProvider) CleanUp(domain, token, keyAuth string) error {
d.config.WebhookConfig.Data = configData
return d.send(domain, token, keyAuth, "cleanup")
}
func (d *DNSProvider) send(domain, token, keyAuth, action string) error {
info := dns01.GetChallengeInfo(domain, keyAuth)
data, err := public.ReplaceJSONPlaceholders(d.config.WebhookConfig.Data, map[string]interface{}{"domain": info.EffectiveFQDN, "token": token, "keyAuth": info.Value, "action": action})
if err != nil {
return fmt.Errorf("替换JSON占位符失败: %w", err)
}
d.config.WebhookConfig.Data = data
return d.config.WebhookConfig.Send()
}

View File

@ -91,7 +91,8 @@ func Request1panel(data *map[string]any, method, providerID, requestUrl string)
ignoreSsl = true
}
tr := &http.Transport{
TLSClientConfig: &tls.Config{InsecureSkipVerify: ignoreSsl},
TLSClientConfig: &tls.Config{InsecureSkipVerify: ignoreSsl},
DisableKeepAlives: true,
}
client := &http.Client{Transport: tr}
@ -269,7 +270,7 @@ func OnePanelSiteList(providerID string) ([]response.AccessSiteList, error) {
if err != nil {
return nil, fmt.Errorf("获取网站列表失败 %v", err)
}
var result []response.AccessSiteList
sites, ok := siteList["data"].(map[string]any)["items"].([]any)
if !ok {

View File

@ -65,7 +65,8 @@ func RequestBt(data *url.Values, method, providerID, requestUrl string) (map[str
ignoreSsl = true
}
tr := &http.Transport{
TLSClientConfig: &tls.Config{InsecureSkipVerify: ignoreSsl},
TLSClientConfig: &tls.Config{InsecureSkipVerify: ignoreSsl},
DisableKeepAlives: true,
}
client := &http.Client{Transport: tr}

View File

@ -65,7 +65,8 @@ func RequestBtWaf(data *map[string]any, method, providerID, requestUrl string) (
ignoreSsl = true
}
tr := &http.Transport{
TLSClientConfig: &tls.Config{InsecureSkipVerify: ignoreSsl},
TLSClientConfig: &tls.Config{InsecureSkipVerify: ignoreSsl},
DisableKeepAlives: true,
}
client := &http.Client{Transport: tr}
@ -207,4 +208,4 @@ func BtWafAPITest(providerID string) error {
return fmt.Errorf("测试请求失败: %v", err)
}
return nil
}
}

View File

@ -5,6 +5,7 @@ import (
"ALLinSSL/backend/internal/cert/deploy/doge"
"ALLinSSL/backend/internal/cert/deploy/lecdn"
"ALLinSSL/backend/internal/cert/deploy/plugin"
"ALLinSSL/backend/internal/cert/deploy/webhook"
"ALLinSSL/backend/public"
"fmt"
)
@ -106,6 +107,9 @@ func Deploy(cfg map[string]any, logger *public.Logger) error {
case "plugin":
logger.Debug("使用插件部署...")
return plugin.Deploy(cfg, logger)
case "webhook":
logger.Debug("通过Webhook推送证书...")
return webhook.Deploy(cfg)
default:
return fmt.Errorf("不支持的部署: %s", providerName)
}

View File

@ -53,7 +53,8 @@ func requestLecdn(url, method, token string, params map[string]any, ignoreSsl bo
client := &http.Client{
Transport: &http.Transport{
TLSClientConfig: &tls.Config{InsecureSkipVerify: ignoreSsl},
TLSClientConfig: &tls.Config{InsecureSkipVerify: ignoreSsl},
DisableKeepAlives: true,
},
}

View File

@ -50,7 +50,8 @@ func RequestSafeLineWaf(data *map[string]any, method, providerID, requestUrl str
ignoreSsl = true
}
tr := &http.Transport{
TLSClientConfig: &tls.Config{InsecureSkipVerify: ignoreSsl},
TLSClientConfig: &tls.Config{InsecureSkipVerify: ignoreSsl},
DisableKeepAlives: true,
}
client := &http.Client{Transport: tr}
@ -211,4 +212,4 @@ func SafeLineAPITest(providerID string) error {
return fmt.Errorf("测试请求失败: %v", err)
}
return nil
}
}

View File

@ -0,0 +1,57 @@
package webhook
import (
"ALLinSSL/backend/internal/access"
"ALLinSSL/backend/public"
"encoding/json"
"fmt"
"strconv"
)
func Deploy(cfg map[string]any) error {
cert, ok := cfg["certificate"].(map[string]any)
if !ok {
return fmt.Errorf("证书不存在")
}
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, ok := providerData["config"].(string)
if !ok {
return fmt.Errorf("api配置错误")
}
// 解析 JSON 配置
var providerConfig public.WebhookConfig
err = json.Unmarshal([]byte(providerConfigStr), &providerConfig)
if err != nil {
return err
}
certStr, ok := cert["cert"].(string)
if !ok || certStr == "" {
return fmt.Errorf("cert is required and must be a string")
}
keyStr, ok := cert["key"].(string)
if !ok || keyStr == "" {
return fmt.Errorf("key is required and must be a string")
}
data, err := public.ReplaceJSONPlaceholders(providerConfig.Data, map[string]interface{}{"key": keyStr, "cert": certStr})
if err != nil {
return fmt.Errorf("替换JSON占位符失败: %w", err)
}
providerConfig.Data = data
return providerConfig.Send()
}

View File

@ -69,8 +69,8 @@ func Check(certs []*x509.Certificate, host string, advanceDay int) (result *Cert
}
result.CommonName = leafCert.Subject.CommonName
result.NotBefore = leafCert.NotBefore.Format("2006-01-02 15:04:05")
result.NotAfter = leafCert.NotAfter.Format("2006-01-02 15:04:05")
result.NotBefore = leafCert.NotBefore.In(time.Local).Format("2006-01-02 15:04:05")
result.NotAfter = leafCert.NotAfter.In(time.Local).Format("2006-01-02 15:04:05")
result.DaysLeft = int(leafCert.NotAfter.Sub(time.Now()).Hours() / 24)
result.SANs = strings.Join(leafCert.DNSNames, ",")
result.SignatureAlgo = leafCert.SignatureAlgorithm.String()
@ -141,8 +141,9 @@ func CheckHttps(target string, advanceDay int) (result *CertInfo, err error) {
TLSClientConfig: &tls.Config{
InsecureSkipVerify: true,
},
DisableKeepAlives: true,
},
//Timeout: 5 * time.Second,
Timeout: 30 * time.Second,
}
// 发送请求

View File

@ -8,7 +8,6 @@ import (
"fmt"
"github.com/go-resty/resty/v2"
"net/http"
"regexp"
"strings"
"time"
)
@ -184,7 +183,7 @@ func NotifyWebHook(params map[string]any) error {
if err != nil {
return fmt.Errorf("解析配置失败: %v", err)
}
config.Data, err = ReplaceJSONPlaceholders(config.Data, params)
config.Data, err = public.ReplaceJSONPlaceholders(config.Data, params)
if err != nil {
return fmt.Errorf("替换JSON占位符失败: %w", err)
}
@ -197,16 +196,3 @@ func NotifyWebHook(params map[string]any) error {
}
return nil
}
func ReplaceJSONPlaceholders(jsonStr string, vars map[string]any) (string, error) {
re := regexp.MustCompile(`__([a-zA-Z0-9_]+)__`)
result := re.ReplaceAllStringFunc(jsonStr, func(match string) string {
key := re.FindStringSubmatch(match)[1]
if val, ok := vars[key]; ok {
return fmt.Sprintf("%v", val) // 将 any 类型转换为字符串
}
return match // 未匹配到变量则保留原样
})
return result, nil
}

View File

@ -1,6 +1,7 @@
package report
import (
"ALLinSSL/backend/public"
"encoding/json"
"fmt"
"io"
@ -75,7 +76,7 @@ func NotifyWorkWx(params map[string]any) error {
}
`
}
msg, err := ReplaceJSONPlaceholders(config["data"], params)
msg, err := public.ReplaceJSONPlaceholders(config["data"], params)
if err != nil {
return fmt.Errorf("替换JSON占位符失败: %v", err)
}

View File

@ -16,13 +16,14 @@ import (
)
type Setting struct {
Timeout int `json:"timeout" form:"timeout"`
Secure string `json:"secure" form:"secure"`
Https string `json:"https" form:"https"`
Key string `json:"key" form:"key"`
Cert string `json:"cert" form:"cert"`
Username string `json:"username" form:"username"`
Password string `json:"password" form:"password"`
Timeout int `json:"timeout" form:"timeout"`
Secure string `json:"secure" form:"secure"`
Https string `json:"https" form:"https"`
Key string `json:"key" form:"key"`
Cert string `json:"cert" form:"cert"`
Username string `json:"username" form:"username"`
Password string `json:"password" form:"password"`
PluginPath string `json:"plugin_path" form:"plugin_path"`
}
func Get() (Setting, error) {
@ -57,6 +58,7 @@ func Get() (Setting, error) {
}
username := data[0]["username"].(string)
setting.Username = username
setting.PluginPath = public.GetSettingIgnoreError("plugin_dir")
return setting, nil
}
@ -108,6 +110,9 @@ func Save(setting *Setting) error {
public.TimeOut = setting.Timeout
restart = true
}
if setting.PluginPath != "" && setting.PluginPath != public.GetSettingIgnoreError("plugin_dir") {
public.UpdateSetting("plugin_dir", setting.PluginPath)
}
if setting.Https != "" {
if setting.Https == "1" {
if setting.Key == "" || setting.Cert == "" {
@ -192,13 +197,16 @@ func GetVersion() (map[string]string, error) {
update := "0"
newVersionObj, err := http.Get("https://download.allinssl.com/version.json")
if err != nil {
return map[string]string{
"version": version,
"new_version": version,
"update": update,
"log": "",
"date": "",
}, nil
newVersionObj, err = http.Get("https://node1.allinssl.com/version.json")
if err != nil {
return map[string]string{
"version": version,
"new_version": version,
"update": update,
"log": "",
"date": "",
}, nil
}
}
defer newVersionObj.Body.Close()

147
backend/public/webhook.go Normal file
View File

@ -0,0 +1,147 @@
package public
import (
"crypto/tls"
"encoding/json"
"fmt"
"github.com/go-resty/resty/v2"
"net/http"
"regexp"
"strconv"
"strings"
"time"
)
type WebhookConfig struct {
Url string `json:"url"`
Data string `json:"data,omitempty"`
Method string `json:"method,omitempty"`
Headers string `json:"headers,omitempty"`
IgnoreSSL bool `json:"ignore_ssl,omitempty"`
}
func (w *WebhookConfig) Send() error {
// 确定HTTP方法
method := strings.ToUpper(w.Method)
if method == "" {
method = http.MethodPost // 默认使用POST方法
}
client := resty.New()
client.SetTimeout(30 * time.Second)
if w.IgnoreSSL {
client.SetTLSClientConfig(&tls.Config{InsecureSkipVerify: true})
}
req := client.R()
// 设置请求头
if w.Headers != "" {
reqHeader, err := w.ParseHeaders(w.Headers)
if err != nil {
return fmt.Errorf("解析请求头错误: %w", err)
}
req.Header = reqHeader
}
switch method {
case http.MethodPost:
{
contentType := req.Header.Get("application/json")
if contentType == "" {
contentType = "application/json"
}
switch contentType {
case "application/json":
req.SetHeader("Content-Type", "application/json")
var reqData interface{}
err := json.Unmarshal([]byte(w.Data), &reqData)
if err != nil {
return fmt.Errorf("webhook数据解析失败err: %w", err)
}
req.SetBody(reqData)
case "application/x-www-form-urlencoded":
req.SetHeader("Content-Type", "application/x-www-form-urlencoded")
reqData := make(map[string]string)
err := json.Unmarshal([]byte(w.Data), &reqData)
if err != nil {
return fmt.Errorf("webhook数据解析失败err: %w", err)
}
req.SetFormData(reqData)
case "multipart/form-data":
req.SetHeader("Content-Type", "multipart/form-data")
reqData := make(map[string]string)
err := json.Unmarshal([]byte(w.Data), &reqData)
if err != nil {
return fmt.Errorf("webhook数据解析失败err: %w", err)
}
req.SetMultipartFormData(reqData)
}
}
case http.MethodGet:
{
reqData := make(map[string]string)
err := json.Unmarshal([]byte(w.Data), &reqData)
if err != nil {
return fmt.Errorf("webhook数据解析失败err: %w", err)
}
req.SetQueryParams(reqData)
}
default:
return fmt.Errorf("暂不支持的HTTP方法: %s", method)
}
// 发送请求
resp, err := req.Execute(method, w.Url)
if err != nil {
return fmt.Errorf("webhook请求失败: %w", err)
}
// 处理响应
if resp.IsError() {
return fmt.Errorf("webhook返回错误状态码: %d, msg: %s", resp.StatusCode(), resp.String())
}
return nil
}
func (w *WebhookConfig) ParseHeaders(headerStr string) (http.Header, error) {
headers := make(http.Header)
lines := strings.Split(headerStr, "\n")
for i, line := range lines {
line = strings.TrimSpace(line)
if line == "" {
continue
}
parts := strings.SplitN(line, ":", 2)
if len(parts) != 2 {
return nil, fmt.Errorf("解析请求头错误 第%d行: %s", i+1, line)
}
key := strings.TrimSpace(parts[0])
value := strings.TrimSpace(parts[1])
if key == "" || value == "" {
return nil, fmt.Errorf("请求头Key第%d行为空", i+1)
}
canonicalKey := http.CanonicalHeaderKey(key)
headers.Add(canonicalKey, value)
}
return headers, nil
}
func ReplaceJSONPlaceholders(jsonStr string, vars map[string]any) (string, error) {
re := regexp.MustCompile(`__([a-zA-Z0-9_]+)__`)
result := re.ReplaceAllStringFunc(jsonStr, func(match string) string {
key := re.FindStringSubmatch(match)[1]
if val, ok := vars[key]; ok {
escaped := strconv.Quote(fmt.Sprintf("%v", val)) // 将 any 类型转换为字符串
return escaped[1 : len(escaped)-1]
}
return match // 未匹配到变量则保留原样
})
return result, nil
}

View File

@ -98,6 +98,8 @@ func Register(r *gin.Engine) {
setting.POST("/shutdown", api.Shutdown)
setting.POST("/restart", api.Restart)
setting.POST("/get_version", api.GetVersion)
setting.GET("/download_data", api.DownloadData)
setting.POST("/upload_data", api.UploadData)
}
overview := v1.Group("/overview")
{