Compare commits

...
36 Commits
Author SHA1 Message Date
naiba fe234f4f38 improve: Handle case when host is not found in LookupGeoIP 2024-07-28 15:11:50 +08:00
naiba df9487b5fc fix: CountryCode 被冲掉 2024-07-28 14:39:14 +08:00
UUBulbandnaiba 26d81f6d7a feat: use embed geoip database (#396)
* feat: use embed geoip database

* mention ipinfo

* only read once

* comments

comments

* chore: installer version

---------

Co-authored-by: naiba <[email protected]>
2024-07-28 13:59:58 +08:00
UUBulb 9d96c58ff9 api: add DisPlayIndex info (#397) 2024-07-28 13:43:28 +08:00
nap0o 5bb7efdeb9 🎉 通过自定义代码实现server-status主题深色模式半透明样式的前置准备 (#395) 2024-07-24 23:15:17 +08:00
github-actions[bot] 519882c4a9 update contributors[no ci] 2024-07-23 10:27:07 +00:00
MadDogOwner ed96432b79 installer: improve region detection reliability using multiple APIs (#394) 2024-07-23 18:26:45 +08:00
github-actions[bot] 1c057d93bd update contributors[no ci] 2024-07-19 22:49:08 +00:00
UUBulb 6005004681 installer: use multiple geoip api to determine location (#393) 2024-07-20 06:48:53 +08:00
Spetrum d8f4236404 Switch IP checking service to ip-api.com (#391) 2024-07-17 21:21:04 +08:00
UUBulb f95191c8af fix: concurrent write to single WebSocket connection (#392) 2024-07-17 10:06:23 +08:00
naiba 34fedd91d2 fix: dashboard standalon-alone install 2024-07-15 16:01:24 +08:00
naiba f675b884f5 feat: fetch version from api for dashboard standalone install 2024-07-14 22:52:53 +08:00
naiba 0b13fec029 fix: workflow branch name 2024-07-14 19:52:04 +08:00
naiba 67b788a969 内置HTTP内网穿透 2024-07-14 19:41:50 +08:00
naiba b63f693661 fix: branch name 2024-07-14 14:46:08 +08:00
github-actions[bot] ac64aa7355 update contributors[no ci] 2024-07-14 06:38:29 +00:00
UUBulb 05d69d5c07 ci: sync release to gitee (#390) 2024-07-14 14:38:12 +08:00
naiba ce624a0ca6 fix: loop delay 2024-07-14 13:04:35 +08:00
naiba 1c91fcffac feat: 去除 webTerminal 的 websocket 依赖 2024-07-14 12:47:36 +08:00
UUBulb 417f972659 installer: restore the China mirror of agent (#389) 2024-07-14 07:34:45 +08:00
naiba ada344cd6b chore: go 1.21.x 2024-07-13 13:11:40 +08:00
naiba 9d6682fd58 fix: build 2024-07-13 13:00:48 +08:00
github-actions[bot] 22c687c044 update contributors[no ci] 2024-07-13 04:52:14 +00:00
Tao Chen e1513203b4 添加OIDC支持 (#387)
* add general OIDC

* use "github.com/coreos/go-oidc/v3/oidc" to simplify oidc config

* fix: check if https by X-Forwarded-Proto

* recovery config.yaml
2024-07-13 12:51:59 +08:00
nap0o 17373e2dee 🐛 fixbug (#388)
* 修复bug:在server-status主题无法切换

* 替换哪吒面板后台cdn提供商为unpkg

* 替换哪吒面板后台cdn提供商为unpkg

* 加快Agent详情下拉展示速度
2024-07-12 22:29:11 +08:00
github-actions[bot] e7c6984c72 update contributors[no ci] 2024-07-10 15:14:11 +00:00
nap0o f4c6f4c57d ServerStatus主题优化 (#386)
* ServerStatus主题优化
1.更新世界地图基础geo文件,中国部分包含南海10段线,藏南部分划线更准
geo在大佬仓库https://github.com/Surbowl/world-geo-json-zh的基础上加工
2.世界地图可以在后台设置自定义代码切换是否包含南极洲,中国港澳台是否单独统计vps数
```<script>
  localStorage.setItem('countryMapGeoFile', 'nezha.world.geo.json'); //默认
  // localStorage.setItem('countryMapGeoFile', 'nezha.world.plus.geo.json'); //单独绘制香港,澳门,台湾
  // localStorage.setItem('countryMapGeoFile', 'nezha.world.antarctica.geo.json'); //有南极洲
  // localStorage.setItem('countryMapGeoFile', 'nezha.world.plus.antarctica.geo.json'); //有南极洲,单独绘制香港,澳门,台湾
</script>
```
3. mixin.js文件删除冗余代码
4. 首页agent下拉详情,控制网络折线图tooltip数字最多显示小数点后两位

* 修正geo文件路径

* 修复世界地图中国港澳台单独绘制时,vps数量计算逻辑

* 删除header中的无用的地图基础数据引用

* 增加温度控制cpu型号识别
2024-07-10 23:13:53 +08:00
UUBulb 4df60c6955 ddns: improve performance (#385) 2024-07-07 10:19:58 +08:00
UUBulb 0501e120f6 fix: timestamp conversion (#383) 2024-07-04 14:32:19 +08:00
UUBulb cfa425bb33 feat: notifications support for GPU & Temperature (#381) 2024-07-03 12:34:45 +08:00
nap0o 0613b0022d 优化server-status主题服务页 (#382) 2024-07-02 22:11:20 +08:00
github-actions[bot] 740d15bf79 update contributors[no ci] 2024-06-28 02:32:48 +00:00
Kris 7ace5d04fa Update README.md (#380) 2024-06-28 10:32:31 +08:00
github-actions[bot] a38d7bb51d update contributors[no ci] 2024-06-25 15:00:50 +00:00
UUBulb 61e16f96c5 fix: EUID is a readonly variable on some shells (#379) 2024-06-25 23:00:30 +08:00
76 changed files with 4268 additions and 752 deletions
+90
View File
@@ -0,0 +1,90 @@
import os
import time
import requests
import hashlib
from github import Github
def get_github_latest_release():
g = Github()
repo = g.get_repo("naiba/nezha")
release = repo.get_latest_release()
if release:
print(f"Latest release tag is: {release.tag_name}")
print(f"Latest release info is: {release.body}")
files = []
for asset in release.get_assets():
url = asset.browser_download_url
name = asset.name
response = requests.get(url)
if response.status_code == 200:
with open(name, 'wb') as f:
f.write(response.content)
print(f"Downloaded {name}")
else:
print(f"Failed to download {name}")
file_abs_path = get_abs_path(asset.name)
files.append(file_abs_path)
sync_to_gitee(release.tag_name, release.body, files)
else:
print("No releases found.")
def sync_to_gitee(tag: str, body: str, files: slice):
release_id = ""
owner = "naibahq"
repo = "nezha"
release_api_uri = f"https://gitee.com/api/v5/repos/{owner}/{repo}/releases"
api_client = requests.Session()
api_client.headers.update({
'Accept': 'application/json',
'Content-Type': 'application/json'
})
access_token = os.environ['GITEE_TOKEN']
release_data = {
'access_token': access_token,
'tag_name': tag,
'name': tag,
'body': body,
'prerelease': False,
'target_commitish': 'master'
}
release_api_response = api_client.post(release_api_uri, json=release_data)
if release_api_response.status_code == 201:
release_info = release_api_response.json()
release_id = release_info.get('id')
else:
print(
f"Request failed with status code {release_api_response.status_code}")
print(f"Gitee release id: {release_id}")
asset_api_uri = f"{release_api_uri}/{release_id}/attach_files"
for file_path in files:
files = {
'file': open(file_path, 'rb')
}
asset_api_response = requests.post(
asset_api_uri, params={'access_token': access_token}, files=files)
if asset_api_response.status_code == 201:
asset_info = asset_api_response.json()
asset_name = asset_info.get('name')
print(f"Successfully uploaded {asset_name}!")
else:
print(
f"Request failed with status code {asset_api_response.status_code}")
api_client.close()
print("Sync is completed!")
def get_abs_path(path: str):
wd = os.getcwd()
return os.path.join(wd, path)
get_github_latest_release()
+22 -1
View File
@@ -12,6 +12,13 @@ jobs:
steps:
- uses: actions/checkout@v4
- name: Fetch IPInfo GeoIP Database
env:
IPINFO_TOKEN: ${{ secrets.IPINFO_TOKEN }}
run: |
rm pkg/geoip/geoip.db
wget -O pkg/geoip/geoip.db https://ipinfo.io/data/free/country.mmdb?token=${IPINFO_TOKEN}
- name: Extract branch name
run: |
export TAG_NAME=$(echo ${GITHUB_REF#refs/tags/})
@@ -22,7 +29,7 @@ jobs:
uses: crazy-max/ghaction-xgo@v2
with:
xgo_version: latest
go_version: 1.20.x
go_version: 1.21.x
dest: dist
pkg: cmd/dashboard
prefix: dashboard
@@ -36,6 +43,7 @@ jobs:
- name: fix dist
run: |
ls -al dist/
mv dist/dashboard-linux-arm-7 dist/dashboard-linux-arm
- name: Log in to the GHCR
@@ -107,3 +115,16 @@ jobs:
curl -s https://purge.jsdelivr.net/gh/$LOWER_USERNAME/nezha@master/script/nezha-agent.service
curl -s https://purge.jsdelivr.net/gh/$LOWER_USERNAME/nezha@master/script/docker-compose.yaml
curl -s https://purge.jsdelivr.net/gh/$LOWER_USERNAME/nezha@master/script/config.yaml
- name: Trigger sync
if: ${{ env.SYNCED == 0 }}
uses: actions/github-script@v6
with:
github-token: ${{ secrets.GITHUB_TOKEN }}
script: |
await github.rest.actions.createWorkflowDispatch({
owner: 'naiba',
repo: 'nezha',
workflow_id: 'sync-release.yml',
ref: 'master'
})
+16
View File
@@ -0,0 +1,16 @@
name: Sync Release to Gitee
on:
workflow_dispatch:
jobs:
sync-release-to-gitee:
runs-on: ubuntu-latest
env:
GITEE_TOKEN: ${{ secrets.GITEE_TOKEN }}
steps:
- uses: actions/checkout@v4
- name: Sync to Gitee
run: |
pip3 install PyGitHub
python3 .github/sync.py
+5 -1
View File
@@ -25,7 +25,7 @@ jobs:
uses: crazy-max/ghaction-xgo@v2
with:
xgo_version: latest
go_version: 1.20.x
go_version: 1.21.x
dest: dist
pkg: cmd/dashboard
prefix: dashboard
@@ -36,4 +36,8 @@ jobs:
tags: timetzdata
ldflags: -s -w --extldflags '-static -fpic' -X github.com/naiba/nezha/service/singleton.Version=test
buildmode: default
- name: fix dist
run: |
ls -al dist/
mv dist/dashboard-linux-arm-7 dist/dashboard-linux-arm
+5 -1
View File
@@ -32,7 +32,7 @@ jobs:
uses: crazy-max/ghaction-xgo@v2
with:
xgo_version: latest
go_version: 1.20.x
go_version: 1.21.x
dest: dist
pkg: cmd/dashboard
prefix: dashboard
@@ -43,3 +43,7 @@ jobs:
tags: timetzdata
ldflags: -s -w --extldflags '-static -fpic' -X github.com/naiba/nezha/service/singleton.Version=test
buildmode: default
- name: fix dist
run: |
ls -al dist/
mv dist/dashboard-linux-arm-7 dist/dashboard-linux-arm
+35 -27
View File
@@ -4,13 +4,15 @@
<br>
<small><i>LOGO designed by <a href="https://xio.ng" target="_blank">熊大</a> .</i></small>
<br><br>
<img alt="GitHub release (with filter)" src="https://img.shields.io/github/v/release/naiba/nezha?color=brightgreen&style=for-the-badge&logo=github&label=Dashboard">&nbsp;<img src="https://img.shields.io/github/v/release/nezhahq/agent?color=brightgreen&label=Agent&style=for-the-badge&logo=github">&nbsp;<img src="https://img.shields.io/github/actions/workflow/status/nezhahq/agent/agent.yml?label=Agent%20CI&logo=github&style=for-the-badge">&nbsp;<img src="https://img.shields.io/badge/Installer-v0.17.0-brightgreen?style=for-the-badge&logo=linux">
<img alt="GitHub release (with filter)" src="https://img.shields.io/github/v/release/naiba/nezha?color=brightgreen&style=for-the-badge&logo=github&label=Dashboard">&nbsp;<img src="https://img.shields.io/github/v/release/nezhahq/agent?color=brightgreen&label=Agent&style=for-the-badge&logo=github">&nbsp;<img src="https://img.shields.io/github/actions/workflow/status/nezhahq/agent/agent.yml?label=Agent%20CI&logo=github&style=for-the-badge">&nbsp;<img src="https://img.shields.io/badge/Installer-v0.18.2-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>
<p>Supports <b>monitoring</b> system status, HTTP (SSL certificate change, upcoming expiration, expired), TCP, Ping and supports <b>push alerts</b>, run scheduled tasks and <b>web terminal</b>.</p>
</div>
\>> Telegram Channel: [哪吒监控(中文通知频道)](https://t.me/nezhanews)
\>> Telegram Group: [Nezha Monitoring Global (English Only)](https://t.me/nezhamonitoring_global), [哪吒监控(中文群组)](https://t.me/nezhamonitoring)
\>> [Use Cases | 我们的用户](https://www.google.com/search?q=%22powered+by+Nezha+Monitoring%22+OR+%22powered+by+%E5%93%AA%E5%90%92%E7%9B%91%E6%8E%A7%22) (Google)
@@ -41,44 +43,47 @@ You can change the dashboard language in the settings page (`/setting`) after th
<!--GAMFC_DELIMITER--><a href="https://github.com/naiba" title="naiba"><img src="https://avatars.githubusercontent.com/u/29243953?v=4" width="50;" alt="naiba"/></a>
<a href="https://github.com/AkkiaS7" title="Akkia"><img src="https://avatars.githubusercontent.com/u/68485070?v=4" width="50;" alt="Akkia"/></a>
<a href="https://github.com/Erope" title="卖女孩的小火柴"><img src="https://avatars.githubusercontent.com/u/44471469?v=4" width="50;" alt="卖女孩的小火柴"/></a>
<a href="https://github.com/dysf888" title="黑歌"><img src="https://avatars.githubusercontent.com/u/47450409?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/uubulb" title="UUBulb"><img src="https://avatars.githubusercontent.com/u/35923940?v=4" width="50;" alt="UUBulb"/></a>
<a href="https://github.com/MikoyChinese" title="MikoyChinese"><img src="https://avatars.githubusercontent.com/u/22676744?v=4" width="50;" alt="MikoyChinese"/></a>
<a href="https://github.com/dysf888" title="黑歌"><img src="https://avatars.githubusercontent.com/u/47450409?v=4" width="50;" alt="黑歌"/></a>
<a href="https://github.com/nap0o" title="nap0o"><img src="https://avatars.githubusercontent.com/u/144927971?v=4" width="50;" alt="nap0o"/></a>
<a href="https://github.com/xykt" title="xykt"><img src="https://avatars.githubusercontent.com/u/152045469?v=4" width="50;" alt="xykt"/></a>
<a href="https://github.com/MikoyChinese" title="MikoyChinese"><img src="https://avatars.githubusercontent.com/u/22676744?v=4" width="50;" alt="MikoyChinese"/></a>
<a href="https://github.com/JackieSung4ev" title="JackieSung4ev"><img src="https://avatars.githubusercontent.com/u/24974735?v=4" width="50;" alt="JackieSung4ev"/></a>
<a href="https://github.com/lemoeo" title="Lemoe"><img src="https://avatars.githubusercontent.com/u/18618627?v=4" width="50;" alt="Lemoe"/></a>
<a href="https://github.com/cantoblanco" title="Kris"><img src="https://avatars.githubusercontent.com/u/116849421?v=4" width="50;" alt="Kris"/></a>
<a href="https://github.com/spiritLHLS" title="spiritlhl"><img src="https://avatars.githubusercontent.com/u/103393591?v=4" width="50;" alt="spiritlhl"/></a>
<a href="https://github.com/liuyanxi975" title="刘颜溪"><img src="https://avatars.githubusercontent.com/u/24417037?v=4" width="50;" alt="刘颜溪"/></a>
<a href="https://github.com/lemoeo" title="Lemoe"><img src="https://avatars.githubusercontent.com/u/18618627?v=4" width="50;" alt="Lemoe"/></a>
<a href="https://github.com/CosmosZ-code" title="CosmosZ-code"><img src="https://avatars.githubusercontent.com/u/81398224?v=4" width="50;" alt="CosmosZ-code"/></a>
<a href="https://github.com/liuyanxi975" title="刘颜溪"><img src="https://avatars.githubusercontent.com/u/24417037?v=4" width="50;" alt="刘颜溪"/></a>
<a href="https://github.com/spiritLHLS" title="spiritlhl"><img src="https://avatars.githubusercontent.com/u/103393591?v=4" width="50;" alt="spiritlhl"/></a>
<a href="https://github.com/1ridic" title="1ridic"><img src="https://avatars.githubusercontent.com/u/88495501?v=4" width="50;" alt="1ridic"/></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/iilemon" title="Sean"><img src="https://avatars.githubusercontent.com/u/33201711?v=4" width="50;" alt="Sean"/></a>
<a href="https://github.com/fscarmen" title="fscarmen"><img src="https://avatars.githubusercontent.com/u/62703343?v=4" width="50;" alt="fscarmen"/></a>
<a href="https://github.com/ch8o" title="no-name-now"><img src="https://avatars.githubusercontent.com/u/9103372?v=4" width="50;" alt="no-name-now"/></a>
<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/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/Mmx233" title="Mmx"><img src="https://avatars.githubusercontent.com/u/36563672?v=4" width="50;" alt="Mmx"/></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/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/zhucaidan" title="zhucaidan"><img src="https://avatars.githubusercontent.com/u/47970938?v=4" width="50;" alt="zhucaidan"/></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/unclezs" title="unclezs"><img src="https://avatars.githubusercontent.com/u/42318775?v=4" width="50;" alt="unclezs"/></a>
<a href="https://github.com/lvyaoting" title="lvyaoting"><img src="https://avatars.githubusercontent.com/u/166296299?v=4" width="50;" alt="lvyaoting"/></a>
<a href="https://github.com/yuanweize" title="I"><img src="https://avatars.githubusercontent.com/u/30067203?v=4" width="50;" alt="I"/></a>
<a href="https://github.com/guoyongchang" title="guoyongchang"><img src="https://avatars.githubusercontent.com/u/10484506?v=4" width="50;" alt="guoyongchang"/></a>
<a href="https://github.com/eya46" title="eya46"><img src="https://avatars.githubusercontent.com/u/61458340?v=4" width="50;" alt="eya46"/></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/adminsama" title="adminsama"><img src="https://avatars.githubusercontent.com/u/60880076?v=4" width="50;" alt="adminsama"/></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/Creling" title="Creling"><img src="https://avatars.githubusercontent.com/u/43109504?v=4" width="50;" alt="Creling"/></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/HsukqiLee" title="HsukqiLee"><img src="https://avatars.githubusercontent.com/u/79034142?v=4" width="50;" alt="HsukqiLee"/></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/fscarmen" title="fscarmen"><img src="https://avatars.githubusercontent.com/u/62703343?v=4" width="50;" alt="fscarmen"/></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/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/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/eya46" title="eya46"><img src="https://avatars.githubusercontent.com/u/61458340?v=4" width="50;" alt="eya46"/></a>
<a href="https://github.com/guoyongchang" title="guoyongchang"><img src="https://avatars.githubusercontent.com/u/10484506?v=4" width="50;" alt="guoyongchang"/></a>
<a href="https://github.com/yuanweize" title="I"><img src="https://avatars.githubusercontent.com/u/30067203?v=4" width="50;" alt="I"/></a>
<a href="https://github.com/lvyaoting" title="lvyaoting"><img src="https://avatars.githubusercontent.com/u/166296299?v=4" width="50;" alt="lvyaoting"/></a>
<a href="https://github.com/unclezs" title="unclezs"><img src="https://avatars.githubusercontent.com/u/42318775?v=4" width="50;" alt="unclezs"/></a>
<a href="https://github.com/ysicing" title="缘生"><img src="https://avatars.githubusercontent.com/u/8605565?v=4" width="50;" alt="缘生"/></a>
<a href="https://github.com/zhucaidan" title="zhucaidan"><img src="https://avatars.githubusercontent.com/u/47970938?v=4" width="50;" alt="zhucaidan"/></a>
<a href="https://github.com/arkylin" title="凌"><img src="https://avatars.githubusercontent.com/u/35104502?v=4" width="50;" alt="凌"/></a>
<a href="https://github.com/colour93" title="玖叁"><img src="https://avatars.githubusercontent.com/u/64313711?v=4" width="50;" alt="玖叁"/></a>
<a href="https://github.com/IamTaoChen" title="Tao Chen"><img src="https://avatars.githubusercontent.com/u/42793494?v=4" width="50;" alt="Tao Chen"/></a>
<a href="https://github.com/Septrum101" title="Spetrum"><img src="https://avatars.githubusercontent.com/u/11692994?v=4" width="50;" alt="Spetrum"/></a>
<a href="https://github.com/dreamingsleeping" title="Nanjing Hopefun Network Technology Co. Ltd."><img src="https://avatars.githubusercontent.com/u/13828658?v=4" width="50;" alt="Nanjing Hopefun Network Technology Co. Ltd."/></a>
<a href="https://github.com/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/funnyzak" title="Leon"><img src="https://avatars.githubusercontent.com/u/2562087?v=4" width="50;" alt="Leon"/></a>
<a href="https://github.com/KorenKrita" title="KorenKrita"><img src="https://avatars.githubusercontent.com/u/22239339?v=4" width="50;" alt="KorenKrita"/></a>
<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>
@@ -87,6 +92,9 @@ You can change the dashboard language in the settings page (`/setting`) after th
<a href="https://github.com/wwng2333" title="Crazy"><img src="https://avatars.githubusercontent.com/u/17147265?v=4" width="50;" alt="Crazy"/></a>
<a href="https://github.com/wellcoming" title="Coming"><img src="https://avatars.githubusercontent.com/u/74850890?v=4" width="50;" alt="Coming"/></a><!--GAMFC_DELIMITER_END-->
## Special Thanks
- [IPInfo](https://ipinfo.io/) for providing an accurate GeoIP Database.
## Star History
[![Star History Chart](https://api.star-history.com/svg?repos=naiba/nezha&type=Timeline)](https://star-history.com/#naiba/nezha&Timeline)
+31 -212
View File
@@ -3,12 +3,8 @@ package controller
import (
"errors"
"fmt"
"log"
"net/http"
"regexp"
"strconv"
"strings"
"sync"
"time"
"github.com/gin-gonic/gin"
@@ -24,22 +20,13 @@ import (
"github.com/naiba/nezha/pkg/utils"
"github.com/naiba/nezha/pkg/websocketx"
"github.com/naiba/nezha/proto"
"github.com/naiba/nezha/service/rpc"
"github.com/naiba/nezha/service/singleton"
)
type terminalContext struct {
agentConn *websocketx.Conn
userConn *websocketx.Conn
serverID uint64
host string
useSSL bool
}
type commonPage struct {
r *gin.Engine
terminals map[string]*terminalContext
terminalsLock *sync.Mutex
requestGroup singleflight.Group
r *gin.Engine
requestGroup singleflight.Group
}
func (cp *commonPage) serve() {
@@ -68,7 +55,6 @@ type viewPasswordForm struct {
func (p *commonPage) issueViewPassword(c *gin.Context) {
var vpf viewPasswordForm
err := c.ShouldBind(&vpf)
log.Println("bingo", vpf)
var hash []byte
if err == nil && vpf.Password != singleton.Conf.Site.ViewPassword {
err = errors.New(singleton.Localizer.MustLocalize(&i18n.LocalizeConfig{MessageID: "WrongAccessPassword"}))
@@ -274,8 +260,8 @@ func (cp *commonPage) home(c *gin.Context) {
}
var upgrader = websocket.Upgrader{
ReadBufferSize: 1024,
WriteBufferSize: 1024,
ReadBufferSize: 10240,
WriteBufferSize: 10240,
}
type Data struct {
@@ -283,8 +269,6 @@ type Data struct {
Servers []*model.Server `json:"servers,omitempty"`
}
var cloudflareCookiesValidator = regexp.MustCompile("^[A-Za-z0-9-_]+$")
func (cp *commonPage) ws(c *gin.Context) {
conn, err := upgrader.Upgrade(c.Writer, c.Request, nil)
if err != nil {
@@ -321,10 +305,8 @@ func (cp *commonPage) ws(c *gin.Context) {
}
func (cp *commonPage) terminal(c *gin.Context) {
terminalID := c.Param("id")
cp.terminalsLock.Lock()
if terminalID == "" || cp.terminals[terminalID] == nil {
cp.terminalsLock.Unlock()
streamId := c.Param("id")
if _, err := rpc.NezhaHandlerSingleton.GetStream(streamId); err != nil {
mygin.ShowErrorPage(c, mygin.ErrInfo{
Code: http.StatusForbidden,
Title: "无权访问",
@@ -334,104 +316,7 @@ func (cp *commonPage) terminal(c *gin.Context) {
}, true)
return
}
terminal := cp.terminals[terminalID]
cp.terminalsLock.Unlock()
defer func() {
// 清理 context
cp.terminalsLock.Lock()
defer cp.terminalsLock.Unlock()
delete(cp.terminals, terminalID)
}()
var isAgent bool
if _, authorized := c.Get(model.CtxKeyAuthorizedUser); !authorized {
singleton.ServerLock.RLock()
_, hasID := singleton.SecretToID[c.Request.Header.Get("Secret")]
singleton.ServerLock.RUnlock()
if !hasID {
mygin.ShowErrorPage(c, mygin.ErrInfo{
Code: http.StatusForbidden,
Title: "无权访问",
Msg: "用户未登录或非法终端",
Link: "/",
Btn: "返回首页",
}, true)
return
}
if terminal.userConn == nil {
mygin.ShowErrorPage(c, mygin.ErrInfo{
Code: http.StatusForbidden,
Title: "无权访问",
Msg: "用户不在线",
Link: "/",
Btn: "返回首页",
}, true)
return
}
if terminal.agentConn != nil {
mygin.ShowErrorPage(c, mygin.ErrInfo{
Code: http.StatusInternalServerError,
Title: "连接已存在",
Msg: "Websocket协议切换失败",
Link: "/",
Btn: "返回首页",
}, true)
return
}
isAgent = true
} else {
singleton.ServerLock.RLock()
server := singleton.ServerList[terminal.serverID]
singleton.ServerLock.RUnlock()
if server == nil || server.TaskStream == nil {
mygin.ShowErrorPage(c, mygin.ErrInfo{
Code: http.StatusForbidden,
Title: "请求失败",
Msg: "服务器不存在或处于离线状态",
Link: "/server",
Btn: "返回重试",
}, true)
return
}
cloudflareCookies, _ := c.Cookie("CF_Authorization")
// Cloudflare Cookies 合法性验证
// 其应该包含.分隔的三组BASE64-URL编码
if cloudflareCookies != "" {
encodedCookies := strings.Split(cloudflareCookies, ".")
if len(encodedCookies) == 3 {
for i := 0; i < 3; i++ {
if !cloudflareCookiesValidator.MatchString(encodedCookies[i]) {
cloudflareCookies = ""
break
}
}
} else {
cloudflareCookies = ""
}
}
terminalData, _ := utils.Json.Marshal(&model.TerminalTask{
Host: terminal.host,
UseSSL: terminal.useSSL,
Session: terminalID,
Cookie: cloudflareCookies,
})
if err := server.TaskStream.Send(&proto.Task{
Type: model.TaskTypeTerminal,
Data: string(terminalData),
}); err != nil {
mygin.ShowErrorPage(c, mygin.ErrInfo{
Code: http.StatusForbidden,
Title: "请求失败",
Msg: "Agent信令下发失败",
Link: "/server",
Btn: "返回重试",
}, true)
return
}
}
defer rpc.NezhaHandlerSingleton.CloseStream(streamId)
wsConn, err := upgrader.Upgrade(c.Writer, c.Request, nil)
if err != nil {
@@ -447,36 +332,7 @@ func (cp *commonPage) terminal(c *gin.Context) {
return
}
defer wsConn.Close()
conn := &websocketx.Conn{Conn: wsConn}
log.Printf("NEZHA>> terminal connected %t %q", isAgent, c.Request.URL)
defer log.Printf("NEZHA>> terminal disconnected %t %q", isAgent, c.Request.URL)
if isAgent {
terminal.agentConn = conn
defer func() {
// Agent断开链接时断开用户连接
if terminal.userConn != nil {
terminal.userConn.Close()
}
}()
} else {
terminal.userConn = conn
defer func() {
// 用户断开链接时断开 Agent 连接
if terminal.agentConn != nil {
terminal.agentConn.Close()
}
}()
}
deadlineCh := make(chan interface{})
go func() {
// 对方连接超时
connectDeadline := time.NewTimer(time.Second * 15)
<-connectDeadline.C
deadlineCh <- struct{}{}
}()
conn := websocketx.NewConn(wsConn)
go func() {
// PING 保活
@@ -488,58 +344,11 @@ func (cp *commonPage) terminal(c *gin.Context) {
}
}()
dataCh := make(chan []byte)
errorCh := make(chan error)
go func() {
for {
msgType, data, err := conn.ReadMessage()
if err != nil {
errorCh <- err
return
}
// 将文本消息转换为命令输入
if msgType == websocket.TextMessage {
data = append([]byte{0}, data...)
}
dataCh <- data
}
}()
var dataBuffer [][]byte
var distConn *websocketx.Conn
checkDistConn := func() {
if distConn == nil {
if isAgent {
distConn = terminal.userConn
} else {
distConn = terminal.agentConn
}
}
if err = rpc.NezhaHandlerSingleton.UserConnected(streamId, conn); err != nil {
return
}
for {
select {
case <-deadlineCh:
checkDistConn()
if distConn == nil {
return
}
case <-errorCh:
return
case data := <-dataCh:
dataBuffer = append(dataBuffer, data)
checkDistConn()
if distConn != nil {
for i := 0; i < len(dataBuffer); i++ {
err = distConn.WriteMessage(websocket.BinaryMessage, dataBuffer[i])
if err != nil {
return
}
}
dataBuffer = dataBuffer[:0]
}
}
}
rpc.NezhaHandlerSingleton.StartStream(streamId, time.Second*10)
}
type createTerminalRequest struct {
@@ -571,7 +380,7 @@ func (cp *commonPage) createTerminal(c *gin.Context) {
return
}
id, err := uuid.GenerateUUID()
streamId, err := uuid.GenerateUUID()
if err != nil {
mygin.ShowErrorPage(c, mygin.ErrInfo{
Code: http.StatusInternalServerError,
@@ -585,6 +394,8 @@ func (cp *commonPage) createTerminal(c *gin.Context) {
return
}
rpc.NezhaHandlerSingleton.CreateStream(streamId)
singleton.ServerLock.RLock()
server := singleton.ServerList[createTerminalReq.ID]
singleton.ServerLock.RUnlock()
@@ -599,17 +410,25 @@ func (cp *commonPage) createTerminal(c *gin.Context) {
return
}
cp.terminalsLock.Lock()
defer cp.terminalsLock.Unlock()
cp.terminals[id] = &terminalContext{
serverID: createTerminalReq.ID,
host: createTerminalReq.Host,
useSSL: createTerminalReq.Protocol == "https:",
terminalData, _ := utils.Json.Marshal(&model.TerminalTask{
StreamID: streamId,
})
if err := server.TaskStream.Send(&proto.Task{
Type: model.TaskTypeTerminalGRPC,
Data: string(terminalData),
}); err != nil {
mygin.ShowErrorPage(c, mygin.ErrInfo{
Code: http.StatusForbidden,
Title: "请求失败",
Msg: "Agent信令下发失败",
Link: "/server",
Btn: "返回重试",
}, true)
return
}
c.HTML(http.StatusOK, "dashboard-"+singleton.Conf.Site.DashboardTheme+"/terminal", mygin.CommonEnvironment(c, gin.H{
"SessionID": id,
"SessionID": streamId,
"ServerName": server.Name,
}))
}
+73 -7
View File
@@ -1,6 +1,7 @@
package controller
import (
"encoding/json"
"fmt"
"html/template"
"io/fs"
@@ -9,22 +10,31 @@ import (
"os"
"strconv"
"strings"
"sync"
"time"
"code.cloudfoundry.org/bytefmt"
"github.com/gin-contrib/pprof"
"github.com/gin-gonic/gin"
"github.com/hashicorp/go-uuid"
"github.com/nicksnyder/go-i18n/v2/i18n"
"github.com/naiba/nezha/model"
"github.com/naiba/nezha/pkg/mygin"
"github.com/naiba/nezha/pkg/utils"
"github.com/naiba/nezha/proto"
"github.com/naiba/nezha/resource"
"github.com/naiba/nezha/service/rpc"
"github.com/naiba/nezha/service/singleton"
)
func ServeWeb(port uint) *http.Server {
gin.SetMode(gin.ReleaseMode)
r := gin.Default()
if singleton.Conf.Debug {
gin.SetMode(gin.DebugMode)
pprof.Register(r)
}
r.Use(natGateway)
tmpl := template.New("").Funcs(funcMap)
var err error
tmpl, err = tmpl.ParseFS(resource.TemplateFS, "template/**/*.html")
@@ -33,10 +43,6 @@ func ServeWeb(port uint) *http.Server {
}
tmpl = loadThirdPartyTemplates(tmpl)
r.SetHTMLTemplate(tmpl)
if singleton.Conf.Debug {
gin.SetMode(gin.DebugMode)
pprof.Register(r)
}
r.Use(mygin.RecordPath)
staticFs, err := fs.Sub(resource.StaticFS, "static")
if err != nil {
@@ -45,7 +51,6 @@ func ServeWeb(port uint) *http.Server {
r.StaticFS("/static", http.FS(staticFs))
r.Static("/static-custom", "resource/static/custom")
routers(r)
page404 := func(c *gin.Context) {
mygin.ShowErrorPage(c, mygin.ErrInfo{
Code: http.StatusNotFound,
@@ -68,7 +73,7 @@ func ServeWeb(port uint) *http.Server {
func routers(r *gin.Engine) {
// 通用页面
cp := commonPage{r: r, terminals: make(map[string]*terminalContext), terminalsLock: new(sync.Mutex)}
cp := commonPage{r: r}
cp.serve()
// 游客页面
gp := guestPage{r}
@@ -239,3 +244,64 @@ var funcMap = template.FuncMap{
return singleton.StatusCodeToString(singleton.GetStatusCode(val))
},
}
func natGateway(c *gin.Context) {
natConfig := singleton.GetNATConfigByDomain(c.Request.Host)
if natConfig == nil {
return
}
singleton.ServerLock.RLock()
server := singleton.ServerList[natConfig.ServerID]
singleton.ServerLock.RUnlock()
if server == nil || server.TaskStream == nil {
c.Writer.WriteString("server not found or not connected")
c.Abort()
return
}
streamId, err := uuid.GenerateUUID()
if err != nil {
c.Writer.WriteString(fmt.Sprintf("stream id error: %v", err))
c.Abort()
return
}
rpc.NezhaHandlerSingleton.CreateStream(streamId)
defer rpc.NezhaHandlerSingleton.CloseStream(streamId)
taskData, err := json.Marshal(model.TaskNAT{
StreamID: streamId,
Host: natConfig.Host,
})
if err != nil {
c.Writer.WriteString(fmt.Sprintf("task data error: %v", err))
c.Abort()
return
}
if err := server.TaskStream.Send(&proto.Task{
Type: model.TaskTypeNAT,
Data: string(taskData),
}); err != nil {
c.Writer.WriteString(fmt.Sprintf("send task error: %v", err))
c.Abort()
return
}
w, err := utils.NewRequestWrapper(c.Request, c.Writer)
if err != nil {
c.Writer.WriteString(fmt.Sprintf("request wrapper error: %v", err))
c.Abort()
return
}
if err := rpc.NezhaHandlerSingleton.UserConnected(streamId, w); err != nil {
c.Writer.WriteString(fmt.Sprintf("user connected error: %v", err))
c.Abort()
return
}
rpc.NezhaHandlerSingleton.StartStream(streamId, time.Second*10)
c.Abort()
}
+7
View File
@@ -35,6 +35,10 @@ func (gp *guestPage) serve() {
}
func (gp *guestPage) login(c *gin.Context) {
if singleton.Conf.Oauth2.OidcAutoLogin {
c.Redirect(http.StatusFound, "/oauth2/login")
return
}
LoginType := "GitHub"
RegistrationLink := "https://github.com/join"
if singleton.Conf.Oauth2.Type == model.ConfigTypeGitee {
@@ -52,6 +56,9 @@ func (gp *guestPage) login(c *gin.Context) {
} else if singleton.Conf.Oauth2.Type == model.ConfigTypeCloudflare {
LoginType = "Cloudflare"
RegistrationLink = "https://dash.cloudflare.com/sign-up/teams"
} else if singleton.Conf.Oauth2.Type == model.ConfigTypeOidc {
LoginType = singleton.Conf.Oauth2.OidcDisplayName
RegistrationLink = singleton.Conf.Oauth2.OidcRegisterURL
}
c.HTML(http.StatusOK, "dashboard-"+singleton.Conf.Site.DashboardTheme+"/login", mygin.CommonEnvironment(c, gin.H{
"Title": singleton.Localizer.MustLocalize(&i18n.LocalizeConfig{MessageID: "Login"}),
+50
View File
@@ -45,6 +45,7 @@ func (ma *memberAPI) serve() {
mr.POST("/batch-update-server-group", ma.batchUpdateServerGroup)
mr.POST("/batch-delete-server", ma.batchDeleteServer)
mr.POST("/notification", ma.addOrEditNotification)
mr.POST("/nat", ma.addOrEditNAT)
mr.POST("/alert-rule", ma.addOrEditAlertRule)
mr.POST("/setting", ma.updateSetting)
mr.DELETE("/:model/:id", ma.delete)
@@ -209,6 +210,11 @@ func (ma *memberAPI) delete(c *gin.Context) {
if err == nil {
singleton.OnDeleteNotification(id)
}
case "nat":
err = singleton.DB.Unscoped().Delete(&model.NAT{}, "id = ?", id).Error
if err == nil {
singleton.OnNATUpdate()
}
case "monitor":
err = singleton.DB.Unscoped().Delete(&model.Monitor{}, "id = ?", id).Error
if err == nil {
@@ -733,6 +739,45 @@ func (ma *memberAPI) addOrEditNotification(c *gin.Context) {
})
}
type natForm struct {
ID uint64
Name string
ServerID uint64
Host string
Domain string
}
func (ma *memberAPI) addOrEditNAT(c *gin.Context) {
var nf natForm
var n model.NAT
err := c.ShouldBindJSON(&nf)
if err == nil {
n.Name = nf.Name
n.ID = nf.ID
n.Domain = nf.Domain
n.Host = nf.Host
n.ServerID = nf.ServerID
}
if err == nil {
if n.ID == 0 {
err = singleton.DB.Create(&n).Error
} else {
err = singleton.DB.Save(&n).Error
}
}
if err != nil {
c.JSON(http.StatusOK, model.Response{
Code: http.StatusBadRequest,
Message: fmt.Sprintf("请求错误:%s", err),
})
return
}
singleton.OnNATUpdate()
c.JSON(http.StatusOK, model.Response{
Code: http.StatusOK,
})
}
type alertRuleForm struct {
ID uint64
Name string
@@ -847,6 +892,11 @@ func (ma *memberAPI) logout(c *gin.Context) {
c.JSON(http.StatusOK, model.Response{
Code: http.StatusOK,
})
if oidcLogoutUrl := singleton.Conf.Oauth2.OidcLogoutURL; oidcLogoutUrl != "" {
// 重定向到 OIDC 退出登录地址。不知道为什么,这里的重定向不生效
c.Redirect(http.StatusOK, oidcLogoutUrl)
}
}
type settingForm struct {
+10
View File
@@ -27,6 +27,7 @@ func (mp *memberPage) serve() {
mr.GET("/monitor", mp.monitor)
mr.GET("/cron", mp.cron)
mr.GET("/notification", mp.notification)
mr.GET("/nat", mp.nat)
mr.GET("/setting", mp.setting)
mr.GET("/api", mp.api)
}
@@ -77,6 +78,15 @@ func (mp *memberPage) notification(c *gin.Context) {
}))
}
func (mp *memberPage) nat(c *gin.Context) {
var data []model.NAT
singleton.DB.Find(&data)
c.HTML(http.StatusOK, "dashboard-"+singleton.Conf.Site.DashboardTheme+"/nat", mygin.CommonEnvironment(c, gin.H{
"Title": singleton.Localizer.MustLocalize(&i18n.LocalizeConfig{MessageID: "NAT"}),
"NAT": data,
}))
}
func (mp *memberPage) setting(c *gin.Context) {
c.HTML(http.StatusOK, "dashboard-"+singleton.Conf.Site.DashboardTheme+"/setting", mygin.CommonEnvironment(c, gin.H{
"Title": singleton.Localizer.MustLocalize(&i18n.LocalizeConfig{MessageID: "Settings"}),
+65 -12
View File
@@ -10,25 +10,27 @@ import (
"strings"
"time"
"github.com/coreos/go-oidc/v3/oidc"
"github.com/naiba/nezha/pkg/oidc/cloudflare"
myOidc "github.com/naiba/nezha/pkg/oidc/general"
"code.gitea.io/sdk/gitea"
"github.com/gin-gonic/gin"
GitHubAPI "github.com/google/go-github/v47/github"
"github.com/naiba/nezha/model"
"github.com/naiba/nezha/pkg/mygin"
"github.com/naiba/nezha/pkg/utils"
"github.com/naiba/nezha/service/singleton"
"github.com/patrickmn/go-cache"
"github.com/xanzy/go-gitlab"
"golang.org/x/oauth2"
GitHubOauth2 "golang.org/x/oauth2/github"
GitlabOauth2 "golang.org/x/oauth2/gitlab"
"github.com/naiba/nezha/model"
"github.com/naiba/nezha/pkg/mygin"
"github.com/naiba/nezha/pkg/utils"
"github.com/naiba/nezha/service/singleton"
)
type oauth2controller struct {
r gin.IRoutes
r gin.IRoutes
oidcProvider *oidc.Provider
}
func (oa *oauth2controller) serve() {
@@ -88,6 +90,27 @@ func (oa *oauth2controller) getCommonOauth2Config(c *gin.Context) *oauth2.Config
},
RedirectURL: oa.getRedirectURL(c),
}
} else if singleton.Conf.Oauth2.Type == model.ConfigTypeOidc {
var err error
oa.oidcProvider, err = oidc.NewProvider(c.Request.Context(), singleton.Conf.Oauth2.OidcIssuer)
if err != nil {
mygin.ShowErrorPage(c, mygin.ErrInfo{
Code: http.StatusBadRequest,
Title: fmt.Sprintf("Cannot get OIDC infomaion from issuer from %s", singleton.Conf.Oauth2.OidcIssuer),
Msg: err.Error(),
}, true)
return nil
}
scopes := strings.Split(singleton.Conf.Oauth2.OidcScopes, ",")
scopes = append(scopes, oidc.ScopeOpenID)
uniqueScopes := removeDuplicates(scopes)
return &oauth2.Config{
ClientID: singleton.Conf.Oauth2.ClientID,
ClientSecret: singleton.Conf.Oauth2.ClientSecret,
Scopes: uniqueScopes,
Endpoint: oa.oidcProvider.Endpoint(),
RedirectURL: oa.getRedirectURL(c),
}
} else {
return &oauth2.Config{
ClientID: singleton.Conf.Oauth2.ClientID,
@@ -100,7 +123,8 @@ func (oa *oauth2controller) getCommonOauth2Config(c *gin.Context) *oauth2.Config
func (oa *oauth2controller) getRedirectURL(c *gin.Context) string {
scheme := "http://"
if strings.HasPrefix(c.Request.Referer(), "https://") {
referer := c.Request.Referer()
if forwardedProto := c.Request.Header.Get("X-Forwarded-Proto"); forwardedProto == "https" || strings.HasPrefix(referer, "https://") {
scheme = "https://"
}
return scheme + c.Request.Host + "/oauth2/callback"
@@ -179,7 +203,18 @@ func (oa *oauth2controller) callback(c *gin.Context) {
user = cloudflareUserInfo.MapToNezhaUser()
}
}
} else if singleton.Conf.Oauth2.Type == model.ConfigTypeOidc {
userInfo, err := oa.oidcProvider.UserInfo(c.Request.Context(), oauth2.StaticTokenSource(otk))
if err == nil {
loginClaim := singleton.Conf.Oauth2.OidcLoginClaim
groupClain := singleton.Conf.Oauth2.OidcGroupClaim
adminGroups := strings.Split(singleton.Conf.Oauth2.AdminGroups, ",")
autoCreate := singleton.Conf.Oauth2.OidcAutoCreate
var oidceUserInfo *myOidc.UserInfo
if err := userInfo.Claims(&oidceUserInfo); err == nil {
user = oidceUserInfo.MapToNezhaUser(loginClaim, groupClain, adminGroups, autoCreate)
}
}
} else {
var client *GitHubAPI.Client
oc := oauth2Config.Client(ctx, otk)
@@ -212,10 +247,15 @@ func (oa *oauth2controller) callback(c *gin.Context) {
return
}
var isAdmin bool
for _, admin := range strings.Split(singleton.Conf.Oauth2.Admin, ",") {
if admin != "" && strings.EqualFold(user.Login, admin) {
isAdmin = true
break
if user.SuperAdmin {
isAdmin = true
} else {
for _, admin := range strings.Split(singleton.Conf.Oauth2.Admin, ",") {
if admin != "" && strings.EqualFold(user.Login, admin) {
isAdmin = true
break
}
}
}
if !isAdmin {
@@ -242,3 +282,16 @@ func (oa *oauth2controller) callback(c *gin.Context) {
"URL": "/",
}))
}
func removeDuplicates(elements []string) []string {
encountered := map[string]bool{}
result := []string{}
for _, v := range elements {
if !encountered[v] {
encountered[v] = true
result = append(result, v)
}
}
return result
}
+1
View File
@@ -59,6 +59,7 @@ func main() {
return
}
// TODO 使用 cmux 在同一端口服务 HTTP 和 gRPC
singleton.CleanMonitorHistory()
go rpc.ServeRPC(singleton.Conf.GRPCPort)
serviceSentinelDispatchBus := make(chan model.Monitor) // 用于传递服务监控任务信息的channel
+2 -3
View File
@@ -14,9 +14,8 @@ import (
func ServeRPC(port uint) {
server := grpc.NewServer()
pb.RegisterNezhaServiceServer(server, &rpcService.NezhaHandler{
Auth: &rpcService.AuthHandler{},
})
rpcService.NezhaHandlerSingleton = rpcService.NewNezhaHandler()
pb.RegisterNezhaServiceServer(server, rpcService.NezhaHandlerSingleton)
listen, err := net.Listen("tcp", fmt.Sprintf(":%d", port))
if err != nil {
panic(err)
+10 -8
View File
@@ -1,11 +1,12 @@
module github.com/naiba/nezha
go 1.20
go 1.21
require (
code.cloudfoundry.org/bytefmt v0.0.0-20240425163905-bcdc1ad063ea
code.gitea.io/sdk/gitea v0.18.0
github.com/BurntSushi/toml v1.3.2
github.com/coreos/go-oidc/v3 v3.11.0
github.com/gin-contrib/pprof v1.4.0
github.com/gin-gonic/gin v1.9.1
github.com/google/go-github/v47 v47.1.0
@@ -17,17 +18,16 @@ require (
github.com/ory/graceful v0.1.3
github.com/patrickmn/go-cache v2.1.0+incompatible
github.com/robfig/cron/v3 v3.0.1
github.com/samber/lo v1.39.0
github.com/spf13/pflag v1.0.5
github.com/spf13/viper v1.18.2
github.com/xanzy/go-gitlab v0.103.0
golang.org/x/crypto v0.22.0
golang.org/x/net v0.24.0
golang.org/x/oauth2 v0.19.0
golang.org/x/crypto v0.25.0
golang.org/x/net v0.27.0
golang.org/x/oauth2 v0.21.0
golang.org/x/sync v0.7.0
golang.org/x/text v0.14.0
golang.org/x/text v0.16.0
google.golang.org/grpc v1.63.0
google.golang.org/protobuf v1.33.0
google.golang.org/protobuf v1.34.2
gorm.io/driver/sqlite v1.5.5
gorm.io/gorm v1.25.10
sigs.k8s.io/yaml v1.4.0
@@ -41,6 +41,7 @@ require (
github.com/gabriel-vasile/mimetype v1.4.2 // indirect
github.com/gin-contrib/sse v0.1.0 // indirect
github.com/go-fed/httpsig v1.1.0 // indirect
github.com/go-jose/go-jose/v4 v4.0.2 // indirect
github.com/go-playground/locales v0.14.1 // indirect
github.com/go-playground/universal-translator v0.18.1 // indirect
github.com/go-playground/validator/v10 v10.14.0 // indirect
@@ -60,6 +61,7 @@ require (
github.com/mitchellh/mapstructure v1.5.0 // indirect
github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd // indirect
github.com/modern-go/reflect2 v1.0.2 // indirect
github.com/oschwald/maxminddb-golang v1.13.1 // indirect
github.com/pelletier/go-toml/v2 v2.1.0 // indirect
github.com/pkg/errors v0.9.1 // indirect
github.com/sagikazarmark/locafero v0.4.0 // indirect
@@ -74,7 +76,7 @@ require (
go.uber.org/multierr v1.9.0 // indirect
golang.org/x/arch v0.3.0 // indirect
golang.org/x/exp v0.0.0-20230905200255-921286631fa9 // indirect
golang.org/x/sys v0.20.0 // indirect
golang.org/x/sys v0.22.0 // indirect
golang.org/x/time v0.5.0 // indirect
google.golang.org/genproto/googleapis/rpc v0.0.0-20240227224415-6ceb2ff114de // indirect
gopkg.in/ini.v1 v1.67.0 // indirect
+37 -16
View File
@@ -10,14 +10,19 @@ github.com/bytedance/sonic v1.9.1/go.mod h1:i736AoUSYt75HyZLoJW9ERYxcy6eaN6h4BZX
github.com/chenzhuoyu/base64x v0.0.0-20211019084208-fb5309c8db06/go.mod h1:DH46F32mSOjUmXrMHnKwZdA8wcEefY7UVqBKYGjpdQY=
github.com/chenzhuoyu/base64x v0.0.0-20221115062448-fe3a3abad311 h1:qSGYFH7+jGhDF8vLC+iwCD4WpbV1EBDSzWkJODFLams=
github.com/chenzhuoyu/base64x v0.0.0-20221115062448-fe3a3abad311/go.mod h1:b583jCggY9gE99b6G5LEC39OIiVsWj+R97kbl5odCEk=
github.com/coreos/go-oidc/v3 v3.11.0 h1:Ia3MxdwpSw702YW0xgfmP1GVCMA9aEFWu12XUZ3/OtI=
github.com/coreos/go-oidc/v3 v3.11.0/go.mod h1:gE3LgjOgFoHi9a4ce4/tJczr0Ai2/BoDhf0r5lltWI0=
github.com/creack/pty v1.1.9/go.mod h1:oKZEueFk5CKHvIhNR5MUki03XCEU+Q6VDXinZuGJ33E=
github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc h1:U9qPSI2PIWSS1VwoXQT9A3Wy9MM3WgvqSxFWenqJduM=
github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
github.com/davidmz/go-pageant v1.0.2 h1:bPblRCh5jGU+Uptpz6LgMZGD5hJoOt7otgT454WvHn0=
github.com/davidmz/go-pageant v1.0.2/go.mod h1:P2EDDnMqIwG5Rrp05dTRITj9z2zpGcD9efWSkTNKLIE=
github.com/fatih/color v1.16.0 h1:zmkK9Ngbjj+K0yRhTVONQh1p/HknKYSlNT+vZCzyokM=
github.com/fatih/color v1.16.0/go.mod h1:fL2Sau1YI5c0pdGEVCbKQbLXB6edEj1ZgiY4NijnWvE=
github.com/frankban/quicktest v1.14.6 h1:7Xjx+VpznH+oBnejlPUj8oUpdxnVs4f8XU8WnHkI4W8=
github.com/frankban/quicktest v1.14.6/go.mod h1:4ptaffx2x8+WTWXmUCuVU6aPUX1/Mz7zb5vbUoiM6w0=
github.com/fsnotify/fsnotify v1.7.0 h1:8JEhPFa5W2WU7YfeZzPNqzMP6Lwt7L2715Ggo0nosvA=
github.com/fsnotify/fsnotify v1.7.0/go.mod h1:40Bi/Hjc2AVfZrqy+aj+yEI+/bRxZnMJyTJwOpGvigM=
github.com/gabriel-vasile/mimetype v1.4.2 h1:w5qFW6JKBz9Y393Y4q372O9A7cUSequkh1Q7OhCmWKU=
@@ -31,9 +36,13 @@ github.com/gin-gonic/gin v1.9.1 h1:4idEAncQnU5cB7BeOkPtxjfCSye0AAm1R0RVIqJ+Jmg=
github.com/gin-gonic/gin v1.9.1/go.mod h1:hPrL7YrpYKXt5YId3A/Tnip5kqbEAP+KLuI3SUcPTeU=
github.com/go-fed/httpsig v1.1.0 h1:9M+hb0jkEICD8/cAiNqEB66R87tTINszBRTjwjQzWcI=
github.com/go-fed/httpsig v1.1.0/go.mod h1:RCMrTZvN1bJYtofsG4rd5NaO5obxQ5xBkdiS7xsT7bM=
github.com/go-jose/go-jose/v4 v4.0.2 h1:R3l3kkBds16bO7ZFAEEcofK0MkrAJt3jlJznWZG0nvk=
github.com/go-jose/go-jose/v4 v4.0.2/go.mod h1:WVf9LFMHh/QVrmqrOfqun0C45tMe3RoiKJMPvgWwLfY=
github.com/go-logr/logr v1.4.1 h1:pKouT5E8xu9zeFC39JXRDukb6JFQPXM5p5I91188VAQ=
github.com/go-logr/logr v1.4.1/go.mod h1:9T104GzyrTigFIr8wt5mBrctHMim0Nb2HLGrmQ40KvY=
github.com/go-playground/assert/v2 v2.0.1/go.mod h1:VDjEfimB/XKnb+ZQfWdccd7VUvScMdVu0Titje2rxJ4=
github.com/go-playground/assert/v2 v2.2.0 h1:JvknZsQTYeFEAhQwI4qEt9cyV5ONwRHC+lYKSsYSR8s=
github.com/go-playground/assert/v2 v2.2.0/go.mod h1:VDjEfimB/XKnb+ZQfWdccd7VUvScMdVu0Titje2rxJ4=
github.com/go-playground/locales v0.14.0/go.mod h1:sawfccIbzZTqEDETgFXqTho0QybSa7l++s0DH+LDiLs=
github.com/go-playground/locales v0.14.1 h1:EWaQ/wswjilfKLTECiXz7Rh+3BjFhfDFKv/oXslEjJA=
github.com/go-playground/locales v0.14.1/go.mod h1:hxrqLVvrK65+Rwrd5Fc6F2O76J/NuW9t0sjnWqG1slY=
@@ -44,6 +53,7 @@ github.com/go-playground/validator/v10 v10.10.0/go.mod h1:74x4gJWsvQexRdW8Pn3dXS
github.com/go-playground/validator/v10 v10.14.0 h1:vgvQWe3XCz3gIeFDm/HnTIbj6UGmg/+t63MyGU2n5js=
github.com/go-playground/validator/v10 v10.14.0/go.mod h1:9iXMNT7sEkjXb0I+enO7QXmzG6QCsPWY4zveKFVRSyU=
github.com/go-task/slim-sprig v0.0.0-20230315185526-52ccab3ef572 h1:tfuBGBXKqDEevZMzYi5KSi8KkcZtzBcTgAUUtapy0OI=
github.com/go-task/slim-sprig v0.0.0-20230315185526-52ccab3ef572/go.mod h1:9Pwr4B2jHnOSGXyyzV8ROjYa2ojvAY6HCGYYfMoC3Ls=
github.com/goccy/go-json v0.9.7/go.mod h1:6MelG93GURQebXPDq3khkgXZkazVtN9CRI+MGFi0w8I=
github.com/goccy/go-json v0.10.2 h1:CrxCmQqYDkv1z7lO7Wbh2HN93uovUHgrECaO5ZrCXAU=
github.com/goccy/go-json v0.10.2/go.mod h1:6MelG93GURQebXPDq3khkgXZkazVtN9CRI+MGFi0w8I=
@@ -52,17 +62,20 @@ github.com/google/go-cmp v0.5.2/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/
github.com/google/go-cmp v0.5.5/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
github.com/google/go-cmp v0.5.9/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY=
github.com/google/go-cmp v0.6.0 h1:ofyhxvXcZhMsU5ulbFiLKl/XBFqE1GSq7atu8tAmTRI=
github.com/google/go-cmp v0.6.0/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY=
github.com/google/go-github/v47 v47.1.0 h1:Cacm/WxQBOa9lF0FT0EMjZ2BWMetQ1TQfyurn4yF1z8=
github.com/google/go-github/v47 v47.1.0/go.mod h1:VPZBXNbFSJGjyjFRUKo9vZGawTajnWzC/YjGw/oFKi0=
github.com/google/go-querystring v1.1.0 h1:AnCroh3fv4ZBgVIf1Iwtovgjaw/GiKJo8M8yD/fhyJ8=
github.com/google/go-querystring v1.1.0/go.mod h1:Kcdr2DB4koayq7X8pmAG4sNG59So17icRSOU623lUBU=
github.com/google/gofuzz v1.0.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg=
github.com/google/pprof v0.0.0-20240424215950-a892ee059fd6 h1:k7nVchz72niMH6YLQNvHSdIE7iqsQxK1P41mySCvssg=
github.com/google/pprof v0.0.0-20240424215950-a892ee059fd6/go.mod h1:kf6iHlnVGwgKolg33glAes7Yg/8iWP8ukqeldJSO7jw=
github.com/gorilla/websocket v1.5.1 h1:gmztn0JnHVt9JZquRuzLw3g4wouNVzKL15iLr/zn/QY=
github.com/gorilla/websocket v1.5.1/go.mod h1:x3kM2JMyaluk02fnUJpQuwD2dCS5NDG2ZHL0uE0tcaY=
github.com/hashicorp/go-cleanhttp v0.5.2 h1:035FKYIWjmULyFRBKPs8TBQoi0x6d9G4xc9neXJWAZQ=
github.com/hashicorp/go-cleanhttp v0.5.2/go.mod h1:kO/YDlP8L1346E6Sodw+PrpBSV4/SoxCXGY6BqNFT48=
github.com/hashicorp/go-hclog v1.6.3 h1:Qr2kF+eVWjTiYmU7Y31tYlP1h0q/X3Nl3tPGdaB11/k=
github.com/hashicorp/go-hclog v1.6.3/go.mod h1:W4Qnvbt70Wk/zYJryRzDRU/4r0kIg0PVHBcfoyhpF5M=
github.com/hashicorp/go-retryablehttp v0.7.7 h1:C8hUCYzor8PIfXHa4UrZkU4VvK8o9ISHxT2Q8+VepXU=
github.com/hashicorp/go-retryablehttp v0.7.7/go.mod h1:pkQpWZeYWskR+D1tR2O5OcBFOxfA7DoAO6xtkuQnHTk=
github.com/hashicorp/go-uuid v1.0.3 h1:2gKiV6YVmrJ1i2CKKa9obLvRieoRGviZFL26PcT/Co8=
@@ -86,6 +99,7 @@ github.com/kr/pretty v0.1.0/go.mod h1:dAy3ld7l9f0ibDNOQOHHMYYIIbhfbHSm3C4ZsoJORN
github.com/kr/pretty v0.2.1/go.mod h1:ipq/a2n7PKx3OHsz4KJII5eveXtPO4qwEXGdVfWzfnI=
github.com/kr/pretty v0.3.0/go.mod h1:640gp4NfQd8pI5XOwp5fnNeVWj67G7CFk/SaSQn7NBk=
github.com/kr/pretty v0.3.1 h1:flRD4NNwYAUpkphVc1HcthR4KEIFJ65n8Mw5qdRn3LE=
github.com/kr/pretty v0.3.1/go.mod h1:hoEshYVHaxMs3cyo3Yncou5ZscifuDolrwPKZanG3xk=
github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ=
github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI=
github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY=
@@ -96,6 +110,7 @@ github.com/leodido/go-urn v1.2.4/go.mod h1:7ZrI8mTSeBSHl/UaRyKQW1qZeMgak41ANeCNa
github.com/magiconair/properties v1.8.7 h1:IeQXZAiQcpL9mgcAe1Nu6cX9LLw6ExEHKjN0VQdvPDY=
github.com/magiconair/properties v1.8.7/go.mod h1:Dhd985XPs7jluiymwWYZ0G4Z61jb3vdS329zhj2hYo0=
github.com/mattn/go-colorable v0.1.13 h1:fFA4WZxdEF4tXPZVKMLwD8oUnCTTo08duU7wxecdEvA=
github.com/mattn/go-colorable v0.1.13/go.mod h1:7S9/ev0klgBDR4GtXTXX8a3vIGJpMovkB8vQcUbaXHg=
github.com/mattn/go-isatty v0.0.14/go.mod h1:7GGIvUiUoEMVVmxf/4nioHXj79iQHKdU27kJ6hsGG94=
github.com/mattn/go-isatty v0.0.20 h1:xfD0iDuEKnDkl03q4limB+vH+GxLEtL/jb4xVJSWWEY=
github.com/mattn/go-isatty v0.0.20/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y=
@@ -111,9 +126,13 @@ github.com/modern-go/reflect2 v1.0.2/go.mod h1:yWuevngMOJpCy52FWWMvUC8ws7m/LJsjY
github.com/nicksnyder/go-i18n/v2 v2.4.0 h1:3IcvPOAvnCKwNm0TB0dLDTuawWEj+ax/RERNC+diLMM=
github.com/nicksnyder/go-i18n/v2 v2.4.0/go.mod h1:nxYSZE9M0bf3Y70gPQjN9ha7XNHX7gMc814+6wVyEI4=
github.com/onsi/ginkgo/v2 v2.17.1 h1:V++EzdbhI4ZV4ev0UTIj0PzhzOcReJFyJaLjtSF55M8=
github.com/onsi/ginkgo/v2 v2.17.1/go.mod h1:llBI3WDLL9Z6taip6f33H76YcWtJv+7R3HigUjbIBOs=
github.com/onsi/gomega v1.33.0 h1:snPCflnZrpMsy94p4lXVEkHo12lmPnc3vY5XBbreexE=
github.com/onsi/gomega v1.33.0/go.mod h1:+925n5YtiFsLzzafLUHzVMBpvvRAzrydIBiSIxjX3wY=
github.com/ory/graceful v0.1.3 h1:FaeXcHZh168WzS+bqruqWEw/HgXWLdNv2nJ+fbhxbhc=
github.com/ory/graceful v0.1.3/go.mod h1:4zFz687IAF7oNHHiB586U4iL+/4aV09o/PYLE34t2bA=
github.com/oschwald/maxminddb-golang v1.13.1 h1:G3wwjdN9JmIK2o/ermkHM+98oX5fS+k5MbwsmL4MRQE=
github.com/oschwald/maxminddb-golang v1.13.1/go.mod h1:K4pgV9N/GcK694KSTmVSDTODk4IsCNThNdTmnaBZ/F8=
github.com/patrickmn/go-cache v2.1.0+incompatible h1:HRMgzkcYKYpi3C8ajMPV8OFXaaRUnok+kx1WdO15EQc=
github.com/patrickmn/go-cache v2.1.0+incompatible/go.mod h1:3Qf8kWWT7OJRJbdiICTKqZju1ZixQ/KpMGzzAfe6+WQ=
github.com/pelletier/go-toml/v2 v2.0.1/go.mod h1:r9LEWfGN8R5k0VXJ+0BkIe7MYkRdwZOjgMj2KwnJFUo=
@@ -124,17 +143,17 @@ github.com/pkg/errors v0.9.1 h1:FEBLx1zS214owpjy7qsBeixbURkuhQAwrK5UwLGTwt4=
github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0=
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2 h1:Jamvg5psRIccs7FGNTlIRMkT8wgtp5eCXdBlqhYGL6U=
github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
github.com/robfig/cron/v3 v3.0.1 h1:WdRxkvbJztn8LMz/QEvLN5sBU+xKpSqwwUO1Pjr4qDs=
github.com/robfig/cron/v3 v3.0.1/go.mod h1:eQICP3HwyT7UooqI/z+Ov+PtYAWygg1TEWWzGIFLtro=
github.com/rogpeppe/go-internal v1.6.1/go.mod h1:xXDCJY+GAPziupqXw64V24skbSoqbTEfhy4qGm1nDQc=
github.com/rogpeppe/go-internal v1.8.0/go.mod h1:WmiCO8CzOY8rg0OYDC4/i/2WRWAB6poM+XZ2dLUbcbE=
github.com/rogpeppe/go-internal v1.9.0 h1:73kH8U+JUqXU8lRuOHeVHaa/SZPifC7BkcraZVejAe8=
github.com/rogpeppe/go-internal v1.9.0/go.mod h1:WtVeX8xhTBvf0smdhujwtBcq4Qrzq/fJaraNFVN+nFs=
github.com/sagikazarmark/locafero v0.4.0 h1:HApY1R9zGo4DBgr7dqsTH/JJxLTTsOt7u6keLGt6kNQ=
github.com/sagikazarmark/locafero v0.4.0/go.mod h1:Pe1W6UlPYUk/+wc/6KFhbORCfqzgYEpgQ3O5fPuL3H4=
github.com/sagikazarmark/slog-shim v0.1.0 h1:diDBnUNK9N/354PgrxMywXnAwEr1QZcOr6gto+ugjYE=
github.com/sagikazarmark/slog-shim v0.1.0/go.mod h1:SrcSrq8aKtyuqEI1uvTDTK1arOWRIczQRv+GVI1AkeQ=
github.com/samber/lo v1.39.0 h1:4gTz1wUhNYLhFSKl6O+8peW0v2F4BCY034GRpU9WnuA=
github.com/samber/lo v1.39.0/go.mod h1:+m/ZKRl6ClXCE2Lgf3MsQlWfh4bn1bz6CXEOxnEXnEA=
github.com/sourcegraph/conc v0.3.0 h1:OQTbbt6P72L20UqAkXXuLOj79LfEanQ+YQFNpLA9ySo=
github.com/sourcegraph/conc v0.3.0/go.mod h1:Sdozi7LEKbFPqYX2/J+iBAM6HpqSLTASQIKqDmF7Mt0=
github.com/spf13/afero v1.11.0 h1:WJQKhtpdm3v2IzqG8VMqrr6Rf3UYpEF239Jy9wNepM8=
@@ -179,16 +198,16 @@ golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACk
golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto=
golang.org/x/crypto v0.0.0-20210513164829-c07d793c2f9a/go.mod h1:P+XmwS30IXTQdn5tA2iutPOUgjI07+tq3H3K9MVA1s8=
golang.org/x/crypto v0.0.0-20210711020723-a769d52b0f97/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc=
golang.org/x/crypto v0.22.0 h1:g1v0xeRhjcugydODzvb3mEM9SQ0HGp9s/nh3COQ/C30=
golang.org/x/crypto v0.22.0/go.mod h1:vr6Su+7cTlO45qkww3VDJlzDn0ctJvRgYbC2NvXHt+M=
golang.org/x/crypto v0.25.0 h1:ypSNr+bnYL2YhwoMt2zPxHFmbAN1KZs/njMG3hxUp30=
golang.org/x/crypto v0.25.0/go.mod h1:T+wALwcMOSE0kXgUAnPAHqTLW+XHgcELELW8VaDgm/M=
golang.org/x/exp v0.0.0-20230905200255-921286631fa9 h1:GoHiUyI/Tp2nVkLI2mCxVkOjsbSXD66ic0XW0js0R9g=
golang.org/x/exp v0.0.0-20230905200255-921286631fa9/go.mod h1:S2oDrQGGwySpoQPVqRShND87VCbxmc6bL1Yd2oYrm6k=
golang.org/x/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.24.0 h1:1PcaxkF854Fu3+lvBIx5SYn9wRlBzzcnHZSiaFFAb0w=
golang.org/x/net v0.24.0/go.mod h1:2Q7sJY5mzlzWjKtYUEXSlBWCdyaioyXzRB2RtU8KVE8=
golang.org/x/oauth2 v0.19.0 h1:9+E/EZBCbTLNrbN35fHv/a/d/mOBatymz1zbtQrXpIg=
golang.org/x/oauth2 v0.19.0/go.mod h1:vYi7skDa1x015PmRRYZ7+s1cWyPgrPiSYRe4rnsexc8=
golang.org/x/net v0.27.0 h1:5K3Njcw06/l2y9vpGCSdcxWOYHOUk3dVNGDXN+FvAys=
golang.org/x/net v0.27.0/go.mod h1:dDi0PyhWNoiUOrAS8uXv/vnScO4wnHQO4mj9fn/RytE=
golang.org/x/oauth2 v0.21.0 h1:tsimM75w1tF/uws5rbeHzIWxEqElMehnc+iW793zsZs=
golang.org/x/oauth2 v0.21.0/go.mod h1:XYTD2NtWslqkgxebSiOHnXEap4TF09sJSc7H1sXbhtI=
golang.org/x/sync v0.7.0 h1:YsImfSBoP9QPYL0xyKJPq0gcaJdG3rInoqxTWbfQu9M=
golang.org/x/sync v0.7.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk=
golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
@@ -199,19 +218,21 @@ golang.org/x/sys v0.0.0-20210630005230-0f9fa26af87c/go.mod h1:oPkhp1MJrh7nUepCBc
golang.org/x/sys v0.0.0-20210806184541-e5e7981a1069/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.0.0-20220704084225-05e143d24a9e/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.20.0 h1:Od9JTbYCk261bKm4M/mw7AklTlFYIa0bIp9BgSm1S8Y=
golang.org/x/sys v0.20.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
golang.org/x/sys v0.22.0 h1:RI27ohtqKCnwULzJLqkv897zojh5/DwS/ENaMzUOaWI=
golang.org/x/sys v0.22.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo=
golang.org/x/term v0.19.0 h1:+ThwsDv+tYfnJFhF4L8jITxu1tdTWRTZpdsWgEgjL6Q=
golang.org/x/term v0.22.0 h1:BbsgPEJULsl2fV/AT3v15Mjva5yXKQDyKf+TbDz7QJk=
golang.org/x/term v0.22.0/go.mod h1:F3qCibpT5AMpCRfhfT53vVJwhLtIVHhB9XDjfFvnMI4=
golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
golang.org/x/text v0.3.6/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
golang.org/x/text v0.14.0 h1:ScX5w1eTa3QqT8oi6+ziP7dTV1S2+ALU0bI+0zXKWiQ=
golang.org/x/text v0.14.0/go.mod h1:18ZOQIKpY8NJVqYksKHtTdi31H5itFRjB5/qKTNYzSU=
golang.org/x/text v0.16.0 h1:a94ExnEXNtEwYLGJSIUxnWoxoRz/ZcCsV63ROupILh4=
golang.org/x/text v0.16.0/go.mod h1:GhwF1Be+LQoKShO3cGOHzqOgRrGaYc9AvblQOmPVHnI=
golang.org/x/time v0.5.0 h1:o7cqy6amK/52YcAKIPlM3a+Fpj35zvRj2TP+e1xFSfk=
golang.org/x/time v0.5.0/go.mod h1:3BpzKBy/shNhVucY/MWOyx10tF3SFh9QdLuxbVysPQM=
golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
golang.org/x/tools v0.20.0 h1:hz/CVckiOxybQvFw6h7b/q80NTr9IUQb4s1IIzW7KNY=
golang.org/x/tools v0.21.1-0.20240508182429-e35e4ccd0d2d h1:vU5i/LfpvrRCpgM/VPfJLg5KjxD3E+hfT1SH+d9zLwg=
golang.org/x/tools v0.21.1-0.20240508182429-e35e4ccd0d2d/go.mod h1:aiJjzUbINMkxbQROHiO6hDPo2LHcIPhhQsa9DLh0yGk=
golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
google.golang.org/genproto/googleapis/rpc v0.0.0-20240227224415-6ceb2ff114de h1:cZGRis4/ot9uVm639a+rHCUaG0JJHEsdyzSQTMX+suY=
google.golang.org/genproto/googleapis/rpc v0.0.0-20240227224415-6ceb2ff114de/go.mod h1:H4O17MA/PE9BsGx3w+a+W2VOLLD1Qf7oJneAoU6WktY=
@@ -219,8 +240,8 @@ google.golang.org/grpc v1.63.0 h1:WjKe+dnvABXyPJMD7KDNLxtoGk5tgk+YFWN6cBWjZE8=
google.golang.org/grpc v1.63.0/go.mod h1:WAX/8DgncnokcFUldAxq7GeB5DXHDbMF+lLvDomNkRA=
google.golang.org/protobuf v1.26.0-rc.1/go.mod h1:jlhhOSvTdKEhbULTjvd4ARK9grFBp09yW+WbY/TyQbw=
google.golang.org/protobuf v1.28.0/go.mod h1:HV8QOd/L58Z+nl8r43ehVNZIU/HEI6OcFqwMG9pJV4I=
google.golang.org/protobuf v1.33.0 h1:uNO2rsAINq/JlFpSdYEKIZ0uKD/R9cpdv0T+yoGwGmI=
google.golang.org/protobuf v1.33.0/go.mod h1:c6P6GXX6sHbq/GpV6MGZEdwhWPcYBgnhAHhKbcUYpos=
google.golang.org/protobuf v1.34.2 h1:6xV6lTsCfpGD21XK49h7MhtcApnLqkfYgPcdHftf6hg=
google.golang.org/protobuf v1.34.2/go.mod h1:qYOHts0dSfpeUzUFpOMr/WGzszTmLH+DiWniOlNbLDw=
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c h1:Hei/4ADfdWqJk1ZMxUNpqntNwaWcugrBjAiHlqqRiVk=
+28 -5
View File
@@ -38,6 +38,7 @@ const (
ConfigTypeJihulab = "jihulab"
ConfigTypeGitea = "gitea"
ConfigTypeCloudflare = "cloudflare"
ConfigTypeOidc = "oidc"
)
const (
@@ -87,11 +88,21 @@ type Config struct {
ViewPassword string // 前台查看密码
}
Oauth2 struct {
Type string
Admin string // 管理员用户名列表
ClientID string
ClientSecret string
Endpoint string
Type string
Admin string // 管理员用户名列表
AdminGroups string // 管理员用户组列表
ClientID string
ClientSecret string
Endpoint string
OidcDisplayName string // for OIDC Display Name
OidcIssuer string // for OIDC Issuer
OidcLogoutURL string // for OIDC Logout URL
OidcRegisterURL string // for OIDC Register URL
OidcLoginClaim string // for OIDC Claim
OidcGroupClaim string // for OIDC Group Claim
OidcScopes string // for OIDC Scopes
OidcAutoCreate bool // for OIDC Auto Create
OidcAutoLogin bool // for OIDC Auto Login
}
HTTPPort uint
GRPCPort uint
@@ -181,6 +192,18 @@ func (c *Config) Read(path string) error {
if c.DDNS.MaxRetries == 0 {
c.DDNS.MaxRetries = 3
}
if c.Oauth2.OidcScopes == "" {
c.Oauth2.OidcScopes = "openid,profile,email"
}
if c.Oauth2.OidcLoginClaim == "" {
c.Oauth2.OidcLoginClaim = "sub"
}
if c.Oauth2.OidcDisplayName == "" {
c.Oauth2.OidcDisplayName = "OIDC"
}
if c.Oauth2.OidcGroupClaim == "" {
c.Oauth2.OidcGroupClaim = "groups"
}
c.updateIgnoredIPNotificationID()
return nil
+9 -9
View File
@@ -20,17 +20,17 @@ const (
TaskTypeTerminal
TaskTypeUpgrade
TaskTypeKeepalive
TaskTypeTerminalGRPC
TaskTypeNAT
)
type TerminalTask struct {
// websocket 主机名
Host string `json:"host,omitempty"`
// 是否启用 SSL
UseSSL bool `json:"use_ssl,omitempty"`
// 会话标识
Session string `json:"session,omitempty"`
// Agent在连接Server时需要的额外Cookie信息
Cookie string `json:"cookie,omitempty"`
StreamID string
}
type TaskNAT struct {
StreamID string
Host string
}
const (
@@ -120,7 +120,7 @@ func (m *Monitor) AfterFind(tx *gorm.DB) error {
// IsServiceSentinelNeeded 判断该任务类型是否需要进行服务监控 需要则返回true
func IsServiceSentinelNeeded(t uint64) bool {
return t != TaskTypeCommand && t != TaskTypeTerminal && t != TaskTypeUpgrade
return t != TaskTypeCommand && t != TaskTypeTerminalGRPC && t != TaskTypeUpgrade
}
func (m *Monitor) InitSkipServers() error {
+9
View File
@@ -0,0 +1,9 @@
package model
type NAT struct {
Common
Name string
ServerID uint64
Host string
Domain string `gorm:"unique"`
}
+28 -3
View File
@@ -42,6 +42,19 @@ func percentage(used, total uint64) float64 {
return float64(used) * 100 / float64(total)
}
func maxSliceValue(slice []float64) float64 {
if len(slice) != 0 {
max := slice[0]
for _, val := range slice {
if max < val {
max = val
}
}
return max
}
return 0
}
// Snapshot 未通过规则返回 struct{}{}, 通过返回 nil
func (u *Rule) Snapshot(cycleTransferStats *CycleTransferStats, server *Server, db *gorm.DB) interface{} {
// 监控全部但是排除了此服务器
@@ -63,6 +76,8 @@ func (u *Rule) Snapshot(cycleTransferStats *CycleTransferStats, server *Server,
switch u.Type {
case "cpu":
src = float64(server.State.CPU)
case "gpu":
src = float64(server.State.GPU)
case "memory":
src = percentage(server.State.MemUsed, server.Host.MemTotal)
case "swap":
@@ -91,21 +106,21 @@ func (u *Rule) Snapshot(cycleTransferStats *CycleTransferStats, server *Server,
src = float64(server.State.NetInTransfer - uint64(server.PrevHourlyTransferIn))
if u.CycleInterval != 0 {
var res NResult
db.Model(&Transfer{}).Select("SUM(`in`) AS n").Where("created_at > ? AND server_id = ?", u.GetTransferDurationStart(), server.ID).Scan(&res)
db.Model(&Transfer{}).Select("SUM(`in`) AS n").Where("datetime(`created_at`) >= datetime(?) AND server_id = ?", u.GetTransferDurationStart().UTC(), server.ID).Scan(&res)
src += float64(res.N)
}
case "transfer_out_cycle":
src = float64(server.State.NetOutTransfer - uint64(server.PrevHourlyTransferOut))
if u.CycleInterval != 0 {
var res NResult
db.Model(&Transfer{}).Select("SUM(`out`) AS n").Where("created_at > ? AND server_id = ?", u.GetTransferDurationStart(), server.ID).Scan(&res)
db.Model(&Transfer{}).Select("SUM(`out`) AS n").Where("datetime(`created_at`) >= datetime(?) AND server_id = ?", u.GetTransferDurationStart().UTC(), server.ID).Scan(&res)
src += float64(res.N)
}
case "transfer_all_cycle":
src = float64(server.State.NetOutTransfer - uint64(server.PrevHourlyTransferOut) + server.State.NetInTransfer - uint64(server.PrevHourlyTransferIn))
if u.CycleInterval != 0 {
var res NResult
db.Model(&Transfer{}).Select("SUM(`in`+`out`) AS n").Where("created_at > ? AND server_id = ?", u.GetTransferDurationStart(), server.ID).Scan(&res)
db.Model(&Transfer{}).Select("SUM(`in`+`out`) AS n").Where("datetime(`created_at`) >= datetime(?) AND server_id = ?", u.GetTransferDurationStart().UTC(), server.ID).Scan(&res)
src += float64(res.N)
}
case "load1":
@@ -120,6 +135,16 @@ func (u *Rule) Snapshot(cycleTransferStats *CycleTransferStats, server *Server,
src = float64(server.State.UdpConnCount)
case "process_count":
src = float64(server.State.ProcessCount)
case "temperature_max":
var temp []float64
if server.State.Temperatures != nil {
for _, tempStat := range server.State.Temperatures {
if tempStat.Temperature != 0 {
temp = append(temp, tempStat.Temperature)
}
}
src = maxSliceValue(temp)
}
}
// 循环区间流量检测 · 更新下次需要检测时间
+7 -7
View File
@@ -13,7 +13,7 @@ type ProviderCloudflare struct {
Secret string
}
func (provider ProviderCloudflare) UpdateDomain(domainConfig *DomainConfig) bool {
func (provider *ProviderCloudflare) UpdateDomain(domainConfig *DomainConfig) bool {
if domainConfig == nil {
return false
}
@@ -42,7 +42,7 @@ func (provider ProviderCloudflare) UpdateDomain(domainConfig *DomainConfig) bool
return resultV4 && resultV6
}
func (provider ProviderCloudflare) addDomainRecord(zoneID string, domainConfig *DomainConfig, isIpv4 bool) bool {
func (provider *ProviderCloudflare) addDomainRecord(zoneID string, domainConfig *DomainConfig, isIpv4 bool) bool {
record, err := provider.findDNSRecord(zoneID, domainConfig.FullDomain, isIpv4)
if err != nil {
log.Printf("查找 DNS 记录时出错: %s\n", err)
@@ -58,7 +58,7 @@ func (provider ProviderCloudflare) addDomainRecord(zoneID string, domainConfig *
}
}
func (provider ProviderCloudflare) getZoneID(domain string) (string, error) {
func (provider *ProviderCloudflare) getZoneID(domain string) (string, error) {
_, realDomain := SplitDomain(domain)
url := fmt.Sprintf("https://api.cloudflare.com/client/v4/zones?name=%s", realDomain)
body, err := provider.sendRequest("GET", url, nil)
@@ -81,7 +81,7 @@ func (provider ProviderCloudflare) getZoneID(domain string) (string, error) {
return "", fmt.Errorf("找不到 Zone ID")
}
func (provider ProviderCloudflare) findDNSRecord(zoneID string, domain string, isIPv4 bool) (map[string]interface{}, error) {
func (provider *ProviderCloudflare) findDNSRecord(zoneID string, domain string, isIPv4 bool) (map[string]interface{}, error) {
var ipType = "A"
if !isIPv4 {
ipType = "AAAA"
@@ -106,7 +106,7 @@ func (provider ProviderCloudflare) findDNSRecord(zoneID string, domain string, i
return nil, nil // 没有找到 DNS 记录
}
func (provider ProviderCloudflare) createDNSRecord(zoneID string, domainConfig *DomainConfig, isIPv4 bool) bool {
func (provider *ProviderCloudflare) createDNSRecord(zoneID string, domainConfig *DomainConfig, isIPv4 bool) bool {
var ipType = "A"
var ipAddr = domainConfig.Ipv4Addr
if !isIPv4 {
@@ -126,7 +126,7 @@ func (provider ProviderCloudflare) createDNSRecord(zoneID string, domainConfig *
return err == nil
}
func (provider ProviderCloudflare) updateDNSRecord(zoneID string, recordID string, domainConfig *DomainConfig, isIPv4 bool) bool {
func (provider *ProviderCloudflare) updateDNSRecord(zoneID string, recordID string, domainConfig *DomainConfig, isIPv4 bool) bool {
var ipType = "A"
var ipAddr = domainConfig.Ipv4Addr
if !isIPv4 {
@@ -147,7 +147,7 @@ func (provider ProviderCloudflare) updateDNSRecord(zoneID string, recordID strin
}
// 以下为辅助方法,如发送 HTTP 请求等
func (provider ProviderCloudflare) sendRequest(method string, url string, data []byte) ([]byte, error) {
func (provider *ProviderCloudflare) sendRequest(method string, url string, data []byte) ([]byte, error) {
client := &http.Client{}
req, err := http.NewRequest(method, url, bytes.NewBuffer(data))
if err != nil {
+1 -1
View File
@@ -2,6 +2,6 @@ package ddns
type ProviderDummy struct{}
func (provider ProviderDummy) UpdateDomain(domainConfig *DomainConfig) bool {
func (provider *ProviderDummy) UpdateDomain(domainConfig *DomainConfig) bool {
return false
}
+10 -10
View File
@@ -23,7 +23,7 @@ type ProviderTencentCloud struct {
SecretKey string
}
func (provider ProviderTencentCloud) UpdateDomain(domainConfig *DomainConfig) bool {
func (provider *ProviderTencentCloud) UpdateDomain(domainConfig *DomainConfig) bool {
if domainConfig == nil {
return false
}
@@ -46,7 +46,7 @@ func (provider ProviderTencentCloud) UpdateDomain(domainConfig *DomainConfig) bo
return resultV4 && resultV6
}
func (provider ProviderTencentCloud) addDomainRecord(domainConfig *DomainConfig, isIpv4 bool) bool {
func (provider *ProviderTencentCloud) addDomainRecord(domainConfig *DomainConfig, isIpv4 bool) bool {
record, err := provider.findDNSRecord(domainConfig.FullDomain, isIpv4)
if err != nil {
log.Printf("查找 DNS 记录时出错: %s\n", err)
@@ -66,7 +66,7 @@ func (provider ProviderTencentCloud) addDomainRecord(domainConfig *DomainConfig,
return provider.updateDNSRecord(domainConfig.FullDomain, record["RecordList"].([]interface{})[0].(map[string]interface{})["RecordId"].(float64), domainConfig, isIpv4)
}
func (provider ProviderTencentCloud) findDNSRecord(domain string, isIPv4 bool) (map[string]interface{}, error) {
func (provider *ProviderTencentCloud) findDNSRecord(domain string, isIPv4 bool) (map[string]interface{}, error) {
var ipType = "A"
if !isIPv4 {
ipType = "AAAA"
@@ -95,7 +95,7 @@ func (provider ProviderTencentCloud) findDNSRecord(domain string, isIPv4 bool) (
return result, nil
}
func (provider ProviderTencentCloud) createDNSRecord(domain string, domainConfig *DomainConfig, isIPv4 bool) bool {
func (provider *ProviderTencentCloud) createDNSRecord(domain string, domainConfig *DomainConfig, isIPv4 bool) bool {
var ipType = "A"
var ipAddr = domainConfig.Ipv4Addr
if !isIPv4 {
@@ -117,7 +117,7 @@ func (provider ProviderTencentCloud) createDNSRecord(domain string, domainConfig
return err == nil
}
func (provider ProviderTencentCloud) updateDNSRecord(domain string, recordID float64, domainConfig *DomainConfig, isIPv4 bool) bool {
func (provider *ProviderTencentCloud) updateDNSRecord(domain string, recordID float64, domainConfig *DomainConfig, isIPv4 bool) bool {
var ipType = "A"
var ipAddr = domainConfig.Ipv4Addr
if !isIPv4 {
@@ -141,7 +141,7 @@ func (provider ProviderTencentCloud) updateDNSRecord(domain string, recordID flo
}
// 以下为辅助方法,如发送 HTTP 请求等
func (provider ProviderTencentCloud) sendRequest(action string, data []byte) ([]byte, error) {
func (provider *ProviderTencentCloud) sendRequest(action string, data []byte) ([]byte, error) {
client := &http.Client{}
req, err := http.NewRequest("POST", url, bytes.NewBuffer(data))
if err != nil {
@@ -173,18 +173,18 @@ func (provider ProviderTencentCloud) sendRequest(action string, data []byte) ([]
// https://github.com/jeessy2/ddns-go/blob/master/util/tencent_cloud_signer.go
func (provider ProviderTencentCloud) sha256hex(s string) string {
func (provider *ProviderTencentCloud) sha256hex(s string) string {
b := sha256.Sum256([]byte(s))
return hex.EncodeToString(b[:])
}
func (provider ProviderTencentCloud) hmacsha256(s, key string) string {
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 {
func (provider *ProviderTencentCloud) WriteString(strs ...string) string {
var b strings.Builder
for _, str := range strs {
b.WriteString(str)
@@ -193,7 +193,7 @@ func (provider ProviderTencentCloud) WriteString(strs ...string) string {
return b.String()
}
func (provider ProviderTencentCloud) signRequest(secretId string, secretKey string, r *http.Request, action string, payload 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")
+1 -1
View File
@@ -14,7 +14,7 @@ type ProviderWebHook struct {
RequestHeader string
}
func (provider ProviderWebHook) UpdateDomain(domainConfig *DomainConfig) bool {
func (provider *ProviderWebHook) UpdateDomain(domainConfig *DomainConfig) bool {
if domainConfig == nil {
return false
}
+1
View File
@@ -0,0 +1 @@
stub
+54
View File
@@ -0,0 +1,54 @@
package geoip
import (
"embed"
"fmt"
"log"
"net"
"strings"
maxminddb "github.com/oschwald/maxminddb-golang"
)
//go:embed geoip.db
var geoDBFS embed.FS
var (
dbData []byte
err error
)
type IPInfo struct {
Country string `maxminddb:"country"`
CountryName string `maxminddb:"country_name"`
Continent string `maxminddb:"continent"`
ContinentName string `maxminddb:"continent_name"`
}
func init() {
dbData, err = geoDBFS.ReadFile("geoip.db")
if err != nil {
log.Printf("NEZHA>> Failed to open geoip database: %v", err)
}
}
func Lookup(ip net.IP, record *IPInfo) (string, error) {
db, err := maxminddb.FromBytes(dbData)
if err != nil {
return "", err
}
defer db.Close()
err = db.Lookup(ip, record)
if err != nil {
return "", err
}
if record.Country != "" {
return strings.ToLower(record.Country), nil
} else if record.Continent != "" {
return strings.ToLower(record.Continent), nil
}
return "", fmt.Errorf("IP not found")
}
+65
View File
@@ -0,0 +1,65 @@
package grpcx
import (
"context"
"io"
"sync/atomic"
"github.com/naiba/nezha/proto"
)
var _ io.ReadWriteCloser = &IOStreamWrapper{}
type IOStream interface {
Recv() (*proto.IOStreamData, error)
Send(*proto.IOStreamData) error
Context() context.Context
}
type IOStreamWrapper struct {
IOStream
dataBuf []byte
closed *atomic.Bool
closeCh chan struct{}
}
func NewIOStreamWrapper(stream IOStream) *IOStreamWrapper {
return &IOStreamWrapper{
IOStream: stream,
closeCh: make(chan struct{}),
closed: new(atomic.Bool),
}
}
func (iw *IOStreamWrapper) Read(p []byte) (n int, err error) {
if len(iw.dataBuf) > 0 {
n := copy(p, iw.dataBuf)
iw.dataBuf = iw.dataBuf[n:]
return n, nil
}
var data *proto.IOStreamData
if data, err = iw.Recv(); err != nil {
return 0, err
}
n = copy(p, data.Data)
if n < len(data.Data) {
iw.dataBuf = data.Data[n:]
}
return n, nil
}
func (iw *IOStreamWrapper) Write(p []byte) (n int, err error) {
err = iw.Send(&proto.IOStreamData{Data: p})
return len(p), err
}
func (iw *IOStreamWrapper) Close() error {
if iw.closed.CompareAndSwap(false, true) {
close(iw.closeCh)
}
return nil
}
func (iw *IOStreamWrapper) Wait() {
<-iw.closeCh
}
+1
View File
@@ -16,6 +16,7 @@ var adminPage = map[string]bool{
"/monitor": true,
"/setting": true,
"/notification": true,
"/nat": true,
"/cron": true,
"/api": true,
}
+56
View File
@@ -0,0 +1,56 @@
package general
import (
"github.com/naiba/nezha/model"
"github.com/naiba/nezha/service/singleton"
)
type UserInfo struct {
Sub string `json:"sub"`
Username string `json:"preferred_username"`
Email string `json:"email"`
Name string `json:"name"`
Groups []string `json:"groups,omitempty"`
Roles []string `json:"roles,omitempty"`
}
func (u UserInfo) MapToNezhaUser(loginClaim string, groupClaim string, adminGroups []string, autoCreate bool) model.User {
var user model.User
var login string
var groups []string
var isAdmin bool
if loginClaim == "email" {
login = u.Email
} else if loginClaim == "preferred_username" {
login = u.Username
} else {
login = u.Sub
}
if groupClaim == "roles" {
groups = u.Roles
} else {
groups = u.Groups
}
// Check if user is admin
adminGroupSet := make(map[string]struct{}, len(adminGroups))
for _, adminGroup := range adminGroups {
adminGroupSet[adminGroup] = struct{}{}
}
for _, group := range groups {
if _, found := adminGroupSet[group]; found {
isAdmin = true
break
}
}
result := singleton.DB.Where("login = ?", login).First(&user)
user.Login = login
user.Email = u.Email
user.Name = u.Name
user.SuperAdmin = isAdmin
if result.Error != nil && autoCreate {
singleton.DB.Create(&user)
} else if result.Error != nil {
return model.User{}
}
return user
}
+56
View File
@@ -0,0 +1,56 @@
package utils
import (
"bytes"
"io"
"net"
"net/http"
"github.com/gin-gonic/gin"
)
var _ io.ReadWriteCloser = &RequestWrapper{}
type RequestWrapper struct {
req *http.Request
reader *bytes.Buffer
writer net.Conn
}
func NewRequestWrapper(req *http.Request, writer gin.ResponseWriter) (*RequestWrapper, error) {
conn, _, err := writer.Hijack()
if err != nil {
return nil, err
}
buf := bytes.NewBuffer(nil)
if err = req.Write(buf); err != nil {
return nil, err
}
return &RequestWrapper{
req: req,
reader: buf,
writer: conn,
}, nil
}
func (rw *RequestWrapper) Read(p []byte) (int, error) {
count, err := rw.reader.Read(p)
if err == nil {
return count, nil
}
if err != io.EOF {
return count, err
}
// request 数据读完之后等待客户端断开连接或 grpc 超时
return rw.writer.Read(p)
}
func (rw *RequestWrapper) Write(p []byte) (int, error) {
return rw.writer.Write(p)
}
func (rw *RequestWrapper) Close() error {
rw.req.Body.Close()
rw.writer.Close()
return nil
}
+41 -10
View File
@@ -1,25 +1,56 @@
package websocketx
import (
"io"
"sync"
"github.com/gorilla/websocket"
"github.com/samber/lo"
)
var _ io.ReadWriteCloser = &Conn{}
type Conn struct {
*websocket.Conn
writeLock sync.Mutex
writeLock *sync.Mutex
dataBuf []byte
}
func (conn *Conn) WriteMessage(msgType int, data []byte) error {
func NewConn(conn *websocket.Conn) *Conn {
return &Conn{Conn: conn, writeLock: new(sync.Mutex)}
}
func (conn *Conn) Write(data []byte) (int, error) {
conn.writeLock.Lock()
defer conn.writeLock.Unlock()
var err error
lo.TryCatchWithErrorValue(func() error {
return conn.Conn.WriteMessage(msgType, data)
}, func(res any) {
err = res.(error)
})
return err
if err := conn.Conn.WriteMessage(websocket.BinaryMessage, data); err != nil {
return 0, err
}
return len(data), nil
}
func (conn *Conn) WriteMessage(messageType int, data []byte) error {
conn.writeLock.Lock()
defer conn.writeLock.Unlock()
return conn.Conn.WriteMessage(messageType, data)
}
func (conn *Conn) Read(data []byte) (int, error) {
if len(conn.dataBuf) > 0 {
n := copy(data, conn.dataBuf)
conn.dataBuf = conn.dataBuf[n:]
return n, nil
}
mType, innerData, err := conn.Conn.ReadMessage()
if err != nil {
return 0, err
}
// 将文本消息转换为命令输入
if mType == websocket.TextMessage {
innerData = append([]byte{0}, innerData...)
}
n := copy(data, innerData)
if n < len(innerData) {
conn.dataBuf = innerData[n:]
}
return n, nil
}
+156 -12
View File
@@ -35,7 +35,7 @@ type Host struct {
Virtualization string `protobuf:"bytes,8,opt,name=virtualization,proto3" json:"virtualization,omitempty"`
BootTime uint64 `protobuf:"varint,9,opt,name=boot_time,json=bootTime,proto3" json:"boot_time,omitempty"`
Ip string `protobuf:"bytes,10,opt,name=ip,proto3" json:"ip,omitempty"`
CountryCode string `protobuf:"bytes,11,opt,name=country_code,json=countryCode,proto3" json:"country_code,omitempty"`
CountryCode string `protobuf:"bytes,11,opt,name=country_code,json=countryCode,proto3" json:"country_code,omitempty"` // deprecated
Version string `protobuf:"bytes,12,opt,name=version,proto3" json:"version,omitempty"`
Gpu []string `protobuf:"bytes,13,rep,name=gpu,proto3" json:"gpu,omitempty"`
}
@@ -582,6 +582,108 @@ func (x *Receipt) GetProced() bool {
return false
}
type IOStreamData struct {
state protoimpl.MessageState
sizeCache protoimpl.SizeCache
unknownFields protoimpl.UnknownFields
Data []byte `protobuf:"bytes,1,opt,name=data,proto3" json:"data,omitempty"`
}
func (x *IOStreamData) Reset() {
*x = IOStreamData{}
if protoimpl.UnsafeEnabled {
mi := &file_proto_nezha_proto_msgTypes[6]
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
ms.StoreMessageInfo(mi)
}
}
func (x *IOStreamData) String() string {
return protoimpl.X.MessageStringOf(x)
}
func (*IOStreamData) ProtoMessage() {}
func (x *IOStreamData) ProtoReflect() protoreflect.Message {
mi := &file_proto_nezha_proto_msgTypes[6]
if protoimpl.UnsafeEnabled && x != nil {
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
if ms.LoadMessageInfo() == nil {
ms.StoreMessageInfo(mi)
}
return ms
}
return mi.MessageOf(x)
}
// Deprecated: Use IOStreamData.ProtoReflect.Descriptor instead.
func (*IOStreamData) Descriptor() ([]byte, []int) {
return file_proto_nezha_proto_rawDescGZIP(), []int{6}
}
func (x *IOStreamData) GetData() []byte {
if x != nil {
return x.Data
}
return nil
}
type GeoIP struct {
state protoimpl.MessageState
sizeCache protoimpl.SizeCache
unknownFields protoimpl.UnknownFields
Ip string `protobuf:"bytes,1,opt,name=ip,proto3" json:"ip,omitempty"`
CountryCode string `protobuf:"bytes,2,opt,name=country_code,json=countryCode,proto3" json:"country_code,omitempty"`
}
func (x *GeoIP) Reset() {
*x = GeoIP{}
if protoimpl.UnsafeEnabled {
mi := &file_proto_nezha_proto_msgTypes[7]
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
ms.StoreMessageInfo(mi)
}
}
func (x *GeoIP) String() string {
return protoimpl.X.MessageStringOf(x)
}
func (*GeoIP) ProtoMessage() {}
func (x *GeoIP) ProtoReflect() protoreflect.Message {
mi := &file_proto_nezha_proto_msgTypes[7]
if protoimpl.UnsafeEnabled && x != nil {
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
if ms.LoadMessageInfo() == nil {
ms.StoreMessageInfo(mi)
}
return ms
}
return mi.MessageOf(x)
}
// Deprecated: Use GeoIP.ProtoReflect.Descriptor instead.
func (*GeoIP) Descriptor() ([]byte, []int) {
return file_proto_nezha_proto_rawDescGZIP(), []int{7}
}
func (x *GeoIP) GetIp() string {
if x != nil {
return x.Ip
}
return ""
}
func (x *GeoIP) GetCountryCode() string {
if x != nil {
return x.CountryCode
}
return ""
}
var File_proto_nezha_proto protoreflect.FileDescriptor
var file_proto_nezha_proto_rawDesc = []byte{
@@ -663,7 +765,13 @@ var file_proto_nezha_proto_rawDesc = []byte{
0x65, 0x73, 0x73, 0x66, 0x75, 0x6c, 0x18, 0x05, 0x20, 0x01, 0x28, 0x08, 0x52, 0x0a, 0x73, 0x75,
0x63, 0x63, 0x65, 0x73, 0x73, 0x66, 0x75, 0x6c, 0x22, 0x21, 0x0a, 0x07, 0x52, 0x65, 0x63, 0x65,
0x69, 0x70, 0x74, 0x12, 0x16, 0x0a, 0x06, 0x70, 0x72, 0x6f, 0x63, 0x65, 0x64, 0x18, 0x01, 0x20,
0x01, 0x28, 0x08, 0x52, 0x06, 0x70, 0x72, 0x6f, 0x63, 0x65, 0x64, 0x32, 0xd6, 0x01, 0x0a, 0x0c,
0x01, 0x28, 0x08, 0x52, 0x06, 0x70, 0x72, 0x6f, 0x63, 0x65, 0x64, 0x22, 0x22, 0x0a, 0x0c, 0x49,
0x4f, 0x53, 0x74, 0x72, 0x65, 0x61, 0x6d, 0x44, 0x61, 0x74, 0x61, 0x12, 0x12, 0x0a, 0x04, 0x64,
0x61, 0x74, 0x61, 0x18, 0x01, 0x20, 0x01, 0x28, 0x0c, 0x52, 0x04, 0x64, 0x61, 0x74, 0x61, 0x22,
0x3a, 0x0a, 0x05, 0x47, 0x65, 0x6f, 0x49, 0x50, 0x12, 0x0e, 0x0a, 0x02, 0x69, 0x70, 0x18, 0x01,
0x20, 0x01, 0x28, 0x09, 0x52, 0x02, 0x69, 0x70, 0x12, 0x21, 0x0a, 0x0c, 0x63, 0x6f, 0x75, 0x6e,
0x74, 0x72, 0x79, 0x5f, 0x63, 0x6f, 0x64, 0x65, 0x18, 0x02, 0x20, 0x01, 0x28, 0x09, 0x52, 0x0b,
0x63, 0x6f, 0x75, 0x6e, 0x74, 0x72, 0x79, 0x43, 0x6f, 0x64, 0x65, 0x32, 0xbf, 0x02, 0x0a, 0x0c,
0x4e, 0x65, 0x7a, 0x68, 0x61, 0x53, 0x65, 0x72, 0x76, 0x69, 0x63, 0x65, 0x12, 0x33, 0x0a, 0x11,
0x52, 0x65, 0x70, 0x6f, 0x72, 0x74, 0x53, 0x79, 0x73, 0x74, 0x65, 0x6d, 0x53, 0x74, 0x61, 0x74,
0x65, 0x12, 0x0c, 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x2e, 0x53, 0x74, 0x61, 0x74, 0x65, 0x1a,
@@ -677,8 +785,14 @@ var file_proto_nezha_proto_rawDesc = []byte{
0x63, 0x65, 0x69, 0x70, 0x74, 0x22, 0x00, 0x12, 0x2b, 0x0a, 0x0b, 0x52, 0x65, 0x71, 0x75, 0x65,
0x73, 0x74, 0x54, 0x61, 0x73, 0x6b, 0x12, 0x0b, 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x2e, 0x48,
0x6f, 0x73, 0x74, 0x1a, 0x0b, 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x2e, 0x54, 0x61, 0x73, 0x6b,
0x22, 0x00, 0x30, 0x01, 0x42, 0x09, 0x5a, 0x07, 0x2e, 0x2f, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x62,
0x06, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x33,
0x22, 0x00, 0x30, 0x01, 0x12, 0x3a, 0x0a, 0x08, 0x49, 0x4f, 0x53, 0x74, 0x72, 0x65, 0x61, 0x6d,
0x12, 0x13, 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x2e, 0x49, 0x4f, 0x53, 0x74, 0x72, 0x65, 0x61,
0x6d, 0x44, 0x61, 0x74, 0x61, 0x1a, 0x13, 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x2e, 0x49, 0x4f,
0x53, 0x74, 0x72, 0x65, 0x61, 0x6d, 0x44, 0x61, 0x74, 0x61, 0x22, 0x00, 0x28, 0x01, 0x30, 0x01,
0x12, 0x2b, 0x0a, 0x0b, 0x4c, 0x6f, 0x6f, 0x6b, 0x75, 0x70, 0x47, 0x65, 0x6f, 0x49, 0x50, 0x12,
0x0c, 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x2e, 0x47, 0x65, 0x6f, 0x49, 0x50, 0x1a, 0x0c, 0x2e,
0x70, 0x72, 0x6f, 0x74, 0x6f, 0x2e, 0x47, 0x65, 0x6f, 0x49, 0x50, 0x22, 0x00, 0x42, 0x09, 0x5a,
0x07, 0x2e, 0x2f, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x62, 0x06, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x33,
}
var (
@@ -693,7 +807,7 @@ func file_proto_nezha_proto_rawDescGZIP() []byte {
return file_proto_nezha_proto_rawDescData
}
var file_proto_nezha_proto_msgTypes = make([]protoimpl.MessageInfo, 6)
var file_proto_nezha_proto_msgTypes = make([]protoimpl.MessageInfo, 8)
var file_proto_nezha_proto_goTypes = []interface{}{
(*Host)(nil), // 0: proto.Host
(*State)(nil), // 1: proto.State
@@ -701,6 +815,8 @@ var file_proto_nezha_proto_goTypes = []interface{}{
(*Task)(nil), // 3: proto.Task
(*TaskResult)(nil), // 4: proto.TaskResult
(*Receipt)(nil), // 5: proto.Receipt
(*IOStreamData)(nil), // 6: proto.IOStreamData
(*GeoIP)(nil), // 7: proto.GeoIP
}
var file_proto_nezha_proto_depIdxs = []int32{
2, // 0: proto.State.temperatures:type_name -> proto.State_SensorTemperature
@@ -708,12 +824,16 @@ var file_proto_nezha_proto_depIdxs = []int32{
0, // 2: proto.NezhaService.ReportSystemInfo:input_type -> proto.Host
4, // 3: proto.NezhaService.ReportTask:input_type -> proto.TaskResult
0, // 4: proto.NezhaService.RequestTask:input_type -> proto.Host
5, // 5: proto.NezhaService.ReportSystemState:output_type -> proto.Receipt
5, // 6: proto.NezhaService.ReportSystemInfo:output_type -> proto.Receipt
5, // 7: proto.NezhaService.ReportTask:output_type -> proto.Receipt
3, // 8: proto.NezhaService.RequestTask:output_type -> proto.Task
5, // [5:9] is the sub-list for method output_type
1, // [1:5] is the sub-list for method input_type
6, // 5: proto.NezhaService.IOStream:input_type -> proto.IOStreamData
7, // 6: proto.NezhaService.LookupGeoIP:input_type -> proto.GeoIP
5, // 7: proto.NezhaService.ReportSystemState:output_type -> proto.Receipt
5, // 8: proto.NezhaService.ReportSystemInfo:output_type -> proto.Receipt
5, // 9: proto.NezhaService.ReportTask:output_type -> proto.Receipt
3, // 10: proto.NezhaService.RequestTask:output_type -> proto.Task
6, // 11: proto.NezhaService.IOStream:output_type -> proto.IOStreamData
7, // 12: proto.NezhaService.LookupGeoIP:output_type -> proto.GeoIP
7, // [7:13] is the sub-list for method output_type
1, // [1:7] is the sub-list for method input_type
1, // [1:1] is the sub-list for extension type_name
1, // [1:1] is the sub-list for extension extendee
0, // [0:1] is the sub-list for field type_name
@@ -797,6 +917,30 @@ func file_proto_nezha_proto_init() {
return nil
}
}
file_proto_nezha_proto_msgTypes[6].Exporter = func(v interface{}, i int) interface{} {
switch v := v.(*IOStreamData); i {
case 0:
return &v.state
case 1:
return &v.sizeCache
case 2:
return &v.unknownFields
default:
return nil
}
}
file_proto_nezha_proto_msgTypes[7].Exporter = func(v interface{}, i int) interface{} {
switch v := v.(*GeoIP); i {
case 0:
return &v.state
case 1:
return &v.sizeCache
case 2:
return &v.unknownFields
default:
return nil
}
}
}
type x struct{}
out := protoimpl.TypeBuilder{
@@ -804,7 +948,7 @@ func file_proto_nezha_proto_init() {
GoPackagePath: reflect.TypeOf(x{}).PkgPath(),
RawDescriptor: file_proto_nezha_proto_rawDesc,
NumEnums: 0,
NumMessages: 6,
NumMessages: 8,
NumExtensions: 0,
NumServices: 1,
},
+12 -1
View File
@@ -8,6 +8,8 @@ service NezhaService {
rpc ReportSystemInfo(Host)returns(Receipt){}
rpc ReportTask(TaskResult)returns(Receipt){}
rpc RequestTask(Host)returns(stream Task){}
rpc IOStream(stream IOStreamData)returns(stream IOStreamData){}
rpc LookupGeoIP(GeoIP)returns(GeoIP){}
}
message Host {
@@ -21,7 +23,7 @@ message Host {
string virtualization = 8;
uint64 boot_time = 9;
string ip = 10;
string country_code = 11;
string country_code = 11; // deprecated
string version = 12;
repeated string gpu = 13;
}
@@ -68,3 +70,12 @@ message TaskResult {
message Receipt{
bool proced = 1;
}
message IOStreamData {
bytes data = 1;
}
message GeoIP {
string ip = 1;
string country_code = 2;
}
+107 -1
View File
@@ -1,7 +1,7 @@
// Code generated by protoc-gen-go-grpc. DO NOT EDIT.
// versions:
// - protoc-gen-go-grpc v1.3.0
// - protoc v3.21.12
// - protoc v5.26.1
// source: proto/nezha.proto
package proto
@@ -23,6 +23,8 @@ const (
NezhaService_ReportSystemInfo_FullMethodName = "/proto.NezhaService/ReportSystemInfo"
NezhaService_ReportTask_FullMethodName = "/proto.NezhaService/ReportTask"
NezhaService_RequestTask_FullMethodName = "/proto.NezhaService/RequestTask"
NezhaService_IOStream_FullMethodName = "/proto.NezhaService/IOStream"
NezhaService_LookupGeoIP_FullMethodName = "/proto.NezhaService/LookupGeoIP"
)
// NezhaServiceClient is the client API for NezhaService service.
@@ -33,6 +35,8 @@ type NezhaServiceClient interface {
ReportSystemInfo(ctx context.Context, in *Host, opts ...grpc.CallOption) (*Receipt, error)
ReportTask(ctx context.Context, in *TaskResult, opts ...grpc.CallOption) (*Receipt, error)
RequestTask(ctx context.Context, in *Host, opts ...grpc.CallOption) (NezhaService_RequestTaskClient, error)
IOStream(ctx context.Context, opts ...grpc.CallOption) (NezhaService_IOStreamClient, error)
LookupGeoIP(ctx context.Context, in *GeoIP, opts ...grpc.CallOption) (*GeoIP, error)
}
type nezhaServiceClient struct {
@@ -102,6 +106,46 @@ func (x *nezhaServiceRequestTaskClient) Recv() (*Task, error) {
return m, nil
}
func (c *nezhaServiceClient) IOStream(ctx context.Context, opts ...grpc.CallOption) (NezhaService_IOStreamClient, error) {
stream, err := c.cc.NewStream(ctx, &NezhaService_ServiceDesc.Streams[1], NezhaService_IOStream_FullMethodName, opts...)
if err != nil {
return nil, err
}
x := &nezhaServiceIOStreamClient{stream}
return x, nil
}
type NezhaService_IOStreamClient interface {
Send(*IOStreamData) error
Recv() (*IOStreamData, error)
grpc.ClientStream
}
type nezhaServiceIOStreamClient struct {
grpc.ClientStream
}
func (x *nezhaServiceIOStreamClient) Send(m *IOStreamData) error {
return x.ClientStream.SendMsg(m)
}
func (x *nezhaServiceIOStreamClient) Recv() (*IOStreamData, error) {
m := new(IOStreamData)
if err := x.ClientStream.RecvMsg(m); err != nil {
return nil, err
}
return m, nil
}
func (c *nezhaServiceClient) LookupGeoIP(ctx context.Context, in *GeoIP, opts ...grpc.CallOption) (*GeoIP, error) {
out := new(GeoIP)
err := c.cc.Invoke(ctx, NezhaService_LookupGeoIP_FullMethodName, in, out, opts...)
if err != nil {
return nil, err
}
return out, nil
}
// NezhaServiceServer is the server API for NezhaService service.
// All implementations should embed UnimplementedNezhaServiceServer
// for forward compatibility
@@ -110,6 +154,8 @@ type NezhaServiceServer interface {
ReportSystemInfo(context.Context, *Host) (*Receipt, error)
ReportTask(context.Context, *TaskResult) (*Receipt, error)
RequestTask(*Host, NezhaService_RequestTaskServer) error
IOStream(NezhaService_IOStreamServer) error
LookupGeoIP(context.Context, *GeoIP) (*GeoIP, error)
}
// UnimplementedNezhaServiceServer should be embedded to have forward compatible implementations.
@@ -128,6 +174,12 @@ func (UnimplementedNezhaServiceServer) ReportTask(context.Context, *TaskResult)
func (UnimplementedNezhaServiceServer) RequestTask(*Host, NezhaService_RequestTaskServer) error {
return status.Errorf(codes.Unimplemented, "method RequestTask not implemented")
}
func (UnimplementedNezhaServiceServer) IOStream(NezhaService_IOStreamServer) error {
return status.Errorf(codes.Unimplemented, "method IOStream not implemented")
}
func (UnimplementedNezhaServiceServer) LookupGeoIP(context.Context, *GeoIP) (*GeoIP, error) {
return nil, status.Errorf(codes.Unimplemented, "method LookupGeoIP not implemented")
}
// UnsafeNezhaServiceServer may be embedded to opt out of forward compatibility for this service.
// Use of this interface is not recommended, as added methods to NezhaServiceServer will
@@ -215,6 +267,50 @@ func (x *nezhaServiceRequestTaskServer) Send(m *Task) error {
return x.ServerStream.SendMsg(m)
}
func _NezhaService_IOStream_Handler(srv interface{}, stream grpc.ServerStream) error {
return srv.(NezhaServiceServer).IOStream(&nezhaServiceIOStreamServer{stream})
}
type NezhaService_IOStreamServer interface {
Send(*IOStreamData) error
Recv() (*IOStreamData, error)
grpc.ServerStream
}
type nezhaServiceIOStreamServer struct {
grpc.ServerStream
}
func (x *nezhaServiceIOStreamServer) Send(m *IOStreamData) error {
return x.ServerStream.SendMsg(m)
}
func (x *nezhaServiceIOStreamServer) Recv() (*IOStreamData, error) {
m := new(IOStreamData)
if err := x.ServerStream.RecvMsg(m); err != nil {
return nil, err
}
return m, nil
}
func _NezhaService_LookupGeoIP_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) {
in := new(GeoIP)
if err := dec(in); err != nil {
return nil, err
}
if interceptor == nil {
return srv.(NezhaServiceServer).LookupGeoIP(ctx, in)
}
info := &grpc.UnaryServerInfo{
Server: srv,
FullMethod: NezhaService_LookupGeoIP_FullMethodName,
}
handler := func(ctx context.Context, req interface{}) (interface{}, error) {
return srv.(NezhaServiceServer).LookupGeoIP(ctx, req.(*GeoIP))
}
return interceptor(ctx, in, info, handler)
}
// NezhaService_ServiceDesc is the grpc.ServiceDesc for NezhaService service.
// It's only intended for direct use with grpc.RegisterService,
// and not to be introspected or modified (even as a copy)
@@ -234,6 +330,10 @@ var NezhaService_ServiceDesc = grpc.ServiceDesc{
MethodName: "ReportTask",
Handler: _NezhaService_ReportTask_Handler,
},
{
MethodName: "LookupGeoIP",
Handler: _NezhaService_LookupGeoIP_Handler,
},
},
Streams: []grpc.StreamDesc{
{
@@ -241,6 +341,12 @@ var NezhaService_ServiceDesc = grpc.ServiceDesc{
Handler: _NezhaService_RequestTask_Handler,
ServerStreams: true,
},
{
StreamName: "IOStream",
Handler: _NezhaService_IOStream_Handler,
ServerStreams: true,
ClientStreams: true,
},
},
Metadata: "proto/nezha.proto",
}
+3
View File
@@ -648,3 +648,6 @@ other = "Disable Switch Template in Frontend"
[ServersOnWorldMap]
other = "Servers On World Map"
[NAT]
other = "NAT"
+4 -1
View File
@@ -647,4 +647,7 @@ other = "Temperatura"
other = "Deshabilitar Cambio de Plantilla en Frontend"
[ServersOnWorldMap]
other = "Servidores en el mapa mundial"
other = "Servidores en el mapa mundial"
[NAT]
other = "NAT"
+3
View File
@@ -648,3 +648,6 @@ other = "禁止前台切换模板"
[ServersOnWorldMap]
other = "服务器世界分布图"
[NAT]
other = "内网穿透"
+3
View File
@@ -648,3 +648,6 @@ other = "禁止前台切換主題"
[ServersOnWorldMap]
other = "伺服器世界分布圖"
[NAT]
other = "NAT"
+23
View File
@@ -91,6 +91,7 @@ function showFormModal(modelSelector, formID, URL, getData) {
item.name.endsWith("_id") ||
item.name === "id" ||
item.name === "ID" ||
item.name === "ServerID" ||
item.name === "RequestType" ||
item.name === "RequestMethod" ||
item.name === "TriggerMode" ||
@@ -255,6 +256,28 @@ function addOrEditNotification(notification) {
);
}
function addOrEditNAT(nat) {
const modal = $(".nat.modal");
modal.children(".header").text((nat ? LANG.Edit : LANG.Add));
modal
.find(".nezha-primary-btn.button")
.html(
nat
? LANG.Edit + '<i class="edit icon"></i>'
: LANG.Add + '<i class="add icon"></i>'
);
modal.find("input[name=ID]").val(nat ? nat.ID : null);
modal.find("input[name=ServerID]").val(nat ? nat.ServerID : null);
modal.find("input[name=Name]").val(nat ? nat.Name : null);
modal.find("input[name=Host]").val(nat ? nat.Host : null);
modal.find("input[name=Domain]").val(nat ? nat.Domain : null);
showFormModal(
".nat.modal",
"#natForm",
"/api/nat"
);
}
function connectToServer(id) {
post('/terminal', { Host: window.location.host, Protocol: window.location.protocol, ID: id })
}
+74 -72
View File
@@ -1,5 +1,6 @@
body[theme="dark"] {
background: #31363b;
font-family: "Helvetica Neue",Helvetica,Arial,sans-serif;
background: rgba(49, 54, 59, 1);
color: #f1f1f1;
}
@@ -7,14 +8,10 @@ body[theme="dark"] .navbar .navbar-brand {
color: #ffffff;
}
body[theme="dark"] .navbar .navbar-brand {
color: #ffffff;
}
body[theme="dark"] .navbar .dropdown-menu {
list-style-image: initial;
background-color: #171a1e;
border-color: #31363b;
background-color: rgba(23, 26, 30, 1);
border-color: rgba(49, 54, 59, 1);
box-shadow: rgba(0, 0, 0, 0.18) 0px 6px 12px;
}
@@ -24,8 +21,8 @@ body[theme="dark"] .navbar .dropdown-menu > li > a {
body[theme="dark"] .navbar .dropdown-menu > li > a:focus,
body[theme="dark"] .navbar .dropdown-menu > li > a:hover {
background-color: #25282a;
background-image: linear-gradient(#1c1d26 0, #1c1d26 100%);
background-color: rgba(37, 40, 42, 1);
background-image: linear-gradient(rgba(28, 29, 38, 1) 0, rgba(28, 29, 38, 1) 100%);
}
body[theme="dark"] .navbar .navbar-nav > .open > a:focus,
@@ -37,18 +34,13 @@ body[theme="dark"] .navbar .navbar-nav > .open > a{
background-color: transparent !important;
}
body[theme="dark"] .navbar .navbar-nav .open .dropdown-menu>li>a {
body[theme="dark"] .navbar .navbar-nav .open .dropdown-menu > li > a {
color: #f1f1f1;
}
body[theme="dark"] .toolbox i{
color: rgba(241,241,241,1);
background-color: rgba(0,0,0,0.5);
}
body[theme="dark"] .content {
background-image: initial;
background-color: #1c1d26;
background-color: rgba(28, 29, 38, 1);
border: none;
box-shadow: rgba(0, 0, 0, 0.5) 0 0.625em 2em;
-webkit-box-shadow: rgba(0, 0, 0, 0.5) 0 0.625em 2em;
@@ -57,57 +49,10 @@ body[theme="dark"] .content {
body[theme="dark"] .table {
background-image: initial;
background-color: #1c1d26;
background-color: rgba(28, 29, 38, 1);
border-color: #545b5e;
}
body[theme="dark"] .table>thead>tr.node-group-tag>th {
background-color: #1c1d26;
}
body[theme="dark"] .table-striped tbody > tr.even > td,
body[theme="dark"] .table-striped tbody > tr.even > th {
background-color: #1c1d26;
}
body[theme="dark"] .table-striped tbody > tr.odd > td,
body[theme="dark"] .table-striped tbody > tr.odd > th {
background-color: #1c1d26;
}
body[theme="dark"] .progress {
background-image: linear-gradient(#2c2c2c 0,#1c1d26 100%);
background-color: #1c1d26;
}
body[theme="dark"] .progress-bar {
color: white;
}
body[theme="dark"] .progress-bar-success {
background-image: linear-gradient(#4d853a 0, #367e36 100%);
}
body[theme="dark"] .progress-offline .progress-bar-success {
background-image: linear-gradient(grey 0, grey 100%);
}
body[theme="dark"] .progress-bar-warning {
background-image: linear-gradient(#995f0d 0, #b5710f 100%);
}
body[theme="dark"] .progress-offline .progress-bar-warning {
background-image: linear-gradient(grey 0, grey 100%);
}
body[theme="dark"] .progress-bar-danger {
background-image: linear-gradient(#942320 0, #a12623 100%);
}
body[theme="dark"] .progress-offline .progress-bar-danger {
background-image: linear-gradient(grey 0, grey 100%);
}
body[theme="dark"] .table > tbody > tr > td:before,
body[theme="dark"] .table > tfoot > tr > td:before,
body[theme="dark"] .table > thead > tr > td:before,
@@ -115,17 +60,69 @@ body[theme="dark"] .table > thead > tr.node-group-cell > th:before{
background-color: rgba(58, 62, 65, 1);
}
body[theme="dark"] .table > tbody > tr.expandRow > td:before,
body[theme="dark"] .table > thead > tr.node-group-tag > th:before{
background-color: #1c1d26;
body[theme="dark"] .table-striped tbody > tr,
body[theme="dark"] .table-striped tbody > tr.even > td,
body[theme="dark"] .table-striped tbody > tr.even > th {
background-color: rgba(28, 29, 38, 1);
}
body[theme="dark"] .table-striped tbody > tr,
body[theme="dark"] .table-striped tbody > tr.odd > td,
body[theme="dark"] .table-striped tbody > tr.odd > th {
/* background-color: rgba(28, 29, 38, 1); 282B2E*/
background-color: rgba(34, 35, 46, 1);
}
body[theme="dark"] .table-hover > tbody > tr:hover{
background-color: unset;
}
body[theme="dark"] .table-hover > tbody > tr:not(.expandRow):hover > td {
background-color: #171a1e;
background-color: rgba(48, 50, 65, 1);
}
body[theme="dark"] .table-striped > tbody > tr:nth-of-type(2n+1) {
background-color: #1c1d26;
/* expandRow展开部分样式 */
body[theme="dark"] .table > tbody > tr.expandRow.odd > td:before{
/* background-color: rgba(28, 29, 38, 1); 282B2E*/
background-color: rgba(34, 35, 46, 1);
}
body[theme="dark"] .table > tbody > tr.expandRow.even > td:before{
background-color: rgba(28, 29, 38, 1);
}
/* expandRow展开部分样式结束 */
body[theme="dark"] .progress {
background-image: linear-gradient(#2c2c2c 0,rgba(28, 29, 38, 1) 100%);
background-color: rgba(28, 29, 38, 1);
}
body[theme="dark"] .progress-bar {
color: #ffffff;
}
body[theme="dark"] .progress-bar-success {
background-image: linear-gradient(#4d853a 0, #367e36 100%);
}
body[theme="dark"] .progress-offline .progress-bar-success {
background-image: linear-gradient(#808080 0, #808080 100%);
}
body[theme="dark"] .progress-bar-warning {
background-image: linear-gradient(#995f0d 0, #b5710f 100%);
}
body[theme="dark"] .progress-offline .progress-bar-warning {
background-image: linear-gradient(#808080 0, #808080 100%);
}
body[theme="dark"] .progress-bar-danger {
background-image: linear-gradient(#942320 0, #a12623 100%);
}
body[theme="dark"] .progress-offline .progress-bar-danger {
background-image: linear-gradient(#808080 0, #808080 100%);
}
body[theme="dark"] .node-cell.status .status-icon.online {
@@ -149,7 +146,7 @@ body[theme="dark"] .service-status .warning {
}
body[theme="dark"] .service-day-status-icon {
background-color: grey;
background-color: rgba(128, 128, 128, 1);
}
body[theme="dark"] footer p a,
@@ -159,7 +156,7 @@ body[theme="dark"] footer p a:hover {
}
body[theme="dark"] .modal-content{
background-color: #1c1d26;
background-color: rgba(28, 29, 38, 1);
color: #f1f1f1;
}
@@ -170,3 +167,8 @@ body[theme="dark"] .modal-header{
body[theme="dark"] .modal-header i{
color: #f1f1f1;
}
body[theme="dark"] .toolbox i{
color: rgba(241, 241, 241, 1);
background-color: rgba(0, 0, 0, 0.5);
}
+32 -1
View File
@@ -50,6 +50,11 @@ body {
/*margin-right: 5px;*/
}
.navbar .dropdown:hover,
.navbar .dropdown a{
cursor: pointer;
}
.navbar .dropdown .dropdown-toggle {
padding-bottom: 10px;
padding-top: 10px;
@@ -59,7 +64,7 @@ body {
margin:0px -15px;
}
.navbar .navbar-nav>li>a {
.navbar .navbar-nav > li > a {
color:#f1f1f1;
}
@@ -137,6 +142,10 @@ body {
text-align: left;
}
tr.accordion-toggle{
cursor:pointer;
}
.table > tbody > tr > td,
.table > tbody > tr > th,
.table > tfoot > tr > td,
@@ -234,6 +243,19 @@ body {
cursor: pointer;
}
/*加快Agent详情下拉展示速度*/
.collapsing{
-webkit-transition-property:height,visibility;
-o-transition-property:height,visibility;
transition-property:height,visibility;
-webkit-transition-duration:.15s;
-o-transition-duration:.15s;
transition-duration:.15s;
-webkit-transition-timing-function: linear;
-o-transition-timing-function: linear;
transition-timing-function: linear
}
/*正文结束*/
/* 服务页 正文*/
@@ -321,6 +343,15 @@ body {
right: 13px;
}
footer{
padding-bottom: 2rem;
}
footer p{
text-align: center;
font-size: 10px;
}
@media only screen and (max-width: 1200px) {
.accordian-body{
margin: 5px 0px 5px 10px;
+35 -50
View File
@@ -7,6 +7,7 @@ const mixinsVue = {
showGoTop: false,
preferredTemplate: null,
isMobile: false,
staticUrl: '/static/theme-server-status',
adaptedTemplates: [
{ key: 'default', name: 'Default', icon: 'th large' },
{ key: 'angel-kanade', name: 'AngelKanade', icon: 'square' },
@@ -15,8 +16,8 @@ const mixinsVue = {
},
created() {
this.isMobile = this.checkIsMobile();
this.initTheme();
this.storedShowGroup();
this.theme = this.initTheme();
this.showGroup = this.initShowGroup();
this.preferredTemplate = this.getCookie('preferred_theme') ? this.getCookie('preferred_theme') : this.$root.defaultTemplate;
window.addEventListener('scroll', this.handleScroll);
},
@@ -24,17 +25,6 @@ const mixinsVue = {
window.removeEventListener('scroll', this.handleScroll);
},
methods: {
toggleView() {
this.showGroup = !this.showGroup;
localStorage.setItem("showGroup", JSON.stringify(this.showGroup));
return this.showGroup;
},
storedShowGroup() {
const storedShowGroup = localStorage.getItem("showGroup");
if (storedShowGroup !== null) {
this.showGroup = JSON.parse(storedShowGroup);
}
},
toggleTemplate(template) {
if( template != this.preferredTemplate){
this.preferredTemplate = template;
@@ -42,6 +32,38 @@ const mixinsVue = {
window.location.reload();
}
},
initTheme() {
const storedTheme = localStorage.getItem("theme");
const theme = (storedTheme === 'dark' || storedTheme === 'light') ? storedTheme : (window.matchMedia('(prefers-color-scheme: dark)').matches ? 'dark' : 'light');
this.setTheme(theme);
return theme;
},
setTheme(theme) {
document.body.setAttribute("theme", theme);
this.theme = theme;
localStorage.setItem("theme", theme);
// 重新赋值全局调色
this.colors = this.theme == "dark" ? this.colorsDark : this.colorsLight;
if(this.$root.page == 'index') {
this.reloadCharts(); // 重新载入echarts图表
}
},
initShowGroup() {
const storedShowGroup = localStorage.getItem("showGroup");
const showGroup = storedShowGroup !== null ? JSON.parse(storedShowGroup) : false;
if (storedShowGroup === null) {
localStorage.setItem("showGroup", showGroup);
}
return showGroup;
},
toggleShowGroup() {
this.showGroup = !this.showGroup;
localStorage.setItem("showGroup", this.showGroup);
if (this.$root.page == 'service') {
this.$root.initTooltip();
}
},
updateCookie(name, value) {
document.cookie = name + "=" + value +"; path=/";
},
@@ -57,43 +79,6 @@ const mixinsVue = {
}
return cookieValue;
},
setTheme(title, store = false) {
this.theme = title;
document.body.setAttribute("theme", title);
if (store) {
localStorage.setItem("theme", title);
this.isSystemTheme = false;
if(this.$root.page == 'index') {
this.$root.reloadCharts(); //重新载入echarts图表
}
}
},
setSystemTheme() {
localStorage.removeItem("theme");
this.initTheme();
this.isSystemTheme = true;
},
initTheme() {
const storeTheme = localStorage.getItem("theme");
if (storeTheme === 'dark' || storeTheme === 'light') {
this.setTheme(storeTheme, true);
} else {
this.isSystemTheme = true
const handleChange = (mediaQueryListEvent) => {
if (localStorage.getItem("theme")) {
return
}
if (mediaQueryListEvent.matches) {
this.setTheme('dark');
} else {
this.setTheme('light');
}
}
const mediaQueryListDark = window.matchMedia('(prefers-color-scheme: dark)');
this.setTheme(mediaQueryListDark.matches ? 'dark' : 'light');
mediaQueryListDark.addEventListener("change", handleChange);
}
},
toFixed2(f) {
return f.toFixed(2)
},
File diff suppressed because it is too large Load Diff
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
+4 -5
View File
@@ -6,17 +6,16 @@
{{.Version}}</small>
</div>
</div>
<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/semantic-ui/2.4.1/semantic.min.js"></script>
<script src="https://unpkg.com/jquery@3.7.1/dist/jquery.min.js"></script>
<script src="https://unpkg.com/semantic-ui@2.4.0/dist/semantic.min.js"></script>
<script src="/static/semantic-ui-alerts.min.js"></script>
<script src="https://lf6-cdn-tos.bytecdntp.com/cdn/expire-1-y/vue/2.6.14/vue.min.js"></script>
<script src="/static/main.js?v20240330"></script>
<script src="https://unpkg.com/vue@2.6.14/dist/vue.min.js"></script>
<script src="/static/main.js?v20240714"></script>
<script>
(function () {
updateLang({{.LANG }});
})();
</script>
</body>
</html>
{{end}}
+4 -8
View File
@@ -1,21 +1,17 @@
{{define "common/header"}}
<!DOCTYPE html>
<html lang="{{.Conf.Language}}">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<meta http-equiv="X-UA-Compatible" content="ie=edge">
<meta content="telephone=no" name="format-detection">
<title>{{.Title}}</title>
<link rel="stylesheet" type="text/css"
href="https://lf6-cdn-tos.bytecdntp.com/cdn/expire-1-y/semantic-ui/2.4.1/semantic.min.css">
<link href="https://lf6-cdn-tos.bytecdntp.com/cdn/expire-1-y/font-logos/0.17/font-logos.min.css" type="text/css"
rel="stylesheet" />
<link rel="shortcut icon" type="image/png" href="/static/logo.svg?v20210804" />
<link rel="stylesheet" type="text/css" href="https://unpkg.com/semantic-ui@2.4.0/dist/semantic.min.css">
<link rel="stylesheet" type="text/css" href="https://unpkg.com/font-logos@0.17.0/assets/font-logos.css">
<link rel="stylesheet" type="text/css" href="/static/semantic-ui-alerts.min.css">
<link rel="stylesheet" type="text/css" href="/static/main.css?v2022042314">
<link rel="shortcut icon" type="image/png" href="/static/logo.svg?v20210804" />
</head>
<body>
{{end}}
{{end}}
+1
View File
@@ -9,6 +9,7 @@
<a class='item{{if eq .MatchedPath "/monitor"}} active{{end}}' href="/monitor"><i class="rss icon"></i>{{tr "Services"}}</a>
<a class='item{{if eq .MatchedPath "/cron"}} active{{end}}' href="/cron"><i class="clock icon"></i>{{tr "Task"}}</a>
<a class='item{{if eq .MatchedPath "/notification"}} active{{end}}' href="/notification"><i class="bell icon"></i>{{tr "Notification"}}</a>
<a class='item{{if eq .MatchedPath "/nat"}} active{{end}}' href="/nat"><i class="exchange icon"></i>{{tr "NAT"}}</a>
<a class='item{{if eq .MatchedPath "/setting"}} active{{end}}' href="/setting">
<i class="settings icon"></i>{{tr "Settings"}}
</a>
+31
View File
@@ -0,0 +1,31 @@
{{define "component/nat"}}
<div class="ui tiny nat modal transition hidden">
<div class="header">Add</div>
<div class="content">
<form id="natForm" 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>Agent ID</label>
<input type="number" name="ServerID" placeholder="1">
</div>
<div class="field">
<label>内网服务</label>
<input type="text" name="Host" placeholder="192.168.1.1:80(带端口)">
</div>
<div class="field">
<label>绑定域名</label>
<input type="text" name="Domain" placeholder="router.app.yourdomain.com">
</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}}
+54
View File
@@ -0,0 +1,54 @@
{{define "dashboard-default/nat"}}
{{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="addOrEditNAT()"><i
class="add icon"></i> Add
</button>
</div>
</div>
<table class="ui very basic table">
<thead>
<tr>
<th>ID</th>
<th>{{tr "Name"}}</th>
<th>Agent ID</th>
<th>内网服务</th>
<th>绑定域名</th>
<th>{{tr "Administration"}}</th>
</tr>
</thead>
<tbody>
{{range $item := .NAT}}
<tr>
<td>{{$item.ID}}</td>
<td>{{$item.Name}}</td>
<td>{{$item.ServerID}}</td>
<td>{{$item.Host}}</td>
<td>{{$item.Domain}}</td>
<td>
<div class="ui mini icon buttons">
<button class="ui button" onclick="addOrEditNAT({{$item}})">
<i class="edit icon"></i>
</button>
<button class="ui button"
onclick="showConfirm('确定删除NAT隧道?','确认删除',deleteRequest,'/api/nat/'+{{$item.ID}})">
<i class="trash alternate outline icon"></i>
</button>
</div>
</td>
</tr>
{{end}}
</tbody>
</table>
</div>
</div>
{{template "component/nat"}}
{{template "common/footer" .}}
<script>
$('.checkbox').checkbox()
</script>
{{end}}
-3
View File
@@ -1,18 +1,15 @@
{{define "dashboard-default/redirect"}}
<!DOCTYPE html>
<html lang="{{.Conf.Language}}">
<head>
<meta charset="UTF-8">
<meta http-equiv="X-UA-Compatible" content="IE=edge">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Redirecting..</title>
</head>
<body>
<p>If you are not redirected, please click <a href="{{.URL}}">here</a>.</p>
<script>window.location.href = "{{.URL}}"</script>
</body>
</html>
{{end}}
+2 -2
View File
@@ -60,7 +60,7 @@
</td>
<td>
<button class="ui icon green mini button"
data-clipboard-text="{{if $.Conf.GRPCHost}}{{if eq $.Conf.Language "zh-CN"}}curl -L https://raw.githubusercontent.com/naiba/nezha/master/script/install.sh{{else}}curl -L https://raw.githubusercontent.com/naiba/nezha/master/script/install_en.sh{{end}} -o nezha.sh && chmod +x nezha.sh && sudo ./nezha.sh install_agent {{$.Conf.GRPCHost}} {{if $.Conf.ProxyGRPCPort}}{{$.Conf.ProxyGRPCPort}}{{else}}{{$.Conf.GRPCPort}}{{end}} {{$server.Secret}}{{if $.Conf.TLS}} --tls{{end}}{{else}}{{tr "NoDomainAlert"}}{{end}}"
data-clipboard-text="{{if $.Conf.GRPCHost}}{{if eq $.Conf.Language "zh-CN"}}curl -L https://raw.githubusercontent.com/naiba/nezha/master/script/install.sh{{else}}curl -L https://raw.githubusercontent.com/naiba/nezha/master/script/install_en.sh{{end}} -o nezha.sh && chmod +x nezha.sh && ./nezha.sh install_agent {{$.Conf.GRPCHost}} {{if $.Conf.ProxyGRPCPort}}{{$.Conf.ProxyGRPCPort}}{{else}}{{$.Conf.GRPCPort}}{{end}} {{$server.Secret}}{{if $.Conf.TLS}} --tls{{end}}{{else}}{{tr "NoDomainAlert"}}{{end}}"
data-tooltip="{{tr "ClickToCopy"}}">
<i class="linux icon"></i>
</button>
@@ -98,7 +98,7 @@
</div>
{{template "component/server" .}}
{{template "common/footer" .}}
<script src="https://lf6-cdn-tos.bytecdntp.com/cdn/expire-1-y/clipboard.js/2.0.10/clipboard.min.js" type="application/javascript"></script>
<script src="https://unpkg.com/clipboard@2.0.11/dist/clipboard.min.js"></script>
<script>
var clipboard = new ClipboardJS('.ui.icon.green.mini.button');
const checkBoxList = document.querySelectorAll('tbody > tr > td > input.nezha-servers[type=checkbox]')
+4 -8
View File
@@ -1,16 +1,14 @@
{{define "dashboard-default/terminal"}}
<!DOCTYPE html>
<html lang="{{.Conf.Language}}">
<head>
<meta charset="UTF-8">
<meta http-equiv="X-UA-Compatible" content="IE=edge">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>tty@{{.ServerName}} - {{.Title}}</title>
<link rel="shortcut icon" type="image/png" href="/static/logo.svg?v20210804" />
<link href="https://lf6-cdn-tos.bytecdntp.com/cdn/expire-1-y/xterm/4.11.0/xterm.css" type="text/css" rel="stylesheet"/>
<link type="text/css" rel="stylesheet" href="https://unpkg.com/[email protected]/css/xterm.css" />
</head>
<style>
html,
body,
@@ -25,12 +23,11 @@
background-color: black;
}
</style>
<body onresize="onResize()">
<div id="terminal-container"></div>
<script src="https://lf6-cdn-tos.bytecdntp.com/cdn/expire-1-y/xterm/4.11.0/xterm.js" type="application/javascript"></script>
<script src="https://lf6-cdn-tos.bytecdntp.com/cdn/expire-1-y/xterm/4.11.0/addons/attach/xterm-addon-attach.js"></script>
<script src="https://lf6-cdn-tos.bytecdntp.com/cdn/expire-1-y/xterm/4.11.0/addons/fit/xterm-addon-fit.js"></script>
<script src="https://unpkg.com/[email protected]/lib/xterm.js"></script>
<script src="https://unpkg.com/@xterm/addon-attach@0.11.0/lib/addon-attach.js"></script>
<script src="https://unpkg.com/@xterm/addon-fit@0.10.0/lib/addon-fit.js"></script>
<script>
let sendResizing = false;
@@ -93,6 +90,5 @@
}
</script>
</body>
</html>
{{end}}
+1
View File
@@ -180,6 +180,7 @@
'TC0H', //CPU Heatsink 温度,代表 CPU 散热器的温度
'TC0P', //CPU Proximity 温度,代表 CPU 附近的温度
'k10temp', //AMD K10Phenom、Athlon、Sempron 等)系列处理器的温度监测
'k10temp_tctl', //AMD K10 (Athlon II、Phenom II 等)系列处理器的温度监测
'coretemp_package_id_0', //整个封装处理器温度
'cpu_thermal_zone', //全志
'cpu-thermal', //树莓派(博通)
+6 -8
View File
@@ -1,20 +1,18 @@
{{define "theme-server-status/content-footer"}}
<footer class="container" style="padding-bottom: 2rem;">
<p style="text-align: center; font-size: 10px;">
{{ .Conf.Site.Brand }} | Theme ServerStatus | Powered by <a target="_blank" href="https://github.com/naiba/nezha">{{tr "NezhaMonitoring"}}</a> {{.Version}}
</p>
<footer>
<p>{{ .Conf.Site.Brand }} | Theme ServerStatus | Powered by <a target="_blank" href="https://github.com/naiba/nezha">{{tr "NezhaMonitoring"}}</a> {{.Version}}</p>
</footer>
<aside class="toolbox">
<span v-if="page=='index' && countryMapChartData.length!=0" class="showMapChart">
<i @click="showMapChart" data-toggle="modal" data-target="#mapChartBox" class="bi bi-geo-alt"></i>
</span>
<span class="toggleView">
<i v-if="showGroup" @click="toggleView" class="show-nogroup bi bi-justify"></i>
<i v-else @click="toggleView" class="show-group bi bi-view-stacked"></i>
<i v-if="showGroup" @click="toggleShowGroup" class="show-nogroup bi bi-justify"></i>
<i v-else @click="toggleShowGroup" class="show-group bi bi-view-stacked"></i>
</span>
<span class="setTheme">
<i v-if="theme === 'light'" @click="setTheme('dark', true)" class="setTheme-dark bi bi-moon-fill"></i>
<i v-else @click="setTheme('light', true)" class="setTheme-light bi bi-brightness-high-fill"></i>
<i v-if="theme === 'light'" @click="setTheme('dark')" class="setTheme-dark bi bi-moon-fill"></i>
<i v-else @click="setTheme('light')" class="setTheme-light bi bi-brightness-high-fill"></i>
</span>
<span v-if="showGoTop" class="showGoTop">
<i @click="goTop" class="goTop bi bi-arrow-up"></i>
+3 -4
View File
@@ -9,8 +9,8 @@
<link rel="shortcut icon" type="image/png" href="/static/logo.svg" />
<link rel="stylesheet" href="https://unpkg.com/[email protected]/dist/css/bootstrap.min.css">
<link rel="stylesheet" href="https://unpkg.com/[email protected]/dist/css/bootstrap-theme.min.css">
<link rel="stylesheet" href="/static/theme-server-status/css/main.css?v20240616">
<link rel="stylesheet" href="/static/theme-server-status/css/dark.css?v20240407">
<link rel="stylesheet" href="/static/theme-server-status/css/main.css?v20240724">
<link rel="stylesheet" href="/static/theme-server-status/css/dark.css?v20240724">
<link rel="stylesheet" href="/static/theme-server-status/css/light.css?v20240407">
<link rel="stylesheet" href="https://unpkg.com/[email protected]/font/bootstrap-icons.min.css">
<link rel="stylesheet" href="https://unpkg.com/[email protected]/assets/font-logos.css">
@@ -20,8 +20,7 @@
<script src="https://unpkg.com/[email protected]/dist/js/bootstrap.min.js"></script>
<script src="https://unpkg.com/[email protected]/dist/vue.min.js"></script>
<script src="https://unpkg.com/[email protected]/dist/echarts.min.js"></script>
<script src="https://unpkg.com/[email protected]/map/js/world.js"></script>
<script src="/static/theme-server-status/js/mixin.js?v20240302"></script>
<script src="/static/theme-server-status/js/mixin.js?v20240711"></script>
<!-- HTML5 shim and Respond.js IE8 support of HTML5 elements and media queries -->
<!--[if lt IE 9]>
<script src="/static/theme-server-status/js/html5shiv.js"></script>
File diff suppressed because one or more lines are too long
+117 -98
View File
@@ -4,74 +4,74 @@
{{template "theme-server-status/content-nav" .}}
<!-- showGroup true -->
<template v-if="showGroup">
<section class="container content" style="max-width: 95vw; min-height: .01%;overflow-x: auto;" v-for="group in servicesTag">
<section v-if="servicesTag.length === 0" class="container content" style="max-width: 95vw; min-height: .01%;overflow-x: auto;">
<p>No Valid Service Monitor Configuration Entries Found. Please Verify in the <a href="/monitor">Admin Panel</a>.</p>
</section>
<section v-else class="container content" style="max-width: 95vw; min-height: .01%;overflow-x: auto;" v-for="group in servicesTag">
{{template "theme-server-status/service-group-true" .}}
</section>
</template>
<!-- showGroup false -->
<template v-else>
<section class="container content" style="max-width: 95vw; min-height: .01%;overflow-x: auto;">
<section v-if="servicesNoTag.length === 0" class="container content" style="max-width: 95vw; min-height: .01%;overflow-x: auto;">
<p>No Valid Service Monitor Configuration Entries Found. Please Verify in the <a href="/monitor">Admin Panel</a>.</p>
</section>
<section v-else class="container content" style="max-width: 95vw; min-height: .01%;overflow-x: auto;">
{{template "theme-server-status/service-group-false" .}}
</section>
</template>
<section class="container content table-responsive" style="max-width: 95vw">
{{if .CycleTransferStats}}
<table class="table table-striped table-condensed table-hover">
<thead>
<tr class="node-group-tag">
<th colspan="16" style="border:none;">
{{tr "CycleTransferStats"}}
</th>
</tr>
<tr class="node-group-cell">
<th class="node-cell center">ID</th>
<th class="node-cell center">{{tr "Rules"}}</th>
<th class="node-cell center">{{tr "Server"}}</th>
<th class="node-cell center">{{tr "From"}}</th>
<th class="node-cell center">{{tr "To"}}</th>
<th class="node-cell center">MAX</th>
<th class="node-cell center">MIN</th>
<th class="node-cell center">{{tr "NextCheck"}}</th>
<th class="node-cell center">{{tr "CurrentUsage"}}</th>
<th class="node-cell center">{{tr "Transleft"}}</th>
</tr>
</thead>
<tbody>
{{range $id, $stats := .CycleTransferStats}}
{{range $innerId, $transfer := $stats.Transfer}}
{{$TransLeftPercent := TransLeftPercent (UintToFloat $transfer) (UintToFloat $stats.Max)}}
<tr>
<td class="node-cell center">{{$id}}</td>
<td class="node-cell center">{{$stats.Name}}</td>
<td class="node-cell center">{{index $stats.ServerName $innerId}}</td>
<td class="node-cell center">{{$stats.From|tf}}</td>
<td class="node-cell center">{{$stats.To|tf}}</td>
<td class="node-cell center">{{$stats.Max|bf}}</td>
<td class="node-cell center">{{$stats.Min|bf}}</td>
<td class="node-cell center">{{(index $stats.NextUpdate $innerId)|sft}}</td>
<td class="node-cell center">{{$transfer|bf}}</td>
<td class="node-cell center">
<div class="progress">
<div style="width: {{$TransLeftPercent}}%" :class="'progress-bar progress-bar-' + toSSBar('{{TransClassName $TransLeftPercent}}')">
<small style="display: inline-block;width: max-content;">{{TransLeft $stats.Max $transfer}} / {{$TransLeftPercent}} %</small>
</div>
</div>
</td>
</tr>
{{end}}
{{end}}
</tbody>
</table>
{{end}}
</section>
{{if .CycleTransferStats}}
<section class="container content table-responsive" style="max-width: 95vw">
<table class="table table-striped table-condensed table-hover">
<thead>
<tr class="node-group-tag">
<th colspan="16" style="border:none;">
{{tr "CycleTransferStats"}}
</th>
</tr>
<tr class="node-group-cell">
<th class="node-cell center">ID</th>
<th class="node-cell center">{{tr "Rules"}}</th>
<th class="node-cell center">{{tr "Server"}}</th>
<th class="node-cell center">{{tr "From"}}</th>
<th class="node-cell center">{{tr "To"}}</th>
<th class="node-cell center">MAX</th>
<th class="node-cell center">MIN</th>
<th class="node-cell center">{{tr "NextCheck"}}</th>
<th class="node-cell center">{{tr "CurrentUsage"}}</th>
<th class="node-cell center">{{tr "Transleft"}}</th>
</tr>
</thead>
<tbody>
{{range $id, $stats := .CycleTransferStats}}
{{range $innerId, $transfer := $stats.Transfer}}
{{$TransLeftPercent := TransLeftPercent (UintToFloat $transfer) (UintToFloat $stats.Max)}}
<tr>
<td class="node-cell center">{{$id}}</td>
<td class="node-cell center">{{$stats.Name}}</td>
<td class="node-cell center">{{index $stats.ServerName $innerId}}</td>
<td class="node-cell center">{{$stats.From|tf}}</td>
<td class="node-cell center">{{$stats.To|tf}}</td>
<td class="node-cell center">{{$stats.Max|bf}}</td>
<td class="node-cell center">{{$stats.Min|bf}}</td>
<td class="node-cell center">{{(index $stats.NextUpdate $innerId)|sft}}</td>
<td class="node-cell center">{{$transfer|bf}}</td>
<td class="node-cell center">
<div class="progress">
<div style="width: {{$TransLeftPercent}}%" :class="'progress-bar progress-bar-' + toSSBar('{{TransClassName $TransLeftPercent}}')">
<small style="display: inline-block;width: max-content;">{{TransLeft $stats.Max $transfer}} / {{$TransLeftPercent}} %</small>
</div>
</div>
</td>
</tr>
{{end}}
{{end}}
</tbody>
</table>
</section>
{{end}}
{{template "theme-server-status/content-footer" .}}
</div>
<script>
// 初始化 Tooltip
$(document).ready(function(){
$('[data-toggle="tooltip"]').tooltip();
});
</script>
<script>
new Vue({
el: '#app',
@@ -80,13 +80,16 @@
page: 'service',
defaultTemplate: {{.Conf.Site.Theme}},
templates: {{.Themes}},
services: {{.Services}},
servicesTag: [],
servicesNoTag: [],
servicesNoTag: []
},
created() {
this.initData();
this.servicesTag = this.groupingData(this.initData(this.services),"type");
this.servicesNoTag = this.initData(this.services);
},
mounted() {
this.initTooltip();
},
mixins: [mixinsVue],
methods: {
@@ -103,32 +106,36 @@
}
return n
},
initData() {
// @formatter:off
const services = []
{{range $service := .Services}}
services.push({
type: '{{$service.Monitor.Type}}',
name: '{{$service.Monitor.Name}}',
currentUp: parseInt('{{$service.CurrentUp}}'),
currentDown: parseInt('{{$service.CurrentDown}}'),
totalUp: parseInt('{{$service.TotalUp}}'),
totalDown: parseInt('{{$service.TotalDown}}'),
delay: '{{$service.Delay}}'.replaceAll("[","").replaceAll("]","").split(" "),
up: '{{$service.Up}}'.replaceAll("[","").replaceAll("]","").split(" "),
down: '{{$service.Down}}'.replaceAll("[","").replaceAll("]","").split(" "),
})
{{end}}
// @formatter:on
for (let i = 0; i < services.length; i++) {
const service = services[i];
service.avgDelay = parseInt(service.delay[service.delay.length - 1]) + "ms"
service.health = this.getStateInfo(this.getPercent(service.currentUp, service.currentDown))
service.dayDetail = this.getDayTails(service)
service.totalUpTime = this.getProgressInfo(this.getPercent(service.totalUp, service.totalDown))
initData(services) {
let nodes = [];
for (let key in services) {
const service = services[key];
let node = {
type: service.Monitor.Type,
name: service.Monitor.Name,
currentUp: parseInt(service.CurrentUp),
currentDown: parseInt(service.CurrentDown),
totalUp: parseInt(service.TotalUp),
totalDown: parseInt(service.TotalDown),
up: service.Up,
down: service.Down,
delay: service.Delay,
avgDelay: parseInt(this.getAvgDelay(service.Delay)) + "ms",
health: this.getStateInfo(this.getPercent(service.CurrentUp, service.CurrentDown)),
totalUpTime: this.getProgressInfo(this.getPercent(service.TotalUp, service.TotalDown))
};
nodes.push(node);
};
for (let i = 0; i < nodes.length; i++) {
const node = nodes[i];
node.dayDetail = this.getDayTails(node)
}
this.servicesTag = this.groupingData(services,"type");
this.servicesNoTag = services;
return nodes
},
initTooltip() {
$(document).ready(function(){
$('[data-toggle="tooltip"]').tooltip();
});
},
getPercent(up, down) {
if (!up) {
@@ -151,24 +158,24 @@
return this.toFixed2(currentUp / total * 100)
},
getDayTails(service) {
const result = []
const result = [];
for (let i = 0; i < service.up.length; i++) {
const up = service.up[i]
const down = service.down[i]
const delay = service.delay[i]
let percent = this.getPercent(up, down)
const up = service.up[i];
const down = service.down[i];
let delay = Number(service.delay[i]);
let percent = this.getPercent(up, down);
if (percent <= 0) {
percent = 0;
}
let className = this.getStateInfo(percent).className
let available = '{{tr "Availability"}}'
let averageLatency = '{{tr "AverageLatency"}}'
const text = `${this.beforeDay(service.up.length - i - 1)}${available}${percent}%${averageLatency}${delay}ms`
let className = this.getStateInfo(percent).className;
let available = '{{tr "Availability"}}';
let averageLatency = '{{tr "AverageLatency"}}';
const text = `${this.beforeDay(service.up.length - i - 1)}${available}${Number(percent).toFixed(3)}%${averageLatency}${Number(delay).toFixed(3)}ms`;
result.push({
text, className
})
});
}
return result
return result;
},
beforeDay(days) {
const today = new Date();
@@ -203,8 +210,9 @@
return result;
},
getProgressInfo(percent) {
const result = this.getStateInfo(percent)
const result = this.getStateInfo(percent);
result.style = `width: ${parseInt(percent)}%`;
result.percent = Number(percent).toFixed(2);
const className = result.className;
if (className === "good") {
result.className = 'progress-bar progress-bar-success'
@@ -218,8 +226,19 @@
}
return result
},
getAvgDelay(array) {
const { sum, count } = array.reduce((acc, value) => {
if (value !== 0) {
acc.sum += value;
acc.count += 1;
}
return acc;
}, { sum: 0, count: 0 });
return count > 0 ? sum / count : 0;
}
}
})
</script>
{{template "theme-server-status/footer" .}}
{{end}}
{{end}}
+12 -10
View File
@@ -28,8 +28,8 @@ pre_check() {
## China_IP
if [[ -z "${CN}" ]]; then
if [[ $(curl -m 10 -s https://ipapi.co/json | grep 'China') != "" ]]; then
echo "According to the information provided by ipapi.co, the current IP may be in China"
if [[ $(curl -m 10 -s http://ip-api.com/json |grep 'country' |grep -q 'China') != "" ]]; then
echo "According to the information provided by ip-api.com, the current IP may be in China"
read -e -r -p "Is the installation done with a Chinese Mirror? [Y/n] (Custom Mirror Input 3):" input
case $input in
[yY][eE][sS] | [yY])
@@ -65,15 +65,9 @@ pre_check() {
if [[ -z "${CN}" ]]; then
GITHUB_RAW_URL="raw.githubusercontent.com/naiba/nezha/master"
GITHUB_URL="github.com"
Get_Docker_URL="get.docker.com"
Get_Docker_Argu=" "
Docker_IMG="ghcr.io\/naiba\/nezha-dashboard"
else
GITHUB_RAW_URL="gitee.com/naibahq/nezha/raw/master"
GITHUB_URL="github.com"
Get_Docker_URL="get.docker.com"
Get_Docker_Argu=" -s docker --mirror Aliyun"
Docker_IMG="registry.cn-shanghai.aliyuncs.com\/naibahq\/nezha-dashboard"
GITHUB_URL="gitee.com"
fi
fi
}
@@ -89,6 +83,9 @@ install_agent() {
echo -e "Obtaining Agent version"
local version=$(curl -m 10 -sL "https://api.github.com/repos/nezhahq/agent/releases/latest" | grep "tag_name" | head -n 1 | awk -F ":" '{print $2}' | sed 's/\"//g;s/,//g;s/ //g')
if [ ! -n "$version" ]; then
version=$(curl -m 10 -sL "https://gitee.com/api/v5/repos/naibahq/agent/releases/latest" | awk -F '"' '{for(i=1;i<=NF;i++){if($i=="tag_name"){print $(i+2)}}}')
fi
if [ ! -n "$version" ]; then
version=$(curl -m 10 -sL "https://fastly.jsdelivr.net/gh/nezhahq/agent/" | grep "option\.value" | awk -F "'" '{print $2}' | sed 's/nezhahq\/agent@/v/g')
fi
@@ -108,7 +105,12 @@ install_agent() {
chmod -R 777 $NZ_AGENT_PATH
echo -e "Downloading Agent"
curl -o nezha-agent_darwin_${os_arch}.zip -L -f --retry 2 --retry-max-time 60 https://${GITHUB_URL}/nezhahq/agent/releases/download/${version}/nezha-agent_darwin_${os_arch}.zip >/dev/null 2>&1
if [[ -z $CN ]]; then
NZ_AGENT_URL="https://${GITHUB_URL}/nezhahq/agent/releases/download/${version}/nezha-agent_darwin_${os_arch}.zip"
else
NZ_AGENT_URL="https://${GITHUB_URL}/naibahq/agent/releases/download/${version}/nezha-agent_darwin_${os_arch}.zip"
fi
curl -o nezha-agent_darwin_${os_arch}.zip -L -f --retry 2 --retry-max-time 60 $NZ_AGENT_URL >/dev/null 2>&1
if [[ $? != 0 ]]; then
echo -e "${red}Fail to download agent, please check if the network can link ${GITHUB_URL}${plain}"
return 0
+15 -3
View File
@@ -49,14 +49,26 @@ if ([string]::IsNullOrWhiteSpace($agenttag)) {
}
}
#Region判断
$ipapi= Invoke-RestMethod -Uri "https://api.myip.com/" -UserAgent "Mozilla/5.0 (Windows NT 6.1; WOW64) AppleWebKit/535.1 (KHTML, like Gecko) Chrome/14.0.835.163 Safari/535.1"
$region=$ipapi.cc
$ipapi = ""
$region = "Unknown"
foreach ($url in ("https://dash.cloudflare.com/cdn-cgi/trace","https://cf-ns.com/cdn-cgi/trace","https://1.0.0.1/cdn-cgi/trace")) {
try {
$ipapi = Invoke-RestMethod -Uri $url -TimeoutSec 5 -UseBasicParsing
if ($ipapi -match "loc=(\w+)" ) {
$region = $Matches[1]
break
}
}
catch {
Write-Host "Error occurred while querying $url : $_"
}
}
echo $ipapi
if($region -ne "CN"){
$download = "https://github.com/$agentrepo/releases/download/$agenttag/$file"
Write-Host "Location:$region,connect directly!" -BackgroundColor DarkRed -ForegroundColor Green
}else{
$download = "https://github.com/$agentrepo/releases/download/$agenttag/$file"
$download = "https://gitee.com/naibahq/agent/releases/download/$agenttag/$file"
Write-Host "Location:CN,use mirror address" -BackgroundColor DarkRed -ForegroundColor Green
}
echo $download
+54 -8
View File
@@ -12,7 +12,7 @@ NZ_DASHBOARD_PATH="${NZ_BASE_PATH}/dashboard"
NZ_AGENT_PATH="${NZ_BASE_PATH}/agent"
NZ_DASHBOARD_SERVICE="/etc/systemd/system/nezha-dashboard.service"
NZ_DASHBOARD_SERVICERC="/etc/init.d/nezha-dashboard"
NZ_VERSION="v0.17.0"
NZ_VERSION="v0.18.2"
red='\033[0;31m'
green='\033[0;32m'
@@ -24,8 +24,8 @@ os_arch=""
[ -e /etc/os-release ] && grep -i "PRETTY_NAME" /etc/os-release | grep -qi "alpine" && os_alpine='1'
sudo() {
EUID=$(id -ru)
if [ "$EUID" -ne 0 ]; then
myEUID=$(id -ru)
if [ "$myEUID" -ne 0 ]; then
if command -v sudo > /dev/null 2>&1; then
command sudo "$@"
else
@@ -48,6 +48,19 @@ err() {
printf "${red}$*${plain}\n" >&2
}
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"
ua="Mozilla/5.0 (X11; Linux x86_64; rv:60.0) Gecko/20100101 Firefox/81.0"
set -- $api_list
for url in $api_list; do
text="$(curl -A $ua -m 10 -s $url)"
if echo $text | grep -qw 'CN'; then
isCN=true
break
fi
done
}
pre_check() {
## os_arch
if uname -m | grep -q 'x86_64'; then
@@ -66,8 +79,9 @@ pre_check() {
## China_IP
if [ -z "$CN" ]; then
if curl -m 10 -s https://ipapi.co/json | grep -q 'China'; then
echo "根据ipapi.co提供的信息,当前IP可能在中国"
geo_check
if [ ! -z "$isCN" ]; then
echo "根据geoip api提供的信息,当前IP可能在中国"
printf "是否选用中国镜像完成安装? [Y/n] (自定义镜像输入 3):"
read -r input
case $input in
@@ -114,7 +128,7 @@ pre_check() {
Docker_IMG="ghcr.io\/naiba\/nezha-dashboard"
else
GITHUB_RAW_URL="gitee.com/naibahq/nezha/raw/master"
GITHUB_URL="github.com"
GITHUB_URL="gitee.com"
Get_Docker_URL="get.docker.com"
Get_Docker_Argu=" -s docker --mirror Aliyun"
Docker_IMG="registry.cn-shanghai.aliyuncs.com\/naibahq\/nezha-dashboard"
@@ -339,6 +353,9 @@ install_agent() {
echo "正在获取监控Agent版本号"
local version=$(curl -m 10 -sL "https://api.github.com/repos/nezhahq/agent/releases/latest" | grep "tag_name" | head -n 1 | awk -F ":" '{print $2}' | sed 's/\"//g;s/,//g;s/ //g')
if [ ! -n "$version" ]; then
version=$(curl -m 10 -sL "https://gitee.com/api/v5/repos/naibahq/agent/releases/latest" | awk -F '"' '{for(i=1;i<=NF;i++){if($i=="tag_name"){print $(i+2)}}}')
fi
if [ ! -n "$version" ]; then
version=$(curl -m 10 -sL "https://fastly.jsdelivr.net/gh/nezhahq/agent/" | grep "option\.value" | awk -F "'" '{print $2}' | sed 's/nezhahq\/agent@/v/g')
fi
@@ -358,7 +375,12 @@ install_agent() {
sudo chmod -R 700 $NZ_AGENT_PATH
echo "正在下载监控端"
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
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
wget -t 2 -T 60 -O nezha-agent_linux_${os_arch}.zip $NZ_AGENT_URL >/dev/null 2>&1
if [ $? != 0 ]; then
err "Release 下载失败,请检查本机能否连接 ${GITHUB_URL}"
return 1
@@ -548,6 +570,17 @@ restart_and_update_docker() {
}
restart_and_update_standalone() {
local version=$(curl -m 10 -sL "https://api.github.com/repos/naiba/nezha/releases/latest" | grep "tag_name" | head -n 1 | awk -F ":" '{print $2}' | sed 's/\"//g;s/,//g;s/ //g')
if [ ! -n "$version" ]; then
version=$(curl -m 10 -sL "https://gitee.com/api/v5/repos/naibahq/nezha/releases/latest" | awk -F '"' '{for(i=1;i<=NF;i++){if($i=="tag_name"){print $(i+2)}}}')
fi
if [ ! -n "$version" ]; then
version=$(curl -m 10 -sL "https://fastly.jsdelivr.net/gh/naiba/nezha/" | grep "option\.value" | awk -F "'" '{print $2}' | sed 's/naiba\/nezha@/v/g')
fi
if [ ! -n "$version" ]; then
version=$(curl -m 10 -sL "https://gcore.jsdelivr.net/gh/naiba/nezha/" | grep "option\.value" | awk -F "'" '{print $2}' | sed 's/naiba\/nezha@/v/g')
fi
if [ "$os_alpine" != 1 ]; then
sudo systemctl daemon-reload
sudo systemctl stop nezha-dashboard
@@ -555,7 +588,20 @@ restart_and_update_standalone() {
sudo rc-service nezha-dashboard stop
fi
sudo wget -qO $NZ_DASHBOARD_PATH/app.zip https://${GITHUB_URL}/naiba/nezha/releases/latest/download/dashboard-linux-$os_arch.zip >/dev/null 2>&1 && sudo unzip -qq $NZ_DASHBOARD_PATH/app.zip -d $NZ_DASHBOARD_PATH && sudo mv $NZ_DASHBOARD_PATH/dist/dashboard-linux-$os_arch $NZ_DASHBOARD_PATH/app && sudo rm -r $NZ_DASHBOARD_PATH/app.zip $NZ_DASHBOARD_PATH/dist
if [ ! -n "$version" ]; then
err "获取版本号失败,请检查本机能否链接 https://api.github.com/repos/naiba/nezha/releases/latest"
return 1
else
echo "当前最新版本为: ${version}"
fi
if [ -z "$CN" ]; then
NZ_DASHBOARD_URL="https://${GITHUB_URL}/naiba/nezha/releases/download/$version/dashboard-linux-$os_arch.zip"
else
NZ_DASHBOARD_URL="https://${GITHUB_URL}/naibahq/nezha/releases/download/$version/dashboard-linux-$os_arch.zip"
fi
sudo wget -qO $NZ_DASHBOARD_PATH/app.zip $NZ_DASHBOARD_URL >/dev/null 2>&1 && sudo unzip -qq $NZ_DASHBOARD_PATH/app.zip -d $NZ_DASHBOARD_PATH && sudo mv $NZ_DASHBOARD_PATH/dist/dashboard-linux-$os_arch $NZ_DASHBOARD_PATH/app && sudo rm -r $NZ_DASHBOARD_PATH/app.zip $NZ_DASHBOARD_PATH/dist
if [ "$os_alpine" != 1 ]; then
sudo systemctl enable nezha-dashboard
+49 -8
View File
@@ -12,7 +12,7 @@ NZ_DASHBOARD_PATH="${NZ_BASE_PATH}/dashboard"
NZ_AGENT_PATH="${NZ_BASE_PATH}/agent"
NZ_DASHBOARD_SERVICE="/etc/systemd/system/nezha-dashboard.service"
NZ_DASHBOARD_SERVICERC="/etc/init.d/nezha-dashboard"
NZ_VERSION="v0.17.0"
NZ_VERSION="v0.18.2"
red='\033[0;31m'
green='\033[0;32m'
@@ -24,8 +24,8 @@ os_arch=""
[ -e /etc/os-release ] && grep -i "PRETTY_NAME" /etc/os-release | grep -qi "alpine" && os_alpine='1'
sudo() {
EUID=$(id -ru)
if [ "$EUID" -ne 0 ]; then
myEUID=$(id -ru)
if [ "$myEUID" -ne 0 ]; then
if command -v sudo > /dev/null 2>&1; then
command sudo "$@"
else
@@ -48,6 +48,19 @@ err() {
printf "${red}$*${plain}\n" >&2
}
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"
ua="Mozilla/5.0 (X11; Linux x86_64; rv:60.0) Gecko/20100101 Firefox/81.0"
set -- $api_list
for url in $api_list; do
text="$(curl -A $ua -m 10 -s $url)"
if echo $text | grep -qw 'CN'; then
isCN=true
break
fi
done
}
pre_check() {
## os_arch
if uname -m | grep -q 'x86_64'; then
@@ -66,8 +79,9 @@ pre_check() {
## China_IP
if [ -z "$CN" ]; then
if curl -m 10 -s https://ipapi.co/json | grep -q 'China'; then
echo "According to the information provided by ipapi.co, the current IP may be in China"
geo_check
if [ ! -z "$isCN" ]; then
echo "According to the information provided by various geoip api, the current IP may be in China"
printf "Will the installation be done with a Chinese Mirror? [Y/n] (Custom Mirror Input 3): "
read -r input
case $input in
@@ -113,7 +127,7 @@ pre_check() {
Docker_IMG="ghcr.io\/naiba\/nezha-dashboard"
else
GITHUB_RAW_URL="gitee.com/naibahq/nezha/raw/master"
GITHUB_URL="github.com"
GITHUB_URL="gitee.com"
Get_Docker_URL="get.docker.com"
Get_Docker_Argu=" -s docker --mirror Aliyun"
Docker_IMG="registry.cn-shanghai.aliyuncs.com\/naibahq\/nezha-dashboard"
@@ -335,9 +349,12 @@ install_agent() {
echo "> Install Agent"
echo "Obtaining Agent version"
echo "Obtaining Agent version number"
local version=$(curl -m 10 -sL "https://api.github.com/repos/nezhahq/agent/releases/latest" | grep "tag_name" | head -n 1 | awk -F ":" '{print $2}' | sed 's/\"//g;s/,//g;s/ //g')
if [ ! -n "$version" ]; then
version=$(curl -m 10 -sL "https://gitee.com/api/v5/repos/naibahq/agent/releases/latest" | awk -F '"' '{for(i=1;i<=NF;i++){if($i=="tag_name"){print $(i+2)}}}')
fi
if [ ! -n "$version" ]; then
version=$(curl -m 10 -sL "https://fastly.jsdelivr.net/gh/nezhahq/agent/" | grep "option\.value" | awk -F "'" '{print $2}' | sed 's/nezhahq\/agent@/v/g')
fi
@@ -547,6 +564,24 @@ restart_and_update_docker() {
}
restart_and_update_standalone() {
local version=$(curl -m 10 -sL "https://api.github.com/repos/naiba/nezha/releases/latest" | grep "tag_name" | head -n 1 | awk -F ":" '{print $2}' | sed 's/\"//g;s/,//g;s/ //g')
if [ ! -n "$version" ]; then
version=$(curl -m 10 -sL "https://gitee.com/api/v5/repos/naibahq/nezha/releases/latest" | awk -F '"' '{for(i=1;i<=NF;i++){if($i=="tag_name"){print $(i+2)}}}')
fi
if [ ! -n "$version" ]; then
version=$(curl -m 10 -sL "https://fastly.jsdelivr.net/gh/naiba/nezha/" | grep "option\.value" | awk -F "'" '{print $2}' | sed 's/naiba\/nezha@/v/g')
fi
if [ ! -n "$version" ]; then
version=$(curl -m 10 -sL "https://gcore.jsdelivr.net/gh/naiba/nezha/" | grep "option\.value" | awk -F "'" '{print $2}' | sed 's/naiba\/nezha@/v/g')
fi
if [ ! -n "$version" ]; then
err "Fail to obtaine agent version, please check if the network can link https://api.github.com/repos/nezhahq/agent/releases/latest"
return 1
else
echo "The current latest version is: ${version}"
fi
if [ "$os_alpine" != 1 ]; then
sudo systemctl daemon-reload
sudo systemctl stop nezha-dashboard
@@ -554,7 +589,13 @@ restart_and_update_standalone() {
sudo rc-service nezha-dashboard stop
fi
sudo wget -qO $NZ_DASHBOARD_PATH/app.zip https://${GITHUB_URL}/naiba/nezha/releases/latest/download/dashboard-linux-$os_arch.zip >/dev/null 2>&1 && sudo unzip -qq $NZ_DASHBOARD_PATH/app.zip -d $NZ_DASHBOARD_PATH && sudo mv $NZ_DASHBOARD_PATH/dist/dashboard-linux-$os_arch $NZ_DASHBOARD_PATH/app && sudo rm -r $NZ_DASHBOARD_PATH/app.zip $NZ_DASHBOARD_PATH/dist
if [ -z "$CN" ]; then
NZ_DASHBOARD_URL="https://${GITHUB_URL}/naiba/nezha/releases/download/$version/dashboard-linux-$os_arch.zip"
else
NZ_DASHBOARD_URL="https://${GITHUB_URL}/naibahq/nezha/releases/download/$version/dashboard-linux-$os_arch.zip"
fi
sudo wget -qO $NZ_DASHBOARD_PATH/app.zip $NZ_DASHBOARD_URL >/dev/null 2>&1 && sudo unzip -qq $NZ_DASHBOARD_PATH/app.zip -d $NZ_DASHBOARD_PATH && sudo mv $NZ_DASHBOARD_PATH/dist/dashboard-linux-$os_arch $NZ_DASHBOARD_PATH/app && sudo rm -r $NZ_DASHBOARD_PATH/app.zip $NZ_DASHBOARD_PATH/dist
if [ "$os_alpine" != 1 ]; then
sudo systemctl enable nezha-dashboard
+3 -1
View File
@@ -1 +1,3 @@
protoc --go-grpc_out="require_unimplemented_servers=false:." --go_out="." proto/*.proto
protoc --go-grpc_out="require_unimplemented_servers=false:." --go_out="." proto/*.proto
rm -rf ../agent/proto
cp -r proto ../agent
+4 -4
View File
@@ -10,19 +10,19 @@ import (
"github.com/naiba/nezha/service/singleton"
)
type AuthHandler struct {
type authHandler struct {
ClientSecret string
}
func (a *AuthHandler) GetRequestMetadata(ctx context.Context, uri ...string) (map[string]string, error) {
func (a *authHandler) GetRequestMetadata(ctx context.Context, uri ...string) (map[string]string, error) {
return map[string]string{"client_secret": a.ClientSecret}, nil
}
func (a *AuthHandler) RequireTransportSecurity() bool {
func (a *authHandler) RequireTransportSecurity() bool {
return false
}
func (a *AuthHandler) Check(ctx context.Context) (uint64, error) {
func (a *authHandler) Check(ctx context.Context) (uint64, error) {
md, ok := metadata.FromIncomingContext(ctx)
if !ok {
return 0, status.Errorf(codes.Unauthenticated, "获取 metaData 失败")
+140
View File
@@ -0,0 +1,140 @@
package rpc
import (
"errors"
"io"
"sync/atomic"
"time"
)
type ioStreamContext struct {
userIo io.ReadWriteCloser
agentIo io.ReadWriteCloser
userIoConnectCh chan struct{}
agentIoConnectCh chan struct{}
}
func (s *NezhaHandler) CreateStream(streamId string) {
s.ioStreamMutex.Lock()
defer s.ioStreamMutex.Unlock()
s.ioStreams[streamId] = &ioStreamContext{
userIoConnectCh: make(chan struct{}),
agentIoConnectCh: make(chan struct{}),
}
}
func (s *NezhaHandler) GetStream(streamId string) (*ioStreamContext, error) {
s.ioStreamMutex.RLock()
defer s.ioStreamMutex.RUnlock()
if ctx, ok := s.ioStreams[streamId]; ok {
return ctx, nil
}
return nil, errors.New("stream not found")
}
func (s *NezhaHandler) CloseStream(streamId string) error {
s.ioStreamMutex.Lock()
defer s.ioStreamMutex.Unlock()
if ctx, ok := s.ioStreams[streamId]; ok {
if ctx.userIo != nil {
ctx.userIo.Close()
}
if ctx.agentIo != nil {
ctx.agentIo.Close()
}
delete(s.ioStreams, streamId)
}
return nil
}
func (s *NezhaHandler) UserConnected(streamId string, userIo io.ReadWriteCloser) error {
stream, err := s.GetStream(streamId)
if err != nil {
return err
}
stream.userIo = userIo
close(stream.userIoConnectCh)
return nil
}
func (s *NezhaHandler) AgentConnected(streamId string, agentIo io.ReadWriteCloser) error {
stream, err := s.GetStream(streamId)
if err != nil {
return err
}
stream.agentIo = agentIo
close(stream.agentIoConnectCh)
return nil
}
func (s *NezhaHandler) StartStream(streamId string, timeout time.Duration) error {
stream, err := s.GetStream(streamId)
if err != nil {
return err
}
timeoutTimer := time.NewTimer(timeout)
LOOP:
for {
select {
case <-stream.userIoConnectCh:
if stream.agentIo != nil {
timeoutTimer.Stop()
break LOOP
}
case <-stream.agentIoConnectCh:
if stream.userIo != nil {
timeoutTimer.Stop()
break LOOP
}
case <-time.After(timeout):
break LOOP
}
time.Sleep(time.Millisecond * 500)
}
if stream.userIo == nil && stream.agentIo == nil {
return errors.New("timeout: no connection established")
}
if stream.userIo == nil {
return errors.New("timeout: user connection not established")
}
if stream.agentIo == nil {
return errors.New("timeout: agent connection not established")
}
isDone := new(atomic.Bool)
endCh := make(chan struct{})
go func() {
_, innerErr := io.Copy(stream.userIo, stream.agentIo)
if innerErr != nil {
err = innerErr
}
if isDone.CompareAndSwap(false, true) {
close(endCh)
}
}()
go func() {
_, innerErr := io.Copy(stream.agentIo, stream.userIo)
if innerErr != nil {
err = innerErr
}
if isDone.CompareAndSwap(false, true) {
close(endCh)
}
}()
<-endCh
return err
}
+77 -4
View File
@@ -3,11 +3,16 @@ package rpc
import (
"context"
"fmt"
"github.com/naiba/nezha/pkg/ddns"
"github.com/naiba/nezha/pkg/utils"
"log"
"net"
"sync"
"time"
"github.com/naiba/nezha/pkg/ddns"
"github.com/naiba/nezha/pkg/geoip"
"github.com/naiba/nezha/pkg/grpcx"
"github.com/naiba/nezha/pkg/utils"
"github.com/jinzhu/copier"
"github.com/nicksnyder/go-i18n/v2/i18n"
@@ -16,8 +21,20 @@ import (
"github.com/naiba/nezha/service/singleton"
)
var NezhaHandlerSingleton *NezhaHandler
type NezhaHandler struct {
Auth *AuthHandler
Auth *authHandler
ioStreams map[string]*ioStreamContext
ioStreamMutex *sync.RWMutex
}
func NewNezhaHandler() *NezhaHandler {
return &NezhaHandler{
Auth: &authHandler{},
ioStreamMutex: new(sync.RWMutex),
ioStreams: make(map[string]*ioStreamContext),
}
}
func (s *NezhaHandler) ReportTask(c context.Context, r *pb.TaskResult) (*pb.Receipt, error) {
@@ -121,7 +138,6 @@ func (s *NezhaHandler) ReportSystemInfo(c context.Context, r *pb.Host) (*pb.Rece
singleton.ServerList[clientID].Host != nil &&
host.IP != "" &&
singleton.ServerList[clientID].Host.IP != host.IP {
serverDomain := singleton.ServerList[clientID].DDNSDomain
if singleton.Conf.DDNS.Provider == "" {
provider, err = singleton.GetDDNSProviderFromProfile(singleton.ServerList[clientID].DDNSProfile)
@@ -174,6 +190,63 @@ func (s *NezhaHandler) ReportSystemInfo(c context.Context, r *pb.Host) (*pb.Rece
singleton.ServerList[clientID].PrevHourlyTransferOut = singleton.ServerList[clientID].PrevHourlyTransferOut - int64(singleton.ServerList[clientID].State.NetOutTransfer)
}
// 不要冲掉国家码
if singleton.ServerList[clientID].Host != nil {
host.CountryCode = singleton.ServerList[clientID].Host.CountryCode
}
singleton.ServerList[clientID].Host = &host
return &pb.Receipt{Proced: true}, nil
}
func (s *NezhaHandler) IOStream(stream pb.NezhaService_IOStreamServer) error {
if _, err := s.Auth.Check(stream.Context()); err != nil {
return err
}
id, err := stream.Recv()
if err != nil {
return err
}
if id == nil || len(id.Data) < 4 || (id.Data[0] != 0xff && id.Data[1] != 0x05 && id.Data[2] != 0xff && id.Data[3] == 0x05) {
return fmt.Errorf("invalid stream id")
}
streamId := string(id.Data[4:])
if _, err := s.GetStream(streamId); err != nil {
return err
}
iw := grpcx.NewIOStreamWrapper(stream)
if err := s.AgentConnected(streamId, iw); err != nil {
return err
}
iw.Wait()
return nil
}
func (s *NezhaHandler) LookupGeoIP(c context.Context, r *pb.GeoIP) (*pb.GeoIP, error) {
var clientID uint64
var err error
if clientID, err = s.Auth.Check(c); err != nil {
return nil, err
}
// 根据内置数据库查询 IP 地理位置
record := &geoip.IPInfo{}
ip := r.GetIp()
netIP := net.ParseIP(ip)
location, err := geoip.Lookup(netIP, record)
if err != nil {
return nil, err
}
// 将地区码写入到 Host
singleton.ServerLock.RLock()
defer singleton.ServerLock.RUnlock()
if singleton.ServerList[clientID].Host == nil {
return nil, fmt.Errorf("host not found")
}
singleton.ServerList[clientID].Host.CountryCode = location
return &pb.GeoIP{Ip: ip, CountryCode: location}, nil
}
+17 -17
View File
@@ -15,8 +15,6 @@ var (
ServerAPI = &ServerAPIService{}
MonitorAPI = &MonitorAPIService{}
once = &sync.Once{}
)
type ServerAPIService struct{}
@@ -28,13 +26,14 @@ type CommonResponse struct {
}
type CommonServerInfo struct {
ID uint64 `json:"id"`
Name string `json:"name"`
Tag string `json:"tag"`
LastActive int64 `json:"last_active"`
IPV4 string `json:"ipv4"`
IPV6 string `json:"ipv6"`
ValidIP string `json:"valid_ip"`
ID uint64 `json:"id"`
Name string `json:"name"`
Tag string `json:"tag"`
LastActive int64 `json:"last_active"`
IPV4 string `json:"ipv4"`
IPV6 string `json:"ipv6"`
ValidIP string `json:"valid_ip"`
DisplayIndex int `json:"display_index"`
}
// StatusResponse 服务器状态子结构 包含服务器信息与状态信息
@@ -78,7 +77,7 @@ func InitAPI() {
UserIDToApiTokenList = make(map[uint64][]string)
}
func LoadAPI() {
func loadAPI() {
InitAPI()
var tokenList []*model.ApiToken
DB.Find(&tokenList)
@@ -143,13 +142,14 @@ func (s *ServerAPIService) GetAllStatus() *ServerStatusResponse {
}
ipv4, ipv6, validIP := utils.SplitIPAddr(host.IP)
info := CommonServerInfo{
ID: v.ID,
Name: v.Name,
Tag: v.Tag,
LastActive: v.LastActive.Unix(),
IPV4: ipv4,
IPV6: ipv6,
ValidIP: validIP,
ID: v.ID,
Name: v.Name,
Tag: v.Tag,
LastActive: v.LastActive.Unix(),
IPV4: ipv4,
IPV6: ipv6,
ValidIP: validIP,
DisplayIndex: v.DisplayIndex,
}
res.Result = append(res.Result, &StatusResponse{
CommonServerInfo: info,
+2 -2
View File
@@ -24,8 +24,8 @@ func InitCronTask() {
Crons = make(map[uint64]*model.Cron)
}
// LoadCronTasks 加载计划任务
func LoadCronTasks() {
// loadCronTasks 加载计划任务
func loadCronTasks() {
InitCronTask()
var crons []model.Cron
DB.Find(&crons)
+14 -14
View File
@@ -1,10 +1,10 @@
package singleton
import (
"errors"
"fmt"
ddns2 "github.com/naiba/nezha/pkg/ddns"
"log"
ddns2 "github.com/naiba/nezha/pkg/ddns"
)
func RetryableUpdateDomain(provider ddns2.Provider, config *ddns2.DomainConfig, maxRetries int) bool {
@@ -25,54 +25,54 @@ func RetryableUpdateDomain(provider ddns2.Provider, config *ddns2.DomainConfig,
func GetDDNSProviderFromString(provider string) (ddns2.Provider, error) {
switch provider {
case "webhook":
return ddns2.ProviderWebHook{
return &ddns2.ProviderWebHook{
URL: Conf.DDNS.WebhookURL,
RequestMethod: Conf.DDNS.WebhookMethod,
RequestBody: Conf.DDNS.WebhookRequestBody,
RequestHeader: Conf.DDNS.WebhookHeaders,
}, nil
case "dummy":
return ddns2.ProviderDummy{}, nil
return &ddns2.ProviderDummy{}, nil
case "cloudflare":
return ddns2.ProviderCloudflare{
return &ddns2.ProviderCloudflare{
Secret: Conf.DDNS.AccessSecret,
}, nil
case "tencentcloud":
return ddns2.ProviderTencentCloud{
return &ddns2.ProviderTencentCloud{
SecretID: Conf.DDNS.AccessID,
SecretKey: Conf.DDNS.AccessSecret,
}, nil
}
return ddns2.ProviderDummy{}, errors.New(fmt.Sprintf("无法找到配置的DDNS提供者%s", Conf.DDNS.Provider))
return &ddns2.ProviderDummy{}, fmt.Errorf("无法找到配置的DDNS提供者%s", Conf.DDNS.Provider)
}
func GetDDNSProviderFromProfile(profileName string) (ddns2.Provider, error) {
profile, ok := Conf.DDNS.Profiles[profileName]
if !ok {
return ddns2.ProviderDummy{}, errors.New(fmt.Sprintf("未找到配置项 %s", profileName))
return &ddns2.ProviderDummy{}, fmt.Errorf("未找到配置项 %s", profileName)
}
switch profile.Provider {
case "webhook":
return ddns2.ProviderWebHook{
return &ddns2.ProviderWebHook{
URL: profile.WebhookURL,
RequestMethod: profile.WebhookMethod,
RequestBody: profile.WebhookRequestBody,
RequestHeader: profile.WebhookHeaders,
}, nil
case "dummy":
return ddns2.ProviderDummy{}, nil
return &ddns2.ProviderDummy{}, nil
case "cloudflare":
return ddns2.ProviderCloudflare{
return &ddns2.ProviderCloudflare{
Secret: profile.AccessSecret,
}, nil
case "tencentcloud":
return ddns2.ProviderTencentCloud{
return &ddns2.ProviderTencentCloud{
SecretID: profile.AccessID,
SecretKey: profile.AccessSecret,
}, nil
}
return ddns2.ProviderDummy{}, errors.New(fmt.Sprintf("无法找到配置的DDNS提供者%s", profile.Provider))
return &ddns2.ProviderDummy{}, fmt.Errorf("无法找到配置的DDNS提供者%s", profile.Provider)
}
func ValidateDDNSProvidersFromProfiles() error {
@@ -80,7 +80,7 @@ func ValidateDDNSProvidersFromProfiles() error {
providers := make(map[string]string)
for profileName, profile := range Conf.DDNS.Profiles {
if _, ok := validProviders[profile.Provider]; !ok {
return errors.New(fmt.Sprintf("无法找到配置的DDNS提供者%s", profile.Provider))
return fmt.Errorf("无法找到配置的DDNS提供者%s", profile.Provider)
}
providers[profileName] = profile.Provider
}
+31
View File
@@ -0,0 +1,31 @@
package singleton
import (
"sync"
"github.com/naiba/nezha/model"
)
var natCache = make(map[string]*model.NAT)
var natCacheRwLock = new(sync.RWMutex)
func initNAT() {
OnNATUpdate()
}
func OnNATUpdate() {
natCacheRwLock.Lock()
defer natCacheRwLock.Unlock()
var nats []*model.NAT
DB.Find(&nats)
natCache = make(map[string]*model.NAT)
for i := 0; i < len(nats); i++ {
natCache[nats[i].Domain] = nats[i]
}
}
func GetNATConfigByDomain(domain string) *model.NAT {
natCacheRwLock.RLock()
defer natCacheRwLock.RUnlock()
return natCache[domain]
}
+2 -2
View File
@@ -24,8 +24,8 @@ func InitNotification() {
NotificationIDToTag = make(map[uint64]string)
}
// LoadNotifications 从 DB 初始化通知方式相关参数
func LoadNotifications() {
// loadNotifications 从 DB 初始化通知方式相关参数
func loadNotifications() {
InitNotification()
notificationsLock.Lock()
defer notificationsLock.Unlock()
+2 -2
View File
@@ -25,8 +25,8 @@ func InitServer() {
ServerTagToIDList = make(map[string][]uint64)
}
// LoadServers 加载服务器列表并根据ID排序
func LoadServers() {
// loadServers 加载服务器列表并根据ID排序
func loadServers() {
InitServer()
var servers []model.Server
DB.Find(&servers)
+14 -12
View File
@@ -34,10 +34,11 @@ func InitTimezoneAndCache() {
// LoadSingleton 加载子服务并执行
func LoadSingleton() {
LoadNotifications() // 加载通知服务
LoadServers() // 加载服务器列表
LoadCronTasks() // 加载定时任务
LoadAPI()
loadNotifications() // 加载通知服务
loadServers() // 加载服务器列表
loadCronTasks() // 加载定时任务
loadAPI()
initNAT()
}
// InitConfigFromPath 从给出的文件路径中加载配置
@@ -47,11 +48,11 @@ func InitConfigFromPath(path string) {
if err != nil {
panic(err)
}
ValidateConfig()
validateConfig()
}
// ValidateConfig 验证配置文件有效性
func ValidateConfig() {
// validateConfig 验证配置文件有效性
func validateConfig() {
var err error
if Conf.DDNS.Provider == "" {
err = ValidateDDNSProvidersFromProfiles()
@@ -82,7 +83,8 @@ func InitDBFromPath(path string) {
}
err = DB.AutoMigrate(model.Server{}, model.User{},
model.Notification{}, model.AlertRule{}, model.Monitor{},
model.MonitorHistory{}, model.Cron{}, model.Transfer{}, model.ApiToken{})
model.MonitorHistory{}, model.Cron{}, model.Transfer{},
model.ApiToken{}, model.NAT{})
if err != nil {
panic(err)
}
@@ -93,7 +95,7 @@ func RecordTransferHourlyUsage() {
ServerLock.Lock()
defer ServerLock.Unlock()
now := time.Now()
nowTrimSeconds := time.Date(now.Year(), now.Month(), now.Day(), now.Hour(), 0, 0, 0, Loc)
nowTrimSeconds := time.Date(now.Year(), now.Month(), now.Day(), now.Hour(), 0, 0, 0, now.Location())
var txs []model.Transfer
for id, server := range ServerList {
tx := model.Transfer{
@@ -136,7 +138,7 @@ func CleanMonitorHistory() {
if !rule.IsTransferDurationRule() {
continue
}
dataCouldRemoveBefore := rule.GetTransferDurationStart()
dataCouldRemoveBefore := rule.GetTransferDurationStart().UTC()
// 判断规则影响的机器范围
if rule.Cover == model.RuleCoverAll {
// 更新全局可以清理的数据点
@@ -155,12 +157,12 @@ func CleanMonitorHistory() {
}
}
for id, couldRemove := range specialServerKeep {
DB.Unscoped().Delete(&model.Transfer{}, "server_id = ? AND created_at < ?", id, couldRemove)
DB.Unscoped().Delete(&model.Transfer{}, "server_id = ? AND datetime(`created_at`) < datetime(?)", id, couldRemove)
}
if allServerKeep.IsZero() {
DB.Unscoped().Delete(&model.Transfer{}, "server_id NOT IN (?)", specialServerIDs)
} else {
DB.Unscoped().Delete(&model.Transfer{}, "server_id NOT IN (?) AND created_at < ?", specialServerIDs, allServerKeep)
DB.Unscoped().Delete(&model.Transfer{}, "server_id NOT IN (?) AND datetime(`created_at`) < datetime(?)", specialServerIDs, allServerKeep)
}
}