Compare commits

...
20 Commits
Author SHA1 Message Date
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
60 changed files with 2065 additions and 1320 deletions
+2 -2
View File
@@ -21,7 +21,7 @@ jobs:
name: Build artifacts name: Build artifacts
runs-on: ubuntu-latest runs-on: ubuntu-latest
container: container:
image: goreleaser/goreleaser-cross:v1.21 image: goreleaser/goreleaser-cross:v1.23
env: env:
GOOS: ${{ matrix.goos }} GOOS: ${{ matrix.goos }}
GOARCH: ${{ matrix.goarch }} GOARCH: ${{ matrix.goarch }}
@@ -43,7 +43,7 @@ jobs:
- name: Set up Go - name: Set up Go
uses: actions/setup-go@v5 uses: actions/setup-go@v5
with: with:
go-version: "1.21.x" go-version: "1.23.x"
- name: Build - name: Build
uses: goreleaser/goreleaser-action@v6 uses: goreleaser/goreleaser-action@v6
+1 -1
View File
@@ -29,7 +29,7 @@ jobs:
- uses: actions/setup-go@v5 - uses: actions/setup-go@v5
with: with:
go-version: "1.21.x" go-version: "1.23.x"
- name: Unit test - name: Unit test
run: | run: |
+4 -3
View File
@@ -4,7 +4,7 @@
<br> <br>
<small><i>LOGO designed by <a href="https://xio.ng" target="_blank">熊大</a> .</i></small> <small><i>LOGO designed by <a href="https://xio.ng" target="_blank">熊大</a> .</i></small>
<br><br> <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.2-brightgreen?style=for-the-badge&logo=linux"> <img alt="GitHub release (with filter)" src="https://img.shields.io/github/v/release/naiba/nezha?color=brightgreen&style=for-the-badge&logo=github&label=Dashboard">&nbsp;<img src="https://img.shields.io/github/v/release/nezhahq/agent?color=brightgreen&label=Agent&style=for-the-badge&logo=github">&nbsp;<img src="https://img.shields.io/github/actions/workflow/status/nezhahq/agent/agent.yml?label=Agent%20CI&logo=github&style=for-the-badge">&nbsp;<img src="https://img.shields.io/badge/Installer-v0.20.2-brightgreen?style=for-the-badge&logo=linux">
<br> <br>
<br> <br>
<p>:trollface: <b>Nezha Monitoring: Self-hostable, lightweight, servers and websites monitoring and O&M tool.</b></p> <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/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/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/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/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/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> <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>
@@ -66,6 +67,7 @@ You can change the dashboard language in the settings page (`/setting`) after th
<a href="https://github.com/DarcJC" title="Darc Z."><img src="https://avatars.githubusercontent.com/u/53445798?v=4" width="50;" alt="Darc Z."/></a> <a href="https://github.com/DarcJC" title="Darc Z."><img src="https://avatars.githubusercontent.com/u/53445798?v=4" width="50;" alt="Darc Z."/></a>
<a href="https://github.com/Creling" title="Creling"><img src="https://avatars.githubusercontent.com/u/43109504?v=4" width="50;" alt="Creling"/></a> <a href="https://github.com/Creling" title="Creling"><img src="https://avatars.githubusercontent.com/u/43109504?v=4" width="50;" alt="Creling"/></a>
<a href="https://github.com/coreff" title="Core F"><img src="https://avatars.githubusercontent.com/u/38347122?v=4" width="50;" alt="Core F"/></a> <a href="https://github.com/coreff" title="Core F"><img src="https://avatars.githubusercontent.com/u/38347122?v=4" width="50;" alt="Core F"/></a>
<a href="https://github.com/nickfox-taterli" title="Tater Li"><img src="https://avatars.githubusercontent.com/u/19658596?v=4" width="50;" alt="Tater Li"/></a>
<a href="https://github.com/hmsjy2017" title="Tony"><img src="https://avatars.githubusercontent.com/u/42692274?v=4" width="50;" alt="Tony"/></a> <a href="https://github.com/hmsjy2017" title="Tony"><img src="https://avatars.githubusercontent.com/u/42692274?v=4" width="50;" alt="Tony"/></a>
<a href="https://github.com/adminsama" title="adminsama"><img src="https://avatars.githubusercontent.com/u/60880076?v=4" width="50;" alt="adminsama"/></a> <a href="https://github.com/adminsama" title="adminsama"><img src="https://avatars.githubusercontent.com/u/60880076?v=4" width="50;" alt="adminsama"/></a>
<a href="https://github.com/acgpiano" title="Acgpiano"><img src="https://avatars.githubusercontent.com/u/15900800?v=4" width="50;" alt="Acgpiano"/></a> <a href="https://github.com/acgpiano" title="Acgpiano"><img src="https://avatars.githubusercontent.com/u/15900800?v=4" width="50;" alt="Acgpiano"/></a>
@@ -77,13 +79,12 @@ You can change the dashboard language in the settings page (`/setting`) after th
<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/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/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/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/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/colour93" title="玖叁"><img src="https://avatars.githubusercontent.com/u/64313711?v=4" width="50;" alt="玖叁"/></a>
<a href="https://github.com/nickfox-taterli" title="Tater Li"><img src="https://avatars.githubusercontent.com/u/19658596?v=4" width="50;" alt="Tater Li"/></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/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/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/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/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/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> <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 ( import (
"fmt" "fmt"
"html/template" "html/template"
"io/fs"
"log" "log"
"net/http" "net/http"
"os" "os"
"path/filepath"
"strconv" "strconv"
"strings" "strings"
"time" "time"
@@ -34,28 +34,16 @@ func ServeWeb(port uint) *http.Server {
pprof.Register(r) pprof.Register(r)
} }
r.Use(natGateway) r.Use(natGateway)
if os.Getenv("NZ_LOCAL_TEMPLATE") == "true" { tmpl := template.New("").Funcs(funcMap)
r.SetFuncMap(funcMap) var err error
r.Use(mygin.RecordPath) tmpl, err = tmpl.ParseFS(resource.TemplateFS, "template/**/*.html")
r.Static("/static", "resource/static") if err != nil {
r.LoadHTMLGlob("resource/template/**/*.html") panic(err)
} 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))
} }
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) routers(r)
page404 := func(c *gin.Context) { page404 := func(c *gin.Context) {
mygin.ShowErrorPage(c, mygin.ErrInfo{ mygin.ShowErrorPage(c, mygin.ErrInfo{
@@ -96,7 +84,7 @@ func routers(r *gin.Engine) {
} }
func loadThirdPartyTemplates(tmpl *template.Template) *template.Template { func loadThirdPartyTemplates(tmpl *template.Template) *template.Template {
var ret = tmpl ret := tmpl
themes, err := os.ReadDir("resource/template") themes, err := os.ReadDir("resource/template")
if err != nil { if err != nil {
log.Printf("NEZHA>> Error reading themes folder: %v", err) log.Printf("NEZHA>> Error reading themes folder: %v", err)
@@ -106,17 +94,61 @@ func loadThirdPartyTemplates(tmpl *template.Template) *template.Template {
if !theme.IsDir() { if !theme.IsDir() {
continue continue
} }
// load templates
t, err := ret.ParseGlob(fmt.Sprintf("resource/template/%s/*.html", theme.Name())) themeDir := theme.Name()
if err != nil { if themeDir == "theme-custom" {
log.Printf("NEZHA>> Error parsing templates %s error: %v", theme.Name(), err) // for backward compatibility
// note: will remove this in future versions
ret = loadTemplates(ret, themeDir)
continue 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 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{ var funcMap = template.FuncMap{
"tr": func(id string, dataAndCount ...interface{}) string { "tr": func(id string, dataAndCount ...interface{}) string {
conf := i18n.LocalizeConfig{ conf := i18n.LocalizeConfig{
+130 -25
View File
@@ -12,6 +12,7 @@ import (
"github.com/gin-gonic/gin" "github.com/gin-gonic/gin"
"github.com/jinzhu/copier" "github.com/jinzhu/copier"
"golang.org/x/net/idna"
"gorm.io/gorm" "gorm.io/gorm"
"github.com/naiba/nezha/model" "github.com/naiba/nezha/model"
@@ -38,6 +39,7 @@ func (ma *memberAPI) serve() {
mr.GET("/search-server", ma.searchServer) mr.GET("/search-server", ma.searchServer)
mr.GET("/search-tasks", ma.searchTask) mr.GET("/search-tasks", ma.searchTask)
mr.GET("/search-ddns", ma.searchDDNS)
mr.POST("/server", ma.addOrEditServer) mr.POST("/server", ma.addOrEditServer)
mr.POST("/monitor", ma.addOrEditMonitor) mr.POST("/monitor", ma.addOrEditMonitor)
mr.POST("/cron", ma.addOrEditCron) mr.POST("/cron", ma.addOrEditCron)
@@ -46,6 +48,7 @@ func (ma *memberAPI) serve() {
mr.POST("/batch-update-server-group", ma.batchUpdateServerGroup) mr.POST("/batch-update-server-group", ma.batchUpdateServerGroup)
mr.POST("/batch-delete-server", ma.batchDeleteServer) mr.POST("/batch-delete-server", ma.batchDeleteServer)
mr.POST("/notification", ma.addOrEditNotification) mr.POST("/notification", ma.addOrEditNotification)
mr.POST("/ddns", ma.addOrEditDDNS)
mr.POST("/nat", ma.addOrEditNAT) mr.POST("/nat", ma.addOrEditNAT)
mr.POST("/alert-rule", ma.addOrEditAlertRule) mr.POST("/alert-rule", ma.addOrEditAlertRule)
mr.POST("/setting", ma.updateSetting) mr.POST("/setting", ma.updateSetting)
@@ -211,6 +214,11 @@ func (ma *memberAPI) delete(c *gin.Context) {
if err == nil { if err == nil {
singleton.OnDeleteNotification(id) singleton.OnDeleteNotification(id)
} }
case "ddns":
err = singleton.DB.Unscoped().Delete(&model.DDNSProfile{}, "id = ?", id).Error
if err == nil {
singleton.OnDDNSUpdate()
}
case "nat": case "nat":
err = singleton.DB.Unscoped().Delete(&model.NAT{}, "id = ?", id).Error err = singleton.DB.Unscoped().Delete(&model.NAT{}, "id = ?", id).Error
if err == nil { 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 { type serverForm struct {
ID uint64 ID uint64
Name string `binding:"required"` Name string `binding:"required"`
DisplayIndex int DisplayIndex int
Secret string Secret string
Tag string Tag string
Note string Note string
PublicNote string PublicNote string
HideForGuest string HideForGuest string
EnableDDNS string EnableDDNS string
EnableIPv4 string DDNSProfilesRaw string
EnableIpv6 string
DDNSDomain string
DDNSProfile string
} }
func (ma *memberAPI) addOrEditServer(c *gin.Context) { func (ma *memberAPI) addOrEditServer(c *gin.Context) {
@@ -330,18 +356,18 @@ func (ma *memberAPI) addOrEditServer(c *gin.Context) {
s.PublicNote = sf.PublicNote s.PublicNote = sf.PublicNote
s.HideForGuest = sf.HideForGuest == "on" s.HideForGuest = sf.HideForGuest == "on"
s.EnableDDNS = sf.EnableDDNS == "on" s.EnableDDNS = sf.EnableDDNS == "on"
s.EnableIPv4 = sf.EnableIPv4 == "on" s.DDNSProfilesRaw = sf.DDNSProfilesRaw
s.EnableIpv6 = sf.EnableIpv6 == "on" err = utils.Json.Unmarshal([]byte(sf.DDNSProfilesRaw), &s.DDNSProfiles)
s.DDNSDomain = sf.DDNSDomain if err == nil {
s.DDNSProfile = sf.DDNSProfile if s.ID == 0 {
if s.ID == 0 { s.Secret, err = utils.GenerateRandomString(18)
s.Secret, err = utils.GenerateRandomString(18) if err == nil {
if err == nil { err = singleton.DB.Create(&s).Error
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 { 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 { type natForm struct {
ID uint64 ID uint64
Name string Name string
@@ -911,6 +1012,7 @@ type settingForm struct {
DashboardTheme string DashboardTheme string
CustomCode string CustomCode string
CustomCodeDashboard string CustomCodeDashboard string
CustomNameservers string
ViewPassword string ViewPassword string
IgnoredIPNotification string IgnoredIPNotification string
IPChangeNotificationTag string // IP变更提醒的通知组 IPChangeNotificationTag string // IP变更提醒的通知组
@@ -940,7 +1042,7 @@ func (ma *memberAPI) updateSetting(c *gin.Context) {
return return
} }
if _, yes := model.Themes[sf.DashboardTheme]; !yes { if _, yes := model.DashboardThemes[sf.DashboardTheme]; !yes {
c.JSON(http.StatusOK, model.Response{ c.JSON(http.StatusOK, model.Response{
Code: http.StatusBadRequest, Code: http.StatusBadRequest,
Message: fmt.Sprintf("后台主题不存在:%s", sf.DashboardTheme), 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.DashboardTheme = sf.DashboardTheme
singleton.Conf.Site.CustomCode = sf.CustomCode singleton.Conf.Site.CustomCode = sf.CustomCode
singleton.Conf.Site.CustomCodeDashboard = sf.CustomCodeDashboard singleton.Conf.Site.CustomCodeDashboard = sf.CustomCodeDashboard
singleton.Conf.DNSServers = sf.CustomNameservers
singleton.Conf.Site.ViewPassword = sf.ViewPassword singleton.Conf.Site.ViewPassword = sf.ViewPassword
singleton.Conf.Oauth2.Admin = sf.Admin singleton.Conf.Oauth2.Admin = sf.Admin
// 保证NotificationTag不为空 // 保证NotificationTag不为空
@@ -992,6 +1095,8 @@ func (ma *memberAPI) updateSetting(c *gin.Context) {
} }
// 更新系统语言 // 更新系统语言
singleton.InitLocalizer() singleton.InitLocalizer()
// 更新DNS服务器
singleton.OnNameserverUpdate()
c.JSON(http.StatusOK, model.Response{ c.JSON(http.StatusOK, model.Response{
Code: http.StatusOK, Code: http.StatusOK,
}) })
+12
View File
@@ -27,6 +27,7 @@ func (mp *memberPage) serve() {
mr.GET("/monitor", mp.monitor) mr.GET("/monitor", mp.monitor)
mr.GET("/cron", mp.cron) mr.GET("/cron", mp.cron)
mr.GET("/notification", mp.notification) mr.GET("/notification", mp.notification)
mr.GET("/ddns", mp.ddns)
mr.GET("/nat", mp.nat) mr.GET("/nat", mp.nat)
mr.GET("/setting", mp.setting) mr.GET("/setting", mp.setting)
mr.GET("/api", mp.api) 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) { func (mp *memberPage) nat(c *gin.Context) {
var data []model.NAT var data []model.NAT
singleton.DB.Find(&data) singleton.DB.Find(&data)
+9 -8
View File
@@ -1,6 +1,7 @@
package main package main
import ( import (
"os"
"context" "context"
"fmt" "fmt"
"log" "log"
@@ -32,13 +33,6 @@ func init() {
flag.StringVarP(&dashboardCliParam.ConfigFile, "config", "c", "data/config.yaml", "配置文件路径") flag.StringVarP(&dashboardCliParam.ConfigFile, "config", "c", "data/config.yaml", "配置文件路径")
flag.StringVar(&dashboardCliParam.DatebaseLocation, "db", "data/sqlite.db", "Sqlite3数据库文件路径") flag.StringVar(&dashboardCliParam.DatebaseLocation, "db", "data/sqlite.db", "Sqlite3数据库文件路径")
flag.Parse() flag.Parse()
// 初始化 dao 包
singleton.InitConfigFromPath(dashboardCliParam.ConfigFile)
singleton.InitTimezoneAndCache()
singleton.InitDBFromPath(dashboardCliParam.DatebaseLocation)
singleton.InitLocalizer()
initSystem()
} }
func initSystem() { func initSystem() {
@@ -59,9 +53,16 @@ func initSystem() {
func main() { func main() {
if dashboardCliParam.Version { if dashboardCliParam.Version {
fmt.Println(singleton.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 // TODO 使用 cmux 在同一端口服务 HTTP 和 gRPC
singleton.CleanMonitorHistory() singleton.CleanMonitorHistory()
go rpc.ServeRPC(singleton.Conf.GRPCPort) go rpc.ServeRPC(singleton.Conf.GRPCPort)
+10
View File
@@ -14,6 +14,10 @@ require (
github.com/hashicorp/go-uuid v1.0.3 github.com/hashicorp/go-uuid v1.0.3
github.com/jinzhu/copier v0.4.0 github.com/jinzhu/copier v0.4.0
github.com/json-iterator/go v1.1.12 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/nicksnyder/go-i18n/v2 v2.4.0
github.com/ory/graceful v0.1.3 github.com/ory/graceful v0.1.3
github.com/oschwald/maxminddb-golang v1.13.1 github.com/oschwald/maxminddb-golang v1.13.1
@@ -21,6 +25,7 @@ require (
github.com/robfig/cron/v3 v3.0.1 github.com/robfig/cron/v3 v3.0.1
github.com/spf13/pflag v1.0.5 github.com/spf13/pflag v1.0.5
github.com/spf13/viper v1.18.2 github.com/spf13/viper v1.18.2
github.com/tidwall/gjson v1.18.0
github.com/xanzy/go-gitlab v0.103.0 github.com/xanzy/go-gitlab v0.103.0
golang.org/x/crypto v0.25.0 golang.org/x/crypto v0.25.0
golang.org/x/net v0.27.0 golang.org/x/net v0.27.0
@@ -70,14 +75,19 @@ require (
github.com/spf13/afero v1.11.0 // indirect github.com/spf13/afero v1.11.0 // indirect
github.com/spf13/cast v1.6.0 // indirect github.com/spf13/cast v1.6.0 // indirect
github.com/subosito/gotenv 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/twitchyliquid64/golang-asm v0.15.1 // indirect
github.com/ugorji/go/codec v1.2.11 // indirect github.com/ugorji/go/codec v1.2.11 // indirect
go.uber.org/atomic v1.9.0 // indirect go.uber.org/atomic v1.9.0 // indirect
go.uber.org/multierr v1.9.0 // indirect go.uber.org/multierr v1.9.0 // indirect
golang.org/x/arch v0.3.0 // indirect golang.org/x/arch v0.3.0 // indirect
golang.org/x/exp v0.0.0-20230905200255-921286631fa9 // 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/sys v0.22.0 // indirect
golang.org/x/time v0.5.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 google.golang.org/genproto/googleapis/rpc v0.0.0-20240227224415-6ceb2ff114de // indirect
gopkg.in/ini.v1 v1.67.0 // indirect gopkg.in/ini.v1 v1.67.0 // indirect
gopkg.in/yaml.v3 v3.0.1 // 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.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 h1:XlAE/cm/ms7TE/VMVoduSpNBoyc2dOxHs5MZSwAN63Q=
github.com/leodido/go-urn v1.2.4/go.mod h1:7ZrI8mTSeBSHl/UaRyKQW1qZeMgak41ANeCNaVckg+4= 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 h1:IeQXZAiQcpL9mgcAe1Nu6cX9LLw6ExEHKjN0VQdvPDY=
github.com/magiconair/properties v1.8.7/go.mod h1:Dhd985XPs7jluiymwWYZ0G4Z61jb3vdS329zhj2hYo0= github.com/magiconair/properties v1.8.7/go.mod h1:Dhd985XPs7jluiymwWYZ0G4Z61jb3vdS329zhj2hYo0=
github.com/mattn/go-colorable v0.1.13 h1:fFA4WZxdEF4tXPZVKMLwD8oUnCTTo08duU7wxecdEvA= 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-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 h1:mCRHCLDUBXgpKAqIKsaAaAsrAlbkeomtRFKXh2L6YIM=
github.com/mattn/go-sqlite3 v1.14.17/go.mod h1:2eHXhiwb8IkHr+BDWZGa96P6+rkvnG63S2DGjv9HUNg= 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 h1:jeMsZIYE/09sWLaz43PL7Gy6RuMjD2eJVyuac5Z2hdY=
github.com/mitchellh/mapstructure v1.5.0/go.mod h1:bFUtVrKA4DC2yAKiSyO/QUcy7e+RRV2QTWOzhPopBRo= 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= 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/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 h1:xBagoLtFs94CBntxluKeaWgTMpvLxC4ur3nMaC9Gz0M=
github.com/modern-go/reflect2 v1.0.2/go.mod h1:yWuevngMOJpCy52FWWMvUC8ws7m/LJsjYzDa0/r8luk= 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 h1:3IcvPOAvnCKwNm0TB0dLDTuawWEj+ax/RERNC+diLMM=
github.com/nicksnyder/go-i18n/v2 v2.4.0/go.mod h1:nxYSZE9M0bf3Y70gPQjN9ha7XNHX7gMc814+6wVyEI4= 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= 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/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 h1:9NlTDc1FTs4qu0DDq7AEtTPNw6SVm7uBMsUCUjABIf8=
github.com/subosito/gotenv v1.6.0/go.mod h1:Dk4QP5c2W3ibzajGcXpNraDfq2IrhjMIvMSWPKKo0FU= 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 h1:SU5vSMR7hnwNxj24w34ZyCi/FmDZTkS4MhqMhdFk5YI=
github.com/twitchyliquid64/golang-asm v0.15.1/go.mod h1:a1lVb/DtPvCB8fslRZhAngC2+aY1QWCk3Cedj/Gdt08= 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= 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/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 h1:GoHiUyI/Tp2nVkLI2mCxVkOjsbSXD66ic0XW0js0R9g=
golang.org/x/exp v0.0.0-20230905200255-921286631fa9/go.mod h1:S2oDrQGGwySpoQPVqRShND87VCbxmc6bL1Yd2oYrm6k= 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-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg=
golang.org/x/net v0.0.0-20210226172049-e18ecbb05110/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg= golang.org/x/net v0.0.0-20210226172049-e18ecbb05110/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg=
golang.org/x/net v0.27.0 h1:5K3Njcw06/l2y9vpGCSdcxWOYHOUk3dVNGDXN+FvAys= 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 h1:o7cqy6amK/52YcAKIPlM3a+Fpj35zvRj2TP+e1xFSfk=
golang.org/x/time v0.5.0/go.mod h1:3BpzKBy/shNhVucY/MWOyx10tF3SFh9QdLuxbVysPQM= 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.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.22.0 h1:gqSGLZqv+AI9lIQzniJ0nZDRG5GBPsSi+DRNHWNz6yA=
golang.org/x/tools v0.21.1-0.20240508182429-e35e4ccd0d2d/go.mod h1:aiJjzUbINMkxbQROHiO6hDPo2LHcIPhhQsa9DLh0yGk= 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= 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 h1:cZGRis4/ot9uVm639a+rHCUaG0JJHEsdyzSQTMX+suY=
google.golang.org/genproto/googleapis/rpc v0.0.0-20240227224415-6ceb2ff114de/go.mod h1:H4O17MA/PE9BsGx3w+a+W2VOLLD1Qf7oJneAoU6WktY= 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 MaxTCPPingValue int32
AvgPingCount int AvgPingCount int
// 动态域名解析更新 DNSServers string
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
} }
// Read 读取配置文件并应用 // Read 读取配置文件并应用
@@ -190,9 +168,6 @@ func (c *Config) Read(path string) error {
if c.AvgPingCount == 0 { if c.AvgPingCount == 0 {
c.AvgPingCount = 2 c.AvgPingCount = 2
} }
if c.DDNS.MaxRetries == 0 {
c.DDNS.MaxRetries = 3
}
if c.Oauth2.OidcScopes == "" { if c.Oauth2.OidcScopes == "" {
c.Oauth2.OidcScopes = "openid,profile,email" 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] return string(msgBytes)[1 : len(msgBytes)-1]
}), nil }), nil
case NotificationRequestTypeForm: case NotificationRequestTypeForm:
var data map[string]string data, err := utils.GjsonParseStringMap(n.RequestBody)
if err := utils.Json.Unmarshal([]byte(n.RequestBody), &data); err != nil { if err != nil {
return "", err return "", err
} }
params := url.Values{} params := url.Values{}
@@ -99,8 +99,8 @@ func (n *Notification) setRequestHeader(req *http.Request) error {
if n.RequestHeader == "" { if n.RequestHeader == "" {
return nil return nil
} }
var m map[string]string m, err := utils.GjsonParseStringMap(n.RequestHeader)
if err := utils.Json.Unmarshal([]byte(n.RequestHeader), &m); err != nil { if err != nil {
return err return err
} }
for k, v := range m { for k, v := range m {
+24 -14
View File
@@ -3,27 +3,28 @@ package model
import ( import (
"fmt" "fmt"
"html/template" "html/template"
"log"
"sync" "sync"
"time" "time"
"github.com/naiba/nezha/pkg/utils" "github.com/naiba/nezha/pkg/utils"
pb "github.com/naiba/nezha/proto" pb "github.com/naiba/nezha/proto"
"gorm.io/gorm"
) )
type Server struct { type Server struct {
Common Common
Name string Name string
Tag string // 分组名 Tag string // 分组名
Secret string `gorm:"uniqueIndex" json:"-"` Secret string `gorm:"uniqueIndex" json:"-"`
Note string `json:"-"` // 管理员可见备注 Note string `json:"-"` // 管理员可见备注
PublicNote string `json:"PublicNote,omitempty"` // 公开备注 PublicNote string `json:"PublicNote,omitempty"` // 公开备注
DisplayIndex int // 展示排序,越大越靠前 DisplayIndex int // 展示排序,越大越靠前
HideForGuest bool // 对游客隐藏 HideForGuest bool // 对游客隐藏
EnableDDNS bool `json:"-"` // 是否启用DDNS 未在配置文件中启用DDNS 或 DDNS检查时间为0时此项无效 EnableDDNS bool // 启用DDNS
EnableIPv4 bool `json:"-"` // 是否启用DDNS IPv4 DDNSProfiles []uint64 `gorm:"-" json:"-"` // DDNS配置
EnableIpv6 bool `json:"-"` // 是否启用DDNS IPv6
DDNSDomain string `json:"-"` // DDNS中的前缀 如基础域名为abc.oracle DDNSName为mjj 就会把mjj.abc.oracle解析服务器IP 为空则停用 DDNSProfilesRaw string `gorm:"default:'[]';column:ddns_profiles_raw" json:"-"`
DDNSProfile string `json:"-"` // DDNS配置
Host *Host `gorm:"-"` Host *Host `gorm:"-"`
State *HostState `gorm:"-"` State *HostState `gorm:"-"`
@@ -48,6 +49,16 @@ func (s *Server) CopyFromRunningServer(old *Server) {
s.PrevTransferOutSnapshot = old.PrevTransferOutSnapshot 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 { func boolToString(b bool) string {
if b { if b {
return "true" return "true"
@@ -60,8 +71,7 @@ func (s Server) MarshalForDashboard() template.JS {
tag, _ := utils.Json.Marshal(s.Tag) tag, _ := utils.Json.Marshal(s.Tag)
note, _ := utils.Json.Marshal(s.Note) note, _ := utils.Json.Marshal(s.Note)
secret, _ := utils.Json.Marshal(s.Secret) secret, _ := utils.Json.Marshal(s.Secret)
ddnsDomain, _ := utils.Json.Marshal(s.DDNSDomain) ddnsProfilesRaw, _ := utils.Json.Marshal(s.DDNSProfilesRaw)
ddnsProfile, _ := utils.Json.Marshal(s.DDNSProfile)
publicNote, _ := utils.Json.Marshal(s.PublicNote) 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 package ddns
import "golang.org/x/net/publicsuffix" import (
"context"
"fmt"
"log"
"strings"
"time"
type DomainConfig struct { "github.com/libdns/libdns"
EnableIPv4 bool "github.com/miekg/dns"
EnableIpv6 bool
FullDomain string "github.com/naiba/nezha/model"
Ipv4Addr string "github.com/naiba/nezha/pkg/utils"
Ipv6Addr string )
var (
dnsTimeOut = 10 * time.Second
customDNSServers []string
)
type IP struct {
Ipv4Addr string
Ipv6Addr string
} }
type Provider interface { type Provider struct {
// UpdateDomain Return is updated ctx context.Context
UpdateDomain(*DomainConfig) error 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) { func InitDNSServers(s string) {
realDomain, _ = publicsuffix.EffectiveTLDPlusOne(domain) if s != "" {
prefix = domain[:len(domain)-len(realDomain)-1] customDNSServers = strings.Split(s, ",")
return prefix, realDomain }
}
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, "/monitor": true,
"/setting": true, "/setting": true,
"/notification": true, "/notification": true,
"/ddns": true,
"/nat": true, "/nat": true,
"/cron": true, "/cron": true,
"/api": 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, SkipVerifySSL: false,
}), }),
}) })
http.DefaultClient.Timeout = time.Minute * 10
} }
type _httpTransport struct { type _httpTransport struct {
+5 -14
View File
@@ -3,7 +3,6 @@ package utils
import ( import (
"crypto/rand" "crypto/rand"
"math/big" "math/big"
"net/http"
"os" "os"
"regexp" "regexp"
"strings" "strings"
@@ -11,7 +10,11 @@ import (
jsoniter "github.com/json-iterator/go" 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 { func IsWindows() bool {
return os.PathSeparator == '\\' && os.PathListSeparator == ';' return os.PathSeparator == '\\' && os.PathListSeparator == ';'
@@ -87,15 +90,3 @@ func Uint64SubInt64(a uint64, b int64) uint64 {
} }
return a - uint64(b) 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])
}
}
}
+84 -36
View File
@@ -23,19 +23,19 @@ other = "Home"
other = "Back to Homepage" other = "Back to Homepage"
[AdminPanel] [AdminPanel]
other = "Admin Panel" other = "Dashboard"
[Logout] [Logout]
other = "Logout" other = "Log out"
[Login] [Login]
other = "Login" other = "Log in"
[ConfirmLogout] [ConfirmLogout]
other = "Do you want to Logout?" other = "Confirm Logging Out?"
[AfterLoggingOutYouHaveToLoginAgain] [AfterLoggingOutYouHaveToLoginAgain]
other = "You need to Login again to continue" other = "You will need to log in again to continue"
[Cancel] [Cancel]
other = "Cancel" other = "Cancel"
@@ -56,7 +56,7 @@ other = "Cron Expression"
other = "Backup" other = "Backup"
[3amDaily] [3amDaily]
other = "(Daily at 3 AM)" other = "(At 3 AM)"
[Command] [Command]
other = "Command" other = "Command"
@@ -71,7 +71,7 @@ other = "Ignore All, Execute Only on Specific Servers"
other = "Cover All, Except Specific Servers" other = "Cover All, Except Specific Servers"
[ExecuteByTriggerServer] [ExecuteByTriggerServer]
other = "Execute on Trigger Server" other = "Execute on Alarmed Servers"
[SpecificServers] [SpecificServers]
other = "Specific Servers" other = "Specific Servers"
@@ -104,7 +104,7 @@ other = "Sec Min Hour Day Month Week, see details in "
other = "CRON Expression Format" other = "CRON Expression Format"
[IntroductionOfCommands] [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] [AddMonitor]
other = "Add Service Monitor" other = "Add Service Monitor"
@@ -119,7 +119,7 @@ other = "Target"
other = "Type" other = "Type"
[SslExpirationOrChange] [SslExpirationOrChange]
other = "(SSL Expiration or Change)" other = "(Certificate Expiration and Changes)"
[Duration] [Duration]
other = "Interval" other = "Interval"
@@ -134,10 +134,10 @@ other = "Enable Failure Notification"
other = "Failure Notification" other = "Failure Notification"
[MaxLatency] [MaxLatency]
other = "Max Latency (ms)" other = "Maximum Latency (ms)"
[MinLatency] [MinLatency]
other = "Min Latency (ms)" other = "Minimum Latency (ms)"
[EnableLatencyNotification] [EnableLatencyNotification]
other = "Enable Latency Notification" other = "Enable Latency Notification"
@@ -147,7 +147,7 @@ other = "Latency Notification"
[IntroductionOfMonitor] [IntroductionOfMonitor]
other = """ 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>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""" 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" other = "Always Trigger"
[ModeOnetimeTrigger] [ModeOnetimeTrigger]
other = "Single Trigger" other = "Trigger Once"
[EnableTriggerTask] [EnableTriggerTask]
other = "Enable Trigger Task" other = "Enable Trigger Task"
[FailTriggerTasks] [FailTriggerTasks]
other = "Tasks triggered during an alarm" other = "Tasks to trigger on an alarm"
[RecoverTriggerTasks] [RecoverTriggerTasks]
other = "Tasks triggered during recovery" other = "Tasks to trigger after recovery"
[Enable] [Enable]
other = "Enable" other = "Enable"
@@ -218,7 +218,7 @@ other = "Einstein Lightspeed 1"
other = "Display Index" other = "Display Index"
[TheLargerTheNumberTheHigherThePriority] [TheLargerTheNumberTheHigherThePriority]
other = "Higher Number, Higher Priority" other = "Greater Number, Higher Priority"
[Secret] [Secret]
other = "Secret" other = "Secret"
@@ -230,10 +230,10 @@ other = "Note"
other = "Public Note" other = "Public Note"
[LinuxOneKeyInstall] [LinuxOneKeyInstall]
other = "Linux One-Command Install" other = "Linux Installation Command"
[NoDomainAlert] [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] [PushSuccessfully]
other = "Send Success Notification" other = "Send Success Notification"
@@ -254,13 +254,13 @@ other = "Cover All"
other = "Ignore All" other = "Ignore All"
[ByTrigger] [ByTrigger]
other = "Execute by Trigger" other = "Alarmed Servers"
[DeleteScheduledTask] [DeleteScheduledTask]
other = "Delete Scheduled Task" other = "Delete Scheduled Task"
[ConfirmToDeleteThisScheduledTask] [ConfirmToDeleteThisScheduledTask]
other = "Are you sure you want to Delete?" other = "Confirm deletion?"
[AccessDenied] [AccessDenied]
other = "Access Denied" other = "Access Denied"
@@ -281,22 +281,22 @@ other = "Don't Have an Account?"
other = "HTTP(S)/SSL Certificate" other = "HTTP(S)/SSL Certificate"
[TCPPort] [TCPPort]
other = "TCP Port" other = "TCPing"
[DeleteService] [DeleteService]
other = "Remove Service Monitor" other = "Remove Service Monitor"
[ConfirmToDeleteThisService] [ConfirmToDeleteThisService]
other = "Confirm to Delete This Service?" other = "Confirm Deleting This Service?"
[DeleteNotificationMethod] [DeleteNotificationMethod]
other = "Delete Notification Method" other = "Delete Notification Method"
[ConfirmToDeleteThisNotificationMethod] [ConfirmToDeleteThisNotificationMethod]
other = "Confirm to Delete This Notification Method?" other = "Confirm Deleting This Notification Method?"
[ForceUpdate] [ForceUpdate]
other = "Force Update Agent" other = "Trigger Agent Update"
[SelectAll] [SelectAll]
other = "Select All" other = "Select All"
@@ -305,7 +305,7 @@ other = "Select All"
other = "Version" other = "Version"
[OneKeyInstall] [OneKeyInstall]
other = "One-Command Install" other = "Installation commands"
[ClickToCopy] [ClickToCopy]
other = "Click To Copy" other = "Click To Copy"
@@ -314,7 +314,7 @@ other = "Click To Copy"
other = "Delete Server" other = "Delete Server"
[ConfirmToDeleteServer] [ConfirmToDeleteServer]
other = "Confirm to Delete Server?" other = "Confirm Deleting This Server?"
[NoServerSelected] [NoServerSelected]
other = "No Server Selected" other = "No Server Selected"
@@ -545,7 +545,7 @@ other = "Scheduled Tasks"
other = "API Management" other = "API Management"
[IssueNewApiToken] [IssueNewApiToken]
other = "Issue New API Token" other = "Generate New API Token"
[Token] [Token]
other = "Token" other = "Token"
@@ -554,10 +554,10 @@ other = "Token"
other = "Delete Token" other = "Delete Token"
[ConfirmToDeleteThisToken] [ConfirmToDeleteThisToken]
other = "Confirm to Delete This Token?" other = "Confirm Deleting This Token?"
[YouAreNotAuthorized] [YouAreNotAuthorized]
other = "Not Authorized for This Page" other = "This Page needs Authorization"
[WrongAccessPassword] [WrongAccessPassword]
other = "Wrong Access Password" other = "Wrong Access Password"
@@ -605,7 +605,7 @@ other = "IP Changed"
other = "Remaining Data" other = "Remaining Data"
[DashboardTheme] [DashboardTheme]
other = "Admin Panel Theme" other = "Dashboard Theme"
[Info] [Info]
other = "Info" other = "Info"
@@ -622,20 +622,62 @@ other = "Network"
[EnableShowInService] [EnableShowInService]
other = "Enable Show in Service" other = "Enable Show in Service"
[DDNS]
other = "Dynamic DNS"
[DDNSProfiles]
other = "DDNS Profiles"
[AddDDNSProfile]
other = "New Profile"
[EnableDDNS] [EnableDDNS]
other = "Enable DDNS" other = "Enable DDNS"
[EnableIPv4] [EnableIPv4]
other = "Enable DDNS IPv4" other = "IPv4 Enabled"
[EnableIpv6] [EnableIPv6]
other = "Enable DDNS IPv6" other = "IPv6 Enabled"
[DDNSDomain] [DDNSDomain]
other = "DDNS Domain" other = "Domains"
[DDNSProfile] [DDNSDomains]
other = "DDNS Profile Name" 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] [Feature]
other = "Feature" other = "Feature"
@@ -705,3 +747,9 @@ other = "Expired"
[Days] [Days]
other = "d" 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" other = "Incluir todo, excepto servidores específicos"
[ExecuteByTriggerServer] [ExecuteByTriggerServer]
other = "Ejecutar por servidor disparador" other = "Ejecutar en servidores con alarma"
[SpecificServers] [SpecificServers]
other = "Servidores Específicos" 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" other = "Formato de Expresión de Cron"
[IntroductionOfCommands] [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] [AddMonitor]
other = "Agregar Monitor de Servicio" other = "Agregar Monitor de Servicio"
@@ -254,7 +254,7 @@ other = "Cubrir Todo"
other = "Ignorar Todo" other = "Ignorar Todo"
[ByTrigger] [ByTrigger]
other = "Ejecutar por Disparador" other = "Servidores con alarma"
[DeleteScheduledTask] [DeleteScheduledTask]
other = "Eliminar Tarea Programada" other = "Eliminar Tarea Programada"
@@ -281,7 +281,7 @@ other = "¿No tiene una cuenta?"
other = "Certificados HTTP(S)/SSL" other = "Certificados HTTP(S)/SSL"
[TCPPort] [TCPPort]
other = "Puerto TCP" other = "TCPing"
[DeleteService] [DeleteService]
other = "Eliminar Servicio" other = "Eliminar Servicio"
@@ -622,20 +622,62 @@ other = "Red"
[EnableShowInService] [EnableShowInService]
other = "Mostrar en servicio" other = "Mostrar en servicio"
[DDNS]
other = "DNS Dinámico"
[DDNSProfiles]
other = "Perfiles DDNS"
[AddDDNSProfile]
other = "Nuevo Perfil"
[EnableDDNS] [EnableDDNS]
other = "Habilitar DDNS" other = "Habilitar DDNS"
[EnableIPv4] [EnableIPv4]
other = "Habilitar DDNS IPv4" other = "IPv4 Activado"
[EnableIpv6] [EnableIPv6]
other = "Habilitar DDNS IPv6" other = "IPv6 Activado"
[DDNSDomain] [DDNSDomain]
other = "Dominio DDNS" other = "Dominios"
[DDNSProfile] [DDNSDomains]
other = "Nombre del perfil de DDNS" 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] [Feature]
other = "Característica" other = "Característica"
@@ -705,3 +747,9 @@ other = "Expired"
[Days] [Days]
other = "d" 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 = "覆盖所有,仅特定服务器不执行" other = "覆盖所有,仅特定服务器不执行"
[ExecuteByTriggerServer] [ExecuteByTriggerServer]
other = "由触发的服务器执行" other = "由触发告警的服务器执行"
[SpecificServers] [SpecificServers]
other = "特定服务器" other = "特定服务器"
@@ -104,7 +104,7 @@ other = "秒 分 时 天 月 星期,详情见"
other = "计划表达式格式" other = "计划表达式格式"
[IntroductionOfCommands] [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] [AddMonitor]
other = "新增监控" other = "新增监控"
@@ -254,7 +254,7 @@ other = "覆盖所有"
other = "忽略所有" other = "忽略所有"
[ByTrigger] [ByTrigger]
other = "触发执行" other = "触发告警的服务器"
[DeleteScheduledTask] [DeleteScheduledTask]
other = "删除计划任务" other = "删除计划任务"
@@ -281,7 +281,7 @@ other = "没有账号?"
other = "HTTP(S)/SSL证书" other = "HTTP(S)/SSL证书"
[TCPPort] [TCPPort]
other = "TCP 端口" other = "TCPing"
[DeleteService] [DeleteService]
other = "移除服务" other = "移除服务"
@@ -425,7 +425,7 @@ other = "活动"
other = "版本" other = "版本"
[NetSpeed] [NetSpeed]
other = "网" other = "网"
[Uptime] [Uptime]
other = "在线" other = "在线"
@@ -622,20 +622,62 @@ other = "网络"
[EnableShowInService] [EnableShowInService]
other = "在服务中显示" other = "在服务中显示"
[DDNS]
other = "动态 DNS"
[DDNSProfiles]
other = "DDNS配置"
[AddDDNSProfile]
other = "新配置"
[EnableDDNS] [EnableDDNS]
other = "启用DDNS" other = "启用 DDNS"
[EnableIPv4] [EnableIPv4]
other = "启用DDNS IPv4" other = "启用 DDNS IPv4"
[EnableIpv6] [EnableIPv6]
other = "启用DDNS IPv6" other = "启用 DDNS IPv6"
[DDNSDomain] [DDNSDomain]
other = "DDNS域名" other = "DDNS 域名"
[DDNSProfile] [DDNSDomains]
other = "DDNS配置名" 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] [Feature]
other = "功能" other = "功能"
@@ -705,3 +747,9 @@ other = "已到期"
[Days] [Days]
other = "天" other = "天"
[CustomNameservers]
other = "自定义DDNS使用的公共DNS服务器(逗号分隔)"
[Plan]
other = "套餐"
+60 -12
View File
@@ -71,7 +71,7 @@ other = "忽略所有,僅通過特定伺服器執行"
other = "覆蓋所有,僅特定伺服器不執行" other = "覆蓋所有,僅特定伺服器不執行"
[ExecuteByTriggerServer] [ExecuteByTriggerServer]
other = "由觸發的伺服器執行" other = "由觸發告警的伺服器執行"
[SpecificServers] [SpecificServers]
other = "特定伺服器" other = "特定伺服器"
@@ -104,7 +104,7 @@ other = "秒 分 時 天 月 星期,詳情見"
other = "排程表達式格式" other = "排程表達式格式"
[IntroductionOfCommands] [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] [AddMonitor]
other = "新增監控" other = "新增監控"
@@ -254,7 +254,7 @@ other = "覆蓋所有"
other = "忽略所有" other = "忽略所有"
[ByTrigger] [ByTrigger]
other = "觸發執行" other = "觸發告警的伺服器"
[DeleteScheduledTask] [DeleteScheduledTask]
other = "刪除排程任務" other = "刪除排程任務"
@@ -281,7 +281,7 @@ other = "沒有帳號?"
other = "HTTP(S)/SSL證書" other = "HTTP(S)/SSL證書"
[TCPPort] [TCPPort]
other = "TCP 端口" other = "TCPing"
[DeleteService] [DeleteService]
other = "移除服務" other = "移除服務"
@@ -425,7 +425,7 @@ other = "活動"
other = "版本" other = "版本"
[NetSpeed] [NetSpeed]
other = "網" other = "網"
[Uptime] [Uptime]
other = "在線" other = "在線"
@@ -622,20 +622,62 @@ other = "網路"
[EnableShowInService] [EnableShowInService]
other = "在服務中顯示" other = "在服務中顯示"
[DDNS]
other = "動態 DNS"
[DDNSProfiles]
other = "DDNS配置"
[AddDDNSProfile]
other = "新增配置"
[EnableDDNS] [EnableDDNS]
other = "啟用DDNS" other = "啟用 DDNS"
[EnableIPv4] [EnableIPv4]
other = "啟用DDNS IPv4" other = "啟用 DDNS IPv4"
[EnableIpv6] [EnableIPv6]
other = "啟用DDNS IPv6" other = "啟用 DDNS IPv6"
[DDNSDomain] [DDNSDomain]
other = "DDNS網域" other = "DDNS 域名"
[DDNSProfile] [DDNSDomains]
other = "DDNS設定名" 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] [Feature]
other = "功能" other = "功能"
@@ -705,3 +747,9 @@ other = "已到期"
[Days] [Days]
other = "天" other = "天"
[CustomNameservers]
other = "自訂DDNS使用的公共DNS伺服器(逗號分隔)"
[Plan]
other = "套餐"
+13 -1
View File
@@ -2,10 +2,14 @@ package resource
import ( import (
"embed" "embed"
"github.com/naiba/nezha/pkg/utils"
) )
var StaticFS *utils.HybridFS
//go:embed static //go:embed static
var StaticFS embed.FS var staticFS embed.FS
//go:embed template //go:embed template
var TemplateFS embed.FS var TemplateFS embed.FS
@@ -13,6 +17,14 @@ var TemplateFS embed.FS
//go:embed l10n //go:embed l10n
var I18nFS embed.FS 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 { func IsTemplateFileExist(name string) bool {
_, err := TemplateFS.Open(name) _, err := TemplateFS.Open(name)
return err == nil return err == nil
+110 -16
View File
@@ -99,7 +99,11 @@ function showFormModal(modelSelector, formID, URL, getData) {
item.name === "DisplayIndex" || item.name === "DisplayIndex" ||
item.name === "Type" || item.name === "Type" ||
item.name === "Cover" || 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); obj[item.name] = parseInt(item.value);
} else if (item.name.endsWith("Latency")) { } 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; return obj;
}, {}); }, {});
$.post(URL, JSON.stringify(data)) $.post(URL, JSON.stringify(data))
@@ -207,6 +221,7 @@ function addOrEditAlertRule(rule) {
); );
} }
} }
// 需要在 showFormModal 进一步拼接数组
modal modal
.find("input[name=FailTriggerTasksRaw]") .find("input[name=FailTriggerTasksRaw]")
.val(rule ? "[]," + failTriggerTasks.substr(1, failTriggerTasks.length - 2) : "[]"); .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) { function addOrEditNAT(nat) {
const modal = $(".nat.modal"); const modal = $(".nat.modal");
modal.children(".header").text((nat ? LANG.Edit : LANG.Add)); 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=id]").val(server ? server.ID : null);
modal.find("input[name=name]").val(server ? server.Name : 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=Tag]").val(server ? server.Tag : null);
modal.find("input[name=DDNSDomain]").val(server ? server.DDNSDomain : null); modal.find("a.ui.label.visible").each((i, el) => {
modal.find("input[name=DDNSProfile]").val(server ? server.DDNSProfile : null); 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 modal
.find("input[name=DisplayIndex]") .find("input[name=DisplayIndex]")
.val(server ? server.DisplayIndex : null); .val(server ? server.DisplayIndex : null);
@@ -342,26 +431,17 @@ function addOrEditServer(server, conf) {
modal.find(".command.field").attr("style", "display:none"); modal.find(".command.field").attr("style", "display:none");
modal.find("input[name=secret]").val(""); 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) { if (server && server.EnableDDNS) {
modal.find(".ui.enableddns.checkbox").checkbox("set checked"); modal.find(".ui.enableddns.checkbox").checkbox("set checked");
} else { } else {
modal.find(".ui.enableddns.checkbox").checkbox("set unchecked"); modal.find(".ui.enableddns.checkbox").checkbox("set unchecked");
} }
if (server && server.EnableIPv4) { if (server && server.HideForGuest) {
modal.find(".ui.enableipv4.checkbox").checkbox("set checked"); modal.find(".ui.hideforguest.checkbox").checkbox("set checked");
} else { } else {
modal.find(".ui.enableipv4.checkbox").checkbox("set unchecked"); modal.find(".ui.hideforguest.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");
} }
showFormModal(".server.modal", "#serverForm", "/api/server"); showFormModal(".server.modal", "#serverForm", "/api/server");
} }
@@ -447,6 +527,7 @@ function addOrEditMonitor(monitor) {
); );
} }
} }
// 需要在 showFormModal 进一步拼接数组
modal modal
.find("input[name=FailTriggerTasksRaw]") .find("input[name=FailTriggerTasksRaw]")
.val(monitor ? "[]," + failTriggerTasks.substr(1, failTriggerTasks.length - 2) : "[]"); .val(monitor ? "[]," + failTriggerTasks.substr(1, failTriggerTasks.length - 2) : "[]");
@@ -492,6 +573,7 @@ function addOrEditCron(cron) {
); );
} }
} }
// 需要在 showFormModal 进一步拼接数组
modal modal
.find("input[name=ServersRaw]") .find("input[name=ServersRaw]")
.val(cron ? "[]," + servers.substr(1, servers.length - 2) : "[]"); .val(cron ? "[]," + servers.substr(1, servers.length - 2) : "[]");
@@ -621,3 +703,15 @@ $(document).ready(() => {
}); });
} catch (error) { } } 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.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); 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:focus,
body[theme="dark"] .navbar .navbar-nav > .open > a:hover, body[theme="dark"] .navbar .navbar-nav > .open > a:hover,
body[theme="dark"] .navbar .navbar-nav > .active > a, 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; background-image: none;
box-shadow: none; box-shadow: none;
background-color: transparent; background-color: transparent;
@@ -46,8 +46,6 @@ body[theme="dark"] .content {
background-color: rgba(28, 29, 38, 1); background-color: rgba(28, 29, 38, 1);
border: none; border: none;
box-shadow: rgba(0, 0, 0, 0.5) 0 0.625em 2em; 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 { body[theme="dark"] .table {
@@ -58,7 +56,7 @@ body[theme="dark"] .table {
body[theme="dark"] .table > tbody > tr > td:before, body[theme="dark"] .table > tbody > tr > td:before,
body[theme="dark"] .table > tfoot > 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 > 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); 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); 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; background-color: unset;
} }
@@ -83,13 +81,18 @@ body[theme="dark"] .table-hover > tbody > tr:not(.expandRow):hover > td {
} }
/* expandRow展开部分样式 */ /* 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); 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); background-color: rgba(28, 29, 38, 1);
} }
body[theme="dark"] .plan {
background-image: none;
background-color: rgba(255, 255, 255, 0.075);
}
/* expandRow展开部分样式结束 */ /* expandRow展开部分样式结束 */
body[theme="dark"] .progress { body[theme="dark"] .progress {
@@ -155,29 +158,29 @@ body[theme="dark"] footer p a:hover {
text-decoration: none; text-decoration: none;
} }
body[theme="dark"] .modal-content{ body[theme="dark"] .modal-content {
background-color: rgba(28, 29, 38, 1); background-color: rgba(28, 29, 38, 1);
color: #f1f1f1; color: #f1f1f1;
} }
body[theme="dark"] #earthChartBox .modal-content{ body[theme="dark"] #earthChartBox .modal-content {
background-color: rgba(0, 0, 0, 1); background-color: rgba(0, 0, 0, 1);
color: #f1f1f1; color: #f1f1f1;
} }
body[theme="dark"] .modal-header{ body[theme="dark"] .modal-header {
border-bottom-color:#080808; border-bottom-color:#080808;
} }
body[theme="dark"] .modal-header i.xclose{ body[theme="dark"] .modal-header i.xclose {
color: #f1f1f1; color: #f1f1f1;
} }
body[theme="dark"] .toolbox i{ body[theme="dark"] .toolbox i {
color: rgba(241, 241, 241, 1); color: rgba(241, 241, 241, 1);
background-color: rgba(0, 0, 0, 0.5); 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); 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); background-color: rgba(28, 29, 38, 0.8);
border: none; border: none;
box-shadow: rgba(0, 0, 0, 0.5) 0 0.625em 2em; 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, 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 > tbody > tr > td:before,
body[theme="dark"] .table > tfoot > 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 > 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 颜色 **/ /** border-bottom 颜色 **/
background-color: rgba(155, 155, 155, 0.1); background-color: rgba(155, 155, 155, 0.1);
} }
@@ -96,11 +94,11 @@ body[theme="dark"] .table-hover > tbody > tr:not(.expandRow):hover > td {
} }
/* expandRow展开部分样式 */ /* expandRow展开部分样式 */
body[theme="dark"] .table > tbody > tr.expandRow.odd > td:before{ body[theme="dark"] .table > tbody > tr.expandRow.odd > td:before {
background-color: unset; 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; background-color: unset;
} }
/* expandRow展开部分样式结束 */ /* 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); color: rgba(0, 0, 0, 0.87);
} }
body[theme="light"] .navbar .navbar-toggle{ body[theme="light"] .navbar .navbar-toggle {
border:none; border:none;
padding:9px 12px; padding:9px 12px;
} }
body[theme="light"] .navbar .navbar-collapse{ body[theme="light"] .navbar .navbar-collapse {
border-color:rgba(0, 0, 0, 0.085); 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); background-color: rgba(0, 0, 0, 0.87);
} }
@@ -69,8 +69,6 @@ body[theme="light"] .content {
background: rgba(255, 255, 255, 1); background: rgba(255, 255, 255, 1);
color: rgba(0, 0, 0, 0.87); color: rgba(0, 0, 0, 0.87);
border: 1px #cecece solid; 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); 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 > tbody > tr > td:before,
body[theme="light"] .table > tfoot > 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 > 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); background-color: rgba(221, 221, 221, 1);
} }
body[theme="light"] .table > tbody > tr.expandRow > td:before, 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); background-color: rgba(255, 255, 255, 1);
} }
@@ -116,6 +114,12 @@ body[theme="light"] tr.odd.expandRow > :hover {
background: #ffffff !important; 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 { body[theme="light"] .progress-bar {
color: #000000; color: #000000;
} }
@@ -174,35 +178,35 @@ body[theme="light"] footer p a:hover {
text-decoration: none; text-decoration: none;
} }
body[theme="light"] .toolbox i{ body[theme="light"] .toolbox i {
color: rgba(94, 114, 228, .9); color: rgba(94, 114, 228, .9);
background-color: rgba(255, 255, 255, .5); background-color: rgba(255, 255, 255, .5);
box-shadow:0 .125rem .25rem rgba(0, 0, 0, .15) !important; 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; background: #ffffff;
box-shadow: 0 .125rem .25rem rgba(0, 0, 0, .15) !important; 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; background-color:#e6e6e6;
} }
body[theme="light"] .modal-content{ body[theme="light"] .modal-content {
background-color: rgba(249, 249, 249, 1); background-color: rgba(249, 249, 249, 1);
color: rgba(0, 0, 0, 0.87); color: rgba(0, 0, 0, 0.87);
} }
body[theme="light"] .modal-header{ body[theme="light"] .modal-header {
border-bottom-color:#f1f1f1; 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); 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); 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: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); 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); 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); 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 > tbody > tr > td:before,
body[theme="light"] .table > tfoot > 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 > 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 颜色 **/ /** border-bottom 颜色 **/
background-color: rgba(0, 0, 0, 0.075); background-color: rgba(0, 0, 0, 0.075);
} }
@@ -97,13 +97,18 @@ body[theme="light"] .table-hover > tbody > tr:not(.expandRow):hover > td {
} }
/* expandRow展开部分样式 */ /* expandRow展开部分样式 */
body[theme="light"] .table > tbody > tr.expandRow.odd > td:before{ body[theme="light"] .table > tbody > tr.expandRow.odd > td:before {
background-color: unset; 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; background-color: unset;
} }
body[theme="light"] .plan {
background-image: none;
background-color: rgba(0, 0, 0, 0.015);
}
/* expandRow展开部分样式结束 */ /* expandRow展开部分样式结束 */
body[theme="light"] .progress { 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%); 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); background-color: rgba(255, 255, 255, .55);
} }
+59 -36
View File
@@ -268,6 +268,22 @@ tr.accordion-toggle{
font-size: 80%; 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 { .temp-detail {
cursor: pointer; 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; box-shadow:unset;
} }
.network-box .network-box-header{ .network-box .network-box-header {
font-size: 18px; font-size: 18px;
padding: 5px 0px 15px 5px; padding: 5px 0px 15px 5px;
cursor: pointer; cursor: pointer;
@@ -365,12 +381,12 @@ td.ping-network-quality {
display: none; display: none;
} }
.network-box .network-box-header .dropdown-menu li{ .network-box .network-box-header .dropdown-menu li {
height: 2em; height: 2em;
cursor: pointer; 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; margin: 0 auto;
display: flex; display: flex;
position: sticky; position: sticky;
@@ -378,7 +394,7 @@ td.ping-network-quality {
z-index: 99999997; 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%; width: 100%;
height: 2em; height: 2em;
border-top: none; border-top: none;
@@ -407,7 +423,7 @@ td.ping-network-quality {
margin: 18px 0px 15px 0px; margin: 18px 0px 15px 0px;
} }
.network-box .chartTitle i.chartCountryCode{ .network-box .chartTitle i.chartCountryCode {
font-size: 16px; font-size: 16px;
border-radius: 12.5%; border-radius: 12.5%;
} }
@@ -415,17 +431,17 @@ td.ping-network-quality {
/* 网络页 正文结束 */ /* 网络页 正文结束 */
/* 地图版服务器分布图 */ /* 地图版服务器分布图 */
#mapChartBox{ #mapChartBox {
z-index: 999999999; z-index: 999999999;
padding-left: 0 !important; padding-left: 0 !important;
} }
.modal-open .modal{ .modal-open .modal {
overflow-x: hidden; overflow-x: hidden;
overflow-y: hidden; overflow-y: hidden;
} }
.modal-dialog-centered{ .modal-dialog-centered {
margin: 0 auto; margin: 0 auto;
width: 100%; width: 100%;
max-width: 100%; max-width: 100%;
@@ -435,7 +451,7 @@ td.ping-network-quality {
min-height: calc(100%) /* 确保模态框占据整个视口高度 */ min-height: calc(100%) /* 确保模态框占据整个视口高度 */
} }
.modal-content{ .modal-content {
display: flex; display: flex;
flex-direction: column; flex-direction: column;
pointer-events: auto; pointer-events: auto;
@@ -443,15 +459,15 @@ td.ping-network-quality {
border: none; border: none;
} }
.modal-header{ .modal-header {
position: relative; position: relative;
} }
.modal-header h4{ .modal-header h4 {
cursor: pointer; cursor: pointer;
} }
.modal-header i.xclose{ .modal-header i.xclose {
cursor: pointer; cursor: pointer;
position: absolute; position: absolute;
font-size: 30px; font-size: 30px;
@@ -459,32 +475,32 @@ td.ping-network-quality {
right: 10px; right: 10px;
} }
.modal-body{ .modal-body {
padding: 0px; padding: 0px;
} }
a.template-item, a.cdn-item{ a.template-item, a.cdn-item {
display: block; display: block;
} }
footer{ footer {
padding-bottom: 20px; padding-bottom: 20px;
} }
footer p{ footer p {
text-align: center; text-align: center;
font-size: 10px; font-size: 10px;
} }
@media only screen and (max-width: 1200px) { @media only screen and (max-width: 1200px) {
.accordian-body{ .accordian-body {
margin: 5px 0px 5px 10px; margin: 5px 0px 5px 10px;
} }
table.monitor-detail{ table.monitor-detail {
margin:0px 0px 2px 0px; margin:0px 0px 2px 0px;
} }
.table .network-thead-tr th, .table .network-thead-tr th,
.table .node-group-tag th{ .table .node-group-tag th {
font-size:16px; font-size:16px;
padding-bottom:6px; padding-bottom:6px;
} }
@@ -512,10 +528,10 @@ footer p{
padding: 0; padding: 0;
margin-bottom: 10px; margin-bottom: 10px;
} }
footer{ footer {
padding-bottom: 10px; padding-bottom: 10px;
} }
.cycle-transfer-stats .dataTables_wrapper{ .cycle-transfer-stats .dataTables_wrapper {
margin-bottom: -10px; margin-bottom: -10px;
} }
.navbar .navbar-nav .open .dropdown-menu>li>a { .navbar .navbar-nav .open .dropdown-menu>li>a {
@@ -537,54 +553,61 @@ footer p{
} }
.node-cell.os, .node-cell.os,
.node-cell.uptime, .node-cell.uptime,
.node-cell.traffic{ .node-cell.traffic {
display: none; display: none;
visibility: hidden; visibility: hidden;
} }
.node-cell.location{ .node-cell.location {
display: table-cell; display: table-cell;
visibility: visible; visibility: visible;
} }
.node-cell-os-text,.node-cell-location-text{ .node-cell-os-text,.node-cell-location-text {
display: none; display: none;
visibility: hidden; visibility: hidden;
} }
.node-cell.network {
min-width: 95px;
max-width: 95px;
}
.node-cell.remaining { .node-cell.remaining {
min-width: 75px; min-width: 75px;
max-width: 75px; max-width: 75px;
} }
.accordian-body{ .plan {
display: inline;
}
.accordian-body {
margin: 5px 0px 5px 10px; margin: 5px 0px 5px 10px;
} }
table.monitor-detail{ table.monitor-detail {
margin:0px 0px 2px 0px; margin:0px 0px 2px 0px;
} }
.table .network-thead-tr th, .table .network-thead-tr th,
.table .node-group-tag th{ .table .node-group-tag th {
font-size:14px; font-size:14px;
padding-bottom:6px; padding-bottom:6px;
} }
.table tr.network td{ .table tr.network td {
font-size: 12px; font-size: 12px;
} }
.service-status .service-status-th{ .service-status .service-status-th {
min-width:30px; min-width:30px;
} }
.service-status .delay-today{ .service-status .delay-today {
margin-top:4px; margin-top:4px;
justify-content: center; justify-content: center;
} }
.service-status .delay-today i{ .service-status .delay-today i {
margin-right:0px; margin-right:0px;
} }
.service-status .delay-today-text{ .service-status .delay-today-text {
display: none; display: none;
visibility: hidden; visibility: hidden;
} }
.service-status .service-averagelatency-th{ .service-status .service-averagelatency-th {
min-width:70px; min-width:70px;
} }
.service-status .service-30daysonline-th{ .service-status .service-30daysonline-th {
min-width:75px; min-width:75px;
} }
.toolbox { .toolbox {
@@ -602,7 +625,7 @@ footer p{
font-size: 16px; font-size: 16px;
margin: 10px 0px 10px 0px; margin: 10px 0px 10px 0px;
} }
.network-box .chartTitle i.chartCountryCode{ .network-box .chartTitle i.chartCountryCode {
font-size: 15px; font-size: 15px;
border-radius: 12.5%; 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="https://unpkg.com/[email protected]/dist/semantic.min.js"></script>
<script src="/static/semantic-ui-alerts.min.js"></script> <script src="/static/semantic-ui-alerts.min.js"></script>
<script src="https://unpkg.com/[email protected]/dist/vue.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> <script>
(function () { (function () {
updateLang({{.LANG }}); 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 "/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 "/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 "/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 "/nat"}} active{{end}}' href="/nat"><i class="exchange icon"></i>{{tr "NAT"}}</a>
<a class='item{{if eq .MatchedPath "/setting"}} active{{end}}' href="/setting"> <a class='item{{if eq .MatchedPath "/setting"}} active{{end}}' href="/setting">
<i class="settings icon"></i>{{tr "Settings"}} <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}}
+10 -21
View File
@@ -21,37 +21,26 @@
<input type="text" name="secret"> <input type="text" name="secret">
</div> </div>
<div class="field"> <div class="field">
<div class="ui hideforguest checkbox"> <label>{{tr "DDNSProfiles"}}</label>
<input name="HideForGuest" type="checkbox" tabindex="0" class="hidden" /> <div class="ui fluid multiple ddns search selection dropdown">
<label>{{tr "HideForGuest"}}</label> <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> </div>
<div class="field"> <div class="field">
<div class="ui enableddns checkbox"> <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> <label>{{tr "EnableDDNS"}}</label>
</div> </div>
</div> </div>
<div class="field"> <div class="field">
<div class="ui enableipv4 checkbox"> <div class="ui hideforguest checkbox">
<input name="EnableIPv4" type="checkbox" tabindex="0" /> <input name="HideForGuest" type="checkbox" tabindex="0" class="hidden" />
<label>{{tr "EnableIPv4"}}</label> <label>{{tr "HideForGuest"}}</label>
</div> </div>
</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"> <div class="field">
<label>{{tr "Note"}}</label> <label>{{tr "Note"}}</label>
<textarea name="Note"></textarea> <textarea name="Note"></textarea>
+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}}
+2 -2
View File
@@ -28,8 +28,8 @@
<th>{{tr "ServerGroup"}}</th> <th>{{tr "ServerGroup"}}</th>
<th>IP</th> <th>IP</th>
<th>{{tr "VersionNumber"}}</th> <th>{{tr "VersionNumber"}}</th>
<th>{{tr "HideForGuest"}}</th>
<th>{{tr "EnableDDNS"}}</th> <th>{{tr "EnableDDNS"}}</th>
<th>{{tr "HideForGuest"}}</th>
<th>{{tr "Secret"}}</th> <th>{{tr "Secret"}}</th>
<th>{{tr "OneKeyInstall"}}</th> <th>{{tr "OneKeyInstall"}}</th>
<th>{{tr "Note"}}</th> <th>{{tr "Note"}}</th>
@@ -46,8 +46,8 @@
<td>{{$server.Tag}}</td> <td>{{$server.Tag}}</td>
<td>{{$server.Host.IP}}</td> <td>{{$server.Host.IP}}</td>
<td>{{$server.Host.Version}}</td> <td>{{$server.Host.Version}}</td>
<td>{{$server.HideForGuest}}</td>
<td>{{$server.EnableDDNS}}</td> <td>{{$server.EnableDDNS}}</td>
<td>{{$server.HideForGuest}}</td>
<td> <td>
<button class="ui icon green mini button" data-clipboard-text="{{$server.Secret}}" data-tooltip="{{tr "ClickToCopy"}}"> <button class="ui icon green mini button" data-clipboard-text="{{$server.Secret}}" data-tooltip="{{tr "ClickToCopy"}}">
<i class="copy icon"></i> <i class="copy icon"></i>
+4
View File
@@ -53,6 +53,10 @@
<label>{{tr "PanelServerDomainAndIP"}}</label> <label>{{tr "PanelServerDomainAndIP"}}</label>
<input type="text" name="GRPCHost" placeholder="" value="{{.Conf.GRPCHost}}"> <input type="text" name="GRPCHost" placeholder="" value="{{.Conf.GRPCHost}}">
</div> </div>
<div class="field">
<label>{{tr "CustomNameservers"}}</label>
<input type="text" name="CustomNameservers" placeholder="" value="{{.Conf.DNSServers}}">
</div>
<div class="field"> <div class="field">
<label>{{tr "IPChangeAlert"}}</label> <label>{{tr "IPChangeAlert"}}</label>
</div> </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="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="/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="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 src="/static/theme-default/js/mixin.js?v20240302"></script>
<script> <script>
(function () { (function () {
+2 -2
View File
@@ -28,8 +28,8 @@
</span> </span>
</aside> </aside>
<template v-if="semiTransparent"> <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/light.plus.css?v20241103">
<link rel="stylesheet" href="/static/theme-server-status/css/dark.plus.css?v20240807"> <link rel="stylesheet" href="/static/theme-server-status/css/dark.plus.css?v20241103">
</template> </template>
</div> </div>
{{if ts .CustomCode}}{{.CustomCode|safe}}{{end}} {{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/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/vue.min.js"></script>
<script src="https://unpkg.com/[email protected]/dist/echarts.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/main.css?v20241103">
<link rel="stylesheet" href="/static/theme-server-status/css/dark.css?v202409029"> <link rel="stylesheet" href="/static/theme-server-status/css/dark.css?v20241103">
<link rel="stylesheet" href="/static/theme-server-status/css/light.css?v20240916"> <link rel="stylesheet" href="/static/theme-server-status/css/light.css?v20241103">
<script src="/static/theme-server-status/js/mixin.js?v20240915"></script> <script src="/static/theme-server-status/js/mixin.js?v20240915"></script>
</head> </head>
<body> <body>
+48 -11
View File
@@ -32,15 +32,16 @@
<i v-if='isWindowsPlatform(node.host.Platform)' class="windows icon"></i> <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-if='getFontLogoClass(node.host.Platform) == "" && node.stateuptime > 0' class="fl-tux"></i>
<i v-else :class="'fl-' + getFontLogoClass(node.host.Platform)"></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>
<td style="text-align: center;" class="node-cell location"> <td class="node-cell location center">
<i :class="'fi fi-' + node.location"></i> <i :class="'fi fi-' + (node.stateuptime > 0 ? (node.location || 'un') : '')"></i>
<span class="node-cell-location-text text-uppercase">@#node.location#@</span> <span class="node-cell-location-text text-uppercase">@#node.stateuptime > 0 ? (node.location || 'UN') : ''#@</span>
</td> </td>
<td v-if="nodesNoTag.some(item => item.additional && item.additional.price && Object.keys(item.additional.price).length > 0)" class="node-cell price center"> <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"> <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-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> <span v-else class="node-cell-price-text">@#node.additional.price.amount#@@#(node.additional.price.cycle ? '/' + node.additional.price.cycle : '')#@</span>
</template> </template>
</td> </td>
@@ -87,10 +88,49 @@
<td colspan="16"> <td colspan="16">
<div class="accordian-body collapse" :id="'rt'+node.ID"> <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"> <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">
<span class="node-cell-expand-label">{{tr "Platform"}}:</span> <span class="node-cell-expand-label">{{tr "Platform"}}:</span>
@#node.host.Platform#@-@#node.host.PlatformVersion#@ <span v-if="node.host.Platform">@#node.host.Platform#@@#node.host.PlatformVersion ? '-' + node.host.PlatformVersion : ''#@</span>
[<span v-if="node.host.Virtualization">@#node.host.Virtualization#@:</span>@#node.host.Arch#@] <span v-if="node.host.Arch">[@#node.host.Virtualization ? node.host.Virtualization + ':' : ''#@@#node.host.Arch#@]</span>
</span> </span>
<span class="node-cell-expand" v-if="node.host.CPU"> <span class="node-cell-expand" v-if="node.host.CPU">
<span class="node-cell-expand-label">CPU:</span> <span class="node-cell-expand-label">CPU:</span>
@@ -119,10 +159,7 @@
</span> </span>
<span class="node-cell-expand"> <span class="node-cell-expand">
<span class="node-cell-expand-label">{{tr "NetTransfer"}}:</span> <span class="node-cell-expand-label">{{tr "NetTransfer"}}:</span>
<i class="arrow alternate circle down outline icon" IN @#formatByteSize(node.state.NetInTransfer)#@ / OUT @#formatByteSize(node.state.NetOutTransfer)#@
style="margin: 0"></i>@#formatByteSize(node.state.NetInTransfer)#@
<i class="arrow alternate circle up outline icon"
style="margin: 0"></i>@#formatByteSize(node.state.NetOutTransfer)#@
</span> </span>
<span class="node-cell-expand load"> <span class="node-cell-expand load">
<span class="node-cell-expand-label">{{tr "Load"}}:</span> <span class="node-cell-expand-label">{{tr "Load"}}:</span>
+48 -11
View File
@@ -35,15 +35,16 @@
<i v-if='isWindowsPlatform(node.host.Platform)' class="windows icon"></i> <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-if='getFontLogoClass(node.host.Platform) == "" && node.stateuptime > 0' class="fl-tux"></i>
<i v-else :class="'fl-' + getFontLogoClass(node.host.Platform)"></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>
<td style="text-align: center;" class="node-cell location"> <td class="node-cell location center">
<i :class="'fi fi-' + node.location"></i> <i :class="'fi fi-' + (node.stateuptime > 0 ? (node.location || 'un') : '')"></i>
<span class="node-cell-location-text text-uppercase">&nbsp;@#node.location#@</span> <span class="node-cell-location-text text-uppercase">@#node.stateuptime > 0 ? (node.location || 'UN') : ''#@</span>
</td> </td>
<td v-if="group.data.some(item => item.additional && item.additional.price && Object.keys(item.additional.price).length > 0)" class="node-cell price center"> <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"> <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-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> <span v-else class="node-cell-price-text">@#node.additional.price.amount#@@#(node.additional.price.cycle ? '/' + node.additional.price.cycle : '')#@</span>
</template> </template>
</td> </td>
@@ -90,10 +91,49 @@
<td colspan="16"> <td colspan="16">
<div class="accordian-body collapse" :id="'rt'+node.ID"> <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"> <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">
<span class="node-cell-expand-label">{{tr "Platform"}}:</span> <span class="node-cell-expand-label">{{tr "Platform"}}:</span>
@#node.host.Platform#@-@#node.host.PlatformVersion#@ <span v-if="node.host.Platform">@#node.host.Platform#@@#node.host.PlatformVersion ? '-' + node.host.PlatformVersion : ''#@</span>
[<span v-if="node.host.Virtualization">@#node.host.Virtualization#@:</span>@#node.host.Arch#@] <span v-if="node.host.Arch">[@#node.host.Virtualization ? node.host.Virtualization + ':' : ''#@@#node.host.Arch#@]</span>
</span> </span>
<span class="node-cell-expand" v-if="node.host.CPU"> <span class="node-cell-expand" v-if="node.host.CPU">
<span class="node-cell-expand-label">CPU:</span> <span class="node-cell-expand-label">CPU:</span>
@@ -122,10 +162,7 @@
</span> </span>
<span class="node-cell-expand"> <span class="node-cell-expand">
<span class="node-cell-expand-label">{{tr "NetTransfer"}}:</span> <span class="node-cell-expand-label">{{tr "NetTransfer"}}:</span>
<i class="arrow alternate circle down outline icon" IN @#formatByteSize(node.state.NetInTransfer)#@ / OUT @#formatByteSize(node.state.NetOutTransfer)#@
style="margin: 0"></i>@#formatByteSize(node.state.NetInTransfer)#@
<i class="arrow alternate circle up outline icon"
style="margin: 0"></i>@#formatByteSize(node.state.NetOutTransfer)#@
</span> </span>
<span class="node-cell-expand load"> <span class="node-cell-expand load">
<span class="node-cell-expand-label">{{tr "Load"}}:</span> <span class="node-cell-expand-label">{{tr "Load"}}:</span>
+54 -38
View File
@@ -117,43 +117,54 @@
initAdditional(servers) { initAdditional(servers) {
let nodes = {}; let nodes = {};
servers?.forEach(server => { servers?.forEach(server => {
if (server.PublicNote) { //处理异常
const remainingFormat = this.getRemainingFormat(server.live, server.PublicNote); if (!server.PublicNote) return;
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
);
const priceAmount = this.getNoteElementValue(server.PublicNote, "billingDataMod", "amount");
const priceCycle = this.getNoteElementValue(server.PublicNote, "billingDataMod", "cycle");
// 初始化节点 // 初始化节点
nodes[server.ID] = { nodes[server.ID] = {
"remaining": {}, "remaining": {},
"price": {} "price": {},
}; "plan": {}
};
if (remainingFormat) { // 处理 billingDataMod 的 remaining 配置
nodes[server.ID].remaining.format = remainingFormat; 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);
if (remainingDays) { // 处理 billingDataMod 的 price 配置
nodes[server.ID].remaining.days = remainingDays; const priceAmount = this.getNoteElementValue(server.PublicNote, "billingDataMod", "amount");
} const priceCycle = this.getNoteElementValue(server.PublicNote, "billingDataMod", "cycle");
// 设置 price 属性
if (remainingPercent) { if (priceAmount) nodes[server.ID].price.amount = priceAmount;
nodes[server.ID].remaining.percent = this.toFixed2(100 - remainingPercent); if (priceCycle && priceAmount) nodes[server.ID].price.cycle = priceCycle;
}
// 处理 planDataMod 配置
if (priceAmount) { const planBandwidth = this.getNoteElementValue(server.PublicNote, "planDataMod", "bandwidth");
nodes[server.ID].price.amount = priceAmount; const planTrafficVol = this.getNoteElementValue(server.PublicNote, "planDataMod", "trafficVol");
} const planTrafficType = this.getNoteElementValue(server.PublicNote, "planDataMod", "trafficType");
const planIPv4 = this.getNoteElementValue(server.PublicNote, "planDataMod", "IPv4");
if (priceCycle && priceAmount) { const planIPv6 = this.getNoteElementValue(server.PublicNote, "planDataMod", "IPv6");
nodes[server.ID].price.cycle = priceCycle; 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; return nodes;
}, },
@@ -819,6 +830,7 @@
switch (cycle) { switch (cycle) {
case '月': case '月':
case 'mo':
case 'month': case 'month':
case 'monthly': case 'monthly':
case 'm': case 'm':
@@ -834,6 +846,7 @@
case 'h': case 'h':
return "H"; return "H";
case '年': case '年':
case 'yr':
case 'year': case 'year':
case 'annually': case 'annually':
case 'y': case 'y':
@@ -857,7 +870,7 @@
const expiration = new Date(endDate); const expiration = new Date(endDate);
const current = this.getAdjustTimezone(new Date(endDate), new Date()); const current = this.getAdjustTimezone(new Date(endDate), new Date());
// 如果 expiration 无效,返回 null 并记录日志 // 如果 expiration 无效,记录日志
if (isNaN(expiration.getTime())) { if (isNaN(expiration.getTime())) {
console.log("getAutoRenewalEndDate: Invalid expiration format"); console.log("getAutoRenewalEndDate: Invalid expiration format");
} }
@@ -1053,7 +1066,7 @@
return this.formatPercents(online, this.toFixed2(percent)); return this.formatPercents(online, this.toFixed2(percent));
}, },
getRemainingDays(endDate, note) { getRemainingDays(endDate, note, type) {
// 检查 endDate 是否有效 // 检查 endDate 是否有效
if (!endDate || typeof endDate !== 'string') { if (!endDate || typeof endDate !== 'string') {
return null; return null;
@@ -1064,9 +1077,9 @@
return "lifetime"; return "lifetime";
} }
// 检查 startDate 和 endDate 是否为合法的Date // 检查 endDate 是否为合法的Date
if (isNaN(new Date(endDate).getTime())) { if (isNaN(new Date(endDate).getTime())) {
return "NaN"; return type === 1 ? null : "NaN";
} }
// 获取当前时间,并调整时区 // 获取当前时间,并调整时区
@@ -1085,6 +1098,9 @@
// 确定到期时间 // 确定到期时间
const end = autoRenewal ? autoEndDate.date : new Date(endDate); const end = autoRenewal ? autoEndDate.date : new Date(endDate);
// 直接返回处理后的到期时间
if (type === 1) return end;
// 计算剩余天数 // 计算剩余天数
const timeDiff = end - currentTime; const timeDiff = end - currentTime;
const daysDiff = Math.ceil(timeDiff / (1000 * 60 * 60 * 24)); const daysDiff = Math.ceil(timeDiff / (1000 * 60 * 60 * 24));
+3 -3
View File
@@ -11,11 +11,11 @@
<input type="text" id="dropdown-search" class="form-control" placeholder="Search..."> <input type="text" id="dropdown-search" class="form-control" placeholder="Search...">
</li> </li>
<li class="dropdown-item" v-for="server in servers" @click="showCharts(server.ID)"> <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> </li>
</ul> </ul>
</div> </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 id="chartbox" style="width:100%;height:auto;"></div>
</div> </div>
{{template "theme-server-status/footer" .}} {{template "theme-server-status/footer" .}}
@@ -332,7 +332,7 @@
}, },
getServerCountryCode(id){ getServerCountryCode(id){
const result = this.servers.find(item => item.ID == 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) { getNextServerId(id) {
const currentIndex = this.servers.findIndex(item => item.ID === id); const currentIndex = this.servers.findIndex(item => item.ID === id);
-19
View File
@@ -12,22 +12,3 @@ site:
brand: "nz_site_title" brand: "nz_site_title"
cookiename: "nezha-dashboard" #浏览器 Cookie 字段名,可不改 cookiename: "nezha-dashboard" #浏览器 Cookie 字段名,可不改
theme: "default" 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判断 #Region判断
$ipapi = "" $ipapi = ""
$region = "Unknown" $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 { try {
$ipapi = Invoke-RestMethod -Uri $url -TimeoutSec 5 -UseBasicParsing $ipapi = Invoke-RestMethod -Uri $url -TimeoutSec 5 -UseBasicParsing
if ($ipapi -match "loc=(\w+)" ) { if ($ipapi -match "loc=(\w+)" ) {
+103 -91
View File
@@ -12,13 +12,13 @@ NZ_DASHBOARD_PATH="${NZ_BASE_PATH}/dashboard"
NZ_AGENT_PATH="${NZ_BASE_PATH}/agent" NZ_AGENT_PATH="${NZ_BASE_PATH}/agent"
NZ_DASHBOARD_SERVICE="/etc/systemd/system/nezha-dashboard.service" NZ_DASHBOARD_SERVICE="/etc/systemd/system/nezha-dashboard.service"
NZ_DASHBOARD_SERVICERC="/etc/init.d/nezha-dashboard" NZ_DASHBOARD_SERVICERC="/etc/init.d/nezha-dashboard"
NZ_VERSION="v0.19.2" NZ_VERSION="v0.20.2"
red='\033[0;31m' red='\033[0;31m'
green='\033[0;32m' green='\033[0;32m'
yellow='\033[0;33m' yellow='\033[0;33m'
plain='\033[0m' plain='\033[0m'
export PATH=$PATH:/usr/local/bin export PATH="$PATH:/usr/local/bin"
os_arch="" os_arch=""
[ -e /etc/os-release ] && grep -i "PRETTY_NAME" /etc/os-release | grep -qi "alpine" && os_alpine='1' [ -e /etc/os-release ] && grep -i "PRETTY_NAME" /etc/os-release | grep -qi "alpine" && os_alpine='1'
@@ -45,20 +45,28 @@ check_systemd() {
} }
err() { 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() { 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" 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 for url in $api_list; do
text="$(curl -A "$ua" -m 10 -s $url)" text="$(curl -A "$ua" -m 10 -s "$url")"
endpoint="$(echo $text | sed -n 's/.*h=\([^ ]*\).*/\1/p')" endpoint="$(echo "$text" | sed -n 's/.*h=\([^ ]*\).*/\1/p')"
if echo $text | grep -qw 'CN'; then if echo "$text" | grep -qw 'CN'; then
isCN=true isCN=true
break break
elif echo $url | grep -q $endpoint; then elif echo "$url" | grep -q "$endpoint"; then
break break
fi fi
done done
@@ -85,9 +93,9 @@ pre_check() {
## China_IP ## China_IP
if [ -z "$CN" ]; then if [ -z "$CN" ]; then
geo_check geo_check
if [ ! -z "$isCN" ]; then if [ -n "$isCN" ]; then
echo "根据geoip api提供的信息,当前IP可能在中国" echo "根据geoip api提供的信息,当前IP可能在中国"
printf "是否选用中国镜像完成安装? [Y/n] (自定义镜像输入 3):" printf "是否选用中国镜像完成安装? [Y/n] (自定义镜像输入 3)"
read -r input read -r input
case $input in case $input in
[yY][eE][sS] | [yY]) [yY][eE][sS] | [yY])
@@ -180,7 +188,9 @@ installation_check() {
select_version() { select_version() {
if [ -z "$IS_DOCKER_NEZHA" ]; then if [ -z "$IS_DOCKER_NEZHA" ]; then
printf "${yellow}请自行选择您的安装方式(如果你是安装Agent,输入哪个都是一样的):\n1. Docker\n2. 独立安装${plain}\n" info "请自行选择您的安装方式(如果你是安装Agent,输入哪个都是一样的):"
info "1. Docker"
info "2. 独立安装"
while true; do while true; do
printf "请输入选择 [1-2]" printf "请输入选择 [1-2]"
read -r option read -r option
@@ -206,7 +216,7 @@ update_script() {
curl -sL https://${GITHUB_RAW_URL}/script/install.sh -o /tmp/nezha.sh 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') 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 if [ -z "$new_version" ]; then
echo "脚本获取失败,请检查本机能否链接 https://${GITHUB_RAW_URL}/script/install.sh" echo "脚本获取失败,请检查本机能否链接 https://${GITHUB_RAW_URL}/script/install.sh"
return 1 return 1
fi fi
@@ -221,7 +231,7 @@ update_script() {
} }
before_show_menu() { before_show_menu() {
echo && printf "${yellow}* 按回车返回主菜单 *${plain}" && read temp echo && info "* 按回车返回主菜单 *" && read temp
show_menu show_menu
} }
@@ -231,7 +241,7 @@ install_base() {
} }
install_arch() { install_arch() {
printf "${green}提示: ${plain} Arch安装libselinux需添加nezha-agent用户,安装完会自动删除,建议手动检查一次\n" info "提示:Arch安装libselinux需添加nezha-agent用户,安装完会自动删除,建议手动检查一次"
read -r -p "是否安装libselinux? [Y/n] " input read -r -p "是否安装libselinux? [Y/n] " input
case $input in case $input in
[yY][eE][sS] | [yY]) [yY][eE][sS] | [yY])
@@ -255,11 +265,11 @@ install_arch() {
} }
install_soft() { install_soft() {
(command -v yum >/dev/null 2>&1 && sudo yum makecache && sudo yum install $* selinux-policy -y) || (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 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 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 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 apk >/dev/null 2>&1 && sudo apk update && sudo apk add "$@" -f)
} }
install_dashboard() { install_dashboard() {
@@ -305,12 +315,10 @@ install_dashboard() {
install_dashboard_docker() { install_dashboard_docker() {
if [ ! "$FRESH_INSTALL" = 0 ]; then if [ ! "$FRESH_INSTALL" = 0 ]; then
command -v docker >/dev/null 2>&1 if ! command -v docker >/dev/null 2>&1; then
if [ $? != 0 ]; then
echo "正在安装 Docker" echo "正在安装 Docker"
if [ "$os_alpine" != 1 ]; then if [ "$os_alpine" != 1 ]; then
curl -sL https://${Get_Docker_URL} | sudo bash -s ${Get_Docker_Argu} if ! curl -sL https://${Get_Docker_URL} | sudo bash -s "${Get_Docker_Argu}"; then
if [ $? != 0 ]; then
err "下载脚本失败,请检查本机能否连接 ${Get_Docker_URL}" err "下载脚本失败,请检查本机能否连接 ${Get_Docker_URL}"
return 0 return 0
fi fi
@@ -321,7 +329,7 @@ install_dashboard_docker() {
sudo rc-update add docker sudo rc-update add docker
sudo rc-service docker start sudo rc-service docker start
fi fi
printf "${green}Docker${plain} 安装成功\n" success "Docker 安装成功"
installation_check installation_check
fi fi
fi fi
@@ -334,13 +342,11 @@ install_dashboard_standalone() {
} }
selinux() { selinux() {
#判断当前的状态 #Check SELinux
command -v getenforce >/dev/null 2>&1 if command -v getenforce >/dev/null 2>&1; then
if [ $? -eq 0 ]; then if getenforce | grep '[Ee]nfor'; then
getenforce | grep '[Ee]nfor'
if [ $? -eq 0 ]; then
echo "SELinux是开启状态,正在关闭!" echo "SELinux是开启状态,正在关闭!"
sudo setenforce 0 &>/dev/null sudo setenforce 0 >/dev/null 2>&1
find_key="SELINUX=" find_key="SELINUX="
sudo sed -ri "/^$find_key/c${find_key}disabled" /etc/selinux/config sudo sed -ri "/^$find_key/c${find_key}disabled" /etc/selinux/config
fi fi
@@ -355,22 +361,23 @@ install_agent() {
echo "正在获取监控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://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')
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)}}}') 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 fi
if [ ! -n "$version" ]; then 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') _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 fi
if [ ! -n "$version" ]; then 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') _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 fi
if [ ! -n "$version" ]; then if [ -z "$_version" ]; then
err "获取版本号失败,请检查本机能否链接 https://api.github.com/repos/nezhahq/agent/releases/latest" err "获取版本号失败,请检查本机能否链接 https://api.github.com/repos/nezhahq/agent/releases/latest"
return 1 return 1
else else
echo "当前最新版本为: ${version}" echo "当前最新版本为: ${_version}"
fi fi
# 哪吒监控文件夹 # 哪吒监控文件夹
@@ -378,12 +385,13 @@ install_agent() {
echo "正在下载监控端" echo "正在下载监控端"
if [ -z "$CN" ]; then 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 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 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}" err "Release 下载失败,请检查本机能否连接 ${GITHUB_URL}"
return 1 return 1
fi fi
@@ -435,14 +443,14 @@ modify_agent_config() {
fi fi
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 if ! eval "$_cmd"; then
sudo ${NZ_AGENT_PATH}/nezha-agent service uninstall >/dev/null 2>&1 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 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 fi
printf "Agent配置 ${green}修改成功,请稍等重启生效${plain}\n" success "Agent配置 修改成功,请稍等重启生效"
#if [[ $# == 0 ]]; then #if [[ $# == 0 ]]; then
# before_show_menu # before_show_menu
@@ -453,10 +461,10 @@ modify_dashboard_config() {
echo "> 修改面板配置" echo "> 修改面板配置"
if [ "$IS_DOCKER_NEZHA" = 1 ]; then if [ "$IS_DOCKER_NEZHA" = 1 ]; then
if [ ! -z "$DOCKER_COMPOSE_COMMAND" ]; then if [ -n "$DOCKER_COMPOSE_COMMAND" ]; then
echo "正在下载 Docker 脚本" 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 _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 [ $? != 0 ]; then if ! eval "$_cmd"; then
err "下载脚本失败,请检查本机能否连接 ${GITHUB_RAW_URL}" err "下载脚本失败,请检查本机能否连接 ${GITHUB_RAW_URL}"
return 0 return 0
fi fi
@@ -466,8 +474,8 @@ modify_dashboard_config() {
fi fi
fi fi
wget -t 2 -T 60 -O /tmp/nezha-config.yaml https://${GITHUB_RAW_URL}/script/config.yaml >/dev/null 2>&1 _cmd="wget -t 2 -T 60 -O /tmp/nezha-config.yaml https://${GITHUB_RAW_URL}/script/config.yaml >/dev/null 2>&1"
if [ $? != 0 ]; then if ! eval "$_cmd"; then
err "下载脚本失败,请检查本机能否连接 ${GITHUB_RAW_URL}" err "下载脚本失败,请检查本机能否连接 ${GITHUB_RAW_URL}"
return 0 return 0
fi fi
@@ -529,18 +537,22 @@ modify_dashboard_config() {
if [ "$IS_DOCKER_NEZHA" = 0 ]; then if [ "$IS_DOCKER_NEZHA" = 0 ]; then
echo "正在下载服务文件" echo "正在下载服务文件"
if [ "$os_alpine" != 1 ]; then 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 _download="sudo wget -t 2 -T 60 -O $NZ_DASHBOARD_SERVICE https://${GITHUB_RAW_URL}/script/nezha-dashboard.service >/dev/null 2>&1"
else if ! eval "$_download"; then
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
err "文件下载失败,请检查本机能否连接 ${GITHUB_RAW_URL}" err "文件下载失败,请检查本机能否连接 ${GITHUB_RAW_URL}"
return 0 return 0
fi 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
fi fi
printf "面板配置 ${green}修改成功,请稍等重启生效${plain}\n" success "面板配置 修改成功,请稍等重启生效"
restart_and_update restart_and_update
@@ -553,14 +565,14 @@ restart_and_update() {
echo "> 重启并更新面板" echo "> 重启并更新面板"
if [ "$IS_DOCKER_NEZHA" = 1 ]; then if [ "$IS_DOCKER_NEZHA" = 1 ]; then
restart_and_update_docker _cmd="restart_and_update_docker"
elif [ "$IS_DOCKER_NEZHA" = 0 ]; then elif [ "$IS_DOCKER_NEZHA" = 0 ]; then
restart_and_update_standalone _cmd="restart_and_update_standalone"
fi fi
if [ $? = 0 ]; then if eval "$_cmd"; then
printf "${green}哪吒监控 重启成功${plain}\n" success "哪吒监控 重启成功"
printf "默认管理面板地址:${yellow}域名:站点访问端口${plain}\n" info "默认管理面板地址:域名:站点访问端口"
else else
err "重启失败,可能是因为启动时间超过了两秒,请稍后查看日志信息" err "重启失败,可能是因为启动时间超过了两秒,请稍后查看日志信息"
fi fi
@@ -577,15 +589,22 @@ restart_and_update_docker() {
} }
restart_and_update_standalone() { 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') _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 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)}}}') _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 fi
if [ ! -n "$version" ]; then 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') _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 fi
if [ ! -n "$version" ]; then 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') _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 fi
if [ "$os_alpine" != 1 ]; then if [ "$os_alpine" != 1 ]; then
@@ -595,20 +614,13 @@ restart_and_update_standalone() {
sudo rc-service nezha-dashboard stop sudo rc-service nezha-dashboard stop
fi 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 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 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 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
if [ "$os_alpine" != 1 ]; then if [ "$os_alpine" != 1 ]; then
sudo systemctl enable nezha-dashboard sudo systemctl enable nezha-dashboard
@@ -623,13 +635,13 @@ start_dashboard() {
echo "> 启动面板" echo "> 启动面板"
if [ "$IS_DOCKER_NEZHA" = 1 ]; then if [ "$IS_DOCKER_NEZHA" = 1 ]; then
start_dashboard_docker _cmd="start_dashboard_docker"
elif [ "$IS_DOCKER_NEZHA" = 0 ]; then elif [ "$IS_DOCKER_NEZHA" = 0 ]; then
start_dashboard_standalone _cmd="start_dashboard_standalone"
fi fi
if [ $? = 0 ]; then if eval "$_cmd"; then
printf "${green}哪吒监控 启动成功${plain}\n" success "哪吒监控 启动成功"
else else
err "启动失败,请稍后查看日志信息" err "启动失败,请稍后查看日志信息"
fi fi
@@ -655,13 +667,13 @@ stop_dashboard() {
echo "> 停止面板" echo "> 停止面板"
if [ "$IS_DOCKER_NEZHA" = 1 ]; then if [ "$IS_DOCKER_NEZHA" = 1 ]; then
stop_dashboard_docker _cmd="stop_dashboard_docker"
elif [ "$IS_DOCKER_NEZHA" = 0 ]; then elif [ "$IS_DOCKER_NEZHA" = 0 ]; then
stop_dashboard_standalone _cmd="stop_dashboard_standalone"
fi fi
if [ $? = 0 ]; then if eval "$_cmd"; then
printf "${green}哪吒监控 停止成功${plain}\n" success "哪吒监控 停止成功"
else else
err "停止失败,请稍后查看日志信息" err "停止失败,请稍后查看日志信息"
fi fi
@@ -940,4 +952,4 @@ if [ $# -gt 0 ]; then
else else
select_version select_version
show_menu show_menu
fi fi
+124 -107
View File
@@ -12,13 +12,13 @@ NZ_DASHBOARD_PATH="${NZ_BASE_PATH}/dashboard"
NZ_AGENT_PATH="${NZ_BASE_PATH}/agent" NZ_AGENT_PATH="${NZ_BASE_PATH}/agent"
NZ_DASHBOARD_SERVICE="/etc/systemd/system/nezha-dashboard.service" NZ_DASHBOARD_SERVICE="/etc/systemd/system/nezha-dashboard.service"
NZ_DASHBOARD_SERVICERC="/etc/init.d/nezha-dashboard" NZ_DASHBOARD_SERVICERC="/etc/init.d/nezha-dashboard"
NZ_VERSION="v0.19.2" NZ_VERSION="v0.20.2"
red='\033[0;31m' red='\033[0;31m'
green='\033[0;32m' green='\033[0;32m'
yellow='\033[0;33m' yellow='\033[0;33m'
plain='\033[0m' plain='\033[0m'
export PATH=$PATH:/usr/local/bin export PATH="$PATH:/usr/local/bin"
os_arch="" os_arch=""
[ -e /etc/os-release ] && grep -i "PRETTY_NAME" /etc/os-release | grep -qi "alpine" && os_alpine='1' [ -e /etc/os-release ] && grep -i "PRETTY_NAME" /etc/os-release | grep -qi "alpine" && os_alpine='1'
@@ -45,20 +45,28 @@ check_systemd() {
} }
err() { 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() { 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" 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 for url in $api_list; do
text="$(curl -A "$ua" -m 10 -s $url)" text="$(curl -A "$ua" -m 10 -s "$url")"
endpoint="$(echo $text | sed -n 's/.*h=\([^ ]*\).*/\1/p')" endpoint="$(echo "$text" | sed -n 's/.*h=\([^ ]*\).*/\1/p')"
if echo $text | grep -qw 'CN'; then if echo "$text" | grep -qw 'CN'; then
isCN=true isCN=true
break break
elif echo $url | grep -q $endpoint; then elif echo "$url" | grep -q "$endpoint"; then
break break
fi fi
done done
@@ -85,7 +93,7 @@ pre_check() {
## China_IP ## China_IP
if [ -z "$CN" ]; then if [ -z "$CN" ]; then
geo_check 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" 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): " printf "Will the installation be done with a Chinese Mirror? [Y/n] (Custom Mirror Input 3): "
read -r input read -r input
@@ -179,7 +187,9 @@ installation_check() {
select_version() { select_version() {
if [ -z "$IS_DOCKER_NEZHA" ]; then 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 while true; do
printf "Please enter [1-2]: " printf "Please enter [1-2]: "
read -r option read -r option
@@ -205,7 +215,7 @@ update_script() {
curl -sL https://${GITHUB_RAW_URL}/script/install_en.sh -o /tmp/nezha.sh 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') 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 if [ -z "$new_version" ]; then
echo "Script failed to get, please check if the network can link https://${GITHUB_RAW_URL}/script/install.sh" echo "Script failed to get, please check if the network can link https://${GITHUB_RAW_URL}/script/install.sh"
return 1 return 1
fi fi
@@ -220,7 +230,7 @@ update_script() {
} }
before_show_menu() { 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 show_menu
} }
@@ -230,7 +240,7 @@ install_base() {
} }
install_arch() { 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 read -r -p "Do you need to install libselinux? [Y/n] " input
case $input in case $input in
[yY][eE][sS] | [yY]) [yY][eE][sS] | [yY])
@@ -254,11 +264,11 @@ install_arch() {
} }
install_soft() { install_soft() {
(command -v yum >/dev/null 2>&1 && sudo yum makecache && sudo yum install $* selinux-policy -y) || (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 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 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 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 apk >/dev/null 2>&1 && sudo apk update && sudo apk add "$@" -f)
} }
install_dashboard() { install_dashboard() {
@@ -304,12 +314,10 @@ install_dashboard() {
install_dashboard_docker() { install_dashboard_docker() {
if [ ! "$FRESH_INSTALL" = 0 ]; then if [ ! "$FRESH_INSTALL" = 0 ]; then
command -v docker >/dev/null 2>&1 if ! command -v docker >/dev/null 2>&1; then
if [ $? != 0 ]; then
echo "Installing Docker" echo "Installing Docker"
if [ "$os_alpine" != 1 ]; then if [ "$os_alpine" != 1 ]; then
curl -sL https://${Get_Docker_URL} | sudo bash -s ${Get_Docker_Argu} if ! curl -sL https://${Get_Docker_URL} | sudo bash -s "${Get_Docker_Argu}"; then
if [ $? != 0 ]; then
err "Script failed to get, please check if the network can link ${Get_Docker_URL}" err "Script failed to get, please check if the network can link ${Get_Docker_URL}"
return 0 return 0
fi fi
@@ -320,7 +328,7 @@ install_dashboard_docker() {
sudo rc-update add docker sudo rc-update add docker
sudo rc-service docker start sudo rc-service docker start
fi fi
printf "${green}Docker${plain} installed successfully\n" success "Docker installed successfully"
installation_check installation_check
fi fi
fi fi
@@ -334,12 +342,10 @@ install_dashboard_standalone() {
selinux() { selinux() {
#Check SELinux #Check SELinux
command -v getenforce >/dev/null 2>&1 if command -v getenforce >/dev/null 2>&1; then
if [ $? -eq 0 ]; then if getenforce | grep '[Ee]nfor'; then
getenforce | grep '[Ee]nfor'
if [ $? -eq 0 ]; then
echo "SELinux running, closing now!" echo "SELinux running, closing now!"
sudo setenforce 0 &>/dev/null sudo setenforce 0 >/dev/null 2>&1
find_key="SELINUX=" find_key="SELINUX="
sudo sed -ri "/^$find_key/c${find_key}disabled" /etc/selinux/config sudo sed -ri "/^$find_key/c${find_key}disabled" /etc/selinux/config
fi fi
@@ -354,30 +360,37 @@ install_agent() {
echo "Obtaining Agent version number" 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://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')
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)}}}') 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 fi
if [ ! -n "$version" ]; then 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') _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 fi
if [ ! -n "$version" ]; then 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') _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 fi
if [ ! -n "$version" ]; then if [ -z "$_version" ]; then
err "Fail to obtaine agent version, please check if the network can link https://api.github.com/repos/nezhahq/agent/releases/latest" err "Fail to obtain agent version, please check if the network can link https://api.github.com/repos/nezhahq/agent/releases/latest"
return 1 return 1
else else
echo "The current latest version is: ${version}" echo "The current latest version is: ${_version}"
fi fi
# Nezha Monitoring Folder # Nezha Monitoring Folder
sudo mkdir -p $NZ_AGENT_PATH sudo mkdir -p $NZ_AGENT_PATH
echo "Downloading Agent" 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 [ -z "$CN" ]; then
if [ $? != 0 ]; 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}" err "Fail to download agent, please check if the network can link ${GITHUB_URL}"
return 1 return 1
fi fi
@@ -401,10 +414,10 @@ modify_agent_config() {
echo "> Modify Agent Configuration" echo "> Modify Agent Configuration"
if [ $# -lt 3 ]; then if [ $# -lt 3 ]; then
echo "Please add Agent in the admin panel first, record the secret" echo "Please add Agent in the Dashboard first, record the secret"
printf "Please enter a domain that resolves to the IP where the panel is located (no CDN): " printf "Please enter a domain that resolves to the IP where Dashboard is located (no CDN): "
read -r nz_grpc_host 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 read -r nz_grpc_port
printf "Please enter the Agent secret: " printf "Please enter the Agent secret: "
read -r nz_client_secret read -r nz_client_secret
@@ -429,14 +442,14 @@ modify_agent_config() {
fi fi
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 if ! eval "$_cmd"; then
sudo ${NZ_AGENT_PATH}/nezha-agent service uninstall >/dev/null 2>&1 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 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 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 #if [[ $# == 0 ]]; then
# before_show_menu # before_show_menu
@@ -447,21 +460,21 @@ modify_dashboard_config() {
echo "> Modify Dashboard Configuration" echo "> Modify Dashboard Configuration"
if [ "$IS_DOCKER_NEZHA" = 1 ]; then if [ "$IS_DOCKER_NEZHA" = 1 ]; then
if [ ! -z "$DOCKER_COMPOSE_COMMAND" ]; then if [ -n "$DOCKER_COMPOSE_COMMAND" ]; then
echo "Download Docker Script" 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 _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 [ $? != 0 ]; then if ! eval "$_cmd"; then
err "Script failed to get, please check if the network can link ${GITHUB_RAW_URL}" err "Script failed to get, please check if the network can link ${GITHUB_RAW_URL}"
return 0 return 0
fi fi
else else
err "请手动安装 docker-composehttps://docs.docker.com/compose/install/linux/" err "Please install docker-compose manually. https://docs.docker.com/compose/install/linux/"
before_show_menu before_show_menu
fi fi
fi fi
wget -t 2 -T 60 -O /tmp/nezha-config.yaml https://${GITHUB_RAW_URL}/script/config.yaml >/dev/null 2>&1 _cmd="wget -t 2 -T 60 -O /tmp/nezha-config.yaml https://${GITHUB_RAW_URL}/script/config.yaml >/dev/null 2>&1"
if [ $? != 0 ]; then if ! eval "$_cmd"; then
err "Script failed to get, please check if the network can link ${GITHUB_RAW_URL}" err "Script failed to get, please check if the network can link ${GITHUB_RAW_URL}"
return 0 return 0
fi fi
@@ -523,18 +536,22 @@ modify_dashboard_config() {
if [ "$IS_DOCKER_NEZHA" = 0 ]; then if [ "$IS_DOCKER_NEZHA" = 0 ]; then
echo "Downloading service file" echo "Downloading service file"
if [ "$os_alpine" != 1 ]; then 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 _download="sudo wget -t 2 -T 60 -O $NZ_DASHBOARD_SERVICE https://${GITHUB_RAW_URL}/script/nezha-dashboard.service >/dev/null 2>&1"
else if ! eval "$_download"; then
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
err "File failed to get, please check if the network can link ${GITHUB_RAW_URL}" err "File failed to get, please check if the network can link ${GITHUB_RAW_URL}"
return 0 return 0
fi 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
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 restart_and_update
@@ -544,17 +561,17 @@ modify_dashboard_config() {
} }
restart_and_update() { restart_and_update() {
echo "> Restart and Update the Panel" echo "> Restart and Update Dashboard"
if [ "$IS_DOCKER_NEZHA" = 1 ]; then if [ "$IS_DOCKER_NEZHA" = 1 ]; then
restart_and_update_docker _cmd="restart_and_update_docker"
elif [ "$IS_DOCKER_NEZHA" = 0 ]; then elif [ "$IS_DOCKER_NEZHA" = 0 ]; then
restart_and_update_standalone _cmd="restart_and_update_standalone"
fi fi
if [ $? = 0 ]; then if eval "$_cmd"; then
printf "${green}Nezha Monitoring Restart Successful${plain}\n" success "Nezha Monitoring Restart Successful"
printf "Default panel address: ${yellow}domain:Site_access_port${plain}\n" info "Default Dashboard address: domain:site_access_port"
else else
err "The restart failed, probably because the boot time exceeded two seconds, please check the log information later" err "The restart failed, probably because the boot time exceeded two seconds, please check the log information later"
fi fi
@@ -571,22 +588,22 @@ restart_and_update_docker() {
} }
restart_and_update_standalone() { 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') _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 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)}}}') _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 fi
if [ ! -n "$version" ]; then 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') _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 fi
if [ ! -n "$version" ]; then 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') _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 fi
if [ ! -n "$version" ]; then if [ -z "$_version" ]; then
err "Fail to obtaine agent version, please check if the network can link https://api.github.com/repos/nezhahq/agent/releases/latest" err "Fail to obtain agent version, please check if the network can link https://api.github.com/repos/nezhahq/agent/releases/latest"
return 1 return 1
else else
echo "The current latest version is: ${version}" echo "The current latest version is: ${_version}"
fi fi
if [ "$os_alpine" != 1 ]; then if [ "$os_alpine" != 1 ]; then
@@ -597,12 +614,12 @@ restart_and_update_standalone() {
fi fi
if [ -z "$CN" ]; then 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 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 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
if [ "$os_alpine" != 1 ]; then if [ "$os_alpine" != 1 ]; then
sudo systemctl enable nezha-dashboard sudo systemctl enable nezha-dashboard
@@ -614,16 +631,16 @@ restart_and_update_standalone() {
} }
start_dashboard() { start_dashboard() {
echo "> Start Panel" echo "> Start Dashboard"
if [ "$IS_DOCKER_NEZHA" = 1 ]; then if [ "$IS_DOCKER_NEZHA" = 1 ]; then
start_dashboard_docker _cmd="start_dashboard_docker"
elif [ "$IS_DOCKER_NEZHA" = 0 ]; then elif [ "$IS_DOCKER_NEZHA" = 0 ]; then
start_dashboard_standalone _cmd="start_dashboard_standalone"
fi fi
if [ $? = 0 ]; then if eval "$_cmd"; then
printf "${green}Nezha Monitoring Start Successful${plain}\n" success "Nezha Monitoring Start Successful"
else else
err "Failed to start, please check the log message later" err "Failed to start, please check the log message later"
fi fi
@@ -646,16 +663,16 @@ start_dashboard_standalone() {
} }
stop_dashboard() { stop_dashboard() {
echo "> Stop Panel" echo "> Stop Dashboard"
if [ "$IS_DOCKER_NEZHA" = 1 ]; then if [ "$IS_DOCKER_NEZHA" = 1 ]; then
stop_dashboard_docker _cmd="stop_dashboard_docker"
elif [ "$IS_DOCKER_NEZHA" = 0 ]; then elif [ "$IS_DOCKER_NEZHA" = 0 ]; then
stop_dashboard_standalone _cmd="stop_dashboard_standalone"
fi fi
if [ $? = 0 ]; then if eval "$_cmd"; then
printf "${green}Nezha Monitoring Stop Successful${plain}\n" success "Nezha Monitoring Stop Successful"
else else
err "Failed to stop, please check the log message later" err "Failed to stop, please check the log message later"
fi fi
@@ -678,7 +695,7 @@ stop_dashboard_standalone() {
} }
show_dashboard_log() { show_dashboard_log() {
echo "> View Panel Log" echo "> View Dashboard Log"
if [ "$IS_DOCKER_NEZHA" = 1 ]; then if [ "$IS_DOCKER_NEZHA" = 1 ]; then
show_dashboard_log_docker show_dashboard_log_docker
@@ -704,7 +721,7 @@ show_dashboard_log_standalone() {
} }
uninstall_dashboard() { uninstall_dashboard() {
echo "> Uninstall Panel" echo "> Uninstall Dashboard"
if [ "$IS_DOCKER_NEZHA" = 1 ]; then if [ "$IS_DOCKER_NEZHA" = 1 ]; then
uninstall_dashboard_docker uninstall_dashboard_docker
@@ -791,13 +808,13 @@ show_usage() {
echo "Nezha Monitor Management Script Usage: " echo "Nezha Monitor Management Script Usage: "
echo "--------------------------------------------------------" echo "--------------------------------------------------------"
echo "./nezha.sh - Show Menu" echo "./nezha.sh - Show Menu"
echo "./nezha.sh install_dashboard - Install Panel" echo "./nezha.sh install_dashboard - Install Dashboard"
echo "./nezha.sh modify_dashboard_config - Modify Panel Configuration" echo "./nezha.sh modify_dashboard_config - Modify Dashboard Configuration"
echo "./nezha.sh start_dashboard - Start Panel" echo "./nezha.sh start_dashboard - Start Dashboard"
echo "./nezha.sh stop_dashboard - Stop Panel" echo "./nezha.sh stop_dashboard - Stop Dashboard"
echo "./nezha.sh restart_and_update - Restart and Update the Panel" echo "./nezha.sh restart_and_update - Restart and Update the Dashboard"
echo "./nezha.sh show_dashboard_log - View Panel Log" echo "./nezha.sh show_dashboard_log - View Dashboard Log"
echo "./nezha.sh uninstall_dashboard - Uninstall Panel" echo "./nezha.sh uninstall_dashboard - Uninstall Dashboard"
echo "--------------------------------------------------------" echo "--------------------------------------------------------"
echo "./nezha.sh install_agent - Install Agent" echo "./nezha.sh install_agent - Install Agent"
echo "./nezha.sh modify_agent_config - Modify Agent Configuration" echo "./nezha.sh modify_agent_config - Modify Agent Configuration"
@@ -812,13 +829,13 @@ show_menu() {
printf " printf "
${green}Nezha Monitor Management Script${plain} ${red}${NZ_VERSION}${plain} ${green}Nezha Monitor Management Script${plain} ${red}${NZ_VERSION}${plain}
--- https://github.com/naiba/nezha --- --- https://github.com/naiba/nezha ---
${green}1.${plain} Install Panel ${green}1.${plain} Install Dashboard
${green}2.${plain} Modify Panel Configuration ${green}2.${plain} Modify Dashbaord Configuration
${green}3.${plain} Start Panel ${green}3.${plain} Start Dashboard
${green}4.${plain} Stop Panel ${green}4.${plain} Stop Dashboard
${green}5.${plain} Restart and Update the Panel ${green}5.${plain} Restart and Update Dashboard
${green}6.${plain} View Panel Log ${green}6.${plain} View Dashboard Log
${green}7.${plain} Uninstall Panel ${green}7.${plain} Uninstall Dashboard
————————————————- ————————————————-
${green}8.${plain} Install Agent ${green}8.${plain} Install Agent
${green}9.${plain} Modify Agent Configuration ${green}9.${plain} Modify Agent Configuration
@@ -934,4 +951,4 @@ if [ $# -gt 0 ]; then
else else
select_version select_version
show_menu 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) { func (s *NezhaHandler) ReportSystemInfo(c context.Context, r *pb.Host) (*pb.Receipt, error) {
var clientID uint64 var clientID uint64
var provider ddns.Provider
var err error var err error
if clientID, err = s.Auth.Check(c); err != nil { if clientID, err = s.Auth.Check(c); err != nil {
return nil, err return nil, err
@@ -135,33 +134,19 @@ func (s *NezhaHandler) ReportSystemInfo(c context.Context, r *pb.Host) (*pb.Rece
defer singleton.ServerLock.RUnlock() defer singleton.ServerLock.RUnlock()
// 检查并更新DDNS // 检查并更新DDNS
if singleton.Conf.DDNS.Enable && if singleton.ServerList[clientID].EnableDDNS && host.IP != "" &&
singleton.ServerList[clientID].EnableDDNS &&
host.IP != "" &&
(singleton.ServerList[clientID].Host == nil || singleton.ServerList[clientID].Host.IP != host.IP) { (singleton.ServerList[clientID].Host == nil || singleton.ServerList[clientID].Host.IP != host.IP) {
serverDomain := singleton.ServerList[clientID].DDNSDomain ipv4, ipv6, _ := utils.SplitIPAddr(host.IP)
if singleton.Conf.DDNS.Provider == "" { providers, err := singleton.GetDDNSProvidersFromProfiles(singleton.ServerList[clientID].DDNSProfiles, &ddns.IP{Ipv4Addr: ipv4, Ipv6Addr: ipv6})
provider, err = singleton.GetDDNSProviderFromProfile(singleton.ServerList[clientID].DDNSProfile) if err == nil {
} else { for _, provider := range providers {
provider, err = singleton.GetDDNSProviderFromString(singleton.Conf.DDNS.Provider) go func(provider *ddns.Provider) {
} provider.UpdateDomain(context.Background())
if err == nil && serverDomain != "" { }(provider)
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,
} }
go singleton.RetryableUpdateDomain(provider, config, maxRetries)
} else { } else {
// 虽然会在启动时panic, 可以断言不会走这个分支, 但是考虑到动态加载配置或者其它情况, 这里输出一下方便检查奇奇怪怪的BUG log.Printf("NEZHA>> 获取DDNS配置时发生错误: %v", err)
log.Printf("NEZHA>> 未找到对应的DDNS配置(%s), 或者是provider填写不正确, 请前往config.yml检查你的设置", singleton.ServerList[clientID].DDNSProfile)
} }
} }
// 发送IP变动通知 // 发送IP变动通知
+2
View File
@@ -34,6 +34,7 @@ type CommonServerInfo struct {
IPV6 string `json:"ipv6"` IPV6 string `json:"ipv6"`
ValidIP string `json:"valid_ip"` ValidIP string `json:"valid_ip"`
DisplayIndex int `json:"display_index"` DisplayIndex int `json:"display_index"`
HideForGuest bool `json:"hide_for_guest"`
} }
// StatusResponse 服务器状态子结构 包含服务器信息与状态信息 // StatusResponse 服务器状态子结构 包含服务器信息与状态信息
@@ -150,6 +151,7 @@ func (s *ServerAPIService) GetAllStatus() *ServerStatusResponse {
IPV6: ipv6, IPV6: ipv6,
ValidIP: validIP, ValidIP: validIP,
DisplayIndex: v.DisplayIndex, DisplayIndex: v.DisplayIndex,
HideForGuest: v.HideForGuest,
} }
res.Result = append(res.Result, &StatusResponse{ res.Result = append(res.Result, &StatusResponse{
CommonServerInfo: info, CommonServerInfo: info,
+56 -55
View File
@@ -2,73 +2,74 @@ package singleton
import ( import (
"fmt" "fmt"
"log" "sync"
"slices"
"github.com/libdns/cloudflare"
tencentcloud "github.com/nezhahq/libdns-tencentcloud"
"github.com/naiba/nezha/model"
ddns2 "github.com/naiba/nezha/pkg/ddns" ddns2 "github.com/naiba/nezha/pkg/ddns"
"github.com/naiba/nezha/pkg/ddns/dummy"
"github.com/naiba/nezha/pkg/ddns/webhook"
) )
const ( var (
ProviderWebHook = "webhook" ddnsCache map[uint64]*model.DDNSProfile
ProviderCloudflare = "cloudflare" ddnsCacheLock sync.RWMutex
ProviderTencentCloud = "tencentcloud"
) )
type ProviderFunc func(*ddns2.DomainConfig) ddns2.Provider func initDDNS() {
OnDDNSUpdate()
OnNameserverUpdate()
}
func RetryableUpdateDomain(provider ddns2.Provider, domainConfig *ddns2.DomainConfig, maxRetries int) { func OnDDNSUpdate() {
if domainConfig == nil { var ddns []*model.DDNSProfile
return 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 { func OnNameserverUpdate() {
log.Printf("NEZHA>> 尝试更新域名(%s)DDNS失败: %v", domainConfig.FullDomain, err) 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 { } else {
log.Printf("NEZHA>> 尝试更新域名(%s)DDNS成功", domainConfig.FullDomain) ddnsCacheLock.RUnlock()
break return nil, fmt.Errorf("无法找到DDNS配置 ID %d", id)
} }
} }
} ddnsCacheLock.RUnlock()
// Deprecated providers := make([]*ddns2.Provider, 0, len(profiles))
func GetDDNSProviderFromString(provider string) (ddns2.Provider, error) { for _, profile := range profiles {
switch provider { provider := &ddns2.Provider{DDNSProfile: profile, IPAddrs: ip}
case ProviderWebHook: switch profile.Provider {
return ddns2.NewProviderWebHook(Conf.DDNS.WebhookURL, Conf.DDNS.WebhookMethod, Conf.DDNS.WebhookRequestBody, Conf.DDNS.WebhookHeaders), nil case model.ProviderDummy:
case ProviderCloudflare: provider.Setter = &dummy.Provider{}
return ddns2.NewProviderCloudflare(Conf.DDNS.AccessSecret), nil providers = append(providers, provider)
case ProviderTencentCloud: case model.ProviderWebHook:
return ddns2.NewProviderTencentCloud(Conf.DDNS.AccessID, Conf.DDNS.AccessSecret), nil provider.Setter = &webhook.Provider{DDNSProfile: profile}
default: providers = append(providers, provider)
return new(ddns2.ProviderDummy), fmt.Errorf("无法找到配置的DDNS提供者 %s", provider) case model.ProviderCloudflare:
} provider.Setter = &cloudflare.Provider{APIToken: profile.AccessSecret}
} providers = append(providers, provider)
case model.ProviderTencentCloud:
func GetDDNSProviderFromProfile(profileName string) (ddns2.Provider, error) { provider.Setter = &tencentcloud.Provider{SecretId: profile.AccessID, SecretKey: profile.AccessSecret}
profile, ok := Conf.DDNS.Profiles[profileName] providers = append(providers, provider)
if !ok { default:
return new(ddns2.ProviderDummy), fmt.Errorf("找到配置 %s", profileName) return nil, fmt.Errorf("无法找到配置的DDNS提供者ID %d", profile.Provider)
}
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)
} }
} }
return nil return providers, nil
} }
+2 -21
View File
@@ -1,7 +1,6 @@
package singleton package singleton
import ( import (
"fmt"
"log" "log"
"time" "time"
@@ -39,6 +38,7 @@ func LoadSingleton() {
loadCronTasks() // 加载定时任务 loadCronTasks() // 加载定时任务
loadAPI() loadAPI()
initNAT() initNAT()
initDDNS()
} }
// InitConfigFromPath 从给出的文件路径中加载配置 // InitConfigFromPath 从给出的文件路径中加载配置
@@ -48,25 +48,6 @@ func InitConfigFromPath(path string) {
if err != nil { if err != nil {
panic(err) 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 从给出的文件路径中加载数据库 // InitDBFromPath 从给出的文件路径中加载数据库
@@ -84,7 +65,7 @@ func InitDBFromPath(path string) {
err = DB.AutoMigrate(model.Server{}, model.User{}, err = DB.AutoMigrate(model.Server{}, model.User{},
model.Notification{}, model.AlertRule{}, model.Monitor{}, model.Notification{}, model.AlertRule{}, model.Monitor{},
model.MonitorHistory{}, model.Cron{}, model.Transfer{}, model.MonitorHistory{}, model.Cron{}, model.Transfer{},
model.ApiToken{}, model.NAT{}) model.ApiToken{}, model.NAT{}, model.DDNSProfile{})
if err != nil { if err != nil {
panic(err) panic(err)
} }