Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
3b5ee464a7 | ||
|
|
c4b2c47beb | ||
|
|
0835f13f28 | ||
|
|
ee3cda8aed | ||
|
|
604fc7d4c0 | ||
|
|
5c549261dd | ||
|
|
00041c0ebd | ||
|
|
dbfea9a00b | ||
|
|
e8b208edfa | ||
|
|
7d38634933 | ||
|
|
5bd4b57cbf |
@@ -4,7 +4,7 @@
|
||||
<br>
|
||||
<small><i>LOGO designed by <a href="https://xio.ng" target="_blank">熊大</a> .</i></small>
|
||||
<br><br>
|
||||
<img alt="GitHub release (with filter)" src="https://img.shields.io/github/v/release/naiba/nezha?color=brightgreen&style=for-the-badge&logo=github&label=Dashboard"> <img src="https://img.shields.io/github/v/release/nezhahq/agent?color=brightgreen&label=Agent&style=for-the-badge&logo=github"> <img src="https://img.shields.io/github/actions/workflow/status/nezhahq/agent/agent.yml?label=Agent%20CI&logo=github&style=for-the-badge"> <img src="https://img.shields.io/badge/Installer-v0.15.6-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"> <img src="https://img.shields.io/github/v/release/nezhahq/agent?color=brightgreen&label=Agent&style=for-the-badge&logo=github"> <img src="https://img.shields.io/github/actions/workflow/status/nezhahq/agent/agent.yml?label=Agent%20CI&logo=github&style=for-the-badge"> <img src="https://img.shields.io/badge/Installer-v0.15.8-brightgreen?style=for-the-badge&logo=linux">
|
||||
<br>
|
||||
<br>
|
||||
<p>:trollface: <b>Nezha Monitoring: Self-hostable, lightweight, servers and websites monitoring and O&M tool.</b></p>
|
||||
@@ -77,18 +77,21 @@ You can change the dashboard language in the settings page (`/setting`) after th
|
||||
<a href="https://github.com/uubulb" title="UUBulb">
|
||||
<img src="https://avatars.githubusercontent.com/u/35923940?v=4" width="50;" alt="UUBulb"/>
|
||||
</a>
|
||||
<a href="https://github.com/lvgj-stack" title="Ko no dio">
|
||||
<img src="https://avatars.githubusercontent.com/u/38449861?v=4" width="50;" alt="Ko no dio"/>
|
||||
</a>
|
||||
<a href="https://github.com/hhhkkk520" title="Kris">
|
||||
<img src="https://avatars.githubusercontent.com/u/52115472?v=4" width="50;" alt="Kris"/>
|
||||
</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/rootmelo92118" title="rootmelo92118">
|
||||
<img src="https://avatars.githubusercontent.com/u/32770959?v=4" width="50;" alt="rootmelo92118"/>
|
||||
</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/iilemon" title="Sean">
|
||||
<img src="https://avatars.githubusercontent.com/u/33201711?v=4" width="50;" alt="Sean"/>
|
||||
</a>
|
||||
@@ -98,9 +101,6 @@ You can change the dashboard language in the settings page (`/setting`) after th
|
||||
<a href="https://github.com/ch8o" title="no-name-now">
|
||||
<img src="https://avatars.githubusercontent.com/u/9103372?v=4" width="50;" alt="no-name-now"/>
|
||||
</a>
|
||||
<a href="https://github.com/lvgj-stack" title="Ko no dio">
|
||||
<img src="https://avatars.githubusercontent.com/u/38449861?v=4" width="50;" alt="Ko no dio"/>
|
||||
</a>
|
||||
<a href="https://github.com/Creling" title="Creling">
|
||||
<img src="https://avatars.githubusercontent.com/u/43109504?v=4" width="50;" alt="Creling"/>
|
||||
</a>
|
||||
@@ -110,9 +110,15 @@ You can change the dashboard language in the settings page (`/setting`) after th
|
||||
<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/arkylin" title="凌">
|
||||
<img src="https://avatars.githubusercontent.com/u/35104502?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/xykt" title="xykt">
|
||||
<img src="https://avatars.githubusercontent.com/u/152045469?v=4" width="50;" alt="xykt"/>
|
||||
</a>
|
||||
<a href="https://github.com/unclezs" title="unclezs">
|
||||
<img src="https://avatars.githubusercontent.com/u/42318775?v=4" width="50;" alt="unclezs"/>
|
||||
</a>
|
||||
@@ -146,6 +152,9 @@ You can change the dashboard language in the settings page (`/setting`) after th
|
||||
<a href="https://github.com/techotaku" title="Ian Li">
|
||||
<img src="https://avatars.githubusercontent.com/u/1948179?v=4" width="50;" alt="Ian Li"/>
|
||||
</a>
|
||||
<a href="https://github.com/HsukqiLee" title="HsukqiLee">
|
||||
<img src="https://avatars.githubusercontent.com/u/79034142?v=4" width="50;" alt="HsukqiLee"/>
|
||||
</a>
|
||||
<a href="https://github.com/GreenTeodoro839" title="GreenTeodoro839">
|
||||
<img src="https://avatars.githubusercontent.com/u/77104800?v=4" width="50;" alt="GreenTeodoro839"/>
|
||||
</a>
|
||||
|
||||
@@ -300,6 +300,8 @@ type serverForm struct {
|
||||
Tag string
|
||||
Note string
|
||||
HideForGuest string
|
||||
EnableDDNS string
|
||||
DDNSDomain string
|
||||
}
|
||||
|
||||
func (ma *memberAPI) addOrEditServer(c *gin.Context) {
|
||||
@@ -315,6 +317,8 @@ func (ma *memberAPI) addOrEditServer(c *gin.Context) {
|
||||
s.Tag = sf.Tag
|
||||
s.Note = sf.Note
|
||||
s.HideForGuest = sf.HideForGuest == "on"
|
||||
s.EnableDDNS = sf.EnableDDNS == "on"
|
||||
s.DDNSDomain = sf.DDNSDomain
|
||||
if s.ID == 0 {
|
||||
s.Secret, err = utils.GenerateRandomString(18)
|
||||
if err == nil {
|
||||
|
||||
+23
-1
@@ -112,6 +112,19 @@ type Config struct {
|
||||
IgnoredIPNotificationServerIDs map[uint64]bool // [ServerID] -> bool(值为true代表当前ServerID在特定服务器列表内)
|
||||
MaxTCPPingValue int32
|
||||
AvgPingCount int
|
||||
|
||||
// 动态域名解析更新
|
||||
DDNS struct {
|
||||
Enable bool
|
||||
Provider string
|
||||
AccessID string
|
||||
AccessSecret string
|
||||
WebhookURL string
|
||||
WebhookMethod string
|
||||
WebhookRequestBody string
|
||||
WebhookHeaders string
|
||||
MaxRetries uint32
|
||||
}
|
||||
}
|
||||
|
||||
// Read 读取配置文件并应用
|
||||
@@ -147,11 +160,20 @@ func (c *Config) Read(path string) error {
|
||||
c.Location = "Asia/Shanghai"
|
||||
}
|
||||
if c.MaxTCPPingValue == 0 {
|
||||
c.MaxTCPPingValue = 300
|
||||
c.MaxTCPPingValue = 1000
|
||||
}
|
||||
if c.AvgPingCount == 0 {
|
||||
c.AvgPingCount = 2
|
||||
}
|
||||
if c.DDNS.Provider == "" {
|
||||
c.DDNS.Provider = "webhook"
|
||||
}
|
||||
if c.DDNS.WebhookMethod == "" {
|
||||
c.DDNS.WebhookMethod = "POST"
|
||||
}
|
||||
if c.DDNS.MaxRetries == 0 {
|
||||
c.DDNS.MaxRetries = 3
|
||||
}
|
||||
|
||||
c.updateIgnoredIPNotificationID()
|
||||
return nil
|
||||
|
||||
+14
-12
@@ -1,20 +1,22 @@
|
||||
package model
|
||||
|
||||
const (
|
||||
Cycle = iota
|
||||
Hour
|
||||
Day
|
||||
Week
|
||||
Month
|
||||
import (
|
||||
"time"
|
||||
|
||||
"gorm.io/gorm"
|
||||
)
|
||||
|
||||
// MonitorHistory 历史监控记录
|
||||
type MonitorHistory struct {
|
||||
Common
|
||||
MonitorID uint64
|
||||
ServerID uint64
|
||||
AvgDelay float32 // 平均延迟,毫秒
|
||||
Up uint64 // 检查状态良好计数
|
||||
Down uint64 // 检查状态异常计数
|
||||
ID uint64 `gorm:"primaryKey"`
|
||||
CreatedAt time.Time `gorm:"index;<-:create;index:idx_server_id_created_at_monitor_id_avg_delay"`
|
||||
UpdatedAt time.Time `gorm:"autoUpdateTime"`
|
||||
DeletedAt gorm.DeletedAt `gorm:"index"`
|
||||
MonitorID uint64 `gorm:"index:idx_server_id_created_at_monitor_id_avg_delay"`
|
||||
ServerID uint64 `gorm:"index:idx_server_id_created_at_monitor_id_avg_delay"`
|
||||
AvgDelay float32 `gorm:"index:idx_server_id_created_at_monitor_id_avg_delay"` // 平均延迟,毫秒
|
||||
Up uint64 // 检查状态良好计数
|
||||
Down uint64 // 检查状态异常计数
|
||||
Data string
|
||||
}
|
||||
|
||||
|
||||
+4
-1
@@ -17,6 +17,8 @@ type Server struct {
|
||||
Note string `json:"-"` // 管理员可见备注
|
||||
DisplayIndex int // 展示排序,越大越靠前
|
||||
HideForGuest bool // 对游客隐藏
|
||||
EnableDDNS bool // 是否启用DDNS 未在配置文件中启用DDNS 或 DDNS检查时间为0时此项无效
|
||||
DDNSDomain string // DDNS中的前缀 如基础域名为abc.oracle DDNSName为mjj 就会把mjj.abc.oracle解析服务器IP 为空则停用
|
||||
|
||||
Host *Host `gorm:"-"`
|
||||
State *HostState `gorm:"-"`
|
||||
@@ -51,5 +53,6 @@ func (s Server) Marshal() template.JS {
|
||||
tag, _ := utils.Json.Marshal(s.Tag)
|
||||
note, _ := utils.Json.Marshal(s.Note)
|
||||
secret, _ := utils.Json.Marshal(s.Secret)
|
||||
return template.JS(fmt.Sprintf(`{"ID":%d,"Name":%s,"Secret":%s,"DisplayIndex":%d,"Tag":%s,"Note":%s,"HideForGuest": %s}`, s.ID, name, secret, s.DisplayIndex, tag, note, boolToString(s.HideForGuest))) // #nosec
|
||||
ddnsDomain, _ := utils.Json.Marshal(s.DDNSDomain)
|
||||
return template.JS(fmt.Sprintf(`{"ID":%d,"Name":%s,"Secret":%s,"DisplayIndex":%d,"Tag":%s,"Note":%s,"HideForGuest": %s,"EnableDDNS": %s,"DDNSDomain": %s}`, s.ID, name, secret, s.DisplayIndex, tag, note, boolToString(s.HideForGuest), boolToString(s.EnableDDNS), ddnsDomain)) // #nosec
|
||||
}
|
||||
|
||||
Vendored
+7
-1
@@ -614,4 +614,10 @@ other = "Menu"
|
||||
other = "Network"
|
||||
|
||||
[EnableShowInService]
|
||||
other = "Enable Show in Service"
|
||||
other = "Enable Show in Service"
|
||||
|
||||
[EnableDDNS]
|
||||
other = "Enable DDNS"
|
||||
|
||||
[DDNSDomain]
|
||||
other = "DDNS Domain"
|
||||
|
||||
Vendored
+7
-1
@@ -614,4 +614,10 @@ other = "Menú"
|
||||
other = "Red"
|
||||
|
||||
[EnableShowInService]
|
||||
other = "Mostrar en servicio"
|
||||
other = "Mostrar en servicio"
|
||||
|
||||
[EnableDDNS]
|
||||
other = "Habilitar DDNS"
|
||||
|
||||
[DDNSDomain]
|
||||
other = "Dominio DDNS"
|
||||
|
||||
Vendored
+6
@@ -615,3 +615,9 @@ other = "网络"
|
||||
|
||||
[EnableShowInService]
|
||||
other = "在服务中显示"
|
||||
|
||||
[EnableDDNS]
|
||||
other = "启用DDNS"
|
||||
|
||||
[DDNSDomain]
|
||||
other = "DDNS域名"
|
||||
|
||||
Vendored
+7
-1
@@ -614,4 +614,10 @@ other = "菜單"
|
||||
other = "網絡"
|
||||
|
||||
[EnableShowInService]
|
||||
other = "在服務中顯示"
|
||||
other = "在服務中顯示"
|
||||
|
||||
[EnableDDNS]
|
||||
other = "啟用DDNS"
|
||||
|
||||
[DDNSDomain]
|
||||
other = "DDNS網域"
|
||||
|
||||
@@ -302,6 +302,7 @@ function addOrEditServer(server, conf) {
|
||||
modal.find("input[name=id]").val(server ? server.ID : null);
|
||||
modal.find("input[name=name]").val(server ? server.Name : null);
|
||||
modal.find("input[name=Tag]").val(server ? server.Tag : null);
|
||||
modal.find("input[name=DDNSDomain]").val(server ? server.DDNSDomain : null);
|
||||
modal
|
||||
.find("input[name=DisplayIndex]")
|
||||
.val(server ? server.DisplayIndex : null);
|
||||
@@ -321,6 +322,11 @@ function addOrEditServer(server, conf) {
|
||||
} else {
|
||||
modal.find(".ui.hideforguest.checkbox").checkbox("set unchecked");
|
||||
}
|
||||
if (server && server.EnableDDNS) {
|
||||
modal.find(".ui.enableddns.checkbox").checkbox("set checked");
|
||||
} else {
|
||||
modal.find(".ui.enableddns.checkbox").checkbox("set unchecked");
|
||||
}
|
||||
showFormModal(".server.modal", "#serverForm", "/api/server");
|
||||
}
|
||||
|
||||
|
||||
Vendored
+1
-1
@@ -10,7 +10,7 @@
|
||||
<script src="https://lf6-cdn-tos.bytecdntp.com/cdn/expire-1-y/semantic-ui/2.4.1/semantic.min.js"></script>
|
||||
<script src="/static/semantic-ui-alerts.min.js"></script>
|
||||
<script src="https://lf6-cdn-tos.bytecdntp.com/cdn/expire-1-y/vue/2.6.14/vue.min.js"></script>
|
||||
<script src="/static/main.js?v20240213"></script>
|
||||
<script src="/static/main.js?v20240224"></script>
|
||||
<script>
|
||||
(function () {
|
||||
updateLang({{.LANG }});
|
||||
|
||||
+10
@@ -26,6 +26,16 @@
|
||||
<label>{{tr "HideForGuest"}}</label>
|
||||
</div>
|
||||
</div>
|
||||
<div class="field">
|
||||
<div class="ui enableddns checkbox">
|
||||
<input name="EnableDDNS" type="checkbox" tabindex="0" />
|
||||
<label>{{tr "EnableDDNS"}}</label>
|
||||
</div>
|
||||
</div>
|
||||
<div class="field">
|
||||
<label>{{tr "DDNSDomain"}}</label>
|
||||
<input type="text" name="DDNSDomain" placeholder="{{tr "DDNSDomain"}}">
|
||||
</div>
|
||||
<div class="field">
|
||||
<label>{{tr "Note"}}</label>
|
||||
<textarea name="Note"></textarea>
|
||||
|
||||
@@ -29,6 +29,8 @@
|
||||
<th>IP</th>
|
||||
<th>{{tr "VersionNumber"}}</th>
|
||||
<th>{{tr "HideForGuest"}}</th>
|
||||
<th>{{tr "EnableDDNS"}}</th>
|
||||
<th>{{tr "DDNSDomain"}}</th>
|
||||
<th>{{tr "Secret"}}</th>
|
||||
<th>{{tr "OneKeyInstall"}}</th>
|
||||
<th>{{tr "Note"}}</th>
|
||||
@@ -45,6 +47,8 @@
|
||||
<td>{{$server.Host.IP}}</td>
|
||||
<td>{{$server.Host.Version}}</td>
|
||||
<td>{{$server.HideForGuest}}</td>
|
||||
<td>{{$server.EnableDDNS}}</td>
|
||||
<td>{{$server.DDNSDomain}}</td>
|
||||
<td>
|
||||
<button class="ui icon green mini button" data-clipboard-text="{{$server.Secret}}" data-tooltip="{{tr "ClickToCopy"}}">
|
||||
<i class="copy icon"></i>
|
||||
|
||||
+1
-1
@@ -12,7 +12,7 @@
|
||||
<script src="https://lf6-cdn-tos.bytecdntp.com/cdn/expire-1-y/semantic-ui/2.4.1/semantic.min.js"></script>
|
||||
<script src="/static/semantic-ui-alerts.min.js"></script>
|
||||
<script src="https://lf6-cdn-tos.bytecdntp.com/cdn/expire-1-y/vue/2.6.14/vue.min.js"></script>
|
||||
<script src="/static/main.js?v20240213"></script>
|
||||
<script src="/static/main.js?v20240224"></script>
|
||||
<script>
|
||||
(function () {
|
||||
updateLang({{.LANG }});
|
||||
|
||||
+14
-6
@@ -30,7 +30,7 @@
|
||||
const initData = JSON.parse('{{.Servers}}').servers;
|
||||
let MaxTCPPingValue = {{.MaxTCPPingValue}};
|
||||
if (MaxTCPPingValue == null) {
|
||||
MaxTCPPingValue = 300;
|
||||
MaxTCPPingValue = 1000;
|
||||
}
|
||||
new Vue({
|
||||
el: '#app',
|
||||
@@ -77,7 +77,7 @@
|
||||
},
|
||||
dataZoom: [
|
||||
{
|
||||
start: 94,
|
||||
start: 0,
|
||||
end: 100
|
||||
}
|
||||
],
|
||||
@@ -87,7 +87,7 @@
|
||||
},
|
||||
yAxis: {
|
||||
type: 'value',
|
||||
boundaryGap: [0, '100%']
|
||||
boundaryGap: false
|
||||
},
|
||||
series: [],
|
||||
},
|
||||
@@ -178,11 +178,13 @@
|
||||
let loss = 0;
|
||||
let data = [];
|
||||
for (let j = 0; j < monitorInfo.result[i].created_at.length; j++) {
|
||||
avgDelay = monitorInfo.result[i].avg_delay[j];
|
||||
avgDelay = Math.round(monitorInfo.result[i].avg_delay[j]);
|
||||
if (avgDelay > 0.9 * MaxTCPPingValue) {
|
||||
loss += 1
|
||||
}
|
||||
data.push([monitorInfo.result[i].created_at[j], avgDelay]);
|
||||
if (avgDelay > 0) {
|
||||
data.push([monitorInfo.result[i].created_at[j], avgDelay]);
|
||||
}
|
||||
}
|
||||
lossRate = ((loss / monitorInfo.result[i].created_at.length) * 100).toFixed(1);
|
||||
legendName = monitorInfo.result[i].monitor_name +" "+ lossRate + "%";
|
||||
@@ -192,7 +194,13 @@
|
||||
type: 'line',
|
||||
smooth: true,
|
||||
symbol: 'none',
|
||||
data: data
|
||||
data: data,
|
||||
markPoint: {
|
||||
data: [
|
||||
{ type: 'max', symbol: 'pin', name: 'Max', itemStyle: { color: '#f00' } },
|
||||
{ type: 'min', symbol: 'pin', name: 'Min', itemStyle: { color: '#0f0' } }
|
||||
]
|
||||
}
|
||||
});
|
||||
}
|
||||
this.option.title.text = monitorInfo.result[0].server_name;
|
||||
|
||||
+1
@@ -12,6 +12,7 @@
|
||||
{{else}}
|
||||
<a href="/" class='mdui-ripple mdui-ripple-white mdui-hoverable{{if eq .MatchedPath "/"}} mdui-tab-active{{end}}'><i class="mdui-icon material-icons">home</i>{{tr "Home"}}</a>
|
||||
<a href="/service" class='mdui-ripple mdui-ripple-white mdui-hoverable{{if eq .MatchedPath "/service"}} mdui-tab-active{{end}}'><i class="mdui-icon material-icons">accessibility</i>{{tr "Services"}}</a>
|
||||
<a href="/network" class='mdui-ripple mdui-ripple-white mdui-hoverable{{if eq .MatchedPath "/network"}} mdui-tab-active{{end}}'><i class="mdui-icon material-icons">network_check</i>{{tr "NetworkSpiter"}}</a>
|
||||
{{end}}
|
||||
<div class="mdui-toolbar-spacer"></div>
|
||||
{{if .Admin}}
|
||||
|
||||
+255
@@ -0,0 +1,255 @@
|
||||
{{define "theme-mdui/network"}}
|
||||
<!doctype html>
|
||||
<html lang="{{.Conf.Language}}">
|
||||
|
||||
<head>
|
||||
<meta charset="utf-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1"/>
|
||||
<title>{{.Title}}</title>
|
||||
<link rel="shortcut icon" type="image/png" href="/static/logo.svg?v20210804" />
|
||||
|
||||
<!-- MDUI CSS -->
|
||||
<link rel="stylesheet" href="https://lf6-cdn-tos.bytecdntp.com/cdn/expire-1-y/mdui/1.0.2/css/mdui.min.css"/>
|
||||
<link rel="stylesheet" href="/static/theme-mdui/mdui.css" type="text/css">
|
||||
<style>
|
||||
.mdui-table td, .mdui-table th{padding: 6px;}
|
||||
.progress{width: 10%;min-width: 75px;}
|
||||
.progress-text{font-size: 16px;font-weight: 800;position: relative;top: 4px;left: 6px;}
|
||||
.offline st,.offline at,.offline gt,.offline .progress-text{color: grey;}
|
||||
a{text-decoration:none;color:#333;}.mdui-theme-layout-dark a{color:#fff;}
|
||||
</style>
|
||||
{{if ts .CustomCode}}
|
||||
{{.CustomCode|safe}}
|
||||
{{end}}
|
||||
</head>
|
||||
|
||||
<body>
|
||||
{{template "theme-mdui/menu" .}}
|
||||
<div class="nb-container" id="app">
|
||||
<div class="ui container">
|
||||
<div class="service-status">
|
||||
<table class="ui celled table">
|
||||
<button
|
||||
v-for="server in servers"
|
||||
@click="redirectNetwork(server.ID)"
|
||||
class="mdui-btn mdui-btn-raised mdui-color-theme mdui-color-indigo mdui-text-color-white"
|
||||
style="margin-top: 6px;margin-left:5px">
|
||||
<img :src="'https://lf6-cdn-tos.bytecdntp.com/cdn/expire-1-y/flag-icon-css/4.1.5/flags/4x3/' + (server.Host.CountryCode?server.Host.CountryCode:'cn') + '.svg'" alt="Flag Icon" style="vertical-align: middle;height:14px"> <span>@#server.Name#@ </span></button>
|
||||
</table>
|
||||
</div>
|
||||
</div>
|
||||
<div class="ui container" style="max-width: 95vw">
|
||||
<div ref="chartDom" style="border-radius: 28px; margin-top: 15px;height: 520px;max-width: 1400px;overflow: hidden"></div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{{template "theme-mdui/footer" .}}
|
||||
<script src="/static/theme-mdui/mdui.js"></script>
|
||||
<script src="https://lf6-cdn-tos.bytecdntp.com/cdn/expire-1-y/mdui/1.0.2/js/mdui.min.js"></script>
|
||||
<script src="https://lf6-cdn-tos.bytecdntp.com/cdn/expire-1-y/jquery/3.6.0/jquery.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://lf26-cdn-tos.bytecdntp.com/cdn/expire-1-M/echarts/5.3.0-rc.1/echarts.min.js"></script>
|
||||
|
||||
<script>
|
||||
const monitorInfo = JSON.parse('{{.MonitorInfos}}');
|
||||
const initData = JSON.parse('{{.Servers}}').servers;
|
||||
let MaxTCPPingValue = {{.MaxTCPPingValue}};
|
||||
if (MaxTCPPingValue == null) {
|
||||
MaxTCPPingValue = 1000;
|
||||
}
|
||||
new Vue({
|
||||
el: '#app',
|
||||
delimiters: ['@#', '#@'],
|
||||
data: {
|
||||
servers: initData,
|
||||
option: {
|
||||
tooltip: {
|
||||
trigger: 'axis',
|
||||
position: function (pt) {
|
||||
return [pt[0], '10%'];
|
||||
},
|
||||
formatter: function(params){
|
||||
let result = params[0].axisValueLabel + "<br />";
|
||||
params.forEach(function(item){
|
||||
result += item.marker + item.seriesName + ": " + item.value[1].toFixed(2) + " ms<br />";
|
||||
})
|
||||
return result;
|
||||
},
|
||||
confine: true,
|
||||
transitionDuration: 0
|
||||
},
|
||||
title: {
|
||||
left: 'center',
|
||||
text: "",
|
||||
textStyle: {}
|
||||
},
|
||||
legend: {
|
||||
top: '5%',
|
||||
data: [],
|
||||
textStyle: {
|
||||
fontSize: 14
|
||||
}
|
||||
},
|
||||
backgroundColor: 'rgba(255, 255, 255, 0.8)',
|
||||
toolbox: {
|
||||
feature: {
|
||||
dataZoom: {
|
||||
yAxisIndex: 'none'
|
||||
},
|
||||
restore: {},
|
||||
saveAsImage: {}
|
||||
}
|
||||
},
|
||||
dataZoom: [
|
||||
{
|
||||
start: 0,
|
||||
end: 100
|
||||
}
|
||||
],
|
||||
xAxis: {
|
||||
type: 'time',
|
||||
boundaryGap: false
|
||||
},
|
||||
yAxis: {
|
||||
type: 'value',
|
||||
boundaryGap: false
|
||||
},
|
||||
series: [],
|
||||
},
|
||||
chartOnOff: true,
|
||||
},
|
||||
mounted() {
|
||||
this.renderChart();
|
||||
this.parseMonitorInfo(monitorInfo);
|
||||
},
|
||||
methods: {
|
||||
getFontLogoClass(str) {
|
||||
if (["almalinux",
|
||||
"alpine",
|
||||
"aosc",
|
||||
"apple",
|
||||
"archlinux",
|
||||
"archlabs",
|
||||
"artix",
|
||||
"budgie",
|
||||
"centos",
|
||||
"coreos",
|
||||
"debian",
|
||||
"deepin",
|
||||
"devuan",
|
||||
"docker",
|
||||
"elementary",
|
||||
"fedora",
|
||||
"ferris",
|
||||
"flathub",
|
||||
"freebsd",
|
||||
"gentoo",
|
||||
"gnu-guix",
|
||||
"illumos",
|
||||
"kali-linux",
|
||||
"linuxmint",
|
||||
"mageia",
|
||||
"mandriva",
|
||||
"manjaro",
|
||||
"nixos",
|
||||
"openbsd",
|
||||
"opensuse",
|
||||
"pop-os",
|
||||
"raspberry-pi",
|
||||
"redhat",
|
||||
"rocky-linux",
|
||||
"sabayon",
|
||||
"slackware",
|
||||
"snappy",
|
||||
"solus",
|
||||
"tux",
|
||||
"ubuntu",
|
||||
"void",
|
||||
"zorin"].indexOf(str)
|
||||
> -1) {
|
||||
return str;
|
||||
}
|
||||
if (['openwrt', 'linux', "immortalwrt"].indexOf(str) > -1) {
|
||||
return 'tux';
|
||||
}
|
||||
if (str == 'amazon') {
|
||||
return 'redhat';
|
||||
}
|
||||
if (str == 'arch') {
|
||||
return 'archlinux';
|
||||
}
|
||||
return '';
|
||||
},
|
||||
redirectNetwork(id) {
|
||||
this.getMonitorHistory(id)
|
||||
.then(function(monitorInfo) {
|
||||
var vm = app.__vue__;
|
||||
vm.parseMonitorInfo(monitorInfo);
|
||||
})
|
||||
.catch(function(error){
|
||||
window.location.href = "/404";
|
||||
})
|
||||
},
|
||||
getMonitorHistory(id) {
|
||||
return $.ajax({
|
||||
url: "/api/v1/monitor/"+id,
|
||||
method: "GET"
|
||||
});
|
||||
},
|
||||
parseMonitorInfo(monitorInfo) {
|
||||
let tSeries = [];
|
||||
let tLegendData = [];
|
||||
for (let i = 0; i < monitorInfo.result.length; i++) {
|
||||
let loss = 0;
|
||||
let data = [];
|
||||
for (let j = 0; j < monitorInfo.result[i].created_at.length; j++) {
|
||||
avgDelay = Math.round(monitorInfo.result[i].avg_delay[j]);
|
||||
if (avgDelay > 0.9 * MaxTCPPingValue) {
|
||||
loss += 1
|
||||
}
|
||||
if (avgDelay > 0) {
|
||||
data.push([monitorInfo.result[i].created_at[j], avgDelay]);
|
||||
}
|
||||
}
|
||||
lossRate = ((loss / monitorInfo.result[i].created_at.length) * 100).toFixed(1);
|
||||
legendName = monitorInfo.result[i].monitor_name +" "+ lossRate + "%";
|
||||
tLegendData.push(legendName);
|
||||
tSeries.push({
|
||||
name: legendName,
|
||||
type: 'line',
|
||||
smooth: true,
|
||||
symbol: 'none',
|
||||
data: data,
|
||||
markPoint: {
|
||||
data: [
|
||||
{ type: 'max', symbol: 'pin', name: 'Max', itemStyle: { color: '#f00' } },
|
||||
{ type: 'min', symbol: 'pin', name: 'Min', itemStyle: { color: '#0f0' } }
|
||||
]
|
||||
}
|
||||
});
|
||||
}
|
||||
this.option.title.text = monitorInfo.result[0].server_name;
|
||||
this.option.series = tSeries;
|
||||
this.option.legend.data = tLegendData;
|
||||
this.myChart.clear();
|
||||
this.myChart.setOption(this.option);
|
||||
},
|
||||
isWindowsPlatform(str) {
|
||||
return str.includes('Windows')
|
||||
},
|
||||
renderChart() {
|
||||
this.myChart = echarts.init(this.$refs.chartDom);
|
||||
this.myChart.setOption(this.option);
|
||||
},
|
||||
},
|
||||
beforeDestroy() {
|
||||
this.myChart.dispose();
|
||||
this.myChart = null;
|
||||
},
|
||||
});
|
||||
</script>
|
||||
|
||||
</body>
|
||||
</html>
|
||||
{{end}}
|
||||
@@ -12,3 +12,13 @@ site:
|
||||
brand: "nz_site_title"
|
||||
cookiename: "nezha-dashboard" #浏览器 Cookie 字段名,可不改
|
||||
theme: "default"
|
||||
ddns:
|
||||
enable: false
|
||||
provider: "webhook"
|
||||
accessid: ""
|
||||
accesssecret: ""
|
||||
webhookmethod: ""
|
||||
webhookurl: ""
|
||||
webhookrequestbody: ""
|
||||
webhookheaders: ""
|
||||
maxretries: 3
|
||||
|
||||
+4
-3
@@ -14,7 +14,7 @@ NZ_AGENT_SERVICE="/etc/systemd/system/nezha-agent.service"
|
||||
NZ_AGENT_SERVICERC="/etc/init.d/nezha-agent"
|
||||
NZ_DASHBOARD_SERVICE="/etc/systemd/system/nezha-dashboard.service"
|
||||
NZ_DASHBOARD_SERVICERC="/etc/init.d/nezha-dashboard"
|
||||
NZ_VERSION="v0.15.6"
|
||||
NZ_VERSION="v0.15.8"
|
||||
|
||||
red='\033[0;31m'
|
||||
green='\033[0;32m'
|
||||
@@ -159,7 +159,7 @@ install_arch() {
|
||||
cd /tmp; git clone https://aur.archlinux.org/libsepol.git; cd libsepol; makepkg -si --noconfirm --asdeps; cd ..;
|
||||
git clone https://aur.archlinux.org/libselinux.git; cd libselinux; makepkg -si --noconfirm; cd ..;
|
||||
rm -rf libsepol libselinux'
|
||||
sed -i '/nezha-agent/d' /etc/sudoers && sleep 30s && killall -u nezha-agent && userdel nezha-agent
|
||||
sed -i '/nezha-agent/d' /etc/sudoers && sleep 30s && killall -u nezha-agent && userdel -r nezha-agent
|
||||
echo -e "${red}提示: ${plain}已删除用户nezha-agent,请务必手动核查一遍!\n"
|
||||
;;
|
||||
[nN][oO] | [nN])
|
||||
@@ -269,7 +269,8 @@ install_dashboard_standalone() {
|
||||
|
||||
selinux() {
|
||||
#判断当前的状态
|
||||
if [ "$os_alpine" != 1 ]; then
|
||||
command -v getenforce >/dev/null 2>&1
|
||||
if [ $? -eq 0 ]; then
|
||||
getenforce | grep '[Ee]nfor'
|
||||
if [ $? -eq 0 ]; then
|
||||
echo -e "SELinux是开启状态,正在关闭!"
|
||||
|
||||
@@ -14,7 +14,7 @@ NZ_AGENT_SERVICE="/etc/systemd/system/nezha-agent.service"
|
||||
NZ_AGENT_SERVICERC="/etc/init.d/nezha-agent"
|
||||
NZ_DASHBOARD_SERVICE="/etc/systemd/system/nezha-dashboard.service"
|
||||
NZ_DASHBOARD_SERVICERC="/etc/init.d/nezha-dashboard"
|
||||
NZ_VERSION="v0.15.6"
|
||||
NZ_VERSION="v0.15.8"
|
||||
|
||||
red='\033[0;31m'
|
||||
green='\033[0;32m'
|
||||
@@ -157,7 +157,7 @@ install_arch() {
|
||||
cd /tmp; git clone https://aur.archlinux.org/libsepol.git; cd libsepol; makepkg -si --noconfirm --asdeps; cd ..;
|
||||
git clone https://aur.archlinux.org/libselinux.git; cd libselinux; makepkg -si --noconfirm; cd ..;
|
||||
rm -rf libsepol libselinux'
|
||||
sed -i '/nezha-agent/d' /etc/sudoers && sleep 30s && killall -u nezha-agent && userdel nezha-agent
|
||||
sed -i '/nezha-agent/d' /etc/sudoers && sleep 30s && killall -u nezha-agent && userdel -r nezha-agent
|
||||
echo -e "${red}Info: ${plain}user nezha-agent has been deleted, Be sure to check it manually!\n"
|
||||
;;
|
||||
[nN][oO] | [nN])
|
||||
@@ -266,7 +266,8 @@ install_dashboard_standalone() {
|
||||
|
||||
selinux() {
|
||||
#Check SELinux
|
||||
if [ "$os_alpine" != 1 ]; then
|
||||
command -v getenforce >/dev/null 2>&1
|
||||
if [ $? -eq 0 ]; then
|
||||
getenforce | grep '[Ee]nfor'
|
||||
if [ $? -eq 0 ]; then
|
||||
echo -e "SELinux running,closing now!"
|
||||
|
||||
@@ -3,6 +3,8 @@ package rpc
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"github.com/naiba/nezha/pkg/utils"
|
||||
"log"
|
||||
"time"
|
||||
|
||||
"github.com/jinzhu/copier"
|
||||
@@ -110,6 +112,36 @@ func (s *NezhaHandler) ReportSystemInfo(c context.Context, r *pb.Host) (*pb.Rece
|
||||
host := model.PB2Host(r)
|
||||
singleton.ServerLock.RLock()
|
||||
defer singleton.ServerLock.RUnlock()
|
||||
|
||||
// 检查并更新DDNS
|
||||
if singleton.Conf.DDNS.Enable &&
|
||||
singleton.ServerList[clientID].EnableDDNS &&
|
||||
singleton.ServerList[clientID].Host != nil &&
|
||||
host.IP != "" &&
|
||||
singleton.ServerList[clientID].Host.IP != host.IP {
|
||||
|
||||
serverDomain := singleton.ServerList[clientID].DDNSDomain
|
||||
provider, err := singleton.GetDDNSProviderFromString(singleton.Conf.DDNS.Provider)
|
||||
if err == nil && serverDomain != "" {
|
||||
ipv4, ipv6, _ := utils.SplitIPAddr(host.IP)
|
||||
maxRetries := int(singleton.Conf.DDNS.MaxRetries)
|
||||
config := &singleton.DDNSDomainConfig{
|
||||
EnableIPv4: true,
|
||||
EnableIpv6: true,
|
||||
FullDomain: serverDomain,
|
||||
Ipv4Addr: ipv4,
|
||||
Ipv6Addr: ipv6,
|
||||
}
|
||||
go singleton.RetryableUpdateDomain(provider, config, maxRetries)
|
||||
|
||||
} else {
|
||||
// 虽然会在启动时panic, 可以断言不会走这个分支, 但是考虑到动态加载配置或者其它情况, 这里输出一下方便检查奇奇怪怪的BUG
|
||||
log.Printf("NEZHA>> 未找到对应的DDNS提供者(%s), 请前往config.yml检查你的设置\n", singleton.Conf.DDNS.Provider)
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
// 发送IP变动通知
|
||||
if singleton.Conf.EnableIPChangeNotification &&
|
||||
((singleton.Conf.Cover == model.ConfigCoverAll && !singleton.Conf.IgnoredIPNotificationServerIDs[clientID]) ||
|
||||
(singleton.Conf.Cover == model.ConfigCoverIgnoreAll && singleton.Conf.IgnoredIPNotificationServerIDs[clientID])) &&
|
||||
|
||||
@@ -0,0 +1,302 @@
|
||||
package singleton
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"encoding/json"
|
||||
"errors"
|
||||
"fmt"
|
||||
"io"
|
||||
"log"
|
||||
"net/http"
|
||||
"strings"
|
||||
)
|
||||
|
||||
type DDNSDomainConfig struct {
|
||||
EnableIPv4 bool
|
||||
EnableIpv6 bool
|
||||
FullDomain string
|
||||
Ipv4Addr string
|
||||
Ipv6Addr string
|
||||
}
|
||||
|
||||
type DDNSProvider interface {
|
||||
// UpdateDomain Return is updated
|
||||
UpdateDomain(domainConfig *DDNSDomainConfig) bool
|
||||
}
|
||||
|
||||
type DDNSProviderWebHook struct {
|
||||
URL string
|
||||
RequestMethod string
|
||||
RequestBody string
|
||||
RequestHeader string
|
||||
}
|
||||
|
||||
func (provider DDNSProviderWebHook) UpdateDomain(domainConfig *DDNSDomainConfig) bool {
|
||||
if domainConfig == nil {
|
||||
return false
|
||||
}
|
||||
|
||||
if domainConfig.FullDomain == "" {
|
||||
log.Println("NEZHA>> Failed to update an empty domain")
|
||||
return false
|
||||
}
|
||||
updated := false
|
||||
client := &http.Client{}
|
||||
if domainConfig.EnableIPv4 && domainConfig.Ipv4Addr != "" {
|
||||
url := provider.FormatWebhookString(provider.URL, domainConfig, "ipv4")
|
||||
body := provider.FormatWebhookString(provider.RequestBody, domainConfig, "ipv4")
|
||||
header := provider.FormatWebhookString(provider.RequestHeader, domainConfig, "ipv4")
|
||||
headers := strings.Split(header, "\n")
|
||||
req, err := http.NewRequest(provider.RequestMethod, url, bytes.NewBufferString(body))
|
||||
if err == nil && req != nil {
|
||||
SetStringHeadersToRequest(req, headers)
|
||||
if _, err := client.Do(req); err != nil {
|
||||
log.Printf("NEZHA>> Failed to update a domain: %s. Cause by: %s\n", domainConfig.FullDomain, err.Error())
|
||||
}
|
||||
updated = true
|
||||
}
|
||||
}
|
||||
if domainConfig.EnableIpv6 && domainConfig.Ipv6Addr != "" {
|
||||
url := provider.FormatWebhookString(provider.URL, domainConfig, "ipv6")
|
||||
body := provider.FormatWebhookString(provider.RequestBody, domainConfig, "ipv6")
|
||||
header := provider.FormatWebhookString(provider.RequestHeader, domainConfig, "ipv6")
|
||||
headers := strings.Split(header, "\n")
|
||||
req, err := http.NewRequest(provider.RequestMethod, url, bytes.NewBufferString(body))
|
||||
if err == nil && req != nil {
|
||||
SetStringHeadersToRequest(req, headers)
|
||||
if _, err := client.Do(req); err != nil {
|
||||
log.Printf("NEZHA>> Failed to update a domain: %s. Cause by: %s\n", domainConfig.FullDomain, err.Error())
|
||||
}
|
||||
updated = true
|
||||
}
|
||||
}
|
||||
return updated
|
||||
}
|
||||
|
||||
type DDNSProviderDummy struct{}
|
||||
|
||||
func (provider DDNSProviderDummy) UpdateDomain(domainConfig *DDNSDomainConfig) bool {
|
||||
return false
|
||||
}
|
||||
|
||||
type DDNSProviderCloudflare struct {
|
||||
Secret string
|
||||
}
|
||||
|
||||
func (provider DDNSProviderCloudflare) UpdateDomain(domainConfig *DDNSDomainConfig) bool {
|
||||
if domainConfig == nil {
|
||||
return false
|
||||
}
|
||||
|
||||
zoneID, err := provider.getZoneID(domainConfig.FullDomain)
|
||||
if err != nil {
|
||||
log.Printf("无法获取 zone ID: %s\n", err)
|
||||
return false
|
||||
}
|
||||
|
||||
record, err := provider.findDNSRecord(zoneID, domainConfig.FullDomain)
|
||||
if err != nil {
|
||||
log.Printf("查找 DNS 记录时出错: %s\n", err)
|
||||
return false
|
||||
}
|
||||
|
||||
if record == nil {
|
||||
// 添加 DNS 记录
|
||||
return provider.createDNSRecord(zoneID, domainConfig)
|
||||
} else {
|
||||
// 更新 DNS 记录
|
||||
return provider.updateDNSRecord(zoneID, record["id"].(string), domainConfig)
|
||||
}
|
||||
}
|
||||
|
||||
func (provider DDNSProviderCloudflare) getZoneID(domain string) (string, error) {
|
||||
_, realDomain := SplitDomain(domain)
|
||||
url := fmt.Sprintf("https://api.cloudflare.com/client/v4/zones?name=%s", realDomain)
|
||||
body, err := provider.sendRequest("GET", url, nil)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
|
||||
var res map[string]interface{}
|
||||
err = json.Unmarshal(body, &res)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
|
||||
result := res["result"].([]interface{})
|
||||
if len(result) > 0 {
|
||||
zoneID := result[0].(map[string]interface{})["id"].(string)
|
||||
return zoneID, nil
|
||||
}
|
||||
|
||||
return "", fmt.Errorf("找不到 Zone ID")
|
||||
}
|
||||
|
||||
func (provider DDNSProviderCloudflare) findDNSRecord(zoneID string, domain string) (map[string]interface{}, error) {
|
||||
url := fmt.Sprintf("https://api.cloudflare.com/client/v4/zones/%s/dns_records?type=A&name=%s", zoneID, domain)
|
||||
body, err := provider.sendRequest("GET", url, nil)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
var res map[string]interface{}
|
||||
err = json.Unmarshal(body, &res)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
result := res["result"].([]interface{})
|
||||
if len(result) > 0 {
|
||||
return result[0].(map[string]interface{}), nil
|
||||
}
|
||||
|
||||
return nil, nil // 没有找到 DNS 记录
|
||||
}
|
||||
|
||||
func (provider DDNSProviderCloudflare) createDNSRecord(zoneID string, domainConfig *DDNSDomainConfig) bool {
|
||||
url := fmt.Sprintf("https://api.cloudflare.com/client/v4/zones/%s/dns_records", zoneID)
|
||||
data := map[string]interface{}{
|
||||
"type": "A",
|
||||
"name": domainConfig.FullDomain,
|
||||
"content": domainConfig.Ipv4Addr,
|
||||
"ttl": 3600,
|
||||
"proxied": false,
|
||||
}
|
||||
jsonData, _ := json.Marshal(data)
|
||||
_, err := provider.sendRequest("POST", url, jsonData)
|
||||
return err == nil
|
||||
}
|
||||
|
||||
func (provider DDNSProviderCloudflare) updateDNSRecord(zoneID string, recordID string, domainConfig *DDNSDomainConfig) bool {
|
||||
url := fmt.Sprintf("https://api.cloudflare.com/client/v4/zones/%s/dns_records/%s", zoneID, recordID)
|
||||
data := map[string]interface{}{
|
||||
"type": "A",
|
||||
"name": domainConfig.FullDomain,
|
||||
"content": domainConfig.Ipv4Addr,
|
||||
"ttl": 3600,
|
||||
"proxied": false,
|
||||
}
|
||||
jsonData, _ := json.Marshal(data)
|
||||
_, err := provider.sendRequest("PATCH", url, jsonData)
|
||||
return err == nil
|
||||
}
|
||||
|
||||
// 以下为辅助方法,如发送 HTTP 请求等
|
||||
func (provider DDNSProviderCloudflare) sendRequest(method string, url string, data []byte) ([]byte, error) {
|
||||
client := &http.Client{}
|
||||
req, err := http.NewRequest(method, url, bytes.NewBuffer(data))
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
req.Header.Add("Authorization", fmt.Sprintf("Bearer %s", provider.Secret))
|
||||
req.Header.Add("Content-Type", "application/json")
|
||||
|
||||
resp, err := client.Do(req)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
defer func(Body io.ReadCloser) {
|
||||
err := Body.Close()
|
||||
if err != nil {
|
||||
log.Printf("NEZHA>> 无法关闭HTTP响应体流: %s\n", err.Error())
|
||||
}
|
||||
}(resp.Body)
|
||||
|
||||
body, err := io.ReadAll(resp.Body)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return body, nil
|
||||
}
|
||||
|
||||
func (provider DDNSProviderWebHook) FormatWebhookString(s string, config *DDNSDomainConfig, ipType string) string {
|
||||
if config == nil {
|
||||
return s
|
||||
}
|
||||
|
||||
result := strings.TrimSpace(s)
|
||||
result = strings.Replace(s, "{ip}", config.Ipv4Addr, -1)
|
||||
result = strings.Replace(result, "{domain}", config.FullDomain, -1)
|
||||
result = strings.Replace(result, "{type}", ipType, -1)
|
||||
result = strings.Replace(result, "{access_id}", Conf.DDNS.AccessID, -1)
|
||||
result = strings.Replace(result, "{access_secret}", Conf.DDNS.AccessSecret, -1)
|
||||
// remove \r
|
||||
result = strings.Replace(result, "\r", "", -1)
|
||||
return result
|
||||
}
|
||||
|
||||
func SetStringHeadersToRequest(req *http.Request, headers []string) {
|
||||
if req == nil {
|
||||
return
|
||||
}
|
||||
for _, element := range headers {
|
||||
kv := strings.SplitN(element, ":", 2)
|
||||
if len(kv) == 2 {
|
||||
req.Header.Add(kv[0], kv[1])
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// SplitDomain 分割域名为前缀和一级域名
|
||||
func SplitDomain(domain string) (prefix string, topLevelDomain string) {
|
||||
// 带有二级TLD的一些常见例子,需要特别处理
|
||||
secondLevelTLDs := map[string]bool{
|
||||
".co.uk": true, ".com.cn": true, ".gov.cn": true, ".net.cn": true, ".org.cn": true,
|
||||
}
|
||||
|
||||
// 分割域名为"."的各部分
|
||||
parts := strings.Split(domain, ".")
|
||||
|
||||
// 处理特殊情况,例如 ".co.uk"
|
||||
for i := len(parts) - 2; i > 0; i-- {
|
||||
potentialTLD := fmt.Sprintf(".%s.%s", parts[i], parts[i+1])
|
||||
if secondLevelTLDs[potentialTLD] {
|
||||
if i > 1 {
|
||||
return strings.Join(parts[:i-1], "."), strings.Join(parts[i-1:], ".")
|
||||
}
|
||||
return "", domain // 当域名仅为二级TLD时,无前缀
|
||||
}
|
||||
}
|
||||
|
||||
// 常规处理,查找最后一个"."前的所有内容作为前缀
|
||||
if len(parts) > 2 {
|
||||
return strings.Join(parts[:len(parts)-2], "."), strings.Join(parts[len(parts)-2:], ".")
|
||||
}
|
||||
return "", domain // 当域名不包含子域名时,无前缀
|
||||
}
|
||||
|
||||
func RetryableUpdateDomain(provider DDNSProvider, config *DDNSDomainConfig, maxRetries int) bool {
|
||||
if nil == config {
|
||||
return false
|
||||
}
|
||||
for retries := 0; retries < maxRetries; retries++ {
|
||||
log.Printf("NEZHA>> 正在尝试更新域名(%s)DDNS(%d/%d)\n", config.FullDomain, retries+1, maxRetries)
|
||||
if provider.UpdateDomain(config) {
|
||||
log.Printf("NEZHA>> 尝试更新域名(%s)DDNS成功\n", config.FullDomain)
|
||||
return true
|
||||
}
|
||||
}
|
||||
log.Printf("NEZHA>> 尝试更新域名(%s)DDNS失败\n", config.FullDomain)
|
||||
return false
|
||||
}
|
||||
|
||||
func GetDDNSProviderFromString(provider string) (DDNSProvider, error) {
|
||||
switch provider {
|
||||
case "webhook":
|
||||
return DDNSProviderWebHook{
|
||||
URL: Conf.DDNS.WebhookURL,
|
||||
RequestMethod: Conf.DDNS.WebhookMethod,
|
||||
RequestBody: Conf.DDNS.WebhookRequestBody,
|
||||
RequestHeader: Conf.DDNS.WebhookHeaders,
|
||||
}, nil
|
||||
case "dummy":
|
||||
return DDNSProviderDummy{}, nil
|
||||
case "cloudflare":
|
||||
return DDNSProviderCloudflare{
|
||||
Secret: Conf.DDNS.AccessSecret,
|
||||
}, nil
|
||||
}
|
||||
return DDNSProviderDummy{}, errors.New(fmt.Sprintf("无法找到配置的DDNS提供者%s", Conf.DDNS.Provider))
|
||||
}
|
||||
@@ -1,6 +1,7 @@
|
||||
package singleton
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"log"
|
||||
"time"
|
||||
|
||||
@@ -46,6 +47,21 @@ func InitConfigFromPath(path string) {
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
ValidateConfig()
|
||||
}
|
||||
|
||||
// ValidateConfig 验证配置文件有效性
|
||||
func ValidateConfig() {
|
||||
// 如果DDNS启用则检查Provider是否存在, 不存在直接退出
|
||||
if Conf.DDNS.Enable {
|
||||
_, err := GetDDNSProviderFromString(Conf.DDNS.Provider)
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
if Conf.DDNS.MaxRetries < 1 || Conf.DDNS.MaxRetries > 10 {
|
||||
panic(fmt.Errorf("DDNS.MaxRetries值域为[1, 10]的整数, 当前为 %d", Conf.DDNS.MaxRetries))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// InitDBFromPath 从给出的文件路径中加载数据库
|
||||
|
||||
Reference in New Issue
Block a user