Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
cf9d9bd248 | ||
|
|
5e528c42c7 | ||
|
|
daa7e03240 | ||
|
|
ab39782c78 | ||
|
|
e24925b049 | ||
|
|
1d4245ee09 |
@@ -89,6 +89,9 @@ You can change the dashboard language in the settings page (`/setting`) after th
|
|||||||
<a href="https://github.com/Mmx233" title="Mmx">
|
<a href="https://github.com/Mmx233" title="Mmx">
|
||||||
<img src="https://avatars.githubusercontent.com/u/36563672?v=4" width="50;" alt="Mmx"/>
|
<img src="https://avatars.githubusercontent.com/u/36563672?v=4" width="50;" alt="Mmx"/>
|
||||||
</a>
|
</a>
|
||||||
|
<a href="https://github.com/nap0o" title="nap0o">
|
||||||
|
<img src="https://avatars.githubusercontent.com/u/144927971?v=4" width="50;" alt="nap0o"/>
|
||||||
|
</a>
|
||||||
<a href="https://github.com/rootmelo92118" title="rootmelo92118">
|
<a href="https://github.com/rootmelo92118" title="rootmelo92118">
|
||||||
<img src="https://avatars.githubusercontent.com/u/32770959?v=4" width="50;" alt="rootmelo92118"/>
|
<img src="https://avatars.githubusercontent.com/u/32770959?v=4" width="50;" alt="rootmelo92118"/>
|
||||||
</a>
|
</a>
|
||||||
@@ -122,9 +125,6 @@ You can change the dashboard language in the settings page (`/setting`) after th
|
|||||||
<a href="https://github.com/unclezs" title="unclezs">
|
<a href="https://github.com/unclezs" title="unclezs">
|
||||||
<img src="https://avatars.githubusercontent.com/u/42318775?v=4" width="50;" alt="unclezs"/>
|
<img src="https://avatars.githubusercontent.com/u/42318775?v=4" width="50;" alt="unclezs"/>
|
||||||
</a>
|
</a>
|
||||||
<a href="https://github.com/nap0o" title="nap0o">
|
|
||||||
<img src="https://avatars.githubusercontent.com/u/144927971?v=4" width="50;" alt="nap0o"/>
|
|
||||||
</a>
|
|
||||||
<a href="https://github.com/yuanweize" title="I">
|
<a href="https://github.com/yuanweize" title="I">
|
||||||
<img src="https://avatars.githubusercontent.com/u/30067203?v=4" width="50;" alt="I"/>
|
<img src="https://avatars.githubusercontent.com/u/30067203?v=4" width="50;" alt="I"/>
|
||||||
</a>
|
</a>
|
||||||
|
|||||||
@@ -31,13 +31,16 @@ func (v *apiV1) serve() {
|
|||||||
// 不强制认证的 API
|
// 不强制认证的 API
|
||||||
mr := v.r.Group("monitor")
|
mr := v.r.Group("monitor")
|
||||||
mr.Use(mygin.Authorize(mygin.AuthorizeOption{
|
mr.Use(mygin.Authorize(mygin.AuthorizeOption{
|
||||||
MemberOnly: false,
|
MemberOnly: false,
|
||||||
IsPage: false,
|
IsPage: false,
|
||||||
ValidateViewPassword: true,
|
AllowAPI: true,
|
||||||
AllowAPI: true,
|
Msg: "访问此接口需要认证",
|
||||||
Msg: "访问此接口需要认证",
|
Btn: "点此登录",
|
||||||
Btn: "点此登录",
|
Redirect: "/login",
|
||||||
Redirect: "/login",
|
}))
|
||||||
|
mr.Use(mygin.ValidateViewPassword(mygin.ValidateViewPasswordOption{
|
||||||
|
IsPage: false,
|
||||||
|
AbortWhenFail: true,
|
||||||
}))
|
}))
|
||||||
mr.GET("/:id", v.monitorHistoriesById)
|
mr.GET("/:id", v.monitorHistoriesById)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -43,12 +43,14 @@ type commonPage struct {
|
|||||||
|
|
||||||
func (cp *commonPage) serve() {
|
func (cp *commonPage) serve() {
|
||||||
cr := cp.r.Group("")
|
cr := cp.r.Group("")
|
||||||
cr.Use(mygin.Authorize(mygin.AuthorizeOption{
|
cr.Use(mygin.Authorize(mygin.AuthorizeOption{}))
|
||||||
ValidateViewPassword: true,
|
|
||||||
}))
|
|
||||||
cr.Use(mygin.PreferredTheme)
|
cr.Use(mygin.PreferredTheme)
|
||||||
cr.GET("/terminal/:id", cp.terminal)
|
|
||||||
cr.POST("/view-password", cp.issueViewPassword)
|
cr.POST("/view-password", cp.issueViewPassword)
|
||||||
|
cr.GET("/terminal/:id", cp.terminal)
|
||||||
|
cr.Use(mygin.ValidateViewPassword(mygin.ValidateViewPasswordOption{
|
||||||
|
IsPage: true,
|
||||||
|
AbortWhenFail: true,
|
||||||
|
}))
|
||||||
cr.GET("/", cp.home)
|
cr.GET("/", cp.home)
|
||||||
cr.GET("/service", cp.service)
|
cr.GET("/service", cp.service)
|
||||||
// TODO: 界面直接跳转使用该接口
|
// TODO: 界面直接跳转使用该接口
|
||||||
@@ -65,6 +67,7 @@ type viewPasswordForm struct {
|
|||||||
func (p *commonPage) issueViewPassword(c *gin.Context) {
|
func (p *commonPage) issueViewPassword(c *gin.Context) {
|
||||||
var vpf viewPasswordForm
|
var vpf viewPasswordForm
|
||||||
err := c.ShouldBind(&vpf)
|
err := c.ShouldBind(&vpf)
|
||||||
|
log.Println("bingo", vpf)
|
||||||
var hash []byte
|
var hash []byte
|
||||||
if err == nil && vpf.Password != singleton.Conf.Site.ViewPassword {
|
if err == nil && vpf.Password != singleton.Conf.Site.ViewPassword {
|
||||||
err = errors.New(singleton.Localizer.MustLocalize(&i18n.LocalizeConfig{MessageID: "WrongAccessPassword"}))
|
err = errors.New(singleton.Localizer.MustLocalize(&i18n.LocalizeConfig{MessageID: "WrongAccessPassword"}))
|
||||||
|
|||||||
@@ -0,0 +1,177 @@
|
|||||||
|
package ddns
|
||||||
|
|
||||||
|
import (
|
||||||
|
"bytes"
|
||||||
|
"encoding/json"
|
||||||
|
"fmt"
|
||||||
|
"io"
|
||||||
|
"log"
|
||||||
|
"net/http"
|
||||||
|
)
|
||||||
|
|
||||||
|
type ProviderCloudflare struct {
|
||||||
|
Secret string
|
||||||
|
}
|
||||||
|
|
||||||
|
func (provider ProviderCloudflare) UpdateDomain(domainConfig *DomainConfig) bool {
|
||||||
|
if domainConfig == nil {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
zoneID, err := provider.getZoneID(domainConfig.FullDomain)
|
||||||
|
if err != nil {
|
||||||
|
log.Printf("无法获取 zone ID: %s\n", err)
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
// 当IPv4和IPv6同时成功才算作成功
|
||||||
|
var resultV4 = true
|
||||||
|
var resultV6 = true
|
||||||
|
if domainConfig.EnableIPv4 {
|
||||||
|
if !provider.addDomainRecord(zoneID, domainConfig, true) {
|
||||||
|
resultV4 = false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if domainConfig.EnableIpv6 {
|
||||||
|
if !provider.addDomainRecord(zoneID, domainConfig, false) {
|
||||||
|
resultV6 = false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return resultV4 && resultV6
|
||||||
|
}
|
||||||
|
|
||||||
|
func (provider ProviderCloudflare) addDomainRecord(zoneID string, domainConfig *DomainConfig, isIpv4 bool) bool {
|
||||||
|
record, err := provider.findDNSRecord(zoneID, domainConfig.FullDomain, isIpv4)
|
||||||
|
if err != nil {
|
||||||
|
log.Printf("查找 DNS 记录时出错: %s\n", err)
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
if record == nil {
|
||||||
|
// 添加 DNS 记录
|
||||||
|
return provider.createDNSRecord(zoneID, domainConfig, isIpv4)
|
||||||
|
} else {
|
||||||
|
// 更新 DNS 记录
|
||||||
|
return provider.updateDNSRecord(zoneID, record["id"].(string), domainConfig, 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)
|
||||||
|
if err != nil {
|
||||||
|
return "", err
|
||||||
|
}
|
||||||
|
|
||||||
|
var res map[string]interface{}
|
||||||
|
err = json.Unmarshal(body, &res)
|
||||||
|
if err != nil {
|
||||||
|
return "", err
|
||||||
|
}
|
||||||
|
|
||||||
|
result := res["result"].([]interface{})
|
||||||
|
if len(result) > 0 {
|
||||||
|
zoneID := result[0].(map[string]interface{})["id"].(string)
|
||||||
|
return zoneID, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
return "", fmt.Errorf("找不到 Zone ID")
|
||||||
|
}
|
||||||
|
|
||||||
|
func (provider ProviderCloudflare) findDNSRecord(zoneID string, domain string, isIPv4 bool) (map[string]interface{}, error) {
|
||||||
|
var ipType = "A"
|
||||||
|
if !isIPv4 {
|
||||||
|
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)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
var res map[string]interface{}
|
||||||
|
err = json.Unmarshal(body, &res)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
result := res["result"].([]interface{})
|
||||||
|
if len(result) > 0 {
|
||||||
|
return result[0].(map[string]interface{}), nil
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil, nil // 没有找到 DNS 记录
|
||||||
|
}
|
||||||
|
|
||||||
|
func (provider ProviderCloudflare) createDNSRecord(zoneID string, domainConfig *DomainConfig, isIPv4 bool) bool {
|
||||||
|
var ipType = "A"
|
||||||
|
var ipAddr = domainConfig.Ipv4Addr
|
||||||
|
if !isIPv4 {
|
||||||
|
ipType = "AAAA"
|
||||||
|
ipAddr = 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,
|
||||||
|
}
|
||||||
|
jsonData, _ := json.Marshal(data)
|
||||||
|
_, err := provider.sendRequest("POST", url, jsonData)
|
||||||
|
return err == nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (provider ProviderCloudflare) updateDNSRecord(zoneID string, recordID string, domainConfig *DomainConfig, isIPv4 bool) bool {
|
||||||
|
var ipType = "A"
|
||||||
|
var ipAddr = domainConfig.Ipv4Addr
|
||||||
|
if !isIPv4 {
|
||||||
|
ipType = "AAAA"
|
||||||
|
ipAddr = 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,
|
||||||
|
}
|
||||||
|
jsonData, _ := json.Marshal(data)
|
||||||
|
_, err := provider.sendRequest("PATCH", url, jsonData)
|
||||||
|
return err == nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// 以下为辅助方法,如发送 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("Content-Type", "application/json")
|
||||||
|
|
||||||
|
resp, err := client.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())
|
||||||
|
}
|
||||||
|
}(resp.Body)
|
||||||
|
|
||||||
|
body, err := io.ReadAll(resp.Body)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
return body, nil
|
||||||
|
}
|
||||||
@@ -0,0 +1,14 @@
|
|||||||
|
package ddns
|
||||||
|
|
||||||
|
type DomainConfig struct {
|
||||||
|
EnableIPv4 bool
|
||||||
|
EnableIpv6 bool
|
||||||
|
FullDomain string
|
||||||
|
Ipv4Addr string
|
||||||
|
Ipv6Addr string
|
||||||
|
}
|
||||||
|
|
||||||
|
type Provider interface {
|
||||||
|
// UpdateDomain Return is updated
|
||||||
|
UpdateDomain(domainConfig *DomainConfig) bool
|
||||||
|
}
|
||||||
@@ -0,0 +1,7 @@
|
|||||||
|
package ddns
|
||||||
|
|
||||||
|
type ProviderDummy struct{}
|
||||||
|
|
||||||
|
func (provider ProviderDummy) UpdateDomain(domainConfig *DomainConfig) bool {
|
||||||
|
return false
|
||||||
|
}
|
||||||
@@ -0,0 +1,61 @@
|
|||||||
|
package ddns
|
||||||
|
|
||||||
|
import (
|
||||||
|
"fmt"
|
||||||
|
"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, topLevelDomain string) {
|
||||||
|
// 带有二级TLD的一些常见例子,需要特别处理
|
||||||
|
secondLevelTLDs := map[string]bool{
|
||||||
|
".co.uk": true, ".com.cn": true, ".gov.cn": true, ".net.cn": true, ".org.cn": true,
|
||||||
|
}
|
||||||
|
|
||||||
|
// 分割域名为"."的各部分
|
||||||
|
parts := strings.Split(domain, ".")
|
||||||
|
|
||||||
|
// 处理特殊情况,例如 ".co.uk"
|
||||||
|
for i := len(parts) - 2; i > 0; i-- {
|
||||||
|
potentialTLD := fmt.Sprintf(".%s.%s", parts[i], parts[i+1])
|
||||||
|
if secondLevelTLDs[potentialTLD] {
|
||||||
|
if i > 1 {
|
||||||
|
return strings.Join(parts[:i-1], "."), strings.Join(parts[i-1:], ".")
|
||||||
|
}
|
||||||
|
return "", domain // 当域名仅为二级TLD时,无前缀
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 常规处理,查找最后一个"."前的所有内容作为前缀
|
||||||
|
if len(parts) > 2 {
|
||||||
|
return strings.Join(parts[:len(parts)-2], "."), strings.Join(parts[len(parts)-2:], ".")
|
||||||
|
}
|
||||||
|
return "", domain // 当域名不包含子域名时,无前缀
|
||||||
|
}
|
||||||
@@ -0,0 +1,59 @@
|
|||||||
|
package ddns
|
||||||
|
|
||||||
|
import (
|
||||||
|
"bytes"
|
||||||
|
"log"
|
||||||
|
"net/http"
|
||||||
|
"strings"
|
||||||
|
)
|
||||||
|
|
||||||
|
type ProviderWebHook struct {
|
||||||
|
URL string
|
||||||
|
RequestMethod string
|
||||||
|
RequestBody string
|
||||||
|
RequestHeader string
|
||||||
|
}
|
||||||
|
|
||||||
|
func (provider ProviderWebHook) UpdateDomain(domainConfig *DomainConfig) bool {
|
||||||
|
if domainConfig == nil {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
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 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
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return updated
|
||||||
|
}
|
||||||
+7
-25
@@ -6,22 +6,19 @@ import (
|
|||||||
"time"
|
"time"
|
||||||
|
|
||||||
"github.com/gin-gonic/gin"
|
"github.com/gin-gonic/gin"
|
||||||
"github.com/nicksnyder/go-i18n/v2/i18n"
|
|
||||||
"golang.org/x/crypto/bcrypt"
|
|
||||||
|
|
||||||
"github.com/naiba/nezha/model"
|
"github.com/naiba/nezha/model"
|
||||||
"github.com/naiba/nezha/service/singleton"
|
"github.com/naiba/nezha/service/singleton"
|
||||||
)
|
)
|
||||||
|
|
||||||
type AuthorizeOption struct {
|
type AuthorizeOption struct {
|
||||||
GuestOnly bool
|
GuestOnly bool
|
||||||
MemberOnly bool
|
MemberOnly bool
|
||||||
ValidateViewPassword bool
|
IsPage bool
|
||||||
IsPage bool
|
AllowAPI bool
|
||||||
AllowAPI bool
|
Msg string
|
||||||
Msg string
|
Redirect string
|
||||||
Redirect string
|
Btn string
|
||||||
Btn string
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func Authorize(opt AuthorizeOption) func(*gin.Context) {
|
func Authorize(opt AuthorizeOption) func(*gin.Context) {
|
||||||
@@ -82,20 +79,5 @@ func Authorize(opt AuthorizeOption) func(*gin.Context) {
|
|||||||
ShowErrorPage(c, commonErr, opt.IsPage)
|
ShowErrorPage(c, commonErr, opt.IsPage)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
// 验证查看密码
|
|
||||||
if opt.ValidateViewPassword && singleton.Conf.Site.ViewPassword != "" {
|
|
||||||
viewPassword, _ := c.Cookie(singleton.Conf.Site.CookieName + "-vp")
|
|
||||||
if err := bcrypt.CompareHashAndPassword([]byte(viewPassword), []byte(singleton.Conf.Site.ViewPassword)); err != nil {
|
|
||||||
c.HTML(http.StatusOK, GetPreferredTheme(c, "/viewpassword"), CommonEnvironment(c, gin.H{
|
|
||||||
"Title": singleton.Localizer.MustLocalize(&i18n.LocalizeConfig{MessageID: "VerifyPassword"}),
|
|
||||||
"CustomCode": singleton.Conf.Site.CustomCode,
|
|
||||||
}))
|
|
||||||
c.Abort()
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
c.Set(model.CtxKeyViewPasswordVerified, true)
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -14,7 +14,7 @@ func PreferredTheme(c *gin.Context) {
|
|||||||
if theme, err := c.Cookie("preferred_theme"); err == nil {
|
if theme, err := c.Cookie("preferred_theme"); err == nil {
|
||||||
if _, has := model.Themes[theme]; has {
|
if _, has := model.Themes[theme]; has {
|
||||||
// 检验自定义主题
|
// 检验自定义主题
|
||||||
if theme == "custom" && singleton.Conf.Site.Theme != "custom" && !utils.IsFileExists("resource/template/custom/home.html") {
|
if theme == "custom" && singleton.Conf.Site.Theme != "custom" && !utils.IsFileExists("resource/template/theme-custom/home.html") {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
c.Set(model.CtxKeyPreferredTheme, theme)
|
c.Set(model.CtxKeyPreferredTheme, theme)
|
||||||
|
|||||||
@@ -0,0 +1,52 @@
|
|||||||
|
package mygin
|
||||||
|
|
||||||
|
import (
|
||||||
|
"net/http"
|
||||||
|
|
||||||
|
"github.com/gin-gonic/gin"
|
||||||
|
"github.com/naiba/nezha/model"
|
||||||
|
"github.com/naiba/nezha/service/singleton"
|
||||||
|
"github.com/nicksnyder/go-i18n/v2/i18n"
|
||||||
|
"golang.org/x/crypto/bcrypt"
|
||||||
|
)
|
||||||
|
|
||||||
|
type ValidateViewPasswordOption struct {
|
||||||
|
IsPage bool
|
||||||
|
AbortWhenFail bool
|
||||||
|
}
|
||||||
|
|
||||||
|
func ValidateViewPassword(opt ValidateViewPasswordOption) gin.HandlerFunc {
|
||||||
|
return func(c *gin.Context) {
|
||||||
|
if singleton.Conf.Site.ViewPassword == "" {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
_, authorized := c.Get(model.CtxKeyAuthorizedUser)
|
||||||
|
if authorized {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
viewPassword, err := c.Cookie(singleton.Conf.Site.CookieName + "-vp")
|
||||||
|
if err == nil {
|
||||||
|
err = bcrypt.CompareHashAndPassword([]byte(viewPassword), []byte(singleton.Conf.Site.ViewPassword))
|
||||||
|
}
|
||||||
|
if err == nil {
|
||||||
|
c.Set(model.CtxKeyViewPasswordVerified, true)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
if !opt.AbortWhenFail {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
if opt.IsPage {
|
||||||
|
c.HTML(http.StatusOK, GetPreferredTheme(c, "/viewpassword"), CommonEnvironment(c, gin.H{
|
||||||
|
"Title": singleton.Localizer.MustLocalize(&i18n.LocalizeConfig{MessageID: "VerifyPassword"}),
|
||||||
|
"CustomCode": singleton.Conf.Site.CustomCode,
|
||||||
|
}))
|
||||||
|
|
||||||
|
} else {
|
||||||
|
c.JSON(http.StatusOK, model.Response{
|
||||||
|
Code: http.StatusForbidden,
|
||||||
|
Message: "访问受限",
|
||||||
|
})
|
||||||
|
}
|
||||||
|
c.Abort()
|
||||||
|
}
|
||||||
|
}
|
||||||
Vendored
+5
-2
@@ -398,7 +398,7 @@ other = "Virtualization"
|
|||||||
other = "Swap"
|
other = "Swap"
|
||||||
|
|
||||||
[NetTransfer]
|
[NetTransfer]
|
||||||
other = "Network Transfer"
|
other = "Transfer"
|
||||||
|
|
||||||
[Load]
|
[Load]
|
||||||
other = "Load"
|
other = "Load"
|
||||||
@@ -419,7 +419,7 @@ other = "Last Active"
|
|||||||
other = "Version"
|
other = "Version"
|
||||||
|
|
||||||
[NetSpeed]
|
[NetSpeed]
|
||||||
other = "Network Speed"
|
other = "Speed"
|
||||||
|
|
||||||
[Uptime]
|
[Uptime]
|
||||||
other = "Uptime"
|
other = "Uptime"
|
||||||
@@ -627,3 +627,6 @@ other = "Feature"
|
|||||||
|
|
||||||
[Template]
|
[Template]
|
||||||
other = "Template"
|
other = "Template"
|
||||||
|
|
||||||
|
[Stat]
|
||||||
|
other = "Stat"
|
||||||
|
|||||||
Vendored
+3
@@ -627,3 +627,6 @@ other = "Característica"
|
|||||||
|
|
||||||
[Template]
|
[Template]
|
||||||
other = "Plantilla"
|
other = "Plantilla"
|
||||||
|
|
||||||
|
[Stat]
|
||||||
|
other = "Stat"
|
||||||
|
|||||||
Vendored
+3
@@ -627,3 +627,6 @@ other = "功能"
|
|||||||
|
|
||||||
[Template]
|
[Template]
|
||||||
other = "主题"
|
other = "主题"
|
||||||
|
|
||||||
|
[Stat]
|
||||||
|
other = "信息"
|
||||||
|
|||||||
Vendored
+3
@@ -627,3 +627,6 @@ other = "功能"
|
|||||||
|
|
||||||
[Template]
|
[Template]
|
||||||
other = "主題"
|
other = "主題"
|
||||||
|
|
||||||
|
[Stat]
|
||||||
|
other = "信息"
|
||||||
|
|||||||
+19
-4
@@ -86,21 +86,35 @@ td {
|
|||||||
margin-right: 0;
|
margin-right: 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.ui.grid {
|
||||||
|
margin-bottom:-0.5em
|
||||||
|
}
|
||||||
|
|
||||||
|
.ui.card>.content>.header:not(.ui), .ui.cards>.card>.content>.header:not(.ui){
|
||||||
|
line-height: 1em;
|
||||||
|
}
|
||||||
|
|
||||||
.status.cards .wide.column {
|
.status.cards .wide.column {
|
||||||
padding-top: 0 !important;
|
padding-top: 0 !important;
|
||||||
padding-bottom: 0 !important;
|
padding-bottom: 0 !important;
|
||||||
height:3.3rem !important;
|
height:2.3rem !important;
|
||||||
}
|
}
|
||||||
|
|
||||||
.status.cards .wide.column:nth-child(1) {
|
.status.cards .wide.column:nth-child(1) {
|
||||||
margin-top:2rem !important;
|
margin-top:1.2rem !important;
|
||||||
}
|
}
|
||||||
|
|
||||||
.status.cards .wide.column:nth-child(2) {
|
.status.cards .wide.column:nth-child(2) {
|
||||||
margin-top:2rem !important;
|
margin-top:1.2rem !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
.status.cards .three.wide.column {
|
||||||
|
text-align: center;
|
||||||
|
width: 22%!important;
|
||||||
}
|
}
|
||||||
|
|
||||||
.status.cards .thirteen.wide.column{
|
.status.cards .thirteen.wide.column{
|
||||||
|
width: 78%!important;
|
||||||
padding-left:0;
|
padding-left:0;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -124,7 +138,7 @@ td {
|
|||||||
.closePopup{
|
.closePopup{
|
||||||
color:rgb(10, 148, 242) !important;
|
color:rgb(10, 148, 242) !important;
|
||||||
position: absolute;
|
position: absolute;
|
||||||
top: 10px;
|
top: 7px;
|
||||||
right: 10px;
|
right: 10px;
|
||||||
cursor: pointer;
|
cursor: pointer;
|
||||||
z-index: 9999;
|
z-index: 9999;
|
||||||
@@ -194,6 +208,7 @@ td {
|
|||||||
}
|
}
|
||||||
|
|
||||||
.nezha-secondary-font {
|
.nezha-secondary-font {
|
||||||
|
height: 1em;
|
||||||
color: rgb(10, 148, 242) !important;
|
color: rgb(10, 148, 242) !important;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
+56
@@ -0,0 +1,56 @@
|
|||||||
|
const mixinsVue = {
|
||||||
|
delimiters: ['@#', '#@'],
|
||||||
|
data: {
|
||||||
|
preferredTemplate: null,
|
||||||
|
isMobile: false
|
||||||
|
},
|
||||||
|
created() {
|
||||||
|
this.isMobile = this.checkIsMobile();
|
||||||
|
this.preferredTemplate = this.getCookie('preferred_theme') ? this.getCookie('preferred_theme') : this.$root.defaultTemplate;
|
||||||
|
},
|
||||||
|
methods: {
|
||||||
|
toggleTemplate(template) {
|
||||||
|
if( template != this.preferredTemplate){
|
||||||
|
this.preferredTemplate = template;
|
||||||
|
this.updateCookie("preferred_theme", template);
|
||||||
|
window.location.reload();
|
||||||
|
}
|
||||||
|
},
|
||||||
|
updateCookie(name, value) {
|
||||||
|
document.cookie = name + "=" + value +"; path=/";
|
||||||
|
},
|
||||||
|
getCookie(name) {
|
||||||
|
const cookies = document.cookie.split(';');
|
||||||
|
let cookieValue = null;
|
||||||
|
for (let i = 0; i < cookies.length; i++) {
|
||||||
|
const cookie = cookies[i].trim();
|
||||||
|
if (cookie.startsWith(name + '=')) {
|
||||||
|
cookieValue = cookie.substring(name.length + 1, cookie.length);
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return cookieValue;
|
||||||
|
},
|
||||||
|
checkIsMobile() { // 检测设备类型,页面宽度小于768px认为是移动设备
|
||||||
|
return window.innerWidth <= 768;
|
||||||
|
},
|
||||||
|
logOut(id) {
|
||||||
|
$.ajax({
|
||||||
|
type: 'POST',
|
||||||
|
url: '/api/logout',
|
||||||
|
data: JSON.stringify({ id: id }),
|
||||||
|
contentType: 'application/json',
|
||||||
|
success: function (resp) {
|
||||||
|
if (resp.code == 200) {
|
||||||
|
window.location.reload();
|
||||||
|
} else {
|
||||||
|
alert('注销失败(Error ' + resp.code + '): ' + resp.message);
|
||||||
|
}
|
||||||
|
},
|
||||||
|
error: function (err) {
|
||||||
|
alert('网络错误: ' + err.responseText);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
+2
-2
@@ -5,14 +5,14 @@ const mixinsVue = {
|
|||||||
isSystemTheme: false,
|
isSystemTheme: false,
|
||||||
showGroup: false,
|
showGroup: false,
|
||||||
showGoTop: false,
|
showGoTop: false,
|
||||||
preferredTemplate: 'default',
|
preferredTemplate: null,
|
||||||
isMobile: false
|
isMobile: false
|
||||||
},
|
},
|
||||||
created() {
|
created() {
|
||||||
this.isMobile = this.checkIsMobile();
|
this.isMobile = this.checkIsMobile();
|
||||||
this.initTheme();
|
this.initTheme();
|
||||||
this.storedShowGroup();
|
this.storedShowGroup();
|
||||||
this.preferredTemplate = this.getCookie('preferred_theme') ? this.getCookie('preferred_theme') : 'default';
|
this.preferredTemplate = this.getCookie('preferred_theme') ? this.getCookie('preferred_theme') : this.$root.defaultTemplate;
|
||||||
window.addEventListener('scroll', this.handleScroll);
|
window.addEventListener('scroll', this.handleScroll);
|
||||||
},
|
},
|
||||||
destroyed() {
|
destroyed() {
|
||||||
|
|||||||
+2
-2
@@ -12,14 +12,14 @@
|
|||||||
<link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/[email protected]/font/bootstrap-icons.min.css">
|
<link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/[email protected]/font/bootstrap-icons.min.css">
|
||||||
<link rel="stylesheet" href="https://cdn.jsdelivr.net/gh/lipis/[email protected]/css/flag-icons.min.css">
|
<link rel="stylesheet" href="https://cdn.jsdelivr.net/gh/lipis/[email protected]/css/flag-icons.min.css">
|
||||||
<link rel="stylesheet" type="text/css" href="/static/semantic-ui-alerts.min.css">
|
<link rel="stylesheet" type="text/css" href="/static/semantic-ui-alerts.min.css">
|
||||||
<link rel="stylesheet" type="text/css" href="/static/theme-default/css/main.css?v20240222">
|
<link rel="stylesheet" type="text/css" href="/static/theme-default/css/main.css?v20240226">
|
||||||
<link rel="shortcut icon" type="image/png" href="/static/logo.svg" />
|
<link rel="shortcut icon" type="image/png" href="/static/logo.svg" />
|
||||||
<script src="https://cdn.jsdelivr.net/npm/[email protected]/dist/jquery.min.js"></script>
|
<script src="https://cdn.jsdelivr.net/npm/[email protected]/dist/jquery.min.js"></script>
|
||||||
<script src="https://cdn.jsdelivr.net/npm/[email protected]/dist/semantic.min.js"></script>
|
<script src="https://cdn.jsdelivr.net/npm/[email protected]/dist/semantic.min.js"></script>
|
||||||
<script src="/static/semantic-ui-alerts.min.js"></script>
|
<script src="/static/semantic-ui-alerts.min.js"></script>
|
||||||
<script src="https://cdn.jsdelivr.net/npm/[email protected]"></script>
|
<script src="https://cdn.jsdelivr.net/npm/[email protected]"></script>
|
||||||
<script src="https://cdn.jsdelivr.net/npm/[email protected]/dist/echarts.min.js"></script>
|
<script src="https://cdn.jsdelivr.net/npm/[email protected]/dist/echarts.min.js"></script>
|
||||||
<script src="/static/theme-default/js/mixin.js?v20240225"></script>
|
<script src="/static/theme-default/js/mixin.js?v20240226"></script>
|
||||||
</head>
|
</head>
|
||||||
<body>
|
<body>
|
||||||
{{end}}
|
{{end}}
|
||||||
+4
-3
@@ -19,7 +19,7 @@
|
|||||||
class="apple icon"></i><i v-else-if='isWindowsPlatform(server.Host.Platform)'
|
class="apple icon"></i><i v-else-if='isWindowsPlatform(server.Host.Platform)'
|
||||||
class="windows icon"></i><i v-else :class="'fl-' + getFontLogoClass(server.Host.Platform)"></i>
|
class="windows icon"></i><i v-else :class="'fl-' + getFontLogoClass(server.Host.Platform)"></i>
|
||||||
@#server.Name + (server.live?'':'[{{tr "Offline"}}]')#@
|
@#server.Name + (server.live?'':'[{{tr "Offline"}}]')#@
|
||||||
<i @click="togglePopup($event, server.ID)" aria-expanded="false" class="nezha-secondary-font info circle icon" style="height: 28px"></i>
|
<i @click="togglePopup($event, server.ID)" aria-expanded="false" class="nezha-secondary-font info circle icon"></i>
|
||||||
<div class="ui content popup" :class="{ 'visible': isActive(server.ID) }" style="margin-bottom: 0;">
|
<div class="ui content popup" :class="{ 'visible': isActive(server.ID) }" style="margin-bottom: 0;">
|
||||||
<i class="closePopup window close icon" @click="closePopup(server.ID)"></i>
|
<i class="closePopup window close icon" @click="closePopup(server.ID)"></i>
|
||||||
{{tr "Platform"}}: @#server.Host.Platform#@-@#server.Host.PlatformVersion#@
|
{{tr "Platform"}}: @#server.Host.Platform#@-@#server.Host.PlatformVersion#@
|
||||||
@@ -90,7 +90,7 @@
|
|||||||
<i class="arrow alternate circle up outline icon"></i>
|
<i class="arrow alternate circle up outline icon"></i>
|
||||||
@#formatByteSize(server.State.NetOutSpeed)#@/s
|
@#formatByteSize(server.State.NetOutSpeed)#@/s
|
||||||
</div>
|
</div>
|
||||||
<div class="three wide column">流量</div>
|
<div class="three wide column">{{tr "NetTransfer"}}</div>
|
||||||
<div class="thirteen wide column">
|
<div class="thirteen wide column">
|
||||||
<i class="arrow circle down icon"></i>
|
<i class="arrow circle down icon"></i>
|
||||||
@#formatByteSize(server.State.NetInTransfer)#@
|
@#formatByteSize(server.State.NetInTransfer)#@
|
||||||
@@ -98,7 +98,7 @@
|
|||||||
<i class="arrow circle up icon"></i>
|
<i class="arrow circle up icon"></i>
|
||||||
@#formatByteSize(server.State.NetOutTransfer)#@
|
@#formatByteSize(server.State.NetOutTransfer)#@
|
||||||
</div>
|
</div>
|
||||||
<div class="three wide column">信息</div>
|
<div class="three wide column">{{tr "Stat"}}</div>
|
||||||
<div class="thirteen wide column">
|
<div class="thirteen wide column">
|
||||||
<i class="bi bi-cpu-fill" style="font-size: 1.1rem; color: #4a86e8;"></i> @#getCoreAndGHz(server.Host.CPU)#@
|
<i class="bi bi-cpu-fill" style="font-size: 1.1rem; color: #4a86e8;"></i> @#getCoreAndGHz(server.Host.CPU)#@
|
||||||
|
|
||||||
@@ -138,6 +138,7 @@
|
|||||||
delimiters: ['@#', '#@'],
|
delimiters: ['@#', '#@'],
|
||||||
data: {
|
data: {
|
||||||
page: 'index',
|
page: 'index',
|
||||||
|
defaultTemplate: {{.Conf.Site.Theme}},
|
||||||
templates: {{.Themes}},
|
templates: {{.Themes}},
|
||||||
data: [],
|
data: [],
|
||||||
groups: [],
|
groups: [],
|
||||||
|
|||||||
@@ -36,6 +36,7 @@
|
|||||||
delimiters: ['@#', '#@'],
|
delimiters: ['@#', '#@'],
|
||||||
data: {
|
data: {
|
||||||
page: 'network',
|
page: 'network',
|
||||||
|
defaultTemplate: {{.Conf.Site.Theme}},
|
||||||
templates: {{.Themes}},
|
templates: {{.Themes}},
|
||||||
servers: initData,
|
servers: initData,
|
||||||
option: {
|
option: {
|
||||||
|
|||||||
@@ -87,6 +87,7 @@
|
|||||||
delimiters: ['@#', '#@'],
|
delimiters: ['@#', '#@'],
|
||||||
data: {
|
data: {
|
||||||
page: 'service',
|
page: 'service',
|
||||||
|
defaultTemplate: {{.Conf.Site.Theme}},
|
||||||
templates: {{.Themes}}
|
templates: {{.Themes}}
|
||||||
},
|
},
|
||||||
mixins: [mixinsVue]
|
mixins: [mixinsVue]
|
||||||
|
|||||||
+1
-1
@@ -28,7 +28,7 @@
|
|||||||
<script src="https://cdn.jsdelivr.net/npm/[email protected]/dist/js/bootstrap.min.js"></script>
|
<script src="https://cdn.jsdelivr.net/npm/[email protected]/dist/js/bootstrap.min.js"></script>
|
||||||
<script src="https://cdn.jsdelivr.net/npm/[email protected]"></script>
|
<script src="https://cdn.jsdelivr.net/npm/[email protected]"></script>
|
||||||
<script src="https://cdn.jsdelivr.net/npm/[email protected]/dist/echarts.min.js"></script>
|
<script src="https://cdn.jsdelivr.net/npm/[email protected]/dist/echarts.min.js"></script>
|
||||||
<script src="/static/theme-server-status/js/mixin.js?v20240225"></script>
|
<script src="/static/theme-server-status/js/mixin.js?v20240226"></script>
|
||||||
</head>
|
</head>
|
||||||
<body>
|
<body>
|
||||||
{{end}}
|
{{end}}
|
||||||
@@ -22,6 +22,7 @@
|
|||||||
delimiters: ['@#', '#@'],
|
delimiters: ['@#', '#@'],
|
||||||
data: {
|
data: {
|
||||||
page: 'index',
|
page: 'index',
|
||||||
|
defaultTemplate: {{.Conf.Site.Theme}},
|
||||||
templates: {{.Themes}},
|
templates: {{.Themes}},
|
||||||
nodesTag: [],
|
nodesTag: [],
|
||||||
nodesNoTag: [],
|
nodesNoTag: [],
|
||||||
|
|||||||
@@ -30,6 +30,7 @@
|
|||||||
delimiters: ['@#', '#@'],
|
delimiters: ['@#', '#@'],
|
||||||
data: {
|
data: {
|
||||||
page: 'network',
|
page: 'network',
|
||||||
|
defaultTemplate: {{.Conf.Site.Theme}},
|
||||||
templates: {{.Themes}},
|
templates: {{.Themes}},
|
||||||
servers: initData,
|
servers: initData,
|
||||||
option: {
|
option: {
|
||||||
|
|||||||
@@ -78,6 +78,7 @@
|
|||||||
delimiters: ['@#', '#@'],
|
delimiters: ['@#', '#@'],
|
||||||
data: {
|
data: {
|
||||||
page: 'service',
|
page: 'service',
|
||||||
|
defaultTemplate: {{.Conf.Site.Theme}},
|
||||||
templates: {{.Themes}},
|
templates: {{.Themes}},
|
||||||
servicesTag: [],
|
servicesTag: [],
|
||||||
servicesNoTag: [],
|
servicesNoTag: [],
|
||||||
|
|||||||
@@ -3,6 +3,7 @@ package rpc
|
|||||||
import (
|
import (
|
||||||
"context"
|
"context"
|
||||||
"fmt"
|
"fmt"
|
||||||
|
"github.com/naiba/nezha/pkg/ddns"
|
||||||
"github.com/naiba/nezha/pkg/utils"
|
"github.com/naiba/nezha/pkg/utils"
|
||||||
"log"
|
"log"
|
||||||
"time"
|
"time"
|
||||||
@@ -125,7 +126,7 @@ func (s *NezhaHandler) ReportSystemInfo(c context.Context, r *pb.Host) (*pb.Rece
|
|||||||
if err == nil && serverDomain != "" {
|
if err == nil && serverDomain != "" {
|
||||||
ipv4, ipv6, _ := utils.SplitIPAddr(host.IP)
|
ipv4, ipv6, _ := utils.SplitIPAddr(host.IP)
|
||||||
maxRetries := int(singleton.Conf.DDNS.MaxRetries)
|
maxRetries := int(singleton.Conf.DDNS.MaxRetries)
|
||||||
config := &singleton.DDNSDomainConfig{
|
config := &ddns.DomainConfig{
|
||||||
EnableIPv4: true,
|
EnableIPv4: true,
|
||||||
EnableIpv6: true,
|
EnableIpv6: true,
|
||||||
FullDomain: serverDomain,
|
FullDomain: serverDomain,
|
||||||
|
|||||||
+7
-267
@@ -1,273 +1,13 @@
|
|||||||
package singleton
|
package singleton
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"bytes"
|
|
||||||
"encoding/json"
|
|
||||||
"errors"
|
"errors"
|
||||||
"fmt"
|
"fmt"
|
||||||
"io"
|
ddns2 "github.com/naiba/nezha/pkg/ddns"
|
||||||
"log"
|
"log"
|
||||||
"net/http"
|
|
||||||
"strings"
|
|
||||||
)
|
)
|
||||||
|
|
||||||
type DDNSDomainConfig struct {
|
func RetryableUpdateDomain(provider ddns2.Provider, config *ddns2.DomainConfig, maxRetries int) bool {
|
||||||
EnableIPv4 bool
|
|
||||||
EnableIpv6 bool
|
|
||||||
FullDomain string
|
|
||||||
Ipv4Addr string
|
|
||||||
Ipv6Addr string
|
|
||||||
}
|
|
||||||
|
|
||||||
type DDNSProvider interface {
|
|
||||||
// UpdateDomain Return is updated
|
|
||||||
UpdateDomain(domainConfig *DDNSDomainConfig) bool
|
|
||||||
}
|
|
||||||
|
|
||||||
type DDNSProviderWebHook struct {
|
|
||||||
URL string
|
|
||||||
RequestMethod string
|
|
||||||
RequestBody string
|
|
||||||
RequestHeader string
|
|
||||||
}
|
|
||||||
|
|
||||||
func (provider DDNSProviderWebHook) UpdateDomain(domainConfig *DDNSDomainConfig) bool {
|
|
||||||
if domainConfig == nil {
|
|
||||||
return false
|
|
||||||
}
|
|
||||||
|
|
||||||
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())
|
|
||||||
}
|
|
||||||
updated = true
|
|
||||||
}
|
|
||||||
}
|
|
||||||
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())
|
|
||||||
}
|
|
||||||
updated = true
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return updated
|
|
||||||
}
|
|
||||||
|
|
||||||
type DDNSProviderDummy struct{}
|
|
||||||
|
|
||||||
func (provider DDNSProviderDummy) UpdateDomain(domainConfig *DDNSDomainConfig) bool {
|
|
||||||
return false
|
|
||||||
}
|
|
||||||
|
|
||||||
type DDNSProviderCloudflare struct {
|
|
||||||
Secret string
|
|
||||||
}
|
|
||||||
|
|
||||||
func (provider DDNSProviderCloudflare) UpdateDomain(domainConfig *DDNSDomainConfig) bool {
|
|
||||||
if domainConfig == nil {
|
|
||||||
return false
|
|
||||||
}
|
|
||||||
|
|
||||||
zoneID, err := provider.getZoneID(domainConfig.FullDomain)
|
|
||||||
if err != nil {
|
|
||||||
log.Printf("无法获取 zone ID: %s\n", err)
|
|
||||||
return false
|
|
||||||
}
|
|
||||||
|
|
||||||
record, err := provider.findDNSRecord(zoneID, domainConfig.FullDomain)
|
|
||||||
if err != nil {
|
|
||||||
log.Printf("查找 DNS 记录时出错: %s\n", err)
|
|
||||||
return false
|
|
||||||
}
|
|
||||||
|
|
||||||
if record == nil {
|
|
||||||
// 添加 DNS 记录
|
|
||||||
return provider.createDNSRecord(zoneID, domainConfig)
|
|
||||||
} else {
|
|
||||||
// 更新 DNS 记录
|
|
||||||
return provider.updateDNSRecord(zoneID, record["id"].(string), domainConfig)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func (provider DDNSProviderCloudflare) 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)
|
|
||||||
if err != nil {
|
|
||||||
return "", err
|
|
||||||
}
|
|
||||||
|
|
||||||
var res map[string]interface{}
|
|
||||||
err = json.Unmarshal(body, &res)
|
|
||||||
if err != nil {
|
|
||||||
return "", err
|
|
||||||
}
|
|
||||||
|
|
||||||
result := res["result"].([]interface{})
|
|
||||||
if len(result) > 0 {
|
|
||||||
zoneID := result[0].(map[string]interface{})["id"].(string)
|
|
||||||
return zoneID, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
return "", fmt.Errorf("找不到 Zone ID")
|
|
||||||
}
|
|
||||||
|
|
||||||
func (provider DDNSProviderCloudflare) findDNSRecord(zoneID string, domain string) (map[string]interface{}, error) {
|
|
||||||
url := fmt.Sprintf("https://api.cloudflare.com/client/v4/zones/%s/dns_records?type=A&name=%s", zoneID, domain)
|
|
||||||
body, err := provider.sendRequest("GET", url, nil)
|
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
|
|
||||||
var res map[string]interface{}
|
|
||||||
err = json.Unmarshal(body, &res)
|
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
|
|
||||||
result := res["result"].([]interface{})
|
|
||||||
if len(result) > 0 {
|
|
||||||
return result[0].(map[string]interface{}), nil
|
|
||||||
}
|
|
||||||
|
|
||||||
return nil, nil // 没有找到 DNS 记录
|
|
||||||
}
|
|
||||||
|
|
||||||
func (provider DDNSProviderCloudflare) createDNSRecord(zoneID string, domainConfig *DDNSDomainConfig) bool {
|
|
||||||
url := fmt.Sprintf("https://api.cloudflare.com/client/v4/zones/%s/dns_records", zoneID)
|
|
||||||
data := map[string]interface{}{
|
|
||||||
"type": "A",
|
|
||||||
"name": domainConfig.FullDomain,
|
|
||||||
"content": domainConfig.Ipv4Addr,
|
|
||||||
"ttl": 3600,
|
|
||||||
"proxied": false,
|
|
||||||
}
|
|
||||||
jsonData, _ := json.Marshal(data)
|
|
||||||
_, err := provider.sendRequest("POST", url, jsonData)
|
|
||||||
return err == nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func (provider DDNSProviderCloudflare) updateDNSRecord(zoneID string, recordID string, domainConfig *DDNSDomainConfig) bool {
|
|
||||||
url := fmt.Sprintf("https://api.cloudflare.com/client/v4/zones/%s/dns_records/%s", zoneID, recordID)
|
|
||||||
data := map[string]interface{}{
|
|
||||||
"type": "A",
|
|
||||||
"name": domainConfig.FullDomain,
|
|
||||||
"content": domainConfig.Ipv4Addr,
|
|
||||||
"ttl": 3600,
|
|
||||||
"proxied": false,
|
|
||||||
}
|
|
||||||
jsonData, _ := json.Marshal(data)
|
|
||||||
_, err := provider.sendRequest("PATCH", url, jsonData)
|
|
||||||
return err == nil
|
|
||||||
}
|
|
||||||
|
|
||||||
// 以下为辅助方法,如发送 HTTP 请求等
|
|
||||||
func (provider DDNSProviderCloudflare) 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("Content-Type", "application/json")
|
|
||||||
|
|
||||||
resp, err := client.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())
|
|
||||||
}
|
|
||||||
}(resp.Body)
|
|
||||||
|
|
||||||
body, err := io.ReadAll(resp.Body)
|
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
|
|
||||||
return body, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func (provider DDNSProviderWebHook) FormatWebhookString(s string, config *DDNSDomainConfig, 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)
|
|
||||||
result = strings.Replace(result, "{access_id}", Conf.DDNS.AccessID, -1)
|
|
||||||
result = strings.Replace(result, "{access_secret}", Conf.DDNS.AccessSecret, -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, topLevelDomain string) {
|
|
||||||
// 带有二级TLD的一些常见例子,需要特别处理
|
|
||||||
secondLevelTLDs := map[string]bool{
|
|
||||||
".co.uk": true, ".com.cn": true, ".gov.cn": true, ".net.cn": true, ".org.cn": true,
|
|
||||||
}
|
|
||||||
|
|
||||||
// 分割域名为"."的各部分
|
|
||||||
parts := strings.Split(domain, ".")
|
|
||||||
|
|
||||||
// 处理特殊情况,例如 ".co.uk"
|
|
||||||
for i := len(parts) - 2; i > 0; i-- {
|
|
||||||
potentialTLD := fmt.Sprintf(".%s.%s", parts[i], parts[i+1])
|
|
||||||
if secondLevelTLDs[potentialTLD] {
|
|
||||||
if i > 1 {
|
|
||||||
return strings.Join(parts[:i-1], "."), strings.Join(parts[i-1:], ".")
|
|
||||||
}
|
|
||||||
return "", domain // 当域名仅为二级TLD时,无前缀
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// 常规处理,查找最后一个"."前的所有内容作为前缀
|
|
||||||
if len(parts) > 2 {
|
|
||||||
return strings.Join(parts[:len(parts)-2], "."), strings.Join(parts[len(parts)-2:], ".")
|
|
||||||
}
|
|
||||||
return "", domain // 当域名不包含子域名时,无前缀
|
|
||||||
}
|
|
||||||
|
|
||||||
func RetryableUpdateDomain(provider DDNSProvider, config *DDNSDomainConfig, maxRetries int) bool {
|
|
||||||
if nil == config {
|
if nil == config {
|
||||||
return false
|
return false
|
||||||
}
|
}
|
||||||
@@ -282,21 +22,21 @@ func RetryableUpdateDomain(provider DDNSProvider, config *DDNSDomainConfig, maxR
|
|||||||
return false
|
return false
|
||||||
}
|
}
|
||||||
|
|
||||||
func GetDDNSProviderFromString(provider string) (DDNSProvider, error) {
|
func GetDDNSProviderFromString(provider string) (ddns2.Provider, error) {
|
||||||
switch provider {
|
switch provider {
|
||||||
case "webhook":
|
case "webhook":
|
||||||
return DDNSProviderWebHook{
|
return ddns2.ProviderWebHook{
|
||||||
URL: Conf.DDNS.WebhookURL,
|
URL: Conf.DDNS.WebhookURL,
|
||||||
RequestMethod: Conf.DDNS.WebhookMethod,
|
RequestMethod: Conf.DDNS.WebhookMethod,
|
||||||
RequestBody: Conf.DDNS.WebhookRequestBody,
|
RequestBody: Conf.DDNS.WebhookRequestBody,
|
||||||
RequestHeader: Conf.DDNS.WebhookHeaders,
|
RequestHeader: Conf.DDNS.WebhookHeaders,
|
||||||
}, nil
|
}, nil
|
||||||
case "dummy":
|
case "dummy":
|
||||||
return DDNSProviderDummy{}, nil
|
return ddns2.ProviderDummy{}, nil
|
||||||
case "cloudflare":
|
case "cloudflare":
|
||||||
return DDNSProviderCloudflare{
|
return ddns2.ProviderCloudflare{
|
||||||
Secret: Conf.DDNS.AccessSecret,
|
Secret: Conf.DDNS.AccessSecret,
|
||||||
}, nil
|
}, nil
|
||||||
}
|
}
|
||||||
return DDNSProviderDummy{}, errors.New(fmt.Sprintf("无法找到配置的DDNS提供者%s", Conf.DDNS.Provider))
|
return ddns2.ProviderDummy{}, errors.New(fmt.Sprintf("无法找到配置的DDNS提供者%s", Conf.DDNS.Provider))
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user