Compare commits

..
28 Commits
Author SHA1 Message Date
UUBulb 3d6edd602c ci: fix permission when packaging; move installer to another repository (#470)
* installer: fix permission & move to another repository

* update gitee url

* update resource

* ci: fix permission
2024-11-06 16:44:31 +08:00
UUBulb 2590815a6c ci: fix arm64 path parsing (#468) 2024-11-05 22:54:02 +08:00
nap0o c20dfdc7a3 improve: status-server主题日常优化 (#467)
* improve: status-server主题日常优化
1.首页vps套餐流量增加一种类型max(in,out)类型,感谢@hi2global
2.network页修复逻辑缺陷
3.一些其他小优化

演示地址 https://dev.nezha.pp.ua/

* 修正 NetTransfer 展示方式
2024-11-04 23:11:24 +08:00
nap0o 96c3fd433f feat: status-server主题增加套餐信息展示 (#464)
* feat: status-server主题增加套餐信息展示
1. 首页通过在后台配置PublicNote字段,实现agent套餐信息展示
2. 一些其他小优化

* 1.未获取agent国家时,默认彩虹旗修改为联合国旗
2024-11-01 21:28:14 +08:00
github-actions[bot] 45b0f1edfc update contributors[no ci] 2024-10-30 16:15:58 +00:00
Moraxyc 61fdcd2a9e fix(cli): display version before init app (#463) 2024-10-31 00:15:41 +08:00
UUBulb a474b8484b ddns: replace tencentcloud implemention (#462) 2024-10-29 21:39:45 +08:00
UUBulb 67b003a27d installer: fix download service scripts (#449) 2024-10-22 13:02:54 +08:00
UUBulb 99aeda7939 installer: fix format (#448) 2024-10-22 10:25:31 +08:00
UUBulb d5744d0c73 fix installer (#442)
* fix installer

* version
2024-10-20 00:33:28 +08:00
UUBulb e7837d8eaf installer: fix some style issues (#441)
* installer: fix some style issues

* translate

* translate

* replace cf-ns
2024-10-19 22:03:00 +08:00
UUBulb aa81fc02ab fix: add custom theme key (#440) 2024-10-19 11:59:31 +08:00
UUBulb f6531a52bd ddns: remove ipv6 nameservers, support custom nameservers (#439) 2024-10-18 22:06:01 +08:00
UUBulb c58c4f866a do not check description file for theme-custom (#437)
* do not check description file for theme-custom

* deprecation note
2024-10-18 12:26:59 +08:00
UUBulb fc9f1b6bcc fix(ddns): handle second-level domain correctly (#438) 2024-10-18 12:26:43 +08:00
UUBulb be7b6e9c5e fix(ddns): add missing field WebhookRequestType (#436)
* fix(ddns): add missing field WebhookRequestType

* add missing placeholders
2024-10-17 23:35:28 +08:00
UUBulbandnap0o a503f0cf40 ddns: store configuation in database (#435)
* ddns: store configuation in database

Co-authored-by: nap0o <[email protected]>

* feat: split domain with soa lookup

* switch to libdns interface

* ddns: add unit test

* ddns: skip TestSplitDomainSOA on ci

network is not steady

* fix error handling

* fix error handling

---------

Co-authored-by: nap0o <[email protected]>
2024-10-17 21:03:03 +08:00
UUBulb 0b7f43b149 fix dashboard custom theme, expose HideForGuest for api (#434)
* fix: dashboard custom theme

* api: expose HideForGuest
2024-10-10 13:38:09 +08:00
UUBulb 55f5c89c1c feat: description file for custom theme; use gjson (#433)
* feat: description file for custom theme; use gjson

* fix gosec

* remove outdated stuff
2024-10-10 00:08:16 +08:00
nap0o 937696c26d improve: status-server主题日常优化 (#432)
* improve: status-server主题日常优化
1. 首页账单信息:价格设置成“-1”前台展示为”PAYG“,自动续期(autoRenewal)功能增加支持付费周期yr,mo;
2. 当未获取到国旗图标时,默认彩虹旗;
3. 当未获取到os(操作系统)信息时,默认linux
4. 一些其他优化

* 修改表达不准确的语言文件
2024-10-08 11:39:44 +08:00
github-actions[bot] 08aaea8244 update contributors[no ci] 2024-10-06 12:54:13 +00:00
UUBulbandnaiba b886f70e48 chore: l10n: improve translation (#431)
* l10n: improve translation

* refine

---------

Co-authored-by: naiba <[email protected]>
2024-10-06 20:53:55 +08:00
zhucaidanandnaiba 12c6b8a6f6 Update en-US.toml (#429)
* Update en-US.toml

Optimize translation

* refine

---------

Co-authored-by: naiba <[email protected]>
2024-10-06 11:41:19 +08:00
UUBulb ebcc8a6efb installer: fix docker-compose detection logic (#428) 2024-10-06 09:35:12 +08:00
UUBulb c4d8b63d32 installer: fix docker-compose detection (#427) 2024-10-05 23:43:16 +08:00
nap0o 318da135b9 improve: status-server主题agent账单信息展示 (#425)
1. 支持价格和期限单独设置
2. 账单信息附加到group分组控制
3. 一些其他优化
2024-10-04 23:35:44 +08:00
github-actions[bot] da74bf4749 update contributors[no ci] 2024-10-04 15:33:58 +00:00
lyjandnaiba 3250283ef9 fix: 允许更新默认分组 (#426)
* fix: 允许更新默认分组

* improve: check null

---------

Co-authored-by: naiba <[email protected]>
2024-10-04 23:33:40 +08:00
61 changed files with 2217 additions and 1375 deletions
+13 -13
View File
@@ -17,11 +17,11 @@ jobs:
goarch: s390x
- goos: linux
goarch: arm64
name: Build artifacts
runs-on: ubuntu-latest
container:
image: goreleaser/goreleaser-cross:v1.21
image: goreleaser/goreleaser-cross:v1.23
env:
GOOS: ${{ matrix.goos }}
GOARCH: ${{ matrix.goarch }}
@@ -43,13 +43,13 @@ jobs:
- name: Set up Go
uses: actions/setup-go@v5
with:
go-version: "1.21.x"
go-version: "1.23.x"
- name: Build
uses: goreleaser/goreleaser-action@v6
with:
distribution: goreleaser
version: '~> v2'
version: "~> v2"
args: build --single-target --clean --skip=validate
- name: Upload artifacts
@@ -72,12 +72,12 @@ jobs:
- name: Archive and compress binaries
run: |
for file in assets/*/*/*; do
if [ -f "$file" ]; then
chmod +x "$file"
export fileWithoutExt=${file%.*}
zip -jr "$fileWithoutExt.zip" "$file"
fi
find assets/*/*/* -type f | while read -r file; do
chmod +x $file
dir=$(dirname "$file")
filename=$(basename "$file")
fileWithoutExt="${filename%.*}"
zip -jr "$dir/$fileWithoutExt.zip" "$file"
done
- name: Release
@@ -113,7 +113,7 @@ jobs:
name: Release Docker images
steps:
- uses: actions/checkout@v4
- name: Download artifacts
uses: actions/download-artifact@v4
with:
@@ -130,7 +130,7 @@ jobs:
export TAG_NAME=$(echo ${GITHUB_REF#refs/tags/})
echo "tag=$TAG_NAME" >> $GITHUB_OUTPUT
id: extract_branch
- name: Log into GHCR
uses: docker/login-action@master
with:
+1
View File
@@ -6,6 +6,7 @@ on:
jobs:
sync-release-to-gitee:
runs-on: ubuntu-latest
timeout-minutes: 30
env:
GITEE_TOKEN: ${{ secrets.GITEE_TOKEN }}
steps:
+1 -1
View File
@@ -29,7 +29,7 @@ jobs:
- uses: actions/setup-go@v5
with:
go-version: "1.21.x"
go-version: "1.23.x"
- name: Unit test
run: |
+4 -2
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.19.0-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">
<br>
<br>
<p>:trollface: <b>Nezha Monitoring: Self-hostable, lightweight, servers and websites monitoring and O&M tool.</b></p>
@@ -59,6 +59,7 @@ You can change the dashboard language in the settings page (`/setting`) after th
<a href="https://github.com/1ridic" title="1ridic"><img src="https://avatars.githubusercontent.com/u/88495501?v=4" width="50;" alt="1ridic"/></a>
<a href="https://github.com/Mmx233" title="Mmx"><img src="https://avatars.githubusercontent.com/u/36563672?v=4" width="50;" alt="Mmx"/></a>
<a href="https://github.com/rootmelo92118" title="rootmelo92118"><img src="https://avatars.githubusercontent.com/u/32770959?v=4" width="50;" alt="rootmelo92118"/></a>
<a href="https://github.com/zhucaidan" title="zhucaidan"><img src="https://avatars.githubusercontent.com/u/47970938?v=4" width="50;" alt="zhucaidan"/></a>
<a href="https://github.com/iilemon" title="Sean"><img src="https://avatars.githubusercontent.com/u/33201711?v=4" width="50;" alt="Sean"/></a>
<a href="https://github.com/fscarmen" title="fscarmen"><img src="https://avatars.githubusercontent.com/u/62703343?v=4" width="50;" alt="fscarmen"/></a>
<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>
@@ -75,14 +76,15 @@ You can change the dashboard language in the settings page (`/setting`) after th
<a href="https://github.com/hiDandelion" title="hiDandelion"><img src="https://avatars.githubusercontent.com/u/77157418?v=4" width="50;" alt="hiDandelion"/></a>
<a href="https://github.com/yuanweize" title="I"><img src="https://avatars.githubusercontent.com/u/30067203?v=4" width="50;" alt="I"/></a>
<a href="https://github.com/lvyaoting" title="lvyaoting"><img src="https://avatars.githubusercontent.com/u/166296299?v=4" width="50;" alt="lvyaoting"/></a>
<a href="https://github.com/lyj0309" title="lyj"><img src="https://avatars.githubusercontent.com/u/50474995?v=4" width="50;" alt="lyj"/></a>
<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/ysicing" title="缘生"><img src="https://avatars.githubusercontent.com/u/8605565?v=4" width="50;" alt="缘生"/></a>
<a href="https://github.com/zhucaidan" title="zhucaidan"><img src="https://avatars.githubusercontent.com/u/47970938?v=4" width="50;" alt="zhucaidan"/></a>
<a href="https://github.com/arkylin" title="凌"><img src="https://avatars.githubusercontent.com/u/35104502?v=4" width="50;" alt="凌"/></a>
<a href="https://github.com/colour93" title="玖叁"><img src="https://avatars.githubusercontent.com/u/64313711?v=4" width="50;" alt="玖叁"/></a>
<a href="https://github.com/IamTaoChen" title="Tao Chen"><img src="https://avatars.githubusercontent.com/u/42793494?v=4" width="50;" alt="Tao Chen"/></a>
<a href="https://github.com/Septrum101" title="Spetrum"><img src="https://avatars.githubusercontent.com/u/11692994?v=4" width="50;" alt="Spetrum"/></a>
<a href="https://github.com/dreamingsleeping" title="Nanjing Hopefun Network Technology Co. Ltd."><img src="https://avatars.githubusercontent.com/u/13828658?v=4" width="50;" alt="Nanjing Hopefun Network Technology Co. Ltd."/></a>
<a href="https://github.com/Moraxyc" title="Moraxyc"><img src="https://avatars.githubusercontent.com/u/69713071?v=4" width="50;" alt="Moraxyc"/></a>
<a href="https://github.com/silver-ymz" title="Mingzhuo Yin"><img src="https://avatars.githubusercontent.com/u/78400701?v=4" width="50;" alt="Mingzhuo Yin"/></a>
<a href="https://github.com/MartijnLindeman" title="Martijn Lindeman"><img src="https://avatars.githubusercontent.com/u/78365708?v=4" width="50;" alt="Martijn Lindeman"/></a>
<a href="https://github.com/xrgzs" title="MadDogOwner"><img src="https://avatars.githubusercontent.com/u/26499123?v=4" width="50;" alt="MadDogOwner"/></a>
+60 -28
View File
@@ -3,10 +3,10 @@ package controller
import (
"fmt"
"html/template"
"io/fs"
"log"
"net/http"
"os"
"path/filepath"
"strconv"
"strings"
"time"
@@ -34,28 +34,16 @@ func ServeWeb(port uint) *http.Server {
pprof.Register(r)
}
r.Use(natGateway)
if os.Getenv("NZ_LOCAL_TEMPLATE") == "true" {
r.SetFuncMap(funcMap)
r.Use(mygin.RecordPath)
r.Static("/static", "resource/static")
r.LoadHTMLGlob("resource/template/**/*.html")
} else {
tmpl := template.New("").Funcs(funcMap)
var err error
tmpl, err = tmpl.ParseFS(resource.TemplateFS, "template/**/*.html")
if err != nil {
panic(err)
}
tmpl = loadThirdPartyTemplates(tmpl)
r.SetHTMLTemplate(tmpl)
r.Use(mygin.RecordPath)
staticFs, err := fs.Sub(resource.StaticFS, "static")
if err != nil {
panic(err)
}
r.StaticFS("/static", http.FS(staticFs))
tmpl := template.New("").Funcs(funcMap)
var err error
tmpl, err = tmpl.ParseFS(resource.TemplateFS, "template/**/*.html")
if err != nil {
panic(err)
}
r.Static("/static-custom", "resource/static/custom")
tmpl = loadThirdPartyTemplates(tmpl)
r.SetHTMLTemplate(tmpl)
r.Use(mygin.RecordPath)
r.StaticFS("/static", http.FS(resource.StaticFS))
routers(r)
page404 := func(c *gin.Context) {
mygin.ShowErrorPage(c, mygin.ErrInfo{
@@ -96,7 +84,7 @@ func routers(r *gin.Engine) {
}
func loadThirdPartyTemplates(tmpl *template.Template) *template.Template {
var ret = tmpl
ret := tmpl
themes, err := os.ReadDir("resource/template")
if err != nil {
log.Printf("NEZHA>> Error reading themes folder: %v", err)
@@ -106,17 +94,61 @@ func loadThirdPartyTemplates(tmpl *template.Template) *template.Template {
if !theme.IsDir() {
continue
}
// load templates
t, err := ret.ParseGlob(fmt.Sprintf("resource/template/%s/*.html", theme.Name()))
if err != nil {
log.Printf("NEZHA>> Error parsing templates %s error: %v", theme.Name(), err)
themeDir := theme.Name()
if themeDir == "theme-custom" {
// for backward compatibility
// note: will remove this in future versions
ret = loadTemplates(ret, themeDir)
continue
}
ret = t
if strings.HasPrefix(themeDir, "dashboard-") {
// load dashboard templates, ignore desc file
ret = loadTemplates(ret, themeDir)
continue
}
if !strings.HasPrefix(themeDir, "theme-") {
log.Printf("NEZHA>> Invalid theme name: %s", themeDir)
continue
}
descPath := filepath.Join("resource", "template", themeDir, "theme.json")
desc, err := os.ReadFile(filepath.Clean(descPath))
if err != nil {
log.Printf("NEZHA>> Error opening %s config: %v", themeDir, err)
continue
}
themeName, err := utils.GjsonGet(desc, "name")
if err != nil {
log.Printf("NEZHA>> Error opening %s config: not a valid description file", theme.Name())
continue
}
// load templates
ret = loadTemplates(ret, themeDir)
themeKey := strings.TrimPrefix(themeDir, "theme-")
model.Themes[themeKey] = themeName.String()
}
return ret
}
func loadTemplates(tmpl *template.Template, themeDir string) *template.Template {
// load templates
templatePath := filepath.Join("resource", "template", themeDir, "*.html")
t, err := tmpl.ParseGlob(templatePath)
if err != nil {
log.Printf("NEZHA>> Error parsing templates %s: %v", themeDir, err)
return tmpl
}
return t
}
var funcMap = template.FuncMap{
"tr": func(id string, dataAndCount ...interface{}) string {
conf := i18n.LocalizeConfig{
+130 -25
View File
@@ -12,6 +12,7 @@ import (
"github.com/gin-gonic/gin"
"github.com/jinzhu/copier"
"golang.org/x/net/idna"
"gorm.io/gorm"
"github.com/naiba/nezha/model"
@@ -38,6 +39,7 @@ func (ma *memberAPI) serve() {
mr.GET("/search-server", ma.searchServer)
mr.GET("/search-tasks", ma.searchTask)
mr.GET("/search-ddns", ma.searchDDNS)
mr.POST("/server", ma.addOrEditServer)
mr.POST("/monitor", ma.addOrEditMonitor)
mr.POST("/cron", ma.addOrEditCron)
@@ -46,6 +48,7 @@ func (ma *memberAPI) serve() {
mr.POST("/batch-update-server-group", ma.batchUpdateServerGroup)
mr.POST("/batch-delete-server", ma.batchDeleteServer)
mr.POST("/notification", ma.addOrEditNotification)
mr.POST("/ddns", ma.addOrEditDDNS)
mr.POST("/nat", ma.addOrEditNAT)
mr.POST("/alert-rule", ma.addOrEditAlertRule)
mr.POST("/setting", ma.updateSetting)
@@ -211,6 +214,11 @@ func (ma *memberAPI) delete(c *gin.Context) {
if err == nil {
singleton.OnDeleteNotification(id)
}
case "ddns":
err = singleton.DB.Unscoped().Delete(&model.DDNSProfile{}, "id = ?", id).Error
if err == nil {
singleton.OnDDNSUpdate()
}
case "nat":
err = singleton.DB.Unscoped().Delete(&model.NAT{}, "id = ?", id).Error
if err == nil {
@@ -299,20 +307,38 @@ func (ma *memberAPI) searchTask(c *gin.Context) {
})
}
func (ma *memberAPI) searchDDNS(c *gin.Context) {
var ddns []model.DDNSProfile
likeWord := "%" + c.Query("word") + "%"
singleton.DB.Select("id,name").Where("id = ? OR name LIKE ?",
c.Query("word"), likeWord).Find(&ddns)
var resp []searchResult
for i := 0; i < len(ddns); i++ {
resp = append(resp, searchResult{
Value: ddns[i].ID,
Name: ddns[i].Name,
Text: ddns[i].Name,
})
}
c.JSON(http.StatusOK, map[string]interface{}{
"success": true,
"results": resp,
})
}
type serverForm struct {
ID uint64
Name string `binding:"required"`
DisplayIndex int
Secret string
Tag string
Note string
PublicNote string
HideForGuest string
EnableDDNS string
EnableIPv4 string
EnableIpv6 string
DDNSDomain string
DDNSProfile string
ID uint64
Name string `binding:"required"`
DisplayIndex int
Secret string
Tag string
Note string
PublicNote string
HideForGuest string
EnableDDNS string
DDNSProfilesRaw string
}
func (ma *memberAPI) addOrEditServer(c *gin.Context) {
@@ -330,18 +356,18 @@ func (ma *memberAPI) addOrEditServer(c *gin.Context) {
s.PublicNote = sf.PublicNote
s.HideForGuest = sf.HideForGuest == "on"
s.EnableDDNS = sf.EnableDDNS == "on"
s.EnableIPv4 = sf.EnableIPv4 == "on"
s.EnableIpv6 = sf.EnableIpv6 == "on"
s.DDNSDomain = sf.DDNSDomain
s.DDNSProfile = sf.DDNSProfile
if s.ID == 0 {
s.Secret, err = utils.GenerateRandomString(18)
if err == nil {
err = singleton.DB.Create(&s).Error
s.DDNSProfilesRaw = sf.DDNSProfilesRaw
err = utils.Json.Unmarshal([]byte(sf.DDNSProfilesRaw), &s.DDNSProfiles)
if err == nil {
if s.ID == 0 {
s.Secret, err = utils.GenerateRandomString(18)
if err == nil {
err = singleton.DB.Create(&s).Error
}
} else {
isEdit = true
err = singleton.DB.Save(&s).Error
}
} else {
isEdit = true
err = singleton.DB.Save(&s).Error
}
}
if err != nil {
@@ -743,6 +769,81 @@ func (ma *memberAPI) addOrEditNotification(c *gin.Context) {
})
}
type ddnsForm struct {
ID uint64
MaxRetries uint64
EnableIPv4 string
EnableIPv6 string
Name string
Provider uint8
DomainsRaw string
AccessID string
AccessSecret string
WebhookURL string
WebhookMethod uint8
WebhookRequestType uint8
WebhookRequestBody string
WebhookHeaders string
}
func (ma *memberAPI) addOrEditDDNS(c *gin.Context) {
var df ddnsForm
var p model.DDNSProfile
err := c.ShouldBindJSON(&df)
if err == nil {
if df.MaxRetries < 1 || df.MaxRetries > 10 {
err = errors.New("重试次数必须为大于 1 且不超过 10 的整数")
}
}
if err == nil {
p.Name = df.Name
p.ID = df.ID
enableIPv4 := df.EnableIPv4 == "on"
enableIPv6 := df.EnableIPv6 == "on"
p.EnableIPv4 = &enableIPv4
p.EnableIPv6 = &enableIPv6
p.MaxRetries = df.MaxRetries
p.Provider = df.Provider
p.DomainsRaw = df.DomainsRaw
p.Domains = strings.Split(p.DomainsRaw, ",")
p.AccessID = df.AccessID
p.AccessSecret = df.AccessSecret
p.WebhookURL = df.WebhookURL
p.WebhookMethod = df.WebhookMethod
p.WebhookRequestType = df.WebhookRequestType
p.WebhookRequestBody = df.WebhookRequestBody
p.WebhookHeaders = df.WebhookHeaders
for n, domain := range p.Domains {
// IDN to ASCII
domainValid, domainErr := idna.Lookup.ToASCII(domain)
if domainErr != nil {
err = fmt.Errorf("域名 %s 解析错误: %v", domain, domainErr)
break
}
p.Domains[n] = domainValid
}
}
if err == nil {
if p.ID == 0 {
err = singleton.DB.Create(&p).Error
} else {
err = singleton.DB.Save(&p).Error
}
}
if err != nil {
c.JSON(http.StatusOK, model.Response{
Code: http.StatusBadRequest,
Message: fmt.Sprintf("请求错误:%s", err),
})
return
}
singleton.OnDDNSUpdate()
c.JSON(http.StatusOK, model.Response{
Code: http.StatusOK,
})
}
type natForm struct {
ID uint64
Name string
@@ -911,6 +1012,7 @@ type settingForm struct {
DashboardTheme string
CustomCode string
CustomCodeDashboard string
CustomNameservers string
ViewPassword string
IgnoredIPNotification string
IPChangeNotificationTag string // IP变更提醒的通知组
@@ -940,7 +1042,7 @@ func (ma *memberAPI) updateSetting(c *gin.Context) {
return
}
if _, yes := model.Themes[sf.DashboardTheme]; !yes {
if _, yes := model.DashboardThemes[sf.DashboardTheme]; !yes {
c.JSON(http.StatusOK, model.Response{
Code: http.StatusBadRequest,
Message: fmt.Sprintf("后台主题不存在:%s", sf.DashboardTheme),
@@ -977,6 +1079,7 @@ func (ma *memberAPI) updateSetting(c *gin.Context) {
singleton.Conf.Site.DashboardTheme = sf.DashboardTheme
singleton.Conf.Site.CustomCode = sf.CustomCode
singleton.Conf.Site.CustomCodeDashboard = sf.CustomCodeDashboard
singleton.Conf.DNSServers = sf.CustomNameservers
singleton.Conf.Site.ViewPassword = sf.ViewPassword
singleton.Conf.Oauth2.Admin = sf.Admin
// 保证NotificationTag不为空
@@ -992,6 +1095,8 @@ func (ma *memberAPI) updateSetting(c *gin.Context) {
}
// 更新系统语言
singleton.InitLocalizer()
// 更新DNS服务器
singleton.OnNameserverUpdate()
c.JSON(http.StatusOK, model.Response{
Code: http.StatusOK,
})
+12
View File
@@ -27,6 +27,7 @@ func (mp *memberPage) serve() {
mr.GET("/monitor", mp.monitor)
mr.GET("/cron", mp.cron)
mr.GET("/notification", mp.notification)
mr.GET("/ddns", mp.ddns)
mr.GET("/nat", mp.nat)
mr.GET("/setting", mp.setting)
mr.GET("/api", mp.api)
@@ -78,6 +79,17 @@ func (mp *memberPage) notification(c *gin.Context) {
}))
}
func (mp *memberPage) ddns(c *gin.Context) {
var data []model.DDNSProfile
singleton.DB.Find(&data)
c.HTML(http.StatusOK, "dashboard-"+singleton.Conf.Site.DashboardTheme+"/ddns", mygin.CommonEnvironment(c, gin.H{
"Title": singleton.Localizer.MustLocalize(&i18n.LocalizeConfig{MessageID: "DDNS"}),
"DDNS": data,
"ProviderMap": model.ProviderMap,
"ProviderList": model.ProviderList,
}))
}
func (mp *memberPage) nat(c *gin.Context) {
var data []model.NAT
singleton.DB.Find(&data)
+9 -8
View File
@@ -1,6 +1,7 @@
package main
import (
"os"
"context"
"fmt"
"log"
@@ -32,13 +33,6 @@ func init() {
flag.StringVarP(&dashboardCliParam.ConfigFile, "config", "c", "data/config.yaml", "配置文件路径")
flag.StringVar(&dashboardCliParam.DatebaseLocation, "db", "data/sqlite.db", "Sqlite3数据库文件路径")
flag.Parse()
// 初始化 dao 包
singleton.InitConfigFromPath(dashboardCliParam.ConfigFile)
singleton.InitTimezoneAndCache()
singleton.InitDBFromPath(dashboardCliParam.DatebaseLocation)
singleton.InitLocalizer()
initSystem()
}
func initSystem() {
@@ -59,9 +53,16 @@ func initSystem() {
func main() {
if dashboardCliParam.Version {
fmt.Println(singleton.Version)
return
os.Exit(0)
}
// 初始化 dao 包
singleton.InitConfigFromPath(dashboardCliParam.ConfigFile)
singleton.InitTimezoneAndCache()
singleton.InitDBFromPath(dashboardCliParam.DatebaseLocation)
singleton.InitLocalizer()
initSystem()
// TODO 使用 cmux 在同一端口服务 HTTP 和 gRPC
singleton.CleanMonitorHistory()
go rpc.ServeRPC(singleton.Conf.GRPCPort)
+10
View File
@@ -14,6 +14,10 @@ require (
github.com/hashicorp/go-uuid v1.0.3
github.com/jinzhu/copier v0.4.0
github.com/json-iterator/go v1.1.12
github.com/libdns/cloudflare v0.1.1
github.com/libdns/libdns v0.2.2
github.com/miekg/dns v1.1.62
github.com/nezhahq/libdns-tencentcloud v0.0.0-20241029120103-889957240fff
github.com/nicksnyder/go-i18n/v2 v2.4.0
github.com/ory/graceful v0.1.3
github.com/oschwald/maxminddb-golang v1.13.1
@@ -21,6 +25,7 @@ require (
github.com/robfig/cron/v3 v3.0.1
github.com/spf13/pflag v1.0.5
github.com/spf13/viper v1.18.2
github.com/tidwall/gjson v1.18.0
github.com/xanzy/go-gitlab v0.103.0
golang.org/x/crypto v0.25.0
golang.org/x/net v0.27.0
@@ -70,14 +75,19 @@ require (
github.com/spf13/afero v1.11.0 // indirect
github.com/spf13/cast v1.6.0 // indirect
github.com/subosito/gotenv v1.6.0 // indirect
github.com/tidwall/match v1.1.1 // indirect
github.com/tidwall/pretty v1.2.0 // indirect
github.com/tidwall/sjson v1.2.5 // indirect
github.com/twitchyliquid64/golang-asm v0.15.1 // indirect
github.com/ugorji/go/codec v1.2.11 // indirect
go.uber.org/atomic v1.9.0 // indirect
go.uber.org/multierr v1.9.0 // indirect
golang.org/x/arch v0.3.0 // indirect
golang.org/x/exp v0.0.0-20230905200255-921286631fa9 // indirect
golang.org/x/mod v0.18.0 // indirect
golang.org/x/sys v0.22.0 // indirect
golang.org/x/time v0.5.0 // indirect
golang.org/x/tools v0.22.0 // indirect
google.golang.org/genproto/googleapis/rpc v0.0.0-20240227224415-6ceb2ff114de // indirect
gopkg.in/ini.v1 v1.67.0 // indirect
gopkg.in/yaml.v3 v3.0.1 // indirect
+21 -2
View File
@@ -107,6 +107,10 @@ github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE=
github.com/leodido/go-urn v1.2.1/go.mod h1:zt4jvISO2HfUBqxjfIshjdMTYS56ZS/qv49ictyFfxY=
github.com/leodido/go-urn v1.2.4 h1:XlAE/cm/ms7TE/VMVoduSpNBoyc2dOxHs5MZSwAN63Q=
github.com/leodido/go-urn v1.2.4/go.mod h1:7ZrI8mTSeBSHl/UaRyKQW1qZeMgak41ANeCNaVckg+4=
github.com/libdns/cloudflare v0.1.1 h1:FVPfWwP8zZCqj268LZjmkDleXlHPlFU9KC4OJ3yn054=
github.com/libdns/cloudflare v0.1.1/go.mod h1:9VK91idpOjg6v7/WbjkEW49bSCxj00ALesIFDhJ8PBU=
github.com/libdns/libdns v0.2.2 h1:O6ws7bAfRPaBsgAYt8MDe2HcNBGC29hkZ9MX2eUSX3s=
github.com/libdns/libdns v0.2.2/go.mod h1:4Bj9+5CQiNMVGf87wjX4CY3HQJypUHRuLvlsfsZqLWQ=
github.com/magiconair/properties v1.8.7 h1:IeQXZAiQcpL9mgcAe1Nu6cX9LLw6ExEHKjN0VQdvPDY=
github.com/magiconair/properties v1.8.7/go.mod h1:Dhd985XPs7jluiymwWYZ0G4Z61jb3vdS329zhj2hYo0=
github.com/mattn/go-colorable v0.1.13 h1:fFA4WZxdEF4tXPZVKMLwD8oUnCTTo08duU7wxecdEvA=
@@ -116,6 +120,8 @@ github.com/mattn/go-isatty v0.0.20 h1:xfD0iDuEKnDkl03q4limB+vH+GxLEtL/jb4xVJSWWE
github.com/mattn/go-isatty v0.0.20/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y=
github.com/mattn/go-sqlite3 v1.14.17 h1:mCRHCLDUBXgpKAqIKsaAaAsrAlbkeomtRFKXh2L6YIM=
github.com/mattn/go-sqlite3 v1.14.17/go.mod h1:2eHXhiwb8IkHr+BDWZGa96P6+rkvnG63S2DGjv9HUNg=
github.com/miekg/dns v1.1.62 h1:cN8OuEF1/x5Rq6Np+h1epln8OiyPWV+lROx9LxcGgIQ=
github.com/miekg/dns v1.1.62/go.mod h1:mvDlcItzm+br7MToIKqkglaGhlFMHJ9DTNNWONWXbNQ=
github.com/mitchellh/mapstructure v1.5.0 h1:jeMsZIYE/09sWLaz43PL7Gy6RuMjD2eJVyuac5Z2hdY=
github.com/mitchellh/mapstructure v1.5.0/go.mod h1:bFUtVrKA4DC2yAKiSyO/QUcy7e+RRV2QTWOzhPopBRo=
github.com/modern-go/concurrent v0.0.0-20180228061459-e0a39a4cb421/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q=
@@ -123,6 +129,8 @@ github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd h1:TRLaZ9cD/w
github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q=
github.com/modern-go/reflect2 v1.0.2 h1:xBagoLtFs94CBntxluKeaWgTMpvLxC4ur3nMaC9Gz0M=
github.com/modern-go/reflect2 v1.0.2/go.mod h1:yWuevngMOJpCy52FWWMvUC8ws7m/LJsjYzDa0/r8luk=
github.com/nezhahq/libdns-tencentcloud v0.0.0-20241029120103-889957240fff h1:3WDsbsg3dmsRENYLUPGPTkEcWWmTvk41i+rM91AIIbY=
github.com/nezhahq/libdns-tencentcloud v0.0.0-20241029120103-889957240fff/go.mod h1:k+cDtTbUY+UV56Aqv4Ahmc9bWmhpga9JJXZlAIPKBEc=
github.com/nicksnyder/go-i18n/v2 v2.4.0 h1:3IcvPOAvnCKwNm0TB0dLDTuawWEj+ax/RERNC+diLMM=
github.com/nicksnyder/go-i18n/v2 v2.4.0/go.mod h1:nxYSZE9M0bf3Y70gPQjN9ha7XNHX7gMc814+6wVyEI4=
github.com/onsi/ginkgo/v2 v2.17.1 h1:V++EzdbhI4ZV4ev0UTIj0PzhzOcReJFyJaLjtSF55M8=
@@ -180,6 +188,15 @@ github.com/stretchr/testify v1.9.0 h1:HtqpIVDClZ4nwg75+f6Lvsy/wHu+3BoSGCbBAcpTsT
github.com/stretchr/testify v1.9.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY=
github.com/subosito/gotenv v1.6.0 h1:9NlTDc1FTs4qu0DDq7AEtTPNw6SVm7uBMsUCUjABIf8=
github.com/subosito/gotenv v1.6.0/go.mod h1:Dk4QP5c2W3ibzajGcXpNraDfq2IrhjMIvMSWPKKo0FU=
github.com/tidwall/gjson v1.14.2/go.mod h1:/wbyibRr2FHMks5tjHJ5F8dMZh3AcwJEMf5vlfC0lxk=
github.com/tidwall/gjson v1.18.0 h1:FIDeeyB800efLX89e5a8Y0BNH+LOngJyGrIWxG2FKQY=
github.com/tidwall/gjson v1.18.0/go.mod h1:/wbyibRr2FHMks5tjHJ5F8dMZh3AcwJEMf5vlfC0lxk=
github.com/tidwall/match v1.1.1 h1:+Ho715JplO36QYgwN9PGYNhgZvoUSc9X2c80KVTi+GA=
github.com/tidwall/match v1.1.1/go.mod h1:eRSPERbgtNPcGhD8UCthc6PmLEQXEWd3PRB5JTxsfmM=
github.com/tidwall/pretty v1.2.0 h1:RWIZEg2iJ8/g6fDDYzMpobmaoGh5OLl4AXtGUGPcqCs=
github.com/tidwall/pretty v1.2.0/go.mod h1:ITEVvHYasfjBbM0u2Pg8T2nJnzm8xPwvNhhsoaGGjNU=
github.com/tidwall/sjson v1.2.5 h1:kLy8mja+1c9jlljvWTlSazM7cKDRfJuR/bOJhcY5NcY=
github.com/tidwall/sjson v1.2.5/go.mod h1:Fvgq9kS/6ociJEDnK0Fk1cpYF4FIW6ZF7LAe+6jwd28=
github.com/twitchyliquid64/golang-asm v0.15.1 h1:SU5vSMR7hnwNxj24w34ZyCi/FmDZTkS4MhqMhdFk5YI=
github.com/twitchyliquid64/golang-asm v0.15.1/go.mod h1:a1lVb/DtPvCB8fslRZhAngC2+aY1QWCk3Cedj/Gdt08=
github.com/ugorji/go v1.2.7/go.mod h1:nF9osbDWLy6bDVv/Rtoh6QgnvNDpmCalQV5urGCCS6M=
@@ -203,6 +220,8 @@ golang.org/x/crypto v0.25.0 h1:ypSNr+bnYL2YhwoMt2zPxHFmbAN1KZs/njMG3hxUp30=
golang.org/x/crypto v0.25.0/go.mod h1:T+wALwcMOSE0kXgUAnPAHqTLW+XHgcELELW8VaDgm/M=
golang.org/x/exp v0.0.0-20230905200255-921286631fa9 h1:GoHiUyI/Tp2nVkLI2mCxVkOjsbSXD66ic0XW0js0R9g=
golang.org/x/exp v0.0.0-20230905200255-921286631fa9/go.mod h1:S2oDrQGGwySpoQPVqRShND87VCbxmc6bL1Yd2oYrm6k=
golang.org/x/mod v0.18.0 h1:5+9lSbEzPSdWkH32vYPBwEpX8KwDbM52Ud9xBUvNlb0=
golang.org/x/mod v0.18.0/go.mod h1:hTbmBsO62+eylJbnUtE2MGJUyE7QWk4xUqPFrRgJ+7c=
golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg=
golang.org/x/net v0.0.0-20210226172049-e18ecbb05110/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg=
golang.org/x/net v0.27.0 h1:5K3Njcw06/l2y9vpGCSdcxWOYHOUk3dVNGDXN+FvAys=
@@ -232,8 +251,8 @@ golang.org/x/text v0.16.0/go.mod h1:GhwF1Be+LQoKShO3cGOHzqOgRrGaYc9AvblQOmPVHnI=
golang.org/x/time v0.5.0 h1:o7cqy6amK/52YcAKIPlM3a+Fpj35zvRj2TP+e1xFSfk=
golang.org/x/time v0.5.0/go.mod h1:3BpzKBy/shNhVucY/MWOyx10tF3SFh9QdLuxbVysPQM=
golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
golang.org/x/tools v0.21.1-0.20240508182429-e35e4ccd0d2d h1:vU5i/LfpvrRCpgM/VPfJLg5KjxD3E+hfT1SH+d9zLwg=
golang.org/x/tools v0.21.1-0.20240508182429-e35e4ccd0d2d/go.mod h1:aiJjzUbINMkxbQROHiO6hDPo2LHcIPhhQsa9DLh0yGk=
golang.org/x/tools v0.22.0 h1:gqSGLZqv+AI9lIQzniJ0nZDRG5GBPsSi+DRNHWNz6yA=
golang.org/x/tools v0.22.0/go.mod h1:aCwcsjqvq7Yqt6TNyX7QMU2enbQ/Gt0bo6krSeEri+c=
golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
google.golang.org/genproto/googleapis/rpc v0.0.0-20240227224415-6ceb2ff114de h1:cZGRis4/ot9uVm639a+rHCUaG0JJHEsdyzSQTMX+suY=
google.golang.org/genproto/googleapis/rpc v0.0.0-20240227224415-6ceb2ff114de/go.mod h1:H4O17MA/PE9BsGx3w+a+W2VOLLD1Qf7oJneAoU6WktY=
+1 -26
View File
@@ -127,29 +127,7 @@ type Config struct {
MaxTCPPingValue int32
AvgPingCount int
// 动态域名解析更新
DDNS struct {
Enable bool
Provider string
AccessID string
AccessSecret string
WebhookURL string
WebhookMethod string
WebhookRequestBody string
WebhookHeaders string
MaxRetries uint32
Profiles map[string]DDNSProfile
}
}
type DDNSProfile struct {
Provider string
AccessID string
AccessSecret string
WebhookURL string
WebhookMethod string
WebhookRequestBody string
WebhookHeaders string
DNSServers string
}
// Read 读取配置文件并应用
@@ -190,9 +168,6 @@ func (c *Config) Read(path string) error {
if c.AvgPingCount == 0 {
c.AvgPingCount = 2
}
if c.DDNS.MaxRetries == 0 {
c.DDNS.MaxRetries = 3
}
if c.Oauth2.OidcScopes == "" {
c.Oauth2.OidcScopes = "openid,profile,email"
}
+100
View File
@@ -0,0 +1,100 @@
package model
import (
"strings"
"gorm.io/gorm"
)
const (
ProviderDummy = iota
ProviderWebHook
ProviderCloudflare
ProviderTencentCloud
)
const (
_Dummy = "dummy"
_WebHook = "webhook"
_Cloudflare = "cloudflare"
_TencentCloud = "tencentcloud"
)
var ProviderMap = map[uint8]string{
ProviderDummy: _Dummy,
ProviderWebHook: _WebHook,
ProviderCloudflare: _Cloudflare,
ProviderTencentCloud: _TencentCloud,
}
var ProviderList = []DDNSProvider{
{
Name: _Dummy,
ID: ProviderDummy,
},
{
Name: _Cloudflare,
ID: ProviderCloudflare,
AccessSecret: true,
},
{
Name: _TencentCloud,
ID: ProviderTencentCloud,
AccessID: true,
AccessSecret: true,
},
// Least frequently used, always place this at the end
{
Name: _WebHook,
ID: ProviderWebHook,
AccessID: true,
AccessSecret: true,
WebhookURL: true,
WebhookMethod: true,
WebhookRequestType: true,
WebhookRequestBody: true,
WebhookHeaders: true,
},
}
type DDNSProfile struct {
Common
EnableIPv4 *bool
EnableIPv6 *bool
MaxRetries uint64
Name string
Provider uint8
AccessID string
AccessSecret string
WebhookURL string
WebhookMethod uint8
WebhookRequestType uint8
WebhookRequestBody string
WebhookHeaders string
Domains []string `gorm:"-"`
DomainsRaw string
}
func (d DDNSProfile) TableName() string {
return "ddns"
}
func (d *DDNSProfile) AfterFind(tx *gorm.DB) error {
if d.DomainsRaw != "" {
d.Domains = strings.Split(d.DomainsRaw, ",")
}
return nil
}
type DDNSProvider struct {
Name string
ID uint8
AccessID bool
AccessSecret bool
WebhookURL bool
WebhookMethod bool
WebhookRequestType bool
WebhookRequestBody bool
WebhookHeaders bool
}
+4 -4
View File
@@ -71,8 +71,8 @@ func (ns *NotificationServerBundle) reqBody(message string) (string, error) {
return string(msgBytes)[1 : len(msgBytes)-1]
}), nil
case NotificationRequestTypeForm:
var data map[string]string
if err := utils.Json.Unmarshal([]byte(n.RequestBody), &data); err != nil {
data, err := utils.GjsonParseStringMap(n.RequestBody)
if err != nil {
return "", err
}
params := url.Values{}
@@ -99,8 +99,8 @@ func (n *Notification) setRequestHeader(req *http.Request) error {
if n.RequestHeader == "" {
return nil
}
var m map[string]string
if err := utils.Json.Unmarshal([]byte(n.RequestHeader), &m); err != nil {
m, err := utils.GjsonParseStringMap(n.RequestHeader)
if err != nil {
return err
}
for k, v := range m {
+24 -14
View File
@@ -3,27 +3,28 @@ package model
import (
"fmt"
"html/template"
"log"
"sync"
"time"
"github.com/naiba/nezha/pkg/utils"
pb "github.com/naiba/nezha/proto"
"gorm.io/gorm"
)
type Server struct {
Common
Name string
Tag string // 分组名
Secret string `gorm:"uniqueIndex" json:"-"`
Note string `json:"-"` // 管理员可见备注
PublicNote string `json:"PublicNote,omitempty"` // 公开备注
DisplayIndex int // 展示排序,越大越靠前
HideForGuest bool // 对游客隐藏
EnableDDNS bool `json:"-"` // 是否启用DDNS 未在配置文件中启用DDNS 或 DDNS检查时间为0时此项无效
EnableIPv4 bool `json:"-"` // 是否启用DDNS IPv4
EnableIpv6 bool `json:"-"` // 是否启用DDNS IPv6
DDNSDomain string `json:"-"` // DDNS中的前缀 如基础域名为abc.oracle DDNSName为mjj 就会把mjj.abc.oracle解析服务器IP 为空则停用
DDNSProfile string `json:"-"` // DDNS配置
Tag string // 分组名
Secret string `gorm:"uniqueIndex" json:"-"`
Note string `json:"-"` // 管理员可见备注
PublicNote string `json:"PublicNote,omitempty"` // 公开备注
DisplayIndex int // 展示排序,越大越靠前
HideForGuest bool // 对游客隐藏
EnableDDNS bool // 启用DDNS
DDNSProfiles []uint64 `gorm:"-" json:"-"` // DDNS配置
DDNSProfilesRaw string `gorm:"default:'[]';column:ddns_profiles_raw" json:"-"`
Host *Host `gorm:"-"`
State *HostState `gorm:"-"`
@@ -48,6 +49,16 @@ func (s *Server) CopyFromRunningServer(old *Server) {
s.PrevTransferOutSnapshot = old.PrevTransferOutSnapshot
}
func (s *Server) AfterFind(tx *gorm.DB) error {
if s.DDNSProfilesRaw != "" {
if err := utils.Json.Unmarshal([]byte(s.DDNSProfilesRaw), &s.DDNSProfiles); err != nil {
log.Println("NEZHA>> Server.AfterFind:", err)
return nil
}
}
return nil
}
func boolToString(b bool) string {
if b {
return "true"
@@ -60,8 +71,7 @@ func (s Server) MarshalForDashboard() template.JS {
tag, _ := utils.Json.Marshal(s.Tag)
note, _ := utils.Json.Marshal(s.Note)
secret, _ := utils.Json.Marshal(s.Secret)
ddnsDomain, _ := utils.Json.Marshal(s.DDNSDomain)
ddnsProfile, _ := utils.Json.Marshal(s.DDNSProfile)
ddnsProfilesRaw, _ := utils.Json.Marshal(s.DDNSProfilesRaw)
publicNote, _ := utils.Json.Marshal(s.PublicNote)
return template.JS(fmt.Sprintf(`{"ID":%d,"Name":%s,"Secret":%s,"DisplayIndex":%d,"Tag":%s,"Note":%s,"HideForGuest": %s,"EnableDDNS": %s,"EnableIPv4": %s,"EnableIpv6": %s,"DDNSDomain": %s,"DDNSProfile": %s,"PublicNote": %s}`, s.ID, name, secret, s.DisplayIndex, tag, note, boolToString(s.HideForGuest), boolToString(s.EnableDDNS), boolToString(s.EnableIPv4), boolToString(s.EnableIpv6), ddnsDomain, ddnsProfile, publicNote))
return template.JS(fmt.Sprintf(`{"ID":%d,"Name":%s,"Secret":%s,"DisplayIndex":%d,"Tag":%s,"Note":%s,"HideForGuest": %s,"EnableDDNS": %s,"DDNSProfilesRaw": %s,"PublicNote": %s}`, s.ID, name, secret, s.DisplayIndex, tag, note, boolToString(s.HideForGuest), boolToString(s.EnableDDNS), ddnsProfilesRaw, publicNote))
}
-225
View File
@@ -1,225 +0,0 @@
package ddns
import (
"bytes"
"fmt"
"io"
"log"
"net/http"
"net/url"
"github.com/naiba/nezha/pkg/utils"
)
const baseEndpoint = "https://api.cloudflare.com/client/v4/zones"
type ProviderCloudflare struct {
secret string
zoneId string
recordId string
domainConfig *DomainConfig
}
type cfReq struct {
Name string `json:"name"`
Type string `json:"type"`
Content string `json:"content"`
TTL uint32 `json:"ttl"`
Proxied bool `json:"proxied"`
}
type cfResp struct {
Result []struct {
ID string `json:"id"`
Name string `json:"name"`
} `json:"result"`
}
func NewProviderCloudflare(s string) *ProviderCloudflare {
return &ProviderCloudflare{
secret: s,
}
}
func (provider *ProviderCloudflare) UpdateDomain(domainConfig *DomainConfig) error {
if domainConfig == nil {
return fmt.Errorf("获取 DDNS 配置失败")
}
provider.domainConfig = domainConfig
err := provider.getZoneID()
if err != nil {
return fmt.Errorf("无法获取 zone ID: %s", err)
}
// 当IPv4和IPv6同时成功才算作成功
if provider.domainConfig.EnableIPv4 {
if err = provider.addDomainRecord(true); err != nil {
return err
}
}
if provider.domainConfig.EnableIpv6 {
if err = provider.addDomainRecord(false); err != nil {
return err
}
}
return nil
}
func (provider *ProviderCloudflare) addDomainRecord(isIpv4 bool) error {
err := provider.findDNSRecord(isIpv4)
if err != nil {
return fmt.Errorf("查找 DNS 记录时出错: %s", err)
}
if provider.recordId == "" {
// 添加 DNS 记录
return provider.createDNSRecord(isIpv4)
} else {
// 更新 DNS 记录
return provider.updateDNSRecord(isIpv4)
}
}
func (provider *ProviderCloudflare) getZoneID() error {
_, realDomain := splitDomain(provider.domainConfig.FullDomain)
zu, _ := url.Parse(baseEndpoint)
q := zu.Query()
q.Set("name", realDomain)
zu.RawQuery = q.Encode()
body, err := provider.sendRequest("GET", zu.String(), nil)
if err != nil {
return err
}
res := &cfResp{}
err = utils.Json.Unmarshal(body, res)
if err != nil {
return err
}
result := res.Result
if len(result) > 0 {
provider.zoneId = result[0].ID
return nil
}
return fmt.Errorf("找不到 Zone ID")
}
func (provider *ProviderCloudflare) findDNSRecord(isIPv4 bool) error {
var ipType string
if isIPv4 {
ipType = "A"
} else {
ipType = "AAAA"
}
de, _ := url.JoinPath(baseEndpoint, provider.zoneId, "dns_records")
du, _ := url.Parse(de)
q := du.Query()
q.Set("name", provider.domainConfig.FullDomain)
q.Set("type", ipType)
du.RawQuery = q.Encode()
body, err := provider.sendRequest("GET", du.String(), nil)
if err != nil {
return err
}
res := &cfResp{}
err = utils.Json.Unmarshal(body, res)
if err != nil {
return err
}
result := res.Result
if len(result) > 0 {
provider.recordId = result[0].ID
return nil
}
return nil
}
func (provider *ProviderCloudflare) createDNSRecord(isIPv4 bool) error {
var ipType, ipAddr string
if isIPv4 {
ipType = "A"
ipAddr = provider.domainConfig.Ipv4Addr
} else {
ipType = "AAAA"
ipAddr = provider.domainConfig.Ipv6Addr
}
de, _ := url.JoinPath(baseEndpoint, provider.zoneId, "dns_records")
data := &cfReq{
Name: provider.domainConfig.FullDomain,
Type: ipType,
Content: ipAddr,
TTL: 60,
Proxied: false,
}
jsonData, _ := utils.Json.Marshal(data)
_, err := provider.sendRequest("POST", de, jsonData)
return err
}
func (provider *ProviderCloudflare) updateDNSRecord(isIPv4 bool) error {
var ipType, ipAddr string
if isIPv4 {
ipType = "A"
ipAddr = provider.domainConfig.Ipv4Addr
} else {
ipType = "AAAA"
ipAddr = provider.domainConfig.Ipv6Addr
}
de, _ := url.JoinPath(baseEndpoint, provider.zoneId, "dns_records", provider.recordId)
data := &cfReq{
Name: provider.domainConfig.FullDomain,
Type: ipType,
Content: ipAddr,
TTL: 60,
Proxied: false,
}
jsonData, _ := utils.Json.Marshal(data)
_, err := provider.sendRequest("PATCH", de, jsonData)
return err
}
// 以下为辅助方法,如发送 HTTP 请求等
func (provider *ProviderCloudflare) sendRequest(method string, url string, data []byte) ([]byte, error) {
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 := utils.HttpClient.Do(req)
if err != nil {
return nil, err
}
defer func(Body io.ReadCloser) {
err := Body.Close()
if err != nil {
log.Printf("NEZHA>> 无法关闭HTTP响应体流: %s", err.Error())
}
}(resp.Body)
body, err := io.ReadAll(resp.Body)
if err != nil {
return nil, err
}
return body, nil
}
+133 -14
View File
@@ -1,22 +1,141 @@
package ddns
import "golang.org/x/net/publicsuffix"
import (
"context"
"fmt"
"log"
"strings"
"time"
type DomainConfig struct {
EnableIPv4 bool
EnableIpv6 bool
FullDomain string
Ipv4Addr string
Ipv6Addr string
"github.com/libdns/libdns"
"github.com/miekg/dns"
"github.com/naiba/nezha/model"
"github.com/naiba/nezha/pkg/utils"
)
var (
dnsTimeOut = 10 * time.Second
customDNSServers []string
)
type IP struct {
Ipv4Addr string
Ipv6Addr string
}
type Provider interface {
// UpdateDomain Return is updated
UpdateDomain(*DomainConfig) error
type Provider struct {
ctx context.Context
ipAddr string
recordType string
domain string
prefix string
zone string
DDNSProfile *model.DDNSProfile
IPAddrs *IP
Setter libdns.RecordSetter
}
func splitDomain(domain string) (prefix string, realDomain string) {
realDomain, _ = publicsuffix.EffectiveTLDPlusOne(domain)
prefix = domain[:len(domain)-len(realDomain)-1]
return prefix, realDomain
func InitDNSServers(s string) {
if s != "" {
customDNSServers = strings.Split(s, ",")
}
}
func (provider *Provider) UpdateDomain(ctx context.Context) {
provider.ctx = ctx
for _, domain := range provider.DDNSProfile.Domains {
for retries := 0; retries < int(provider.DDNSProfile.MaxRetries); retries++ {
provider.domain = domain
log.Printf("NEZHA>> 正在尝试更新域名(%s)DDNS(%d/%d)", provider.domain, retries+1, provider.DDNSProfile.MaxRetries)
if err := provider.updateDomain(); err != nil {
log.Printf("NEZHA>> 尝试更新域名(%s)DDNS失败: %v", provider.domain, err)
} else {
log.Printf("NEZHA>> 尝试更新域名(%s)DDNS成功", provider.domain)
break
}
}
}
}
func (provider *Provider) updateDomain() error {
var err error
provider.prefix, provider.zone, err = splitDomainSOA(provider.domain)
if err != nil {
return err
}
// 当IPv4和IPv6同时成功才算作成功
if *provider.DDNSProfile.EnableIPv4 {
provider.recordType = getRecordString(true)
provider.ipAddr = provider.IPAddrs.Ipv4Addr
if err = provider.addDomainRecord(); err != nil {
return err
}
}
if *provider.DDNSProfile.EnableIPv6 {
provider.recordType = getRecordString(false)
provider.ipAddr = provider.IPAddrs.Ipv6Addr
if err = provider.addDomainRecord(); err != nil {
return err
}
}
return nil
}
func (provider *Provider) addDomainRecord() error {
_, err := provider.Setter.SetRecords(provider.ctx, provider.zone,
[]libdns.Record{
{
Type: provider.recordType,
Name: provider.prefix,
Value: provider.ipAddr,
TTL: time.Minute,
},
})
return err
}
func splitDomainSOA(domain string) (prefix string, zone string, err error) {
c := &dns.Client{Timeout: dnsTimeOut}
domain += "."
indexes := dns.Split(domain)
servers := utils.DNSServers
if len(customDNSServers) > 0 {
servers = customDNSServers
}
var r *dns.Msg
for _, idx := range indexes {
m := new(dns.Msg)
m.SetQuestion(domain[idx:], dns.TypeSOA)
for _, server := range servers {
r, _, err = c.Exchange(m, server)
if err != nil {
return
}
if len(r.Answer) > 0 {
if soa, ok := r.Answer[0].(*dns.SOA); ok {
zone = soa.Hdr.Name
prefix = libdns.RelativeName(domain, zone)
return
}
}
}
}
return "", "", fmt.Errorf("SOA record not found for domain: %s", domain)
}
func getRecordString(isIpv4 bool) string {
if isIpv4 {
return "A"
}
return "AAAA"
}
+49
View File
@@ -0,0 +1,49 @@
package ddns
import (
"os"
"testing"
)
type testSt struct {
domain string
zone string
prefix string
}
func TestSplitDomainSOA(t *testing.T) {
if ci := os.Getenv("CI"); ci != "" { // skip if test on CI
return
}
cases := []testSt{
{
domain: "www.example.co.uk",
zone: "example.co.uk.",
prefix: "www",
},
{
domain: "abc.example.com",
zone: "example.com.",
prefix: "abc",
},
{
domain: "example.com",
zone: "example.com.",
prefix: "",
},
}
for _, c := range cases {
prefix, zone, err := splitDomainSOA(c.domain)
if err != nil {
t.Fatalf("Error: %s", err)
}
if prefix != c.prefix {
t.Fatalf("Expected prefix %s, but got %s", c.prefix, prefix)
}
if zone != c.zone {
t.Fatalf("Expected zone %s, but got %s", c.zone, zone)
}
}
}
-7
View File
@@ -1,7 +0,0 @@
package ddns
type ProviderDummy struct{}
func (provider *ProviderDummy) UpdateDomain(domainConfig *DomainConfig) error {
return nil
}
+16
View File
@@ -0,0 +1,16 @@
package dummy
import (
"context"
"github.com/libdns/libdns"
)
// Internal use
type Provider struct {
}
func (provider *Provider) SetRecords(ctx context.Context, zone string,
recs []libdns.Record) ([]libdns.Record, error) {
return recs, nil
}
-263
View File
@@ -1,263 +0,0 @@
package ddns
import (
"bytes"
"crypto/hmac"
"crypto/sha256"
"encoding/hex"
"fmt"
"io"
"log"
"net/http"
"strconv"
"strings"
"time"
"github.com/naiba/nezha/pkg/utils"
)
const te = "https://dnspod.tencentcloudapi.com"
type ProviderTencentCloud struct {
secretID string
secretKey string
domainConfig *DomainConfig
resp *tcResp
}
type tcReq struct {
RecordType string `json:"RecordType"`
Domain string `json:"Domain"`
RecordLine string `json:"RecordLine"`
Subdomain string `json:"Subdomain,omitempty"`
SubDomain string `json:"SubDomain,omitempty"` // As is
Value string `json:"Value,omitempty"`
TTL uint32 `json:"TTL,omitempty"`
RecordId uint64 `json:"RecordId,omitempty"`
}
type tcResp struct {
Response struct {
RecordList []struct {
RecordId uint64
Value string
}
Error struct {
Code string
}
}
}
func NewProviderTencentCloud(id, key string) *ProviderTencentCloud {
return &ProviderTencentCloud{
secretID: id,
secretKey: key,
}
}
func (provider *ProviderTencentCloud) UpdateDomain(domainConfig *DomainConfig) error {
if domainConfig == nil {
return fmt.Errorf("获取 DDNS 配置失败")
}
provider.domainConfig = domainConfig
// 当IPv4和IPv6同时成功才算作成功
var err error
if provider.domainConfig.EnableIPv4 {
if err = provider.addDomainRecord(true); err != nil {
return err
}
}
if provider.domainConfig.EnableIpv6 {
if err = provider.addDomainRecord(false); err != nil {
return err
}
}
return err
}
func (provider *ProviderTencentCloud) addDomainRecord(isIpv4 bool) error {
err := provider.findDNSRecord(isIpv4)
if err != nil {
return fmt.Errorf("查找 DNS 记录时出错: %s", err)
}
if provider.resp.Response.Error.Code == "ResourceNotFound.NoDataOfRecord" { // 没有找到 DNS 记录
return provider.createDNSRecord(isIpv4)
} else if provider.resp.Response.Error.Code != "" {
return fmt.Errorf("查询 DNS 记录时出错,错误代码为: %s", provider.resp.Response.Error.Code)
}
// 默认情况下更新 DNS 记录
return provider.updateDNSRecord(isIpv4)
}
func (provider *ProviderTencentCloud) findDNSRecord(isIPv4 bool) error {
var ipType string
if isIPv4 {
ipType = "A"
} else {
ipType = "AAAA"
}
prefix, realDomain := splitDomain(provider.domainConfig.FullDomain)
data := &tcReq{
RecordType: ipType,
Domain: realDomain,
RecordLine: "默认",
Subdomain: prefix,
}
jsonData, _ := utils.Json.Marshal(data)
body, err := provider.sendRequest("DescribeRecordList", jsonData)
if err != nil {
return err
}
provider.resp = &tcResp{}
err = utils.Json.Unmarshal(body, provider.resp)
if err != nil {
return err
}
return nil
}
func (provider *ProviderTencentCloud) createDNSRecord(isIPv4 bool) error {
var ipType, ipAddr string
if isIPv4 {
ipType = "A"
ipAddr = provider.domainConfig.Ipv4Addr
} else {
ipType = "AAAA"
ipAddr = provider.domainConfig.Ipv6Addr
}
prefix, realDomain := splitDomain(provider.domainConfig.FullDomain)
data := &tcReq{
RecordType: ipType,
RecordLine: "默认",
Domain: realDomain,
SubDomain: prefix,
Value: ipAddr,
TTL: 600,
}
jsonData, _ := utils.Json.Marshal(data)
_, err := provider.sendRequest("CreateRecord", jsonData)
return err
}
func (provider *ProviderTencentCloud) updateDNSRecord(isIPv4 bool) error {
var ipType, ipAddr string
if isIPv4 {
ipType = "A"
ipAddr = provider.domainConfig.Ipv4Addr
} else {
ipType = "AAAA"
ipAddr = provider.domainConfig.Ipv6Addr
}
prefix, realDomain := splitDomain(provider.domainConfig.FullDomain)
data := &tcReq{
RecordType: ipType,
RecordLine: "默认",
Domain: realDomain,
SubDomain: prefix,
Value: ipAddr,
TTL: 600,
RecordId: provider.resp.Response.RecordList[0].RecordId,
}
jsonData, _ := utils.Json.Marshal(data)
_, err := provider.sendRequest("ModifyRecord", jsonData)
return err
}
// 以下为辅助方法,如发送 HTTP 请求等
func (provider *ProviderTencentCloud) sendRequest(action string, data []byte) ([]byte, error) {
req, err := http.NewRequest("POST", te, bytes.NewBuffer(data))
if err != nil {
return nil, err
}
req.Header.Set("Content-Type", "application/json")
req.Header.Set("X-TC-Version", "2021-03-23")
provider.signRequest(provider.secretID, provider.secretKey, req, action, string(data))
resp, err := utils.HttpClient.Do(req)
if err != nil {
return nil, err
}
defer func(Body io.ReadCloser) {
err := Body.Close()
if err != nil {
log.Printf("NEZHA>> 无法关闭HTTP响应体流: %s\n", err.Error())
}
}(resp.Body)
body, err := io.ReadAll(resp.Body)
if err != nil {
return nil, err
}
return body, nil
}
// https://github.com/jeessy2/ddns-go/blob/master/util/tencent_cloud_signer.go
func (provider *ProviderTencentCloud) sha256hex(s string) string {
b := sha256.Sum256([]byte(s))
return hex.EncodeToString(b[:])
}
func (provider *ProviderTencentCloud) hmacsha256(s, key string) string {
hashed := hmac.New(sha256.New, []byte(key))
hashed.Write([]byte(s))
return string(hashed.Sum(nil))
}
func (provider *ProviderTencentCloud) WriteString(strs ...string) string {
var b strings.Builder
for _, str := range strs {
b.WriteString(str)
}
return b.String()
}
func (provider *ProviderTencentCloud) signRequest(secretId string, secretKey string, r *http.Request, action string, payload string) {
algorithm := "TC3-HMAC-SHA256"
service := "dnspod"
host := provider.WriteString(service, ".tencentcloudapi.com")
timestamp := time.Now().Unix()
timestampStr := strconv.FormatInt(timestamp, 10)
// 步骤 1:拼接规范请求串
canonicalHeaders := provider.WriteString("content-type:application/json\nhost:", host, "\nx-tc-action:", strings.ToLower(action), "\n")
signedHeaders := "content-type;host;x-tc-action"
hashedRequestPayload := provider.sha256hex(payload)
canonicalRequest := provider.WriteString("POST\n/\n\n", canonicalHeaders, "\n", signedHeaders, "\n", hashedRequestPayload)
// 步骤 2:拼接待签名字符串
date := time.Unix(timestamp, 0).UTC().Format("2006-01-02")
credentialScope := provider.WriteString(date, "/", service, "/tc3_request")
hashedCanonicalRequest := provider.sha256hex(canonicalRequest)
string2sign := provider.WriteString(algorithm, "\n", timestampStr, "\n", credentialScope, "\n", hashedCanonicalRequest)
// 步骤 3:计算签名
secretDate := provider.hmacsha256(date, provider.WriteString("TC3", secretKey))
secretService := provider.hmacsha256(service, secretDate)
secretSigning := provider.hmacsha256("tc3_request", secretService)
signature := hex.EncodeToString([]byte(provider.hmacsha256(string2sign, secretSigning)))
// 步骤 4:拼接 Authorization
authorization := provider.WriteString(algorithm, " Credential=", secretId, "/", credentialScope, ", SignedHeaders=", signedHeaders, ", Signature=", signature)
r.Header.Add("Authorization", authorization)
r.Header.Set("Host", host)
r.Header.Set("X-TC-Action", action)
r.Header.Add("X-TC-Timestamp", timestampStr)
}
-110
View File
@@ -1,110 +0,0 @@
package ddns
import (
"bytes"
"fmt"
"net/http"
"net/url"
"strings"
"github.com/naiba/nezha/pkg/utils"
)
type ProviderWebHook struct {
url string
requestMethod string
requestBody string
requestHeader string
domainConfig *DomainConfig
}
func NewProviderWebHook(s, rm, rb, rh string) *ProviderWebHook {
return &ProviderWebHook{
url: s,
requestMethod: rm,
requestBody: rb,
requestHeader: rh,
}
}
func (provider *ProviderWebHook) UpdateDomain(domainConfig *DomainConfig) error {
if domainConfig == nil {
return fmt.Errorf("获取 DDNS 配置失败")
}
provider.domainConfig = domainConfig
if provider.domainConfig.FullDomain == "" {
return fmt.Errorf("failed to update an empty domain")
}
if provider.domainConfig.EnableIPv4 && provider.domainConfig.Ipv4Addr != "" {
req, err := provider.prepareRequest(true)
if err != nil {
return fmt.Errorf("failed to update a domain: %s. Cause by: %v", provider.domainConfig.FullDomain, err)
}
if _, err := utils.HttpClient.Do(req); err != nil {
return fmt.Errorf("failed to update a domain: %s. Cause by: %v", provider.domainConfig.FullDomain, err)
}
}
if provider.domainConfig.EnableIpv6 && provider.domainConfig.Ipv6Addr != "" {
req, err := provider.prepareRequest(false)
if err != nil {
return fmt.Errorf("failed to update a domain: %s. Cause by: %v", provider.domainConfig.FullDomain, err)
}
if _, err := utils.HttpClient.Do(req); err != nil {
return fmt.Errorf("failed to update a domain: %s. Cause by: %v", provider.domainConfig.FullDomain, err)
}
}
return nil
}
func (provider *ProviderWebHook) prepareRequest(isIPv4 bool) (*http.Request, error) {
u, err := url.Parse(provider.url)
if err != nil {
return nil, fmt.Errorf("failed parsing url: %v", err)
}
// Only handle queries here
q := u.Query()
for p, vals := range q {
for n, v := range vals {
vals[n] = provider.formatWebhookString(v, isIPv4)
}
q[p] = vals
}
u.RawQuery = q.Encode()
body := provider.formatWebhookString(provider.requestBody, isIPv4)
header := provider.formatWebhookString(provider.requestHeader, isIPv4)
headers := strings.Split(header, "\n")
req, err := http.NewRequest(provider.requestMethod, u.String(), bytes.NewBufferString(body))
if err != nil {
return nil, fmt.Errorf("failed creating new request: %v", err)
}
utils.SetStringHeadersToRequest(req, headers)
return req, nil
}
func (provider *ProviderWebHook) formatWebhookString(s string, isIPv4 bool) string {
var ipAddr, ipType string
if isIPv4 {
ipAddr = provider.domainConfig.Ipv4Addr
ipType = "ipv4"
} else {
ipAddr = provider.domainConfig.Ipv6Addr
ipType = "ipv6"
}
r := strings.NewReplacer(
"{ip}", ipAddr,
"{domain}", provider.domainConfig.FullDomain,
"{type}", ipType,
"\r", "",
)
result := r.Replace(strings.TrimSpace(s))
return result
}
+180
View File
@@ -0,0 +1,180 @@
package webhook
import (
"context"
"errors"
"fmt"
"net/http"
"net/url"
"strings"
"github.com/libdns/libdns"
"github.com/naiba/nezha/model"
"github.com/naiba/nezha/pkg/utils"
)
const (
_ = iota
methodGET
methodPOST
methodPATCH
methodDELETE
methodPUT
)
const (
_ = iota
requestTypeJSON
requestTypeForm
)
var requestTypes = map[uint8]string{
methodGET: "GET",
methodPOST: "POST",
methodPATCH: "PATCH",
methodDELETE: "DELETE",
methodPUT: "PUT",
}
// Internal use
type Provider struct {
ipAddr string
ipType string
recordType string
domain string
DDNSProfile *model.DDNSProfile
}
func (provider *Provider) SetRecords(ctx context.Context, zone string,
recs []libdns.Record) ([]libdns.Record, error) {
for _, rec := range recs {
provider.recordType = rec.Type
provider.ipType = recordToIPType(provider.recordType)
provider.ipAddr = rec.Value
provider.domain = fmt.Sprintf("%s.%s", rec.Name, strings.TrimSuffix(zone, "."))
req, err := provider.prepareRequest(ctx)
if err != nil {
return nil, fmt.Errorf("failed to update a domain: %s. Cause by: %v", provider.domain, err)
}
if _, err := utils.HttpClient.Do(req); err != nil {
return nil, fmt.Errorf("failed to update a domain: %s. Cause by: %v", provider.domain, err)
}
}
return recs, nil
}
func (provider *Provider) prepareRequest(ctx context.Context) (*http.Request, error) {
u, err := provider.reqUrl()
if err != nil {
return nil, err
}
body, err := provider.reqBody()
if err != nil {
return nil, err
}
headers, err := utils.GjsonParseStringMap(
provider.formatWebhookString(provider.DDNSProfile.WebhookHeaders))
if err != nil {
return nil, err
}
req, err := http.NewRequestWithContext(ctx, requestTypes[provider.DDNSProfile.WebhookMethod], u.String(), strings.NewReader(body))
if err != nil {
return nil, err
}
provider.setContentType(req)
for k, v := range headers {
req.Header.Set(k, v)
}
return req, nil
}
func (provider *Provider) setContentType(req *http.Request) {
if provider.DDNSProfile.WebhookMethod == methodGET {
return
}
if provider.DDNSProfile.WebhookRequestType == requestTypeForm {
req.Header.Set("Content-Type", "application/x-www-form-urlencoded")
} else {
req.Header.Set("Content-Type", "application/json")
}
}
func (provider *Provider) reqUrl() (*url.URL, error) {
formattedUrl := strings.ReplaceAll(provider.DDNSProfile.WebhookURL, "#", "%23")
u, err := url.Parse(formattedUrl)
if err != nil {
return nil, err
}
// Only handle queries here
q := u.Query()
for p, vals := range q {
for n, v := range vals {
vals[n] = provider.formatWebhookString(v)
}
q[p] = vals
}
u.RawQuery = q.Encode()
return u, nil
}
func (provider *Provider) reqBody() (string, error) {
if provider.DDNSProfile.WebhookMethod == methodGET ||
provider.DDNSProfile.WebhookMethod == methodDELETE {
return "", nil
}
switch provider.DDNSProfile.WebhookRequestType {
case requestTypeJSON:
return provider.formatWebhookString(provider.DDNSProfile.WebhookRequestBody), nil
case requestTypeForm:
data, err := utils.GjsonParseStringMap(provider.DDNSProfile.WebhookRequestBody)
if err != nil {
return "", err
}
params := url.Values{}
for k, v := range data {
params.Add(k, provider.formatWebhookString(v))
}
return params.Encode(), nil
default:
return "", errors.New("request type not supported")
}
}
func (provider *Provider) formatWebhookString(s string) string {
r := strings.NewReplacer(
"#ip#", provider.ipAddr,
"#domain#", provider.domain,
"#type#", provider.ipType,
"#record#", provider.recordType,
"#access_id#", provider.DDNSProfile.AccessID,
"#access_secret#", provider.DDNSProfile.AccessSecret,
"\r", "",
)
result := r.Replace(strings.TrimSpace(s))
return result
}
func recordToIPType(record string) string {
switch record {
case "A":
return "ipv4"
case "AAAA":
return "ipv6"
default:
return ""
}
}
+116
View File
@@ -0,0 +1,116 @@
package webhook
import (
"context"
"testing"
"github.com/naiba/nezha/model"
)
var (
reqTypeForm = "application/x-www-form-urlencoded"
reqTypeJSON = "application/json"
)
type testSt struct {
profile model.DDNSProfile
expectURL string
expectBody string
expectContentType string
expectHeader map[string]string
}
func execCase(t *testing.T, item testSt) {
pw := Provider{DDNSProfile: &item.profile}
pw.ipAddr = "1.1.1.1"
pw.domain = item.profile.Domains[0]
pw.ipType = "ipv4"
pw.recordType = "A"
pw.DDNSProfile = &item.profile
reqUrl, err := pw.reqUrl()
if err != nil {
t.Fatalf("Error: %s", err)
}
if item.expectURL != reqUrl.String() {
t.Fatalf("Expected %s, but got %s", item.expectURL, reqUrl.String())
}
reqBody, err := pw.reqBody()
if err != nil {
t.Fatalf("Error: %s", err)
}
if item.expectBody != reqBody {
t.Fatalf("Expected %s, but got %s", item.expectBody, reqBody)
}
req, err := pw.prepareRequest(context.Background())
if err != nil {
t.Fatalf("Error: %s", err)
}
if item.expectContentType != req.Header.Get("Content-Type") {
t.Fatalf("Expected %s, but got %s", item.expectContentType, req.Header.Get("Content-Type"))
}
for k, v := range item.expectHeader {
if v != req.Header.Get(k) {
t.Fatalf("Expected %s, but got %s", v, req.Header.Get(k))
}
}
}
func TestWebhookRequest(t *testing.T) {
ipv4 := true
cases := []testSt{
{
profile: model.DDNSProfile{
Domains: []string{"www.example.com"},
MaxRetries: 1,
EnableIPv4: &ipv4,
WebhookURL: "http://ddns.example.com/?ip=#ip#",
WebhookMethod: methodGET,
WebhookHeaders: `{"ip":"#ip#","record":"#record#"}`,
},
expectURL: "http://ddns.example.com/?ip=1.1.1.1",
expectContentType: "",
expectHeader: map[string]string{
"ip": "1.1.1.1",
"record": "A",
},
},
{
profile: model.DDNSProfile{
Domains: []string{"www.example.com"},
MaxRetries: 1,
EnableIPv4: &ipv4,
WebhookURL: "http://ddns.example.com/api",
WebhookMethod: methodPOST,
WebhookRequestType: requestTypeJSON,
WebhookRequestBody: `{"ip":"#ip#","record":"#record#"}`,
},
expectURL: "http://ddns.example.com/api",
expectContentType: reqTypeJSON,
expectBody: `{"ip":"1.1.1.1","record":"A"}`,
},
{
profile: model.DDNSProfile{
Domains: []string{"www.example.com"},
MaxRetries: 1,
EnableIPv4: &ipv4,
WebhookURL: "http://ddns.example.com/api",
WebhookMethod: methodPOST,
WebhookRequestType: requestTypeForm,
WebhookRequestBody: `{"ip":"#ip#","record":"#record#"}`,
},
expectURL: "http://ddns.example.com/api",
expectContentType: reqTypeForm,
expectBody: "ip=1.1.1.1&record=A",
},
}
for _, c := range cases {
execCase(t, c)
}
}
+1
View File
@@ -16,6 +16,7 @@ var adminPage = map[string]bool{
"/monitor": true,
"/setting": true,
"/notification": true,
"/ddns": true,
"/nat": true,
"/cron": true,
"/api": true,
+40
View File
@@ -0,0 +1,40 @@
package utils
import (
"errors"
"github.com/tidwall/gjson"
)
var (
ErrGjsonNotFound = errors.New("specified path does not exist")
ErrGjsonWrongType = errors.New("wrong type")
)
func GjsonGet(json []byte, path string) (gjson.Result, error) {
result := gjson.GetBytes(json, path)
if !result.Exists() {
return result, ErrGjsonNotFound
}
return result, nil
}
func GjsonParseStringMap(jsonObject string) (map[string]string, error) {
if jsonObject == "" {
return nil, nil
}
result := gjson.Parse(jsonObject)
if !result.IsObject() {
return nil, ErrGjsonWrongType
}
ret := make(map[string]string)
result.ForEach(func(key, value gjson.Result) bool {
ret[key.String()] = value.String()
return true
})
return ret, nil
}
+33
View File
@@ -0,0 +1,33 @@
package utils
import (
"embed"
"io/fs"
"os"
)
// HybridFS combines embed.FS and os.DirFS.
type HybridFS struct {
embedFS, dir fs.FS
}
func NewHybridFS(embed embed.FS, subDir string, localDir string) (*HybridFS, error) {
subFS, err := fs.Sub(embed, subDir)
if err != nil {
return nil, err
}
return &HybridFS{
embedFS: subFS,
dir: os.DirFS(localDir),
}, nil
}
func (hfs *HybridFS) Open(name string) (fs.File, error) {
// Ensure embed files are not replaced
if file, err := hfs.embedFS.Open(name); err == nil {
return file, nil
}
return hfs.dir.Open(name)
}
+2
View File
@@ -22,6 +22,8 @@ func init() {
SkipVerifySSL: false,
}),
})
http.DefaultClient.Timeout = time.Minute * 10
}
type _httpTransport struct {
+5 -14
View File
@@ -3,7 +3,6 @@ package utils
import (
"crypto/rand"
"math/big"
"net/http"
"os"
"regexp"
"strings"
@@ -11,7 +10,11 @@ import (
jsoniter "github.com/json-iterator/go"
)
var Json = jsoniter.ConfigCompatibleWithStandardLibrary
var (
Json = jsoniter.ConfigCompatibleWithStandardLibrary
DNSServers = []string{"1.1.1.1:53", "223.5.5.5:53"}
)
func IsWindows() bool {
return os.PathSeparator == '\\' && os.PathListSeparator == ';'
@@ -87,15 +90,3 @@ func Uint64SubInt64(a uint64, b int64) uint64 {
}
return a - uint64(b)
}
func SetStringHeadersToRequest(req *http.Request, headers []string) {
if req == nil {
return
}
for _, element := range headers {
kv := strings.SplitN(element, ":", 2)
if len(kv) == 2 {
req.Header.Add(kv[0], kv[1])
}
}
}
+86 -38
View File
@@ -23,19 +23,19 @@ other = "Home"
other = "Back to Homepage"
[AdminPanel]
other = "Admin Panel"
other = "Dashboard"
[Logout]
other = "Logout"
other = "Log out"
[Login]
other = "Login"
other = "Log in"
[ConfirmLogout]
other = "Do you want to Logout?"
other = "Confirm Logging Out?"
[AfterLoggingOutYouHaveToLoginAgain]
other = "You need to Login again to continue"
other = "You will need to log in again to continue"
[Cancel]
other = "Cancel"
@@ -56,7 +56,7 @@ other = "Cron Expression"
other = "Backup"
[3amDaily]
other = "(Daily at 3 AM)"
other = "(At 3 AM)"
[Command]
other = "Command"
@@ -71,7 +71,7 @@ other = "Ignore All, Execute Only on Specific Servers"
other = "Cover All, Except Specific Servers"
[ExecuteByTriggerServer]
other = "Execute on Trigger Server"
other = "Execute on Alarmed Servers"
[SpecificServers]
other = "Specific Servers"
@@ -95,7 +95,7 @@ other = "Cron Task"
other = "Trigger Task"
[TheFormaOfTheScheduleIs]
other = "The Cron Expression is like:"
other = "The Cron Expression format is:"
[SecondsMinutesHoursDaysMonthsWeeksSeeDetails]
other = "Sec Min Hour Day Month Week, see details in "
@@ -104,7 +104,7 @@ other = "Sec Min Hour Day Month Week, see details in "
other = "CRON Expression Format"
[IntroductionOfCommands]
other = "Introduction of Commands: Writing commands is similar to shell/bat scripts. It's advised not to break lines, and multiple commands can be connected with <code>&&</code> or <code>&</code>. If a command cannot be found, it might be due to <code>PATH</code> environment variable configuration issues. On <code>Linux</code> servers, you can add <code>source ~/.bashrc</code> at the beginning of the command, or use the command's absolute path."
other = "Note on writing commands: It is similar to shell/bat scripts. but it's advised not to start a newline, and connect multiple commands with <code>&&</code> or <code>;</code>. If a command cannot be found, it is possibly due to <code>PATH</code> environment variable issues. On <code>Linux</code> servers, you can add <code>source ~/.bashrc</code> at the beginning of the command, or just use the absolute path."
[AddMonitor]
other = "Add Service Monitor"
@@ -119,7 +119,7 @@ other = "Target"
other = "Type"
[SslExpirationOrChange]
other = "(SSL Expiration or Change)"
other = "(Certificate Expiration and Changes)"
[Duration]
other = "Interval"
@@ -134,10 +134,10 @@ other = "Enable Failure Notification"
other = "Failure Notification"
[MaxLatency]
other = "Max Latency (ms)"
other = "Maximum Latency (ms)"
[MinLatency]
other = "Min Latency (ms)"
other = "Minimum Latency (ms)"
[EnableLatencyNotification]
other = "Enable Latency Notification"
@@ -147,7 +147,7 @@ other = "Latency Notification"
[IntroductionOfMonitor]
other = """
For type <b>HTTP-GET</b>, enter URL (with http/https, HTTPS protocol will also monitor SSL certificate);<br>
For type <b>HTTP-GET</b>, enter URL (with http/https scheme, HTTPS protocol will also monitor SSL certificate);<br>
For type <b>ICMP-Ping</b>, enter domain/IP without port: example.com;<br>
For type <b>TCP-Ping</b>, enter domain/IP + port number: example.com:22"""
@@ -182,16 +182,16 @@ other = "Notification Trigger Mode"
other = "Always Trigger"
[ModeOnetimeTrigger]
other = "Always Trigger"
other = "Trigger Once"
[EnableTriggerTask]
other = "Enable Trigger Task"
[FailTriggerTasks]
other = "Tasks triggered during an alarm"
other = "Tasks to trigger on an alarm"
[RecoverTriggerTasks]
other = "Tasks triggered during recovery"
other = "Tasks to trigger after recovery"
[Enable]
other = "Enable"
@@ -218,7 +218,7 @@ other = "Einstein Lightspeed 1"
other = "Display Index"
[TheLargerTheNumberTheHigherThePriority]
other = "Higher Number, Higher Priority"
other = "Greater Number, Higher Priority"
[Secret]
other = "Secret"
@@ -230,10 +230,10 @@ other = "Note"
other = "Public Note"
[LinuxOneKeyInstall]
other = "Linux One-Command Install"
other = "Linux Installation Command"
[NoDomainAlert]
other = "Set CDN bypassed Domain/IP in setting before using this function"
other = "Please set the IP (or a domain pointed to it) of the original server in settings"
[PushSuccessfully]
other = "Send Success Notification"
@@ -254,13 +254,13 @@ other = "Cover All"
other = "Ignore All"
[ByTrigger]
other = "Execute by Trigger"
other = "Alarmed Servers"
[DeleteScheduledTask]
other = "Delete Scheduled Task"
[ConfirmToDeleteThisScheduledTask]
other = "Are you sure you want to Delete?"
other = "Confirm deletion?"
[AccessDenied]
other = "Access Denied"
@@ -281,22 +281,22 @@ other = "Don't Have an Account?"
other = "HTTP(S)/SSL Certificate"
[TCPPort]
other = "TCP Port"
other = "TCPing"
[DeleteService]
other = "Remove Service Monitor"
[ConfirmToDeleteThisService]
other = "Confirm to Delete This Service?"
other = "Confirm Deleting This Service?"
[DeleteNotificationMethod]
other = "Delete Notification Method"
[ConfirmToDeleteThisNotificationMethod]
other = "Confirm to Delete This Notification Method?"
other = "Confirm Deleting This Notification Method?"
[ForceUpdate]
other = "Force to Update"
other = "Trigger Agent Update"
[SelectAll]
other = "Select All"
@@ -305,7 +305,7 @@ other = "Select All"
other = "Version"
[OneKeyInstall]
other = "One-Command Install"
other = "Installation commands"
[ClickToCopy]
other = "Click To Copy"
@@ -314,7 +314,7 @@ other = "Click To Copy"
other = "Delete Server"
[ConfirmToDeleteServer]
other = "Confirm to Delete Server?"
other = "Confirm Deleting This Server?"
[NoServerSelected]
other = "No Server Selected"
@@ -545,7 +545,7 @@ other = "Scheduled Tasks"
other = "API Management"
[IssueNewApiToken]
other = "Issue New API Token"
other = "Generate New API Token"
[Token]
other = "Token"
@@ -554,10 +554,10 @@ other = "Token"
other = "Delete Token"
[ConfirmToDeleteThisToken]
other = "Confirm to Delete This Token?"
other = "Confirm Deleting This Token?"
[YouAreNotAuthorized]
other = "Not Authorized for This Page"
other = "This Page needs Authorization"
[WrongAccessPassword]
other = "Wrong Access Password"
@@ -605,13 +605,13 @@ other = "IP Changed"
other = "Remaining Data"
[DashboardTheme]
other = "Admin Panel Theme"
other = "Dashboard Theme"
[Info]
other = "Info"
[HideForGuest]
other = "Hide for Guest"
other = "Hide from Guest"
[Menu]
other = "Menu"
@@ -622,20 +622,62 @@ other = "Network"
[EnableShowInService]
other = "Enable Show in Service"
[DDNS]
other = "Dynamic DNS"
[DDNSProfiles]
other = "DDNS Profiles"
[AddDDNSProfile]
other = "New Profile"
[EnableDDNS]
other = "Enable DDNS"
[EnableIPv4]
other = "Enable DDNS IPv4"
other = "IPv4 Enabled"
[EnableIpv6]
other = "Enable DDNS IPv6"
[EnableIPv6]
other = "IPv6 Enabled"
[DDNSDomain]
other = "DDNS Domain"
other = "Domains"
[DDNSProfile]
other = "DDNS Profile Name"
[DDNSDomains]
other = "Domains (separate with comma)"
[DDNSProvider]
other = "DDNS Provider"
[MaxRetries]
other = "Maximum retry attempts"
[DDNSAccessID]
other = "Credential 1"
[DDNSAccessSecret]
other = "Credential 2"
[DDNSTokenID]
other = "Token ID"
[DDNSTokenSecret]
other = "Token Secret"
[WebhookURL]
other = "Webhook URL"
[WebhookMethod]
other = "Webhook Request Method"
[WebhookRequestType]
other = "Webhook Request Type"
[WebhookHeaders]
other = "Webhook Request Headers"
[WebhookRequestBody]
other = "Webhook Request Body"
[Feature]
other = "Feature"
@@ -705,3 +747,9 @@ other = "Expired"
[Days]
other = "d"
[CustomNameservers]
other = "Custom Public DNS Nameservers for DDNS (separate with comma)"
[Plan]
other = "Plan"
+58 -10
View File
@@ -71,7 +71,7 @@ other = "Ignorar todo, ejecutar solo a través de servidores específicos"
other = "Incluir todo, excepto servidores específicos"
[ExecuteByTriggerServer]
other = "Ejecutar por servidor disparador"
other = "Ejecutar en servidores con alarma"
[SpecificServers]
other = "Servidores Específicos"
@@ -104,7 +104,7 @@ other = "Segundos, Minutos, Horas, Días, Meses, Semanas - Ver detalles a"
other = "Formato de Expresión de Cron"
[IntroductionOfCommands]
other = "Introducción de Comandos: Escriba comandos como en scripts shell/bat. Evite saltos de línea; conecte múltiples comandos con <code>&&</code> o <code>&</code>. Si no se encuentra un comando, verifique la variable de entorno <code>PATH</code>. En servidores <code>Linux</code>, antepóngale <code>source ~/.bashrc</code> a los comandos, o use rutas absolutas."
other = "Introducción de Comandos: Escriba comandos como en scripts shell/bat. Evite saltos de línea; conecte múltiples comandos con <code>&&</code> o <code>;</code>. Si no se encuentra un comando, verifique la variable de entorno <code>PATH</code>. En servidores <code>Linux</code>, antepóngale <code>source ~/.bashrc</code> a los comandos, o use rutas absolutas."
[AddMonitor]
other = "Agregar Monitor de Servicio"
@@ -254,7 +254,7 @@ other = "Cubrir Todo"
other = "Ignorar Todo"
[ByTrigger]
other = "Ejecutar por Disparador"
other = "Servidores con alarma"
[DeleteScheduledTask]
other = "Eliminar Tarea Programada"
@@ -281,7 +281,7 @@ other = "¿No tiene una cuenta?"
other = "Certificados HTTP(S)/SSL"
[TCPPort]
other = "Puerto TCP"
other = "TCPing"
[DeleteService]
other = "Eliminar Servicio"
@@ -622,20 +622,62 @@ other = "Red"
[EnableShowInService]
other = "Mostrar en servicio"
[DDNS]
other = "DNS Dinámico"
[DDNSProfiles]
other = "Perfiles DDNS"
[AddDDNSProfile]
other = "Nuevo Perfil"
[EnableDDNS]
other = "Habilitar DDNS"
[EnableIPv4]
other = "Habilitar DDNS IPv4"
other = "IPv4 Activado"
[EnableIpv6]
other = "Habilitar DDNS IPv6"
[EnableIPv6]
other = "IPv6 Activado"
[DDNSDomain]
other = "Dominio DDNS"
other = "Dominios"
[DDNSProfile]
other = "Nombre del perfil de DDNS"
[DDNSDomains]
other = "Dominios (separados por comas)"
[DDNSProvider]
other = "Proveedor DDNS"
[MaxRetries]
other = "Número máximo de intentos de reintento"
[DDNSAccessID]
other = "Credencial 1"
[DDNSAccessSecret]
other = "Credencial 2"
[DDNSTokenID]
other = "ID del Token"
[DDNSTokenSecret]
other = "Secreto del Token"
[WebhookURL]
other = "URL del Webhook"
[WebhookMethod]
other = "Método de Solicitud del Webhook"
[WebhookRequestType]
other = "Tipo de solicitud del Webhook"
[WebhookHeaders]
other = "Encabezados de Solicitud del Webhook"
[WebhookRequestBody]
other = "Cuerpo de Solicitud del Webhook"
[Feature]
other = "Característica"
@@ -705,3 +747,9 @@ other = "Expired"
[Days]
other = "d"
[CustomNameservers]
other = "Servidores DNS públicos personalizados para DDNS (separar con coma)"
[Plan]
other = "Plan"
+60 -12
View File
@@ -71,7 +71,7 @@ other = "忽略所有,仅通过特定服务器执行"
other = "覆盖所有,仅特定服务器不执行"
[ExecuteByTriggerServer]
other = "由触发的服务器执行"
other = "由触发告警的服务器执行"
[SpecificServers]
other = "特定服务器"
@@ -104,7 +104,7 @@ other = "秒 分 时 天 月 星期,详情见"
other = "计划表达式格式"
[IntroductionOfCommands]
other = "命令说明:编写命令时类似于 shell/bat 脚本。建议不要换行,多个命令可用 <code>&&</code> 或 <code>&</code> 连接,若出现命令无法找到的情况,可能是由于 <code>PATH</code> 环境变量配置问题。在 <code>Linux</code> 服务器上,可在命令开头加入 <code>source ~/.bashrc</code>,或使用命令的绝对路径执行。"
other = "命令说明:编写命令时类似于 shell/bat 脚本。建议不要换行,多个命令可用 <code>&&</code> 或 <code>;</code> 连接,若出现命令无法找到的情况,可能是由于 <code>PATH</code> 环境变量配置问题。在 <code>Linux</code> 服务器上,可在命令开头加入 <code>source ~/.bashrc</code>,或使用命令的绝对路径执行。"
[AddMonitor]
other = "新增监控"
@@ -254,7 +254,7 @@ other = "覆盖所有"
other = "忽略所有"
[ByTrigger]
other = "触发执行"
other = "触发告警的服务器"
[DeleteScheduledTask]
other = "删除计划任务"
@@ -281,7 +281,7 @@ other = "没有账号?"
other = "HTTP(S)/SSL证书"
[TCPPort]
other = "TCP 端口"
other = "TCPing"
[DeleteService]
other = "移除服务"
@@ -425,7 +425,7 @@ other = "活动"
other = "版本"
[NetSpeed]
other = "网"
other = "网"
[Uptime]
other = "在线"
@@ -622,20 +622,62 @@ other = "网络"
[EnableShowInService]
other = "在服务中显示"
[DDNS]
other = "动态 DNS"
[DDNSProfiles]
other = "DDNS配置"
[AddDDNSProfile]
other = "新配置"
[EnableDDNS]
other = "启用DDNS"
other = "启用 DDNS"
[EnableIPv4]
other = "启用DDNS IPv4"
other = "启用 DDNS IPv4"
[EnableIpv6]
other = "启用DDNS IPv6"
[EnableIPv6]
other = "启用 DDNS IPv6"
[DDNSDomain]
other = "DDNS域名"
other = "DDNS 域名"
[DDNSProfile]
other = "DDNS配置名"
[DDNSDomains]
other = "域名(逗号分隔)"
[DDNSProvider]
other = "DDNS 供应商"
[MaxRetries]
other = "最大重试次数"
[DDNSAccessID]
other = "DDNS 凭据 1"
[DDNSAccessSecret]
other = "DDNS 凭据 2"
[DDNSTokenID]
other = "令牌 ID"
[DDNSTokenSecret]
other = "令牌 Secret"
[WebhookURL]
other = "Webhook 地址"
[WebhookMethod]
other = "Webhook 请求方式"
[WebhookRequestType]
other = "Webhook 请求类型"
[WebhookHeaders]
other = "Webhook 请求头"
[WebhookRequestBody]
other = "Webhook 请求体"
[Feature]
other = "功能"
@@ -705,3 +747,9 @@ other = "已到期"
[Days]
other = "天"
[CustomNameservers]
other = "自定义DDNS使用的公共DNS服务器(逗号分隔)"
[Plan]
other = "套餐"
+60 -12
View File
@@ -71,7 +71,7 @@ other = "忽略所有,僅通過特定伺服器執行"
other = "覆蓋所有,僅特定伺服器不執行"
[ExecuteByTriggerServer]
other = "由觸發的伺服器執行"
other = "由觸發告警的伺服器執行"
[SpecificServers]
other = "特定伺服器"
@@ -104,7 +104,7 @@ other = "秒 分 時 天 月 星期,詳情見"
other = "排程表達式格式"
[IntroductionOfCommands]
other = "命令說明:編寫命令時類似於 shell/bat 腳本。建議不要換行,多個命令可用 <code>&&</code> 或 <code>&</code> 連接,若出現命令無法找到的情況,可能是由於 <code>PATH</code> 環境變數配置問題。在 <code>Linux</code> 伺服器上,可在命令開頭加入 <code>source ~/.bashrc</code>,或使用命令的絕對路徑執行。"
other = "命令說明:編寫命令時類似於 shell/bat 腳本。建議不要換行,多個命令可用 <code>&&</code> 或 <code>;</code> 連接,若出現命令無法找到的情況,可能是由於 <code>PATH</code> 環境變數配置問題。在 <code>Linux</code> 伺服器上,可在命令開頭加入 <code>source ~/.bashrc</code>,或使用命令的絕對路徑執行。"
[AddMonitor]
other = "新增監控"
@@ -254,7 +254,7 @@ other = "覆蓋所有"
other = "忽略所有"
[ByTrigger]
other = "觸發執行"
other = "觸發告警的伺服器"
[DeleteScheduledTask]
other = "刪除排程任務"
@@ -281,7 +281,7 @@ other = "沒有帳號?"
other = "HTTP(S)/SSL證書"
[TCPPort]
other = "TCP 端口"
other = "TCPing"
[DeleteService]
other = "移除服務"
@@ -425,7 +425,7 @@ other = "活動"
other = "版本"
[NetSpeed]
other = "網"
other = "網"
[Uptime]
other = "在線"
@@ -622,20 +622,62 @@ other = "網路"
[EnableShowInService]
other = "在服務中顯示"
[DDNS]
other = "動態 DNS"
[DDNSProfiles]
other = "DDNS配置"
[AddDDNSProfile]
other = "新增配置"
[EnableDDNS]
other = "啟用DDNS"
other = "啟用 DDNS"
[EnableIPv4]
other = "啟用DDNS IPv4"
other = "啟用 DDNS IPv4"
[EnableIpv6]
other = "啟用DDNS IPv6"
[EnableIPv6]
other = "啟用 DDNS IPv6"
[DDNSDomain]
other = "DDNS網域"
other = "DDNS 域名"
[DDNSProfile]
other = "DDNS設定名"
[DDNSDomains]
other = "域名(逗號分隔)"
[DDNSProvider]
other = "DDN S供應商"
[MaxRetries]
other = "最大重試次數"
[DDNSAccessID]
other = "DDNS 憑據 1"
[DDNSAccessSecret]
other = "DDNS 憑據 2"
[DDNSTokenID]
other = "令牌 ID"
[DDNSTokenSecret]
other = "令牌 Secret"
[WebhookURL]
other = "Webhook 地址"
[WebhookMethod]
other = "Webhook 請求方式"
[WebhookRequestType]
other = "Webhook 請求類型"
[WebhookHeaders]
other = "Webhook 請求頭"
[WebhookRequestBody]
other = "Webhook 請求體"
[Feature]
other = "功能"
@@ -705,3 +747,9 @@ other = "已到期"
[Days]
other = "天"
[CustomNameservers]
other = "自訂DDNS使用的公共DNS伺服器(逗號分隔)"
[Plan]
other = "套餐"
+13 -1
View File
@@ -2,10 +2,14 @@ package resource
import (
"embed"
"github.com/naiba/nezha/pkg/utils"
)
var StaticFS *utils.HybridFS
//go:embed static
var StaticFS embed.FS
var staticFS embed.FS
//go:embed template
var TemplateFS embed.FS
@@ -13,6 +17,14 @@ var TemplateFS embed.FS
//go:embed l10n
var I18nFS embed.FS
func init() {
var err error
StaticFS, err = utils.NewHybridFS(staticFS, "static", "resource/static/custom")
if err != nil {
panic(err)
}
}
func IsTemplateFileExist(name string) bool {
_, err := TemplateFS.Open(name)
return err == nil
+110 -16
View File
@@ -99,7 +99,11 @@ function showFormModal(modelSelector, formID, URL, getData) {
item.name === "DisplayIndex" ||
item.name === "Type" ||
item.name === "Cover" ||
item.name === "Duration"
item.name === "Duration" ||
item.name === "MaxRetries" ||
item.name === "Provider" ||
item.name === "WebhookMethod" ||
item.name === "WebhookRequestType"
) {
obj[item.name] = parseInt(item.value);
} else if (item.name.endsWith("Latency")) {
@@ -128,6 +132,16 @@ function showFormModal(modelSelector, formID, URL, getData) {
}
}
if (item.name.endsWith("DDNSProfilesRaw")) {
if (item.value.length > 2) {
obj[item.name] = JSON.stringify(
[...item.value.matchAll(/\d+/gm)].map((k) =>
parseInt(k[0])
)
);
}
}
return obj;
}, {});
$.post(URL, JSON.stringify(data))
@@ -207,6 +221,7 @@ function addOrEditAlertRule(rule) {
);
}
}
// 需要在 showFormModal 进一步拼接数组
modal
.find("input[name=FailTriggerTasksRaw]")
.val(rule ? "[]," + failTriggerTasks.substr(1, failTriggerTasks.length - 2) : "[]");
@@ -256,6 +271,55 @@ function addOrEditNotification(notification) {
);
}
function addOrEditDDNS(ddns) {
const modal = $(".ddns.modal");
modal.children(".header").text((ddns ? LANG.Edit : LANG.Add));
modal
.find(".nezha-primary-btn.button")
.html(
ddns
? LANG.Edit + '<i class="edit icon"></i>'
: LANG.Add + '<i class="add icon"></i>'
);
modal.find("input[name=ID]").val(ddns ? ddns.ID : null);
modal.find("input[name=Name]").val(ddns ? ddns.Name : null);
modal.find("input[name=DomainsRaw]").val(ddns ? ddns.DomainsRaw : null);
modal.find("input[name=AccessID]").val(ddns ? ddns.AccessID : null);
modal.find("input[name=AccessSecret]").val(ddns ? ddns.AccessSecret : null);
modal.find("input[name=MaxRetries]").val(ddns ? ddns.MaxRetries : 3);
modal.find("input[name=WebhookURL]").val(ddns ? ddns.WebhookURL : null);
modal
.find("textarea[name=WebhookHeaders]")
.val(ddns ? ddns.WebhookHeaders : null);
modal
.find("textarea[name=WebhookRequestBody]")
.val(ddns ? ddns.WebhookRequestBody : null);
modal
.find("select[name=Provider]")
.val(ddns ? ddns.Provider : 0);
modal
.find("select[name=WebhookMethod]")
.val(ddns ? ddns.WebhookMethod : 1);
modal
.find("select[name=WebhookRequestType]")
.val(ddns ? ddns.WebhookRequestType : 1);
if (ddns && ddns.EnableIPv4) {
modal.find(".ui.enableipv4.checkbox").checkbox("set checked");
} else {
modal.find(".ui.enableipv4.checkbox").checkbox("set unchecked");
}
if (ddns && ddns.EnableIPv6) {
modal.find(".ui.enableipv6.checkbox").checkbox("set checked");
} else {
modal.find(".ui.enableipv6.checkbox").checkbox("set unchecked");
}
showFormModal(
".ddns.modal",
"#ddnsForm",
"/api/ddns"
);
}
function addOrEditNAT(nat) {
const modal = $(".nat.modal");
modal.children(".header").text((nat ? LANG.Edit : LANG.Add));
@@ -325,8 +389,33 @@ function addOrEditServer(server, conf) {
modal.find("input[name=id]").val(server ? server.ID : null);
modal.find("input[name=name]").val(server ? server.Name : null);
modal.find("input[name=Tag]").val(server ? server.Tag : null);
modal.find("input[name=DDNSDomain]").val(server ? server.DDNSDomain : null);
modal.find("input[name=DDNSProfile]").val(server ? server.DDNSProfile : null);
modal.find("a.ui.label.visible").each((i, el) => {
el.remove();
});
var ddns;
if (server) {
ddns = server.DDNSProfilesRaw;
let serverList;
try {
serverList = JSON.parse(ddns);
} catch (error) {
serverList = "[]";
}
const node = modal.find("i.dropdown.icon.ddnsProfiles");
for (let i = 0; i < serverList.length; i++) {
node.after(
'<a class="ui label transition visible" data-value="' +
serverList[i] +
'" style="display: inline-block !important;">ID:' +
serverList[i] +
'<i class="delete icon"></i></a>'
);
}
}
// 需要在 showFormModal 进一步拼接数组
modal
.find("input[name=DDNSProfilesRaw]")
.val(server ? "[]," + ddns.substr(1, ddns.length - 2) : "[]");
modal
.find("input[name=DisplayIndex]")
.val(server ? server.DisplayIndex : null);
@@ -342,26 +431,17 @@ function addOrEditServer(server, conf) {
modal.find(".command.field").attr("style", "display:none");
modal.find("input[name=secret]").val("");
}
if (server && server.HideForGuest) {
modal.find(".ui.hideforguest.checkbox").checkbox("set checked");
} else {
modal.find(".ui.hideforguest.checkbox").checkbox("set unchecked");
}
if (server && server.EnableDDNS) {
modal.find(".ui.enableddns.checkbox").checkbox("set checked");
} else {
modal.find(".ui.enableddns.checkbox").checkbox("set unchecked");
}
if (server && server.EnableIPv4) {
modal.find(".ui.enableipv4.checkbox").checkbox("set checked");
if (server && server.HideForGuest) {
modal.find(".ui.hideforguest.checkbox").checkbox("set checked");
} else {
modal.find(".ui.enableipv4.checkbox").checkbox("set unchecked");
}
if (server && server.EnableIpv6) {
modal.find(".ui.enableipv6.checkbox").checkbox("set checked");
} else {
modal.find(".ui.enableipv6.checkbox").checkbox("set unchecked");
modal.find(".ui.hideforguest.checkbox").checkbox("set unchecked");
}
showFormModal(".server.modal", "#serverForm", "/api/server");
}
@@ -447,6 +527,7 @@ function addOrEditMonitor(monitor) {
);
}
}
// 需要在 showFormModal 进一步拼接数组
modal
.find("input[name=FailTriggerTasksRaw]")
.val(monitor ? "[]," + failTriggerTasks.substr(1, failTriggerTasks.length - 2) : "[]");
@@ -492,6 +573,7 @@ function addOrEditCron(cron) {
);
}
}
// 需要在 showFormModal 进一步拼接数组
modal
.find("input[name=ServersRaw]")
.val(cron ? "[]," + servers.substr(1, servers.length - 2) : "[]");
@@ -621,3 +703,15 @@ $(document).ready(() => {
});
} catch (error) { }
});
$(document).ready(() => {
try {
$(".ui.ddns.search.dropdown").dropdown({
clearable: true,
apiSettings: {
url: "/api/search-ddns?word={query}",
cache: false,
},
});
} catch (error) { }
});
+17 -14
View File
@@ -9,7 +9,7 @@ body[theme="dark"] .navbar .navbar-brand {
}
body[theme="dark"] .navbar .navbar-nav li.pc-active a,
body[theme="dark"] .navbar .navbar-nav li.m-active a{
body[theme="dark"] .navbar .navbar-nav li.m-active a {
color: rgba(73, 146, 255, 1);
}
@@ -32,7 +32,7 @@ body[theme="dark"] .navbar .dropdown-menu > li > a:hover {
body[theme="dark"] .navbar .navbar-nav > .open > a:focus,
body[theme="dark"] .navbar .navbar-nav > .open > a:hover,
body[theme="dark"] .navbar .navbar-nav > .active > a,
body[theme="dark"] .navbar .navbar-nav > .open > a{
body[theme="dark"] .navbar .navbar-nav > .open > a {
background-image: none;
box-shadow: none;
background-color: transparent;
@@ -46,8 +46,6 @@ body[theme="dark"] .content {
background-color: rgba(28, 29, 38, 1);
border: none;
box-shadow: rgba(0, 0, 0, 0.5) 0 0.625em 2em;
-webkit-box-shadow: rgba(0, 0, 0, 0.5) 0 0.625em 2em;
box-shadow: rgba(0, 0, 0, 0.5) 0 0.625em 2em;
}
body[theme="dark"] .table {
@@ -58,7 +56,7 @@ body[theme="dark"] .table {
body[theme="dark"] .table > tbody > tr > td:before,
body[theme="dark"] .table > tfoot > tr > td:before,
body[theme="dark"] .table > thead > tr > td:before,
body[theme="dark"] .table > thead > tr.node-group-cell > th:before{
body[theme="dark"] .table > thead > tr.node-group-cell > th:before {
background-color: rgba(58, 62, 65, 1);
}
@@ -74,7 +72,7 @@ body[theme="dark"] .table-striped tbody > tr.odd > th {
background-color: rgba(34, 35, 46, 1);
}
body[theme="dark"] .table-hover > tbody > tr:hover{
body[theme="dark"] .table-hover > tbody > tr:hover {
background-color: unset;
}
@@ -83,13 +81,18 @@ body[theme="dark"] .table-hover > tbody > tr:not(.expandRow):hover > td {
}
/* expandRow展开部分样式 */
body[theme="dark"] .table > tbody > tr.expandRow.odd > td:before{
body[theme="dark"] .table > tbody > tr.expandRow.odd > td:before {
background-color: rgba(34, 35, 46, 1);
}
body[theme="dark"] .table > tbody > tr.expandRow.even > td:before{
body[theme="dark"] .table > tbody > tr.expandRow.even > td:before {
background-color: rgba(28, 29, 38, 1);
}
body[theme="dark"] .plan {
background-image: none;
background-color: rgba(255, 255, 255, 0.075);
}
/* expandRow展开部分样式结束 */
body[theme="dark"] .progress {
@@ -155,29 +158,29 @@ body[theme="dark"] footer p a:hover {
text-decoration: none;
}
body[theme="dark"] .modal-content{
body[theme="dark"] .modal-content {
background-color: rgba(28, 29, 38, 1);
color: #f1f1f1;
}
body[theme="dark"] #earthChartBox .modal-content{
body[theme="dark"] #earthChartBox .modal-content {
background-color: rgba(0, 0, 0, 1);
color: #f1f1f1;
}
body[theme="dark"] .modal-header{
body[theme="dark"] .modal-header {
border-bottom-color:#080808;
}
body[theme="dark"] .modal-header i.xclose{
body[theme="dark"] .modal-header i.xclose {
color: #f1f1f1;
}
body[theme="dark"] .toolbox i{
body[theme="dark"] .toolbox i {
color: rgba(241, 241, 241, 1);
background-color: rgba(0, 0, 0, 0.5);
}
body[theme="dark"] .network-box .network-box-header{
body[theme="dark"] .network-box .network-box-header {
border-bottom-color: rgba(110, 112, 121, 0.25);
}
+3 -5
View File
@@ -74,8 +74,6 @@ body[theme="dark"] .content {
background-color: rgba(28, 29, 38, 0.8);
border: none;
box-shadow: rgba(0, 0, 0, 0.5) 0 0.625em 2em;
-webkit-box-shadow: rgba(0, 0, 0, 0.5) 0 0.625em 2em;
box-shadow: rgba(0, 0, 0, 0.5) 0 0.625em 2em;
}
body[theme="dark"] .table > thead > tr.node-group-tag > th,
@@ -86,7 +84,7 @@ body[theme="dark"] .table > thead > tr.node-group-tag > th:before {
body[theme="dark"] .table > tbody > tr > td:before,
body[theme="dark"] .table > tfoot > tr > td:before,
body[theme="dark"] .table > thead > tr > td:before,
body[theme="dark"] .table > thead > tr.node-group-cell > th:before{
body[theme="dark"] .table > thead > tr.node-group-cell > th:before {
/** border-bottom 颜色 **/
background-color: rgba(155, 155, 155, 0.1);
}
@@ -96,11 +94,11 @@ body[theme="dark"] .table-hover > tbody > tr:not(.expandRow):hover > td {
}
/* expandRow展开部分样式 */
body[theme="dark"] .table > tbody > tr.expandRow.odd > td:before{
body[theme="dark"] .table > tbody > tr.expandRow.odd > td:before {
background-color: unset;
}
body[theme="dark"] .table > tbody > tr.expandRow.even > td:before{
body[theme="dark"] .table > tbody > tr.expandRow.even > td:before {
background-color: unset;
}
/* expandRow展开部分样式结束 */
+18 -14
View File
@@ -47,16 +47,16 @@ body[theme="light"] .navbar .navbar-nav .open .dropdown-menu > li > a {
color: rgba(0, 0, 0, 0.87);
}
body[theme="light"] .navbar .navbar-toggle{
body[theme="light"] .navbar .navbar-toggle {
border:none;
padding:9px 12px;
}
body[theme="light"] .navbar .navbar-collapse{
body[theme="light"] .navbar .navbar-collapse {
border-color:rgba(0, 0, 0, 0.085);
}
body[theme="light"] .navbar .navbar-toggle .icon-bar{
body[theme="light"] .navbar .navbar-toggle .icon-bar {
background-color: rgba(0, 0, 0, 0.87);
}
@@ -69,8 +69,6 @@ body[theme="light"] .content {
background: rgba(255, 255, 255, 1);
color: rgba(0, 0, 0, 0.87);
border: 1px #cecece solid;
-webkit-box-shadow: 0 1px 10px rgba(0, 0, 0, .1);
-moz-box-shadow: 0 1px 10px rgba(0, 0, 0, .1);
box-shadow: 0 1px 10px rgba(0, 0, 0, .1);
}
@@ -95,12 +93,12 @@ body[theme="light"] .table-striped tbody > tr.odd > th {
body[theme="light"] .table > tbody > tr > td:before,
body[theme="light"] .table > tfoot > tr > td:before,
body[theme="light"] .table > thead > tr > td:before,
body[theme="light"] .table > thead > tr.node-group-cell > th:before{
body[theme="light"] .table > thead > tr.node-group-cell > th:before {
background-color: rgba(221, 221, 221, 1);
}
body[theme="light"] .table > tbody > tr.expandRow > td:before,
body[theme="light"] .table > thead > tr.node-group-tag > th:before{
body[theme="light"] .table > thead > tr.node-group-tag > th:before {
background-color: rgba(255, 255, 255, 1);
}
@@ -116,6 +114,12 @@ body[theme="light"] tr.odd.expandRow > :hover {
background: #ffffff !important;
}
body[theme="light"] .plan {
color: #000000;
background-color: #f5f5f5;
box-shadow: inset 0 -0.5px 2px rgba(0, 0, 0, .1);
}
body[theme="light"] .progress-bar {
color: #000000;
}
@@ -174,35 +178,35 @@ body[theme="light"] footer p a:hover {
text-decoration: none;
}
body[theme="light"] .toolbox i{
body[theme="light"] .toolbox i {
color: rgba(94, 114, 228, .9);
background-color: rgba(255, 255, 255, .5);
box-shadow:0 .125rem .25rem rgba(0, 0, 0, .15) !important;
}
body[theme="light"] .toolbox .mytoggle .tooltip-inner{
body[theme="light"] .toolbox .mytoggle .tooltip-inner {
background: #ffffff;
box-shadow: 0 .125rem .25rem rgba(0, 0, 0, .15) !important;
}
body[theme="light"] .toolbox .mytoggle li:hover{
body[theme="light"] .toolbox .mytoggle li:hover {
background-color:#e6e6e6;
}
body[theme="light"] .modal-content{
body[theme="light"] .modal-content {
background-color: rgba(249, 249, 249, 1);
color: rgba(0, 0, 0, 0.87);
}
body[theme="light"] .modal-header{
body[theme="light"] .modal-header {
border-bottom-color:#f1f1f1;
}
body[theme="light"] .modal-header i.xclose{
body[theme="light"] .modal-header i.xclose {
color: rgba(0, 0, 0, 0.87);
}
body[theme="light"] .network-box .network-box-header{
body[theme="light"] .network-box .network-box-header {
border-bottom-color: rgba(224, 230, 241, 0.6);
}
+12 -7
View File
@@ -36,16 +36,16 @@ body[theme="light"] .navbar .dropdown-menu {
body[theme="light"] .navbar .dropdown-menu > li > a:focus,
body[theme="light"] .navbar .dropdown-menu > li > a:hover{
body[theme="light"] .navbar .dropdown-menu > li > a:hover {
/** 二级导航鼠标悬停选中背景 **/
background-color: rgba(0, 0, 0, .05);
}
body[theme="light"] .navbar .navbar-collapse{
body[theme="light"] .navbar .navbar-collapse {
border-color:rgba(0, 0, 0, 0.085);
}
body[theme="light"] .navbar .navbar-toggle .icon-bar{
body[theme="light"] .navbar .navbar-toggle .icon-bar {
background-color: rgba(0, 0, 0, 0.9);
}
@@ -87,7 +87,7 @@ body[theme="light"] .table > thead > tr.node-group-tag > th:before {
body[theme="light"] .table > tbody > tr > td:before,
body[theme="light"] .table > tfoot > tr > td:before,
body[theme="light"] .table > thead > tr > td:before,
body[theme="light"] .table > thead > tr.node-group-cell > th:before{
body[theme="light"] .table > thead > tr.node-group-cell > th:before {
/** border-bottom 颜色 **/
background-color: rgba(0, 0, 0, 0.075);
}
@@ -97,13 +97,18 @@ body[theme="light"] .table-hover > tbody > tr:not(.expandRow):hover > td {
}
/* expandRow展开部分样式 */
body[theme="light"] .table > tbody > tr.expandRow.odd > td:before{
body[theme="light"] .table > tbody > tr.expandRow.odd > td:before {
background-color: unset;
}
body[theme="light"] .table > tbody > tr.expandRow.even > td:before{
body[theme="light"] .table > tbody > tr.expandRow.even > td:before {
background-color: unset;
}
body[theme="light"] .plan {
background-image: none;
background-color: rgba(0, 0, 0, 0.015);
}
/* expandRow展开部分样式结束 */
body[theme="light"] .progress {
@@ -123,7 +128,7 @@ body[theme="light"] .progress-bar-danger {
background-image: linear-gradient(to right, rgba(255, 70, 0, 0.6) 0, rgba(201, 48, 44,0.6) 100%);
}
body[theme="light"] .toolbox i{
body[theme="light"] .toolbox i {
background-color: rgba(255, 255, 255, .55);
}
+59 -36
View File
@@ -268,6 +268,22 @@ tr.accordion-toggle{
font-size: 80%;
}
.plan {
display: inline-block;
font-size: 85%;
margin-right: 2px;
padding: 2px 5px;
border-radius: 2px;
}
.network-route, .extra {
margin-right: 6px;
}
.last {
margin-right: 2px;
}
.temp-detail {
cursor: pointer;
}
@@ -340,11 +356,11 @@ td.ping-network-quality {
/* 服务页 正文结束 */
/* 网络页 正文*/
.network-box .btn-group.open .dropdown-toggle{
.network-box .btn-group.open .dropdown-toggle {
box-shadow:unset;
}
.network-box .network-box-header{
.network-box .network-box-header {
font-size: 18px;
padding: 5px 0px 15px 5px;
cursor: pointer;
@@ -365,12 +381,12 @@ td.ping-network-quality {
display: none;
}
.network-box .network-box-header .dropdown-menu li{
.network-box .network-box-header .dropdown-menu li {
height: 2em;
cursor: pointer;
}
.network-box .network-box-header .dropdown-menu li.input-group{
.network-box .network-box-header .dropdown-menu li.input-group {
margin: 0 auto;
display: flex;
position: sticky;
@@ -378,7 +394,7 @@ td.ping-network-quality {
z-index: 99999997;
}
.network-box .network-box-header .dropdown-menu li.input-group input{
.network-box .network-box-header .dropdown-menu li.input-group input {
width: 100%;
height: 2em;
border-top: none;
@@ -407,7 +423,7 @@ td.ping-network-quality {
margin: 18px 0px 15px 0px;
}
.network-box .chartTitle i.chartCountryCode{
.network-box .chartTitle i.chartCountryCode {
font-size: 16px;
border-radius: 12.5%;
}
@@ -415,17 +431,17 @@ td.ping-network-quality {
/* 网络页 正文结束 */
/* 地图版服务器分布图 */
#mapChartBox{
#mapChartBox {
z-index: 999999999;
padding-left: 0 !important;
}
.modal-open .modal{
.modal-open .modal {
overflow-x: hidden;
overflow-y: hidden;
}
.modal-dialog-centered{
.modal-dialog-centered {
margin: 0 auto;
width: 100%;
max-width: 100%;
@@ -435,7 +451,7 @@ td.ping-network-quality {
min-height: calc(100%) /* 确保模态框占据整个视口高度 */
}
.modal-content{
.modal-content {
display: flex;
flex-direction: column;
pointer-events: auto;
@@ -443,15 +459,15 @@ td.ping-network-quality {
border: none;
}
.modal-header{
.modal-header {
position: relative;
}
.modal-header h4{
.modal-header h4 {
cursor: pointer;
}
.modal-header i.xclose{
.modal-header i.xclose {
cursor: pointer;
position: absolute;
font-size: 30px;
@@ -459,32 +475,32 @@ td.ping-network-quality {
right: 10px;
}
.modal-body{
.modal-body {
padding: 0px;
}
a.template-item, a.cdn-item{
a.template-item, a.cdn-item {
display: block;
}
footer{
footer {
padding-bottom: 20px;
}
footer p{
footer p {
text-align: center;
font-size: 10px;
}
@media only screen and (max-width: 1200px) {
.accordian-body{
.accordian-body {
margin: 5px 0px 5px 10px;
}
table.monitor-detail{
table.monitor-detail {
margin:0px 0px 2px 0px;
}
.table .network-thead-tr th,
.table .node-group-tag th{
.table .node-group-tag th {
font-size:16px;
padding-bottom:6px;
}
@@ -512,10 +528,10 @@ footer p{
padding: 0;
margin-bottom: 10px;
}
footer{
footer {
padding-bottom: 10px;
}
.cycle-transfer-stats .dataTables_wrapper{
.cycle-transfer-stats .dataTables_wrapper {
margin-bottom: -10px;
}
.navbar .navbar-nav .open .dropdown-menu>li>a {
@@ -537,54 +553,61 @@ footer p{
}
.node-cell.os,
.node-cell.uptime,
.node-cell.traffic{
.node-cell.traffic {
display: none;
visibility: hidden;
}
.node-cell.location{
.node-cell.location {
display: table-cell;
visibility: visible;
}
.node-cell-os-text,.node-cell-location-text{
.node-cell-os-text,.node-cell-location-text {
display: none;
visibility: hidden;
}
.node-cell.network {
min-width: 95px;
max-width: 95px;
}
.node-cell.remaining {
min-width: 75px;
max-width: 75px;
}
.accordian-body{
.plan {
display: inline;
}
.accordian-body {
margin: 5px 0px 5px 10px;
}
table.monitor-detail{
table.monitor-detail {
margin:0px 0px 2px 0px;
}
.table .network-thead-tr th,
.table .node-group-tag th{
.table .node-group-tag th {
font-size:14px;
padding-bottom:6px;
}
.table tr.network td{
.table tr.network td {
font-size: 12px;
}
.service-status .service-status-th{
.service-status .service-status-th {
min-width:30px;
}
.service-status .delay-today{
.service-status .delay-today {
margin-top:4px;
justify-content: center;
}
.service-status .delay-today i{
.service-status .delay-today i {
margin-right:0px;
}
.service-status .delay-today-text{
.service-status .delay-today-text {
display: none;
visibility: hidden;
}
.service-status .service-averagelatency-th{
.service-status .service-averagelatency-th {
min-width:70px;
}
.service-status .service-30daysonline-th{
.service-status .service-30daysonline-th {
min-width:75px;
}
.toolbox {
@@ -602,7 +625,7 @@ footer p{
font-size: 16px;
margin: 10px 0px 10px 0px;
}
.network-box .chartTitle i.chartCountryCode{
.network-box .chartTitle i.chartCountryCode {
font-size: 15px;
border-radius: 12.5%;
}
+1 -1
View File
@@ -10,7 +10,7 @@
<script src="https://unpkg.com/[email protected]/dist/semantic.min.js"></script>
<script src="/static/semantic-ui-alerts.min.js"></script>
<script src="https://unpkg.com/[email protected]/dist/vue.min.js"></script>
<script src="/static/main.js?v2024927"></script>
<script src="/static/main.js?v20241011"></script>
<script>
(function () {
updateLang({{.LANG }});
+1
View File
@@ -9,6 +9,7 @@
<a class='item{{if eq .MatchedPath "/monitor"}} active{{end}}' href="/monitor"><i class="rss icon"></i>{{tr "Services"}}</a>
<a class='item{{if eq .MatchedPath "/cron"}} active{{end}}' href="/cron"><i class="clock icon"></i>{{tr "Task"}}</a>
<a class='item{{if eq .MatchedPath "/notification"}} active{{end}}' href="/notification"><i class="bell icon"></i>{{tr "Notification"}}</a>
<a class='item{{if eq .MatchedPath "/ddns"}} active{{end}}' href="/ddns"><i class="globe icon"></i>{{tr "DDNS"}}</a>
<a class='item{{if eq .MatchedPath "/nat"}} active{{end}}' href="/nat"><i class="exchange icon"></i>{{tr "NAT"}}</a>
<a class='item{{if eq .MatchedPath "/setting"}} active{{end}}' href="/setting">
<i class="settings icon"></i>{{tr "Settings"}}
+86
View File
@@ -0,0 +1,86 @@
{{define "component/ddns"}}
<div class="ui tiny ddns modal transition hidden">
<div class="header">Add</div>
<div class="content">
<form id="ddnsForm" class="ui form">
<input type="hidden" name="ID">
<div class="field">
<label>{{tr "Name"}}</label>
<input type="text" name="Name">
</div>
<div class="field">
<label>{{tr "DDNSProvider"}}</label>
<select name="Provider" class="ui fluid dropdown" id="providerSelect" onchange="toggleFields()">
{{ range $provider := .ProviderList }}
<option value="{{ $provider.ID }}">
{{ $provider.Name }}
</option>
{{ end }}
</select>
</div>
<div class="field">
<label>{{tr "DDNSDomains"}}</label>
<input type="text" name="DomainsRaw" placeholder="www.example.com">
</div>
<div class="field">
<label>{{tr "DDNSAccessID"}}</label>
<input type="text" name="AccessID" placeholder="{{tr "DDNSTokenID"}}">
</div>
<div class="field">
<label>{{tr "DDNSAccessSecret"}}</label>
<input type="text" name="AccessSecret" placeholder="{{tr "DDNSTokenSecret"}}">
</div>
<div class="field">
<label>{{tr "MaxRetries"}}</label>
<input type="number" name="MaxRetries" placeholder="3">
</div>
<div class="field">
<label>{{tr "WebhookURL"}}</label>
<input type="text" name="WebhookURL" placeholder="https://ddns.example.com/?record=#record#">
</div>
<div class="field">
<label>{{tr "WebhookMethod"}}</label>
<select name="WebhookMethod" class="ui fluid dropdown">
<option value="1">GET</option>
<option value="2">POST</option>
<option value="3">PATCH</option>
<option value="4">DELETE</option>
<option value="5">PUT</option>
</select>
</div>
<div class="field">
<label>{{tr "WebhookRequestType"}}</label>
<select name="WebhookRequestType" class="ui fluid dropdown">
<option value="1">JSON</option>
<option value="2">Form</option>
</select>
</div>
<div class="field">
<label>{{tr "WebhookHeaders"}}</label>
<textarea name="WebhookHeaders" placeholder='{"User-Agent":"Nezha-Agent"}'></textarea>
</div>
<div class="field">
<label>{{tr "WebhookRequestBody"}}</label>
<textarea name="WebhookRequestBody" placeholder='{&#13;&#10; "ip": #ip#,&#13;&#10; "domain": "#domain#"&#13;&#10;}'></textarea>
</div>
<div class="field">
<div class="ui enableipv4 checkbox">
<input name="EnableIPv4" type="checkbox" tabindex="0" class="hidden">
<label>{{tr "EnableIPv4"}}</label>
</div>
</div>
<div class="field">
<div class="ui enableipv6 checkbox">
<input name="EnableIPv6" type="checkbox" tabindex="0" class="hidden">
<label>{{tr "EnableIPv6"}}</label>
</div>
</div>
</form>
</div>
<div class="actions">
<div class="ui negative button">{{tr "Cancel"}}</div>
<button class="ui positive nezha-primary-btn right labeled icon button">{{tr "Confirm"}}<i class="checkmark icon"></i>
</button>
</div>
</div>
{{end}}
+11 -22
View File
@@ -21,37 +21,26 @@
<input type="text" name="secret">
</div>
<div class="field">
<div class="ui hideforguest checkbox">
<input name="HideForGuest" type="checkbox" tabindex="0" class="hidden" />
<label>{{tr "HideForGuest"}}</label>
<label>{{tr "DDNSProfiles"}}</label>
<div class="ui fluid multiple ddns search selection dropdown">
<input type="hidden" name="DDNSProfilesRaw">
<i class="dropdown icon ddnsProfiles"></i>
<div class="default text">{{tr "EnterIdAndNameToSearch"}}</div>
<div class="menu"></div>
</div>
</div>
<div class="field">
<div class="ui enableddns checkbox">
<input name="EnableDDNS" type="checkbox" tabindex="0" />
<input name="EnableDDNS" type="checkbox" tabindex="0" class="hidden" />
<label>{{tr "EnableDDNS"}}</label>
</div>
</div>
<div class="field">
<div class="ui enableipv4 checkbox">
<input name="EnableIPv4" type="checkbox" tabindex="0" />
<label>{{tr "EnableIPv4"}}</label>
<div class="ui hideforguest checkbox">
<input name="HideForGuest" type="checkbox" tabindex="0" class="hidden" />
<label>{{tr "HideForGuest"}}</label>
</div>
</div>
<div class="field">
<div class="ui enableipv6 checkbox">
<input name="EnableIpv6" type="checkbox" tabindex="0" />
<label>{{tr "EnableIpv6"}}</label>
</div>
</div>
<div class="field">
<label>{{tr "DDNSDomain"}}</label>
<input type="text" name="DDNSDomain" placeholder="{{tr "DDNSDomain"}}">
</div>
<div class="field">
<label>{{tr "DDNSProfile"}}</label>
<input type="text" name="DDNSProfile" placeholder="{{tr "DDNSProfile"}}">
</div>
<div class="field">
<label>{{tr "Note"}}</label>
<textarea name="Note"></textarea>
@@ -64,7 +53,7 @@
<label>{{tr "LinuxOneKeyInstall"}}</label>
<div class="ui message">
{{if .Conf.GRPCHost}}
curl -L https://raw.githubusercontent.com/naiba/nezha/master/script/install.sh -o nezha.sh && chmod
curl -L https://raw.githubusercontent.com/nezhahq/scripts/main/install.sh -o nezha.sh && chmod
+x nezha.sh && ./nezha.sh install_agent <code class="command">{{.Conf.GRPCHost}}</code> <code
class="command">{{if .Conf.ProxyGRPCPort}}{{.Conf.ProxyGRPCPort}}{{else}}{{.Conf.GRPCPort}}{{end}}</code> <code
class="command hostSecret"></code> <code class="command">{{if .Conf.TLS}}--tls{{end}}</code>
+58
View File
@@ -0,0 +1,58 @@
{{define "dashboard-default/ddns"}}
{{template "common/header" .}}
{{template "common/menu" .}}
<div class="nb-container">
<div class="ui container">
<div class="ui grid">
<div class="right floated right aligned twelve wide column">
<button class="ui right labeled nezha-primary-btn icon button" onclick="addOrEditDDNS()"><i
class="add icon"></i> {{tr "AddDDNSProfile"}}
</button>
</div>
</div>
<table class="ui basic table">
<thead>
<tr>
<th>ID</th>
<th>{{tr "Name"}}</th>
<th>{{tr "EnableIPv4"}}</th>
<th>{{tr "EnableIPv6"}}</th>
<th>{{tr "DDNSProvider"}}</th>
<th>{{tr "DDNSDomain"}}</th>
<th>{{tr "MaxRetries"}}</th>
<th>{{tr "Administration"}}</th>
</tr>
</thead>
<tbody>
{{range $item := .DDNS}}
<tr>
<td>{{$item.ID}}</td>
<td>{{$item.Name}}</td>
<td>{{$item.EnableIPv4}}</td>
<td>{{$item.EnableIPv6}}</td>
<td>{{index $.ProviderMap $item.Provider}}</td>
<td>{{$item.DomainsRaw}}</td>
<td>{{$item.MaxRetries}}</td>
<td>
<div class="ui mini icon buttons">
<button class="ui button" onclick="addOrEditDDNS({{$item}})">
<i class="edit icon"></i>
</button>
<button class="ui button"
onclick="showConfirm('确定删除DDNS配置?','确认删除',deleteRequest,'/api/ddns/'+{{$item.ID}})">
<i class="trash alternate outline icon"></i>
</button>
</div>
</td>
</tr>
{{end}}
</tbody>
</table>
</div>
</div>
{{template "component/ddns" .}}
{{template "common/footer" .}}
<script>
$('.checkbox').checkbox()
</script>
{{end}}
+7 -7
View File
@@ -28,8 +28,8 @@
<th>{{tr "ServerGroup"}}</th>
<th>IP</th>
<th>{{tr "VersionNumber"}}</th>
<th>{{tr "HideForGuest"}}</th>
<th>{{tr "EnableDDNS"}}</th>
<th>{{tr "HideForGuest"}}</th>
<th>{{tr "Secret"}}</th>
<th>{{tr "OneKeyInstall"}}</th>
<th>{{tr "Note"}}</th>
@@ -46,8 +46,8 @@
<td>{{$server.Tag}}</td>
<td>{{$server.Host.IP}}</td>
<td>{{$server.Host.Version}}</td>
<td>{{$server.HideForGuest}}</td>
<td>{{$server.EnableDDNS}}</td>
<td>{{$server.HideForGuest}}</td>
<td>
<button class="ui icon green mini button" data-clipboard-text="{{$server.Secret}}" data-tooltip="{{tr "ClickToCopy"}}">
<i class="copy icon"></i>
@@ -55,17 +55,17 @@
</td>
<td>
<button class="ui icon green mini button"
data-clipboard-text="{{if $.Conf.GRPCHost}}{{if eq $.Conf.Language "zh-CN"}}curl -L https://raw.githubusercontent.com/naiba/nezha/master/script/install.sh{{else}}curl -L https://raw.githubusercontent.com/naiba/nezha/master/script/install_en.sh{{end}} -o nezha.sh && chmod +x nezha.sh && ./nezha.sh install_agent {{$.Conf.GRPCHost}} {{if $.Conf.ProxyGRPCPort}}{{$.Conf.ProxyGRPCPort}}{{else}}{{$.Conf.GRPCPort}}{{end}} {{$server.Secret}}{{if $.Conf.TLS}} --tls{{end}}{{else}}{{tr "NoDomainAlert"}}{{end}}"
data-clipboard-text="{{if $.Conf.GRPCHost}}{{if eq $.Conf.Language "zh-CN"}}curl -L https://raw.githubusercontent.com/nezhahq/scripts/main/install.sh{{else}}curl -L https://raw.githubusercontent.com/nezhahq/scripts/main/install_en.sh{{end}} -o nezha.sh && chmod +x nezha.sh && ./nezha.sh install_agent {{$.Conf.GRPCHost}} {{if $.Conf.ProxyGRPCPort}}{{$.Conf.ProxyGRPCPort}}{{else}}{{$.Conf.GRPCPort}}{{end}} {{$server.Secret}}{{if $.Conf.TLS}} --tls{{end}}{{else}}{{tr "NoDomainAlert"}}{{end}}"
data-tooltip="{{tr "ClickToCopy"}}">
<i class="linux icon"></i>
</button>
<button class="ui icon green mini button"
data-clipboard-text="{{if $.Conf.GRPCHost}}[Net.ServicePointManager]::SecurityProtocol = [Net.SecurityProtocolType]::Ssl3 -bor [Net.SecurityProtocolType]::Tls -bor [Net.SecurityProtocolType]::Tls11 -bor [Net.SecurityProtocolType]::Tls12;set-ExecutionPolicy RemoteSigned;Invoke-WebRequest https://raw.githubusercontent.com/naiba/nezha/master/script/install.ps1 -OutFile C:\install.ps1;powershell.exe C:\install.ps1 {{$.Conf.GRPCHost}}:{{if $.Conf.ProxyGRPCPort}}{{$.Conf.ProxyGRPCPort}}{{else}}{{$.Conf.GRPCPort}}{{end}} {{$server.Secret}}{{if $.Conf.TLS}} --tls{{end}}{{else}}{{tr "NoDomainAlert"}}{{end}}"
data-clipboard-text="{{if $.Conf.GRPCHost}}[Net.ServicePointManager]::SecurityProtocol = [Net.SecurityProtocolType]::Ssl3 -bor [Net.SecurityProtocolType]::Tls -bor [Net.SecurityProtocolType]::Tls11 -bor [Net.SecurityProtocolType]::Tls12;set-ExecutionPolicy RemoteSigned;Invoke-WebRequest https://raw.githubusercontent.com/nezhahq/scripts/main/extras/install.ps1 -OutFile C:\install.ps1;powershell.exe C:\install.ps1 {{$.Conf.GRPCHost}}:{{if $.Conf.ProxyGRPCPort}}{{$.Conf.ProxyGRPCPort}}{{else}}{{$.Conf.GRPCPort}}{{end}} {{$server.Secret}}{{if $.Conf.TLS}} --tls{{end}}{{else}}{{tr "NoDomainAlert"}}{{end}}"
data-tooltip="{{tr "ClickToCopy"}}">
<i class="windows icon"></i>
</button>
<button class="ui icon green mini button"
data-clipboard-text="{{if $.Conf.GRPCHost}}curl -L https://raw.githubusercontent.com/naiba/nezha/master/script/install.command -o nezha.command && chmod +x nezha.command && sudo ./nezha.command install_agent {{$.Conf.GRPCHost}} {{if $.Conf.ProxyGRPCPort}}{{$.Conf.ProxyGRPCPort}}{{else}}{{$.Conf.GRPCPort}}{{end}} {{$server.Secret}}{{if $.Conf.TLS}} --tls{{end}}{{else}}{{tr "NoDomainAlert"}}{{end}}"
data-clipboard-text="{{if $.Conf.GRPCHost}}curl -L https://raw.githubusercontent.com/nezhahq/scripts/main/extras/install.command -o nezha.command && chmod +x nezha.command && sudo ./nezha.command install_agent {{$.Conf.GRPCHost}} {{if $.Conf.ProxyGRPCPort}}{{$.Conf.ProxyGRPCPort}}{{else}}{{$.Conf.GRPCPort}}{{end}} {{$server.Secret}}{{if $.Conf.TLS}} --tls{{end}}{{else}}{{tr "NoDomainAlert"}}{{end}}"
data-tooltip="{{tr "ClickToCopy"}}">
<i class="apple icon"></i>
</button>
@@ -159,8 +159,8 @@
}
function batchEditServerGroup() {
let groupName = prompt('{{tr "InputServerGroupName"}}')
if (!groupName) {
return;
if (groupName === null) {
return
}
const servers = []
checkBoxList.forEach(cb => {
+4
View File
@@ -53,6 +53,10 @@
<label>{{tr "PanelServerDomainAndIP"}}</label>
<input type="text" name="GRPCHost" placeholder="" value="{{.Conf.GRPCHost}}">
</div>
<div class="field">
<label>{{tr "CustomNameservers"}}</label>
<input type="text" name="CustomNameservers" placeholder="" value="{{.Conf.DNSServers}}">
</div>
<div class="field">
<label>{{tr "IPChangeAlert"}}</label>
</div>
+1 -1
View File
@@ -13,7 +13,7 @@
<script src="https://lf6-cdn-tos.bytecdntp.com/cdn/expire-1-y/semantic-ui/2.4.1/semantic.min.js"></script>
<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?v20240330"></script>
<script src="/static/main.js?v20241011"></script>
<script src="/static/theme-default/js/mixin.js?v20240302"></script>
<script>
(function () {
+2 -2
View File
@@ -28,8 +28,8 @@
</span>
</aside>
<template v-if="semiTransparent">
<link rel="stylesheet" href="/static/theme-server-status/css/light.plus.css?v20240807">
<link rel="stylesheet" href="/static/theme-server-status/css/dark.plus.css?v20240807">
<link rel="stylesheet" href="/static/theme-server-status/css/light.plus.css?v20241103">
<link rel="stylesheet" href="/static/theme-server-status/css/dark.plus.css?v20241103">
</template>
</div>
{{if ts .CustomCode}}{{.CustomCode|safe}}{{end}}
+3 -3
View File
@@ -26,9 +26,9 @@
<script src="https://unpkg.com/[email protected]/dist/js/bootstrap.min.js"></script>
<script src="https://unpkg.com/[email protected]/dist/vue.min.js"></script>
<script src="https://unpkg.com/[email protected]/dist/echarts.min.js"></script>
<link rel="stylesheet" href="/static/theme-server-status/css/main.css?v20240929">
<link rel="stylesheet" href="/static/theme-server-status/css/dark.css?v202409029">
<link rel="stylesheet" href="/static/theme-server-status/css/light.css?v20240916">
<link rel="stylesheet" href="/static/theme-server-status/css/main.css?v20241103">
<link rel="stylesheet" href="/static/theme-server-status/css/dark.css?v20241103">
<link rel="stylesheet" href="/static/theme-server-status/css/light.css?v20241103">
<script src="/static/theme-server-status/js/mixin.js?v20240915"></script>
</head>
<body>
+60 -23
View File
@@ -6,7 +6,7 @@
<th class="node-cell name center">{{tr "Name"}}</th>
<th class="node-cell os center">{{tr "Platform"}}</th>
<th class="node-cell location center">{{tr "Location"}}</th>
<th v-if="additional && Object.values(additional).filter(item => item.price).length > 0" class="node-cell price center">{{tr "Price"}}</th>
<th v-if="nodesNoTag.some(item => item.additional && item.additional.price && Object.keys(item.additional.price).length > 0)" class="node-cell price center">{{tr "Price"}}</th>
<th class="node-cell uptime center">{{tr "Uptime"}}</th>
<th class="node-cell load center">{{tr "Load"}}</th>
<th class="node-cell network center">{{tr "NetSpeed"}}↓|↑</th>
@@ -14,7 +14,7 @@
<th class="node-cell cpu center">{{tr "CpuUsed"}}</th>
<th class="node-cell memory center">{{tr "MemUsed"}}</th>
<th class="node-cell hdd center">{{tr "DiskUsed"}}</th>
<th v-if="additional && Object.values(additional).filter(item => item.remaining).length > 0" class="node-cell remaining center">{{tr "Remaining"}}</th>
<th v-if="nodesNoTag.some(item => item.additional && item.additional.remaining && Object.keys(item.additional.remaining).length > 0)" class="node-cell remaining center">{{tr "Remaining"}}</th>
</tr>
</thead>
<tbody id="servers">
@@ -32,16 +32,17 @@
<i v-if='isWindowsPlatform(node.host.Platform)' class="windows icon"></i>
<i v-else-if='getFontLogoClass(node.host.Platform) == "" && node.stateuptime > 0' class="fl-tux"></i>
<i v-else :class="'fl-' + getFontLogoClass(node.host.Platform)"></i>
<span class="node-cell-os-text">@#getPlatformName(node.os)#@</span>
<span class="node-cell-os-text">@#getPlatformName(node.os) === '' && node.stateuptime > 0 ? 'linux' : getPlatformName(node.os)#@</span>
</td>
<td style="text-align: center;" class="node-cell location">
<i :class="'fi fi-' + node.location"></i>
<span class="node-cell-location-text text-uppercase">@#node.location#@</span>
<td class="node-cell location center">
<i :class="'fi fi-' + (node.stateuptime > 0 ? (node.location || 'un') : '')"></i>
<span class="node-cell-location-text text-uppercase">@#node.stateuptime > 0 ? (node.location || 'UN') : ''#@</span>
</td>
<td v-if="additional && Object.values(additional).filter(item => item.price).length > 0" class="node-cell price center">
<template v-if="additional[node.ID] && additional[node.ID].price">
<span v-if="additional[node.ID].price.amount == 0" class="node-cell-price-text">Free</span>
<span v-else class="node-cell-price-text">@#additional[node.ID].price.amount#@@#(additional[node.ID].price.cycle ? '/' + additional[node.ID].price.cycle : '')#@</span>
<td v-if="nodesNoTag.some(item => item.additional && item.additional.price && Object.keys(item.additional.price).length > 0)" class="node-cell price center">
<template v-if="node.additional && node.additional.price">
<span v-if="node.additional.price.amount == 0" class="node-cell-price-text">FREE</span>
<span v-else-if="node.additional.price.amount == -1" class="node-cell-price-text">PAYG</span>
<span v-else class="node-cell-price-text">@#node.additional.price.amount#@@#(node.additional.price.cycle ? '/' + node.additional.price.cycle : '')#@</span>
</template>
</td>
<td style="text-align: center;" class="node-cell uptime">@#node.uptime#@</td>
@@ -67,16 +68,16 @@
</div>
</div>
</td>
<td v-if="additional && Object.values(additional).filter(item => item.remaining).length > 0" class="node-cell remaining">
<td v-if="nodesNoTag.some(item => item.additional && item.additional.remaining && Object.keys(item.additional.remaining).length > 0)" class="node-cell remaining">
<div :class="['progress', node.online ? 'progress-online' : 'progress-offline']">
<template v-if="additional[node.ID] && additional[node.ID].remaining.format">
<div :style="additional[node.ID].remaining.format.style" :class="additional[node.ID].remaining.days == 'lifetime' ? '' : additional[node.ID].remaining.format.class"></div>
<template v-if="node.additional && node.additional.remaining.format">
<div :style="node.additional.remaining.format.style" :class="node.additional.remaining.days == 'lifetime' ? '' : node.additional.remaining.format.class"></div>
<div class="additional">
<small v-if="additional[node.ID].remaining.days == 'lifetime'">{{tr "Lifetime"}}</small>
<small v-else-if="additional[node.ID].remaining.days < 0">{{tr "Expired"}}</small>
<small v-if="node.additional.remaining.days == 'lifetime'">{{tr "Lifetime"}}</small>
<small v-else-if="node.additional.remaining.days < 0">{{tr "Expired"}}</small>
<small v-else>
<span class="node-cell-remaining-days">@#additional[node.ID].remaining.days#@{{tr "Days"}}</span>
<span class="node-cell-remaining-percent">@#additional[node.ID].remaining.percent#@%</span>
<span class="node-cell-remaining-days">@#node.additional.remaining.days#@{{tr "Days"}}</span>
<span class="node-cell-remaining-percent">@#node.additional.remaining.percent#@%</span>
</small>
</div>
</template>
@@ -87,10 +88,49 @@
<td colspan="16">
<div class="accordian-body collapse" :id="'rt'+node.ID">
<div style="display: flex;left-items: center;justify-content: center;flex-direction: column; max-width: 89vw">
<span v-if="node.additional && Object.keys(node.additional.plan).length > 0" class="node-cell-expand">
<span class="node-cell-expand-label">{{tr "Plan"}}:</span>
<span v-if="node.additional && Object.keys(node.additional.price).length > 0" class="plan price">
<span v-if="node.additional.price.amount == 0">FREE</span>
<span v-else-if="node.additional.price.amount == -1">PAYG</span>
<span v-else><i class="bi bi-cash-stack"></i> @#node.additional.price.amount#@@#(node.additional.price.cycle ? '/' + node.additional.price.cycle : '')#@</span>
</span>
<span v-if="node.additional && node.additional.remaining.endDate" class="plan enddate">
<span v-if="node.additional.remaining.days == 'lifetime'">{{tr "Lifetime"}}</span>
<span v-else-if="node.additional.remaining.days < 0">{{tr "Expired"}}</span>
<span v-else><i class="bi bi-clock-history"></i> @#node.additional.remaining.endDate.toISOString().split('T')[0]#@</span>
</span>
<span v-if="node.additional && node.additional.plan.bandwidth" class="plan bandwidth">
<i class="bi bi-speedometer2"></i>
<span>@#node.additional.plan.bandwidth#@</span>
</span>
<span v-if="node.additional && node.additional.plan.trafficVol" class="plan traffics">
<i v-if="node.additional && node.additional.plan.trafficType == 1" class="bi bi-arrow-up"></i>
<i v-else-if="node.additional && node.additional.plan.trafficType == 3" class="bi bi-arrows-collapse"></i>
<i v-else class="bi bi-arrow-down-up"></i>
<span>@#node.additional.plan.trafficVol#@</span>
</span>
<span v-if="node.additional && node.additional.plan.ipv4" class="plan ipv4">
<span>IPv4</span>
</span>
<span v-if="node.additional && node.additional.plan.ipv6" class="plan ipv6">
<span>IPv6</span>
</span>
<template v-if="node.additional && node.additional.plan.networkRoute.length>0" v-for="(item, index) in node.additional.plan.networkRoute" :key="index">
<span class="plan network-route" :class="{ last: index === node.additional.plan.networkRoute.length - 1 }">
<span>@#item#@</span>
</span>
</template>
<template v-if="node.additional && node.additional.plan.extra.length>0" v-for="(item, index) in node.additional.plan.extra" :key="index">
<span class="plan extra" :class="{ last: index === node.additional.plan.extra.length - 1 }">
<span>@#item#@</span>
</span>
</template>
</span>
<span class="node-cell-expand">
<span class="node-cell-expand-label">{{tr "Platform"}}:</span>
@#node.host.Platform#@-@#node.host.PlatformVersion#@
[<span v-if="node.host.Virtualization">@#node.host.Virtualization#@:</span>@#node.host.Arch#@]
<span v-if="node.host.Platform">@#node.host.Platform#@@#node.host.PlatformVersion ? '-' + node.host.PlatformVersion : ''#@</span>
<span v-if="node.host.Arch">[@#node.host.Virtualization ? node.host.Virtualization + ':' : ''#@@#node.host.Arch#@]</span>
</span>
<span class="node-cell-expand" v-if="node.host.CPU">
<span class="node-cell-expand-label">CPU:</span>
@@ -119,10 +159,7 @@
</span>
<span class="node-cell-expand">
<span class="node-cell-expand-label">{{tr "NetTransfer"}}:</span>
<i class="arrow alternate circle down outline icon"
style="margin: 0"></i>@#formatByteSize(node.state.NetInTransfer)#@
<i class="arrow alternate circle up outline icon"
style="margin: 0"></i>@#formatByteSize(node.state.NetOutTransfer)#@
IN @#formatByteSize(node.state.NetInTransfer)#@ / OUT @#formatByteSize(node.state.NetOutTransfer)#@
</span>
<span class="node-cell-expand load">
<span class="node-cell-expand-label">{{tr "Load"}}:</span>
+60 -23
View File
@@ -9,7 +9,7 @@
<th class="node-cell name center">{{tr "Name"}}</th>
<th class="node-cell os center">{{tr "Platform"}}</th>
<th class="node-cell location center">{{tr "Location"}}</th>
<th v-if="additional && Object.values(additional).filter(item => item.price).length > 0" class="node-cell price center">{{tr "Price"}}</th>
<th v-if="group.data.some(item => item.additional && item.additional.price && Object.keys(item.additional.price).length > 0)" class="node-cell price center">{{tr "Price"}}</th>
<th class="node-cell uptime center">{{tr "Uptime"}}</th>
<th class="node-cell load center">{{tr "Load"}}</th>
<th class="node-cell network center">{{tr "NetSpeed"}}↓|↑</th>
@@ -17,7 +17,7 @@
<th class="node-cell cpu center">{{tr "CpuUsed"}}</th>
<th class="node-cell memory center">{{tr "MemUsed"}}</th>
<th class="node-cell hdd center">{{tr "DiskUsed"}}</th>
<th v-if="additional && Object.values(additional).filter(item => item.remaining).length > 0" class="node-cell remaining center">{{tr "Remaining"}}</th>
<th v-if="group.data.some(item => item.additional && item.additional.remaining && Object.keys(item.additional.remaining).length > 0)" class="node-cell remaining center">{{tr "Remaining"}}</th>
</tr>
</thead>
<tbody id="servers">
@@ -35,16 +35,17 @@
<i v-if='isWindowsPlatform(node.host.Platform)' class="windows icon"></i>
<i v-else-if='getFontLogoClass(node.host.Platform) == "" && node.stateuptime > 0' class="fl-tux"></i>
<i v-else :class="'fl-' + getFontLogoClass(node.host.Platform)"></i>
<span class="node-cell-os-text">@#getPlatformName(node.os)#@</span>
<span class="node-cell-os-text">@#getPlatformName(node.os) === '' && node.stateuptime > 0 ? 'linux' : getPlatformName(node.os)#@</span>
</td>
<td style="text-align: center;" class="node-cell location">
<i :class="'fi fi-' + node.location"></i>
<span class="node-cell-location-text text-uppercase">&nbsp;@#node.location#@</span>
<td class="node-cell location center">
<i :class="'fi fi-' + (node.stateuptime > 0 ? (node.location || 'un') : '')"></i>
<span class="node-cell-location-text text-uppercase">@#node.stateuptime > 0 ? (node.location || 'UN') : ''#@</span>
</td>
<td v-if="additional && Object.values(additional).filter(item => item.price).length > 0" class="node-cell price center">
<template v-if="additional[node.ID] && additional[node.ID].price">
<span v-if="additional[node.ID].price.amount == 0" class="node-cell-price-text">Free</span>
<span v-else class="node-cell-price-text">@#additional[node.ID].price.amount#@@#(additional[node.ID].price.cycle ? '/' + additional[node.ID].price.cycle : '')#@</span>
<td v-if="group.data.some(item => item.additional && item.additional.price && Object.keys(item.additional.price).length > 0)" class="node-cell price center">
<template v-if="node.additional && node.additional.price">
<span v-if="node.additional.price.amount == 0" class="node-cell-price-text">FREE</span>
<span v-else-if="node.additional.price.amount == -1" class="node-cell-price-text">PAYG</span>
<span v-else class="node-cell-price-text">@#node.additional.price.amount#@@#(node.additional.price.cycle ? '/' + node.additional.price.cycle : '')#@</span>
</template>
</td>
<td style="text-align: center;" class="node-cell uptime">@#node.uptime#@</td>
@@ -70,16 +71,16 @@
</div>
</div>
</td>
<td v-if="additional && Object.values(additional).filter(item => item.remaining).length > 0" class="node-cell remaining">
<td v-if="group.data.some(item => item.additional && item.additional.remaining && Object.keys(item.additional.remaining).length > 0)" class="node-cell remaining">
<div :class="['progress', node.online ? 'progress-online' : 'progress-offline']">
<template v-if="additional[node.ID] && additional[node.ID].remaining.format">
<div :style="additional[node.ID].remaining.format.style" :class="additional[node.ID].remaining.days == 'lifetime' ? '' : additional[node.ID].remaining.format.class"></div>
<template v-if="node.additional && node.additional.remaining.format">
<div :style="node.additional.remaining.format.style" :class="node.additional.remaining.days == 'lifetime' ? '' : node.additional.remaining.format.class"></div>
<div class="additional">
<small v-if="additional[node.ID].remaining.days == 'lifetime'">{{tr "Lifetime"}}</small>
<small v-else-if="additional[node.ID].remaining.days < 0">{{tr "Expired"}}</small>
<small v-if="node.additional.remaining.days == 'lifetime'">{{tr "Lifetime"}}</small>
<small v-else-if="node.additional.remaining.days < 0">{{tr "Expired"}}</small>
<small v-else>
<span class="node-cell-remaining-days">@#additional[node.ID].remaining.days#@{{tr "Days"}}</span>
<span class="node-cell-remaining-percent">@#additional[node.ID].remaining.percent#@%</span>
<span class="node-cell-remaining-days">@#node.additional.remaining.days#@{{tr "Days"}}</span>
<span class="node-cell-remaining-percent">@#node.additional.remaining.percent#@%</span>
</small>
</div>
</template>
@@ -90,10 +91,49 @@
<td colspan="16">
<div class="accordian-body collapse" :id="'rt'+node.ID">
<div style="display: flex;left-items: center;justify-content: center;flex-direction: column; max-width: 89vw">
<span v-if="node.additional && Object.keys(node.additional.plan).length > 0" class="node-cell-expand">
<span class="node-cell-expand-label">{{tr "Plan"}}:</span>
<span v-if="node.additional && Object.keys(node.additional.price).length > 0" class="plan price">
<span v-if="node.additional.price.amount == 0">FREE</span>
<span v-else-if="node.additional.price.amount == -1">PAYG</span>
<span v-else><i class="bi bi-cash-stack"></i> @#node.additional.price.amount#@@#(node.additional.price.cycle ? '/' + node.additional.price.cycle : '')#@</span>
</span>
<span v-if="node.additional && node.additional.remaining.endDate" class="plan enddate">
<span v-if="node.additional.remaining.days == 'lifetime'">{{tr "Lifetime"}}</span>
<span v-else-if="node.additional.remaining.days < 0">{{tr "Expired"}}</span>
<span v-else><i class="bi bi-clock-history"></i> @#node.additional.remaining.endDate.toISOString().split('T')[0]#@</span>
</span>
<span v-if="node.additional && node.additional.plan.bandwidth" class="plan bandwidth">
<i class="bi bi-speedometer2"></i>
<span>@#node.additional.plan.bandwidth#@</span>
</span>
<span v-if="node.additional && node.additional.plan.trafficVol" class="plan traffics">
<i v-if="node.additional && node.additional.plan.trafficType == 1" class="bi bi-arrow-up"></i>
<i v-else-if="node.additional && node.additional.plan.trafficType == 3" class="bi bi-arrows-collapse"></i>
<i v-else class="bi bi-arrow-down-up"></i>
<span>@#node.additional.plan.trafficVol#@</span>
</span>
<span v-if="node.additional && node.additional.plan.ipv4" class="plan ipv4">
<span>IPv4</span>
</span>
<span v-if="node.additional && node.additional.plan.ipv6" class="plan ipv6">
<span>IPv6</span>
</span>
<template v-if="node.additional && node.additional.plan.networkRoute.length>0" v-for="(item, index) in node.additional.plan.networkRoute" :key="index">
<span class="plan network-route" :class="{ last: index === node.additional.plan.networkRoute.length - 1 }">
<span>@#item#@</span>
</span>
</template>
<template v-if="node.additional && node.additional.plan.extra.length>0" v-for="(item, index) in node.additional.plan.extra" :key="index">
<span class="plan extra" :class="{ last: index === node.additional.plan.extra.length - 1 }">
<span>@#item#@</span>
</span>
</template>
</span>
<span class="node-cell-expand">
<span class="node-cell-expand-label">{{tr "Platform"}}:</span>
@#node.host.Platform#@-@#node.host.PlatformVersion#@
[<span v-if="node.host.Virtualization">@#node.host.Virtualization#@:</span>@#node.host.Arch#@]
<span v-if="node.host.Platform">@#node.host.Platform#@@#node.host.PlatformVersion ? '-' + node.host.PlatformVersion : ''#@</span>
<span v-if="node.host.Arch">[@#node.host.Virtualization ? node.host.Virtualization + ':' : ''#@@#node.host.Arch#@]</span>
</span>
<span class="node-cell-expand" v-if="node.host.CPU">
<span class="node-cell-expand-label">CPU:</span>
@@ -122,10 +162,7 @@
</span>
<span class="node-cell-expand">
<span class="node-cell-expand-label">{{tr "NetTransfer"}}:</span>
<i class="arrow alternate circle down outline icon"
style="margin: 0"></i>@#formatByteSize(node.state.NetInTransfer)#@
<i class="arrow alternate circle up outline icon"
style="margin: 0"></i>@#formatByteSize(node.state.NetOutTransfer)#@
IN @#formatByteSize(node.state.NetInTransfer)#@ / OUT @#formatByteSize(node.state.NetOutTransfer)#@
</span>
<span class="node-cell-expand load">
<span class="node-cell-expand-label">{{tr "Load"}}:</span>
+123 -32
View File
@@ -72,10 +72,25 @@
mixins: [mixinsVue],
created() {
this.servers = JSON.parse('{{.Servers}}').servers;
this.additional = this.initAdditional(this.servers);
if(this.showGroup) {
this.nodesTag = this.groupingData(this.handleNodes(this.servers),"Tag");
this.nodesTag.forEach(group => {
group.data.forEach(item => {
const additionalData = this.additional[item.ID];
if (additionalData) {
item.additional = additionalData; // 添加 additional 属性
}
});
});
} else {
this.nodesNoTag = this.handleNodes(this.servers);
this.nodesNoTag.forEach(item => {
const additionalData = this.additional[item.ID];
if (additionalData) {
item.additional = additionalData; // 添加 additional 属性
}
});
}
this.initCountryMap().then(countryMap => {
this.countryMap = countryMap;
@@ -83,7 +98,6 @@
this.countryServer = this.initCountryServer();
this.countryMapChartData = this.initCountryMapChartData();
});
this.additional = this.initAdditional(this.servers);
},
mounted() {
// 初始化时建立WebSocket连接
@@ -103,19 +117,54 @@
initAdditional(servers) {
let nodes = {};
servers?.forEach(server => {
if(server.PublicNote) {
nodes[server.ID] = {
"remaining": {
format: this.getRemainingFormat(server.live, server.PublicNote),
days: this.getRemainingDays(this.getNoteElementValue(server.PublicNote, "billingDataMod", "endDate"),server.PublicNote),
percent: this.toFixed2(100 - this.getRemainingPercent(this.getNoteElementValue(server.PublicNote, "billingDataMod", "startDate"), this.getNoteElementValue(server.PublicNote, "billingDataMod", "endDate"), server.PublicNote))
},
"price": {
amount: this.getNoteElementValue(server.PublicNote, "billingDataMod", "amount"),
cycle: this.getNoteElementValue(server.PublicNote, "billingDataMod", "cycle")
}
};
}
//处理异常
if (!server.PublicNote) return;
// 初始化节点
nodes[server.ID] = {
"remaining": {},
"price": {},
"plan": {}
};
// 处理 billingDataMod 的 remaining 配置
const remainingEndDate = this.getRemainingDays(this.getNoteElementValue(server.PublicNote, "billingDataMod", "endDate"), server.PublicNote, 1);
const remainingFormat = this.getRemainingFormat(server.live, server.PublicNote);
const remainingDays = this.getRemainingDays(this.getNoteElementValue(server.PublicNote, "billingDataMod", "endDate"), server.PublicNote);
const remainingPercent = this.getRemainingPercent(
this.getNoteElementValue(server.PublicNote, "billingDataMod", "startDate"),
this.getNoteElementValue(server.PublicNote, "billingDataMod", "endDate"),
server.PublicNote
);
// 设置 remaining 属性
if (remainingEndDate) nodes[server.ID].remaining.endDate = remainingEndDate;
if (remainingFormat) nodes[server.ID].remaining.format = remainingFormat;
if (remainingDays) nodes[server.ID].remaining.days = remainingDays;
if (remainingPercent) nodes[server.ID].remaining.percent = this.toFixed2(100 - remainingPercent);
// 处理 billingDataMod 的 price 配置
const priceAmount = this.getNoteElementValue(server.PublicNote, "billingDataMod", "amount");
const priceCycle = this.getNoteElementValue(server.PublicNote, "billingDataMod", "cycle");
// 设置 price 属性
if (priceAmount) nodes[server.ID].price.amount = priceAmount;
if (priceCycle && priceAmount) nodes[server.ID].price.cycle = priceCycle;
// 处理 planDataMod 配置
const planBandwidth = this.getNoteElementValue(server.PublicNote, "planDataMod", "bandwidth");
const planTrafficVol = this.getNoteElementValue(server.PublicNote, "planDataMod", "trafficVol");
const planTrafficType = this.getNoteElementValue(server.PublicNote, "planDataMod", "trafficType");
const planIPv4 = this.getNoteElementValue(server.PublicNote, "planDataMod", "IPv4");
const planIPv6 = this.getNoteElementValue(server.PublicNote, "planDataMod", "IPv6");
const planNetworkRoute = this.getNoteElementValue(server.PublicNote, "planDataMod", "networkRoute");
const planExtra = this.getNoteElementValue(server.PublicNote, "planDataMod", "extra");
// 设置 plan 属性
if (planBandwidth) nodes[server.ID].plan.bandwidth = planBandwidth;
if (planTrafficVol) nodes[server.ID].plan.trafficVol = planTrafficVol;
if (planTrafficType) nodes[server.ID].plan.trafficType = planTrafficType;
if (planIPv4) nodes[server.ID].plan.ipv4 = planIPv4 >= 1;
if (planIPv6) nodes[server.ID].plan.ipv6 = planIPv6 >= 1;
if (planNetworkRoute) nodes[server.ID].plan.networkRoute = planNetworkRoute.split(',');
if (planExtra) nodes[server.ID].plan.extra = planExtra.split(',');
});
return nodes;
},
@@ -416,8 +465,22 @@
}
if(this.showGroup) {
this.nodesTag = this.groupingData(this.handleNodes(data.servers),"Tag");
this.nodesTag.forEach(group => {
group.data.forEach(item => {
const additionalData = this.additional[item.ID];
if (additionalData) {
item.additional = additionalData; // 添加 additional 属性
}
});
});
} else {
this.nodesNoTag = this.handleNodes(data.servers);
this.nodesNoTag.forEach(item => {
const additionalData = this.additional[item.ID];
if (additionalData) {
item.additional = additionalData; // 添加 additional 属性
}
});
}
}
this.ws.onclose = () => {
@@ -528,7 +591,7 @@
}
})
.catch(error => {
console.error('Error fetching data:', error);
console.log('Error fetching data:', error);
});
},
renderCharts(id, reload = false) {
@@ -767,6 +830,7 @@
switch (cycle) {
case '月':
case 'mo':
case 'month':
case 'monthly':
case 'm':
@@ -782,6 +846,7 @@
case 'h':
return "H";
case '年':
case 'yr':
case 'year':
case 'annually':
case 'y':
@@ -805,9 +870,9 @@
const expiration = new Date(endDate);
const current = this.getAdjustTimezone(new Date(endDate), new Date());
// 如果 expiration 无效,返回 null 并记录日志
// 如果 expiration 无效,记录日志
if (isNaN(expiration.getTime())) {
console.error("getAutoRenewalEndDate: Invalid expiration format");
console.log("getAutoRenewalEndDate: Invalid expiration format");
}
const result = {
@@ -924,7 +989,7 @@
// 检查 startDate 格式是否有效,若无效返回 null
if (isNaN(start.getTime())) {
console.error("getAutoRenewalStartDate: Invalid startDate format");
console.log("getAutoRenewalStartDate: Invalid startDate format");
}
// 如果 flag 为 0,直接返回开始日期
@@ -976,31 +1041,47 @@
if (!note) return null;
const startDate = this.getNoteElementValue(note, "billingDataMod", "startDate");
const endDate = this.getNoteElementValue(note, "billingDataMod", "endDate");
// 检查 startDate 和 endDate 是否有效
if (!startDate || !endDate || typeof startDate !== 'string' || typeof endDate !== 'string') {
console.error("getRemainingFormat: Invalid startDate or endDate in note");
return null; // 如果无效,返回 null 或其他错误处理逻辑
}
//处理特殊时间格式
// 处理特殊时间格式
if (startDate.includes('0000-00-00') || endDate.includes("0000-00-00")) {
return this.formatPercents(online, this.toFixed2(100));
} else {
const percent = this.getRemainingPercent(startDate, endDate, note);
return this.formatPercents(online, this.toFixed2(percent));
}
// 检查 startDate 和 endDate 是否为合法的Date
if (isNaN(new Date(startDate).getTime()) || isNaN(new Date(endDate).getTime())) {
return {
"class": "",
"style": "width: 0%",
"percent": "0"
}
}
//处理特殊时间格式
const percent = this.getRemainingPercent(startDate, endDate, note);
return this.formatPercents(online, this.toFixed2(percent));
},
getRemainingDays(endDate, note) {
getRemainingDays(endDate, note, type) {
// 检查 endDate 是否有效
if (!endDate || typeof endDate !== 'string') {
console.error("getRemainingDays: Invalid endDate format");
return null;
}
// 处理特殊时间格式
if (endDate.includes("0000-00-00")) return "lifetime";
if (endDate.includes("0000-00-00")) {
return "lifetime";
}
// 检查 endDate 是否为合法的Date
if (isNaN(new Date(endDate).getTime())) {
return type === 1 ? null : "NaN";
}
// 获取当前时间,并调整时区
const currentTime = this.getAdjustTimezone(new Date(endDate), new Date());
@@ -1017,6 +1098,9 @@
// 确定到期时间
const end = autoRenewal ? autoEndDate.date : new Date(endDate);
// 直接返回处理后的到期时间
if (type === 1) return end;
// 计算剩余天数
const timeDiff = end - currentTime;
const daysDiff = Math.ceil(timeDiff / (1000 * 60 * 60 * 24));
@@ -1025,13 +1109,20 @@
},
getRemainingPercent(startDate, endDate, note) {
// 检查 startDate 和 endDate 是否为有效字符串并处理特殊格式
if (typeof startDate !== 'string' || typeof endDate !== 'string') {
console.error("getRemainingPercent: Invalid startDate or endDate format");
if (!startDate || !endDate || typeof startDate !== 'string' || typeof endDate !== 'string') {
return null;
}
if (startDate.includes("0000-00-00") || endDate.includes("0000-00-00")) return 100;
// 处理特殊时间格式
if (startDate.includes("0000-00-00") || endDate.includes("0000-00-00")) {
return 100;
}
// 检查 startDate 和 endDate 是否为合法的Date
if (isNaN(new Date(startDate).getTime()) || isNaN(new Date(endDate).getTime())) {
return "NaN";
}
// 获取当前时间并调整时区
const now = this.getAdjustTimezone(new Date(endDate), new Date());
@@ -1056,7 +1147,7 @@
// 计算剩余百分比
if (now < start) {
return 0;
return 0.01; //避免当条件判断!percent被过滤
}
if (now >= end) {
return 100;
+3 -3
View File
@@ -11,11 +11,11 @@
<input type="text" id="dropdown-search" class="form-control" placeholder="Search...">
</li>
<li class="dropdown-item" v-for="server in servers" @click="showCharts(server.ID)">
<a><i :class="'fi fi-' + (server.Host.CountryCode || 'rb')"></i> @#server.Name#@ <i v-if="server.ID == currentServerId" class="check icon"></i></a>
<a><i :class="'fi fi-' + (server.Host.CountryCode || 'un')"></i> @#server.Name#@ <i v-if="server.ID == currentServerId" class="check icon"></i></a>
</li>
</ul>
</div>
<div class="chartTitle" @click="showCharts(nextServerId)"><i class="chartCountryCode" :class="'fi fi-' + chartCountryCode"></i> @#chartTitle#@</div>
<div v-if="chartTitle" class="chartTitle" @click="showCharts(nextServerId)"><i class="chartCountryCode" :class="'fi fi-' + chartCountryCode"></i> @#chartTitle#@</div>
<div id="chartbox" style="width:100%;height:auto;"></div>
</div>
{{template "theme-server-status/footer" .}}
@@ -332,7 +332,7 @@
},
getServerCountryCode(id){
const result = this.servers.find(item => item.ID == id);
return result.Host.CountryCode ? result.Host.CountryCode : 'rb';
return result.Host.CountryCode ? result.Host.CountryCode : 'un';
},
getNextServerId(id) {
const currentIndex = this.servers.findIndex(item => item.ID === id);
-19
View File
@@ -12,22 +12,3 @@ site:
brand: "nz_site_title"
cookiename: "nezha-dashboard" #浏览器 Cookie 字段名,可不改
theme: "default"
ddns:
enable: false
provider: "webhook" # 如需使用多配置功能,请把此项留空
accessid: ""
accesssecret: ""
webhookmethod: ""
webhookurl: ""
webhookrequestbody: ""
webhookheaders: ""
maxretries: 3
profiles:
example:
provider: ""
accessid: ""
accesssecret: ""
webhookmethod: ""
webhookurl: ""
webhookrequestbody: ""
webhookheaders: ""
+1 -1
View File
@@ -51,7 +51,7 @@ if ([string]::IsNullOrWhiteSpace($agenttag)) {
#Region判断
$ipapi = ""
$region = "Unknown"
foreach ($url in ("https://dash.cloudflare.com/cdn-cgi/trace","https://cf-ns.com/cdn-cgi/trace","https://1.0.0.1/cdn-cgi/trace")) {
foreach ($url in ("https://dash.cloudflare.com/cdn-cgi/trace","https://developers.cloudflare.com/cdn-cgi/trace","https://1.0.0.1/cdn-cgi/trace")) {
try {
$ipapi = Invoke-RestMethod -Uri $url -TimeoutSec 5 -UseBasicParsing
if ($ipapi -match "loc=(\w+)" ) {
+123 -101
View File
@@ -12,13 +12,13 @@ NZ_DASHBOARD_PATH="${NZ_BASE_PATH}/dashboard"
NZ_AGENT_PATH="${NZ_BASE_PATH}/agent"
NZ_DASHBOARD_SERVICE="/etc/systemd/system/nezha-dashboard.service"
NZ_DASHBOARD_SERVICERC="/etc/init.d/nezha-dashboard"
NZ_VERSION="v0.19.0"
NZ_VERSION="v0.20.3"
red='\033[0;31m'
green='\033[0;32m'
yellow='\033[0;33m'
plain='\033[0m'
export PATH=$PATH:/usr/local/bin
export PATH="$PATH:/usr/local/bin"
os_arch=""
[ -e /etc/os-release ] && grep -i "PRETTY_NAME" /etc/os-release | grep -qi "alpine" && os_alpine='1'
@@ -45,26 +45,36 @@ check_systemd() {
}
err() {
printf "${red}$*${plain}\n" >&2
printf "${red}%s${plain}\n" "$*" >&2
}
success() {
printf "${green}%s${plain}\n" "$*"
}
info() {
printf "${yellow}%s${plain}\n" "$*"
}
geo_check() {
api_list="https://blog.cloudflare.com/cdn-cgi/trace https://dash.cloudflare.com/cdn-cgi/trace https://cf-ns.com/cdn-cgi/trace"
api_list="https://blog.cloudflare.com/cdn-cgi/trace https://dash.cloudflare.com/cdn-cgi/trace https://developers.cloudflare.com/cdn-cgi/trace"
ua="Mozilla/5.0 (X11; Linux x86_64; rv:60.0) Gecko/20100101 Firefox/81.0"
set -- $api_list
set -- "$api_list"
for url in $api_list; do
text="$(curl -A "$ua" -m 10 -s $url)"
endpoint="$(echo $text | sed -n 's/.*h=\([^ ]*\).*/\1/p')"
if echo $text | grep -qw 'CN'; then
text="$(curl -A "$ua" -m 10 -s "$url")"
endpoint="$(echo "$text" | sed -n 's/.*h=\([^ ]*\).*/\1/p')"
if echo "$text" | grep -qw 'CN'; then
isCN=true
break
elif echo $url | grep -q $endpoint; then
elif echo "$url" | grep -q "$endpoint"; then
break
fi
done
}
pre_check() {
umask 077
## os_arch
if uname -m | grep -q 'x86_64'; then
os_arch="amd64"
@@ -83,9 +93,9 @@ pre_check() {
## China_IP
if [ -z "$CN" ]; then
geo_check
if [ ! -z "$isCN" ]; then
if [ -n "$isCN" ]; then
echo "根据geoip api提供的信息,当前IP可能在中国"
printf "是否选用中国镜像完成安装? [Y/n] (自定义镜像输入 3):"
printf "是否选用中国镜像完成安装? [Y/n] (自定义镜像输入 3)"
read -r input
case $input in
[yY][eE][sS] | [yY])
@@ -178,7 +188,9 @@ installation_check() {
select_version() {
if [ -z "$IS_DOCKER_NEZHA" ]; then
printf "${yellow}请自行选择您的安装方式(如果你是安装Agent,输入哪个都是一样的):\n1. Docker\n2. 独立安装${plain}\n"
info "请自行选择您的安装方式(如果你是安装Agent,输入哪个都是一样的):"
info "1. Docker"
info "2. 独立安装"
while true; do
printf "请输入选择 [1-2]"
read -r option
@@ -202,13 +214,18 @@ select_version() {
update_script() {
echo "> 更新脚本"
curl -sL https://${GITHUB_RAW_URL}/script/install.sh -o /tmp/nezha.sh
new_version=$(grep "NZ_VERSION" /tmp/nezha.sh | head -n 1 | awk -F "=" '{print $2}' | sed 's/\"//g;s/,//g;s/ //g')
if [ ! -n "$new_version" ]; then
echo "脚本获取失败,请检查本机能否链接 https://${GITHUB_RAW_URL}/script/install.sh"
return 1
#curl -sL https://${GITHUB_RAW_URL}/script/install.sh -o /tmp/nezha.sh
#new_version=$(grep "NZ_VERSION" /tmp/nezha.sh | head -n 1 | awk -F "=" '{print $2}' | sed 's/\"//g;s/,//g;s/ //g')
#if [ -z "$new_version" ]; then
# echo "脚本获取失败,请检查本机能否链接 https://${GITHUB_RAW_URL}/script/install.sh"
# return 1
#fi
#echo "当前最新版本为: ${new_version}"
if [ -z "$CN" ]; then
curl -sL https://raw.githubusercontent.com/nezhahq/scripts/main/install.sh -o /tmp/nezha.sh
else
curl -sL https://gitee.com/naibahq/scripts/raw/main/install.sh -o /tmp/nezha.sh
fi
echo "当前最新版本为: ${new_version}"
mv -f /tmp/nezha.sh ./nezha.sh && chmod a+x ./nezha.sh
echo "3s后执行新脚本"
@@ -219,7 +236,7 @@ update_script() {
}
before_show_menu() {
echo && printf "${yellow}* 按回车返回主菜单 *${plain}" && read temp
echo && info "* 按回车返回主菜单 *" && read temp
show_menu
}
@@ -229,7 +246,7 @@ install_base() {
}
install_arch() {
printf "${green}提示: ${plain} Arch安装libselinux需添加nezha-agent用户,安装完会自动删除,建议手动检查一次\n"
info "提示:Arch安装libselinux需添加nezha-agent用户,安装完会自动删除,建议手动检查一次"
read -r -p "是否安装libselinux? [Y/n] " input
case $input in
[yY][eE][sS] | [yY])
@@ -253,11 +270,11 @@ install_arch() {
}
install_soft() {
(command -v yum >/dev/null 2>&1 && sudo yum makecache && sudo yum install $* selinux-policy -y) ||
(command -v apt >/dev/null 2>&1 && sudo apt update && sudo apt install $* selinux-utils -y) ||
(command -v pacman >/dev/null 2>&1 && sudo pacman -Syu $* base-devel --noconfirm && install_arch) ||
(command -v apt-get >/dev/null 2>&1 && sudo apt-get update && sudo apt-get install $* selinux-utils -y) ||
(command -v apk >/dev/null 2>&1 && sudo apk update && sudo apk add $* -f)
(command -v yum >/dev/null 2>&1 && sudo yum makecache && sudo yum install "$@" selinux-policy -y) ||
(command -v apt >/dev/null 2>&1 && sudo apt update && sudo apt install "$@" selinux-utils -y) ||
(command -v pacman >/dev/null 2>&1 && sudo pacman -Syu "$@" base-devel --noconfirm && install_arch) ||
(command -v apt-get >/dev/null 2>&1 && sudo apt-get update && sudo apt-get install "$@" selinux-utils -y) ||
(command -v apk >/dev/null 2>&1 && sudo apk update && sudo apk add "$@" -f)
}
install_dashboard() {
@@ -288,8 +305,6 @@ install_dashboard() {
esac
fi
sudo chmod -R 700 $NZ_DASHBOARD_PATH
if [ "$IS_DOCKER_NEZHA" = 1 ]; then
install_dashboard_docker
elif [ "$IS_DOCKER_NEZHA" = 0 ]; then
@@ -305,12 +320,10 @@ install_dashboard() {
install_dashboard_docker() {
if [ ! "$FRESH_INSTALL" = 0 ]; then
command -v docker >/dev/null 2>&1
if [ $? != 0 ]; then
if ! command -v docker >/dev/null 2>&1; then
echo "正在安装 Docker"
if [ "$os_alpine" != 1 ]; then
curl -sL https://${Get_Docker_URL} | sudo bash -s ${Get_Docker_Argu}
if [ $? != 0 ]; then
if ! curl -sL https://${Get_Docker_URL} | sudo bash -s "${Get_Docker_Argu}"; then
err "下载脚本失败,请检查本机能否连接 ${Get_Docker_URL}"
return 0
fi
@@ -321,7 +334,7 @@ install_dashboard_docker() {
sudo rc-update add docker
sudo rc-service docker start
fi
printf "${green}Docker${plain} 安装成功\n"
success "Docker 安装成功"
installation_check
fi
fi
@@ -334,13 +347,11 @@ install_dashboard_standalone() {
}
selinux() {
#判断当前的状态
command -v getenforce >/dev/null 2>&1
if [ $? -eq 0 ]; then
getenforce | grep '[Ee]nfor'
if [ $? -eq 0 ]; then
#Check SELinux
if command -v getenforce >/dev/null 2>&1; then
if getenforce | grep '[Ee]nfor'; then
echo "SELinux是开启状态,正在关闭!"
sudo setenforce 0 &>/dev/null
sudo setenforce 0 >/dev/null 2>&1
find_key="SELINUX="
sudo sed -ri "/^$find_key/c${find_key}disabled" /etc/selinux/config
fi
@@ -355,36 +366,37 @@ install_agent() {
echo "正在获取监控Agent版本号"
local version=$(curl -m 10 -sL "https://api.github.com/repos/nezhahq/agent/releases/latest" | grep "tag_name" | head -n 1 | awk -F ":" '{print $2}' | sed 's/\"//g;s/,//g;s/ //g')
if [ ! -n "$version" ]; then
version=$(curl -m 10 -sL "https://gitee.com/api/v5/repos/naibahq/agent/releases/latest" | awk -F '"' '{for(i=1;i<=NF;i++){if($i=="tag_name"){print $(i+2)}}}')
_version=$(curl -m 10 -sL "https://api.github.com/repos/nezhahq/agent/releases/latest" | grep "tag_name" | head -n 1 | awk -F ":" '{print $2}' | sed 's/\"//g;s/,//g;s/ //g')
if [ -z "$_version" ]; then
_version=$(curl -m 10 -sL "https://gitee.com/api/v5/repos/naibahq/agent/releases/latest" | awk -F '"' '{for(i=1;i<=NF;i++){if($i=="tag_name"){print $(i+2)}}}')
fi
if [ ! -n "$version" ]; then
version=$(curl -m 10 -sL "https://fastly.jsdelivr.net/gh/nezhahq/agent/" | grep "option\.value" | awk -F "'" '{print $2}' | sed 's/nezhahq\/agent@/v/g')
if [ -z "$_version" ]; then
_version=$(curl -m 10 -sL "https://fastly.jsdelivr.net/gh/nezhahq/agent/" | grep "option\.value" | awk -F "'" '{print $2}' | sed 's/nezhahq\/agent@/v/g')
fi
if [ ! -n "$version" ]; then
version=$(curl -m 10 -sL "https://gcore.jsdelivr.net/gh/nezhahq/agent/" | grep "option\.value" | awk -F "'" '{print $2}' | sed 's/nezhahq\/agent@/v/g')
if [ -z "$_version" ]; then
_version=$(curl -m 10 -sL "https://gcore.jsdelivr.net/gh/nezhahq/agent/" | grep "option\.value" | awk -F "'" '{print $2}' | sed 's/nezhahq\/agent@/v/g')
fi
if [ ! -n "$version" ]; then
if [ -z "$_version" ]; then
err "获取版本号失败,请检查本机能否链接 https://api.github.com/repos/nezhahq/agent/releases/latest"
return 1
else
echo "当前最新版本为: ${version}"
echo "当前最新版本为: ${_version}"
fi
# 哪吒监控文件夹
sudo mkdir -p $NZ_AGENT_PATH
sudo chmod -R 700 $NZ_AGENT_PATH
echo "正在下载监控端"
if [ -z "$CN" ]; then
NZ_AGENT_URL="https://${GITHUB_URL}/nezhahq/agent/releases/download/${version}/nezha-agent_linux_${os_arch}.zip"
NZ_AGENT_URL="https://${GITHUB_URL}/nezhahq/agent/releases/download/${_version}/nezha-agent_linux_${os_arch}.zip"
else
NZ_AGENT_URL="https://${GITHUB_URL}/naibahq/agent/releases/download/${version}/nezha-agent_linux_${os_arch}.zip"
NZ_AGENT_URL="https://${GITHUB_URL}/naibahq/agent/releases/download/${_version}/nezha-agent_linux_${os_arch}.zip"
fi
wget -t 2 -T 60 -O nezha-agent_linux_${os_arch}.zip $NZ_AGENT_URL >/dev/null 2>&1
if [ $? != 0 ]; then
_cmd="wget -t 2 -T 60 -O nezha-agent_linux_${os_arch}.zip $NZ_AGENT_URL >/dev/null 2>&1"
if ! eval "$_cmd"; then
err "Release 下载失败,请检查本机能否连接 ${GITHUB_URL}"
return 1
fi
@@ -436,14 +448,14 @@ modify_agent_config() {
fi
fi
sudo ${NZ_AGENT_PATH}/nezha-agent service install -s "$nz_grpc_host:$nz_grpc_port" -p $nz_client_secret $args >/dev/null 2>&1
_cmd="sudo ${NZ_AGENT_PATH}/nezha-agent service install -s $nz_grpc_host:$nz_grpc_port -p $nz_client_secret $args >/dev/null 2>&1"
if [ $? -ne 0 ]; then
sudo ${NZ_AGENT_PATH}/nezha-agent service uninstall >/dev/null 2>&1
sudo ${NZ_AGENT_PATH}/nezha-agent service install -s "$nz_grpc_host:$nz_grpc_port" -p $nz_client_secret $args >/dev/null 2>&1
if ! eval "$_cmd"; then
sudo "${NZ_AGENT_PATH}"/nezha-agent service uninstall >/dev/null 2>&1
sudo "${NZ_AGENT_PATH}"/nezha-agent service install -s "$nz_grpc_host:$nz_grpc_port" -p "$nz_client_secret" "$args" >/dev/null 2>&1
fi
printf "Agent配置 ${green}修改成功,请稍等重启生效${plain}\n"
success "Agent配置 修改成功,请稍等重启生效"
#if [[ $# == 0 ]]; then
# before_show_menu
@@ -454,16 +466,21 @@ modify_dashboard_config() {
echo "> 修改面板配置"
if [ "$IS_DOCKER_NEZHA" = 1 ]; then
echo "正在下载 Docker 脚本"
wget -t 2 -T 60 -O /tmp/nezha-docker-compose.yaml https://${GITHUB_RAW_URL}/script/docker-compose.yaml >/dev/null 2>&1
if [ $? != 0 ]; then
err "下载脚本失败,请检查本机能否连接 ${GITHUB_RAW_URL}"
return 0
if [ -n "$DOCKER_COMPOSE_COMMAND" ]; then
echo "正在下载 Docker 脚本"
_cmd="wget -t 2 -T 60 -O /tmp/nezha-docker-compose.yaml https://${GITHUB_RAW_URL}/script/docker-compose.yaml >/dev/null 2>&1"
if ! eval "$_cmd"; then
err "下载脚本失败,请检查本机能否连接 ${GITHUB_RAW_URL}"
return 0
fi
else
err "请手动安装 docker-compose。https://docs.docker.com/compose/install/linux/"
before_show_menu
fi
fi
wget -t 2 -T 60 -O /tmp/nezha-config.yaml https://${GITHUB_RAW_URL}/script/config.yaml >/dev/null 2>&1
if [ $? != 0 ]; then
_cmd="wget -t 2 -T 60 -O /tmp/nezha-config.yaml https://${GITHUB_RAW_URL}/script/config.yaml >/dev/null 2>&1"
if ! eval "$_cmd"; then
err "下载脚本失败,请检查本机能否连接 ${GITHUB_RAW_URL}"
return 0
fi
@@ -525,18 +542,22 @@ modify_dashboard_config() {
if [ "$IS_DOCKER_NEZHA" = 0 ]; then
echo "正在下载服务文件"
if [ "$os_alpine" != 1 ]; then
sudo wget -t 2 -T 60 -O $NZ_DASHBOARD_SERVICE https://${GITHUB_RAW_URL}/script/nezha-dashboard.service >/dev/null 2>&1
else
sudo wget -t 2 -T 60 -O $NZ_DASHBOARD_SERVICERC https://${GITHUB_RAW_URL}/script/nezha-dashboard >/dev/null 2>&1
sudo chmod +x $NZ_DASHBOARD_SERVICERC
if [ $? != 0 ]; then
_download="sudo wget -t 2 -T 60 -O $NZ_DASHBOARD_SERVICE https://${GITHUB_RAW_URL}/script/nezha-dashboard.service >/dev/null 2>&1"
if ! eval "$_download"; then
err "文件下载失败,请检查本机能否连接 ${GITHUB_RAW_URL}"
return 0
fi
else
_download="sudo wget -t 2 -T 60 -O $NZ_DASHBOARD_SERVICERC https://${GITHUB_RAW_URL}/script/nezha-dashboard >/dev/null 2>&1"
if ! eval "$_download"; then
err "文件下载失败,请检查本机能否连接 ${GITHUB_RAW_URL}"
return 0
fi
sudo chmod +x $NZ_DASHBOARD_SERVICERC
fi
fi
printf "面板配置 ${green}修改成功,请稍等重启生效${plain}\n"
success "面板配置 修改成功,请稍等重启生效"
restart_and_update
@@ -549,14 +570,14 @@ restart_and_update() {
echo "> 重启并更新面板"
if [ "$IS_DOCKER_NEZHA" = 1 ]; then
restart_and_update_docker
_cmd="restart_and_update_docker"
elif [ "$IS_DOCKER_NEZHA" = 0 ]; then
restart_and_update_standalone
_cmd="restart_and_update_standalone"
fi
if [ $? = 0 ]; then
printf "${green}哪吒监控 重启成功${plain}\n"
printf "默认管理面板地址:${yellow}域名:站点访问端口${plain}\n"
if eval "$_cmd"; then
success "哪吒监控 重启成功"
info "默认管理面板地址:域名:站点访问端口"
else
err "重启失败,可能是因为启动时间超过了两秒,请稍后查看日志信息"
fi
@@ -573,15 +594,22 @@ restart_and_update_docker() {
}
restart_and_update_standalone() {
local version=$(curl -m 10 -sL "https://api.github.com/repos/naiba/nezha/releases/latest" | grep "tag_name" | head -n 1 | awk -F ":" '{print $2}' | sed 's/\"//g;s/,//g;s/ //g')
if [ ! -n "$version" ]; then
version=$(curl -m 10 -sL "https://gitee.com/api/v5/repos/naibahq/nezha/releases/latest" | awk -F '"' '{for(i=1;i<=NF;i++){if($i=="tag_name"){print $(i+2)}}}')
_version=$(curl -m 10 -sL "https://api.github.com/repos/naiba/nezha/releases/latest" | grep "tag_name" | head -n 1 | awk -F ":" '{print $2}' | sed 's/\"//g;s/,//g;s/ //g')
if [ -z "$_version" ]; then
_version=$(curl -m 10 -sL "https://gitee.com/api/v5/repos/naibahq/nezha/releases/latest" | awk -F '"' '{for(i=1;i<=NF;i++){if($i=="tag_name"){print $(i+2)}}}')
fi
if [ ! -n "$version" ]; then
version=$(curl -m 10 -sL "https://fastly.jsdelivr.net/gh/naiba/nezha/" | grep "option\.value" | awk -F "'" '{print $2}' | sed 's/naiba\/nezha@/v/g')
if [ -z "$_version" ]; then
_version=$(curl -m 10 -sL "https://fastly.jsdelivr.net/gh/naiba/nezha/" | grep "option\.value" | awk -F "'" '{print $2}' | sed 's/naiba\/nezha@/v/g')
fi
if [ ! -n "$version" ]; then
version=$(curl -m 10 -sL "https://gcore.jsdelivr.net/gh/naiba/nezha/" | grep "option\.value" | awk -F "'" '{print $2}' | sed 's/naiba\/nezha@/v/g')
if [ -z "$_version" ]; then
_version=$(curl -m 10 -sL "https://gcore.jsdelivr.net/gh/naiba/nezha/" | grep "option\.value" | awk -F "'" '{print $2}' | sed 's/naiba\/nezha@/v/g')
fi
if [ -z "$_version" ]; then
err "获取版本号失败,请检查本机能否链接 https://api.github.com/repos/naiba/nezha/releases/latest"
return 1
else
echo "当前最新版本为: ${_version}"
fi
if [ "$os_alpine" != 1 ]; then
@@ -591,20 +619,14 @@ restart_and_update_standalone() {
sudo rc-service nezha-dashboard stop
fi
if [ ! -n "$version" ]; then
err "获取版本号失败,请检查本机能否链接 https://api.github.com/repos/naiba/nezha/releases/latest"
return 1
else
echo "当前最新版本为: ${version}"
fi
if [ -z "$CN" ]; then
NZ_DASHBOARD_URL="https://${GITHUB_URL}/naiba/nezha/releases/download/$version/dashboard-linux-$os_arch.zip"
NZ_DASHBOARD_URL="https://${GITHUB_URL}/naiba/nezha/releases/download/${_version}/dashboard-linux-${os_arch}.zip"
else
NZ_DASHBOARD_URL="https://${GITHUB_URL}/naibahq/nezha/releases/download/$version/dashboard-linux-$os_arch.zip"
NZ_DASHBOARD_URL="https://${GITHUB_URL}/naibahq/nezha/releases/download/${_version}/dashboard-linux-${os_arch}.zip"
fi
sudo wget -qO $NZ_DASHBOARD_PATH/app.zip $NZ_DASHBOARD_URL >/dev/null 2>&1 && sudo unzip -qq -o $NZ_DASHBOARD_PATH/app.zip -d $NZ_DASHBOARD_PATH && sudo mv $NZ_DASHBOARD_PATH/dashboard-linux-$os_arch $NZ_DASHBOARD_PATH/app && sudo rm $NZ_DASHBOARD_PATH/app.zip
sudo wget -qO $NZ_DASHBOARD_PATH/app.zip "$NZ_DASHBOARD_URL" >/dev/null 2>&1 && sudo unzip -qq -o $NZ_DASHBOARD_PATH/app.zip -d $NZ_DASHBOARD_PATH && sudo mv $NZ_DASHBOARD_PATH/dashboard-linux-$os_arch $NZ_DASHBOARD_PATH/app && sudo rm $NZ_DASHBOARD_PATH/app.zip
sudo chmod +x $NZ_DASHBOARD_PATH/app
if [ "$os_alpine" != 1 ]; then
sudo systemctl enable nezha-dashboard
@@ -619,13 +641,13 @@ start_dashboard() {
echo "> 启动面板"
if [ "$IS_DOCKER_NEZHA" = 1 ]; then
start_dashboard_docker
_cmd="start_dashboard_docker"
elif [ "$IS_DOCKER_NEZHA" = 0 ]; then
start_dashboard_standalone
_cmd="start_dashboard_standalone"
fi
if [ $? = 0 ]; then
printf "${green}哪吒监控 启动成功${plain}\n"
if eval "$_cmd"; then
success "哪吒监控 启动成功"
else
err "启动失败,请稍后查看日志信息"
fi
@@ -651,13 +673,13 @@ stop_dashboard() {
echo "> 停止面板"
if [ "$IS_DOCKER_NEZHA" = 1 ]; then
stop_dashboard_docker
_cmd="stop_dashboard_docker"
elif [ "$IS_DOCKER_NEZHA" = 0 ]; then
stop_dashboard_standalone
_cmd="stop_dashboard_standalone"
fi
if [ $? = 0 ]; then
printf "${green}哪吒监控 停止成功${plain}\n"
if eval "$_cmd"; then
success "哪吒监控 停止成功"
else
err "停止失败,请稍后查看日志信息"
fi
@@ -936,4 +958,4 @@ if [ $# -gt 0 ]; then
else
select_version
show_menu
fi
fi
+143 -116
View File
@@ -12,13 +12,13 @@ NZ_DASHBOARD_PATH="${NZ_BASE_PATH}/dashboard"
NZ_AGENT_PATH="${NZ_BASE_PATH}/agent"
NZ_DASHBOARD_SERVICE="/etc/systemd/system/nezha-dashboard.service"
NZ_DASHBOARD_SERVICERC="/etc/init.d/nezha-dashboard"
NZ_VERSION="v0.19.0"
NZ_VERSION="v0.20.3"
red='\033[0;31m'
green='\033[0;32m'
yellow='\033[0;33m'
plain='\033[0m'
export PATH=$PATH:/usr/local/bin
export PATH="$PATH:/usr/local/bin"
os_arch=""
[ -e /etc/os-release ] && grep -i "PRETTY_NAME" /etc/os-release | grep -qi "alpine" && os_alpine='1'
@@ -45,26 +45,36 @@ check_systemd() {
}
err() {
printf "${red}$*${plain}\n" >&2
printf "${red}%s${plain}\n" "$*" >&2
}
success() {
printf "${green}%s${plain}\n" "$*"
}
info() {
printf "${yellow}%s${plain}\n" "$*"
}
geo_check() {
api_list="https://blog.cloudflare.com/cdn-cgi/trace https://dash.cloudflare.com/cdn-cgi/trace https://cf-ns.com/cdn-cgi/trace"
api_list="https://blog.cloudflare.com/cdn-cgi/trace https://dash.cloudflare.com/cdn-cgi/trace https://developers.cloudflare.com/cdn-cgi/trace"
ua="Mozilla/5.0 (X11; Linux x86_64; rv:60.0) Gecko/20100101 Firefox/81.0"
set -- $api_list
set -- "$api_list"
for url in $api_list; do
text="$(curl -A "$ua" -m 10 -s $url)"
endpoint="$(echo $text | sed -n 's/.*h=\([^ ]*\).*/\1/p')"
if echo $text | grep -qw 'CN'; then
text="$(curl -A "$ua" -m 10 -s "$url")"
endpoint="$(echo "$text" | sed -n 's/.*h=\([^ ]*\).*/\1/p')"
if echo "$text" | grep -qw 'CN'; then
isCN=true
break
elif echo $url | grep -q $endpoint; then
elif echo "$url" | grep -q "$endpoint"; then
break
fi
done
}
pre_check() {
umask 077
## os_arch
if uname -m | grep -q 'x86_64'; then
os_arch="amd64"
@@ -83,7 +93,7 @@ pre_check() {
## China_IP
if [ -z "$CN" ]; then
geo_check
if [ ! -z "$isCN" ]; then
if [ -n "$isCN" ]; then
echo "According to the information provided by various geoip api, the current IP may be in China"
printf "Will the installation be done with a Chinese Mirror? [Y/n] (Custom Mirror Input 3): "
read -r input
@@ -177,7 +187,9 @@ installation_check() {
select_version() {
if [ -z "$IS_DOCKER_NEZHA" ]; then
printf "${yellow}Select your installation method(Input anything is ok if you are installing agent):\n1. Docker\n2. Standalone${plain}\n"
info "Select your installation method(Input anything is ok if you are installing agent):"
info "1. Docker"
info "2. Standalone"
while true; do
printf "Please enter [1-2]: "
read -r option
@@ -201,13 +213,18 @@ select_version() {
update_script() {
echo "> Update Script"
curl -sL https://${GITHUB_RAW_URL}/script/install_en.sh -o /tmp/nezha.sh
new_version=$(grep "NZ_VERSION" /tmp/nezha.sh | head -n 1 | awk -F "=" '{print $2}' | sed 's/\"//g;s/,//g;s/ //g')
if [ ! -n "$new_version" ]; then
echo "Script failed to get, please check if the network can link https://${GITHUB_RAW_URL}/script/install.sh"
return 1
#curl -sL https://${GITHUB_RAW_URL}/script/install_en.sh -o /tmp/nezha.sh
#new_version=$(grep "NZ_VERSION" /tmp/nezha.sh | head -n 1 | awk -F "=" '{print $2}' | sed 's/\"//g;s/,//g;s/ //g')
#if [ -z "$new_version" ]; then
# echo "Script failed to get, please check if the network can link https://${GITHUB_RAW_URL}/script/install.sh"
# return 1
#fi
#echo "The current latest version is: ${new_version}"
if [ -z "$CN" ]; then
curl -sL https://raw.githubusercontent.com/nezhahq/scripts/main/install_en.sh -o /tmp/nezha.sh
else
curl -sL https://gitee.com/naibahq/scripts/raw/main/install_en.sh -o /tmp/nezha.sh
fi
echo "The current latest version is: ${new_version}"
mv -f /tmp/nezha.sh ./nezha.sh && chmod a+x ./nezha.sh
echo "Execute new script after 3s"
@@ -218,7 +235,7 @@ update_script() {
}
before_show_menu() {
echo && printf "${yellow}* Press Enter to return to the main menu *${plain}" && read temp
echo && info "* Press Enter to return to the main menu *" && read temp
show_menu
}
@@ -228,7 +245,7 @@ install_base() {
}
install_arch() {
printf "${green}Info: ${plain} Archlinux needs to add nezha-agent user to install libselinux. It will be deleted automatically after installation. It is recommended to check manually\n"
info "Archlinux needs to add nezha-agent user to install libselinux. It will be deleted automatically after installation. It is recommended to check manually"
read -r -p "Do you need to install libselinux? [Y/n] " input
case $input in
[yY][eE][sS] | [yY])
@@ -252,11 +269,11 @@ install_arch() {
}
install_soft() {
(command -v yum >/dev/null 2>&1 && sudo yum makecache && sudo yum install $* selinux-policy -y) ||
(command -v apt >/dev/null 2>&1 && sudo apt update && sudo apt install $* selinux-utils -y) ||
(command -v pacman >/dev/null 2>&1 && sudo pacman -Syu $* base-devel --noconfirm && install_arch) ||
(command -v apt-get >/dev/null 2>&1 && sudo apt-get update && sudo apt-get install $* selinux-utils -y) ||
(command -v apk >/dev/null 2>&1 && sudo apk update && sudo apk add $* -f)
(command -v yum >/dev/null 2>&1 && sudo yum makecache && sudo yum install "$@" selinux-policy -y) ||
(command -v apt >/dev/null 2>&1 && sudo apt update && sudo apt install "$@" selinux-utils -y) ||
(command -v pacman >/dev/null 2>&1 && sudo pacman -Syu "$@" base-devel --noconfirm && install_arch) ||
(command -v apt-get >/dev/null 2>&1 && sudo apt-get update && sudo apt-get install "$@" selinux-utils -y) ||
(command -v apk >/dev/null 2>&1 && sudo apk update && sudo apk add "$@" -f)
}
install_dashboard() {
@@ -287,8 +304,6 @@ install_dashboard() {
esac
fi
sudo chmod -R 700 $NZ_DASHBOARD_PATH
if [ "$IS_DOCKER_NEZHA" = 1 ]; then
install_dashboard_docker
elif [ "$IS_DOCKER_NEZHA" = 0 ]; then
@@ -304,12 +319,10 @@ install_dashboard() {
install_dashboard_docker() {
if [ ! "$FRESH_INSTALL" = 0 ]; then
command -v docker >/dev/null 2>&1
if [ $? != 0 ]; then
if ! command -v docker >/dev/null 2>&1; then
echo "Installing Docker"
if [ "$os_alpine" != 1 ]; then
curl -sL https://${Get_Docker_URL} | sudo bash -s ${Get_Docker_Argu}
if [ $? != 0 ]; then
if ! curl -sL https://${Get_Docker_URL} | sudo bash -s "${Get_Docker_Argu}"; then
err "Script failed to get, please check if the network can link ${Get_Docker_URL}"
return 0
fi
@@ -320,7 +333,7 @@ install_dashboard_docker() {
sudo rc-update add docker
sudo rc-service docker start
fi
printf "${green}Docker${plain} installed successfully\n"
success "Docker installed successfully"
installation_check
fi
fi
@@ -334,12 +347,10 @@ install_dashboard_standalone() {
selinux() {
#Check SELinux
command -v getenforce >/dev/null 2>&1
if [ $? -eq 0 ]; then
getenforce | grep '[Ee]nfor'
if [ $? -eq 0 ]; then
if command -v getenforce >/dev/null 2>&1; then
if getenforce | grep '[Ee]nfor'; then
echo "SELinux running, closing now!"
sudo setenforce 0 &>/dev/null
sudo setenforce 0 >/dev/null 2>&1
find_key="SELINUX="
sudo sed -ri "/^$find_key/c${find_key}disabled" /etc/selinux/config
fi
@@ -354,31 +365,37 @@ install_agent() {
echo "Obtaining Agent version number"
local version=$(curl -m 10 -sL "https://api.github.com/repos/nezhahq/agent/releases/latest" | grep "tag_name" | head -n 1 | awk -F ":" '{print $2}' | sed 's/\"//g;s/,//g;s/ //g')
if [ ! -n "$version" ]; then
version=$(curl -m 10 -sL "https://gitee.com/api/v5/repos/naibahq/agent/releases/latest" | awk -F '"' '{for(i=1;i<=NF;i++){if($i=="tag_name"){print $(i+2)}}}')
_version=$(curl -m 10 -sL "https://api.github.com/repos/nezhahq/agent/releases/latest" | grep "tag_name" | head -n 1 | awk -F ":" '{print $2}' | sed 's/\"//g;s/,//g;s/ //g')
if [ -z "$_version" ]; then
_version=$(curl -m 10 -sL "https://gitee.com/api/v5/repos/naibahq/agent/releases/latest" | awk -F '"' '{for(i=1;i<=NF;i++){if($i=="tag_name"){print $(i+2)}}}')
fi
if [ ! -n "$version" ]; then
version=$(curl -m 10 -sL "https://fastly.jsdelivr.net/gh/nezhahq/agent/" | grep "option\.value" | awk -F "'" '{print $2}' | sed 's/nezhahq\/agent@/v/g')
if [ -z "$_version" ]; then
_version=$(curl -m 10 -sL "https://fastly.jsdelivr.net/gh/nezhahq/agent/" | grep "option\.value" | awk -F "'" '{print $2}' | sed 's/nezhahq\/agent@/v/g')
fi
if [ ! -n "$version" ]; then
version=$(curl -m 10 -sL "https://gcore.jsdelivr.net/gh/nezhahq/agent/" | grep "option\.value" | awk -F "'" '{print $2}' | sed 's/nezhahq\/agent@/v/g')
if [ -z "$_version" ]; then
_version=$(curl -m 10 -sL "https://gcore.jsdelivr.net/gh/nezhahq/agent/" | grep "option\.value" | awk -F "'" '{print $2}' | sed 's/nezhahq\/agent@/v/g')
fi
if [ ! -n "$version" ]; then
err "Fail to obtaine agent version, please check if the network can link https://api.github.com/repos/nezhahq/agent/releases/latest"
if [ -z "$_version" ]; then
err "Fail to obtain agent version, please check if the network can link https://api.github.com/repos/nezhahq/agent/releases/latest"
return 1
else
echo "The current latest version is: ${version}"
echo "The current latest version is: ${_version}"
fi
# Nezha Monitoring Folder
sudo mkdir -p $NZ_AGENT_PATH
sudo chmod -R 700 $NZ_AGENT_PATH
echo "Downloading Agent"
wget -t 2 -T 60 -O nezha-agent_linux_${os_arch}.zip https://${GITHUB_URL}/nezhahq/agent/releases/download/${version}/nezha-agent_linux_${os_arch}.zip >/dev/null 2>&1
if [ $? != 0 ]; then
if [ -z "$CN" ]; then
NZ_AGENT_URL="https://${GITHUB_URL}/nezhahq/agent/releases/download/${_version}/nezha-agent_linux_${os_arch}.zip"
else
NZ_AGENT_URL="https://${GITHUB_URL}/naibahq/agent/releases/download/${_version}/nezha-agent_linux_${os_arch}.zip"
fi
_cmd="wget -t 2 -T 60 -O nezha-agent_linux_${os_arch}.zip $NZ_AGENT_URL >/dev/null 2>&1"
if ! eval "$_cmd"; then
err "Fail to download agent, please check if the network can link ${GITHUB_URL}"
return 1
fi
@@ -402,10 +419,10 @@ modify_agent_config() {
echo "> Modify Agent Configuration"
if [ $# -lt 3 ]; then
echo "Please add Agent in the admin panel first, record the secret"
printf "Please enter a domain that resolves to the IP where the panel is located (no CDN): "
echo "Please add Agent in the Dashboard first, record the secret"
printf "Please enter a domain that resolves to the IP where Dashboard is located (no CDN): "
read -r nz_grpc_host
printf "Please enter the panel RPC port (default 5555): "
printf "Please enter Dashboard RPC port (default 5555): "
read -r nz_grpc_port
printf "Please enter the Agent secret: "
read -r nz_client_secret
@@ -430,14 +447,14 @@ modify_agent_config() {
fi
fi
sudo ${NZ_AGENT_PATH}/nezha-agent service install -s "$nz_grpc_host:$nz_grpc_port" -p $nz_client_secret $args >/dev/null 2>&1
_cmd="sudo ${NZ_AGENT_PATH}/nezha-agent service install -s $nz_grpc_host:$nz_grpc_port -p $nz_client_secret $args >/dev/null 2>&1"
if [ $? -ne 0 ]; then
sudo ${NZ_AGENT_PATH}/nezha-agent service uninstall >/dev/null 2>&1
sudo ${NZ_AGENT_PATH}/nezha-agent service install -s "$nz_grpc_host:$nz_grpc_port" -p $nz_client_secret $args >/dev/null 2>&1
if ! eval "$_cmd"; then
sudo "${NZ_AGENT_PATH}"/nezha-agent service uninstall >/dev/null 2>&1
sudo "${NZ_AGENT_PATH}"/nezha-agent service install -s "$nz_grpc_host:$nz_grpc_port" -p "$nz_client_secret" "$args" >/dev/null 2>&1
fi
printf "Agent configuration ${green} modified successfully, please wait for agent self-restart to take effect${plain}\n"
success "Agent configuration modified successfully, please wait for agent self-restart to take effect"
#if [[ $# == 0 ]]; then
# before_show_menu
@@ -448,16 +465,21 @@ modify_dashboard_config() {
echo "> Modify Dashboard Configuration"
if [ "$IS_DOCKER_NEZHA" = 1 ]; then
echo "Download Docker Script"
wget -t 2 -T 60 -O /tmp/nezha-docker-compose.yaml https://${GITHUB_RAW_URL}/script/docker-compose.yaml >/dev/null 2>&1
if [ $? != 0 ]; then
err "Script failed to get, please check if the network can link ${GITHUB_RAW_URL}"
return 0
if [ -n "$DOCKER_COMPOSE_COMMAND" ]; then
echo "Download Docker Script"
_cmd="wget -t 2 -T 60 -O /tmp/nezha-docker-compose.yaml https://${GITHUB_RAW_URL}/script/docker-compose.yaml >/dev/null 2>&1"
if ! eval "$_cmd"; then
err "Script failed to get, please check if the network can link ${GITHUB_RAW_URL}"
return 0
fi
else
err "Please install docker-compose manually. https://docs.docker.com/compose/install/linux/"
before_show_menu
fi
fi
wget -t 2 -T 60 -O /tmp/nezha-config.yaml https://${GITHUB_RAW_URL}/script/config.yaml >/dev/null 2>&1
if [ $? != 0 ]; then
_cmd="wget -t 2 -T 60 -O /tmp/nezha-config.yaml https://${GITHUB_RAW_URL}/script/config.yaml >/dev/null 2>&1"
if ! eval "$_cmd"; then
err "Script failed to get, please check if the network can link ${GITHUB_RAW_URL}"
return 0
fi
@@ -519,18 +541,22 @@ modify_dashboard_config() {
if [ "$IS_DOCKER_NEZHA" = 0 ]; then
echo "Downloading service file"
if [ "$os_alpine" != 1 ]; then
sudo wget -t 2 -T 60 -O $NZ_DASHBOARD_SERVICE https://${GITHUB_RAW_URL}/script/nezha-dashboard.service >/dev/null 2>&1
else
sudo wget -t 2 -T 60 -O $NZ_DASHBOARD_SERVICERC https://${GITHUB_RAW_URL}/script/nezha-dashboard >/dev/null 2>&1
sudo chmod +x $NZ_DASHBOARD_SERVICERC
if [ $? != 0 ]; then
_download="sudo wget -t 2 -T 60 -O $NZ_DASHBOARD_SERVICE https://${GITHUB_RAW_URL}/script/nezha-dashboard.service >/dev/null 2>&1"
if ! eval "$_download"; then
err "File failed to get, please check if the network can link ${GITHUB_RAW_URL}"
return 0
fi
else
_download="sudo wget -t 2 -T 60 -O $NZ_DASHBOARD_SERVICERC https://${GITHUB_RAW_URL}/script/nezha-dashboard >/dev/null 2>&1"
if ! eval "$_download"; then
err "File failed to get, please check if the network can link ${GITHUB_RAW_URL}"
return 0
fi
sudo chmod +x $NZ_DASHBOARD_SERVICERC
fi
fi
printf "Dashboard configuration ${green} modified successfully, please wait for Dashboard self-restart to take effect${plain}\n"
success "Dashboard configuration modified successfully, please wait for Dashboard self-restart to take effect"
restart_and_update
@@ -540,17 +566,17 @@ modify_dashboard_config() {
}
restart_and_update() {
echo "> Restart and Update the Panel"
echo "> Restart and Update Dashboard"
if [ "$IS_DOCKER_NEZHA" = 1 ]; then
restart_and_update_docker
_cmd="restart_and_update_docker"
elif [ "$IS_DOCKER_NEZHA" = 0 ]; then
restart_and_update_standalone
_cmd="restart_and_update_standalone"
fi
if [ $? = 0 ]; then
printf "${green}Nezha Monitoring Restart Successful${plain}\n"
printf "Default panel address: ${yellow}domain:Site_access_port${plain}\n"
if eval "$_cmd"; then
success "Nezha Monitoring Restart Successful"
info "Default Dashboard address: domain:site_access_port"
else
err "The restart failed, probably because the boot time exceeded two seconds, please check the log information later"
fi
@@ -567,22 +593,22 @@ restart_and_update_docker() {
}
restart_and_update_standalone() {
local version=$(curl -m 10 -sL "https://api.github.com/repos/naiba/nezha/releases/latest" | grep "tag_name" | head -n 1 | awk -F ":" '{print $2}' | sed 's/\"//g;s/,//g;s/ //g')
if [ ! -n "$version" ]; then
version=$(curl -m 10 -sL "https://gitee.com/api/v5/repos/naibahq/nezha/releases/latest" | awk -F '"' '{for(i=1;i<=NF;i++){if($i=="tag_name"){print $(i+2)}}}')
_version=$(curl -m 10 -sL "https://api.github.com/repos/naiba/nezha/releases/latest" | grep "tag_name" | head -n 1 | awk -F ":" '{print $2}' | sed 's/\"//g;s/,//g;s/ //g')
if [ -z "$_version" ]; then
_version=$(curl -m 10 -sL "https://gitee.com/api/v5/repos/naibahq/nezha/releases/latest" | awk -F '"' '{for(i=1;i<=NF;i++){if($i=="tag_name"){print $(i+2)}}}')
fi
if [ ! -n "$version" ]; then
version=$(curl -m 10 -sL "https://fastly.jsdelivr.net/gh/naiba/nezha/" | grep "option\.value" | awk -F "'" '{print $2}' | sed 's/naiba\/nezha@/v/g')
if [ -z "$_version" ]; then
_version=$(curl -m 10 -sL "https://fastly.jsdelivr.net/gh/naiba/nezha/" | grep "option\.value" | awk -F "'" '{print $2}' | sed 's/naiba\/nezha@/v/g')
fi
if [ ! -n "$version" ]; then
version=$(curl -m 10 -sL "https://gcore.jsdelivr.net/gh/naiba/nezha/" | grep "option\.value" | awk -F "'" '{print $2}' | sed 's/naiba\/nezha@/v/g')
if [ -z "$_version" ]; then
_version=$(curl -m 10 -sL "https://gcore.jsdelivr.net/gh/naiba/nezha/" | grep "option\.value" | awk -F "'" '{print $2}' | sed 's/naiba\/nezha@/v/g')
fi
if [ ! -n "$version" ]; then
err "Fail to obtaine agent version, please check if the network can link https://api.github.com/repos/nezhahq/agent/releases/latest"
if [ -z "$_version" ]; then
err "Fail to obtain agent version, please check if the network can link https://api.github.com/repos/nezhahq/agent/releases/latest"
return 1
else
echo "The current latest version is: ${version}"
echo "The current latest version is: ${_version}"
fi
if [ "$os_alpine" != 1 ]; then
@@ -593,12 +619,13 @@ restart_and_update_standalone() {
fi
if [ -z "$CN" ]; then
NZ_DASHBOARD_URL="https://${GITHUB_URL}/naiba/nezha/releases/download/$version/dashboard-linux-$os_arch.zip"
NZ_DASHBOARD_URL="https://${GITHUB_URL}/naiba/nezha/releases/download/${_version}/dashboard-linux-${os_arch}.zip"
else
NZ_DASHBOARD_URL="https://${GITHUB_URL}/naibahq/nezha/releases/download/$version/dashboard-linux-$os_arch.zip"
NZ_DASHBOARD_URL="https://${GITHUB_URL}/naibahq/nezha/releases/download/${_version}/dashboard-linux-${os_arch}.zip"
fi
sudo wget -qO $NZ_DASHBOARD_PATH/app.zip $NZ_DASHBOARD_URL >/dev/null 2>&1 && sudo unzip -qq -o $NZ_DASHBOARD_PATH/app.zip -d $NZ_DASHBOARD_PATH && sudo mv $NZ_DASHBOARD_PATH/dashboard-linux-$os_arch $NZ_DASHBOARD_PATH/app && sudo rm $NZ_DASHBOARD_PATH/app.zip
sudo wget -qO $NZ_DASHBOARD_PATH/app.zip "$NZ_DASHBOARD_URL" >/dev/null 2>&1 && sudo unzip -qq -o $NZ_DASHBOARD_PATH/app.zip -d $NZ_DASHBOARD_PATH && sudo mv $NZ_DASHBOARD_PATH/dashboard-linux-$os_arch $NZ_DASHBOARD_PATH/app && sudo rm $NZ_DASHBOARD_PATH/app.zip
sudo chmod +x $NZ_DASHBOARD_PATH/app
if [ "$os_alpine" != 1 ]; then
sudo systemctl enable nezha-dashboard
@@ -610,16 +637,16 @@ restart_and_update_standalone() {
}
start_dashboard() {
echo "> Start Panel"
echo "> Start Dashboard"
if [ "$IS_DOCKER_NEZHA" = 1 ]; then
start_dashboard_docker
_cmd="start_dashboard_docker"
elif [ "$IS_DOCKER_NEZHA" = 0 ]; then
start_dashboard_standalone
_cmd="start_dashboard_standalone"
fi
if [ $? = 0 ]; then
printf "${green}Nezha Monitoring Start Successful${plain}\n"
if eval "$_cmd"; then
success "Nezha Monitoring Start Successful"
else
err "Failed to start, please check the log message later"
fi
@@ -642,16 +669,16 @@ start_dashboard_standalone() {
}
stop_dashboard() {
echo "> Stop Panel"
echo "> Stop Dashboard"
if [ "$IS_DOCKER_NEZHA" = 1 ]; then
stop_dashboard_docker
_cmd="stop_dashboard_docker"
elif [ "$IS_DOCKER_NEZHA" = 0 ]; then
stop_dashboard_standalone
_cmd="stop_dashboard_standalone"
fi
if [ $? = 0 ]; then
printf "${green}Nezha Monitoring Stop Successful${plain}\n"
if eval "$_cmd"; then
success "Nezha Monitoring Stop Successful"
else
err "Failed to stop, please check the log message later"
fi
@@ -674,7 +701,7 @@ stop_dashboard_standalone() {
}
show_dashboard_log() {
echo "> View Panel Log"
echo "> View Dashboard Log"
if [ "$IS_DOCKER_NEZHA" = 1 ]; then
show_dashboard_log_docker
@@ -700,7 +727,7 @@ show_dashboard_log_standalone() {
}
uninstall_dashboard() {
echo "> Uninstall Panel"
echo "> Uninstall Dashboard"
if [ "$IS_DOCKER_NEZHA" = 1 ]; then
uninstall_dashboard_docker
@@ -787,13 +814,13 @@ show_usage() {
echo "Nezha Monitor Management Script Usage: "
echo "--------------------------------------------------------"
echo "./nezha.sh - Show Menu"
echo "./nezha.sh install_dashboard - Install Panel"
echo "./nezha.sh modify_dashboard_config - Modify Panel Configuration"
echo "./nezha.sh start_dashboard - Start Panel"
echo "./nezha.sh stop_dashboard - Stop Panel"
echo "./nezha.sh restart_and_update - Restart and Update the Panel"
echo "./nezha.sh show_dashboard_log - View Panel Log"
echo "./nezha.sh uninstall_dashboard - Uninstall Panel"
echo "./nezha.sh install_dashboard - Install Dashboard"
echo "./nezha.sh modify_dashboard_config - Modify Dashboard Configuration"
echo "./nezha.sh start_dashboard - Start Dashboard"
echo "./nezha.sh stop_dashboard - Stop Dashboard"
echo "./nezha.sh restart_and_update - Restart and Update the Dashboard"
echo "./nezha.sh show_dashboard_log - View Dashboard Log"
echo "./nezha.sh uninstall_dashboard - Uninstall Dashboard"
echo "--------------------------------------------------------"
echo "./nezha.sh install_agent - Install Agent"
echo "./nezha.sh modify_agent_config - Modify Agent Configuration"
@@ -808,13 +835,13 @@ show_menu() {
printf "
${green}Nezha Monitor Management Script${plain} ${red}${NZ_VERSION}${plain}
--- https://github.com/naiba/nezha ---
${green}1.${plain} Install Panel
${green}2.${plain} Modify Panel Configuration
${green}3.${plain} Start Panel
${green}4.${plain} Stop Panel
${green}5.${plain} Restart and Update the Panel
${green}6.${plain} View Panel Log
${green}7.${plain} Uninstall Panel
${green}1.${plain} Install Dashboard
${green}2.${plain} Modify Dashbaord Configuration
${green}3.${plain} Start Dashboard
${green}4.${plain} Stop Dashboard
${green}5.${plain} Restart and Update Dashboard
${green}6.${plain} View Dashboard Log
${green}7.${plain} Uninstall Dashboard
————————————————-
${green}8.${plain} Install Agent
${green}9.${plain} Modify Agent Configuration
@@ -930,4 +957,4 @@ if [ $# -gt 0 ]; then
else
select_version
show_menu
fi
fi
+9 -24
View File
@@ -125,7 +125,6 @@ func (s *NezhaHandler) ReportSystemState(c context.Context, r *pb.State) (*pb.Re
func (s *NezhaHandler) ReportSystemInfo(c context.Context, r *pb.Host) (*pb.Receipt, error) {
var clientID uint64
var provider ddns.Provider
var err error
if clientID, err = s.Auth.Check(c); err != nil {
return nil, err
@@ -135,33 +134,19 @@ func (s *NezhaHandler) ReportSystemInfo(c context.Context, r *pb.Host) (*pb.Rece
defer singleton.ServerLock.RUnlock()
// 检查并更新DDNS
if singleton.Conf.DDNS.Enable &&
singleton.ServerList[clientID].EnableDDNS &&
host.IP != "" &&
if singleton.ServerList[clientID].EnableDDNS && host.IP != "" &&
(singleton.ServerList[clientID].Host == nil || singleton.ServerList[clientID].Host.IP != host.IP) {
serverDomain := singleton.ServerList[clientID].DDNSDomain
if singleton.Conf.DDNS.Provider == "" {
provider, err = singleton.GetDDNSProviderFromProfile(singleton.ServerList[clientID].DDNSProfile)
} else {
provider, err = singleton.GetDDNSProviderFromString(singleton.Conf.DDNS.Provider)
}
if err == nil && serverDomain != "" {
ipv4, ipv6, _ := utils.SplitIPAddr(host.IP)
maxRetries := int(singleton.Conf.DDNS.MaxRetries)
config := &ddns.DomainConfig{
EnableIPv4: singleton.ServerList[clientID].EnableIPv4,
EnableIpv6: singleton.ServerList[clientID].EnableIpv6,
FullDomain: serverDomain,
Ipv4Addr: ipv4,
Ipv6Addr: ipv6,
ipv4, ipv6, _ := utils.SplitIPAddr(host.IP)
providers, err := singleton.GetDDNSProvidersFromProfiles(singleton.ServerList[clientID].DDNSProfiles, &ddns.IP{Ipv4Addr: ipv4, Ipv6Addr: ipv6})
if err == nil {
for _, provider := range providers {
go func(provider *ddns.Provider) {
provider.UpdateDomain(context.Background())
}(provider)
}
go singleton.RetryableUpdateDomain(provider, config, maxRetries)
} else {
// 虽然会在启动时panic, 可以断言不会走这个分支, 但是考虑到动态加载配置或者其它情况, 这里输出一下方便检查奇奇怪怪的BUG
log.Printf("NEZHA>> 未找到对应的DDNS配置(%s), 或者是provider填写不正确, 请前往config.yml检查你的设置", singleton.ServerList[clientID].DDNSProfile)
log.Printf("NEZHA>> 获取DDNS配置时发生错误: %v", err)
}
}
// 发送IP变动通知
+2
View File
@@ -34,6 +34,7 @@ type CommonServerInfo struct {
IPV6 string `json:"ipv6"`
ValidIP string `json:"valid_ip"`
DisplayIndex int `json:"display_index"`
HideForGuest bool `json:"hide_for_guest"`
}
// StatusResponse 服务器状态子结构 包含服务器信息与状态信息
@@ -150,6 +151,7 @@ func (s *ServerAPIService) GetAllStatus() *ServerStatusResponse {
IPV6: ipv6,
ValidIP: validIP,
DisplayIndex: v.DisplayIndex,
HideForGuest: v.HideForGuest,
}
res.Result = append(res.Result, &StatusResponse{
CommonServerInfo: info,
+56 -55
View File
@@ -2,73 +2,74 @@ package singleton
import (
"fmt"
"log"
"slices"
"sync"
"github.com/libdns/cloudflare"
tencentcloud "github.com/nezhahq/libdns-tencentcloud"
"github.com/naiba/nezha/model"
ddns2 "github.com/naiba/nezha/pkg/ddns"
"github.com/naiba/nezha/pkg/ddns/dummy"
"github.com/naiba/nezha/pkg/ddns/webhook"
)
const (
ProviderWebHook = "webhook"
ProviderCloudflare = "cloudflare"
ProviderTencentCloud = "tencentcloud"
var (
ddnsCache map[uint64]*model.DDNSProfile
ddnsCacheLock sync.RWMutex
)
type ProviderFunc func(*ddns2.DomainConfig) ddns2.Provider
func initDDNS() {
OnDDNSUpdate()
OnNameserverUpdate()
}
func RetryableUpdateDomain(provider ddns2.Provider, domainConfig *ddns2.DomainConfig, maxRetries int) {
if domainConfig == nil {
return
func OnDDNSUpdate() {
var ddns []*model.DDNSProfile
DB.Find(&ddns)
ddnsCacheLock.Lock()
defer ddnsCacheLock.Unlock()
ddnsCache = make(map[uint64]*model.DDNSProfile)
for i := 0; i < len(ddns); i++ {
ddnsCache[ddns[i].ID] = ddns[i]
}
for retries := 0; retries < maxRetries; retries++ {
log.Printf("NEZHA>> 正在尝试更新域名(%s)DDNS(%d/%d)", domainConfig.FullDomain, retries+1, maxRetries)
if err := provider.UpdateDomain(domainConfig); err != nil {
log.Printf("NEZHA>> 尝试更新域名(%s)DDNS失败: %v", domainConfig.FullDomain, err)
}
func OnNameserverUpdate() {
ddns2.InitDNSServers(Conf.DNSServers)
}
func GetDDNSProvidersFromProfiles(profileId []uint64, ip *ddns2.IP) ([]*ddns2.Provider, error) {
profiles := make([]*model.DDNSProfile, 0, len(profileId))
ddnsCacheLock.RLock()
for _, id := range profileId {
if profile, ok := ddnsCache[id]; ok {
profiles = append(profiles, profile)
} else {
log.Printf("NEZHA>> 尝试更新域名(%s)DDNS成功", domainConfig.FullDomain)
break
ddnsCacheLock.RUnlock()
return nil, fmt.Errorf("无法找到DDNS配置 ID %d", id)
}
}
}
ddnsCacheLock.RUnlock()
// Deprecated
func GetDDNSProviderFromString(provider string) (ddns2.Provider, error) {
switch provider {
case ProviderWebHook:
return ddns2.NewProviderWebHook(Conf.DDNS.WebhookURL, Conf.DDNS.WebhookMethod, Conf.DDNS.WebhookRequestBody, Conf.DDNS.WebhookHeaders), nil
case ProviderCloudflare:
return ddns2.NewProviderCloudflare(Conf.DDNS.AccessSecret), nil
case ProviderTencentCloud:
return ddns2.NewProviderTencentCloud(Conf.DDNS.AccessID, Conf.DDNS.AccessSecret), nil
default:
return new(ddns2.ProviderDummy), fmt.Errorf("无法找到配置的DDNS提供者 %s", provider)
}
}
func GetDDNSProviderFromProfile(profileName string) (ddns2.Provider, error) {
profile, ok := Conf.DDNS.Profiles[profileName]
if !ok {
return new(ddns2.ProviderDummy), fmt.Errorf("找到配置 %s", profileName)
}
switch profile.Provider {
case ProviderWebHook:
return ddns2.NewProviderWebHook(profile.WebhookURL, profile.WebhookMethod, profile.WebhookRequestBody, profile.WebhookHeaders), nil
case ProviderCloudflare:
return ddns2.NewProviderCloudflare(profile.AccessSecret), nil
case ProviderTencentCloud:
return ddns2.NewProviderTencentCloud(profile.AccessID, profile.AccessSecret), nil
default:
return new(ddns2.ProviderDummy), fmt.Errorf("无法找到配置的DDNS提供者 %s", profile.Provider)
}
}
func ValidateDDNSProvidersFromProfiles() error {
validProviders := []string{ProviderWebHook, ProviderCloudflare, ProviderTencentCloud}
for _, profile := range Conf.DDNS.Profiles {
if ok := slices.Contains(validProviders, profile.Provider); !ok {
return fmt.Errorf("无法找到配置的DDNS提供者%s", profile.Provider)
providers := make([]*ddns2.Provider, 0, len(profiles))
for _, profile := range profiles {
provider := &ddns2.Provider{DDNSProfile: profile, IPAddrs: ip}
switch profile.Provider {
case model.ProviderDummy:
provider.Setter = &dummy.Provider{}
providers = append(providers, provider)
case model.ProviderWebHook:
provider.Setter = &webhook.Provider{DDNSProfile: profile}
providers = append(providers, provider)
case model.ProviderCloudflare:
provider.Setter = &cloudflare.Provider{APIToken: profile.AccessSecret}
providers = append(providers, provider)
case model.ProviderTencentCloud:
provider.Setter = &tencentcloud.Provider{SecretId: profile.AccessID, SecretKey: profile.AccessSecret}
providers = append(providers, provider)
default:
return nil, fmt.Errorf("无法找到配置的DDNS提供者ID %d", profile.Provider)
}
}
return nil
return providers, nil
}
+2 -21
View File
@@ -1,7 +1,6 @@
package singleton
import (
"fmt"
"log"
"time"
@@ -39,6 +38,7 @@ func LoadSingleton() {
loadCronTasks() // 加载定时任务
loadAPI()
initNAT()
initDDNS()
}
// InitConfigFromPath 从给出的文件路径中加载配置
@@ -48,25 +48,6 @@ func InitConfigFromPath(path string) {
if err != nil {
panic(err)
}
validateConfig()
}
// validateConfig 验证配置文件有效性
func validateConfig() {
var err error
if Conf.DDNS.Provider == "" {
err = ValidateDDNSProvidersFromProfiles()
} else {
_, err = GetDDNSProviderFromString(Conf.DDNS.Provider)
}
if err != nil {
panic(err)
}
if Conf.DDNS.Enable {
if Conf.DDNS.MaxRetries < 1 || Conf.DDNS.MaxRetries > 10 {
panic(fmt.Errorf("DDNS.MaxRetries值域为[1, 10]的整数, 当前为 %d", Conf.DDNS.MaxRetries))
}
}
}
// InitDBFromPath 从给出的文件路径中加载数据库
@@ -84,7 +65,7 @@ func InitDBFromPath(path string) {
err = DB.AutoMigrate(model.Server{}, model.User{},
model.Notification{}, model.AlertRule{}, model.Monitor{},
model.MonitorHistory{}, model.Cron{}, model.Transfer{},
model.ApiToken{}, model.NAT{})
model.ApiToken{}, model.NAT{}, model.DDNSProfile{})
if err != nil {
panic(err)
}