Compare commits

..
15 Commits
Author SHA1 Message Date
nap0o 193455c675 主题切换相关优化 (#331)
* angel-kanade模版增加主题切换功能

* Add files via upload

* 调整legend图例间距

* 渲染方式调整会canvas

* Update home.html

* Add files via upload

* Add files via upload

* Update footer.html

* Add files via upload

* Add files via upload

* 消除echarts折线上无效的0值

* 消除echarts折线的0值,替换为平均值

* 消除echarts折线图上的0值

* 修复MaxTCPPingValue取值错误
2024-03-04 09:36:17 +08:00
github-actions[bot] dfb08caae1 update contributors[no ci] 2024-03-02 07:20:48 +00:00
nap0o 7f7763606d 修复echarts不显示bug (#330)
* angel-kanade模版增加主题切换功能

* Add files via upload

* 调整legend图例间距

* 渲染方式调整会canvas

* Update home.html
2024-03-02 15:20:32 +08:00
naiba 77de1b44bc kkgithub 已失效 2024-02-28 23:30:59 +08:00
naiba da6a3463ed 后台关闭切换主题功能 2024-02-27 21:05:39 +08:00
github-actions[bot] 6fa149519d update contributors[no ci] 2024-02-27 12:43:12 +00:00
nap0o 8e67d3487e angel-kanade模版增加主题切换功能 (#329) 2024-02-27 20:42:56 +08:00
github-actions[bot] 8033c6a0b7 update contributors[no ci] 2024-02-26 12:59:18 +00:00
naiba abb108a83e 💄 限制最大宽度 2024-02-26 20:58:50 +08:00
Darc Z cf9d9bd248 Fix DDNS bugs and split up ddns module (#326)
* fix: webhook retry logic
fix: adjust record type based on ipv4 ipv6
refract: move ddns providers to a new module

* refract: move ddns module to pkg/
2024-02-26 11:49:28 +08:00
naiba 5e528c42c7 🐛 查看密码逻辑修复 2024-02-26 10:11:02 +08:00
github-actions[bot] daa7e03240 update contributors[no ci] 2024-02-26 01:31:35 +00:00
nap0o ab39782c78 fix bug (#328)
* fix bugs

1. 修复default主题mixin.js文件丢失
2. 修复主题默认值为后台设置值
3. default主题样式优化

* 修复前台切换 theme-custom 不生效
2024-02-26 09:31:16 +08:00
github-actions[bot] e24925b049 update contributors[no ci] 2024-02-25 15:35:34 +00:00
naiba 1d4245ee09 Update mixin.js file path in header.html 2024-02-25 23:35:06 +08:00
43 changed files with 789 additions and 501 deletions
+7 -7
View File
@@ -4,7 +4,7 @@
<br>
<small><i>LOGO designed by <a href="https://xio.ng" target="_blank">熊大</a> .</i></small>
<br><br>
<img alt="GitHub release (with filter)" src="https://img.shields.io/github/v/release/naiba/nezha?color=brightgreen&style=for-the-badge&logo=github&label=Dashboard">&nbsp;<img src="https://img.shields.io/github/v/release/nezhahq/agent?color=brightgreen&label=Agent&style=for-the-badge&logo=github">&nbsp;<img src="https://img.shields.io/github/actions/workflow/status/nezhahq/agent/agent.yml?label=Agent%20CI&logo=github&style=for-the-badge">&nbsp;<img src="https://img.shields.io/badge/Installer-v0.15.8-brightgreen?style=for-the-badge&logo=linux">
<img alt="GitHub release (with filter)" src="https://img.shields.io/github/v/release/naiba/nezha?color=brightgreen&style=for-the-badge&logo=github&label=Dashboard">&nbsp;<img src="https://img.shields.io/github/v/release/nezhahq/agent?color=brightgreen&label=Agent&style=for-the-badge&logo=github">&nbsp;<img src="https://img.shields.io/github/actions/workflow/status/nezhahq/agent/agent.yml?label=Agent%20CI&logo=github&style=for-the-badge">&nbsp;<img src="https://img.shields.io/badge/Installer-v0.15.9-brightgreen?style=for-the-badge&logo=linux">
<br>
<br>
<p>:trollface: <b>Nezha Monitoring: Self-hostable, lightweight, servers and websites monitoring and O&M tool.</b></p>
@@ -68,6 +68,9 @@ You can change the dashboard language in the settings page (`/setting`) after th
<a href="https://github.com/spiritLHLS" title="spiritlhl">
<img src="https://avatars.githubusercontent.com/u/103393591?v=4" width="50;" alt="spiritlhl"/>
</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/liuyanxi975" title="刘颜溪">
<img src="https://avatars.githubusercontent.com/u/24417037?v=4" width="50;" alt="刘颜溪"/>
</a>
@@ -101,6 +104,9 @@ You can change the dashboard language in the settings page (`/setting`) after th
<a href="https://github.com/ch8o" title="no-name-now">
<img src="https://avatars.githubusercontent.com/u/9103372?v=4" width="50;" alt="no-name-now"/>
</a>
<a href="https://github.com/DarcJC" title="Darc Z.">
<img src="https://avatars.githubusercontent.com/u/53445798?v=4" width="50;" alt="Darc Z."/>
</a>
<a href="https://github.com/Creling" title="Creling">
<img src="https://avatars.githubusercontent.com/u/43109504?v=4" width="50;" alt="Creling"/>
</a>
@@ -122,9 +128,6 @@ You can change the dashboard language in the settings page (`/setting`) after th
<a href="https://github.com/unclezs" title="unclezs">
<img src="https://avatars.githubusercontent.com/u/42318775?v=4" width="50;" alt="unclezs"/>
</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">
<img src="https://avatars.githubusercontent.com/u/30067203?v=4" width="50;" alt="I"/>
</a>
@@ -161,9 +164,6 @@ You can change the dashboard language in the settings page (`/setting`) after th
<a href="https://github.com/Es-dese" title="Esdese">
<img src="https://avatars.githubusercontent.com/u/71542548?v=4" width="50;" alt="Esdese"/>
</a>
<a href="https://github.com/DarcJC" title="Darc Z.">
<img src="https://avatars.githubusercontent.com/u/53445798?v=4" width="50;" alt="Darc Z."/>
</a>
<a href="https://github.com/wwng2333" title="Crazy">
<img src="https://avatars.githubusercontent.com/u/17147265?v=4" width="50;" alt="Crazy"/>
</a><!--GAMFC_DELIMITER_END-->
+10 -7
View File
@@ -31,13 +31,16 @@ func (v *apiV1) serve() {
// 不强制认证的 API
mr := v.r.Group("monitor")
mr.Use(mygin.Authorize(mygin.AuthorizeOption{
MemberOnly: false,
IsPage: false,
ValidateViewPassword: true,
AllowAPI: true,
Msg: "访问此接口需要认证",
Btn: "点此登录",
Redirect: "/login",
MemberOnly: false,
IsPage: false,
AllowAPI: true,
Msg: "访问此接口需要认证",
Btn: "点此登录",
Redirect: "/login",
}))
mr.Use(mygin.ValidateViewPassword(mygin.ValidateViewPasswordOption{
IsPage: false,
AbortWhenFail: true,
}))
mr.GET("/:id", v.monitorHistoriesById)
}
+7 -4
View File
@@ -43,12 +43,14 @@ type commonPage struct {
func (cp *commonPage) serve() {
cr := cp.r.Group("")
cr.Use(mygin.Authorize(mygin.AuthorizeOption{
ValidateViewPassword: true,
}))
cr.Use(mygin.Authorize(mygin.AuthorizeOption{}))
cr.Use(mygin.PreferredTheme)
cr.GET("/terminal/:id", cp.terminal)
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("/service", cp.service)
// TODO: 界面直接跳转使用该接口
@@ -65,6 +67,7 @@ type viewPasswordForm struct {
func (p *commonPage) issueViewPassword(c *gin.Context) {
var vpf viewPasswordForm
err := c.ShouldBind(&vpf)
log.Println("bingo", vpf)
var hash []byte
if err == nil && vpf.Password != singleton.Conf.Site.ViewPassword {
err = errors.New(singleton.Localizer.MustLocalize(&i18n.LocalizeConfig{MessageID: "WrongAccessPassword"}))
+4 -2
View File
@@ -858,8 +858,9 @@ type settingForm struct {
GRPCHost string
Cover uint8
EnableIPChangeNotification string
EnablePlainIPInNotification string
EnableIPChangeNotification string
EnablePlainIPInNotification string
DisableSwitchTemplateInFrontend string
}
func (ma *memberAPI) updateSetting(c *gin.Context) {
@@ -907,6 +908,7 @@ func (ma *memberAPI) updateSetting(c *gin.Context) {
singleton.Conf.Language = sf.Language
singleton.Conf.EnableIPChangeNotification = sf.EnableIPChangeNotification == "on"
singleton.Conf.EnablePlainIPInNotification = sf.EnablePlainIPInNotification == "on"
singleton.Conf.DisableSwitchTemplateInFrontend = sf.DisableSwitchTemplateInFrontend == "on"
singleton.Conf.Cover = sf.Cover
singleton.Conf.GRPCHost = sf.GRPCHost
singleton.Conf.IgnoredIPNotification = sf.IgnoredIPNotification
+2 -1
View File
@@ -98,7 +98,8 @@ type Config struct {
ProxyGRPCPort uint
TLS bool
EnablePlainIPInNotification bool // 通知信息IP不打码
EnablePlainIPInNotification bool // 通知信息IP不打码
DisableSwitchTemplateInFrontend bool // 前台禁用切换模板功能
// IP变更提醒
EnableIPChangeNotification bool
+177
View File
@@ -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
}
+14
View File
@@ -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
}
+7
View File
@@ -0,0 +1,7 @@
package ddns
type ProviderDummy struct{}
func (provider ProviderDummy) UpdateDomain(domainConfig *DomainConfig) bool {
return false
}
+61
View File
@@ -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 // 当域名不包含子域名时,无前缀
}
+59
View File
@@ -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
View File
@@ -6,22 +6,19 @@ import (
"time"
"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/service/singleton"
)
type AuthorizeOption struct {
GuestOnly bool
MemberOnly bool
ValidateViewPassword bool
IsPage bool
AllowAPI bool
Msg string
Redirect string
Btn string
GuestOnly bool
MemberOnly bool
IsPage bool
AllowAPI bool
Msg string
Redirect string
Btn string
}
func Authorize(opt AuthorizeOption) func(*gin.Context) {
@@ -82,20 +79,5 @@ func Authorize(opt AuthorizeOption) func(*gin.Context) {
ShowErrorPage(c, commonErr, opt.IsPage)
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)
}
}
}
+1 -1
View File
@@ -14,7 +14,7 @@ func PreferredTheme(c *gin.Context) {
if theme, err := c.Cookie("preferred_theme"); err == nil {
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
}
c.Set(model.CtxKeyPreferredTheme, theme)
+52
View File
@@ -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()
}
}
+8 -2
View File
@@ -398,7 +398,7 @@ other = "Virtualization"
other = "Swap"
[NetTransfer]
other = "Network Transfer"
other = "Transfer"
[Load]
other = "Load"
@@ -419,7 +419,7 @@ other = "Last Active"
other = "Version"
[NetSpeed]
other = "Network Speed"
other = "Speed"
[Uptime]
other = "Uptime"
@@ -627,3 +627,9 @@ other = "Feature"
[Template]
other = "Template"
[Stat]
other = "Stat"
[DisableSwitchTemplateInFrontend]
other = "Disable Switch Template in Frontend"
+7 -1
View File
@@ -203,7 +203,7 @@ other = "Agregar Servidor"
other = "Editar Grupo de Servidores en Lote"
[BatchDeleteServer]
other = "Eliminar Servidores en Lote""
other = "Eliminar Servidores en Lote"
[InputServerGroupName]
other = "Ingrese Nombre del Grupo de Servidores"
@@ -627,3 +627,9 @@ other = "Característica"
[Template]
other = "Plantilla"
[Stat]
other = "Stat"
[DisableSwitchTemplateInFrontend]
other = "Deshabilitar Cambio de Plantilla en Frontend"
+6
View File
@@ -627,3 +627,9 @@ other = "功能"
[Template]
other = "主题"
[Stat]
other = "信息"
[DisableSwitchTemplateInFrontend]
other = "禁止前台切换模板"
+6
View File
@@ -627,3 +627,9 @@ other = "功能"
[Template]
other = "主題"
[Stat]
other = "信息"
[DisableSwitchTemplateInFrontend]
other = "禁止前台切換主題"
+20 -4
View File
@@ -3,6 +3,7 @@
.ui.container {
width:95% !important;
font-size: 90% !important;
max-width: 1300px !important;
}
}
@media only screen and (max-width:767px) {
@@ -86,21 +87,35 @@ td {
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 {
padding-top: 0 !important;
padding-bottom: 0 !important;
height:3.3rem !important;
height:2.3rem !important;
}
.status.cards .wide.column:nth-child(1) {
margin-top:2rem !important;
margin-top:1.2rem !important;
}
.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{
width: 78%!important;
padding-left:0;
}
@@ -124,7 +139,7 @@ td {
.closePopup{
color:rgb(10, 148, 242) !important;
position: absolute;
top: 10px;
top: 7px;
right: 10px;
cursor: pointer;
z-index: 9999;
@@ -194,6 +209,7 @@ td {
}
.nezha-secondary-font {
height: 1em;
color: rgb(10, 148, 242) !important;
}
+61
View File
@@ -0,0 +1,61 @@
const mixinsVue = {
delimiters: ['@#', '#@'],
data: {
preferredTemplate: null,
isMobile: false,
adaptedTemplates: [
{ key: 'default', name: 'Default', icon: 'th large' },
{ key: 'angel-kanade', name: 'AngelKanade', icon: 'square' },
{ key: 'server-status', name: 'SeverStatus', icon: 'list' }
]
},
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);
}
});
}
}
}
+8 -3
View File
@@ -5,14 +5,19 @@ const mixinsVue = {
isSystemTheme: false,
showGroup: false,
showGoTop: false,
preferredTemplate: 'default',
isMobile: false
preferredTemplate: null,
isMobile: false,
adaptedTemplates: [
{ key: 'default', name: 'Default', icon: 'th large' },
{ key: 'angel-kanade', name: 'AngelKanade', icon: 'square' },
{ key: 'server-status', name: 'SeverStatus', icon: 'list' }
]
},
created() {
this.isMobile = this.checkIsMobile();
this.initTheme();
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);
},
destroyed() {
+11 -2
View File
@@ -70,18 +70,24 @@
<input type="text" name="IPChangeNotificationTag" placeholder="" value="{{.Conf.IPChangeNotificationTag}}">
</div>
<div class="field">
<div class="ui nf-ssl checkbox ip-change">
<div class="ui checkbox ip-change">
<input name="EnableIPChangeNotification" type="checkbox" tabindex="0" class="hidden">
<label>{{tr "Enable"}}</label>
</div>
</div>
</div>
<div class="field">
<div class="ui nf-ssl checkbox plain-ip">
<div class="ui checkbox plain-ip">
<input name="EnablePlainIPInNotification" type="checkbox" tabindex="0" class="hidden">
<label>{{tr "NotificationMessagesDoNotHideIP"}}</label>
</div>
</div>
<div class="field">
<div class="ui checkbox disable-switch-template">
<input name="DisableSwitchTemplateInFrontend" type="checkbox" tabindex="0" class="hidden">
<label>{{tr "DisableSwitchTemplateInFrontend"}}</label>
</div>
</div>
<button class="ui button" type="submit">{{tr "Save"}}</button>
</form>
</div>
@@ -128,5 +134,8 @@
{{if .Conf.EnablePlainIPInNotification}}
$('.checkbox.plain-ip').checkbox('set checked')
{{ end }}
{{if .Conf.DisableSwitchTemplateInFrontend }}
$('.checkbox.disable-switch-template').checkbox('set checked')
{{ end }}
</script>
{{end}}
+2 -1
View File
@@ -1,4 +1,5 @@
{{define "theme-angel-kanade/footer"}}
</div>
<div class="ui inverted vertical footer segment">
<div class="ui center aligned is-size-7 container">
<b>&copy; <a style="color: white;" href="/">{{.Conf.Site.Brand}}</a></b> |
@@ -13,12 +14,12 @@
<script src="/static/semantic-ui-alerts.min.js"></script>
<script src="https://lf6-cdn-tos.bytecdntp.com/cdn/expire-1-y/vue/2.6.14/vue.min.js"></script>
<script src="/static/main.js?v20240224"></script>
<script src="/static/theme-default/js/mixin.js?v20240302"></script>
<script>
(function () {
updateLang({{.LANG }});
})();
</script>
</body>
</html>
{{end}}
+101 -100
View File
@@ -3,118 +3,116 @@
{{if ts .CustomCode}} {{.CustomCode|safe}} {{end}}
{{template "theme-angel-kanade/menu" .}}
<div class="nb-container">
<div class="ui container">
<div id="app">
<div class="ui styled fluid accordion" v-for="group in groups">
<div class="active title">
<i class="dropdown icon"></i>
@#(group.Tag!==''?group.Tag:'{{tr "Default"}}')#@
</div>
<div class="active content">
<div class="ui four stackable status cards">
<div v-for="server in group.data" :id="server.ID" class="ui card">
<div class="content" v-if="server.Host" style="margin-top: 10px; padding-bottom: 5px">
<div class="header">
<img v-if="server.Host.CountryCode" style="border-radius: 50%;box-shadow:-1px -1px 2px #eee, 1px 1px 2px #000;width:19px;" :src="'https://lf6-cdn-tos.bytecdntp.com/cdn/expire-1-y/flag-icon-css/4.1.5/flags/1x1/'+server.Host.CountryCode + '.svg'" alt="国家"/>&nbsp;<i v-if='server.Host.Platform == "darwin"'
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>
@#server.Name + (server.live?'':'[{{tr "Offline"}}]')#@
<i class="nezha-secondary-font info circle icon" style="height: 28px"></i>
<div class="ui content popup" style="margin-bottom: 0">
{{tr "Platform"}}: @#server.Host.Platform#@-@#server.Host.PlatformVersion#@
[<span
v-if="server.Host.Virtualization">@#server.Host.Virtualization#@:</span>@#server.Host.Arch#@]<br />
CPU: @#server.Host.CPU#@<br />
{{tr "DiskUsed"}}:
@#formatByteSize(server.State.DiskUsed)#@/@#formatByteSize(server.Host.DiskTotal)#@<br />
{{tr "MemUsed"}}:
@#formatByteSize(server.State.MemUsed)#@/@#formatByteSize(server.Host.MemTotal)#@<br />
{{tr "SwapUsed"}}:
@#formatByteSize(server.State.SwapUsed)#@/@#formatByteSize(server.Host.SwapTotal)#@<br />
{{tr "NetTransfer"}}: <i
class="arrow alternate circle down outline icon"></i>@#formatByteSize(server.State.NetInTransfer)#@<i
class="arrow alternate circle up outline icon"></i>@#formatByteSize(server.State.NetOutTransfer)#@<br />
{{tr "Load"}}: @# toFixed2(server.State.Load1) #@/@# toFixed2(server.State.Load5) #@/@#
toFixed2(server.State.Load15) #@<br />
{{tr "ProcessCount"}}: @# server.State.ProcessCount #@<br />
{{tr "ConnCount"}}: TCP @# server.State.TcpConnCount #@ / UDP @# server.State.UdpConnCount #@<br />
{{tr "BootTime"}}: @# formatTimestamp(server.Host.BootTime) #@<br />
{{tr "LastActive"}}: @# new Date(server.LastActive).toLocaleString() #@<br />
{{tr "Version"}}: @#server.Host.Version#@<br />
</div>
<div class="ui divider" style="margin-bottom: 5px"></div>
<div class="ui container">
<div class="ui styled fluid accordion" v-for="group in groups">
<div class="active title">
<i class="dropdown icon"></i>
@#(group.Tag!==''?group.Tag:'{{tr "Default"}}')#@
</div>
<div class="active content">
<div class="ui four stackable status cards">
<div v-for="server in group.data" :id="server.ID" class="ui card">
<div class="content" v-if="server.Host" style="margin-top: 10px; padding-bottom: 5px">
<div class="header">
<img v-if="server.Host.CountryCode" style="border-radius: 50%;box-shadow:-1px -1px 2px #eee, 1px 1px 2px #000;width:19px;" :src="'https://lf6-cdn-tos.bytecdntp.com/cdn/expire-1-y/flag-icon-css/4.1.5/flags/1x1/'+server.Host.CountryCode + '.svg'" alt="国家"/>&nbsp;<i v-if='server.Host.Platform == "darwin"'
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>
@#server.Name + (server.live?'':'[{{tr "Offline"}}]')#@
<i class="nezha-secondary-font info circle icon" style="height: 28px"></i>
<div class="ui content popup" style="margin-bottom: 0">
{{tr "Platform"}}: @#server.Host.Platform#@-@#server.Host.PlatformVersion#@
[<span
v-if="server.Host.Virtualization">@#server.Host.Virtualization#@:</span>@#server.Host.Arch#@]<br />
CPU: @#server.Host.CPU#@<br />
{{tr "DiskUsed"}}:
@#formatByteSize(server.State.DiskUsed)#@/@#formatByteSize(server.Host.DiskTotal)#@<br />
{{tr "MemUsed"}}:
@#formatByteSize(server.State.MemUsed)#@/@#formatByteSize(server.Host.MemTotal)#@<br />
{{tr "SwapUsed"}}:
@#formatByteSize(server.State.SwapUsed)#@/@#formatByteSize(server.Host.SwapTotal)#@<br />
{{tr "NetTransfer"}}: <i
class="arrow alternate circle down outline icon"></i>@#formatByteSize(server.State.NetInTransfer)#@<i
class="arrow alternate circle up outline icon"></i>@#formatByteSize(server.State.NetOutTransfer)#@<br />
{{tr "Load"}}: @# toFixed2(server.State.Load1) #@/@# toFixed2(server.State.Load5) #@/@#
toFixed2(server.State.Load15) #@<br />
{{tr "ProcessCount"}}: @# server.State.ProcessCount #@<br />
{{tr "ConnCount"}}: TCP @# server.State.TcpConnCount #@ / UDP @# server.State.UdpConnCount #@<br />
{{tr "BootTime"}}: @# formatTimestamp(server.Host.BootTime) #@<br />
{{tr "LastActive"}}: @# new Date(server.LastActive).toLocaleString() #@<br />
{{tr "Version"}}: @#server.Host.Version#@<br />
</div>
<div class="description">
<div class="ui grid">
<div class="three wide column">CPU</div>
<div class="thirteen wide column">
<div :class="formatPercent(server.live,server.State.CPU, 100).class">
<div class="bar" :style="formatPercent(server.live,server.State.CPU, 100).style">
<small>@#formatPercent(server.live,server.State.CPU,100).percent#@%</small>
</div>
<div class="ui divider" style="margin-bottom: 5px"></div>
</div>
<div class="description">
<div class="ui grid">
<div class="three wide column">CPU</div>
<div class="thirteen wide column">
<div :class="formatPercent(server.live,server.State.CPU, 100).class">
<div class="bar" :style="formatPercent(server.live,server.State.CPU, 100).style">
<small>@#formatPercent(server.live,server.State.CPU,100).percent#@%</small>
</div>
</div>
<div class="three wide column">{{tr "MemUsed"}}</div>
<div class="thirteen wide column">
<div :class="formatPercent(server.live,server.State.MemUsed, server.Host.MemTotal).class">
<div class="bar"
:style="formatPercent(server.live,server.State.MemUsed, server.Host.MemTotal).style">
<small>@#parseInt(server.State?server.State.MemUsed/server.Host.MemTotal*100:0)#@%</small>
</div>
</div>
<div class="three wide column">{{tr "MemUsed"}}</div>
<div class="thirteen wide column">
<div :class="formatPercent(server.live,server.State.MemUsed, server.Host.MemTotal).class">
<div class="bar"
:style="formatPercent(server.live,server.State.MemUsed, server.Host.MemTotal).style">
<small>@#parseInt(server.State?server.State.MemUsed/server.Host.MemTotal*100:0)#@%</small>
</div>
</div>
<div class="three wide column">{{tr "SwapUsed"}}</div>
<div class="thirteen wide column">
<div :class="formatPercent(server.live,server.State.SwapUsed, server.Host.SwapTotal).class">
<div class="bar"
:style="formatPercent(server.live,server.State.SwapUsed, server.Host.SwapTotal).style">
<small>@#parseInt(server.State?server.State.SwapUsed/server.Host.SwapTotal*100:0)#@%</small>
</div>
</div>
<div class="three wide column">{{tr "SwapUsed"}}</div>
<div class="thirteen wide column">
<div :class="formatPercent(server.live,server.State.SwapUsed, server.Host.SwapTotal).class">
<div class="bar"
:style="formatPercent(server.live,server.State.SwapUsed, server.Host.SwapTotal).style">
<small>@#parseInt(server.State?server.State.SwapUsed/server.Host.SwapTotal*100:0)#@%</small>
</div>
</div>
<div class="three wide column">{{tr "NetSpeed"}}</div>
<div class="thirteen wide column">
<i class="arrow alternate circle down outline icon"></i>
@#formatByteSize(server.State.NetInSpeed)#@/s
<i class="arrow alternate circle up outline icon"></i>
@#formatByteSize(server.State.NetOutSpeed)#@/s
</div>
<div class="three wide column">{{tr "NetTransfer"}}</div>
<div class="thirteen wide column">
<i class="arrow circle down icon"></i>
@#formatByteSize(server.State.NetInTransfer)#@
&nbsp;
<i class="arrow circle up icon"></i>
@#formatByteSize(server.State.NetOutTransfer)#@
</div>
<div class="three wide column">{{tr "DiskUsed"}}</div>
<div class="thirteen wide column">
<div :class="formatPercent(server.live,server.State.DiskUsed, server.Host.DiskTotal).class">
<div class="bar"
:style="formatPercent(server.live,server.State.DiskUsed, server.Host.DiskTotal).style">
<small>@#parseInt(server.State?server.State.DiskUsed/server.Host.DiskTotal*100:0)#@%</small>
</div>
</div>
<div class="three wide column">{{tr "NetSpeed"}}</div>
<div class="thirteen wide column">
<i class="arrow alternate circle down outline icon"></i>
@#formatByteSize(server.State.NetInSpeed)#@/s
<i class="arrow alternate circle up outline icon"></i>
@#formatByteSize(server.State.NetOutSpeed)#@/s
</div>
<div class="three wide column">{{tr "NetTransfer"}}</div>
<div class="thirteen wide column">
<i class="arrow circle down icon"></i>
@#formatByteSize(server.State.NetInTransfer)#@
&nbsp;
<i class="arrow circle up icon"></i>
@#formatByteSize(server.State.NetOutTransfer)#@
</div>
<div class="three wide column">{{tr "DiskUsed"}}</div>
<div class="thirteen wide column">
<div :class="formatPercent(server.live,server.State.DiskUsed, server.Host.DiskTotal).class">
<div class="bar"
:style="formatPercent(server.live,server.State.DiskUsed, server.Host.DiskTotal).style">
<small>@#parseInt(server.State?server.State.DiskUsed/server.Host.DiskTotal*100:0)#@%</small>
</div>
</div>
<div class="three wide column">{{tr "Info"}}</div>
<div class="thirteen wide column">
<i class="bi bi-cpu-fill" style="font-size: 1.1rem; color: #4a86e8;"></i> @#getCoreAndGHz(server.Host.CPU)#@
&nbsp;
<i class="bi bi-memory" style="font-size: 1.1rem; color: #00ac0d;"></i> @#getByteToGB(server.Host.MemTotal)#@
&nbsp;
<i class="bi bi-hdd-rack-fill" style="font-size: 1.1rem; color: #980000"></i> @#getByteToGB(server.Host.DiskTotal)#@
</div>
<div class="three wide column">{{tr "Uptime"}}</div>
<div class="thirteen wide column">
<i class="clock icon"></i>@#secondToDate(server.State.Uptime)#@
</div>
</div>
<div class="three wide column">{{tr "Info"}}</div>
<div class="thirteen wide column">
<i class="bi bi-cpu-fill" style="font-size: 1.1rem; color: #4a86e8;"></i> @#getCoreAndGHz(server.Host.CPU)#@
&nbsp;
<i class="bi bi-memory" style="font-size: 1.1rem; color: #00ac0d;"></i> @#getByteToGB(server.Host.MemTotal)#@
&nbsp;
<i class="bi bi-hdd-rack-fill" style="font-size: 1.1rem; color: #980000"></i> @#getByteToGB(server.Host.DiskTotal)#@
</div>
<div class="three wide column">{{tr "Uptime"}}</div>
<div class="thirteen wide column">
<i class="clock icon"></i>@#secondToDate(server.State.Uptime)#@
</div>
</div>
</div>
<div class="content" v-else>
<p>@#server.Name#@</p>
<p>{{tr "ServerIsOffline"}}</p>
</div>
</div>
<div class="content" v-else>
<p>@#server.Name#@</p>
<p>{{tr "ServerIsOffline"}}</p>
</div>
</div>
</div>
@@ -129,10 +127,13 @@
el: '#app',
delimiters: ['@#', '#@'],
data: {
defaultTemplate: {{.Conf.Site.Theme}},
templates: {{.Themes}},
data: initData,
groups: [],
cache: [],
},
mixins: [mixinsVue],
created() {
this.group()
},
+12
View File
@@ -1,4 +1,5 @@
{{define "theme-angel-kanade/menu"}}
<div id="app">
<div class="ui large top fixed menu nb-menu">
<div class="ui container">
<a class="item" href="/">
@@ -15,6 +16,17 @@
{{else}}
<a class='item{{if eq .MatchedPath "/"}} active{{end}}' href="/"><i class="home icon"></i>{{tr "Home"}}</a>
<a class='item{{if eq .MatchedPath "/service"}} active{{end}}' href="/service"><i class="rss icon"></i>{{tr "Services"}}</a>
{{ if not .Conf.DisableSwitchTemplateInFrontend }}
<div class="item ui simple dropdown">
<div class="text"><i class="bi bi-incognito icon" style="margin-right:3px;"></i>{{tr "Template" }}<i class="dropdown icon" style="margin-right:0px;"></i></div>
<div class="menu">
<a v-for="(item, index) in adaptedTemplates" :key="index" @click="toggleTemplate(item.key)" class="item">
<i :class="item.icon + ' icon'"></i>@#item.name#@
<i class="check icon" v-if="preferredTemplate === item.key"></i>
</a>
</div>
</div>
{{ end }}
{{end}}
<div class="right menu">
<div class="item">
+11 -1
View File
@@ -76,10 +76,20 @@
{{end}}
</tbody>
</table>
{{end}}
</div>
</div>
</div>
{{template "theme-angel-kanade/footer" .}}
<script>
new Vue({
el: '#app',
delimiters: ['@#', '#@'],
data: {
defaultTemplate: {{.Conf.Site.Theme}},
templates: {{.Themes}}
},
mixins: [mixinsVue]
})
</script>
{{end}}
+1 -1
View File
@@ -109,7 +109,7 @@
<script>
const monitorInfo = JSON.parse('{{.MonitorInfos}}');
const initData = JSON.parse('{{.Servers}}').servers;
let MaxTCPPingValue = {{.MaxTCPPingValue}};
let MaxTCPPingValue = {{.Conf.MaxTCPPingValue}};
if (MaxTCPPingValue == null) {
MaxTCPPingValue = 300;
}
+11
View File
@@ -7,6 +7,17 @@
{{.Version}}</small>
</div>
</div>
{{ if not .Conf.DisableSwitchTemplateInFrontend }}
<script>
function showSwitchTemplate(list, currentBackendTheme) {
console.log(list, currentBackendTheme);
console.log("currentBackendTheme:",currentBackendTheme);
}
showSwitchTemplate({{ .Themes }}, {{ .Conf.Site.Theme }})
</script>
{{ end }}
<script>
</script>
</body>
</html>
{{end}}
+2 -2
View File
@@ -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/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/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" />
<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="/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]/dist/echarts.min.js"></script>
<script src="/static/theme-default/js/mixin.js?v20240225"></script>
<script src="/static/theme-default/js/mixin.js?v20240302"></script>
</head>
<body>
{{end}}
+22 -16
View File
@@ -19,7 +19,7 @@
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>
@#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;">
<i class="closePopup window close icon" @click="closePopup(server.ID)"></i>
{{tr "Platform"}}: @#server.Host.Platform#@-@#server.Host.PlatformVersion#@
@@ -90,7 +90,7 @@
<i class="arrow alternate circle up outline icon"></i>
@#formatByteSize(server.State.NetOutSpeed)#@/s
</div>
<div class="three wide column">流量</div>
<div class="three wide column">{{tr "NetTransfer"}}</div>
<div class="thirteen wide column">
<i class="arrow circle down icon"></i>
@#formatByteSize(server.State.NetInTransfer)#@
@@ -98,7 +98,7 @@
<i class="arrow circle up icon"></i>
@#formatByteSize(server.State.NetOutTransfer)#@
</div>
<div class="three wide column">信息</div>
<div class="three wide column">{{tr "Stat"}}</div>
<div class="thirteen wide column">
<i class="bi bi-cpu-fill" style="font-size: 1.1rem; color: #4a86e8;"></i> @#getCoreAndGHz(server.Host.CPU)#@
&nbsp;
@@ -138,17 +138,17 @@
delimiters: ['@#', '#@'],
data: {
page: 'index',
defaultTemplate: {{.Conf.Site.Theme}},
templates: {{.Themes}},
data: [],
servers: [],
groups: [],
cache: [],
chartDataList: [],
activePopup: null,
},
mixins: [mixinsVue],
created() {
this.data = JSON.parse('{{.Servers}}').servers;
this.servers = JSON.parse('{{.Servers}}').servers;
this.group()
},
methods: {
@@ -185,11 +185,12 @@
},
renderCharts(id) {
if (!this.chartDataList[id - 1]) return;
const MaxTCPPingValue = {{.MaxTCPPingValue}} ? {{.MaxTCPPingValue}} : 300;
const MaxTCPPingValue = {{.Conf.MaxTCPPingValue}} ? {{.Conf.MaxTCPPingValue}} : 300;
const isMobile = this.checkIsMobile();
const fontSize = isMobile ? 10 : 9;
const itemGap = isMobile ? 5 : 2;
const itemWidth = isMobile ? 20 : 15;
const itemGap = isMobile ? 6 : 6;
const itemWidth = isMobile ? 10 : 10;
const itemHeight = isMobile ? 10 : 10;
const gridLeft = 25;
const gridRight = 12;
const fontColor = "rgba(0, 0, 0, 0.68)";
@@ -206,20 +207,23 @@
const xAxisData = chartData[0].created_at.map(time => new Date(time).toLocaleString());
const seriesData = chartData.map(item => {
let loss = 0;
const data = item.avg_delay.map((avgDelay, index) => {
if (avgDelay > 0.9 * MaxTCPPingValue) {
const data = item.avg_delay.map((avgDelay, index) => {
if(avgDelay > 0 && avgDelay < MaxTCPPingValue){
loss += avgDelay > 0.9 * MaxTCPPingValue ? 1 : 0;
return [item.created_at[index], avgDelay];
}else{
loss += 1;
}
return [new Date(item.created_at[index]).toLocaleString(), avgDelay];
});
const lossRate = ((loss / item.created_at.length) * 100).toFixed(1);
item.monitor_name = item.monitor_name + " " + lossRate + "%";
item.monitor_name = item.monitor_name.includes("%") ? item.monitor_name : `${item.monitor_name} ${lossRate}%`;
return {
name: item.monitor_name,
type: 'line',
smooth: true,
symbol: 'none',
data: data
data: data,
connectNulls: true
};
});
const option = {
@@ -236,6 +240,7 @@
}
},
legend: {
icon: 'rect',
data: chartData.map(item => item.monitor_name),
show: true,
textStyle: {
@@ -249,6 +254,7 @@
bottom: 0,
itemGap: itemGap,
itemWidth: itemWidth,
itemHeight: itemHeight,
padding: [5,0,5,0]
},
xAxis: {
@@ -282,7 +288,7 @@
color: fontColor
},
grid: {
top: '40',
top: '30',
bottom: '20',
left: gridLeft,
right: gridRight
@@ -357,7 +363,7 @@
return '';
},
group() {
this.groups = groupingData(this.data, "Tag")
this.groups = groupingData(this.servers, "Tag")
},
formatPercent(live, used, total) {
const percent = live ? (parseInt(used / total * 100) || 0) : -1
+10 -7
View File
@@ -19,14 +19,17 @@
<a href="/service" class='item'><i class="rss icon"></i>{{tr "Services" }}</a>
<a href="/network" class="item"><i class="bi bi-hdd-network icon"></i>{{tr "NetworkSpiter"}}</a>
</template>
<div class="item ui simple dropdown">
<div class="text"><i class="bi bi-incognito icon" style="margin-right:3px;"></i>{{tr "Template" }}<i class="dropdown icon" style="margin-right:0px;"></i></div>
<div class="menu">
<a v-for="(value, key) in templates" :key="key" @click="toggleTemplate(key)" class="item"><i class="th large icon"></i>@#value#@
<i class="check icon" v-if="preferredTemplate === key"></i>
</a>
{{ if not .Conf.DisableSwitchTemplateInFrontend }}
<div class="item ui simple dropdown">
<div class="text"><i class="bi bi-incognito icon" style="margin-right:3px;"></i>{{tr "Template" }}<i class="dropdown icon" style="margin-right:0px;"></i></div>
<div class="menu">
<a v-for="(item, index) in adaptedTemplates" :key="index" @click="toggleTemplate(item.key)" class="item">
<i :class="item.icon + ' icon'"></i>@#item.name#@
<i class="check icon" v-if="preferredTemplate === item.key"></i>
</a>
</div>
</div>
</div>
{{ end }}
{{if .Admin}}
<div class="item right item-right ui simple dropdown">
<div class="text">
+2 -1
View File
@@ -27,7 +27,7 @@
<script>
const monitorInfo = JSON.parse('{{.MonitorInfos}}');
const initData = JSON.parse('{{.Servers}}').servers;
let MaxTCPPingValue = {{.MaxTCPPingValue}};
let MaxTCPPingValue = {{.Conf.MaxTCPPingValue}};
if (MaxTCPPingValue == null) {
MaxTCPPingValue = 1000;
}
@@ -36,6 +36,7 @@
delimiters: ['@#', '#@'],
data: {
page: 'network',
defaultTemplate: {{.Conf.Site.Theme}},
templates: {{.Themes}},
servers: initData,
option: {
+1
View File
@@ -87,6 +87,7 @@
delimiters: ['@#', '#@'],
data: {
page: 'service',
defaultTemplate: {{.Conf.Site.Theme}},
templates: {{.Themes}}
},
mixins: [mixinsVue]
+1 -1
View File
@@ -54,7 +54,7 @@
<script>
const monitorInfo = JSON.parse('{{.MonitorInfos}}');
const initData = JSON.parse('{{.Servers}}').servers;
let MaxTCPPingValue = {{.MaxTCPPingValue}};
let MaxTCPPingValue = {{.Conf.MaxTCPPingValue}};
if (MaxTCPPingValue == null) {
MaxTCPPingValue = 1000;
}
+14 -12
View File
@@ -21,7 +21,7 @@
<a data-toggle="dropdown"><i class="bi bi-gear-wide-connected" style="position:relative;top:1px;margin-right:3px;font-size:1.1rem;"></i>{{tr "Feature" }}<b class="caret"></b></a>
<ul class="dropdown-menu" style="min-width:100px;">
<li><a href="/service"><i class="rss icon"></i>{{tr "Services" }}</a></li>
<li><a href="/network"><i class="bi bi-hdd-network icon"></i>{{tr "NetworkSpiter"}}</a></li>
<li><a href="/network"><i class="bi bi-hdd-network icon"></i>{{tr "NetworkSpiter"}}</a></li>
</ul>
</li>
</template>
@@ -29,17 +29,19 @@
<li><a href="/service"><i class="rss icon"></i>{{tr "Services" }}</a></li>
<li><a href="/network"><i class="bi bi-hdd-network icon"></i>{{tr "NetworkSpiter"}}</a></li>
</template>
<li class="dropdown">
<a data-toggle="dropdown"><i class="bi bi-incognito" style="position:relative;top:1px;margin-right:3px;font-size:1.2rem;vertical-align:top;"></i>{{tr "Template" }}<b class="caret"></b></a>
<ul class="dropdown-menu">
<li v-for="(value, key) in templates" :key="key">
<a @click="toggleTemplate(key)">
<i class="list icon"></i>@#value#@
<i class="check icon" v-if="preferredTemplate === key"></i>
</a>
</li>
</ul>
</li>
{{ if not .Conf.DisableSwitchTemplateInFrontend }}
<li class="dropdown">
<a data-toggle="dropdown"><i class="bi bi-incognito" style="position:relative;top:1px;margin-right:3px;font-size:1.2rem;vertical-align:top;"></i>{{tr "Template" }}<b class="caret"></b></a>
<ul class="dropdown-menu">
<li v-for="(item, index) in adaptedTemplates" :key="index">
<a @click="toggleTemplate(item.key)">
<i :class="item.icon + ' icon'" style="font-size:1em"></i>@#item.name#@
<i class="check icon" v-if="preferredTemplate === item.key"></i>
</a>
</li>
</ul>
</li>
{{ end }}
</ul>
<ul class="nav navbar-nav navbar-right">
{{if .Admin}}
+1 -1
View File
@@ -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]"></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?v20240302"></script>
</head>
<body>
{{end}}
+45 -24
View File
@@ -22,22 +22,37 @@
delimiters: ['@#', '#@'],
data: {
page: 'index',
defaultTemplate: {{.Conf.Site.Theme}},
templates: {{.Themes}},
cache: [],
servers: [],
nodesTag: [],
nodesNoTag: [],
chartDataList: []
chartDataList: [],
ws: null
},
mixins: [mixinsVue],
created() {
initData = JSON.parse('{{.Servers}}').servers;
this.servers = JSON.parse('{{.Servers}}').servers;
if(this.showGroup) {
this.nodesTag = this.groupingData(this.handleNodes(initData),"Tag");
this.nodesTag = this.groupingData(this.handleNodes(this.servers),"Tag");
} else {
this.nodesNoTag = this.handleNodes(initData);
this.nodesNoTag = this.handleNodes(this.servers);
}
},
mounted() {
// 初始化时建立WebSocket连接
this.connect();
// 监听页面可见性变化
document.addEventListener("visibilitychange", () => {
if (document.visibilityState === "visible") {
setTimeout(() => {
if (document.hasFocus()) {
this.connect();
}
}, 200);
}
});
},
methods: {
isWindowsPlatform(str) {
@@ -155,12 +170,18 @@
return parseFloat((bytes / Math.pow(1024, i)).toFixed(2)) + sizes[i];
},
connect() {
const wsProtocol = window.location.protocol === "https:" ? "wss" : "ws"
const ws = new WebSocket(wsProtocol + '://' + window.location.host + '/ws');
ws.onopen = function () {
// 如果已经存在 WebSocket 连接并且处于开启状态,则不重复建立连接
if (this.ws && this.ws.readyState === WebSocket.OPEN) {
console.log('Closing old WebSocket connection...');
this.ws.close();
}
const wsProtocol = window.location.protocol === "https:" ? "wss" : "ws";
this.ws = new WebSocket(wsProtocol + '://' + window.location.host + '/ws');
this.ws.onopen = function () {
console.log("Connection open ...")
}
ws.onmessage = (evt) => {
this.ws.onmessage = (evt) => {
let jsonData = evt.data
const data = JSON.parse(jsonData)
for (let i = 0; i < data.servers?.length; i++) {
@@ -178,13 +199,13 @@
this.nodesNoTag = this.handleNodes(data.servers);
}
}
ws.onclose = () => {
setTimeout(function () {
this.connect()
this.ws.onclose = () => {
setTimeout(() => {
this.connect();
}, 5000);
}
ws.onerror = function () {
ws.close()
};
this.ws.onerror = () => {
this.ws.close()
}
},
handleNodes(servers) {
@@ -259,7 +280,7 @@
if ( chartboxShow === '0' ) return;
}
// 定义图表参数值
const MaxTCPPingValue = {{.MaxTCPPingValue}} ? {{.MaxTCPPingValue}} : 300;
const MaxTCPPingValue = {{.Conf.MaxTCPPingValue}} ? {{.Conf.MaxTCPPingValue}} : 300;
const isMobile = this.checkIsMobile();
const fontSize = isMobile ? 10 : 14;
const gridLeft = isMobile ? 25 : 36;
@@ -284,22 +305,23 @@
const xAxisData = chartData[0].created_at.map(time => new Date(time).toLocaleString());
const seriesData = chartData.map(item => {
let loss = 0;
const data = item.avg_delay.map((avgDelay, index) => {
if (avgDelay > 0.9 * MaxTCPPingValue) {
const data = item.avg_delay.map((avgDelay, index) => {
if(avgDelay > 0 && avgDelay < MaxTCPPingValue){
loss += avgDelay > 0.9 * MaxTCPPingValue ? 1 : 0;
return [item.created_at[index], avgDelay];
}else{
loss += 1;
}
return [new Date(item.created_at[index]).toLocaleString(), avgDelay];
});
const lossRate = ((loss / item.created_at.length) * 100).toFixed(1);
if (!item.monitor_name.includes("%")) {
item.monitor_name = item.monitor_name + " " + lossRate + "%";
}
item.monitor_name = item.monitor_name.includes("%") ? item.monitor_name : `${item.monitor_name} ${lossRate}%`;
return {
name: item.monitor_name,
type: 'line',
smooth: true,
symbol: 'none',
data: data
data: data,
connectNulls: true
};
});
const option = {
@@ -366,8 +388,7 @@
chart.setOption(option);
},
reloadCharts() { // 重新加载所有图表
const data = JSON.parse('{{.Servers}}').servers;
data.forEach(node => {
this.servers.forEach(node => {
const id = node.ID;
const chartData = this.chartDataList[id - 1];
if (chartData) {
+2 -1
View File
@@ -21,7 +21,7 @@
<script>
const monitorInfo = JSON.parse('{{.MonitorInfos}}');
const initData = JSON.parse('{{.Servers}}').servers;
let MaxTCPPingValue = {{.MaxTCPPingValue}};
let MaxTCPPingValue = {{.Conf.MaxTCPPingValue}};
if (MaxTCPPingValue == null) {
MaxTCPPingValue = 1000;
}
@@ -30,6 +30,7 @@
delimiters: ['@#', '#@'],
data: {
page: 'network',
defaultTemplate: {{.Conf.Site.Theme}},
templates: {{.Themes}},
servers: initData,
option: {
+1
View File
@@ -78,6 +78,7 @@
delimiters: ['@#', '#@'],
data: {
page: 'service',
defaultTemplate: {{.Conf.Site.Theme}},
templates: {{.Themes}},
servicesTag: [],
servicesNoTag: [],
+2 -2
View File
@@ -38,8 +38,8 @@ $download = "https://github.com/$agentrepo/releases/download/$agenttag/$file"
$nssmdownload="https://github.com/$nssmrepo/releases/download/$nssmtag/nssm.zip"
Write-Host "Location:$region,connect directly!" -BackgroundColor DarkRed -ForegroundColor Green
}else{
$download = "https://kkgithub.com/$agentrepo/releases/download/$agenttag/$file"
$nssmdownload="https://kkgithub.com/$nssmrepo/releases/download/$nssmtag/nssm.zip"
$download = "https://dn-dao-github-mirror.daocloud.io/$agentrepo/releases/download/$agenttag/$file"
$nssmdownload="https://dn-dao-github-mirror.daocloud.io/$nssmrepo/releases/download/$nssmtag/nssm.zip"
Write-Host "Location:CN,use mirror address" -BackgroundColor DarkRed -ForegroundColor Green
}
echo $download
+2 -2
View File
@@ -14,7 +14,7 @@ NZ_AGENT_SERVICE="/etc/systemd/system/nezha-agent.service"
NZ_AGENT_SERVICERC="/etc/init.d/nezha-agent"
NZ_DASHBOARD_SERVICE="/etc/systemd/system/nezha-dashboard.service"
NZ_DASHBOARD_SERVICERC="/etc/init.d/nezha-dashboard"
NZ_VERSION="v0.15.8"
NZ_VERSION="v0.15.9"
red='\033[0;31m'
green='\033[0;32m'
@@ -94,7 +94,7 @@ pre_check() {
Docker_IMG="ghcr.io\/naiba\/nezha-dashboard"
else
GITHUB_RAW_URL="gitee.com/naibahq/nezha/raw/master"
GITHUB_URL="kkgithub.com"
GITHUB_URL="dn-dao-github-mirror.daocloud.io"
Get_Docker_URL="get.docker.com"
Get_Docker_Argu=" -s docker --mirror Aliyun"
Docker_IMG="registry.cn-shanghai.aliyuncs.com\/naibahq\/nezha-dashboard"
+2 -2
View File
@@ -14,7 +14,7 @@ NZ_AGENT_SERVICE="/etc/systemd/system/nezha-agent.service"
NZ_AGENT_SERVICERC="/etc/init.d/nezha-agent"
NZ_DASHBOARD_SERVICE="/etc/systemd/system/nezha-dashboard.service"
NZ_DASHBOARD_SERVICERC="/etc/init.d/nezha-dashboard"
NZ_VERSION="v0.15.8"
NZ_VERSION="v0.15.9"
red='\033[0;31m'
green='\033[0;32m'
@@ -93,7 +93,7 @@ pre_check() {
Docker_IMG="ghcr.io\/naiba\/nezha-dashboard"
else
GITHUB_RAW_URL="gitee.com/naibahq/nezha/raw/master"
GITHUB_URL="kkgithub.com"
GITHUB_URL="dn-dao-github-mirror.daocloud.io"
Get_Docker_URL="get.docker.com"
Get_Docker_Argu=" -s docker --mirror Aliyun"
Docker_IMG="registry.cn-shanghai.aliyuncs.com\/naibahq\/nezha-dashboard"
+2 -1
View File
@@ -3,6 +3,7 @@ package rpc
import (
"context"
"fmt"
"github.com/naiba/nezha/pkg/ddns"
"github.com/naiba/nezha/pkg/utils"
"log"
"time"
@@ -125,7 +126,7 @@ func (s *NezhaHandler) ReportSystemInfo(c context.Context, r *pb.Host) (*pb.Rece
if err == nil && serverDomain != "" {
ipv4, ipv6, _ := utils.SplitIPAddr(host.IP)
maxRetries := int(singleton.Conf.DDNS.MaxRetries)
config := &singleton.DDNSDomainConfig{
config := &ddns.DomainConfig{
EnableIPv4: true,
EnableIpv6: true,
FullDomain: serverDomain,
+7 -267
View File
@@ -1,273 +1,13 @@
package singleton
import (
"bytes"
"encoding/json"
"errors"
"fmt"
"io"
ddns2 "github.com/naiba/nezha/pkg/ddns"
"log"
"net/http"
"strings"
)
type DDNSDomainConfig struct {
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 {
func RetryableUpdateDomain(provider ddns2.Provider, config *ddns2.DomainConfig, maxRetries int) bool {
if nil == config {
return false
}
@@ -282,21 +22,21 @@ func RetryableUpdateDomain(provider DDNSProvider, config *DDNSDomainConfig, maxR
return false
}
func GetDDNSProviderFromString(provider string) (DDNSProvider, error) {
func GetDDNSProviderFromString(provider string) (ddns2.Provider, error) {
switch provider {
case "webhook":
return DDNSProviderWebHook{
return ddns2.ProviderWebHook{
URL: Conf.DDNS.WebhookURL,
RequestMethod: Conf.DDNS.WebhookMethod,
RequestBody: Conf.DDNS.WebhookRequestBody,
RequestHeader: Conf.DDNS.WebhookHeaders,
}, nil
case "dummy":
return DDNSProviderDummy{}, nil
return ddns2.ProviderDummy{}, nil
case "cloudflare":
return DDNSProviderCloudflare{
return ddns2.ProviderCloudflare{
Secret: Conf.DDNS.AccessSecret,
}, nil
}
return DDNSProviderDummy{}, errors.New(fmt.Sprintf("无法找到配置的DDNS提供者%s", Conf.DDNS.Provider))
return ddns2.ProviderDummy{}, errors.New(fmt.Sprintf("无法找到配置的DDNS提供者%s", Conf.DDNS.Provider))
}