refactor: ddns (#414)

* refactor ddns

* update webhook
This commit is contained in:
UUBulb 2024-08-24 11:11:06 +08:00 committed by GitHub
parent 64da3c7438
commit eb6dd2855e
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
17 changed files with 523 additions and 352 deletions

View File

@ -1,7 +1,6 @@
package controller
import (
"encoding/json"
"fmt"
"html/template"
"io/fs"
@ -277,7 +276,7 @@ func natGateway(c *gin.Context) {
rpc.NezhaHandlerSingleton.CreateStream(streamId)
defer rpc.NezhaHandlerSingleton.CloseStream(streamId)
taskData, err := json.Marshal(model.TaskNAT{
taskData, err := utils.Json.Marshal(model.TaskNAT{
StreamID: streamId,
Host: natConfig.Host,
})

View File

@ -2,7 +2,6 @@ package controller
import (
"context"
"encoding/json"
"errors"
"fmt"
"net/http"
@ -199,7 +198,7 @@ func (oa *oauth2controller) callback(c *gin.Context) {
if err == nil {
defer resp.Body.Close()
var cloudflareUserInfo *cloudflare.UserInfo
if err := json.NewDecoder(resp.Body).Decode(&cloudflareUserInfo); err == nil {
if err := utils.Json.NewDecoder(resp.Body).Decode(&cloudflareUserInfo); err == nil {
user = cloudflareUserInfo.MapToNezhaUser()
}
}

View File

@ -1,6 +1,7 @@
package model
import (
"slices"
"strings"
"time"
@ -44,19 +45,6 @@ func percentage(used, total uint64) float64 {
return float64(used) * 100 / float64(total)
}
func maxSliceValue(slice []float64) float64 {
if len(slice) != 0 {
max := slice[0]
for _, val := range slice {
if max < val {
max = val
}
}
return max
}
return 0
}
// Snapshot 未通过规则返回 struct{}{}, 通过返回 nil
func (u *Rule) Snapshot(cycleTransferStats *CycleTransferStats, server *Server, db *gorm.DB) interface{} {
// 监控全部但是排除了此服务器
@ -145,7 +133,7 @@ func (u *Rule) Snapshot(cycleTransferStats *CycleTransferStats, server *Server,
temp = append(temp, tempStat.Temperature)
}
}
src = maxSliceValue(temp)
src = slices.Max(temp)
}
}

View File

@ -2,169 +2,217 @@ package ddns
import (
"bytes"
"encoding/json"
"fmt"
"io"
"log"
"net/http"
"net/url"
"github.com/naiba/nezha/pkg/utils"
)
const baseEndpoint = "https://api.cloudflare.com/client/v4/zones"
type ProviderCloudflare struct {
Secret string
secret string
zoneId string
recordId string
domainConfig *DomainConfig
}
func (provider *ProviderCloudflare) UpdateDomain(domainConfig *DomainConfig) bool {
if domainConfig == nil {
return false
}
type cfReq struct {
Name string `json:"name"`
Type string `json:"type"`
Content string `json:"content"`
TTL uint32 `json:"ttl"`
Proxied bool `json:"proxied"`
}
zoneID, err := provider.getZoneID(domainConfig.FullDomain)
type cfResp struct {
Result []struct {
ID string `json:"id"`
Name string `json:"name"`
} `json:"result"`
}
func NewProviderCloudflare(s string) *ProviderCloudflare {
return &ProviderCloudflare{
secret: s,
}
}
func (provider *ProviderCloudflare) UpdateDomain(domainConfig *DomainConfig) error {
if domainConfig == nil {
return fmt.Errorf("获取 DDNS 配置失败")
}
provider.domainConfig = domainConfig
err := provider.getZoneID()
if err != nil {
log.Printf("无法获取 zone ID: %s\n", err)
return false
return fmt.Errorf("无法获取 zone ID: %s", err)
}
// 当IPv4和IPv6同时成功才算作成功
var resultV4 = true
var resultV6 = true
if domainConfig.EnableIPv4 {
if !provider.addDomainRecord(zoneID, domainConfig, true) {
resultV4 = false
if provider.domainConfig.EnableIPv4 {
if err = provider.addDomainRecord(true); err != nil {
return err
}
}
if domainConfig.EnableIpv6 {
if !provider.addDomainRecord(zoneID, domainConfig, false) {
resultV6 = false
if provider.domainConfig.EnableIpv6 {
if err = provider.addDomainRecord(false); err != nil {
return err
}
}
return resultV4 && resultV6
return nil
}
func (provider *ProviderCloudflare) addDomainRecord(zoneID string, domainConfig *DomainConfig, isIpv4 bool) bool {
record, err := provider.findDNSRecord(zoneID, domainConfig.FullDomain, isIpv4)
func (provider *ProviderCloudflare) addDomainRecord(isIpv4 bool) error {
err := provider.findDNSRecord(isIpv4)
if err != nil {
log.Printf("查找 DNS 记录时出错: %s\n", err)
return false
return fmt.Errorf("查找 DNS 记录时出错: %s", err)
}
if record == nil {
if provider.recordId == "" {
// 添加 DNS 记录
return provider.createDNSRecord(zoneID, domainConfig, isIpv4)
return provider.createDNSRecord(isIpv4)
} else {
// 更新 DNS 记录
return provider.updateDNSRecord(zoneID, record["id"].(string), domainConfig, isIpv4)
return provider.updateDNSRecord(isIpv4)
}
}
func (provider *ProviderCloudflare) getZoneID(domain string) (string, error) {
_, realDomain := SplitDomain(domain)
url := fmt.Sprintf("https://api.cloudflare.com/client/v4/zones?name=%s", realDomain)
body, err := provider.sendRequest("GET", url, nil)
func (provider *ProviderCloudflare) getZoneID() error {
_, realDomain := splitDomain(provider.domainConfig.FullDomain)
zu, _ := url.Parse(baseEndpoint)
q := zu.Query()
q.Set("name", realDomain)
zu.RawQuery = q.Encode()
body, err := provider.sendRequest("GET", zu.String(), nil)
if err != nil {
return "", err
return err
}
var res map[string]interface{}
err = json.Unmarshal(body, &res)
res := &cfResp{}
err = utils.Json.Unmarshal(body, res)
if err != nil {
return "", err
return err
}
result := res["result"].([]interface{})
result := res.Result
if len(result) > 0 {
zoneID := result[0].(map[string]interface{})["id"].(string)
return zoneID, nil
provider.zoneId = result[0].ID
return nil
}
return "", fmt.Errorf("找不到 Zone ID")
return fmt.Errorf("找不到 Zone ID")
}
func (provider *ProviderCloudflare) findDNSRecord(zoneID string, domain string, isIPv4 bool) (map[string]interface{}, error) {
var ipType = "A"
if !isIPv4 {
func (provider *ProviderCloudflare) findDNSRecord(isIPv4 bool) error {
var ipType string
if isIPv4 {
ipType = "A"
} else {
ipType = "AAAA"
}
url := fmt.Sprintf("https://api.cloudflare.com/client/v4/zones/%s/dns_records?type=%s&name=%s", zoneID, ipType, domain)
body, err := provider.sendRequest("GET", url, nil)
de, _ := url.JoinPath(baseEndpoint, provider.zoneId, "dns_records")
du, _ := url.Parse(de)
q := du.Query()
q.Set("name", provider.domainConfig.FullDomain)
q.Set("type", ipType)
du.RawQuery = q.Encode()
body, err := provider.sendRequest("GET", du.String(), nil)
if err != nil {
return nil, err
return err
}
var res map[string]interface{}
err = json.Unmarshal(body, &res)
res := &cfResp{}
err = utils.Json.Unmarshal(body, res)
if err != nil {
return nil, err
return err
}
result := res["result"].([]interface{})
result := res.Result
if len(result) > 0 {
return result[0].(map[string]interface{}), nil
provider.recordId = result[0].ID
return nil
}
return nil, nil // 没有找到 DNS 记录
return nil
}
func (provider *ProviderCloudflare) createDNSRecord(zoneID string, domainConfig *DomainConfig, isIPv4 bool) bool {
var ipType = "A"
var ipAddr = domainConfig.Ipv4Addr
if !isIPv4 {
func (provider *ProviderCloudflare) createDNSRecord(isIPv4 bool) error {
var ipType, ipAddr string
if isIPv4 {
ipType = "A"
ipAddr = provider.domainConfig.Ipv4Addr
} else {
ipType = "AAAA"
ipAddr = domainConfig.Ipv6Addr
ipAddr = provider.domainConfig.Ipv6Addr
}
url := fmt.Sprintf("https://api.cloudflare.com/client/v4/zones/%s/dns_records", zoneID)
data := map[string]interface{}{
"type": ipType,
"name": domainConfig.FullDomain,
"content": ipAddr,
"ttl": 60,
"proxied": false,
de, _ := url.JoinPath(baseEndpoint, provider.zoneId, "dns_records")
data := &cfReq{
Name: provider.domainConfig.FullDomain,
Type: ipType,
Content: ipAddr,
TTL: 60,
Proxied: false,
}
jsonData, _ := json.Marshal(data)
_, err := provider.sendRequest("POST", url, jsonData)
return err == nil
jsonData, _ := utils.Json.Marshal(data)
_, err := provider.sendRequest("POST", de, jsonData)
return err
}
func (provider *ProviderCloudflare) updateDNSRecord(zoneID string, recordID string, domainConfig *DomainConfig, isIPv4 bool) bool {
var ipType = "A"
var ipAddr = domainConfig.Ipv4Addr
if !isIPv4 {
func (provider *ProviderCloudflare) updateDNSRecord(isIPv4 bool) error {
var ipType, ipAddr string
if isIPv4 {
ipType = "A"
ipAddr = provider.domainConfig.Ipv4Addr
} else {
ipType = "AAAA"
ipAddr = domainConfig.Ipv6Addr
ipAddr = provider.domainConfig.Ipv6Addr
}
url := fmt.Sprintf("https://api.cloudflare.com/client/v4/zones/%s/dns_records/%s", zoneID, recordID)
data := map[string]interface{}{
"type": ipType,
"name": domainConfig.FullDomain,
"content": ipAddr,
"ttl": 60,
"proxied": false,
de, _ := url.JoinPath(baseEndpoint, provider.zoneId, "dns_records", provider.recordId)
data := &cfReq{
Name: provider.domainConfig.FullDomain,
Type: ipType,
Content: ipAddr,
TTL: 60,
Proxied: false,
}
jsonData, _ := json.Marshal(data)
_, err := provider.sendRequest("PATCH", url, jsonData)
return err == nil
jsonData, _ := utils.Json.Marshal(data)
_, err := provider.sendRequest("PATCH", de, jsonData)
return err
}
// 以下为辅助方法,如发送 HTTP 请求等
func (provider *ProviderCloudflare) sendRequest(method string, url string, data []byte) ([]byte, error) {
client := &http.Client{}
req, err := http.NewRequest(method, url, bytes.NewBuffer(data))
if err != nil {
return nil, err
}
req.Header.Add("Authorization", fmt.Sprintf("Bearer %s", provider.Secret))
req.Header.Add("Authorization", fmt.Sprintf("Bearer %s", provider.secret))
req.Header.Add("Content-Type", "application/json")
resp, err := client.Do(req)
resp, err := utils.HttpClient.Do(req)
if err != nil {
return nil, err
}
defer func(Body io.ReadCloser) {
err := Body.Close()
if err != nil {
log.Printf("NEZHA>> 无法关闭HTTP响应体流: %s\n", err.Error())
log.Printf("NEZHA>> 无法关闭HTTP响应体流: %s", err.Error())
}
}(resp.Body)

View File

@ -1,5 +1,7 @@
package ddns
import "golang.org/x/net/publicsuffix"
type DomainConfig struct {
EnableIPv4 bool
EnableIpv6 bool
@ -10,5 +12,11 @@ type DomainConfig struct {
type Provider interface {
// UpdateDomain Return is updated
UpdateDomain(domainConfig *DomainConfig) bool
UpdateDomain(*DomainConfig) error
}
func splitDomain(domain string) (prefix string, realDomain string) {
realDomain, _ = publicsuffix.EffectiveTLDPlusOne(domain)
prefix = domain[:len(domain)-len(realDomain)-1]
return prefix, realDomain
}

View File

@ -2,6 +2,6 @@ package ddns
type ProviderDummy struct{}
func (provider *ProviderDummy) UpdateDomain(domainConfig *DomainConfig) bool {
return false
func (provider *ProviderDummy) UpdateDomain(domainConfig *DomainConfig) error {
return nil
}

View File

@ -1,40 +0,0 @@
package ddns
import (
"golang.org/x/net/publicsuffix"
"net/http"
"strings"
)
func (provider ProviderWebHook) FormatWebhookString(s string, config *DomainConfig, ipType string) string {
if config == nil {
return s
}
result := strings.TrimSpace(s)
result = strings.Replace(s, "{ip}", config.Ipv4Addr, -1)
result = strings.Replace(result, "{domain}", config.FullDomain, -1)
result = strings.Replace(result, "{type}", ipType, -1)
// remove \r
result = strings.Replace(result, "\r", "", -1)
return result
}
func SetStringHeadersToRequest(req *http.Request, headers []string) {
if req == nil {
return
}
for _, element := range headers {
kv := strings.SplitN(element, ":", 2)
if len(kv) == 2 {
req.Header.Add(kv[0], kv[1])
}
}
}
// SplitDomain 分割域名为前缀和一级域名
func SplitDomain(domain string) (prefix string, realDomain string) {
realDomain, _ = publicsuffix.EffectiveTLDPlusOne(domain)
prefix = domain[:len(domain)-len(realDomain)-1]
return prefix, realDomain
}

View File

@ -5,145 +5,180 @@ import (
"crypto/hmac"
"crypto/sha256"
"encoding/hex"
"encoding/json"
"fmt"
"io"
"log"
"net/http"
"strconv"
"strings"
"time"
"github.com/naiba/nezha/pkg/utils"
)
const (
url = "https://dnspod.tencentcloudapi.com"
)
const te = "https://dnspod.tencentcloudapi.com"
type ProviderTencentCloud struct {
SecretID string
SecretKey string
secretID string
secretKey string
domainConfig *DomainConfig
resp *tcResp
}
func (provider *ProviderTencentCloud) UpdateDomain(domainConfig *DomainConfig) bool {
if domainConfig == nil {
return false
type tcReq struct {
RecordType string `json:"RecordType"`
Domain string `json:"Domain"`
RecordLine string `json:"RecordLine"`
Subdomain string `json:"Subdomain,omitempty"`
SubDomain string `json:"SubDomain,omitempty"` // As is
Value string `json:"Value,omitempty"`
TTL uint32 `json:"TTL,omitempty"`
RecordId uint64 `json:"RecordId,omitempty"`
}
type tcResp struct {
Response struct {
RecordList []struct {
RecordId uint64
Value string
}
Error struct {
Code string
}
}
}
func NewProviderTencentCloud(id, key string) *ProviderTencentCloud {
return &ProviderTencentCloud{
secretID: id,
secretKey: key,
}
}
func (provider *ProviderTencentCloud) UpdateDomain(domainConfig *DomainConfig) error {
if domainConfig == nil {
return fmt.Errorf("获取 DDNS 配置失败")
}
provider.domainConfig = domainConfig
// 当IPv4和IPv6同时成功才算作成功
var resultV4 = true
var resultV6 = true
if domainConfig.EnableIPv4 {
if !provider.addDomainRecord(domainConfig, true) {
resultV4 = false
var err error
if provider.domainConfig.EnableIPv4 {
if err = provider.addDomainRecord(true); err != nil {
return err
}
}
if domainConfig.EnableIpv6 {
if !provider.addDomainRecord(domainConfig, false) {
resultV6 = false
if provider.domainConfig.EnableIpv6 {
if err = provider.addDomainRecord(false); err != nil {
return err
}
}
return resultV4 && resultV6
return err
}
func (provider *ProviderTencentCloud) addDomainRecord(domainConfig *DomainConfig, isIpv4 bool) bool {
record, err := provider.findDNSRecord(domainConfig.FullDomain, isIpv4)
func (provider *ProviderTencentCloud) addDomainRecord(isIpv4 bool) error {
err := provider.findDNSRecord(isIpv4)
if err != nil {
log.Printf("查找 DNS 记录时出错: %s\n", err)
return false
return fmt.Errorf("查找 DNS 记录时出错: %s", err)
}
if errResponse, ok := record["Error"].(map[string]interface{}); ok {
if errCode, ok := errResponse["Code"].(string); ok && errCode == "ResourceNotFound.NoDataOfRecord" { // 没有找到 DNS 记录
// 添加 DNS 记录
return provider.createDNSRecord(domainConfig.FullDomain, domainConfig, isIpv4)
} else {
log.Printf("查询 DNS 记录时出错,错误代码为: %s\n", errCode)
}
if provider.resp.Response.Error.Code == "ResourceNotFound.NoDataOfRecord" { // 没有找到 DNS 记录
return provider.createDNSRecord(isIpv4)
} else if provider.resp.Response.Error.Code != "" {
return fmt.Errorf("查询 DNS 记录时出错,错误代码为: %s", provider.resp.Response.Error.Code)
}
// 默认情况下更新 DNS 记录
return provider.updateDNSRecord(domainConfig.FullDomain, record["RecordList"].([]interface{})[0].(map[string]interface{})["RecordId"].(float64), domainConfig, isIpv4)
return provider.updateDNSRecord(isIpv4)
}
func (provider *ProviderTencentCloud) findDNSRecord(domain string, isIPv4 bool) (map[string]interface{}, error) {
var ipType = "A"
if !isIPv4 {
func (provider *ProviderTencentCloud) findDNSRecord(isIPv4 bool) error {
var ipType string
if isIPv4 {
ipType = "A"
} else {
ipType = "AAAA"
}
_, realDomain := SplitDomain(domain)
prefix, _ := SplitDomain(domain)
data := map[string]interface{}{
"RecordType": ipType,
"Domain": realDomain,
"RecordLine": "默认",
"Subdomain": prefix,
prefix, realDomain := splitDomain(provider.domainConfig.FullDomain)
data := &tcReq{
RecordType: ipType,
Domain: realDomain,
RecordLine: "默认",
Subdomain: prefix,
}
jsonData, _ := json.Marshal(data)
jsonData, _ := utils.Json.Marshal(data)
body, err := provider.sendRequest("DescribeRecordList", jsonData)
if err != nil {
return nil, err
return err
}
var res map[string]interface{}
err = json.Unmarshal(body, &res)
provider.resp = &tcResp{}
err = utils.Json.Unmarshal(body, provider.resp)
if err != nil {
return nil, err
return err
}
result := res["Response"].(map[string]interface{})
return result, nil
return nil
}
func (provider *ProviderTencentCloud) createDNSRecord(domain string, domainConfig *DomainConfig, isIPv4 bool) bool {
var ipType = "A"
var ipAddr = domainConfig.Ipv4Addr
if !isIPv4 {
func (provider *ProviderTencentCloud) createDNSRecord(isIPv4 bool) error {
var ipType, ipAddr string
if isIPv4 {
ipType = "A"
ipAddr = provider.domainConfig.Ipv4Addr
} else {
ipType = "AAAA"
ipAddr = domainConfig.Ipv6Addr
ipAddr = provider.domainConfig.Ipv6Addr
}
_, realDomain := SplitDomain(domain)
prefix, _ := SplitDomain(domain)
data := map[string]interface{}{
"RecordType": ipType,
"RecordLine": "默认",
"Domain": realDomain,
"SubDomain": prefix,
"Value": ipAddr,
"TTL": 600,
prefix, realDomain := splitDomain(provider.domainConfig.FullDomain)
data := &tcReq{
RecordType: ipType,
RecordLine: "默认",
Domain: realDomain,
SubDomain: prefix,
Value: ipAddr,
TTL: 600,
}
jsonData, _ := json.Marshal(data)
jsonData, _ := utils.Json.Marshal(data)
_, err := provider.sendRequest("CreateRecord", jsonData)
return err == nil
return err
}
func (provider *ProviderTencentCloud) updateDNSRecord(domain string, recordID float64, domainConfig *DomainConfig, isIPv4 bool) bool {
var ipType = "A"
var ipAddr = domainConfig.Ipv4Addr
if !isIPv4 {
func (provider *ProviderTencentCloud) updateDNSRecord(isIPv4 bool) error {
var ipType, ipAddr string
if isIPv4 {
ipType = "A"
ipAddr = provider.domainConfig.Ipv4Addr
} else {
ipType = "AAAA"
ipAddr = domainConfig.Ipv6Addr
ipAddr = provider.domainConfig.Ipv6Addr
}
_, realDomain := SplitDomain(domain)
prefix, _ := SplitDomain(domain)
data := map[string]interface{}{
"RecordType": ipType,
"RecordLine": "默认",
"Domain": realDomain,
"SubDomain": prefix,
"Value": ipAddr,
"TTL": 600,
"RecordId": recordID,
prefix, realDomain := splitDomain(provider.domainConfig.FullDomain)
data := &tcReq{
RecordType: ipType,
RecordLine: "默认",
Domain: realDomain,
SubDomain: prefix,
Value: ipAddr,
TTL: 600,
RecordId: provider.resp.Response.RecordList[0].RecordId,
}
jsonData, _ := json.Marshal(data)
jsonData, _ := utils.Json.Marshal(data)
_, err := provider.sendRequest("ModifyRecord", jsonData)
return err == nil
return err
}
// 以下为辅助方法,如发送 HTTP 请求等
func (provider *ProviderTencentCloud) sendRequest(action string, data []byte) ([]byte, error) {
client := &http.Client{}
req, err := http.NewRequest("POST", url, bytes.NewBuffer(data))
req, err := http.NewRequest("POST", te, bytes.NewBuffer(data))
if err != nil {
return nil, err
}
@ -151,8 +186,8 @@ func (provider *ProviderTencentCloud) sendRequest(action string, data []byte) ([
req.Header.Set("Content-Type", "application/json")
req.Header.Set("X-TC-Version", "2021-03-23")
provider.signRequest(provider.SecretID, provider.SecretKey, req, action, string(data))
resp, err := client.Do(req)
provider.signRequest(provider.secretID, provider.secretKey, req, action, string(data))
resp, err := utils.HttpClient.Do(req)
if err != nil {
return nil, err
}

View File

@ -2,58 +2,109 @@ package ddns
import (
"bytes"
"log"
"fmt"
"net/http"
"net/url"
"strings"
"github.com/naiba/nezha/pkg/utils"
)
type ProviderWebHook struct {
URL string
RequestMethod string
RequestBody string
RequestHeader string
url string
requestMethod string
requestBody string
requestHeader string
domainConfig *DomainConfig
}
func (provider *ProviderWebHook) UpdateDomain(domainConfig *DomainConfig) bool {
func NewProviderWebHook(s, rm, rb, rh string) *ProviderWebHook {
return &ProviderWebHook{
url: s,
requestMethod: rm,
requestBody: rb,
requestHeader: rh,
}
}
func (provider *ProviderWebHook) UpdateDomain(domainConfig *DomainConfig) error {
if domainConfig == nil {
return false
return fmt.Errorf("获取 DDNS 配置失败")
}
provider.domainConfig = domainConfig
if provider.domainConfig.FullDomain == "" {
return fmt.Errorf("failed to update an empty domain")
}
if domainConfig.FullDomain == "" {
log.Println("NEZHA>> Failed to update an empty domain")
return false
}
updated := false
client := &http.Client{}
if domainConfig.EnableIPv4 && domainConfig.Ipv4Addr != "" {
url := provider.FormatWebhookString(provider.URL, domainConfig, "ipv4")
body := provider.FormatWebhookString(provider.RequestBody, domainConfig, "ipv4")
header := provider.FormatWebhookString(provider.RequestHeader, domainConfig, "ipv4")
headers := strings.Split(header, "\n")
req, err := http.NewRequest(provider.RequestMethod, url, bytes.NewBufferString(body))
if err == nil && req != nil {
SetStringHeadersToRequest(req, headers)
if _, err := client.Do(req); err != nil {
log.Printf("NEZHA>> Failed to update a domain: %s. Cause by: %s\n", domainConfig.FullDomain, err.Error())
} else {
updated = true
}
if provider.domainConfig.EnableIPv4 && provider.domainConfig.Ipv4Addr != "" {
req, err := provider.prepareRequest(true)
if err != nil {
return fmt.Errorf("failed to update a domain: %s. Cause by: %v", provider.domainConfig.FullDomain, err)
}
if _, err := utils.HttpClient.Do(req); err != nil {
return fmt.Errorf("failed to update a domain: %s. Cause by: %v", provider.domainConfig.FullDomain, err)
}
}
if domainConfig.EnableIpv6 && domainConfig.Ipv6Addr != "" {
url := provider.FormatWebhookString(provider.URL, domainConfig, "ipv6")
body := provider.FormatWebhookString(provider.RequestBody, domainConfig, "ipv6")
header := provider.FormatWebhookString(provider.RequestHeader, domainConfig, "ipv6")
headers := strings.Split(header, "\n")
req, err := http.NewRequest(provider.RequestMethod, url, bytes.NewBufferString(body))
if err == nil && req != nil {
SetStringHeadersToRequest(req, headers)
if _, err := client.Do(req); err != nil {
log.Printf("NEZHA>> Failed to update a domain: %s. Cause by: %s\n", domainConfig.FullDomain, err.Error())
} else {
updated = true
}
if provider.domainConfig.EnableIpv6 && provider.domainConfig.Ipv6Addr != "" {
req, err := provider.prepareRequest(false)
if err != nil {
return fmt.Errorf("failed to update a domain: %s. Cause by: %v", provider.domainConfig.FullDomain, err)
}
if _, err := utils.HttpClient.Do(req); err != nil {
return fmt.Errorf("failed to update a domain: %s. Cause by: %v", provider.domainConfig.FullDomain, err)
}
}
return updated
return nil
}
func (provider *ProviderWebHook) prepareRequest(isIPv4 bool) (*http.Request, error) {
u, err := url.Parse(provider.url)
if err != nil {
return nil, fmt.Errorf("failed parsing url: %v", err)
}
// Only handle queries here
q := u.Query()
for p, vals := range q {
for n, v := range vals {
vals[n] = provider.formatWebhookString(v, isIPv4)
}
q[p] = vals
}
u.RawQuery = q.Encode()
body := provider.formatWebhookString(provider.requestBody, isIPv4)
header := provider.formatWebhookString(provider.requestHeader, isIPv4)
headers := strings.Split(header, "\n")
req, err := http.NewRequest(provider.requestMethod, u.String(), bytes.NewBufferString(body))
if err != nil {
return nil, fmt.Errorf("failed creating new request: %v", err)
}
utils.SetStringHeadersToRequest(req, headers)
return req, nil
}
func (provider *ProviderWebHook) formatWebhookString(s string, isIPv4 bool) string {
var ipAddr, ipType string
if isIPv4 {
ipAddr = provider.domainConfig.Ipv4Addr
ipType = "ipv4"
} else {
ipAddr = provider.domainConfig.Ipv6Addr
ipType = "ipv6"
}
r := strings.NewReplacer(
"{ip}", ipAddr,
"{domain}", provider.domainConfig.FullDomain,
"{type}", ipType,
"\r", "",
)
result := r.Replace(strings.TrimSpace(s))
return result
}

View File

@ -3,6 +3,7 @@ package utils
import (
"crypto/rand"
"math/big"
"net/http"
"os"
"regexp"
"strings"
@ -86,3 +87,15 @@ func Uint64SubInt64(a uint64, b int64) uint64 {
}
return a - uint64(b)
}
func SetStringHeadersToRequest(req *http.Request, headers []string) {
if req == nil {
return
}
for _, element := range headers {
kv := strings.SplitN(element, ":", 2)
if len(kv) == 2 {
req.Header.Add(kv[0], kv[1])
}
}
}

View File

@ -653,7 +653,28 @@ other = "Disable Switch Template in Frontend"
other = "Servers On World Map"
[NAT]
other = "NAT"
other = "NAT Traversal"
[NetworkSpiterList]
other = "Network Monitor"
other = "Network Monitor"
[Refresh]
other = "Refresh"
[CopyPath]
other = "Copy Path"
[Goto]
other = "Go to"
[GotoHeadline]
other = "Go to a Folder"
[GotoGo]
other = "Go"
[GotoClose]
other = "Cancel"
[FMError]
other = "Agent returned an error, please view the console for details. To open a new connection, reopen the FM again."

View File

@ -653,7 +653,28 @@ other = "Deshabilitar Cambio de Plantilla en Frontend"
other = "Servidores en el mapa mundial"
[NAT]
other = "NAT"
other = "NAT traversal"
[NetworkSpiterList]
other = "Red Monitor"
other = "Monitor de red"
[Refresh]
other = "Actualizar"
[CopyPath]
other = "Copiar ruta"
[Goto]
other = "Ir a"
[GotoHeadline]
other = "Ir a una carpeta"
[GotoGo]
other = "Ir"
[GotoClose]
other = "Cancelar"
[FMError]
other = "Agent devolvió un error, consulte la consola para obtener más detalles. Para abrir una nueva conexión, vuelva a abrir el FM."

View File

@ -657,3 +657,24 @@ other = "内网穿透"
[NetworkSpiterList]
other = "网络监控"
[Refresh]
other = "刷新"
[CopyPath]
other = "复制路径"
[Goto]
other = "跳往"
[GotoHeadline]
other = "跳往文件夹"
[GotoGo]
other = "确认"
[GotoClose]
other = "取消"
[FMError]
other = "Agent 返回了错误,请查看控制台获取详细信息。要建立新连接,请重新打开 FM。"

View File

@ -50,7 +50,7 @@ other = "新增計劃任務"
other = "名稱"
[Scheduler]
other = "計劃"
other = "排程"
[BackUp]
other = "備份"
@ -80,37 +80,37 @@ other = "特定伺服器"
other = "輸入ID/名稱以搜尋"
[NotificationMethodGroup]
other = "通知方式組"
other = "通知組"
[PushSuccessMessages]
other = "推送成功的息"
other = "推送成功的息"
[TaskType]
other = "任務類型"
[CronTask]
other = "計劃任務"
other = "排程任務"
[TriggerTask]
other = "觸發任務"
[TheFormaOfTheScheduleIs]
other = "計劃的格式為:"
other = "排程的格式為:"
[SecondsMinutesHoursDaysMonthsWeeksSeeDetails]
other = "秒 分 時 天 月 星期,詳情見"
[ScheduleExpressionFormat]
other = "計劃表達式格式"
other = "排程表達式格式"
[IntroductionOfCommands]
other = "命令說明:編寫命令時類似於 shell/bat 腳本。建議不要換行,多個命令可用 <code>&&</code> 或 <code>&</code> 連接,若出現命令無法找到的情況,可能是由於 <code>PATH</code> 環境變配置問題。在 <code>Linux</code> 伺服器上,可在命令開頭加入 <code>source ~/.bashrc</code>,或使用命令的絕對路徑執行。"
other = "命令說明:編寫命令時類似於 shell/bat 腳本。建議不要換行,多個命令可用 <code>&&</code> 或 <code>&</code> 連接,若出現命令無法找到的情況,可能是由於 <code>PATH</code> 環境變配置問題。在 <code>Linux</code> 伺服器上,可在命令開頭加入 <code>source ~/.bashrc</code>,或使用命令的絕對路徑執行。"
[AddMonitor]
other = "新增監控"
[Blog]
other = "博客"
other = "部落格"
[Target]
other = "目標"
@ -158,7 +158,7 @@ other = "新增通知方式"
other = "分組"
[DoNotSendTestMessages]
other = "不發送測試息"
other = "不發送測試息"
[RequestMethod]
other = "請求方式"
@ -221,7 +221,7 @@ other = "排序"
other = "越大越靠前"
[Secret]
other = "鑰"
other = "鑰"
[Note]
other = "備註"
@ -254,10 +254,10 @@ other = "忽略所有"
other = "觸發執行"
[DeleteScheduledTask]
other = "刪除計劃任務"
other = "刪除排程任務"
[ConfirmToDeleteThisScheduledTask]
other = "確認刪除此計劃任務?"
other = "確認刪除此排程任務?"
[AccessDenied]
other = "訪問被拒絕"
@ -407,7 +407,7 @@ other = "流量"
other = "負載"
[ProcessCount]
other = "程數"
other = "程數"
[ConnCount]
other = "連接數"
@ -422,7 +422,7 @@ other = "活動"
other = "版本"
[NetSpeed]
other = "網"
other = "網"
[Uptime]
other = "在線"
@ -458,7 +458,7 @@ other = "狀態"
other = "可用性"
[AverageLatency]
other = "平均應時間"
other = "平均應時間"
[CycleTransferStats]
other = "周期性流量統計"
@ -524,7 +524,7 @@ other = "發生錯誤"
other = "系統錯誤"
[NetworkError]
other = "網錯誤"
other = "網錯誤"
[ServicesStatus]
other = "服務狀態"
@ -536,7 +536,7 @@ other = "伺服器管理"
other = "服務監控"
[ScheduledTasks]
other = "計劃任務"
other = "排程任務"
[ApiManagement]
other = "API 管理"
@ -614,7 +614,7 @@ other = "對遊客隱藏"
other = "菜單"
[NetworkSpiter]
other = "網"
other = "網"
[EnableShowInService]
other = "在服務中顯示"
@ -656,4 +656,25 @@ other = "伺服器世界分布圖"
other = "NAT"
[NetworkSpiterList]
other = "網絡監控"
other = "網路監控"
[Refresh]
other = "重新整理"
[CopyPath]
other = "複製路徑"
[Goto]
other = "跳至"
[GotoHeadline]
other = "跳至資料夾"
[GotoGo]
other = "確定"
[GotoClose]
other = "取消"
[FMError]
other = "Agent 回傳了錯誤,請查看主控台獲取詳細資訊。要建立新連線,請重新開啟 FM。"

View File

@ -58,9 +58,9 @@
<mdui-dropdown>
<mdui-button-icon slot="trigger" icon="menu"></mdui-button-icon>
<mdui-menu>
<mdui-menu-item id="refresh">Refresh</mdui-menu-item>
<mdui-menu-item id="copy">Copy path</mdui-menu-item>
<mdui-menu-item id="goto">Go to</mdui-menu-item>
<mdui-menu-item id="refresh">{{tr "Refresh"}}</mdui-menu-item>
<mdui-menu-item id="copy">{{tr "CopyPath"}}</mdui-menu-item>
<mdui-menu-item id="goto">{{tr "Goto"}}</mdui-menu-item>
</mdui-menu>
</mdui-dropdown>
<span id="current-directory"></span>
@ -70,16 +70,16 @@
<mdui-list id="file-list" class="file-list"></mdui-list>
<mdui-dialog id="error-dialog" headline="Error"
description="Agent returned an error, please view the console for details. To open a new connection, reopen the FM again."></mdui-dialog>
description="{{tr "FMError"}}"></mdui-dialog>
<mdui-dialog id="upd-modal" class="modal">
<mdui-linear-progress id="upd-progress"></mdui-linear-progress>
</mdui-dialog>
<mdui-dialog id="goto-dialog" headline="Go to a folder" close-on-overlay-click>
<mdui-dialog id="goto-dialog" headline="{{tr "GotoHeadline"}}" close-on-overlay-click>
<mdui-text-field id="goto-text" variant="outlined" value=""></mdui-text-field>
<mdui-button id="goto-go" slot="action" variant="text">Go</mdui-button>
<mdui-button id="goto-close" slot="action" variant="tonal">Close</mdui-button>
<mdui-button id="goto-go" slot="action" variant="text">{{tr "GotoGo"}}</mdui-button>
<mdui-button id="goto-close" slot="action" variant="tonal">{{tr "GotoClose"}}</mdui-button>
</mdui-dialog>
<script>

View File

@ -159,7 +159,7 @@ func (s *NezhaHandler) ReportSystemInfo(c context.Context, r *pb.Host) (*pb.Rece
} else {
// 虽然会在启动时panic, 可以断言不会走这个分支, 但是考虑到动态加载配置或者其它情况, 这里输出一下方便检查奇奇怪怪的BUG
log.Printf("NEZHA>> 未找到对应的DDNS配置(%s), 或者是provider填写不正确, 请前往config.yml检查你的设置\n", singleton.ServerList[clientID].DDNSProfile)
log.Printf("NEZHA>> 未找到对应的DDNS配置(%s), 或者是provider填写不正确, 请前往config.yml检查你的设置", singleton.ServerList[clientID].DDNSProfile)
}
}

View File

@ -3,86 +3,72 @@ package singleton
import (
"fmt"
"log"
"slices"
ddns2 "github.com/naiba/nezha/pkg/ddns"
)
func RetryableUpdateDomain(provider ddns2.Provider, config *ddns2.DomainConfig, maxRetries int) bool {
if nil == config {
return false
const (
ProviderWebHook = "webhook"
ProviderCloudflare = "cloudflare"
ProviderTencentCloud = "tencentcloud"
)
type ProviderFunc func(*ddns2.DomainConfig) ddns2.Provider
func RetryableUpdateDomain(provider ddns2.Provider, domainConfig *ddns2.DomainConfig, maxRetries int) {
if domainConfig == nil {
return
}
for retries := 0; retries < maxRetries; retries++ {
log.Printf("NEZHA>> 正在尝试更新域名(%s)DDNS(%d/%d)\n", config.FullDomain, retries+1, maxRetries)
if provider.UpdateDomain(config) {
log.Printf("NEZHA>> 尝试更新域名(%s)DDNS成功\n", config.FullDomain)
return true
log.Printf("NEZHA>> 正在尝试更新域名(%s)DDNS(%d/%d)", domainConfig.FullDomain, retries+1, maxRetries)
if err := provider.UpdateDomain(domainConfig); err != nil {
log.Printf("NEZHA>> 尝试更新域名(%s)DDNS失败: %v", domainConfig.FullDomain, err)
} else {
log.Printf("NEZHA>> 尝试更新域名(%s)DDNS成功", domainConfig.FullDomain)
break
}
}
log.Printf("NEZHA>> 尝试更新域名(%s)DDNS失败\n", config.FullDomain)
return false
}
// Deprecated
func GetDDNSProviderFromString(provider string) (ddns2.Provider, error) {
switch provider {
case "webhook":
return &ddns2.ProviderWebHook{
URL: Conf.DDNS.WebhookURL,
RequestMethod: Conf.DDNS.WebhookMethod,
RequestBody: Conf.DDNS.WebhookRequestBody,
RequestHeader: Conf.DDNS.WebhookHeaders,
}, nil
case "dummy":
return &ddns2.ProviderDummy{}, nil
case "cloudflare":
return &ddns2.ProviderCloudflare{
Secret: Conf.DDNS.AccessSecret,
}, nil
case "tencentcloud":
return &ddns2.ProviderTencentCloud{
SecretID: Conf.DDNS.AccessID,
SecretKey: Conf.DDNS.AccessSecret,
}, nil
case ProviderWebHook:
return ddns2.NewProviderWebHook(Conf.DDNS.WebhookURL, Conf.DDNS.WebhookMethod, Conf.DDNS.WebhookRequestBody, Conf.DDNS.WebhookHeaders), nil
case ProviderCloudflare:
return ddns2.NewProviderCloudflare(Conf.DDNS.AccessSecret), nil
case ProviderTencentCloud:
return ddns2.NewProviderTencentCloud(Conf.DDNS.AccessID, Conf.DDNS.AccessSecret), nil
default:
return new(ddns2.ProviderDummy), fmt.Errorf("无法找到配置的DDNS提供者 %s", provider)
}
return &ddns2.ProviderDummy{}, fmt.Errorf("无法找到配置的DDNS提供者%s", Conf.DDNS.Provider)
}
func GetDDNSProviderFromProfile(profileName string) (ddns2.Provider, error) {
profile, ok := Conf.DDNS.Profiles[profileName]
if !ok {
return &ddns2.ProviderDummy{}, fmt.Errorf("未找到配置项 %s", profileName)
return new(ddns2.ProviderDummy), fmt.Errorf("未找到配置项 %s", profileName)
}
switch profile.Provider {
case "webhook":
return &ddns2.ProviderWebHook{
URL: profile.WebhookURL,
RequestMethod: profile.WebhookMethod,
RequestBody: profile.WebhookRequestBody,
RequestHeader: profile.WebhookHeaders,
}, nil
case "dummy":
return &ddns2.ProviderDummy{}, nil
case "cloudflare":
return &ddns2.ProviderCloudflare{
Secret: profile.AccessSecret,
}, nil
case "tencentcloud":
return &ddns2.ProviderTencentCloud{
SecretID: profile.AccessID,
SecretKey: profile.AccessSecret,
}, nil
case ProviderWebHook:
return ddns2.NewProviderWebHook(profile.WebhookURL, profile.WebhookMethod, profile.WebhookRequestBody, profile.WebhookHeaders), nil
case ProviderCloudflare:
return ddns2.NewProviderCloudflare(profile.AccessSecret), nil
case ProviderTencentCloud:
return ddns2.NewProviderTencentCloud(profile.AccessID, profile.AccessSecret), nil
default:
return new(ddns2.ProviderDummy), fmt.Errorf("无法找到配置的DDNS提供者 %s", profile.Provider)
}
return &ddns2.ProviderDummy{}, fmt.Errorf("无法找到配置的DDNS提供者%s", profile.Provider)
}
func ValidateDDNSProvidersFromProfiles() error {
validProviders := map[string]bool{"webhook": true, "dummy": true, "cloudflare": true, "tencentcloud": true}
providers := make(map[string]string)
for profileName, profile := range Conf.DDNS.Profiles {
if _, ok := validProviders[profile.Provider]; !ok {
validProviders := []string{ProviderWebHook, ProviderCloudflare, ProviderTencentCloud}
for _, profile := range Conf.DDNS.Profiles {
if ok := slices.Contains(validProviders, profile.Provider); !ok {
return fmt.Errorf("无法找到配置的DDNS提供者%s", profile.Provider)
}
providers[profileName] = profile.Provider
}
return nil
}