Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
61fdcd2a9e | ||
|
|
a474b8484b | ||
|
|
67b003a27d | ||
|
|
99aeda7939 | ||
|
|
d5744d0c73 | ||
|
|
e7837d8eaf | ||
|
|
aa81fc02ab | ||
|
|
f6531a52bd | ||
|
|
c58c4f866a | ||
|
|
fc9f1b6bcc | ||
|
|
be7b6e9c5e | ||
|
|
a503f0cf40 | ||
|
|
0b7f43b149 | ||
|
|
55f5c89c1c | ||
|
|
937696c26d | ||
|
|
08aaea8244 | ||
|
|
b886f70e48 | ||
|
|
12c6b8a6f6 | ||
|
|
ebcc8a6efb | ||
|
|
c4d8b63d32 | ||
|
|
318da135b9 | ||
|
|
da74bf4749 | ||
|
|
3250283ef9 | ||
|
|
2b40040ec5 | ||
|
|
60ada33ee9 | ||
|
|
7940e010e0 | ||
|
|
2df57e6249 | ||
|
|
106d58575b | ||
|
|
bdf36276da | ||
|
|
f32f127dfc | ||
|
|
f78ba281fb | ||
|
|
7f9f9a9129 | ||
|
|
b701efd9b5 | ||
|
|
511f43784e | ||
|
|
3f6112a919 | ||
|
|
23751440c6 | ||
|
|
989c9f9740 | ||
|
|
71508f8724 | ||
|
|
eb6dd2855e | ||
|
|
64da3c7438 | ||
|
|
fd85f599ba | ||
|
|
cae443d5c8 | ||
|
|
0ec1bb2c54 | ||
|
|
47f8447a22 | ||
|
|
9c986d06cb | ||
|
|
e9428d5757 | ||
|
|
daab64d232 | ||
|
|
da8fb57268 | ||
|
|
fda0dad5f1 | ||
|
|
c6fb3196ea | ||
|
|
711d162a46 | ||
|
|
d667ce8afe | ||
|
|
6abc0fce51 | ||
|
|
b1d77a1d27 | ||
|
|
c18e0e420e | ||
|
|
d9097540c3 | ||
|
|
6b05a21641 | ||
|
|
f289b76f47 | ||
|
|
89fe4ec621 | ||
|
|
fca2f60380 | ||
|
|
6a70e67ef9 | ||
|
|
22842ebc75 | ||
|
|
aef968250f | ||
|
|
100600d594 | ||
|
|
fe234f4f38 | ||
|
|
df9487b5fc | ||
|
|
26d81f6d7a | ||
|
|
9d96c58ff9 | ||
|
|
5bb7efdeb9 | ||
|
|
519882c4a9 | ||
|
|
ed96432b79 | ||
|
|
1c057d93bd | ||
|
|
6005004681 | ||
|
|
d8f4236404 | ||
|
|
f95191c8af | ||
|
|
34fedd91d2 | ||
|
|
f675b884f5 | ||
|
|
0b13fec029 | ||
|
|
67b788a969 | ||
|
|
b63f693661 | ||
|
|
ac64aa7355 | ||
|
|
05d69d5c07 | ||
|
|
ce624a0ca6 | ||
|
|
1c91fcffac | ||
|
|
417f972659 | ||
|
|
ada344cd6b | ||
|
|
9d6682fd58 | ||
|
|
22c687c044 | ||
|
|
e1513203b4 | ||
|
|
17373e2dee | ||
|
|
e7c6984c72 |
+135
@@ -0,0 +1,135 @@
|
||||
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 delete_gitee_releases(latest_id, client, uri, token):
|
||||
get_data = {
|
||||
'access_token': token
|
||||
}
|
||||
|
||||
release_info = []
|
||||
release_response = client.get(uri, json=get_data)
|
||||
if release_response.status_code == 200:
|
||||
release_info = release_response.json()
|
||||
else:
|
||||
print(
|
||||
f"Request failed with status code {release_response.status_code}")
|
||||
|
||||
release_ids = []
|
||||
for block in release_info:
|
||||
if 'id' in block:
|
||||
release_ids.append(block['id'])
|
||||
|
||||
print(f'Current release ids: {release_ids}')
|
||||
release_ids.remove(latest_id)
|
||||
|
||||
for id in release_ids:
|
||||
release_uri = f"{uri}/{id}"
|
||||
delete_data = {
|
||||
'access_token': token
|
||||
}
|
||||
delete_response = client.delete(release_uri, json=delete_data)
|
||||
if delete_response.status_code == 204:
|
||||
print(f'Successfully deleted release #{id}.')
|
||||
else:
|
||||
raise ValueError(
|
||||
f"Request failed with status code {delete_response.status_code}")
|
||||
|
||||
|
||||
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:
|
||||
success = False
|
||||
|
||||
while not success:
|
||||
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}!")
|
||||
success = True
|
||||
else:
|
||||
print(
|
||||
f"Request failed with status code {asset_api_response.status_code}")
|
||||
|
||||
# 仅保留最新 Release 以防超出 Gitee 仓库配额
|
||||
try:
|
||||
delete_gitee_releases(release_id, api_client,
|
||||
release_api_uri, access_token)
|
||||
except ValueError as e:
|
||||
print(e)
|
||||
|
||||
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()
|
||||
+123
-56
@@ -4,46 +4,139 @@ on:
|
||||
push:
|
||||
tags:
|
||||
- "v*"
|
||||
workflow_dispatch:
|
||||
|
||||
jobs:
|
||||
build:
|
||||
strategy:
|
||||
fail-fast: true
|
||||
matrix:
|
||||
goos: [linux, windows]
|
||||
goarch: [amd64]
|
||||
include:
|
||||
- goos: linux
|
||||
goarch: s390x
|
||||
- goos: linux
|
||||
goarch: arm64
|
||||
|
||||
name: Build artifacts
|
||||
runs-on: ubuntu-latest
|
||||
container:
|
||||
image: goreleaser/goreleaser-cross:v1.23
|
||||
env:
|
||||
GOOS: ${{ matrix.goos }}
|
||||
GOARCH: ${{ matrix.goarch }}
|
||||
GOARM: ${{ matrix.goarm }}
|
||||
outputs:
|
||||
tag_name: ${{ steps.extract_branch.outputs.tag }}
|
||||
steps:
|
||||
- run: git config --global --add safe.directory /__w/nezha/nezha
|
||||
- uses: actions/checkout@v4
|
||||
|
||||
- name: Fetch IPInfo GeoIP Database
|
||||
if: github.event_name == 'push' && contains(github.ref, 'refs/tags/')
|
||||
env:
|
||||
IPINFO_TOKEN: ${{ secrets.IPINFO_TOKEN }}
|
||||
run: |
|
||||
rm pkg/geoip/geoip.db
|
||||
wget -qO pkg/geoip/geoip.db https://ipinfo.io/data/free/country.mmdb?token=${IPINFO_TOKEN}
|
||||
|
||||
- name: Set up Go
|
||||
uses: actions/setup-go@v5
|
||||
with:
|
||||
go-version: "1.23.x"
|
||||
|
||||
- name: Build
|
||||
uses: goreleaser/goreleaser-action@v6
|
||||
with:
|
||||
distribution: goreleaser
|
||||
version: '~> v2'
|
||||
args: build --single-target --clean --skip=validate
|
||||
|
||||
- name: Upload artifacts
|
||||
uses: actions/upload-artifact@v4
|
||||
with:
|
||||
name: dashboard-${{ env.GOOS }}-${{ env.GOARCH }}
|
||||
path: |
|
||||
./dist/*/*
|
||||
|
||||
release:
|
||||
runs-on: ubuntu-latest
|
||||
if: github.event_name == 'push' && contains(github.ref, 'refs/tags/')
|
||||
needs: build
|
||||
name: Release
|
||||
steps:
|
||||
- name: Download artifacts
|
||||
uses: actions/download-artifact@v4
|
||||
with:
|
||||
path: ./assets
|
||||
|
||||
- name: Archive and compress binaries
|
||||
run: |
|
||||
for file in assets/*/*/*; do
|
||||
if [ -f "$file" ]; then
|
||||
chmod +x "$file"
|
||||
export fileWithoutExt=${file%.*}
|
||||
zip -jr "$fileWithoutExt.zip" "$file"
|
||||
fi
|
||||
done
|
||||
|
||||
- name: Release
|
||||
uses: ncipollo/release-action@v1
|
||||
with:
|
||||
artifacts: "assets/*/*/*.zip"
|
||||
generateReleaseNotes: true
|
||||
|
||||
- name: Purge jsdelivr cache
|
||||
run: |
|
||||
curl -s https://purge.jsdelivr.net/gh/${{ github.repository_owner }}/nezha@master/script/install.sh
|
||||
curl -s https://purge.jsdelivr.net/gh/${{ github.repository_owner }}/nezha@master/script/nezha-agent.service
|
||||
curl -s https://purge.jsdelivr.net/gh/${{ github.repository_owner }}/nezha@master/script/docker-compose.yaml
|
||||
curl -s https://purge.jsdelivr.net/gh/${{ github.repository_owner }}/nezha@master/script/config.yaml
|
||||
LOWER_USERNAME=$(echo "${{ github.repository_owner }}" | tr '[:upper:]' '[:lower:]')
|
||||
curl -s https://purge.jsdelivr.net/gh/$LOWER_USERNAME/nezha@master/script/install.sh
|
||||
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
|
||||
env:
|
||||
GH_REPO: ${{ github.repository }}
|
||||
GH_TOKEN: ${{ github.token }}
|
||||
GH_DEBUG: api
|
||||
run: |
|
||||
gh workflow run sync-release.yml
|
||||
|
||||
release-docker:
|
||||
runs-on: ubuntu-latest
|
||||
if: github.event_name == 'push' && contains(github.ref, 'refs/tags/')
|
||||
needs: build
|
||||
name: Release Docker images
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
|
||||
- name: Download artifacts
|
||||
uses: actions/download-artifact@v4
|
||||
with:
|
||||
path: ./assets
|
||||
|
||||
- name: Fix permissions
|
||||
run: |
|
||||
chmod -R +x ./assets/*
|
||||
mkdir dist
|
||||
mv ./assets/*/*/* ./dist
|
||||
|
||||
- name: Extract branch name
|
||||
run: |
|
||||
export TAG_NAME=$(echo ${GITHUB_REF#refs/tags/})
|
||||
echo "tag=$TAG_NAME" >> $GITHUB_OUTPUT
|
||||
id: extract_branch
|
||||
|
||||
- name: xgo build
|
||||
uses: crazy-max/ghaction-xgo@v2
|
||||
with:
|
||||
xgo_version: latest
|
||||
go_version: 1.20.x
|
||||
dest: dist
|
||||
pkg: cmd/dashboard
|
||||
prefix: dashboard
|
||||
targets: linux/amd64,linux/arm64,linux/arm-7,linux/s390x,linux/riscv64,windows/amd64,windows/386,windows/arm64 # linux/386,
|
||||
v: true
|
||||
x: false
|
||||
race: false
|
||||
tags: timetzdata
|
||||
ldflags: -s -w --extldflags '-static -fpic' -X github.com/naiba/nezha/service/singleton.Version=${{ steps.extract_branch.outputs.tag }}
|
||||
buildmode: default
|
||||
|
||||
- name: fix dist
|
||||
run: |
|
||||
mv dist/dashboard-linux-arm-7 dist/dashboard-linux-arm
|
||||
|
||||
- name: Log in to the GHCR
|
||||
|
||||
- name: Log into GHCR
|
||||
uses: docker/login-action@master
|
||||
with:
|
||||
registry: ghcr.io
|
||||
username: ${{ github.repository_owner }}
|
||||
password: ${{ secrets.GITHUB_TOKEN }}
|
||||
password: ${{ github.token }}
|
||||
|
||||
- name: Log in to the AliyunCS
|
||||
uses: docker/login-action@master
|
||||
@@ -60,13 +153,13 @@ jobs:
|
||||
|
||||
- name: Set up image name
|
||||
run: |
|
||||
GHRC_IMAGE_NAME=$(echo "ghcr.io/${{ github.repository_owner }}/nezha-dashboard" | tr '[:upper:]' '[:lower:]')
|
||||
GHCR_IMAGE_NAME=$(echo "ghcr.io/${{ github.repository_owner }}/nezha-dashboard" | tr '[:upper:]' '[:lower:]')
|
||||
if [ ${{ github.repository_owner }} = "naiba" ]
|
||||
then ALI_IMAGE_NAME=$(echo "registry.cn-shanghai.aliyuncs.com/naibahq/nezha-dashboard")
|
||||
else ALI_IMAGE_NAME=$(echo "registry.cn-shanghai.aliyuncs.com/${{ github.repository_owner }}/nezha-dashboard" | tr '[:upper:]' '[:lower:]')
|
||||
fi
|
||||
echo "::set-output name=GHRC_IMAGE_NAME::$GHRC_IMAGE_NAME"
|
||||
echo "::set-output name=ALI_IMAGE_NAME::$ALI_IMAGE_NAME"
|
||||
echo "GHCR_IMAGE_NAME=$GHCR_IMAGE_NAME" >> $GITHUB_OUTPUT
|
||||
echo "ALI_IMAGE_NAME=$ALI_IMAGE_NAME" >> $GITHUB_OUTPUT
|
||||
id: image-name
|
||||
|
||||
- name: Build dasbboard image And Push
|
||||
@@ -74,36 +167,10 @@ jobs:
|
||||
with:
|
||||
context: .
|
||||
file: ./Dockerfile
|
||||
platforms: linux/amd64,linux/arm64,linux/arm,linux/s390x,linux/riscv64 # linux/386,
|
||||
platforms: linux/amd64,linux/arm64,linux/s390x
|
||||
push: true
|
||||
tags: |
|
||||
${{ steps.image-name.outputs.GHRC_IMAGE_NAME }}:latest
|
||||
${{ steps.image-name.outputs.GHRC_IMAGE_NAME }}:${{ steps.extract_branch.outputs.tag }}
|
||||
${{ steps.image-name.outputs.GHCR_IMAGE_NAME }}:latest
|
||||
${{ steps.image-name.outputs.GHCR_IMAGE_NAME }}:${{ steps.extract_branch.outputs.tag }}
|
||||
${{ steps.image-name.outputs.ALI_IMAGE_NAME }}:latest
|
||||
${{ steps.image-name.outputs.ALI_IMAGE_NAME }}:${{ steps.extract_branch.outputs.tag }}
|
||||
|
||||
- name: Compress dist files
|
||||
run: |
|
||||
for file in dist/*; do
|
||||
if [ -f "$file" ]; then
|
||||
zip -r "$file.zip" "$file"
|
||||
fi
|
||||
done
|
||||
|
||||
- name: Release
|
||||
uses: ncipollo/release-action@v1
|
||||
with:
|
||||
artifacts: "dist/*.zip"
|
||||
generateReleaseNotes: true
|
||||
|
||||
- name: Purge jsdelivr cache
|
||||
run: |
|
||||
curl -s https://purge.jsdelivr.net/gh/${{ github.repository_owner }}/nezha@master/script/install.sh
|
||||
curl -s https://purge.jsdelivr.net/gh/${{ github.repository_owner }}/nezha@master/script/nezha-agent.service
|
||||
curl -s https://purge.jsdelivr.net/gh/${{ github.repository_owner }}/nezha@master/script/docker-compose.yaml
|
||||
curl -s https://purge.jsdelivr.net/gh/${{ github.repository_owner }}/nezha@master/script/config.yaml
|
||||
LOWER_USERNAME=$(echo "${{ github.repository_owner }}" | tr '[:upper:]' '[:lower:]')
|
||||
curl -s https://purge.jsdelivr.net/gh/$LOWER_USERNAME/nezha@master/script/install.sh
|
||||
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
|
||||
|
||||
@@ -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
|
||||
@@ -1,39 +0,0 @@
|
||||
name: Run Tests on PR
|
||||
|
||||
on:
|
||||
pull_request:
|
||||
branches:
|
||||
- master
|
||||
jobs:
|
||||
tests:
|
||||
runs-on: ubuntu-latest
|
||||
env:
|
||||
GO111MODULE: on
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
- uses: actions/setup-go@v4
|
||||
with:
|
||||
go-version: "^1.21.3"
|
||||
- name: Unit test
|
||||
run: |
|
||||
go test -v ./...
|
||||
- name: Run Gosec Security Scanner
|
||||
uses: securego/gosec@master
|
||||
with:
|
||||
args: --exclude=G104,G402 ./...
|
||||
- name: xgo build
|
||||
uses: crazy-max/ghaction-xgo@v2
|
||||
with:
|
||||
xgo_version: latest
|
||||
go_version: 1.20.x
|
||||
dest: dist
|
||||
pkg: cmd/dashboard
|
||||
prefix: dashboard
|
||||
targets: linux/amd64,linux/arm64,linux/arm-7,linux/s390x,linux/riscv64,windows/amd64,windows/386,windows/arm64 # linux/386,
|
||||
v: true
|
||||
x: false
|
||||
race: false
|
||||
tags: timetzdata
|
||||
ldflags: -s -w --extldflags '-static -fpic' -X github.com/naiba/nezha/service/singleton.Version=test
|
||||
buildmode: default
|
||||
|
||||
+19
-19
@@ -10,36 +10,36 @@ on:
|
||||
- "go.sum"
|
||||
- "resource/**"
|
||||
- ".github/workflows/test.yml"
|
||||
pull_request:
|
||||
branches:
|
||||
- master
|
||||
|
||||
jobs:
|
||||
tests:
|
||||
runs-on: ubuntu-latest
|
||||
strategy:
|
||||
fail-fast: true
|
||||
matrix:
|
||||
os: [ubuntu, windows, macos]
|
||||
|
||||
runs-on: ${{ matrix.os }}-latest
|
||||
env:
|
||||
GO111MODULE: on
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
- uses: actions/setup-go@v4
|
||||
|
||||
- uses: actions/setup-go@v5
|
||||
with:
|
||||
go-version: "^1.21.3"
|
||||
go-version: "1.23.x"
|
||||
|
||||
- name: Unit test
|
||||
run: |
|
||||
go test -v ./...
|
||||
|
||||
- name: Build test
|
||||
run: go build -v ./cmd/dashboard
|
||||
|
||||
- name: Run Gosec Security Scanner
|
||||
if: runner.os == 'Linux'
|
||||
uses: securego/gosec@master
|
||||
with:
|
||||
args: --exclude=G104,G402 ./...
|
||||
- name: xgo build
|
||||
uses: crazy-max/ghaction-xgo@v2
|
||||
with:
|
||||
xgo_version: latest
|
||||
go_version: 1.20.x
|
||||
dest: dist
|
||||
pkg: cmd/dashboard
|
||||
prefix: dashboard
|
||||
targets: linux/amd64,linux/arm64,linux/arm-7,linux/s390x,linux/riscv64,windows/amd64,windows/386,windows/arm64 # linux/386,
|
||||
v: true
|
||||
x: false
|
||||
race: false
|
||||
tags: timetzdata
|
||||
ldflags: -s -w --extldflags '-static -fpic' -X github.com/naiba/nezha/service/singleton.Version=test
|
||||
buildmode: default
|
||||
args: --exclude=G104,G402,G115,G203 ./...
|
||||
|
||||
@@ -0,0 +1,71 @@
|
||||
version: 2
|
||||
before:
|
||||
hooks:
|
||||
- go mod tidy -v
|
||||
builds:
|
||||
- id: linux_arm64
|
||||
env:
|
||||
- CGO_ENABLED=1
|
||||
- CC=aarch64-linux-gnu-gcc
|
||||
ldflags:
|
||||
- -s -w
|
||||
- -X github.com/naiba/nezha/service/singleton.Version={{.Version}}
|
||||
- -extldflags "-static -fpic"
|
||||
flags:
|
||||
- -trimpath
|
||||
goos:
|
||||
- linux
|
||||
goarch:
|
||||
- arm64
|
||||
main: ./cmd/dashboard
|
||||
binary: dashboard-{{ .Os }}-{{ .Arch }}
|
||||
- id: linux_amd64
|
||||
env:
|
||||
- CGO_ENABLED=1
|
||||
- CC=x86_64-linux-gnu-gcc
|
||||
ldflags:
|
||||
- -s -w
|
||||
- -X github.com/naiba/nezha/service/singleton.Version={{.Version}}
|
||||
- -extldflags "-static -fpic"
|
||||
flags:
|
||||
- -trimpath
|
||||
goos:
|
||||
- linux
|
||||
goarch:
|
||||
- amd64
|
||||
main: ./cmd/dashboard
|
||||
binary: dashboard-{{ .Os }}-{{ .Arch }}
|
||||
- id: linux_s390x
|
||||
env:
|
||||
- CGO_ENABLED=1
|
||||
- CC=s390x-linux-gnu-gcc
|
||||
ldflags:
|
||||
- -s -w
|
||||
- -X github.com/naiba/nezha/service/singleton.Version={{.Version}}
|
||||
- -extldflags "-static -fpic"
|
||||
flags:
|
||||
- -trimpath
|
||||
goos:
|
||||
- linux
|
||||
goarch:
|
||||
- s390x
|
||||
main: ./cmd/dashboard
|
||||
binary: dashboard-{{ .Os }}-{{ .Arch }}
|
||||
- id: windows_amd64
|
||||
env:
|
||||
- CGO_ENABLED=1
|
||||
- CC=x86_64-w64-mingw32-gcc
|
||||
ldflags:
|
||||
- -s -w
|
||||
- -X github.com/naiba/nezha/service/singleton.Version={{.Version}}
|
||||
- -extldflags "-static -fpic"
|
||||
flags:
|
||||
- -trimpath
|
||||
goos:
|
||||
- windows
|
||||
goarch:
|
||||
- amd64
|
||||
main: ./cmd/dashboard
|
||||
binary: dashboard-{{ .Os }}-{{ .Arch }}
|
||||
snapshot:
|
||||
version_template: "dashboard"
|
||||
+7
-9
@@ -1,16 +1,12 @@
|
||||
# latest 镜像还没有 riscv64 https://hub.docker.com/_/alpine/tags
|
||||
FROM alpine:edge
|
||||
FROM alpine AS certs
|
||||
RUN apk update && apk add ca-certificates
|
||||
|
||||
FROM busybox:stable-musl
|
||||
|
||||
ARG TARGETOS
|
||||
ARG TARGETARCH
|
||||
|
||||
RUN apk update && \
|
||||
apk upgrade --no-cache && \
|
||||
apk add --no-cache tzdata && \
|
||||
cp /usr/share/zoneinfo/Asia/Shanghai /etc/localtime && \
|
||||
echo 'Asia/Shanghai' >/etc/timezone && \
|
||||
rm -rf /var/cache/apk/*
|
||||
|
||||
COPY --from=certs /etc/ssl/certs /etc/ssl/certs
|
||||
COPY ./script/entrypoint.sh /entrypoint.sh
|
||||
RUN chmod +x /entrypoint.sh
|
||||
|
||||
@@ -19,4 +15,6 @@ COPY dist/dashboard-${TARGETOS}-${TARGETARCH} ./app
|
||||
|
||||
VOLUME ["/dashboard/data"]
|
||||
EXPOSE 80 5555
|
||||
ARG TZ=Asia/Shanghai
|
||||
ENV TZ=$TZ
|
||||
ENTRYPOINT ["/entrypoint.sh"]
|
||||
@@ -4,7 +4,7 @@
|
||||
<br>
|
||||
<small><i>LOGO designed by <a href="https://xio.ng" target="_blank">熊大</a> .</i></small>
|
||||
<br><br>
|
||||
<img alt="GitHub release (with filter)" src="https://img.shields.io/github/v/release/naiba/nezha?color=brightgreen&style=for-the-badge&logo=github&label=Dashboard"> <img src="https://img.shields.io/github/v/release/nezhahq/agent?color=brightgreen&label=Agent&style=for-the-badge&logo=github"> <img src="https://img.shields.io/github/actions/workflow/status/nezhahq/agent/agent.yml?label=Agent%20CI&logo=github&style=for-the-badge"> <img src="https://img.shields.io/badge/Installer-v0.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"> <img src="https://img.shields.io/github/v/release/nezhahq/agent?color=brightgreen&label=Agent&style=for-the-badge&logo=github"> <img src="https://img.shields.io/github/actions/workflow/status/nezhahq/agent/agent.yml?label=Agent%20CI&logo=github&style=for-the-badge"> <img src="https://img.shields.io/badge/Installer-v0.20.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>
|
||||
@@ -43,10 +43,10 @@ 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/uubulb" title="UUBulb"><img src="https://avatars.githubusercontent.com/u/35923940?v=4" width="50;" alt="UUBulb"/></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/nap0o" title="nap0o"><img src="https://avatars.githubusercontent.com/u/144927971?v=4" width="50;" alt="nap0o"/></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/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/cantoblanco" title="Kris"><img src="https://avatars.githubusercontent.com/u/116849421?v=4" width="50;" alt="Kris"/></a>
|
||||
@@ -59,6 +59,7 @@ You can change the dashboard language in the settings page (`/setting`) after th
|
||||
<a href="https://github.com/1ridic" title="1ridic"><img src="https://avatars.githubusercontent.com/u/88495501?v=4" width="50;" alt="1ridic"/></a>
|
||||
<a href="https://github.com/Mmx233" title="Mmx"><img src="https://avatars.githubusercontent.com/u/36563672?v=4" width="50;" alt="Mmx"/></a>
|
||||
<a href="https://github.com/rootmelo92118" title="rootmelo92118"><img src="https://avatars.githubusercontent.com/u/32770959?v=4" width="50;" alt="rootmelo92118"/></a>
|
||||
<a href="https://github.com/zhucaidan" title="zhucaidan"><img src="https://avatars.githubusercontent.com/u/47970938?v=4" width="50;" alt="zhucaidan"/></a>
|
||||
<a href="https://github.com/iilemon" title="Sean"><img src="https://avatars.githubusercontent.com/u/33201711?v=4" width="50;" alt="Sean"/></a>
|
||||
<a href="https://github.com/fscarmen" title="fscarmen"><img src="https://avatars.githubusercontent.com/u/62703343?v=4" width="50;" alt="fscarmen"/></a>
|
||||
<a href="https://github.com/ch8o" title="no-name-now"><img src="https://avatars.githubusercontent.com/u/9103372?v=4" width="50;" alt="no-name-now"/></a>
|
||||
@@ -66,29 +67,37 @@ You can change the dashboard language in the settings page (`/setting`) after th
|
||||
<a href="https://github.com/DarcJC" title="Darc Z."><img src="https://avatars.githubusercontent.com/u/53445798?v=4" width="50;" alt="Darc Z."/></a>
|
||||
<a href="https://github.com/Creling" title="Creling"><img src="https://avatars.githubusercontent.com/u/43109504?v=4" width="50;" alt="Creling"/></a>
|
||||
<a href="https://github.com/coreff" title="Core F"><img src="https://avatars.githubusercontent.com/u/38347122?v=4" width="50;" alt="Core F"/></a>
|
||||
<a href="https://github.com/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/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/hiDandelion" title="hiDandelion"><img src="https://avatars.githubusercontent.com/u/77157418?v=4" width="50;" alt="hiDandelion"/></a>
|
||||
<a href="https://github.com/yuanweize" title="I"><img src="https://avatars.githubusercontent.com/u/30067203?v=4" width="50;" alt="I"/></a>
|
||||
<a href="https://github.com/lvyaoting" title="lvyaoting"><img src="https://avatars.githubusercontent.com/u/166296299?v=4" width="50;" alt="lvyaoting"/></a>
|
||||
<a href="https://github.com/lyj0309" title="lyj"><img src="https://avatars.githubusercontent.com/u/50474995?v=4" width="50;" alt="lyj"/></a>
|
||||
<a href="https://github.com/unclezs" title="unclezs"><img src="https://avatars.githubusercontent.com/u/42318775?v=4" width="50;" alt="unclezs"/></a>
|
||||
<a href="https://github.com/ysicing" title="缘生"><img src="https://avatars.githubusercontent.com/u/8605565?v=4" width="50;" alt="缘生"/></a>
|
||||
<a href="https://github.com/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/silver-ymz" title="Mingzhuo Yin"><img src="https://avatars.githubusercontent.com/u/78400701?v=4" width="50;" alt="Mingzhuo Yin"/></a>
|
||||
<a href="https://github.com/MartijnLindeman" title="Martijn Lindeman"><img src="https://avatars.githubusercontent.com/u/78365708?v=4" width="50;" alt="Martijn Lindeman"/></a>
|
||||
<a href="https://github.com/xrgzs" title="MadDogOwner"><img src="https://avatars.githubusercontent.com/u/26499123?v=4" width="50;" alt="MadDogOwner"/></a>
|
||||
<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>
|
||||
<a href="https://github.com/GreenTeodoro839" title="GreenTeodoro839"><img src="https://avatars.githubusercontent.com/u/77104800?v=4" width="50;" alt="GreenTeodoro839"/></a>
|
||||
<a href="https://github.com/Es-dese" title="Esdese"><img src="https://avatars.githubusercontent.com/u/71542548?v=4" width="50;" alt="Esdese"/></a>
|
||||
<a href="https://github.com/wwng2333" title="Crazy"><img src="https://avatars.githubusercontent.com/u/17147265?v=4" width="50;" alt="Crazy"/></a>
|
||||
<a href="https://github.com/wwng2333" title=":D"><img src="https://avatars.githubusercontent.com/u/17147265?v=4" width="50;" alt=":D"/></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
|
||||
|
||||
[](https://star-history.com/#naiba/nezha&Timeline)
|
||||
|
||||
@@ -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() {
|
||||
@@ -59,6 +46,8 @@ func (cp *commonPage) serve() {
|
||||
cr.GET("/network", cp.network)
|
||||
cr.GET("/ws", cp.ws)
|
||||
cr.POST("/terminal", cp.createTerminal)
|
||||
cr.GET("/file", cp.createFM)
|
||||
cr.GET("/file/:id", cp.fm)
|
||||
}
|
||||
|
||||
type viewPasswordForm struct {
|
||||
@@ -68,7 +57,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"}))
|
||||
@@ -113,7 +101,6 @@ func (p *commonPage) service(c *gin.Context) {
|
||||
"Title": singleton.Localizer.MustLocalize(&i18n.LocalizeConfig{MessageID: "ServicesStatus"}),
|
||||
"Services": res.([]interface{})[0],
|
||||
"CycleTransferStats": res.([]interface{})[1],
|
||||
"CustomCode": singleton.Conf.Site.CustomCode,
|
||||
}))
|
||||
}
|
||||
|
||||
@@ -218,12 +205,11 @@ func (cp *commonPage) network(c *gin.Context) {
|
||||
c.HTML(http.StatusOK, mygin.GetPreferredTheme(c, "/network"), mygin.CommonEnvironment(c, gin.H{
|
||||
"Servers": string(serversBytes),
|
||||
"MonitorInfos": string(monitorInfos),
|
||||
"CustomCode": singleton.Conf.Site.CustomCode,
|
||||
"MaxTCPPingValue": singleton.Conf.MaxTCPPingValue,
|
||||
}))
|
||||
}
|
||||
|
||||
func (cp *commonPage) getServerStat(c *gin.Context) ([]byte, error) {
|
||||
func (cp *commonPage) getServerStat(c *gin.Context, withPublicNote bool) ([]byte, error) {
|
||||
_, isMember := c.Get(model.CtxKeyAuthorizedUser)
|
||||
_, isViewPasswordVerfied := c.Get(model.CtxKeyViewPasswordVerified)
|
||||
authorized := isMember || isViewPasswordVerfied
|
||||
@@ -231,18 +217,20 @@ func (cp *commonPage) getServerStat(c *gin.Context) ([]byte, error) {
|
||||
singleton.SortedServerLock.RLock()
|
||||
defer singleton.SortedServerLock.RUnlock()
|
||||
|
||||
var servers []*model.Server
|
||||
|
||||
var serverList []*model.Server
|
||||
if authorized {
|
||||
servers = singleton.SortedServerList
|
||||
serverList = singleton.SortedServerList
|
||||
} else {
|
||||
filteredServers := make([]*model.Server, len(singleton.SortedServerListForGuest))
|
||||
for i, server := range singleton.SortedServerListForGuest {
|
||||
filteredServer := *server
|
||||
filteredServer.DDNSDomain = "redacted"
|
||||
filteredServers[i] = &filteredServer
|
||||
serverList = singleton.SortedServerListForGuest
|
||||
}
|
||||
|
||||
var servers []*model.Server
|
||||
for _, server := range serverList {
|
||||
item := *server
|
||||
if !withPublicNote {
|
||||
item.PublicNote = ""
|
||||
}
|
||||
servers = filteredServers
|
||||
servers = append(servers, &item)
|
||||
}
|
||||
|
||||
return utils.Json.Marshal(Data{
|
||||
@@ -254,7 +242,7 @@ func (cp *commonPage) getServerStat(c *gin.Context) ([]byte, error) {
|
||||
}
|
||||
|
||||
func (cp *commonPage) home(c *gin.Context) {
|
||||
stat, err := cp.getServerStat(c)
|
||||
stat, err := cp.getServerStat(c, true)
|
||||
if err != nil {
|
||||
mygin.ShowErrorPage(c, mygin.ErrInfo{
|
||||
Code: http.StatusInternalServerError,
|
||||
@@ -268,14 +256,13 @@ func (cp *commonPage) home(c *gin.Context) {
|
||||
return
|
||||
}
|
||||
c.HTML(http.StatusOK, mygin.GetPreferredTheme(c, "/home"), mygin.CommonEnvironment(c, gin.H{
|
||||
"Servers": string(stat),
|
||||
"CustomCode": singleton.Conf.Site.CustomCode,
|
||||
"Servers": string(stat),
|
||||
}))
|
||||
}
|
||||
|
||||
var upgrader = websocket.Upgrader{
|
||||
ReadBufferSize: 1024,
|
||||
WriteBufferSize: 1024,
|
||||
ReadBufferSize: 32768,
|
||||
WriteBufferSize: 32768,
|
||||
}
|
||||
|
||||
type Data struct {
|
||||
@@ -283,8 +270,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 {
|
||||
@@ -302,7 +287,7 @@ func (cp *commonPage) ws(c *gin.Context) {
|
||||
defer conn.Close()
|
||||
count := 0
|
||||
for {
|
||||
stat, err := cp.getServerStat(c)
|
||||
stat, err := cp.getServerStat(c, false)
|
||||
if err != nil {
|
||||
continue
|
||||
}
|
||||
@@ -321,10 +306,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 +317,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 +333,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 +345,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 +381,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 +395,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 +411,150 @@ 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,
|
||||
"ServerID": server.ID,
|
||||
}))
|
||||
}
|
||||
|
||||
func (cp *commonPage) fm(c *gin.Context) {
|
||||
streamId := c.Param("id")
|
||||
if _, err := rpc.NezhaHandlerSingleton.GetStream(streamId); err != nil {
|
||||
mygin.ShowErrorPage(c, mygin.ErrInfo{
|
||||
Code: http.StatusForbidden,
|
||||
Title: "无权访问",
|
||||
Msg: "FM会话不存在",
|
||||
Link: "/",
|
||||
Btn: "返回首页",
|
||||
}, true)
|
||||
return
|
||||
}
|
||||
defer rpc.NezhaHandlerSingleton.CloseStream(streamId)
|
||||
|
||||
wsConn, err := upgrader.Upgrade(c.Writer, c.Request, nil)
|
||||
if err != nil {
|
||||
mygin.ShowErrorPage(c, mygin.ErrInfo{
|
||||
Code: http.StatusInternalServerError,
|
||||
Title: singleton.Localizer.MustLocalize(&i18n.LocalizeConfig{
|
||||
MessageID: "NetworkError",
|
||||
}),
|
||||
Msg: "Websocket协议切换失败",
|
||||
Link: "/",
|
||||
Btn: "返回首页",
|
||||
}, true)
|
||||
return
|
||||
}
|
||||
defer wsConn.Close()
|
||||
conn := websocketx.NewConn(wsConn)
|
||||
|
||||
go func() {
|
||||
// PING 保活
|
||||
for {
|
||||
if err = conn.WriteMessage(websocket.PingMessage, []byte{}); err != nil {
|
||||
return
|
||||
}
|
||||
time.Sleep(time.Second * 10)
|
||||
}
|
||||
}()
|
||||
|
||||
if err = rpc.NezhaHandlerSingleton.UserConnected(streamId, conn); err != nil {
|
||||
return
|
||||
}
|
||||
|
||||
rpc.NezhaHandlerSingleton.StartStream(streamId, time.Second*10)
|
||||
}
|
||||
|
||||
func (cp *commonPage) createFM(c *gin.Context) {
|
||||
IdString := c.Query("id")
|
||||
if _, authorized := c.Get(model.CtxKeyAuthorizedUser); !authorized {
|
||||
mygin.ShowErrorPage(c, mygin.ErrInfo{
|
||||
Code: http.StatusForbidden,
|
||||
Title: "无权访问",
|
||||
Msg: "用户未登录",
|
||||
Link: "/login",
|
||||
Btn: "去登录",
|
||||
}, true)
|
||||
return
|
||||
}
|
||||
|
||||
streamId, err := uuid.GenerateUUID()
|
||||
if err != nil {
|
||||
mygin.ShowErrorPage(c, mygin.ErrInfo{
|
||||
Code: http.StatusInternalServerError,
|
||||
Title: singleton.Localizer.MustLocalize(&i18n.LocalizeConfig{
|
||||
MessageID: "SystemError",
|
||||
}),
|
||||
Msg: "生成会话ID失败",
|
||||
Link: "/server",
|
||||
Btn: "返回重试",
|
||||
}, true)
|
||||
return
|
||||
}
|
||||
|
||||
rpc.NezhaHandlerSingleton.CreateStream(streamId)
|
||||
|
||||
serverId, err := strconv.Atoi(IdString)
|
||||
if err != nil {
|
||||
mygin.ShowErrorPage(c, mygin.ErrInfo{
|
||||
Code: http.StatusForbidden,
|
||||
Title: "请求失败",
|
||||
Msg: "请求参数有误:" + err.Error(),
|
||||
Link: "/server",
|
||||
Btn: "返回重试",
|
||||
}, true)
|
||||
return
|
||||
}
|
||||
|
||||
singleton.ServerLock.RLock()
|
||||
server := singleton.ServerList[uint64(serverId)]
|
||||
singleton.ServerLock.RUnlock()
|
||||
if server == nil {
|
||||
mygin.ShowErrorPage(c, mygin.ErrInfo{
|
||||
Code: http.StatusForbidden,
|
||||
Title: "请求失败",
|
||||
Msg: "服务器不存在或处于离线状态",
|
||||
Link: "/server",
|
||||
Btn: "返回重试",
|
||||
}, true)
|
||||
return
|
||||
}
|
||||
|
||||
fmData, _ := utils.Json.Marshal(&model.TaskFM{
|
||||
StreamID: streamId,
|
||||
})
|
||||
if err := server.TaskStream.Send(&proto.Task{
|
||||
Type: model.TaskTypeFM,
|
||||
Data: string(fmData),
|
||||
}); 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+"/file", mygin.CommonEnvironment(c, gin.H{
|
||||
"SessionID": streamId,
|
||||
}))
|
||||
}
|
||||
|
||||
@@ -3,28 +3,37 @@ package controller
|
||||
import (
|
||||
"fmt"
|
||||
"html/template"
|
||||
"io/fs"
|
||||
"log"
|
||||
"net/http"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"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,19 +42,9 @@ 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 {
|
||||
panic(err)
|
||||
}
|
||||
r.StaticFS("/static", http.FS(staticFs))
|
||||
r.Static("/static-custom", "resource/static/custom")
|
||||
r.StaticFS("/static", http.FS(resource.StaticFS))
|
||||
routers(r)
|
||||
|
||||
page404 := func(c *gin.Context) {
|
||||
mygin.ShowErrorPage(c, mygin.ErrInfo{
|
||||
Code: http.StatusNotFound,
|
||||
@@ -68,7 +67,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}
|
||||
@@ -85,7 +84,7 @@ func routers(r *gin.Engine) {
|
||||
}
|
||||
|
||||
func loadThirdPartyTemplates(tmpl *template.Template) *template.Template {
|
||||
var ret = tmpl
|
||||
ret := tmpl
|
||||
themes, err := os.ReadDir("resource/template")
|
||||
if err != nil {
|
||||
log.Printf("NEZHA>> Error reading themes folder: %v", err)
|
||||
@@ -95,17 +94,61 @@ func loadThirdPartyTemplates(tmpl *template.Template) *template.Template {
|
||||
if !theme.IsDir() {
|
||||
continue
|
||||
}
|
||||
// load templates
|
||||
t, err := ret.ParseGlob(fmt.Sprintf("resource/template/%s/*.html", theme.Name()))
|
||||
if err != nil {
|
||||
log.Printf("NEZHA>> Error parsing templates %s error: %v", theme.Name(), err)
|
||||
|
||||
themeDir := theme.Name()
|
||||
if themeDir == "theme-custom" {
|
||||
// for backward compatibility
|
||||
// note: will remove this in future versions
|
||||
ret = loadTemplates(ret, themeDir)
|
||||
continue
|
||||
}
|
||||
ret = t
|
||||
|
||||
if strings.HasPrefix(themeDir, "dashboard-") {
|
||||
// load dashboard templates, ignore desc file
|
||||
ret = loadTemplates(ret, themeDir)
|
||||
continue
|
||||
}
|
||||
|
||||
if !strings.HasPrefix(themeDir, "theme-") {
|
||||
log.Printf("NEZHA>> Invalid theme name: %s", themeDir)
|
||||
continue
|
||||
}
|
||||
|
||||
descPath := filepath.Join("resource", "template", themeDir, "theme.json")
|
||||
desc, err := os.ReadFile(filepath.Clean(descPath))
|
||||
if err != nil {
|
||||
log.Printf("NEZHA>> Error opening %s config: %v", themeDir, err)
|
||||
continue
|
||||
}
|
||||
|
||||
themeName, err := utils.GjsonGet(desc, "name")
|
||||
if err != nil {
|
||||
log.Printf("NEZHA>> Error opening %s config: not a valid description file", theme.Name())
|
||||
continue
|
||||
}
|
||||
|
||||
// load templates
|
||||
ret = loadTemplates(ret, themeDir)
|
||||
|
||||
themeKey := strings.TrimPrefix(themeDir, "theme-")
|
||||
model.Themes[themeKey] = themeName.String()
|
||||
}
|
||||
|
||||
return ret
|
||||
}
|
||||
|
||||
func loadTemplates(tmpl *template.Template, themeDir string) *template.Template {
|
||||
// load templates
|
||||
templatePath := filepath.Join("resource", "template", themeDir, "*.html")
|
||||
t, err := tmpl.ParseGlob(templatePath)
|
||||
if err != nil {
|
||||
log.Printf("NEZHA>> Error parsing templates %s: %v", themeDir, err)
|
||||
return tmpl
|
||||
}
|
||||
|
||||
return t
|
||||
}
|
||||
|
||||
var funcMap = template.FuncMap{
|
||||
"tr": func(id string, dataAndCount ...interface{}) string {
|
||||
conf := i18n.LocalizeConfig{
|
||||
@@ -239,3 +282,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 := utils.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()
|
||||
}
|
||||
|
||||
@@ -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"}),
|
||||
|
||||
@@ -7,10 +7,12 @@ import (
|
||||
"net/http"
|
||||
"strconv"
|
||||
"strings"
|
||||
"sync"
|
||||
"time"
|
||||
|
||||
"github.com/gin-gonic/gin"
|
||||
"github.com/jinzhu/copier"
|
||||
"golang.org/x/net/idna"
|
||||
"gorm.io/gorm"
|
||||
|
||||
"github.com/naiba/nezha/model"
|
||||
@@ -37,6 +39,7 @@ func (ma *memberAPI) serve() {
|
||||
|
||||
mr.GET("/search-server", ma.searchServer)
|
||||
mr.GET("/search-tasks", ma.searchTask)
|
||||
mr.GET("/search-ddns", ma.searchDDNS)
|
||||
mr.POST("/server", ma.addOrEditServer)
|
||||
mr.POST("/monitor", ma.addOrEditMonitor)
|
||||
mr.POST("/cron", ma.addOrEditCron)
|
||||
@@ -45,6 +48,8 @@ func (ma *memberAPI) serve() {
|
||||
mr.POST("/batch-update-server-group", ma.batchUpdateServerGroup)
|
||||
mr.POST("/batch-delete-server", ma.batchDeleteServer)
|
||||
mr.POST("/notification", ma.addOrEditNotification)
|
||||
mr.POST("/ddns", ma.addOrEditDDNS)
|
||||
mr.POST("/nat", ma.addOrEditNAT)
|
||||
mr.POST("/alert-rule", ma.addOrEditAlertRule)
|
||||
mr.POST("/setting", ma.updateSetting)
|
||||
mr.DELETE("/:model/:id", ma.delete)
|
||||
@@ -209,6 +214,16 @@ func (ma *memberAPI) delete(c *gin.Context) {
|
||||
if err == nil {
|
||||
singleton.OnDeleteNotification(id)
|
||||
}
|
||||
case "ddns":
|
||||
err = singleton.DB.Unscoped().Delete(&model.DDNSProfile{}, "id = ?", id).Error
|
||||
if err == nil {
|
||||
singleton.OnDDNSUpdate()
|
||||
}
|
||||
case "nat":
|
||||
err = singleton.DB.Unscoped().Delete(&model.NAT{}, "id = ?", id).Error
|
||||
if err == nil {
|
||||
singleton.OnNATUpdate()
|
||||
}
|
||||
case "monitor":
|
||||
err = singleton.DB.Unscoped().Delete(&model.Monitor{}, "id = ?", id).Error
|
||||
if err == nil {
|
||||
@@ -292,19 +307,38 @@ func (ma *memberAPI) searchTask(c *gin.Context) {
|
||||
})
|
||||
}
|
||||
|
||||
func (ma *memberAPI) searchDDNS(c *gin.Context) {
|
||||
var ddns []model.DDNSProfile
|
||||
likeWord := "%" + c.Query("word") + "%"
|
||||
singleton.DB.Select("id,name").Where("id = ? OR name LIKE ?",
|
||||
c.Query("word"), likeWord).Find(&ddns)
|
||||
|
||||
var resp []searchResult
|
||||
for i := 0; i < len(ddns); i++ {
|
||||
resp = append(resp, searchResult{
|
||||
Value: ddns[i].ID,
|
||||
Name: ddns[i].Name,
|
||||
Text: ddns[i].Name,
|
||||
})
|
||||
}
|
||||
|
||||
c.JSON(http.StatusOK, map[string]interface{}{
|
||||
"success": true,
|
||||
"results": resp,
|
||||
})
|
||||
}
|
||||
|
||||
type serverForm struct {
|
||||
ID uint64
|
||||
Name string `binding:"required"`
|
||||
DisplayIndex int
|
||||
Secret string
|
||||
Tag string
|
||||
Note string
|
||||
HideForGuest string
|
||||
EnableDDNS string
|
||||
EnableIPv4 string
|
||||
EnableIpv6 string
|
||||
DDNSDomain string
|
||||
DDNSProfile string
|
||||
ID uint64
|
||||
Name string `binding:"required"`
|
||||
DisplayIndex int
|
||||
Secret string
|
||||
Tag string
|
||||
Note string
|
||||
PublicNote string
|
||||
HideForGuest string
|
||||
EnableDDNS string
|
||||
DDNSProfilesRaw string
|
||||
}
|
||||
|
||||
func (ma *memberAPI) addOrEditServer(c *gin.Context) {
|
||||
@@ -319,20 +353,21 @@ func (ma *memberAPI) addOrEditServer(c *gin.Context) {
|
||||
s.ID = sf.ID
|
||||
s.Tag = sf.Tag
|
||||
s.Note = sf.Note
|
||||
s.PublicNote = sf.PublicNote
|
||||
s.HideForGuest = sf.HideForGuest == "on"
|
||||
s.EnableDDNS = sf.EnableDDNS == "on"
|
||||
s.EnableIPv4 = sf.EnableIPv4 == "on"
|
||||
s.EnableIpv6 = sf.EnableIpv6 == "on"
|
||||
s.DDNSDomain = sf.DDNSDomain
|
||||
s.DDNSProfile = sf.DDNSProfile
|
||||
if s.ID == 0 {
|
||||
s.Secret, err = utils.GenerateRandomString(18)
|
||||
if err == nil {
|
||||
err = singleton.DB.Create(&s).Error
|
||||
s.DDNSProfilesRaw = sf.DDNSProfilesRaw
|
||||
err = utils.Json.Unmarshal([]byte(sf.DDNSProfilesRaw), &s.DDNSProfiles)
|
||||
if err == nil {
|
||||
if s.ID == 0 {
|
||||
s.Secret, err = utils.GenerateRandomString(18)
|
||||
if err == nil {
|
||||
err = singleton.DB.Create(&s).Error
|
||||
}
|
||||
} else {
|
||||
isEdit = true
|
||||
err = singleton.DB.Save(&s).Error
|
||||
}
|
||||
} else {
|
||||
isEdit = true
|
||||
err = singleton.DB.Save(&s).Error
|
||||
}
|
||||
}
|
||||
if err != nil {
|
||||
@@ -378,6 +413,7 @@ func (ma *memberAPI) addOrEditServer(c *gin.Context) {
|
||||
} else {
|
||||
s.Host = &model.Host{}
|
||||
s.State = &model.HostState{}
|
||||
s.TaskCloseLock = new(sync.Mutex)
|
||||
singleton.ServerLock.Lock()
|
||||
singleton.SecretToID[s.Secret] = s.ID
|
||||
singleton.ServerList[s.ID] = &s
|
||||
@@ -733,6 +769,120 @@ func (ma *memberAPI) addOrEditNotification(c *gin.Context) {
|
||||
})
|
||||
}
|
||||
|
||||
type ddnsForm struct {
|
||||
ID uint64
|
||||
MaxRetries uint64
|
||||
EnableIPv4 string
|
||||
EnableIPv6 string
|
||||
Name string
|
||||
Provider uint8
|
||||
DomainsRaw string
|
||||
AccessID string
|
||||
AccessSecret string
|
||||
WebhookURL string
|
||||
WebhookMethod uint8
|
||||
WebhookRequestType uint8
|
||||
WebhookRequestBody string
|
||||
WebhookHeaders string
|
||||
}
|
||||
|
||||
func (ma *memberAPI) addOrEditDDNS(c *gin.Context) {
|
||||
var df ddnsForm
|
||||
var p model.DDNSProfile
|
||||
err := c.ShouldBindJSON(&df)
|
||||
if err == nil {
|
||||
if df.MaxRetries < 1 || df.MaxRetries > 10 {
|
||||
err = errors.New("重试次数必须为大于 1 且不超过 10 的整数")
|
||||
}
|
||||
}
|
||||
if err == nil {
|
||||
p.Name = df.Name
|
||||
p.ID = df.ID
|
||||
enableIPv4 := df.EnableIPv4 == "on"
|
||||
enableIPv6 := df.EnableIPv6 == "on"
|
||||
p.EnableIPv4 = &enableIPv4
|
||||
p.EnableIPv6 = &enableIPv6
|
||||
p.MaxRetries = df.MaxRetries
|
||||
p.Provider = df.Provider
|
||||
p.DomainsRaw = df.DomainsRaw
|
||||
p.Domains = strings.Split(p.DomainsRaw, ",")
|
||||
p.AccessID = df.AccessID
|
||||
p.AccessSecret = df.AccessSecret
|
||||
p.WebhookURL = df.WebhookURL
|
||||
p.WebhookMethod = df.WebhookMethod
|
||||
p.WebhookRequestType = df.WebhookRequestType
|
||||
p.WebhookRequestBody = df.WebhookRequestBody
|
||||
p.WebhookHeaders = df.WebhookHeaders
|
||||
|
||||
for n, domain := range p.Domains {
|
||||
// IDN to ASCII
|
||||
domainValid, domainErr := idna.Lookup.ToASCII(domain)
|
||||
if domainErr != nil {
|
||||
err = fmt.Errorf("域名 %s 解析错误: %v", domain, domainErr)
|
||||
break
|
||||
}
|
||||
p.Domains[n] = domainValid
|
||||
}
|
||||
}
|
||||
if err == nil {
|
||||
if p.ID == 0 {
|
||||
err = singleton.DB.Create(&p).Error
|
||||
} else {
|
||||
err = singleton.DB.Save(&p).Error
|
||||
}
|
||||
}
|
||||
if err != nil {
|
||||
c.JSON(http.StatusOK, model.Response{
|
||||
Code: http.StatusBadRequest,
|
||||
Message: fmt.Sprintf("请求错误:%s", err),
|
||||
})
|
||||
return
|
||||
}
|
||||
singleton.OnDDNSUpdate()
|
||||
c.JSON(http.StatusOK, model.Response{
|
||||
Code: http.StatusOK,
|
||||
})
|
||||
}
|
||||
|
||||
type natForm struct {
|
||||
ID uint64
|
||||
Name string
|
||||
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 +997,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 {
|
||||
@@ -856,6 +1011,8 @@ type settingForm struct {
|
||||
Theme string
|
||||
DashboardTheme string
|
||||
CustomCode string
|
||||
CustomCodeDashboard string
|
||||
CustomNameservers string
|
||||
ViewPassword string
|
||||
IgnoredIPNotification string
|
||||
IPChangeNotificationTag string // IP变更提醒的通知组
|
||||
@@ -885,7 +1042,7 @@ func (ma *memberAPI) updateSetting(c *gin.Context) {
|
||||
return
|
||||
}
|
||||
|
||||
if _, yes := model.Themes[sf.DashboardTheme]; !yes {
|
||||
if _, yes := model.DashboardThemes[sf.DashboardTheme]; !yes {
|
||||
c.JSON(http.StatusOK, model.Response{
|
||||
Code: http.StatusBadRequest,
|
||||
Message: fmt.Sprintf("后台主题不存在:%s", sf.DashboardTheme),
|
||||
@@ -921,6 +1078,8 @@ func (ma *memberAPI) updateSetting(c *gin.Context) {
|
||||
singleton.Conf.Site.Theme = sf.Theme
|
||||
singleton.Conf.Site.DashboardTheme = sf.DashboardTheme
|
||||
singleton.Conf.Site.CustomCode = sf.CustomCode
|
||||
singleton.Conf.Site.CustomCodeDashboard = sf.CustomCodeDashboard
|
||||
singleton.Conf.DNSServers = sf.CustomNameservers
|
||||
singleton.Conf.Site.ViewPassword = sf.ViewPassword
|
||||
singleton.Conf.Oauth2.Admin = sf.Admin
|
||||
// 保证NotificationTag不为空
|
||||
@@ -936,6 +1095,8 @@ func (ma *memberAPI) updateSetting(c *gin.Context) {
|
||||
}
|
||||
// 更新系统语言
|
||||
singleton.InitLocalizer()
|
||||
// 更新DNS服务器
|
||||
singleton.OnNameserverUpdate()
|
||||
c.JSON(http.StatusOK, model.Response{
|
||||
Code: http.StatusOK,
|
||||
})
|
||||
|
||||
@@ -27,6 +27,8 @@ func (mp *memberPage) serve() {
|
||||
mr.GET("/monitor", mp.monitor)
|
||||
mr.GET("/cron", mp.cron)
|
||||
mr.GET("/notification", mp.notification)
|
||||
mr.GET("/ddns", mp.ddns)
|
||||
mr.GET("/nat", mp.nat)
|
||||
mr.GET("/setting", mp.setting)
|
||||
mr.GET("/api", mp.api)
|
||||
}
|
||||
@@ -77,6 +79,26 @@ func (mp *memberPage) notification(c *gin.Context) {
|
||||
}))
|
||||
}
|
||||
|
||||
func (mp *memberPage) ddns(c *gin.Context) {
|
||||
var data []model.DDNSProfile
|
||||
singleton.DB.Find(&data)
|
||||
c.HTML(http.StatusOK, "dashboard-"+singleton.Conf.Site.DashboardTheme+"/ddns", mygin.CommonEnvironment(c, gin.H{
|
||||
"Title": singleton.Localizer.MustLocalize(&i18n.LocalizeConfig{MessageID: "DDNS"}),
|
||||
"DDNS": data,
|
||||
"ProviderMap": model.ProviderMap,
|
||||
"ProviderList": model.ProviderList,
|
||||
}))
|
||||
}
|
||||
|
||||
func (mp *memberPage) nat(c *gin.Context) {
|
||||
var data []model.NAT
|
||||
singleton.DB.Find(&data)
|
||||
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"}),
|
||||
|
||||
@@ -2,7 +2,6 @@ package controller
|
||||
|
||||
import (
|
||||
"context"
|
||||
"encoding/json"
|
||||
"errors"
|
||||
"fmt"
|
||||
"net/http"
|
||||
@@ -10,25 +9,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 +89,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 +122,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"
|
||||
@@ -175,11 +198,22 @@ func (oa *oauth2controller) callback(c *gin.Context) {
|
||||
if err == nil {
|
||||
defer resp.Body.Close()
|
||||
var cloudflareUserInfo *cloudflare.UserInfo
|
||||
if err := json.NewDecoder(resp.Body).Decode(&cloudflareUserInfo); err == nil {
|
||||
if err := utils.Json.NewDecoder(resp.Body).Decode(&cloudflareUserInfo); err == nil {
|
||||
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 +246,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 +281,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
|
||||
}
|
||||
|
||||
+29
-8
@@ -1,13 +1,17 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"os"
|
||||
"context"
|
||||
"fmt"
|
||||
"log"
|
||||
"time"
|
||||
_ "time/tzdata"
|
||||
|
||||
"github.com/naiba/nezha/cmd/dashboard/controller"
|
||||
"github.com/naiba/nezha/cmd/dashboard/rpc"
|
||||
"github.com/naiba/nezha/model"
|
||||
"github.com/naiba/nezha/proto"
|
||||
"github.com/naiba/nezha/service/singleton"
|
||||
"github.com/ory/graceful"
|
||||
flag "github.com/spf13/pflag"
|
||||
@@ -29,13 +33,6 @@ func init() {
|
||||
flag.StringVarP(&dashboardCliParam.ConfigFile, "config", "c", "data/config.yaml", "配置文件路径")
|
||||
flag.StringVar(&dashboardCliParam.DatebaseLocation, "db", "data/sqlite.db", "Sqlite3数据库文件路径")
|
||||
flag.Parse()
|
||||
|
||||
// 初始化 dao 包
|
||||
singleton.InitConfigFromPath(dashboardCliParam.ConfigFile)
|
||||
singleton.InitTimezoneAndCache()
|
||||
singleton.InitDBFromPath(dashboardCliParam.DatebaseLocation)
|
||||
singleton.InitLocalizer()
|
||||
initSystem()
|
||||
}
|
||||
|
||||
func initSystem() {
|
||||
@@ -56,9 +53,17 @@ func initSystem() {
|
||||
func main() {
|
||||
if dashboardCliParam.Version {
|
||||
fmt.Println(singleton.Version)
|
||||
return
|
||||
os.Exit(0)
|
||||
}
|
||||
|
||||
// 初始化 dao 包
|
||||
singleton.InitConfigFromPath(dashboardCliParam.ConfigFile)
|
||||
singleton.InitTimezoneAndCache()
|
||||
singleton.InitDBFromPath(dashboardCliParam.DatebaseLocation)
|
||||
singleton.InitLocalizer()
|
||||
initSystem()
|
||||
|
||||
// TODO 使用 cmux 在同一端口服务 HTTP 和 gRPC
|
||||
singleton.CleanMonitorHistory()
|
||||
go rpc.ServeRPC(singleton.Conf.GRPCPort)
|
||||
serviceSentinelDispatchBus := make(chan model.Monitor) // 用于传递服务监控任务信息的channel
|
||||
@@ -67,6 +72,7 @@ func main() {
|
||||
go singleton.AlertSentinelStart()
|
||||
singleton.NewServiceSentinel(serviceSentinelDispatchBus)
|
||||
srv := controller.ServeWeb(singleton.Conf.HTTPPort)
|
||||
go dispatchReportInfoTask()
|
||||
if err := graceful.Graceful(func() error {
|
||||
return srv.ListenAndServe()
|
||||
}, func(c context.Context) error {
|
||||
@@ -79,3 +85,18 @@ func main() {
|
||||
log.Printf("NEZHA>> ERROR: %v", err)
|
||||
}
|
||||
}
|
||||
|
||||
func dispatchReportInfoTask() {
|
||||
time.Sleep(time.Second * 15)
|
||||
singleton.ServerLock.RLock()
|
||||
defer singleton.ServerLock.RUnlock()
|
||||
for _, server := range singleton.ServerList {
|
||||
if server == nil || server.TaskStream == nil {
|
||||
continue
|
||||
}
|
||||
server.TaskStream.Send(&proto.Task{
|
||||
Type: model.TaskTypeReportHostInfo,
|
||||
Data: "",
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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
|
||||
@@ -13,21 +14,26 @@ require (
|
||||
github.com/hashicorp/go-uuid v1.0.3
|
||||
github.com/jinzhu/copier v0.4.0
|
||||
github.com/json-iterator/go v1.1.12
|
||||
github.com/libdns/cloudflare v0.1.1
|
||||
github.com/libdns/libdns v0.2.2
|
||||
github.com/miekg/dns v1.1.62
|
||||
github.com/nezhahq/libdns-tencentcloud v0.0.0-20241029120103-889957240fff
|
||||
github.com/nicksnyder/go-i18n/v2 v2.4.0
|
||||
github.com/ory/graceful v0.1.3
|
||||
github.com/oschwald/maxminddb-golang v1.13.1
|
||||
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/tidwall/gjson v1.18.0
|
||||
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 +47,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
|
||||
@@ -68,14 +75,19 @@ require (
|
||||
github.com/spf13/afero v1.11.0 // indirect
|
||||
github.com/spf13/cast v1.6.0 // indirect
|
||||
github.com/subosito/gotenv v1.6.0 // indirect
|
||||
github.com/tidwall/match v1.1.1 // indirect
|
||||
github.com/tidwall/pretty v1.2.0 // indirect
|
||||
github.com/tidwall/sjson v1.2.5 // indirect
|
||||
github.com/twitchyliquid64/golang-asm v0.15.1 // indirect
|
||||
github.com/ugorji/go/codec v1.2.11 // indirect
|
||||
go.uber.org/atomic v1.9.0 // indirect
|
||||
go.uber.org/multierr v1.9.0 // indirect
|
||||
golang.org/x/arch v0.3.0 // indirect
|
||||
golang.org/x/exp v0.0.0-20230905200255-921286631fa9 // indirect
|
||||
golang.org/x/sys v0.20.0 // indirect
|
||||
golang.org/x/mod v0.18.0 // indirect
|
||||
golang.org/x/sys v0.22.0 // indirect
|
||||
golang.org/x/time v0.5.0 // indirect
|
||||
golang.org/x/tools v0.22.0 // indirect
|
||||
google.golang.org/genproto/googleapis/rpc v0.0.0-20240227224415-6ceb2ff114de // indirect
|
||||
gopkg.in/ini.v1 v1.67.0 // indirect
|
||||
gopkg.in/yaml.v3 v3.0.1 // indirect
|
||||
|
||||
@@ -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=
|
||||
@@ -93,14 +107,21 @@ github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE=
|
||||
github.com/leodido/go-urn v1.2.1/go.mod h1:zt4jvISO2HfUBqxjfIshjdMTYS56ZS/qv49ictyFfxY=
|
||||
github.com/leodido/go-urn v1.2.4 h1:XlAE/cm/ms7TE/VMVoduSpNBoyc2dOxHs5MZSwAN63Q=
|
||||
github.com/leodido/go-urn v1.2.4/go.mod h1:7ZrI8mTSeBSHl/UaRyKQW1qZeMgak41ANeCNaVckg+4=
|
||||
github.com/libdns/cloudflare v0.1.1 h1:FVPfWwP8zZCqj268LZjmkDleXlHPlFU9KC4OJ3yn054=
|
||||
github.com/libdns/cloudflare v0.1.1/go.mod h1:9VK91idpOjg6v7/WbjkEW49bSCxj00ALesIFDhJ8PBU=
|
||||
github.com/libdns/libdns v0.2.2 h1:O6ws7bAfRPaBsgAYt8MDe2HcNBGC29hkZ9MX2eUSX3s=
|
||||
github.com/libdns/libdns v0.2.2/go.mod h1:4Bj9+5CQiNMVGf87wjX4CY3HQJypUHRuLvlsfsZqLWQ=
|
||||
github.com/magiconair/properties v1.8.7 h1:IeQXZAiQcpL9mgcAe1Nu6cX9LLw6ExEHKjN0VQdvPDY=
|
||||
github.com/magiconair/properties v1.8.7/go.mod h1:Dhd985XPs7jluiymwWYZ0G4Z61jb3vdS329zhj2hYo0=
|
||||
github.com/mattn/go-colorable v0.1.13 h1:fFA4WZxdEF4tXPZVKMLwD8oUnCTTo08duU7wxecdEvA=
|
||||
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=
|
||||
github.com/mattn/go-sqlite3 v1.14.17 h1:mCRHCLDUBXgpKAqIKsaAaAsrAlbkeomtRFKXh2L6YIM=
|
||||
github.com/mattn/go-sqlite3 v1.14.17/go.mod h1:2eHXhiwb8IkHr+BDWZGa96P6+rkvnG63S2DGjv9HUNg=
|
||||
github.com/miekg/dns v1.1.62 h1:cN8OuEF1/x5Rq6Np+h1epln8OiyPWV+lROx9LxcGgIQ=
|
||||
github.com/miekg/dns v1.1.62/go.mod h1:mvDlcItzm+br7MToIKqkglaGhlFMHJ9DTNNWONWXbNQ=
|
||||
github.com/mitchellh/mapstructure v1.5.0 h1:jeMsZIYE/09sWLaz43PL7Gy6RuMjD2eJVyuac5Z2hdY=
|
||||
github.com/mitchellh/mapstructure v1.5.0/go.mod h1:bFUtVrKA4DC2yAKiSyO/QUcy7e+RRV2QTWOzhPopBRo=
|
||||
github.com/modern-go/concurrent v0.0.0-20180228061459-e0a39a4cb421/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q=
|
||||
@@ -108,12 +129,18 @@ github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd h1:TRLaZ9cD/w
|
||||
github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q=
|
||||
github.com/modern-go/reflect2 v1.0.2 h1:xBagoLtFs94CBntxluKeaWgTMpvLxC4ur3nMaC9Gz0M=
|
||||
github.com/modern-go/reflect2 v1.0.2/go.mod h1:yWuevngMOJpCy52FWWMvUC8ws7m/LJsjYzDa0/r8luk=
|
||||
github.com/nezhahq/libdns-tencentcloud v0.0.0-20241029120103-889957240fff h1:3WDsbsg3dmsRENYLUPGPTkEcWWmTvk41i+rM91AIIbY=
|
||||
github.com/nezhahq/libdns-tencentcloud v0.0.0-20241029120103-889957240fff/go.mod h1:k+cDtTbUY+UV56Aqv4Ahmc9bWmhpga9JJXZlAIPKBEc=
|
||||
github.com/nicksnyder/go-i18n/v2 v2.4.0 h1:3IcvPOAvnCKwNm0TB0dLDTuawWEj+ax/RERNC+diLMM=
|
||||
github.com/nicksnyder/go-i18n/v2 v2.4.0/go.mod h1:nxYSZE9M0bf3Y70gPQjN9ha7XNHX7gMc814+6wVyEI4=
|
||||
github.com/onsi/ginkgo/v2 v2.17.1 h1:V++EzdbhI4ZV4ev0UTIj0PzhzOcReJFyJaLjtSF55M8=
|
||||
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 +151,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=
|
||||
@@ -156,10 +183,20 @@ github.com/stretchr/testify v1.7.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/
|
||||
github.com/stretchr/testify v1.8.0/go.mod h1:yNjHg4UonilssWZ8iaSj1OCr/vHnekPRkoO+kdMU+MU=
|
||||
github.com/stretchr/testify v1.8.1/go.mod h1:w2LPCIKwWwSfY2zedu0+kehJoqGctiVI29o6fzry7u4=
|
||||
github.com/stretchr/testify v1.8.2/go.mod h1:w2LPCIKwWwSfY2zedu0+kehJoqGctiVI29o6fzry7u4=
|
||||
github.com/stretchr/testify v1.8.4 h1:CcVxjf3Q8PM0mHUKJCdn+eZZtm5yQwehR5yeSVQQcUk=
|
||||
github.com/stretchr/testify v1.8.4/go.mod h1:sz/lmYIOXD/1dqDmKjjqLyZ2RngseejIcXlSw2iwfAo=
|
||||
github.com/stretchr/testify v1.9.0 h1:HtqpIVDClZ4nwg75+f6Lvsy/wHu+3BoSGCbBAcpTsTg=
|
||||
github.com/stretchr/testify v1.9.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY=
|
||||
github.com/subosito/gotenv v1.6.0 h1:9NlTDc1FTs4qu0DDq7AEtTPNw6SVm7uBMsUCUjABIf8=
|
||||
github.com/subosito/gotenv v1.6.0/go.mod h1:Dk4QP5c2W3ibzajGcXpNraDfq2IrhjMIvMSWPKKo0FU=
|
||||
github.com/tidwall/gjson v1.14.2/go.mod h1:/wbyibRr2FHMks5tjHJ5F8dMZh3AcwJEMf5vlfC0lxk=
|
||||
github.com/tidwall/gjson v1.18.0 h1:FIDeeyB800efLX89e5a8Y0BNH+LOngJyGrIWxG2FKQY=
|
||||
github.com/tidwall/gjson v1.18.0/go.mod h1:/wbyibRr2FHMks5tjHJ5F8dMZh3AcwJEMf5vlfC0lxk=
|
||||
github.com/tidwall/match v1.1.1 h1:+Ho715JplO36QYgwN9PGYNhgZvoUSc9X2c80KVTi+GA=
|
||||
github.com/tidwall/match v1.1.1/go.mod h1:eRSPERbgtNPcGhD8UCthc6PmLEQXEWd3PRB5JTxsfmM=
|
||||
github.com/tidwall/pretty v1.2.0 h1:RWIZEg2iJ8/g6fDDYzMpobmaoGh5OLl4AXtGUGPcqCs=
|
||||
github.com/tidwall/pretty v1.2.0/go.mod h1:ITEVvHYasfjBbM0u2Pg8T2nJnzm8xPwvNhhsoaGGjNU=
|
||||
github.com/tidwall/sjson v1.2.5 h1:kLy8mja+1c9jlljvWTlSazM7cKDRfJuR/bOJhcY5NcY=
|
||||
github.com/tidwall/sjson v1.2.5/go.mod h1:Fvgq9kS/6ociJEDnK0Fk1cpYF4FIW6ZF7LAe+6jwd28=
|
||||
github.com/twitchyliquid64/golang-asm v0.15.1 h1:SU5vSMR7hnwNxj24w34ZyCi/FmDZTkS4MhqMhdFk5YI=
|
||||
github.com/twitchyliquid64/golang-asm v0.15.1/go.mod h1:a1lVb/DtPvCB8fslRZhAngC2+aY1QWCk3Cedj/Gdt08=
|
||||
github.com/ugorji/go v1.2.7/go.mod h1:nF9osbDWLy6bDVv/Rtoh6QgnvNDpmCalQV5urGCCS6M=
|
||||
@@ -179,16 +216,18 @@ 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/mod v0.18.0 h1:5+9lSbEzPSdWkH32vYPBwEpX8KwDbM52Ud9xBUvNlb0=
|
||||
golang.org/x/mod v0.18.0/go.mod h1:hTbmBsO62+eylJbnUtE2MGJUyE7QWk4xUqPFrRgJ+7c=
|
||||
golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg=
|
||||
golang.org/x/net v0.0.0-20210226172049-e18ecbb05110/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg=
|
||||
golang.org/x/net v0.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 +238,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.22.0 h1:gqSGLZqv+AI9lIQzniJ0nZDRG5GBPsSi+DRNHWNz6yA=
|
||||
golang.org/x/tools v0.22.0/go.mod h1:aCwcsjqvq7Yqt6TNyX7QMU2enbQ/Gt0bo6krSeEri+c=
|
||||
golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
|
||||
google.golang.org/genproto/googleapis/rpc v0.0.0-20240227224415-6ceb2ff114de h1:cZGRis4/ot9uVm639a+rHCUaG0JJHEsdyzSQTMX+suY=
|
||||
google.golang.org/genproto/googleapis/rpc v0.0.0-20240227224415-6ceb2ff114de/go.mod h1:H4O17MA/PE9BsGx3w+a+W2VOLLD1Qf7oJneAoU6WktY=
|
||||
@@ -219,8 +260,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=
|
||||
|
||||
+35
-36
@@ -38,6 +38,7 @@ const (
|
||||
ConfigTypeJihulab = "jihulab"
|
||||
ConfigTypeGitea = "gitea"
|
||||
ConfigTypeCloudflare = "cloudflare"
|
||||
ConfigTypeOidc = "oidc"
|
||||
)
|
||||
|
||||
const (
|
||||
@@ -79,19 +80,30 @@ type Config struct {
|
||||
Debug bool // debug模式开关
|
||||
Language string // 系统语言,默认 zh-CN
|
||||
Site struct {
|
||||
Brand string // 站点名称
|
||||
CookieName string // 浏览器 Cookie 名称
|
||||
Theme string
|
||||
DashboardTheme string
|
||||
CustomCode string
|
||||
ViewPassword string // 前台查看密码
|
||||
Brand string // 站点名称
|
||||
CookieName string // 浏览器 Cookie 名称
|
||||
Theme string
|
||||
DashboardTheme string
|
||||
CustomCode string
|
||||
CustomCodeDashboard string
|
||||
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
|
||||
@@ -115,29 +127,7 @@ type Config struct {
|
||||
MaxTCPPingValue int32
|
||||
AvgPingCount int
|
||||
|
||||
// 动态域名解析更新
|
||||
DDNS struct {
|
||||
Enable bool
|
||||
Provider string
|
||||
AccessID string
|
||||
AccessSecret string
|
||||
WebhookURL string
|
||||
WebhookMethod string
|
||||
WebhookRequestBody string
|
||||
WebhookHeaders string
|
||||
MaxRetries uint32
|
||||
Profiles map[string]DDNSProfile
|
||||
}
|
||||
}
|
||||
|
||||
type DDNSProfile struct {
|
||||
Provider string
|
||||
AccessID string
|
||||
AccessSecret string
|
||||
WebhookURL string
|
||||
WebhookMethod string
|
||||
WebhookRequestBody string
|
||||
WebhookHeaders string
|
||||
DNSServers string
|
||||
}
|
||||
|
||||
// Read 读取配置文件并应用
|
||||
@@ -178,8 +168,17 @@ func (c *Config) Read(path string) error {
|
||||
if c.AvgPingCount == 0 {
|
||||
c.AvgPingCount = 2
|
||||
}
|
||||
if c.DDNS.MaxRetries == 0 {
|
||||
c.DDNS.MaxRetries = 3
|
||||
if c.Oauth2.OidcScopes == "" {
|
||||
c.Oauth2.OidcScopes = "openid,profile,email"
|
||||
}
|
||||
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()
|
||||
|
||||
+100
@@ -0,0 +1,100 @@
|
||||
package model
|
||||
|
||||
import (
|
||||
"strings"
|
||||
|
||||
"gorm.io/gorm"
|
||||
)
|
||||
|
||||
const (
|
||||
ProviderDummy = iota
|
||||
ProviderWebHook
|
||||
ProviderCloudflare
|
||||
ProviderTencentCloud
|
||||
)
|
||||
|
||||
const (
|
||||
_Dummy = "dummy"
|
||||
_WebHook = "webhook"
|
||||
_Cloudflare = "cloudflare"
|
||||
_TencentCloud = "tencentcloud"
|
||||
)
|
||||
|
||||
var ProviderMap = map[uint8]string{
|
||||
ProviderDummy: _Dummy,
|
||||
ProviderWebHook: _WebHook,
|
||||
ProviderCloudflare: _Cloudflare,
|
||||
ProviderTencentCloud: _TencentCloud,
|
||||
}
|
||||
|
||||
var ProviderList = []DDNSProvider{
|
||||
{
|
||||
Name: _Dummy,
|
||||
ID: ProviderDummy,
|
||||
},
|
||||
{
|
||||
Name: _Cloudflare,
|
||||
ID: ProviderCloudflare,
|
||||
AccessSecret: true,
|
||||
},
|
||||
{
|
||||
Name: _TencentCloud,
|
||||
ID: ProviderTencentCloud,
|
||||
AccessID: true,
|
||||
AccessSecret: true,
|
||||
},
|
||||
// Least frequently used, always place this at the end
|
||||
{
|
||||
Name: _WebHook,
|
||||
ID: ProviderWebHook,
|
||||
AccessID: true,
|
||||
AccessSecret: true,
|
||||
WebhookURL: true,
|
||||
WebhookMethod: true,
|
||||
WebhookRequestType: true,
|
||||
WebhookRequestBody: true,
|
||||
WebhookHeaders: true,
|
||||
},
|
||||
}
|
||||
|
||||
type DDNSProfile struct {
|
||||
Common
|
||||
EnableIPv4 *bool
|
||||
EnableIPv6 *bool
|
||||
MaxRetries uint64
|
||||
Name string
|
||||
Provider uint8
|
||||
AccessID string
|
||||
AccessSecret string
|
||||
WebhookURL string
|
||||
WebhookMethod uint8
|
||||
WebhookRequestType uint8
|
||||
WebhookRequestBody string
|
||||
WebhookHeaders string
|
||||
|
||||
Domains []string `gorm:"-"`
|
||||
DomainsRaw string
|
||||
}
|
||||
|
||||
func (d DDNSProfile) TableName() string {
|
||||
return "ddns"
|
||||
}
|
||||
|
||||
func (d *DDNSProfile) AfterFind(tx *gorm.DB) error {
|
||||
if d.DomainsRaw != "" {
|
||||
d.Domains = strings.Split(d.DomainsRaw, ",")
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
type DDNSProvider struct {
|
||||
Name string
|
||||
ID uint8
|
||||
AccessID bool
|
||||
AccessSecret bool
|
||||
WebhookURL bool
|
||||
WebhookMethod bool
|
||||
WebhookRequestType bool
|
||||
WebhookRequestBody bool
|
||||
WebhookHeaders bool
|
||||
}
|
||||
+16
-10
@@ -13,24 +13,30 @@ import (
|
||||
|
||||
const (
|
||||
_ = iota
|
||||
TaskTypeHTTPGET
|
||||
TaskTypeHTTPGet
|
||||
TaskTypeICMPPing
|
||||
TaskTypeTCPPing
|
||||
TaskTypeCommand
|
||||
TaskTypeTerminal
|
||||
TaskTypeUpgrade
|
||||
TaskTypeKeepalive
|
||||
TaskTypeTerminalGRPC
|
||||
TaskTypeNAT
|
||||
TaskTypeReportHostInfo
|
||||
TaskTypeFM
|
||||
)
|
||||
|
||||
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
|
||||
}
|
||||
|
||||
type TaskFM struct {
|
||||
StreamID string
|
||||
}
|
||||
|
||||
const (
|
||||
@@ -120,7 +126,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 {
|
||||
|
||||
@@ -0,0 +1,9 @@
|
||||
package model
|
||||
|
||||
type NAT struct {
|
||||
Common
|
||||
Name string
|
||||
ServerID uint64
|
||||
Host string
|
||||
Domain string `gorm:"unique"`
|
||||
}
|
||||
+13
-4
@@ -71,8 +71,8 @@ func (ns *NotificationServerBundle) reqBody(message string) (string, error) {
|
||||
return string(msgBytes)[1 : len(msgBytes)-1]
|
||||
}), nil
|
||||
case NotificationRequestTypeForm:
|
||||
var data map[string]string
|
||||
if err := utils.Json.Unmarshal([]byte(n.RequestBody), &data); err != nil {
|
||||
data, err := utils.GjsonParseStringMap(n.RequestBody)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
params := url.Values{}
|
||||
@@ -99,8 +99,8 @@ func (n *Notification) setRequestHeader(req *http.Request) error {
|
||||
if n.RequestHeader == "" {
|
||||
return nil
|
||||
}
|
||||
var m map[string]string
|
||||
if err := utils.Json.Unmarshal([]byte(n.RequestHeader), &m); err != nil {
|
||||
m, err := utils.GjsonParseStringMap(n.RequestHeader)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
for k, v := range m {
|
||||
@@ -170,14 +170,23 @@ func (ns *NotificationServerBundle) replaceParamsInString(str string, message st
|
||||
|
||||
if ns.Server != nil {
|
||||
str = strings.ReplaceAll(str, "#SERVER.NAME#", mod(ns.Server.Name))
|
||||
str = strings.ReplaceAll(str, "#SERVER.ID#", mod(fmt.Sprintf("%d", ns.Server.ID)))
|
||||
str = strings.ReplaceAll(str, "#SERVER.CPU#", mod(fmt.Sprintf("%f", ns.Server.State.CPU)))
|
||||
str = strings.ReplaceAll(str, "#SERVER.MEM#", mod(fmt.Sprintf("%d", ns.Server.State.MemUsed)))
|
||||
str = strings.ReplaceAll(str, "#SERVER.SWAP#", mod(fmt.Sprintf("%d", ns.Server.State.SwapUsed)))
|
||||
str = strings.ReplaceAll(str, "#SERVER.DISK#", mod(fmt.Sprintf("%d", ns.Server.State.DiskUsed)))
|
||||
str = strings.ReplaceAll(str, "#SERVER.MEMUSED#", mod(fmt.Sprintf("%d", ns.Server.State.MemUsed)))
|
||||
str = strings.ReplaceAll(str, "#SERVER.SWAPUSED#", mod(fmt.Sprintf("%d", ns.Server.State.SwapUsed)))
|
||||
str = strings.ReplaceAll(str, "#SERVER.DISKUSED#", mod(fmt.Sprintf("%d", ns.Server.State.DiskUsed)))
|
||||
str = strings.ReplaceAll(str, "#SERVER.MEMTOTAL#", mod(fmt.Sprintf("%d", ns.Server.Host.MemTotal)))
|
||||
str = strings.ReplaceAll(str, "#SERVER.SWAPTOTAL#", mod(fmt.Sprintf("%d", ns.Server.Host.SwapTotal)))
|
||||
str = strings.ReplaceAll(str, "#SERVER.DISKTOTAL#", mod(fmt.Sprintf("%d", ns.Server.Host.DiskTotal)))
|
||||
str = strings.ReplaceAll(str, "#SERVER.NETINSPEED#", mod(fmt.Sprintf("%d", ns.Server.State.NetInSpeed)))
|
||||
str = strings.ReplaceAll(str, "#SERVER.NETOUTSPEED#", mod(fmt.Sprintf("%d", ns.Server.State.NetOutSpeed)))
|
||||
str = strings.ReplaceAll(str, "#SERVER.TRANSFERIN#", mod(fmt.Sprintf("%d", ns.Server.State.NetInTransfer)))
|
||||
str = strings.ReplaceAll(str, "#SERVER.TRANSFEROUT#", mod(fmt.Sprintf("%d", ns.Server.State.NetOutTransfer)))
|
||||
str = strings.ReplaceAll(str, "#SERVER.NETINTRANSFER#", mod(fmt.Sprintf("%d", ns.Server.State.NetInTransfer)))
|
||||
str = strings.ReplaceAll(str, "#SERVER.NETOUTTRANSFER#", mod(fmt.Sprintf("%d", ns.Server.State.NetOutTransfer)))
|
||||
str = strings.ReplaceAll(str, "#SERVER.LOAD1#", mod(fmt.Sprintf("%f", ns.Server.State.Load1)))
|
||||
str = strings.ReplaceAll(str, "#SERVER.LOAD5#", mod(fmt.Sprintf("%f", ns.Server.State.Load5)))
|
||||
str = strings.ReplaceAll(str, "#SERVER.LOAD15#", mod(fmt.Sprintf("%f", ns.Server.State.Load15)))
|
||||
|
||||
@@ -72,11 +72,11 @@ func execCase(t *testing.T, item testSt) {
|
||||
UdpConnCount: 0,
|
||||
ProcessCount: 0,
|
||||
},
|
||||
LastActive: time.Time{},
|
||||
TaskClose: nil,
|
||||
TaskStream: nil,
|
||||
PrevHourlyTransferIn: 0,
|
||||
PrevHourlyTransferOut: 0,
|
||||
LastActive: time.Time{},
|
||||
TaskClose: nil,
|
||||
TaskStream: nil,
|
||||
PrevTransferInSnapshot: 0,
|
||||
PrevTransferOutSnapshot: 0,
|
||||
}
|
||||
ns := NotificationServerBundle{
|
||||
Notification: &n,
|
||||
|
||||
+7
-17
@@ -1,10 +1,13 @@
|
||||
package model
|
||||
|
||||
import (
|
||||
"slices"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"gorm.io/gorm"
|
||||
|
||||
"github.com/naiba/nezha/pkg/utils"
|
||||
)
|
||||
|
||||
const (
|
||||
@@ -42,19 +45,6 @@ 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{} {
|
||||
// 监控全部但是排除了此服务器
|
||||
@@ -103,21 +93,21 @@ func (u *Rule) Snapshot(cycleTransferStats *CycleTransferStats, server *Server,
|
||||
src = float64(server.LastActive.Unix())
|
||||
}
|
||||
case "transfer_in_cycle":
|
||||
src = float64(server.State.NetInTransfer - uint64(server.PrevHourlyTransferIn))
|
||||
src = float64(utils.Uint64SubInt64(server.State.NetInTransfer, server.PrevTransferInSnapshot))
|
||||
if u.CycleInterval != 0 {
|
||||
var res NResult
|
||||
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))
|
||||
src = float64(utils.Uint64SubInt64(server.State.NetOutTransfer, server.PrevTransferOutSnapshot))
|
||||
if u.CycleInterval != 0 {
|
||||
var res NResult
|
||||
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))
|
||||
src = float64(utils.Uint64SubInt64(server.State.NetOutTransfer, server.PrevTransferOutSnapshot) + utils.Uint64SubInt64(server.State.NetInTransfer, server.PrevTransferInSnapshot))
|
||||
if u.CycleInterval != 0 {
|
||||
var res NResult
|
||||
db.Model(&Transfer{}).Select("SUM(`in`+`out`) AS n").Where("datetime(`created_at`) >= datetime(?) AND server_id = ?", u.GetTransferDurationStart().UTC(), server.ID).Scan(&res)
|
||||
@@ -143,7 +133,7 @@ func (u *Rule) Snapshot(cycleTransferStats *CycleTransferStats, server *Server,
|
||||
temp = append(temp, tempStat.Temperature)
|
||||
}
|
||||
}
|
||||
src = maxSliceValue(temp)
|
||||
src = slices.Max(temp)
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
+35
-20
@@ -3,35 +3,39 @@ package model
|
||||
import (
|
||||
"fmt"
|
||||
"html/template"
|
||||
"log"
|
||||
"sync"
|
||||
"time"
|
||||
|
||||
"github.com/naiba/nezha/pkg/utils"
|
||||
pb "github.com/naiba/nezha/proto"
|
||||
"gorm.io/gorm"
|
||||
)
|
||||
|
||||
type Server struct {
|
||||
Common
|
||||
Name string
|
||||
Tag string // 分组名
|
||||
Secret string `gorm:"uniqueIndex" json:"-"`
|
||||
Note string `json:"-"` // 管理员可见备注
|
||||
DisplayIndex int // 展示排序,越大越靠前
|
||||
HideForGuest bool // 对游客隐藏
|
||||
EnableDDNS bool // 是否启用DDNS 未在配置文件中启用DDNS 或 DDNS检查时间为0时此项无效
|
||||
EnableIPv4 bool // 是否启用DDNS IPv4
|
||||
EnableIpv6 bool // 是否启用DDNS IPv6
|
||||
DDNSDomain string // DDNS中的前缀 如基础域名为abc.oracle DDNSName为mjj 就会把mjj.abc.oracle解析服务器IP 为空则停用
|
||||
DDNSProfile string // DDNS配置
|
||||
Tag string // 分组名
|
||||
Secret string `gorm:"uniqueIndex" json:"-"`
|
||||
Note string `json:"-"` // 管理员可见备注
|
||||
PublicNote string `json:"PublicNote,omitempty"` // 公开备注
|
||||
DisplayIndex int // 展示排序,越大越靠前
|
||||
HideForGuest bool // 对游客隐藏
|
||||
EnableDDNS bool // 启用DDNS
|
||||
DDNSProfiles []uint64 `gorm:"-" json:"-"` // DDNS配置
|
||||
|
||||
DDNSProfilesRaw string `gorm:"default:'[]';column:ddns_profiles_raw" json:"-"`
|
||||
|
||||
Host *Host `gorm:"-"`
|
||||
State *HostState `gorm:"-"`
|
||||
LastActive time.Time `gorm:"-"`
|
||||
|
||||
TaskClose chan error `gorm:"-" json:"-"`
|
||||
TaskStream pb.NezhaService_RequestTaskServer `gorm:"-" json:"-"`
|
||||
TaskClose chan error `gorm:"-" json:"-"`
|
||||
TaskCloseLock *sync.Mutex `gorm:"-" json:"-"`
|
||||
TaskStream pb.NezhaService_RequestTaskServer `gorm:"-" json:"-"`
|
||||
|
||||
PrevHourlyTransferIn int64 `gorm:"-" json:"-"` // 上次数据点时的入站使用量
|
||||
PrevHourlyTransferOut int64 `gorm:"-" json:"-"` // 上次数据点时的出站使用量
|
||||
PrevTransferInSnapshot int64 `gorm:"-" json:"-"` // 上次数据点时的入站使用量
|
||||
PrevTransferOutSnapshot int64 `gorm:"-" json:"-"` // 上次数据点时的出站使用量
|
||||
}
|
||||
|
||||
func (s *Server) CopyFromRunningServer(old *Server) {
|
||||
@@ -39,9 +43,20 @@ func (s *Server) CopyFromRunningServer(old *Server) {
|
||||
s.State = old.State
|
||||
s.LastActive = old.LastActive
|
||||
s.TaskClose = old.TaskClose
|
||||
s.TaskCloseLock = old.TaskCloseLock
|
||||
s.TaskStream = old.TaskStream
|
||||
s.PrevHourlyTransferIn = old.PrevHourlyTransferIn
|
||||
s.PrevHourlyTransferOut = old.PrevHourlyTransferOut
|
||||
s.PrevTransferInSnapshot = old.PrevTransferInSnapshot
|
||||
s.PrevTransferOutSnapshot = old.PrevTransferOutSnapshot
|
||||
}
|
||||
|
||||
func (s *Server) AfterFind(tx *gorm.DB) error {
|
||||
if s.DDNSProfilesRaw != "" {
|
||||
if err := utils.Json.Unmarshal([]byte(s.DDNSProfilesRaw), &s.DDNSProfiles); err != nil {
|
||||
log.Println("NEZHA>> Server.AfterFind:", err)
|
||||
return nil
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func boolToString(b bool) string {
|
||||
@@ -51,12 +66,12 @@ func boolToString(b bool) string {
|
||||
return "false"
|
||||
}
|
||||
|
||||
func (s Server) Marshal() template.JS {
|
||||
func (s Server) MarshalForDashboard() template.JS {
|
||||
name, _ := utils.Json.Marshal(s.Name)
|
||||
tag, _ := utils.Json.Marshal(s.Tag)
|
||||
note, _ := utils.Json.Marshal(s.Note)
|
||||
secret, _ := utils.Json.Marshal(s.Secret)
|
||||
ddnsDomain, _ := utils.Json.Marshal(s.DDNSDomain)
|
||||
ddnsProfile, _ := utils.Json.Marshal(s.DDNSProfile)
|
||||
return template.JS(fmt.Sprintf(`{"ID":%d,"Name":%s,"Secret":%s,"DisplayIndex":%d,"Tag":%s,"Note":%s,"HideForGuest": %s,"EnableDDNS": %s,"EnableIPv4": %s,"EnableIpv6": %s,"DDNSDomain": %s,"DDNSProfile": %s}`, s.ID, name, secret, s.DisplayIndex, tag, note, boolToString(s.HideForGuest), boolToString(s.EnableDDNS), boolToString(s.EnableIPv4), boolToString(s.EnableIpv6), ddnsDomain, ddnsProfile)) // #nosec
|
||||
ddnsProfilesRaw, _ := utils.Json.Marshal(s.DDNSProfilesRaw)
|
||||
publicNote, _ := utils.Json.Marshal(s.PublicNote)
|
||||
return template.JS(fmt.Sprintf(`{"ID":%d,"Name":%s,"Secret":%s,"DisplayIndex":%d,"Tag":%s,"Note":%s,"HideForGuest": %s,"EnableDDNS": %s,"DDNSProfilesRaw": %s,"PublicNote": %s}`, s.ID, name, secret, s.DisplayIndex, tag, note, boolToString(s.HideForGuest), boolToString(s.EnableDDNS), ddnsProfilesRaw, publicNote))
|
||||
}
|
||||
|
||||
@@ -18,7 +18,7 @@ func TestServerMarshal(t *testing.T) {
|
||||
Name: patterns[i],
|
||||
Tag: patterns[i],
|
||||
}
|
||||
serverStr := string(server.Marshal())
|
||||
serverStr := string(server.MarshalForDashboard())
|
||||
var serverRestore Server
|
||||
if utils.Json.Unmarshal([]byte(serverStr), &serverRestore) != nil {
|
||||
t.Fatalf("Error: %s", serverStr)
|
||||
|
||||
@@ -1,177 +0,0 @@
|
||||
package ddns
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"io"
|
||||
"log"
|
||||
"net/http"
|
||||
)
|
||||
|
||||
type ProviderCloudflare struct {
|
||||
Secret string
|
||||
}
|
||||
|
||||
func (provider *ProviderCloudflare) UpdateDomain(domainConfig *DomainConfig) bool {
|
||||
if domainConfig == nil {
|
||||
return false
|
||||
}
|
||||
|
||||
zoneID, err := provider.getZoneID(domainConfig.FullDomain)
|
||||
if err != nil {
|
||||
log.Printf("无法获取 zone ID: %s\n", err)
|
||||
return false
|
||||
}
|
||||
|
||||
// 当IPv4和IPv6同时成功才算作成功
|
||||
var resultV4 = true
|
||||
var resultV6 = true
|
||||
if domainConfig.EnableIPv4 {
|
||||
if !provider.addDomainRecord(zoneID, domainConfig, true) {
|
||||
resultV4 = false
|
||||
}
|
||||
}
|
||||
|
||||
if domainConfig.EnableIpv6 {
|
||||
if !provider.addDomainRecord(zoneID, domainConfig, false) {
|
||||
resultV6 = false
|
||||
}
|
||||
}
|
||||
|
||||
return resultV4 && resultV6
|
||||
}
|
||||
|
||||
func (provider *ProviderCloudflare) addDomainRecord(zoneID string, domainConfig *DomainConfig, isIpv4 bool) bool {
|
||||
record, err := provider.findDNSRecord(zoneID, domainConfig.FullDomain, isIpv4)
|
||||
if err != nil {
|
||||
log.Printf("查找 DNS 记录时出错: %s\n", err)
|
||||
return false
|
||||
}
|
||||
|
||||
if record == nil {
|
||||
// 添加 DNS 记录
|
||||
return provider.createDNSRecord(zoneID, domainConfig, isIpv4)
|
||||
} else {
|
||||
// 更新 DNS 记录
|
||||
return provider.updateDNSRecord(zoneID, record["id"].(string), domainConfig, isIpv4)
|
||||
}
|
||||
}
|
||||
|
||||
func (provider *ProviderCloudflare) getZoneID(domain string) (string, error) {
|
||||
_, realDomain := SplitDomain(domain)
|
||||
url := fmt.Sprintf("https://api.cloudflare.com/client/v4/zones?name=%s", realDomain)
|
||||
body, err := provider.sendRequest("GET", url, nil)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
|
||||
var res map[string]interface{}
|
||||
err = json.Unmarshal(body, &res)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
|
||||
result := res["result"].([]interface{})
|
||||
if len(result) > 0 {
|
||||
zoneID := result[0].(map[string]interface{})["id"].(string)
|
||||
return zoneID, nil
|
||||
}
|
||||
|
||||
return "", fmt.Errorf("找不到 Zone ID")
|
||||
}
|
||||
|
||||
func (provider *ProviderCloudflare) findDNSRecord(zoneID string, domain string, isIPv4 bool) (map[string]interface{}, error) {
|
||||
var ipType = "A"
|
||||
if !isIPv4 {
|
||||
ipType = "AAAA"
|
||||
}
|
||||
url := fmt.Sprintf("https://api.cloudflare.com/client/v4/zones/%s/dns_records?type=%s&name=%s", zoneID, ipType, domain)
|
||||
body, err := provider.sendRequest("GET", url, nil)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
var res map[string]interface{}
|
||||
err = json.Unmarshal(body, &res)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
result := res["result"].([]interface{})
|
||||
if len(result) > 0 {
|
||||
return result[0].(map[string]interface{}), nil
|
||||
}
|
||||
|
||||
return nil, nil // 没有找到 DNS 记录
|
||||
}
|
||||
|
||||
func (provider *ProviderCloudflare) createDNSRecord(zoneID string, domainConfig *DomainConfig, isIPv4 bool) bool {
|
||||
var ipType = "A"
|
||||
var ipAddr = domainConfig.Ipv4Addr
|
||||
if !isIPv4 {
|
||||
ipType = "AAAA"
|
||||
ipAddr = domainConfig.Ipv6Addr
|
||||
}
|
||||
url := fmt.Sprintf("https://api.cloudflare.com/client/v4/zones/%s/dns_records", zoneID)
|
||||
data := map[string]interface{}{
|
||||
"type": ipType,
|
||||
"name": domainConfig.FullDomain,
|
||||
"content": ipAddr,
|
||||
"ttl": 60,
|
||||
"proxied": false,
|
||||
}
|
||||
jsonData, _ := json.Marshal(data)
|
||||
_, err := provider.sendRequest("POST", url, jsonData)
|
||||
return err == nil
|
||||
}
|
||||
|
||||
func (provider *ProviderCloudflare) updateDNSRecord(zoneID string, recordID string, domainConfig *DomainConfig, isIPv4 bool) bool {
|
||||
var ipType = "A"
|
||||
var ipAddr = domainConfig.Ipv4Addr
|
||||
if !isIPv4 {
|
||||
ipType = "AAAA"
|
||||
ipAddr = domainConfig.Ipv6Addr
|
||||
}
|
||||
url := fmt.Sprintf("https://api.cloudflare.com/client/v4/zones/%s/dns_records/%s", zoneID, recordID)
|
||||
data := map[string]interface{}{
|
||||
"type": ipType,
|
||||
"name": domainConfig.FullDomain,
|
||||
"content": ipAddr,
|
||||
"ttl": 60,
|
||||
"proxied": false,
|
||||
}
|
||||
jsonData, _ := json.Marshal(data)
|
||||
_, err := provider.sendRequest("PATCH", url, jsonData)
|
||||
return err == nil
|
||||
}
|
||||
|
||||
// 以下为辅助方法,如发送 HTTP 请求等
|
||||
func (provider *ProviderCloudflare) sendRequest(method string, url string, data []byte) ([]byte, error) {
|
||||
client := &http.Client{}
|
||||
req, err := http.NewRequest(method, url, bytes.NewBuffer(data))
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
req.Header.Add("Authorization", fmt.Sprintf("Bearer %s", provider.Secret))
|
||||
req.Header.Add("Content-Type", "application/json")
|
||||
|
||||
resp, err := client.Do(req)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
defer func(Body io.ReadCloser) {
|
||||
err := Body.Close()
|
||||
if err != nil {
|
||||
log.Printf("NEZHA>> 无法关闭HTTP响应体流: %s\n", err.Error())
|
||||
}
|
||||
}(resp.Body)
|
||||
|
||||
body, err := io.ReadAll(resp.Body)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return body, nil
|
||||
}
|
||||
+136
-9
@@ -1,14 +1,141 @@
|
||||
package ddns
|
||||
|
||||
type DomainConfig struct {
|
||||
EnableIPv4 bool
|
||||
EnableIpv6 bool
|
||||
FullDomain string
|
||||
Ipv4Addr string
|
||||
Ipv6Addr string
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"log"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"github.com/libdns/libdns"
|
||||
"github.com/miekg/dns"
|
||||
|
||||
"github.com/naiba/nezha/model"
|
||||
"github.com/naiba/nezha/pkg/utils"
|
||||
)
|
||||
|
||||
var (
|
||||
dnsTimeOut = 10 * time.Second
|
||||
customDNSServers []string
|
||||
)
|
||||
|
||||
type IP struct {
|
||||
Ipv4Addr string
|
||||
Ipv6Addr string
|
||||
}
|
||||
|
||||
type Provider interface {
|
||||
// UpdateDomain Return is updated
|
||||
UpdateDomain(domainConfig *DomainConfig) bool
|
||||
type Provider struct {
|
||||
ctx context.Context
|
||||
ipAddr string
|
||||
recordType string
|
||||
domain string
|
||||
prefix string
|
||||
zone string
|
||||
|
||||
DDNSProfile *model.DDNSProfile
|
||||
IPAddrs *IP
|
||||
Setter libdns.RecordSetter
|
||||
}
|
||||
|
||||
func InitDNSServers(s string) {
|
||||
if s != "" {
|
||||
customDNSServers = strings.Split(s, ",")
|
||||
}
|
||||
}
|
||||
|
||||
func (provider *Provider) UpdateDomain(ctx context.Context) {
|
||||
provider.ctx = ctx
|
||||
for _, domain := range provider.DDNSProfile.Domains {
|
||||
for retries := 0; retries < int(provider.DDNSProfile.MaxRetries); retries++ {
|
||||
provider.domain = domain
|
||||
log.Printf("NEZHA>> 正在尝试更新域名(%s)DDNS(%d/%d)", provider.domain, retries+1, provider.DDNSProfile.MaxRetries)
|
||||
if err := provider.updateDomain(); err != nil {
|
||||
log.Printf("NEZHA>> 尝试更新域名(%s)DDNS失败: %v", provider.domain, err)
|
||||
} else {
|
||||
log.Printf("NEZHA>> 尝试更新域名(%s)DDNS成功", provider.domain)
|
||||
break
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func (provider *Provider) updateDomain() error {
|
||||
var err error
|
||||
provider.prefix, provider.zone, err = splitDomainSOA(provider.domain)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
// 当IPv4和IPv6同时成功才算作成功
|
||||
if *provider.DDNSProfile.EnableIPv4 {
|
||||
provider.recordType = getRecordString(true)
|
||||
provider.ipAddr = provider.IPAddrs.Ipv4Addr
|
||||
if err = provider.addDomainRecord(); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
if *provider.DDNSProfile.EnableIPv6 {
|
||||
provider.recordType = getRecordString(false)
|
||||
provider.ipAddr = provider.IPAddrs.Ipv6Addr
|
||||
if err = provider.addDomainRecord(); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (provider *Provider) addDomainRecord() error {
|
||||
_, err := provider.Setter.SetRecords(provider.ctx, provider.zone,
|
||||
[]libdns.Record{
|
||||
{
|
||||
Type: provider.recordType,
|
||||
Name: provider.prefix,
|
||||
Value: provider.ipAddr,
|
||||
TTL: time.Minute,
|
||||
},
|
||||
})
|
||||
return err
|
||||
}
|
||||
|
||||
func splitDomainSOA(domain string) (prefix string, zone string, err error) {
|
||||
c := &dns.Client{Timeout: dnsTimeOut}
|
||||
|
||||
domain += "."
|
||||
indexes := dns.Split(domain)
|
||||
|
||||
servers := utils.DNSServers
|
||||
if len(customDNSServers) > 0 {
|
||||
servers = customDNSServers
|
||||
}
|
||||
|
||||
var r *dns.Msg
|
||||
for _, idx := range indexes {
|
||||
m := new(dns.Msg)
|
||||
m.SetQuestion(domain[idx:], dns.TypeSOA)
|
||||
|
||||
for _, server := range servers {
|
||||
r, _, err = c.Exchange(m, server)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
if len(r.Answer) > 0 {
|
||||
if soa, ok := r.Answer[0].(*dns.SOA); ok {
|
||||
zone = soa.Hdr.Name
|
||||
prefix = libdns.RelativeName(domain, zone)
|
||||
return
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return "", "", fmt.Errorf("SOA record not found for domain: %s", domain)
|
||||
}
|
||||
|
||||
func getRecordString(isIpv4 bool) string {
|
||||
if isIpv4 {
|
||||
return "A"
|
||||
}
|
||||
return "AAAA"
|
||||
}
|
||||
|
||||
@@ -0,0 +1,49 @@
|
||||
package ddns
|
||||
|
||||
import (
|
||||
"os"
|
||||
"testing"
|
||||
)
|
||||
|
||||
type testSt struct {
|
||||
domain string
|
||||
zone string
|
||||
prefix string
|
||||
}
|
||||
|
||||
func TestSplitDomainSOA(t *testing.T) {
|
||||
if ci := os.Getenv("CI"); ci != "" { // skip if test on CI
|
||||
return
|
||||
}
|
||||
|
||||
cases := []testSt{
|
||||
{
|
||||
domain: "www.example.co.uk",
|
||||
zone: "example.co.uk.",
|
||||
prefix: "www",
|
||||
},
|
||||
{
|
||||
domain: "abc.example.com",
|
||||
zone: "example.com.",
|
||||
prefix: "abc",
|
||||
},
|
||||
{
|
||||
domain: "example.com",
|
||||
zone: "example.com.",
|
||||
prefix: "",
|
||||
},
|
||||
}
|
||||
|
||||
for _, c := range cases {
|
||||
prefix, zone, err := splitDomainSOA(c.domain)
|
||||
if err != nil {
|
||||
t.Fatalf("Error: %s", err)
|
||||
}
|
||||
if prefix != c.prefix {
|
||||
t.Fatalf("Expected prefix %s, but got %s", c.prefix, prefix)
|
||||
}
|
||||
if zone != c.zone {
|
||||
t.Fatalf("Expected zone %s, but got %s", c.zone, zone)
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,7 +0,0 @@
|
||||
package ddns
|
||||
|
||||
type ProviderDummy struct{}
|
||||
|
||||
func (provider *ProviderDummy) UpdateDomain(domainConfig *DomainConfig) bool {
|
||||
return false
|
||||
}
|
||||
@@ -0,0 +1,16 @@
|
||||
package dummy
|
||||
|
||||
import (
|
||||
"context"
|
||||
|
||||
"github.com/libdns/libdns"
|
||||
)
|
||||
|
||||
// Internal use
|
||||
type Provider struct {
|
||||
}
|
||||
|
||||
func (provider *Provider) SetRecords(ctx context.Context, zone string,
|
||||
recs []libdns.Record) ([]libdns.Record, error) {
|
||||
return recs, nil
|
||||
}
|
||||
@@ -1,40 +0,0 @@
|
||||
package ddns
|
||||
|
||||
import (
|
||||
"golang.org/x/net/publicsuffix"
|
||||
"net/http"
|
||||
"strings"
|
||||
)
|
||||
|
||||
func (provider ProviderWebHook) FormatWebhookString(s string, config *DomainConfig, ipType string) string {
|
||||
if config == nil {
|
||||
return s
|
||||
}
|
||||
|
||||
result := strings.TrimSpace(s)
|
||||
result = strings.Replace(s, "{ip}", config.Ipv4Addr, -1)
|
||||
result = strings.Replace(result, "{domain}", config.FullDomain, -1)
|
||||
result = strings.Replace(result, "{type}", ipType, -1)
|
||||
// remove \r
|
||||
result = strings.Replace(result, "\r", "", -1)
|
||||
return result
|
||||
}
|
||||
|
||||
func SetStringHeadersToRequest(req *http.Request, headers []string) {
|
||||
if req == nil {
|
||||
return
|
||||
}
|
||||
for _, element := range headers {
|
||||
kv := strings.SplitN(element, ":", 2)
|
||||
if len(kv) == 2 {
|
||||
req.Header.Add(kv[0], kv[1])
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// SplitDomain 分割域名为前缀和一级域名
|
||||
func SplitDomain(domain string) (prefix string, realDomain string) {
|
||||
realDomain, _ = publicsuffix.EffectiveTLDPlusOne(domain)
|
||||
prefix = domain[:len(domain)-len(realDomain)-1]
|
||||
return prefix, realDomain
|
||||
}
|
||||
@@ -1,228 +0,0 @@
|
||||
package ddns
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"crypto/hmac"
|
||||
"crypto/sha256"
|
||||
"encoding/hex"
|
||||
"encoding/json"
|
||||
"io"
|
||||
"log"
|
||||
"net/http"
|
||||
"strconv"
|
||||
"strings"
|
||||
"time"
|
||||
)
|
||||
|
||||
const (
|
||||
url = "https://dnspod.tencentcloudapi.com"
|
||||
)
|
||||
|
||||
type ProviderTencentCloud struct {
|
||||
SecretID string
|
||||
SecretKey string
|
||||
}
|
||||
|
||||
func (provider *ProviderTencentCloud) UpdateDomain(domainConfig *DomainConfig) bool {
|
||||
if domainConfig == nil {
|
||||
return false
|
||||
}
|
||||
|
||||
// 当IPv4和IPv6同时成功才算作成功
|
||||
var resultV4 = true
|
||||
var resultV6 = true
|
||||
if domainConfig.EnableIPv4 {
|
||||
if !provider.addDomainRecord(domainConfig, true) {
|
||||
resultV4 = false
|
||||
}
|
||||
}
|
||||
|
||||
if domainConfig.EnableIpv6 {
|
||||
if !provider.addDomainRecord(domainConfig, false) {
|
||||
resultV6 = false
|
||||
}
|
||||
}
|
||||
|
||||
return resultV4 && resultV6
|
||||
}
|
||||
|
||||
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)
|
||||
return false
|
||||
}
|
||||
|
||||
if errResponse, ok := record["Error"].(map[string]interface{}); ok {
|
||||
if errCode, ok := errResponse["Code"].(string); ok && errCode == "ResourceNotFound.NoDataOfRecord" { // 没有找到 DNS 记录
|
||||
// 添加 DNS 记录
|
||||
return provider.createDNSRecord(domainConfig.FullDomain, domainConfig, isIpv4)
|
||||
} else {
|
||||
log.Printf("查询 DNS 记录时出错,错误代码为: %s\n", errCode)
|
||||
}
|
||||
}
|
||||
|
||||
// 默认情况下更新 DNS 记录
|
||||
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) {
|
||||
var ipType = "A"
|
||||
if !isIPv4 {
|
||||
ipType = "AAAA"
|
||||
}
|
||||
_, realDomain := SplitDomain(domain)
|
||||
prefix, _ := SplitDomain(domain)
|
||||
data := map[string]interface{}{
|
||||
"RecordType": ipType,
|
||||
"Domain": realDomain,
|
||||
"RecordLine": "默认",
|
||||
"Subdomain": prefix,
|
||||
}
|
||||
jsonData, _ := json.Marshal(data)
|
||||
body, err := provider.sendRequest("DescribeRecordList", jsonData)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
var res map[string]interface{}
|
||||
err = json.Unmarshal(body, &res)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
result := res["Response"].(map[string]interface{})
|
||||
return result, nil
|
||||
}
|
||||
|
||||
func (provider *ProviderTencentCloud) createDNSRecord(domain string, domainConfig *DomainConfig, isIPv4 bool) bool {
|
||||
var ipType = "A"
|
||||
var ipAddr = domainConfig.Ipv4Addr
|
||||
if !isIPv4 {
|
||||
ipType = "AAAA"
|
||||
ipAddr = domainConfig.Ipv6Addr
|
||||
}
|
||||
_, realDomain := SplitDomain(domain)
|
||||
prefix, _ := SplitDomain(domain)
|
||||
data := map[string]interface{}{
|
||||
"RecordType": ipType,
|
||||
"RecordLine": "默认",
|
||||
"Domain": realDomain,
|
||||
"SubDomain": prefix,
|
||||
"Value": ipAddr,
|
||||
"TTL": 600,
|
||||
}
|
||||
jsonData, _ := json.Marshal(data)
|
||||
_, err := provider.sendRequest("CreateRecord", jsonData)
|
||||
return err == nil
|
||||
}
|
||||
|
||||
func (provider *ProviderTencentCloud) updateDNSRecord(domain string, recordID float64, domainConfig *DomainConfig, isIPv4 bool) bool {
|
||||
var ipType = "A"
|
||||
var ipAddr = domainConfig.Ipv4Addr
|
||||
if !isIPv4 {
|
||||
ipType = "AAAA"
|
||||
ipAddr = domainConfig.Ipv6Addr
|
||||
}
|
||||
_, realDomain := SplitDomain(domain)
|
||||
prefix, _ := SplitDomain(domain)
|
||||
data := map[string]interface{}{
|
||||
"RecordType": ipType,
|
||||
"RecordLine": "默认",
|
||||
"Domain": realDomain,
|
||||
"SubDomain": prefix,
|
||||
"Value": ipAddr,
|
||||
"TTL": 600,
|
||||
"RecordId": recordID,
|
||||
}
|
||||
jsonData, _ := json.Marshal(data)
|
||||
_, err := provider.sendRequest("ModifyRecord", jsonData)
|
||||
return err == nil
|
||||
}
|
||||
|
||||
// 以下为辅助方法,如发送 HTTP 请求等
|
||||
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 {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
req.Header.Set("Content-Type", "application/json")
|
||||
req.Header.Set("X-TC-Version", "2021-03-23")
|
||||
|
||||
provider.signRequest(provider.SecretID, provider.SecretKey, req, action, string(data))
|
||||
resp, err := client.Do(req)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
defer func(Body io.ReadCloser) {
|
||||
err := Body.Close()
|
||||
if err != nil {
|
||||
log.Printf("NEZHA>> 无法关闭HTTP响应体流: %s\n", err.Error())
|
||||
}
|
||||
}(resp.Body)
|
||||
|
||||
body, err := io.ReadAll(resp.Body)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return body, nil
|
||||
}
|
||||
|
||||
// https://github.com/jeessy2/ddns-go/blob/master/util/tencent_cloud_signer.go
|
||||
|
||||
func (provider *ProviderTencentCloud) sha256hex(s string) string {
|
||||
b := sha256.Sum256([]byte(s))
|
||||
return hex.EncodeToString(b[:])
|
||||
}
|
||||
|
||||
func (provider *ProviderTencentCloud) hmacsha256(s, key string) string {
|
||||
hashed := hmac.New(sha256.New, []byte(key))
|
||||
hashed.Write([]byte(s))
|
||||
return string(hashed.Sum(nil))
|
||||
}
|
||||
|
||||
func (provider *ProviderTencentCloud) WriteString(strs ...string) string {
|
||||
var b strings.Builder
|
||||
for _, str := range strs {
|
||||
b.WriteString(str)
|
||||
}
|
||||
|
||||
return b.String()
|
||||
}
|
||||
|
||||
func (provider *ProviderTencentCloud) signRequest(secretId string, secretKey string, r *http.Request, action string, payload string) {
|
||||
algorithm := "TC3-HMAC-SHA256"
|
||||
service := "dnspod"
|
||||
host := provider.WriteString(service, ".tencentcloudapi.com")
|
||||
timestamp := time.Now().Unix()
|
||||
timestampStr := strconv.FormatInt(timestamp, 10)
|
||||
|
||||
// 步骤 1:拼接规范请求串
|
||||
canonicalHeaders := provider.WriteString("content-type:application/json\nhost:", host, "\nx-tc-action:", strings.ToLower(action), "\n")
|
||||
signedHeaders := "content-type;host;x-tc-action"
|
||||
hashedRequestPayload := provider.sha256hex(payload)
|
||||
canonicalRequest := provider.WriteString("POST\n/\n\n", canonicalHeaders, "\n", signedHeaders, "\n", hashedRequestPayload)
|
||||
|
||||
// 步骤 2:拼接待签名字符串
|
||||
date := time.Unix(timestamp, 0).UTC().Format("2006-01-02")
|
||||
credentialScope := provider.WriteString(date, "/", service, "/tc3_request")
|
||||
hashedCanonicalRequest := provider.sha256hex(canonicalRequest)
|
||||
string2sign := provider.WriteString(algorithm, "\n", timestampStr, "\n", credentialScope, "\n", hashedCanonicalRequest)
|
||||
|
||||
// 步骤 3:计算签名
|
||||
secretDate := provider.hmacsha256(date, provider.WriteString("TC3", secretKey))
|
||||
secretService := provider.hmacsha256(service, secretDate)
|
||||
secretSigning := provider.hmacsha256("tc3_request", secretService)
|
||||
signature := hex.EncodeToString([]byte(provider.hmacsha256(string2sign, secretSigning)))
|
||||
|
||||
// 步骤 4:拼接 Authorization
|
||||
authorization := provider.WriteString(algorithm, " Credential=", secretId, "/", credentialScope, ", SignedHeaders=", signedHeaders, ", Signature=", signature)
|
||||
|
||||
r.Header.Add("Authorization", authorization)
|
||||
r.Header.Set("Host", host)
|
||||
r.Header.Set("X-TC-Action", action)
|
||||
r.Header.Add("X-TC-Timestamp", timestampStr)
|
||||
}
|
||||
@@ -1,59 +0,0 @@
|
||||
package ddns
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"log"
|
||||
"net/http"
|
||||
"strings"
|
||||
)
|
||||
|
||||
type ProviderWebHook struct {
|
||||
URL string
|
||||
RequestMethod string
|
||||
RequestBody string
|
||||
RequestHeader string
|
||||
}
|
||||
|
||||
func (provider *ProviderWebHook) UpdateDomain(domainConfig *DomainConfig) bool {
|
||||
if domainConfig == nil {
|
||||
return false
|
||||
}
|
||||
|
||||
if domainConfig.FullDomain == "" {
|
||||
log.Println("NEZHA>> Failed to update an empty domain")
|
||||
return false
|
||||
}
|
||||
updated := false
|
||||
client := &http.Client{}
|
||||
if domainConfig.EnableIPv4 && domainConfig.Ipv4Addr != "" {
|
||||
url := provider.FormatWebhookString(provider.URL, domainConfig, "ipv4")
|
||||
body := provider.FormatWebhookString(provider.RequestBody, domainConfig, "ipv4")
|
||||
header := provider.FormatWebhookString(provider.RequestHeader, domainConfig, "ipv4")
|
||||
headers := strings.Split(header, "\n")
|
||||
req, err := http.NewRequest(provider.RequestMethod, url, bytes.NewBufferString(body))
|
||||
if err == nil && req != nil {
|
||||
SetStringHeadersToRequest(req, headers)
|
||||
if _, err := client.Do(req); err != nil {
|
||||
log.Printf("NEZHA>> Failed to update a domain: %s. Cause by: %s\n", domainConfig.FullDomain, err.Error())
|
||||
} else {
|
||||
updated = true
|
||||
}
|
||||
}
|
||||
}
|
||||
if domainConfig.EnableIpv6 && domainConfig.Ipv6Addr != "" {
|
||||
url := provider.FormatWebhookString(provider.URL, domainConfig, "ipv6")
|
||||
body := provider.FormatWebhookString(provider.RequestBody, domainConfig, "ipv6")
|
||||
header := provider.FormatWebhookString(provider.RequestHeader, domainConfig, "ipv6")
|
||||
headers := strings.Split(header, "\n")
|
||||
req, err := http.NewRequest(provider.RequestMethod, url, bytes.NewBufferString(body))
|
||||
if err == nil && req != nil {
|
||||
SetStringHeadersToRequest(req, headers)
|
||||
if _, err := client.Do(req); err != nil {
|
||||
log.Printf("NEZHA>> Failed to update a domain: %s. Cause by: %s\n", domainConfig.FullDomain, err.Error())
|
||||
} else {
|
||||
updated = true
|
||||
}
|
||||
}
|
||||
}
|
||||
return updated
|
||||
}
|
||||
@@ -0,0 +1,180 @@
|
||||
package webhook
|
||||
|
||||
import (
|
||||
"context"
|
||||
"errors"
|
||||
"fmt"
|
||||
"net/http"
|
||||
"net/url"
|
||||
"strings"
|
||||
|
||||
"github.com/libdns/libdns"
|
||||
"github.com/naiba/nezha/model"
|
||||
"github.com/naiba/nezha/pkg/utils"
|
||||
)
|
||||
|
||||
const (
|
||||
_ = iota
|
||||
methodGET
|
||||
methodPOST
|
||||
methodPATCH
|
||||
methodDELETE
|
||||
methodPUT
|
||||
)
|
||||
|
||||
const (
|
||||
_ = iota
|
||||
requestTypeJSON
|
||||
requestTypeForm
|
||||
)
|
||||
|
||||
var requestTypes = map[uint8]string{
|
||||
methodGET: "GET",
|
||||
methodPOST: "POST",
|
||||
methodPATCH: "PATCH",
|
||||
methodDELETE: "DELETE",
|
||||
methodPUT: "PUT",
|
||||
}
|
||||
|
||||
// Internal use
|
||||
type Provider struct {
|
||||
ipAddr string
|
||||
ipType string
|
||||
recordType string
|
||||
domain string
|
||||
|
||||
DDNSProfile *model.DDNSProfile
|
||||
}
|
||||
|
||||
func (provider *Provider) SetRecords(ctx context.Context, zone string,
|
||||
recs []libdns.Record) ([]libdns.Record, error) {
|
||||
for _, rec := range recs {
|
||||
provider.recordType = rec.Type
|
||||
provider.ipType = recordToIPType(provider.recordType)
|
||||
provider.ipAddr = rec.Value
|
||||
provider.domain = fmt.Sprintf("%s.%s", rec.Name, strings.TrimSuffix(zone, "."))
|
||||
|
||||
req, err := provider.prepareRequest(ctx)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to update a domain: %s. Cause by: %v", provider.domain, err)
|
||||
}
|
||||
if _, err := utils.HttpClient.Do(req); err != nil {
|
||||
return nil, fmt.Errorf("failed to update a domain: %s. Cause by: %v", provider.domain, err)
|
||||
}
|
||||
}
|
||||
|
||||
return recs, nil
|
||||
}
|
||||
|
||||
func (provider *Provider) prepareRequest(ctx context.Context) (*http.Request, error) {
|
||||
u, err := provider.reqUrl()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
body, err := provider.reqBody()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
headers, err := utils.GjsonParseStringMap(
|
||||
provider.formatWebhookString(provider.DDNSProfile.WebhookHeaders))
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
req, err := http.NewRequestWithContext(ctx, requestTypes[provider.DDNSProfile.WebhookMethod], u.String(), strings.NewReader(body))
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
provider.setContentType(req)
|
||||
|
||||
for k, v := range headers {
|
||||
req.Header.Set(k, v)
|
||||
}
|
||||
|
||||
return req, nil
|
||||
}
|
||||
|
||||
func (provider *Provider) setContentType(req *http.Request) {
|
||||
if provider.DDNSProfile.WebhookMethod == methodGET {
|
||||
return
|
||||
}
|
||||
if provider.DDNSProfile.WebhookRequestType == requestTypeForm {
|
||||
req.Header.Set("Content-Type", "application/x-www-form-urlencoded")
|
||||
} else {
|
||||
req.Header.Set("Content-Type", "application/json")
|
||||
}
|
||||
}
|
||||
|
||||
func (provider *Provider) reqUrl() (*url.URL, error) {
|
||||
formattedUrl := strings.ReplaceAll(provider.DDNSProfile.WebhookURL, "#", "%23")
|
||||
|
||||
u, err := url.Parse(formattedUrl)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
// Only handle queries here
|
||||
q := u.Query()
|
||||
for p, vals := range q {
|
||||
for n, v := range vals {
|
||||
vals[n] = provider.formatWebhookString(v)
|
||||
}
|
||||
q[p] = vals
|
||||
}
|
||||
|
||||
u.RawQuery = q.Encode()
|
||||
return u, nil
|
||||
}
|
||||
|
||||
func (provider *Provider) reqBody() (string, error) {
|
||||
if provider.DDNSProfile.WebhookMethod == methodGET ||
|
||||
provider.DDNSProfile.WebhookMethod == methodDELETE {
|
||||
return "", nil
|
||||
}
|
||||
|
||||
switch provider.DDNSProfile.WebhookRequestType {
|
||||
case requestTypeJSON:
|
||||
return provider.formatWebhookString(provider.DDNSProfile.WebhookRequestBody), nil
|
||||
case requestTypeForm:
|
||||
data, err := utils.GjsonParseStringMap(provider.DDNSProfile.WebhookRequestBody)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
params := url.Values{}
|
||||
for k, v := range data {
|
||||
params.Add(k, provider.formatWebhookString(v))
|
||||
}
|
||||
return params.Encode(), nil
|
||||
default:
|
||||
return "", errors.New("request type not supported")
|
||||
}
|
||||
}
|
||||
|
||||
func (provider *Provider) formatWebhookString(s string) string {
|
||||
r := strings.NewReplacer(
|
||||
"#ip#", provider.ipAddr,
|
||||
"#domain#", provider.domain,
|
||||
"#type#", provider.ipType,
|
||||
"#record#", provider.recordType,
|
||||
"#access_id#", provider.DDNSProfile.AccessID,
|
||||
"#access_secret#", provider.DDNSProfile.AccessSecret,
|
||||
"\r", "",
|
||||
)
|
||||
|
||||
result := r.Replace(strings.TrimSpace(s))
|
||||
return result
|
||||
}
|
||||
|
||||
func recordToIPType(record string) string {
|
||||
switch record {
|
||||
case "A":
|
||||
return "ipv4"
|
||||
case "AAAA":
|
||||
return "ipv6"
|
||||
default:
|
||||
return ""
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,116 @@
|
||||
package webhook
|
||||
|
||||
import (
|
||||
"context"
|
||||
"testing"
|
||||
|
||||
"github.com/naiba/nezha/model"
|
||||
)
|
||||
|
||||
var (
|
||||
reqTypeForm = "application/x-www-form-urlencoded"
|
||||
reqTypeJSON = "application/json"
|
||||
)
|
||||
|
||||
type testSt struct {
|
||||
profile model.DDNSProfile
|
||||
expectURL string
|
||||
expectBody string
|
||||
expectContentType string
|
||||
expectHeader map[string]string
|
||||
}
|
||||
|
||||
func execCase(t *testing.T, item testSt) {
|
||||
pw := Provider{DDNSProfile: &item.profile}
|
||||
pw.ipAddr = "1.1.1.1"
|
||||
pw.domain = item.profile.Domains[0]
|
||||
pw.ipType = "ipv4"
|
||||
pw.recordType = "A"
|
||||
pw.DDNSProfile = &item.profile
|
||||
|
||||
reqUrl, err := pw.reqUrl()
|
||||
if err != nil {
|
||||
t.Fatalf("Error: %s", err)
|
||||
}
|
||||
if item.expectURL != reqUrl.String() {
|
||||
t.Fatalf("Expected %s, but got %s", item.expectURL, reqUrl.String())
|
||||
}
|
||||
|
||||
reqBody, err := pw.reqBody()
|
||||
if err != nil {
|
||||
t.Fatalf("Error: %s", err)
|
||||
}
|
||||
if item.expectBody != reqBody {
|
||||
t.Fatalf("Expected %s, but got %s", item.expectBody, reqBody)
|
||||
}
|
||||
|
||||
req, err := pw.prepareRequest(context.Background())
|
||||
if err != nil {
|
||||
t.Fatalf("Error: %s", err)
|
||||
}
|
||||
|
||||
if item.expectContentType != req.Header.Get("Content-Type") {
|
||||
t.Fatalf("Expected %s, but got %s", item.expectContentType, req.Header.Get("Content-Type"))
|
||||
}
|
||||
|
||||
for k, v := range item.expectHeader {
|
||||
if v != req.Header.Get(k) {
|
||||
t.Fatalf("Expected %s, but got %s", v, req.Header.Get(k))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TestWebhookRequest(t *testing.T) {
|
||||
ipv4 := true
|
||||
|
||||
cases := []testSt{
|
||||
{
|
||||
profile: model.DDNSProfile{
|
||||
Domains: []string{"www.example.com"},
|
||||
MaxRetries: 1,
|
||||
EnableIPv4: &ipv4,
|
||||
WebhookURL: "http://ddns.example.com/?ip=#ip#",
|
||||
WebhookMethod: methodGET,
|
||||
WebhookHeaders: `{"ip":"#ip#","record":"#record#"}`,
|
||||
},
|
||||
expectURL: "http://ddns.example.com/?ip=1.1.1.1",
|
||||
expectContentType: "",
|
||||
expectHeader: map[string]string{
|
||||
"ip": "1.1.1.1",
|
||||
"record": "A",
|
||||
},
|
||||
},
|
||||
{
|
||||
profile: model.DDNSProfile{
|
||||
Domains: []string{"www.example.com"},
|
||||
MaxRetries: 1,
|
||||
EnableIPv4: &ipv4,
|
||||
WebhookURL: "http://ddns.example.com/api",
|
||||
WebhookMethod: methodPOST,
|
||||
WebhookRequestType: requestTypeJSON,
|
||||
WebhookRequestBody: `{"ip":"#ip#","record":"#record#"}`,
|
||||
},
|
||||
expectURL: "http://ddns.example.com/api",
|
||||
expectContentType: reqTypeJSON,
|
||||
expectBody: `{"ip":"1.1.1.1","record":"A"}`,
|
||||
},
|
||||
{
|
||||
profile: model.DDNSProfile{
|
||||
Domains: []string{"www.example.com"},
|
||||
MaxRetries: 1,
|
||||
EnableIPv4: &ipv4,
|
||||
WebhookURL: "http://ddns.example.com/api",
|
||||
WebhookMethod: methodPOST,
|
||||
WebhookRequestType: requestTypeForm,
|
||||
WebhookRequestBody: `{"ip":"#ip#","record":"#record#"}`,
|
||||
},
|
||||
expectURL: "http://ddns.example.com/api",
|
||||
expectContentType: reqTypeForm,
|
||||
expectBody: "ip=1.1.1.1&record=A",
|
||||
},
|
||||
}
|
||||
|
||||
for _, c := range cases {
|
||||
execCase(t, c)
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1 @@
|
||||
stub
|
||||
@@ -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")
|
||||
}
|
||||
@@ -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
|
||||
}
|
||||
@@ -16,6 +16,8 @@ var adminPage = map[string]bool{
|
||||
"/monitor": true,
|
||||
"/setting": true,
|
||||
"/notification": true,
|
||||
"/ddns": true,
|
||||
"/nat": true,
|
||||
"/cron": true,
|
||||
"/api": true,
|
||||
}
|
||||
@@ -25,6 +27,8 @@ func CommonEnvironment(c *gin.Context, data map[string]interface{}) gin.H {
|
||||
data["Version"] = singleton.Version
|
||||
data["Conf"] = singleton.Conf
|
||||
data["Themes"] = model.Themes
|
||||
data["CustomCode"] = singleton.Conf.Site.CustomCode
|
||||
data["CustomCodeDashboard"] = singleton.Conf.Site.CustomCodeDashboard
|
||||
// 是否是管理页面
|
||||
data["IsAdminPage"] = adminPage[data["MatchedPath"].(string)]
|
||||
// 站点标题
|
||||
|
||||
@@ -37,8 +37,7 @@ func ValidateViewPassword(opt ValidateViewPasswordOption) gin.HandlerFunc {
|
||||
}
|
||||
if opt.IsPage {
|
||||
c.HTML(http.StatusOK, GetPreferredTheme(c, "/viewpassword"), CommonEnvironment(c, gin.H{
|
||||
"Title": singleton.Localizer.MustLocalize(&i18n.LocalizeConfig{MessageID: "VerifyPassword"}),
|
||||
"CustomCode": singleton.Conf.Site.CustomCode,
|
||||
"Title": singleton.Localizer.MustLocalize(&i18n.LocalizeConfig{MessageID: "VerifyPassword"}),
|
||||
}))
|
||||
|
||||
} else {
|
||||
|
||||
@@ -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
|
||||
}
|
||||
@@ -0,0 +1,40 @@
|
||||
package utils
|
||||
|
||||
import (
|
||||
"errors"
|
||||
|
||||
"github.com/tidwall/gjson"
|
||||
)
|
||||
|
||||
var (
|
||||
ErrGjsonNotFound = errors.New("specified path does not exist")
|
||||
ErrGjsonWrongType = errors.New("wrong type")
|
||||
)
|
||||
|
||||
func GjsonGet(json []byte, path string) (gjson.Result, error) {
|
||||
result := gjson.GetBytes(json, path)
|
||||
if !result.Exists() {
|
||||
return result, ErrGjsonNotFound
|
||||
}
|
||||
|
||||
return result, nil
|
||||
}
|
||||
|
||||
func GjsonParseStringMap(jsonObject string) (map[string]string, error) {
|
||||
if jsonObject == "" {
|
||||
return nil, nil
|
||||
}
|
||||
|
||||
result := gjson.Parse(jsonObject)
|
||||
if !result.IsObject() {
|
||||
return nil, ErrGjsonWrongType
|
||||
}
|
||||
|
||||
ret := make(map[string]string)
|
||||
result.ForEach(func(key, value gjson.Result) bool {
|
||||
ret[key.String()] = value.String()
|
||||
return true
|
||||
})
|
||||
|
||||
return ret, nil
|
||||
}
|
||||
@@ -0,0 +1,33 @@
|
||||
package utils
|
||||
|
||||
import (
|
||||
"embed"
|
||||
"io/fs"
|
||||
"os"
|
||||
)
|
||||
|
||||
// HybridFS combines embed.FS and os.DirFS.
|
||||
type HybridFS struct {
|
||||
embedFS, dir fs.FS
|
||||
}
|
||||
|
||||
func NewHybridFS(embed embed.FS, subDir string, localDir string) (*HybridFS, error) {
|
||||
subFS, err := fs.Sub(embed, subDir)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return &HybridFS{
|
||||
embedFS: subFS,
|
||||
dir: os.DirFS(localDir),
|
||||
}, nil
|
||||
}
|
||||
|
||||
func (hfs *HybridFS) Open(name string) (fs.File, error) {
|
||||
// Ensure embed files are not replaced
|
||||
if file, err := hfs.embedFS.Open(name); err == nil {
|
||||
return file, nil
|
||||
}
|
||||
|
||||
return hfs.dir.Open(name)
|
||||
}
|
||||
@@ -22,6 +22,8 @@ func init() {
|
||||
SkipVerifySSL: false,
|
||||
}),
|
||||
})
|
||||
|
||||
http.DefaultClient.Timeout = time.Minute * 10
|
||||
}
|
||||
|
||||
type _httpTransport struct {
|
||||
|
||||
@@ -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
|
||||
}
|
||||
+15
-1
@@ -10,7 +10,11 @@ import (
|
||||
jsoniter "github.com/json-iterator/go"
|
||||
)
|
||||
|
||||
var Json = jsoniter.ConfigCompatibleWithStandardLibrary
|
||||
var (
|
||||
Json = jsoniter.ConfigCompatibleWithStandardLibrary
|
||||
|
||||
DNSServers = []string{"1.1.1.1:53", "223.5.5.5:53"}
|
||||
)
|
||||
|
||||
func IsWindows() bool {
|
||||
return os.PathSeparator == '\\' && os.PathListSeparator == ';'
|
||||
@@ -76,3 +80,13 @@ func GenerateRandomString(n int) (string, error) {
|
||||
}
|
||||
return string(ret), nil
|
||||
}
|
||||
|
||||
func Uint64SubInt64(a uint64, b int64) uint64 {
|
||||
if b < 0 {
|
||||
return a + uint64(-b)
|
||||
}
|
||||
if a < uint64(b) {
|
||||
return 0
|
||||
}
|
||||
return a - uint64(b)
|
||||
}
|
||||
|
||||
+41
-10
@@ -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
@@ -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
@@ -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
@@ -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",
|
||||
}
|
||||
|
||||
Vendored
+140
-38
@@ -23,19 +23,19 @@ other = "Home"
|
||||
other = "Back to Homepage"
|
||||
|
||||
[AdminPanel]
|
||||
other = "Admin Panel"
|
||||
other = "Dashboard"
|
||||
|
||||
[Logout]
|
||||
other = "Logout"
|
||||
other = "Log out"
|
||||
|
||||
[Login]
|
||||
other = "Login"
|
||||
other = "Log in"
|
||||
|
||||
[ConfirmLogout]
|
||||
other = "Do you want to Logout?"
|
||||
other = "Confirm Logging Out?"
|
||||
|
||||
[AfterLoggingOutYouHaveToLoginAgain]
|
||||
other = "You need to Login again to continue"
|
||||
other = "You will need to log in again to continue"
|
||||
|
||||
[Cancel]
|
||||
other = "Cancel"
|
||||
@@ -56,7 +56,7 @@ other = "Cron Expression"
|
||||
other = "Backup"
|
||||
|
||||
[3amDaily]
|
||||
other = "(Daily at 3 AM)"
|
||||
other = "(At 3 AM)"
|
||||
|
||||
[Command]
|
||||
other = "Command"
|
||||
@@ -71,7 +71,7 @@ other = "Ignore All, Execute Only on Specific Servers"
|
||||
other = "Cover All, Except Specific Servers"
|
||||
|
||||
[ExecuteByTriggerServer]
|
||||
other = "Execute on Trigger Server"
|
||||
other = "Execute on Alarmed Servers"
|
||||
|
||||
[SpecificServers]
|
||||
other = "Specific Servers"
|
||||
@@ -95,7 +95,7 @@ other = "Cron Task"
|
||||
other = "Trigger Task"
|
||||
|
||||
[TheFormaOfTheScheduleIs]
|
||||
other = "The Cron Expression is like:"
|
||||
other = "The Cron Expression format is:"
|
||||
|
||||
[SecondsMinutesHoursDaysMonthsWeeksSeeDetails]
|
||||
other = "Sec Min Hour Day Month Week, see details in "
|
||||
@@ -104,7 +104,7 @@ other = "Sec Min Hour Day Month Week, see details in "
|
||||
other = "CRON Expression Format"
|
||||
|
||||
[IntroductionOfCommands]
|
||||
other = "Introduction of Commands: Writing commands is similar to shell/bat scripts. It's advised not to break lines, and multiple commands can be connected with <code>&&</code> or <code>&</code>. If a command cannot be found, it might be due to <code>PATH</code> environment variable configuration issues. On <code>Linux</code> servers, you can add <code>source ~/.bashrc</code> at the beginning of the command, or use the command's absolute path."
|
||||
other = "Note on writing commands: It is similar to shell/bat scripts. but it's advised not to start a newline, and connect multiple commands with <code>&&</code> or <code>;</code>. If a command cannot be found, it is possibly due to <code>PATH</code> environment variable issues. On <code>Linux</code> servers, you can add <code>source ~/.bashrc</code> at the beginning of the command, or just use the absolute path."
|
||||
|
||||
[AddMonitor]
|
||||
other = "Add Service Monitor"
|
||||
@@ -119,7 +119,7 @@ other = "Target"
|
||||
other = "Type"
|
||||
|
||||
[SslExpirationOrChange]
|
||||
other = "(SSL Expiration or Change)"
|
||||
other = "(Certificate Expiration and Changes)"
|
||||
|
||||
[Duration]
|
||||
other = "Interval"
|
||||
@@ -134,10 +134,10 @@ other = "Enable Failure Notification"
|
||||
other = "Failure Notification"
|
||||
|
||||
[MaxLatency]
|
||||
other = "Max Latency (ms)"
|
||||
other = "Maximum Latency (ms)"
|
||||
|
||||
[MinLatency]
|
||||
other = "Min Latency (ms)"
|
||||
other = "Minimum Latency (ms)"
|
||||
|
||||
[EnableLatencyNotification]
|
||||
other = "Enable Latency Notification"
|
||||
@@ -147,7 +147,7 @@ other = "Latency Notification"
|
||||
|
||||
[IntroductionOfMonitor]
|
||||
other = """
|
||||
For type <b>HTTP-GET</b>, enter URL (with http/https, HTTPS protocol will also monitor SSL certificate);<br>
|
||||
For type <b>HTTP-GET</b>, enter URL (with http/https scheme, HTTPS protocol will also monitor SSL certificate);<br>
|
||||
For type <b>ICMP-Ping</b>, enter domain/IP without port: example.com;<br>
|
||||
For type <b>TCP-Ping</b>, enter domain/IP + port number: example.com:22"""
|
||||
|
||||
@@ -182,16 +182,16 @@ other = "Notification Trigger Mode"
|
||||
other = "Always Trigger"
|
||||
|
||||
[ModeOnetimeTrigger]
|
||||
other = "Always Trigger"
|
||||
other = "Trigger Once"
|
||||
|
||||
[EnableTriggerTask]
|
||||
other = "Enable Trigger Task"
|
||||
|
||||
[FailTriggerTasks]
|
||||
other = "Tasks triggered during an alarm"
|
||||
other = "Tasks to trigger on an alarm"
|
||||
|
||||
[RecoverTriggerTasks]
|
||||
other = "Tasks triggered during recovery"
|
||||
other = "Tasks to trigger after recovery"
|
||||
|
||||
[Enable]
|
||||
other = "Enable"
|
||||
@@ -218,7 +218,7 @@ other = "Einstein Lightspeed 1"
|
||||
other = "Display Index"
|
||||
|
||||
[TheLargerTheNumberTheHigherThePriority]
|
||||
other = "Higher Number, Higher Priority"
|
||||
other = "Greater Number, Higher Priority"
|
||||
|
||||
[Secret]
|
||||
other = "Secret"
|
||||
@@ -226,11 +226,14 @@ other = "Secret"
|
||||
[Note]
|
||||
other = "Note"
|
||||
|
||||
[PublicNote]
|
||||
other = "Public Note"
|
||||
|
||||
[LinuxOneKeyInstall]
|
||||
other = "Linux One-Command Install"
|
||||
other = "Linux Installation Command"
|
||||
|
||||
[NoDomainAlert]
|
||||
other = "Set CDN bypassed Domain/IP in setting before using this function"
|
||||
other = "Please set the IP (or a domain pointed to it) of the original server in settings"
|
||||
|
||||
[PushSuccessfully]
|
||||
other = "Send Success Notification"
|
||||
@@ -251,13 +254,13 @@ other = "Cover All"
|
||||
other = "Ignore All"
|
||||
|
||||
[ByTrigger]
|
||||
other = "Execute by Trigger"
|
||||
other = "Alarmed Servers"
|
||||
|
||||
[DeleteScheduledTask]
|
||||
other = "Delete Scheduled Task"
|
||||
|
||||
[ConfirmToDeleteThisScheduledTask]
|
||||
other = "Are you sure you want to Delete?"
|
||||
other = "Confirm deletion?"
|
||||
|
||||
[AccessDenied]
|
||||
other = "Access Denied"
|
||||
@@ -278,22 +281,22 @@ other = "Don't Have an Account?"
|
||||
other = "HTTP(S)/SSL Certificate"
|
||||
|
||||
[TCPPort]
|
||||
other = "TCP Port"
|
||||
other = "TCPing"
|
||||
|
||||
[DeleteService]
|
||||
other = "Remove Service Monitor"
|
||||
|
||||
[ConfirmToDeleteThisService]
|
||||
other = "Confirm to Delete This Service?"
|
||||
other = "Confirm Deleting This Service?"
|
||||
|
||||
[DeleteNotificationMethod]
|
||||
other = "Delete Notification Method"
|
||||
|
||||
[ConfirmToDeleteThisNotificationMethod]
|
||||
other = "Confirm to Delete This Notification Method?"
|
||||
other = "Confirm Deleting This Notification Method?"
|
||||
|
||||
[ForceUpdate]
|
||||
other = "Force to Update"
|
||||
other = "Trigger Agent Update"
|
||||
|
||||
[SelectAll]
|
||||
other = "Select All"
|
||||
@@ -302,7 +305,7 @@ other = "Select All"
|
||||
other = "Version"
|
||||
|
||||
[OneKeyInstall]
|
||||
other = "One-Command Install"
|
||||
other = "Installation commands"
|
||||
|
||||
[ClickToCopy]
|
||||
other = "Click To Copy"
|
||||
@@ -311,7 +314,7 @@ other = "Click To Copy"
|
||||
other = "Delete Server"
|
||||
|
||||
[ConfirmToDeleteServer]
|
||||
other = "Confirm to Delete Server?"
|
||||
other = "Confirm Deleting This Server?"
|
||||
|
||||
[NoServerSelected]
|
||||
other = "No Server Selected"
|
||||
@@ -331,6 +334,9 @@ other = "Frontend Theme"
|
||||
[CustomCodes]
|
||||
other = "Custom Codes (Style and Script)"
|
||||
|
||||
[CustomCodesDashboard]
|
||||
other = "Custom Codes for Dashboard"
|
||||
|
||||
[AccessPassword]
|
||||
other = "Frontend Access Password"
|
||||
|
||||
@@ -539,7 +545,7 @@ other = "Scheduled Tasks"
|
||||
other = "API Management"
|
||||
|
||||
[IssueNewApiToken]
|
||||
other = "Issue New API Token"
|
||||
other = "Generate New API Token"
|
||||
|
||||
[Token]
|
||||
other = "Token"
|
||||
@@ -548,10 +554,10 @@ other = "Token"
|
||||
other = "Delete Token"
|
||||
|
||||
[ConfirmToDeleteThisToken]
|
||||
other = "Confirm to Delete This Token?"
|
||||
other = "Confirm Deleting This Token?"
|
||||
|
||||
[YouAreNotAuthorized]
|
||||
other = "Not Authorized for This Page"
|
||||
other = "This Page needs Authorization"
|
||||
|
||||
[WrongAccessPassword]
|
||||
other = "Wrong Access Password"
|
||||
@@ -599,13 +605,13 @@ other = "IP Changed"
|
||||
other = "Remaining Data"
|
||||
|
||||
[DashboardTheme]
|
||||
other = "Admin Panel Theme"
|
||||
other = "Dashboard Theme"
|
||||
|
||||
[Info]
|
||||
other = "Info"
|
||||
|
||||
[HideForGuest]
|
||||
other = "Hide for Guest"
|
||||
other = "Hide from Guest"
|
||||
|
||||
[Menu]
|
||||
other = "Menu"
|
||||
@@ -616,20 +622,62 @@ other = "Network"
|
||||
[EnableShowInService]
|
||||
other = "Enable Show in Service"
|
||||
|
||||
[DDNS]
|
||||
other = "Dynamic DNS"
|
||||
|
||||
[DDNSProfiles]
|
||||
other = "DDNS Profiles"
|
||||
|
||||
[AddDDNSProfile]
|
||||
other = "New Profile"
|
||||
|
||||
[EnableDDNS]
|
||||
other = "Enable DDNS"
|
||||
|
||||
[EnableIPv4]
|
||||
other = "Enable DDNS IPv4"
|
||||
other = "IPv4 Enabled"
|
||||
|
||||
[EnableIpv6]
|
||||
other = "Enable DDNS IPv6"
|
||||
[EnableIPv6]
|
||||
other = "IPv6 Enabled"
|
||||
|
||||
[DDNSDomain]
|
||||
other = "DDNS Domain"
|
||||
other = "Domains"
|
||||
|
||||
[DDNSProfile]
|
||||
other = "DDNS Profile Name"
|
||||
[DDNSDomains]
|
||||
other = "Domains (separate with comma)"
|
||||
|
||||
[DDNSProvider]
|
||||
other = "DDNS Provider"
|
||||
|
||||
[MaxRetries]
|
||||
other = "Maximum retry attempts"
|
||||
|
||||
[DDNSAccessID]
|
||||
other = "Credential 1"
|
||||
|
||||
[DDNSAccessSecret]
|
||||
other = "Credential 2"
|
||||
|
||||
[DDNSTokenID]
|
||||
other = "Token ID"
|
||||
|
||||
[DDNSTokenSecret]
|
||||
other = "Token Secret"
|
||||
|
||||
[WebhookURL]
|
||||
other = "Webhook URL"
|
||||
|
||||
[WebhookMethod]
|
||||
other = "Webhook Request Method"
|
||||
|
||||
[WebhookRequestType]
|
||||
other = "Webhook Request Type"
|
||||
|
||||
[WebhookHeaders]
|
||||
other = "Webhook Request Headers"
|
||||
|
||||
[WebhookRequestBody]
|
||||
other = "Webhook Request Body"
|
||||
|
||||
[Feature]
|
||||
other = "Feature"
|
||||
@@ -648,3 +696,57 @@ other = "Disable Switch Template in Frontend"
|
||||
|
||||
[ServersOnWorldMap]
|
||||
other = "Servers On World Map"
|
||||
|
||||
[NAT]
|
||||
other = "NAT Traversal"
|
||||
|
||||
[LocalService]
|
||||
other = "Local service"
|
||||
|
||||
[LocalServicePlaceholder]
|
||||
other = "192.168.1.1:80 (with port)"
|
||||
|
||||
[BindHostname]
|
||||
other = "Bind hostname"
|
||||
|
||||
[NetworkSpiterList]
|
||||
other = "Network Monitor"
|
||||
|
||||
[Refresh]
|
||||
other = "Refresh"
|
||||
|
||||
[CopyPath]
|
||||
other = "Copy Path"
|
||||
|
||||
[Goto]
|
||||
other = "Go to"
|
||||
|
||||
[GotoHeadline]
|
||||
other = "Go to a Folder"
|
||||
|
||||
[GotoGo]
|
||||
other = "Go"
|
||||
|
||||
[GotoClose]
|
||||
other = "Cancel"
|
||||
|
||||
[FMError]
|
||||
other = "Agent returned an error, please view the console for details. To open a new connection, reopen the FM again."
|
||||
|
||||
[Remaining]
|
||||
other = "Remaining"
|
||||
|
||||
[Lifetime]
|
||||
other = "Lifetime"
|
||||
|
||||
[Price]
|
||||
other = "Price"
|
||||
|
||||
[Expired]
|
||||
other = "Expired"
|
||||
|
||||
[Days]
|
||||
other = "d"
|
||||
|
||||
[CustomNameservers]
|
||||
other = "Custom Public DNS Nameservers for DDNS (separate with comma)"
|
||||
|
||||
Vendored
+113
-11
@@ -71,7 +71,7 @@ other = "Ignorar todo, ejecutar solo a través de servidores específicos"
|
||||
other = "Incluir todo, excepto servidores específicos"
|
||||
|
||||
[ExecuteByTriggerServer]
|
||||
other = "Ejecutar por servidor disparador"
|
||||
other = "Ejecutar en servidores con alarma"
|
||||
|
||||
[SpecificServers]
|
||||
other = "Servidores Específicos"
|
||||
@@ -104,7 +104,7 @@ other = "Segundos, Minutos, Horas, Días, Meses, Semanas - Ver detalles a"
|
||||
other = "Formato de Expresión de Cron"
|
||||
|
||||
[IntroductionOfCommands]
|
||||
other = "Introducción de Comandos: Escriba comandos como en scripts shell/bat. Evite saltos de línea; conecte múltiples comandos con <code>&&</code> o <code>&</code>. Si no se encuentra un comando, verifique la variable de entorno <code>PATH</code>. En servidores <code>Linux</code>, antepóngale <code>source ~/.bashrc</code> a los comandos, o use rutas absolutas."
|
||||
other = "Introducción de Comandos: Escriba comandos como en scripts shell/bat. Evite saltos de línea; conecte múltiples comandos con <code>&&</code> o <code>;</code>. Si no se encuentra un comando, verifique la variable de entorno <code>PATH</code>. En servidores <code>Linux</code>, antepóngale <code>source ~/.bashrc</code> a los comandos, o use rutas absolutas."
|
||||
|
||||
[AddMonitor]
|
||||
other = "Agregar Monitor de Servicio"
|
||||
@@ -226,6 +226,9 @@ other = "Secreto"
|
||||
[Note]
|
||||
other = "Nota"
|
||||
|
||||
[PublicNote]
|
||||
other = "Nota Pública"
|
||||
|
||||
[LinuxOneKeyInstall]
|
||||
other = "Instalación Linux con Un Solo Clic"
|
||||
|
||||
@@ -251,7 +254,7 @@ other = "Cubrir Todo"
|
||||
other = "Ignorar Todo"
|
||||
|
||||
[ByTrigger]
|
||||
other = "Ejecutar por Disparador"
|
||||
other = "Servidores con alarma"
|
||||
|
||||
[DeleteScheduledTask]
|
||||
other = "Eliminar Tarea Programada"
|
||||
@@ -278,7 +281,7 @@ other = "¿No tiene una cuenta?"
|
||||
other = "Certificados HTTP(S)/SSL"
|
||||
|
||||
[TCPPort]
|
||||
other = "Puerto TCP"
|
||||
other = "TCPing"
|
||||
|
||||
[DeleteService]
|
||||
other = "Eliminar Servicio"
|
||||
@@ -331,6 +334,9 @@ other = "Tema de Frontend"
|
||||
[CustomCodes]
|
||||
other = "Códigos Personalizados (Incluye style y script)"
|
||||
|
||||
[CustomCodesDashboard]
|
||||
other = "Custom Codes for Dashboard"
|
||||
|
||||
[AccessPassword]
|
||||
other = "Contraseña de Acceso al Frontend"
|
||||
|
||||
@@ -616,20 +622,62 @@ other = "Red"
|
||||
[EnableShowInService]
|
||||
other = "Mostrar en servicio"
|
||||
|
||||
[DDNS]
|
||||
other = "DNS Dinámico"
|
||||
|
||||
[DDNSProfiles]
|
||||
other = "Perfiles DDNS"
|
||||
|
||||
[AddDDNSProfile]
|
||||
other = "Nuevo Perfil"
|
||||
|
||||
[EnableDDNS]
|
||||
other = "Habilitar DDNS"
|
||||
|
||||
[EnableIPv4]
|
||||
other = "Habilitar DDNS IPv4"
|
||||
other = "IPv4 Activado"
|
||||
|
||||
[EnableIpv6]
|
||||
other = "Habilitar DDNS IPv6"
|
||||
[EnableIPv6]
|
||||
other = "IPv6 Activado"
|
||||
|
||||
[DDNSDomain]
|
||||
other = "Dominio DDNS"
|
||||
other = "Dominios"
|
||||
|
||||
[DDNSProfile]
|
||||
other = "Nombre del perfil de DDNS"
|
||||
[DDNSDomains]
|
||||
other = "Dominios (separados por comas)"
|
||||
|
||||
[DDNSProvider]
|
||||
other = "Proveedor DDNS"
|
||||
|
||||
[MaxRetries]
|
||||
other = "Número máximo de intentos de reintento"
|
||||
|
||||
[DDNSAccessID]
|
||||
other = "Credencial 1"
|
||||
|
||||
[DDNSAccessSecret]
|
||||
other = "Credencial 2"
|
||||
|
||||
[DDNSTokenID]
|
||||
other = "ID del Token"
|
||||
|
||||
[DDNSTokenSecret]
|
||||
other = "Secreto del Token"
|
||||
|
||||
[WebhookURL]
|
||||
other = "URL del Webhook"
|
||||
|
||||
[WebhookMethod]
|
||||
other = "Método de Solicitud del Webhook"
|
||||
|
||||
[WebhookRequestType]
|
||||
other = "Tipo de solicitud del Webhook"
|
||||
|
||||
[WebhookHeaders]
|
||||
other = "Encabezados de Solicitud del Webhook"
|
||||
|
||||
[WebhookRequestBody]
|
||||
other = "Cuerpo de Solicitud del Webhook"
|
||||
|
||||
[Feature]
|
||||
other = "Característica"
|
||||
@@ -647,4 +695,58 @@ 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 traversal"
|
||||
|
||||
[LocalService]
|
||||
other = "Servicio de red local"
|
||||
|
||||
[LocalServicePlaceholder]
|
||||
other = "192.168.1.1:80 (con puerto)"
|
||||
|
||||
[BindHostname]
|
||||
other = "Vincular nombre de host"
|
||||
|
||||
[NetworkSpiterList]
|
||||
other = "Monitor de red"
|
||||
|
||||
[Refresh]
|
||||
other = "Actualizar"
|
||||
|
||||
[CopyPath]
|
||||
other = "Copiar ruta"
|
||||
|
||||
[Goto]
|
||||
other = "Ir a"
|
||||
|
||||
[GotoHeadline]
|
||||
other = "Ir a una carpeta"
|
||||
|
||||
[GotoGo]
|
||||
other = "Ir"
|
||||
|
||||
[GotoClose]
|
||||
other = "Cancelar"
|
||||
|
||||
[FMError]
|
||||
other = "Agent devolvió un error, consulte la consola para obtener más detalles. Para abrir una nueva conexión, vuelva a abrir el FM."
|
||||
|
||||
[Remaining]
|
||||
other = "Remaining"
|
||||
|
||||
[Lifetime]
|
||||
other = "Lifetime"
|
||||
|
||||
[Price]
|
||||
other = "Price"
|
||||
|
||||
[Expired]
|
||||
other = "Expired"
|
||||
|
||||
[Days]
|
||||
other = "d"
|
||||
|
||||
[CustomNameservers]
|
||||
other = "Servidores DNS públicos personalizados para DDNS (separar con coma)"
|
||||
|
||||
Vendored
+114
-12
@@ -71,7 +71,7 @@ other = "忽略所有,仅通过特定服务器执行"
|
||||
other = "覆盖所有,仅特定服务器不执行"
|
||||
|
||||
[ExecuteByTriggerServer]
|
||||
other = "由触发的服务器执行"
|
||||
other = "由触发告警的服务器执行"
|
||||
|
||||
[SpecificServers]
|
||||
other = "特定服务器"
|
||||
@@ -104,7 +104,7 @@ other = "秒 分 时 天 月 星期,详情见"
|
||||
other = "计划表达式格式"
|
||||
|
||||
[IntroductionOfCommands]
|
||||
other = "命令说明:编写命令时类似于 shell/bat 脚本。建议不要换行,多个命令可用 <code>&&</code> 或 <code>&</code> 连接,若出现命令无法找到的情况,可能是由于 <code>PATH</code> 环境变量配置问题。在 <code>Linux</code> 服务器上,可在命令开头加入 <code>source ~/.bashrc</code>,或使用命令的绝对路径执行。"
|
||||
other = "命令说明:编写命令时类似于 shell/bat 脚本。建议不要换行,多个命令可用 <code>&&</code> 或 <code>;</code> 连接,若出现命令无法找到的情况,可能是由于 <code>PATH</code> 环境变量配置问题。在 <code>Linux</code> 服务器上,可在命令开头加入 <code>source ~/.bashrc</code>,或使用命令的绝对路径执行。"
|
||||
|
||||
[AddMonitor]
|
||||
other = "新增监控"
|
||||
@@ -226,6 +226,9 @@ other = "密钥"
|
||||
[Note]
|
||||
other = "备注"
|
||||
|
||||
[PublicNote]
|
||||
other = "公开备注"
|
||||
|
||||
[LinuxOneKeyInstall]
|
||||
other = "Linux 一键安装"
|
||||
|
||||
@@ -251,7 +254,7 @@ other = "覆盖所有"
|
||||
other = "忽略所有"
|
||||
|
||||
[ByTrigger]
|
||||
other = "触发执行"
|
||||
other = "触发告警的服务器"
|
||||
|
||||
[DeleteScheduledTask]
|
||||
other = "删除计划任务"
|
||||
@@ -278,7 +281,7 @@ other = "没有账号?"
|
||||
other = "HTTP(S)/SSL证书"
|
||||
|
||||
[TCPPort]
|
||||
other = "TCP 端口"
|
||||
other = "TCPing"
|
||||
|
||||
[DeleteService]
|
||||
other = "移除服务"
|
||||
@@ -331,6 +334,9 @@ other = "前台界面主题"
|
||||
[CustomCodes]
|
||||
other = "自定义代码(包括 style 和 script)"
|
||||
|
||||
[CustomCodesDashboard]
|
||||
other = "Custom Codes for Dashboard"
|
||||
|
||||
[AccessPassword]
|
||||
other = "前台访问密码"
|
||||
|
||||
@@ -419,7 +425,7 @@ other = "活动"
|
||||
other = "版本"
|
||||
|
||||
[NetSpeed]
|
||||
other = "网络"
|
||||
other = "网速"
|
||||
|
||||
[Uptime]
|
||||
other = "在线"
|
||||
@@ -616,20 +622,62 @@ other = "网络"
|
||||
[EnableShowInService]
|
||||
other = "在服务中显示"
|
||||
|
||||
[DDNS]
|
||||
other = "动态 DNS"
|
||||
|
||||
[DDNSProfiles]
|
||||
other = "DDNS配置"
|
||||
|
||||
[AddDDNSProfile]
|
||||
other = "新配置"
|
||||
|
||||
[EnableDDNS]
|
||||
other = "启用DDNS"
|
||||
other = "启用 DDNS"
|
||||
|
||||
[EnableIPv4]
|
||||
other = "启用DDNS IPv4"
|
||||
other = "启用 DDNS IPv4"
|
||||
|
||||
[EnableIpv6]
|
||||
other = "启用DDNS IPv6"
|
||||
[EnableIPv6]
|
||||
other = "启用 DDNS IPv6"
|
||||
|
||||
[DDNSDomain]
|
||||
other = "DDNS域名"
|
||||
other = "DDNS 域名"
|
||||
|
||||
[DDNSProfile]
|
||||
other = "DDNS配置名"
|
||||
[DDNSDomains]
|
||||
other = "域名(逗号分隔)"
|
||||
|
||||
[DDNSProvider]
|
||||
other = "DDNS 供应商"
|
||||
|
||||
[MaxRetries]
|
||||
other = "最大重试次数"
|
||||
|
||||
[DDNSAccessID]
|
||||
other = "DDNS 凭据 1"
|
||||
|
||||
[DDNSAccessSecret]
|
||||
other = "DDNS 凭据 2"
|
||||
|
||||
[DDNSTokenID]
|
||||
other = "令牌 ID"
|
||||
|
||||
[DDNSTokenSecret]
|
||||
other = "令牌 Secret"
|
||||
|
||||
[WebhookURL]
|
||||
other = "Webhook 地址"
|
||||
|
||||
[WebhookMethod]
|
||||
other = "Webhook 请求方式"
|
||||
|
||||
[WebhookRequestType]
|
||||
other = "Webhook 请求类型"
|
||||
|
||||
[WebhookHeaders]
|
||||
other = "Webhook 请求头"
|
||||
|
||||
[WebhookRequestBody]
|
||||
other = "Webhook 请求体"
|
||||
|
||||
[Feature]
|
||||
other = "功能"
|
||||
@@ -648,3 +696,57 @@ other = "禁止前台切换模板"
|
||||
|
||||
[ServersOnWorldMap]
|
||||
other = "服务器世界分布图"
|
||||
|
||||
[NAT]
|
||||
other = "内网穿透"
|
||||
|
||||
[LocalService]
|
||||
other = "内网服务"
|
||||
|
||||
[LocalServicePlaceholder]
|
||||
other = "192.168.1.1:80(带端口)"
|
||||
|
||||
[BindHostname]
|
||||
other = "绑定域名"
|
||||
|
||||
[NetworkSpiterList]
|
||||
other = "网络监控"
|
||||
|
||||
[Refresh]
|
||||
other = "刷新"
|
||||
|
||||
[CopyPath]
|
||||
other = "复制路径"
|
||||
|
||||
[Goto]
|
||||
other = "跳往"
|
||||
|
||||
[GotoHeadline]
|
||||
other = "跳往文件夹"
|
||||
|
||||
[GotoGo]
|
||||
other = "确认"
|
||||
|
||||
[GotoClose]
|
||||
other = "取消"
|
||||
|
||||
[FMError]
|
||||
other = "Agent 返回了错误,请查看控制台获取详细信息。要建立新连接,请重新打开 FM。"
|
||||
|
||||
[Remaining]
|
||||
other = "剩余"
|
||||
|
||||
[Lifetime]
|
||||
other = "永续"
|
||||
|
||||
[Price]
|
||||
other = "价格"
|
||||
|
||||
[Expired]
|
||||
other = "已到期"
|
||||
|
||||
[Days]
|
||||
other = "天"
|
||||
|
||||
[CustomNameservers]
|
||||
other = "自定义DDNS使用的公共DNS服务器(逗号分隔)"
|
||||
|
||||
Vendored
+130
-28
@@ -50,7 +50,7 @@ other = "新增計劃任務"
|
||||
other = "名稱"
|
||||
|
||||
[Scheduler]
|
||||
other = "計劃"
|
||||
other = "排程"
|
||||
|
||||
[BackUp]
|
||||
other = "備份"
|
||||
@@ -71,7 +71,7 @@ other = "忽略所有,僅通過特定伺服器執行"
|
||||
other = "覆蓋所有,僅特定伺服器不執行"
|
||||
|
||||
[ExecuteByTriggerServer]
|
||||
other = "由觸發的伺服器執行"
|
||||
other = "由觸發告警的伺服器執行"
|
||||
|
||||
[SpecificServers]
|
||||
other = "特定伺服器"
|
||||
@@ -80,37 +80,37 @@ other = "特定伺服器"
|
||||
other = "輸入ID/名稱以搜尋"
|
||||
|
||||
[NotificationMethodGroup]
|
||||
other = "通知方式組"
|
||||
other = "通知群組"
|
||||
|
||||
[PushSuccessMessages]
|
||||
other = "推送成功的消息"
|
||||
other = "推送成功的訊息"
|
||||
|
||||
[TaskType]
|
||||
other = "任務類型"
|
||||
|
||||
[CronTask]
|
||||
other = "計劃任務"
|
||||
other = "排程任務"
|
||||
|
||||
[TriggerTask]
|
||||
other = "觸發任務"
|
||||
|
||||
[TheFormaOfTheScheduleIs]
|
||||
other = "計劃的格式為:"
|
||||
other = "排程的格式為:"
|
||||
|
||||
[SecondsMinutesHoursDaysMonthsWeeksSeeDetails]
|
||||
other = "秒 分 時 天 月 星期,詳情見"
|
||||
|
||||
[ScheduleExpressionFormat]
|
||||
other = "計劃表達式格式"
|
||||
other = "排程表達式格式"
|
||||
|
||||
[IntroductionOfCommands]
|
||||
other = "命令說明:編寫命令時類似於 shell/bat 腳本。建議不要換行,多個命令可用 <code>&&</code> 或 <code>&</code> 連接,若出現命令無法找到的情況,可能是由於 <code>PATH</code> 環境變量配置問題。在 <code>Linux</code> 伺服器上,可在命令開頭加入 <code>source ~/.bashrc</code>,或使用命令的絕對路徑執行。"
|
||||
other = "命令說明:編寫命令時類似於 shell/bat 腳本。建議不要換行,多個命令可用 <code>&&</code> 或 <code>;</code> 連接,若出現命令無法找到的情況,可能是由於 <code>PATH</code> 環境變數配置問題。在 <code>Linux</code> 伺服器上,可在命令開頭加入 <code>source ~/.bashrc</code>,或使用命令的絕對路徑執行。"
|
||||
|
||||
[AddMonitor]
|
||||
other = "新增監控"
|
||||
|
||||
[Blog]
|
||||
other = "博客"
|
||||
other = "部落格"
|
||||
|
||||
[Target]
|
||||
other = "目標"
|
||||
@@ -158,7 +158,7 @@ other = "新增通知方式"
|
||||
other = "分組"
|
||||
|
||||
[DoNotSendTestMessages]
|
||||
other = "不發送測試信息"
|
||||
other = "不發送測試訊息"
|
||||
|
||||
[RequestMethod]
|
||||
other = "請求方式"
|
||||
@@ -221,11 +221,14 @@ other = "排序"
|
||||
other = "越大越靠前"
|
||||
|
||||
[Secret]
|
||||
other = "密鑰"
|
||||
other = "金鑰"
|
||||
|
||||
[Note]
|
||||
other = "備註"
|
||||
|
||||
[PublicNote]
|
||||
other = "公開備註"
|
||||
|
||||
[LinuxOneKeyInstall]
|
||||
other = "Linux 一鍵安裝"
|
||||
|
||||
@@ -251,13 +254,13 @@ other = "覆蓋所有"
|
||||
other = "忽略所有"
|
||||
|
||||
[ByTrigger]
|
||||
other = "觸發執行"
|
||||
other = "觸發告警的伺服器"
|
||||
|
||||
[DeleteScheduledTask]
|
||||
other = "刪除計劃任務"
|
||||
other = "刪除排程任務"
|
||||
|
||||
[ConfirmToDeleteThisScheduledTask]
|
||||
other = "確認刪除此計劃任務?"
|
||||
other = "確認刪除此排程任務?"
|
||||
|
||||
[AccessDenied]
|
||||
other = "訪問被拒絕"
|
||||
@@ -278,7 +281,7 @@ other = "沒有帳號?"
|
||||
other = "HTTP(S)/SSL證書"
|
||||
|
||||
[TCPPort]
|
||||
other = "TCP 端口"
|
||||
other = "TCPing"
|
||||
|
||||
[DeleteService]
|
||||
other = "移除服務"
|
||||
@@ -331,6 +334,9 @@ other = "前台界面主題"
|
||||
[CustomCodes]
|
||||
other = "自定義代碼(包括 style 和 script)"
|
||||
|
||||
[CustomCodesDashboard]
|
||||
other = "Custom Codes for Dashboard"
|
||||
|
||||
[AccessPassword]
|
||||
other = "前台訪問密碼"
|
||||
|
||||
@@ -404,7 +410,7 @@ other = "流量"
|
||||
other = "負載"
|
||||
|
||||
[ProcessCount]
|
||||
other = "進程數"
|
||||
other = "行程數"
|
||||
|
||||
[ConnCount]
|
||||
other = "連接數"
|
||||
@@ -419,7 +425,7 @@ other = "活動"
|
||||
other = "版本"
|
||||
|
||||
[NetSpeed]
|
||||
other = "網絡"
|
||||
other = "網速"
|
||||
|
||||
[Uptime]
|
||||
other = "在線"
|
||||
@@ -455,7 +461,7 @@ other = "狀態"
|
||||
other = "可用性"
|
||||
|
||||
[AverageLatency]
|
||||
other = "平均響應時間"
|
||||
other = "平均回應時間"
|
||||
|
||||
[CycleTransferStats]
|
||||
other = "周期性流量統計"
|
||||
@@ -521,7 +527,7 @@ other = "發生錯誤"
|
||||
other = "系統錯誤"
|
||||
|
||||
[NetworkError]
|
||||
other = "網絡錯誤"
|
||||
other = "網路錯誤"
|
||||
|
||||
[ServicesStatus]
|
||||
other = "服務狀態"
|
||||
@@ -533,7 +539,7 @@ other = "伺服器管理"
|
||||
other = "服務監控"
|
||||
|
||||
[ScheduledTasks]
|
||||
other = "計劃任務"
|
||||
other = "排程任務"
|
||||
|
||||
[ApiManagement]
|
||||
other = "API 管理"
|
||||
@@ -611,25 +617,67 @@ other = "對遊客隱藏"
|
||||
other = "菜單"
|
||||
|
||||
[NetworkSpiter]
|
||||
other = "網絡"
|
||||
other = "網路"
|
||||
|
||||
[EnableShowInService]
|
||||
other = "在服務中顯示"
|
||||
|
||||
[DDNS]
|
||||
other = "動態 DNS"
|
||||
|
||||
[DDNSProfiles]
|
||||
other = "DDNS配置"
|
||||
|
||||
[AddDDNSProfile]
|
||||
other = "新增配置"
|
||||
|
||||
[EnableDDNS]
|
||||
other = "啟用DDNS"
|
||||
other = "啟用 DDNS"
|
||||
|
||||
[EnableIPv4]
|
||||
other = "啟用DDNS IPv4"
|
||||
other = "啟用 DDNS IPv4"
|
||||
|
||||
[EnableIpv6]
|
||||
other = "啟用DDNS IPv6"
|
||||
[EnableIPv6]
|
||||
other = "啟用 DDNS IPv6"
|
||||
|
||||
[DDNSDomain]
|
||||
other = "DDNS網域"
|
||||
other = "DDNS 域名"
|
||||
|
||||
[DDNSProfile]
|
||||
other = "DDNS設定名"
|
||||
[DDNSDomains]
|
||||
other = "域名(逗號分隔)"
|
||||
|
||||
[DDNSProvider]
|
||||
other = "DDN S供應商"
|
||||
|
||||
[MaxRetries]
|
||||
other = "最大重試次數"
|
||||
|
||||
[DDNSAccessID]
|
||||
other = "DDNS 憑據 1"
|
||||
|
||||
[DDNSAccessSecret]
|
||||
other = "DDNS 憑據 2"
|
||||
|
||||
[DDNSTokenID]
|
||||
other = "令牌 ID"
|
||||
|
||||
[DDNSTokenSecret]
|
||||
other = "令牌 Secret"
|
||||
|
||||
[WebhookURL]
|
||||
other = "Webhook 地址"
|
||||
|
||||
[WebhookMethod]
|
||||
other = "Webhook 請求方式"
|
||||
|
||||
[WebhookRequestType]
|
||||
other = "Webhook 請求類型"
|
||||
|
||||
[WebhookHeaders]
|
||||
other = "Webhook 請求頭"
|
||||
|
||||
[WebhookRequestBody]
|
||||
other = "Webhook 請求體"
|
||||
|
||||
[Feature]
|
||||
other = "功能"
|
||||
@@ -648,3 +696,57 @@ other = "禁止前台切換主題"
|
||||
|
||||
[ServersOnWorldMap]
|
||||
other = "伺服器世界分布圖"
|
||||
|
||||
[NAT]
|
||||
other = "NAT穿透"
|
||||
|
||||
[LocalService]
|
||||
other = "內網服務"
|
||||
|
||||
[LocalServicePlaceholder]
|
||||
other = "192.168.1.1:80(帶埠號)"
|
||||
|
||||
[BindHostname]
|
||||
other = "綁定網域"
|
||||
|
||||
[NetworkSpiterList]
|
||||
other = "網路監控"
|
||||
|
||||
[Refresh]
|
||||
other = "重新整理"
|
||||
|
||||
[CopyPath]
|
||||
other = "複製路徑"
|
||||
|
||||
[Goto]
|
||||
other = "跳至"
|
||||
|
||||
[GotoHeadline]
|
||||
other = "跳至資料夾"
|
||||
|
||||
[GotoGo]
|
||||
other = "確定"
|
||||
|
||||
[GotoClose]
|
||||
other = "取消"
|
||||
|
||||
[FMError]
|
||||
other = "Agent 回傳了錯誤,請查看主控台獲取詳細資訊。要建立新連線,請重新開啟 FM。"
|
||||
|
||||
[Remaining]
|
||||
other = "剩餘"
|
||||
|
||||
[Lifetime]
|
||||
other = "永續"
|
||||
|
||||
[Price]
|
||||
other = "價格"
|
||||
|
||||
[Expired]
|
||||
other = "已到期"
|
||||
|
||||
[Days]
|
||||
other = "天"
|
||||
|
||||
[CustomNameservers]
|
||||
other = "自訂DDNS使用的公共DNS伺服器(逗號分隔)"
|
||||
|
||||
Vendored
+13
-1
@@ -2,10 +2,14 @@ package resource
|
||||
|
||||
import (
|
||||
"embed"
|
||||
|
||||
"github.com/naiba/nezha/pkg/utils"
|
||||
)
|
||||
|
||||
var StaticFS *utils.HybridFS
|
||||
|
||||
//go:embed static
|
||||
var StaticFS embed.FS
|
||||
var staticFS embed.FS
|
||||
|
||||
//go:embed template
|
||||
var TemplateFS embed.FS
|
||||
@@ -13,6 +17,14 @@ var TemplateFS embed.FS
|
||||
//go:embed l10n
|
||||
var I18nFS embed.FS
|
||||
|
||||
func init() {
|
||||
var err error
|
||||
StaticFS, err = utils.NewHybridFS(staticFS, "static", "resource/static/custom")
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
}
|
||||
|
||||
func IsTemplateFileExist(name string) bool {
|
||||
_, err := TemplateFS.Open(name)
|
||||
return err == nil
|
||||
|
||||
@@ -0,0 +1,141 @@
|
||||
html[nz-theme='dark'] body {
|
||||
background-color: #121212 !important;
|
||||
color: #fff !important;
|
||||
}
|
||||
|
||||
html[nz-theme='dark'] .ui.menu {
|
||||
background-color: #282828 !important;
|
||||
}
|
||||
|
||||
html[nz-theme='dark'] .ui.menu * {
|
||||
color: #fff !important;
|
||||
}
|
||||
|
||||
html[nz-theme='dark'] .accordion {
|
||||
background-color: #282828 !important;
|
||||
}
|
||||
|
||||
html[nz-theme='dark'] .accordion .title {
|
||||
color: #fff !important;
|
||||
}
|
||||
|
||||
html[nz-theme='dark'] .ui.card {
|
||||
background-color: #3f3f3f !important;
|
||||
border: none !important;
|
||||
box-shadow: none !important;
|
||||
}
|
||||
|
||||
html[nz-theme='dark'] .header {
|
||||
color: #fff !important;
|
||||
}
|
||||
|
||||
html[nz-theme='dark'] .description {
|
||||
color: #fff !important;
|
||||
}
|
||||
|
||||
html[nz-theme='dark'] .icon {
|
||||
color: #fff !important;
|
||||
}
|
||||
|
||||
html[nz-theme='dark'] .ui.popup {
|
||||
background-color: #575757 !important;
|
||||
color: #fff !important;
|
||||
}
|
||||
|
||||
html[nz-theme='dark'] .ui.table {
|
||||
background-color: #282828 !important;
|
||||
color: #fff !important;
|
||||
}
|
||||
|
||||
html[nz-theme='dark'] .ui thead th {
|
||||
background-color: #3f3f3f !important;
|
||||
color: #fff !important;
|
||||
}
|
||||
|
||||
html[nz-theme='dark'] .ui.buttons .button {
|
||||
background-color: #3f3f3f !important;
|
||||
color: #fff !important;
|
||||
}
|
||||
|
||||
html[nz-theme='dark'] .ui.modal {
|
||||
background-color: #282828 !important;
|
||||
}
|
||||
|
||||
html[nz-theme='dark'] .ui.modal * {
|
||||
color: #fff !important;
|
||||
}
|
||||
|
||||
html[nz-theme='dark'] textarea,
|
||||
html[nz-theme='dark'] input,
|
||||
html[nz-theme='dark'] select,
|
||||
html[nz-theme='dark'] .dropdown {
|
||||
background-color: #3f3f3f !important;
|
||||
color: #fff !important;
|
||||
}
|
||||
|
||||
html[nz-theme='dark'] .ui.message {
|
||||
background-color: unset !important;
|
||||
}
|
||||
|
||||
html[nz-theme='dark'] .ui.dropdown .menu {
|
||||
background-color: #575757 !important;
|
||||
color: #fff !important;
|
||||
}
|
||||
|
||||
html[nz-theme='dark'] .ui.modal>.header {
|
||||
background-color: #3f3f3f !important;
|
||||
}
|
||||
|
||||
html[nz-theme='dark'] .ui.modal>.content {
|
||||
background-color: #282828 !important;
|
||||
}
|
||||
|
||||
html[nz-theme='dark'] .ui.modal>.actions {
|
||||
background-color: #3f3f3f !important;
|
||||
}
|
||||
|
||||
html[nz-theme='dark'] #alert {
|
||||
background-color: #3f3f3f !important;
|
||||
color: #fff !important;
|
||||
}
|
||||
|
||||
html[nz-theme='dark'] .ui.form .field>label {
|
||||
color: unset !important;
|
||||
}
|
||||
|
||||
html[nz-theme='dark'] .ui.segment {
|
||||
background-color: #3f3f3f !important;
|
||||
}
|
||||
|
||||
html[nz-theme='dark'] .ui.segment textarea,
|
||||
html[nz-theme='dark'] input,
|
||||
html[nz-theme='dark'] select,
|
||||
html[nz-theme='dark'] .dropdown {
|
||||
background-color: #575757 !important;
|
||||
color: #fff !important;
|
||||
}
|
||||
|
||||
html[nz-theme='dark'] form label {
|
||||
color: #fff !important;
|
||||
}
|
||||
|
||||
html[nz-theme='dark'] .ui.inverted.segment {
|
||||
background-color: #121212 !important;
|
||||
}
|
||||
|
||||
html[nz-theme='dark'] .ui.inverted.segment * {
|
||||
color: #8b8b8b !important;
|
||||
}
|
||||
|
||||
html[nz-theme='dark'] .menu .dropdown {
|
||||
background-color: #282828 !important;
|
||||
color: #fff !important;
|
||||
}
|
||||
|
||||
html[nz-theme='dark'] .ui.menu .ui.dropdown .menu>.item {
|
||||
color: #fff !important;
|
||||
}
|
||||
|
||||
html[nz-theme='dark'] .login .ui.message{
|
||||
color: #8b8b8b !important;
|
||||
}
|
||||
@@ -0,0 +1,70 @@
|
||||
let receivedLength = 0;
|
||||
let expectedLength = 0;
|
||||
let root;
|
||||
let draftHandle;
|
||||
let accessHandle;
|
||||
|
||||
const Operation = Object.freeze({
|
||||
WriteHeader: 1,
|
||||
WriteChunks: 2,
|
||||
DeleteFiles: 3
|
||||
});
|
||||
|
||||
onmessage = async function (event) {
|
||||
try {
|
||||
const { operation, arrayBuffer, fileName } = event.data;
|
||||
|
||||
switch (operation) {
|
||||
case Operation.WriteHeader: {
|
||||
const dataView = new DataView(arrayBuffer);
|
||||
expectedLength = Number(dataView.getBigUint64(4, false));
|
||||
receivedLength = 0;
|
||||
|
||||
// Create a new temporary file
|
||||
root = await navigator.storage.getDirectory();
|
||||
draftHandle = await root.getFileHandle(fileName, { create: true });
|
||||
accessHandle = await draftHandle.createSyncAccessHandle();
|
||||
|
||||
// Inform that file handle is created
|
||||
const dataChunk = arrayBuffer.slice(12);
|
||||
receivedLength += dataChunk.byteLength;
|
||||
accessHandle.write(dataChunk, { at: 0 });
|
||||
const progress = 'got handle';
|
||||
postMessage({ type: 'progress', progress: progress });
|
||||
break;
|
||||
}
|
||||
case Operation.WriteChunks: {
|
||||
if (!accessHandle) {
|
||||
throw new Error('accessHandle is undefined');
|
||||
}
|
||||
|
||||
const dataChunk = arrayBuffer;
|
||||
accessHandle.write(dataChunk, { at: receivedLength });
|
||||
receivedLength += dataChunk.byteLength;
|
||||
|
||||
if (receivedLength === expectedLength) {
|
||||
accessHandle.flush();
|
||||
accessHandle.close();
|
||||
|
||||
const fileBlob = await draftHandle.getFile();
|
||||
const blob = new Blob([fileBlob], { type: 'application/octet-stream' });
|
||||
|
||||
postMessage({ type: 'result', blob: blob, fileName: fileName });
|
||||
}
|
||||
break;
|
||||
}
|
||||
case Operation.DeleteFiles: {
|
||||
for await (const [name, handle] of root.entries()) {
|
||||
if (handle.kind === 'file') {
|
||||
await root.removeEntry(name);
|
||||
} else if (handle.kind === 'directory') {
|
||||
await root.removeEntry(name, { recursive: true });
|
||||
}
|
||||
}
|
||||
break;
|
||||
}
|
||||
}
|
||||
} catch (error) {
|
||||
postMessage({ error: error.message });
|
||||
}
|
||||
};
|
||||
@@ -9,6 +9,11 @@ td {
|
||||
word-break: break-all;
|
||||
}
|
||||
|
||||
.ui.container{
|
||||
width: 95vw !important;
|
||||
max-width: 1680px !important;
|
||||
}
|
||||
|
||||
.nb-container {
|
||||
padding-top: 75px;
|
||||
min-height: 100vh;
|
||||
|
||||
+134
-16
@@ -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" ||
|
||||
@@ -98,7 +99,11 @@ function showFormModal(modelSelector, formID, URL, getData) {
|
||||
item.name === "DisplayIndex" ||
|
||||
item.name === "Type" ||
|
||||
item.name === "Cover" ||
|
||||
item.name === "Duration"
|
||||
item.name === "Duration" ||
|
||||
item.name === "MaxRetries" ||
|
||||
item.name === "Provider" ||
|
||||
item.name === "WebhookMethod" ||
|
||||
item.name === "WebhookRequestType"
|
||||
) {
|
||||
obj[item.name] = parseInt(item.value);
|
||||
} else if (item.name.endsWith("Latency")) {
|
||||
@@ -127,6 +132,16 @@ function showFormModal(modelSelector, formID, URL, getData) {
|
||||
}
|
||||
}
|
||||
|
||||
if (item.name.endsWith("DDNSProfilesRaw")) {
|
||||
if (item.value.length > 2) {
|
||||
obj[item.name] = JSON.stringify(
|
||||
[...item.value.matchAll(/\d+/gm)].map((k) =>
|
||||
parseInt(k[0])
|
||||
)
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
return obj;
|
||||
}, {});
|
||||
$.post(URL, JSON.stringify(data))
|
||||
@@ -206,6 +221,7 @@ function addOrEditAlertRule(rule) {
|
||||
);
|
||||
}
|
||||
}
|
||||
// 需要在 showFormModal 进一步拼接数组
|
||||
modal
|
||||
.find("input[name=FailTriggerTasksRaw]")
|
||||
.val(rule ? "[]," + failTriggerTasks.substr(1, failTriggerTasks.length - 2) : "[]");
|
||||
@@ -255,6 +271,77 @@ function addOrEditNotification(notification) {
|
||||
);
|
||||
}
|
||||
|
||||
function addOrEditDDNS(ddns) {
|
||||
const modal = $(".ddns.modal");
|
||||
modal.children(".header").text((ddns ? LANG.Edit : LANG.Add));
|
||||
modal
|
||||
.find(".nezha-primary-btn.button")
|
||||
.html(
|
||||
ddns
|
||||
? LANG.Edit + '<i class="edit icon"></i>'
|
||||
: LANG.Add + '<i class="add icon"></i>'
|
||||
);
|
||||
modal.find("input[name=ID]").val(ddns ? ddns.ID : null);
|
||||
modal.find("input[name=Name]").val(ddns ? ddns.Name : null);
|
||||
modal.find("input[name=DomainsRaw]").val(ddns ? ddns.DomainsRaw : null);
|
||||
modal.find("input[name=AccessID]").val(ddns ? ddns.AccessID : null);
|
||||
modal.find("input[name=AccessSecret]").val(ddns ? ddns.AccessSecret : null);
|
||||
modal.find("input[name=MaxRetries]").val(ddns ? ddns.MaxRetries : 3);
|
||||
modal.find("input[name=WebhookURL]").val(ddns ? ddns.WebhookURL : null);
|
||||
modal
|
||||
.find("textarea[name=WebhookHeaders]")
|
||||
.val(ddns ? ddns.WebhookHeaders : null);
|
||||
modal
|
||||
.find("textarea[name=WebhookRequestBody]")
|
||||
.val(ddns ? ddns.WebhookRequestBody : null);
|
||||
modal
|
||||
.find("select[name=Provider]")
|
||||
.val(ddns ? ddns.Provider : 0);
|
||||
modal
|
||||
.find("select[name=WebhookMethod]")
|
||||
.val(ddns ? ddns.WebhookMethod : 1);
|
||||
modal
|
||||
.find("select[name=WebhookRequestType]")
|
||||
.val(ddns ? ddns.WebhookRequestType : 1);
|
||||
if (ddns && ddns.EnableIPv4) {
|
||||
modal.find(".ui.enableipv4.checkbox").checkbox("set checked");
|
||||
} else {
|
||||
modal.find(".ui.enableipv4.checkbox").checkbox("set unchecked");
|
||||
}
|
||||
if (ddns && ddns.EnableIPv6) {
|
||||
modal.find(".ui.enableipv6.checkbox").checkbox("set checked");
|
||||
} else {
|
||||
modal.find(".ui.enableipv6.checkbox").checkbox("set unchecked");
|
||||
}
|
||||
showFormModal(
|
||||
".ddns.modal",
|
||||
"#ddnsForm",
|
||||
"/api/ddns"
|
||||
);
|
||||
}
|
||||
|
||||
function addOrEditNAT(nat) {
|
||||
const modal = $(".nat.modal");
|
||||
modal.children(".header").text((nat ? LANG.Edit : LANG.Add));
|
||||
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 })
|
||||
}
|
||||
@@ -302,12 +389,38 @@ function addOrEditServer(server, conf) {
|
||||
modal.find("input[name=id]").val(server ? server.ID : null);
|
||||
modal.find("input[name=name]").val(server ? server.Name : null);
|
||||
modal.find("input[name=Tag]").val(server ? server.Tag : null);
|
||||
modal.find("input[name=DDNSDomain]").val(server ? server.DDNSDomain : null);
|
||||
modal.find("input[name=DDNSProfile]").val(server ? server.DDNSProfile : null);
|
||||
modal.find("a.ui.label.visible").each((i, el) => {
|
||||
el.remove();
|
||||
});
|
||||
var ddns;
|
||||
if (server) {
|
||||
ddns = server.DDNSProfilesRaw;
|
||||
let serverList;
|
||||
try {
|
||||
serverList = JSON.parse(ddns);
|
||||
} catch (error) {
|
||||
serverList = "[]";
|
||||
}
|
||||
const node = modal.find("i.dropdown.icon.ddnsProfiles");
|
||||
for (let i = 0; i < serverList.length; i++) {
|
||||
node.after(
|
||||
'<a class="ui label transition visible" data-value="' +
|
||||
serverList[i] +
|
||||
'" style="display: inline-block !important;">ID:' +
|
||||
serverList[i] +
|
||||
'<i class="delete icon"></i></a>'
|
||||
);
|
||||
}
|
||||
}
|
||||
// 需要在 showFormModal 进一步拼接数组
|
||||
modal
|
||||
.find("input[name=DDNSProfilesRaw]")
|
||||
.val(server ? "[]," + ddns.substr(1, ddns.length - 2) : "[]");
|
||||
modal
|
||||
.find("input[name=DisplayIndex]")
|
||||
.val(server ? server.DisplayIndex : null);
|
||||
modal.find("textarea[name=Note]").val(server ? server.Note : null);
|
||||
modal.find("textarea[name=PublicNote]").val(server ? server.PublicNote : null);
|
||||
if (server) {
|
||||
modal.find(".secret.field").attr("style", "");
|
||||
modal.find(".command.field").attr("style", "");
|
||||
@@ -318,26 +431,17 @@ function addOrEditServer(server, conf) {
|
||||
modal.find(".command.field").attr("style", "display:none");
|
||||
modal.find("input[name=secret]").val("");
|
||||
}
|
||||
if (server && server.HideForGuest) {
|
||||
modal.find(".ui.hideforguest.checkbox").checkbox("set checked");
|
||||
} else {
|
||||
modal.find(".ui.hideforguest.checkbox").checkbox("set unchecked");
|
||||
}
|
||||
if (server && server.EnableDDNS) {
|
||||
modal.find(".ui.enableddns.checkbox").checkbox("set checked");
|
||||
} else {
|
||||
modal.find(".ui.enableddns.checkbox").checkbox("set unchecked");
|
||||
}
|
||||
if (server && server.EnableIPv4) {
|
||||
modal.find(".ui.enableipv4.checkbox").checkbox("set checked");
|
||||
if (server && server.HideForGuest) {
|
||||
modal.find(".ui.hideforguest.checkbox").checkbox("set checked");
|
||||
} else {
|
||||
modal.find(".ui.enableipv4.checkbox").checkbox("set unchecked");
|
||||
}
|
||||
if (server && server.EnableIpv6) {
|
||||
modal.find(".ui.enableipv6.checkbox").checkbox("set checked");
|
||||
} else {
|
||||
modal.find(".ui.enableipv6.checkbox").checkbox("set unchecked");
|
||||
modal.find(".ui.hideforguest.checkbox").checkbox("set unchecked");
|
||||
}
|
||||
|
||||
showFormModal(".server.modal", "#serverForm", "/api/server");
|
||||
}
|
||||
|
||||
@@ -423,6 +527,7 @@ function addOrEditMonitor(monitor) {
|
||||
);
|
||||
}
|
||||
}
|
||||
// 需要在 showFormModal 进一步拼接数组
|
||||
modal
|
||||
.find("input[name=FailTriggerTasksRaw]")
|
||||
.val(monitor ? "[]," + failTriggerTasks.substr(1, failTriggerTasks.length - 2) : "[]");
|
||||
@@ -468,6 +573,7 @@ function addOrEditCron(cron) {
|
||||
);
|
||||
}
|
||||
}
|
||||
// 需要在 showFormModal 进一步拼接数组
|
||||
modal
|
||||
.find("input[name=ServersRaw]")
|
||||
.val(cron ? "[]," + servers.substr(1, servers.length - 2) : "[]");
|
||||
@@ -597,3 +703,15 @@ $(document).ready(() => {
|
||||
});
|
||||
} catch (error) { }
|
||||
});
|
||||
|
||||
$(document).ready(() => {
|
||||
try {
|
||||
$(".ui.ddns.search.dropdown").dropdown({
|
||||
clearable: true,
|
||||
apiSettings: {
|
||||
url: "/api/search-ddns?word={query}",
|
||||
cache: false,
|
||||
},
|
||||
});
|
||||
} catch (error) { }
|
||||
});
|
||||
|
||||
Binary file not shown.
|
After Width: | Height: | Size: 3.9 KiB |
Binary file not shown.
|
After Width: | Height: | Size: 13 KiB |
@@ -0,0 +1,36 @@
|
||||
{
|
||||
"name": "Nezha",
|
||||
"short_name": "Nezha",
|
||||
"icons": [
|
||||
{
|
||||
"src": "/static/manifest-192x192.png",
|
||||
"sizes": "192x192",
|
||||
"type": "image/png",
|
||||
"purpose": "any"
|
||||
},
|
||||
{
|
||||
"src": "/static/manifest-192x192.png",
|
||||
"sizes": "192x192",
|
||||
"type": "image/png",
|
||||
"purpose": "maskable"
|
||||
},
|
||||
{
|
||||
"src": "/static/manifest-512x512.png",
|
||||
"sizes": "512x512",
|
||||
"type": "image/png",
|
||||
"purpose": "any"
|
||||
},
|
||||
{
|
||||
"src": "/static/manifest-512x512.png",
|
||||
"sizes": "512x512",
|
||||
"type": "image/png",
|
||||
"purpose": "maskable"
|
||||
}
|
||||
],
|
||||
"start_url": "/",
|
||||
"display": "standalone",
|
||||
"theme_color": "#000000",
|
||||
"background_color": "#000000",
|
||||
"lang": "eu-US",
|
||||
"dir": "auto"
|
||||
}
|
||||
@@ -0,0 +1,36 @@
|
||||
{
|
||||
"name": "哪吒监控",
|
||||
"short_name": "哪吒监控",
|
||||
"icons": [
|
||||
{
|
||||
"src": "/static/manifest-192x192.png",
|
||||
"sizes": "192x192",
|
||||
"type": "image/png",
|
||||
"purpose": "any"
|
||||
},
|
||||
{
|
||||
"src": "/static/manifest-192x192.png",
|
||||
"sizes": "192x192",
|
||||
"type": "image/png",
|
||||
"purpose": "maskable"
|
||||
},
|
||||
{
|
||||
"src": "/static/manifest-512x512.png",
|
||||
"sizes": "512x512",
|
||||
"type": "image/png",
|
||||
"purpose": "any"
|
||||
},
|
||||
{
|
||||
"src": "/static/manifest-512x512.png",
|
||||
"sizes": "512x512",
|
||||
"type": "image/png",
|
||||
"purpose": "maskable"
|
||||
}
|
||||
],
|
||||
"start_url": "/",
|
||||
"display": "standalone",
|
||||
"theme_color": "#000000",
|
||||
"background_color": "#000000",
|
||||
"lang": "zh-CN",
|
||||
"dir": "auto"
|
||||
}
|
||||
+55
-60
@@ -1,46 +1,35 @@
|
||||
/* 屏幕适配 */
|
||||
@media only screen and (min-width:1200px) {
|
||||
.ui.container {
|
||||
width:95% !important;
|
||||
font-size: 90% !important;
|
||||
max-width: 1300px !important;
|
||||
}
|
||||
}
|
||||
@media only screen and (max-width:767px) {
|
||||
.ui.card>.content>.header:not(.ui),.ui.cards>.card>.content>.header:not(.ui) {
|
||||
margin-top:0.4em !important;
|
||||
.ui.card>.content>.header:not(.ui),
|
||||
.ui.cards>.card>.content>.header:not(.ui) {
|
||||
margin-top: 0.4em !important;
|
||||
}
|
||||
.ui.menu .item>img:not(.ui){
|
||||
|
||||
.ui.menu .item>img:not(.ui) {
|
||||
width: 2.2rem;
|
||||
}
|
||||
.ui.menu .item:before{
|
||||
width:0.5px;
|
||||
|
||||
.ui.menu .item:before {
|
||||
width: 0.5px;
|
||||
}
|
||||
.ui.menu .item{
|
||||
|
||||
.ui.menu .item {
|
||||
padding: 0.9rem 0.55rem;
|
||||
}
|
||||
.ui.large.menu{
|
||||
|
||||
.ui.large.menu {
|
||||
font-size: 1rem;
|
||||
}
|
||||
}
|
||||
|
||||
i.icon {
|
||||
color:#000;
|
||||
width:1.2em !important;
|
||||
}
|
||||
i.fi {
|
||||
width:0.9em;
|
||||
margin:0px 6px 0px 2px;
|
||||
color: #000;
|
||||
width: 1.2em !important;
|
||||
}
|
||||
|
||||
body {
|
||||
content:" " !important;
|
||||
background:fixed !important;
|
||||
z-index:-1 !important;
|
||||
top:0 !important;
|
||||
right:0 !important;
|
||||
bottom:0 !important;
|
||||
left:0 !important;
|
||||
i.fi {
|
||||
width: 0.9em;
|
||||
margin: 0px 6px 0px 2px;
|
||||
}
|
||||
|
||||
td {
|
||||
@@ -55,6 +44,11 @@ td {
|
||||
margin-bottom: -47px;
|
||||
}
|
||||
|
||||
.ui.container {
|
||||
width: 95%;
|
||||
max-width: 1680px !important;
|
||||
}
|
||||
|
||||
#app .ui.fluid.accordion {
|
||||
margin-bottom: 1rem;
|
||||
}
|
||||
@@ -65,79 +59,80 @@ td {
|
||||
padding-top: unset;
|
||||
}
|
||||
|
||||
.login.nb-container > .grid {
|
||||
.login.nb-container>.grid {
|
||||
width: 100%;
|
||||
margin: 0 auto;
|
||||
}
|
||||
|
||||
.login.nb-container > .grid .column {
|
||||
.login.nb-container>.grid .column {
|
||||
max-width: 450px;
|
||||
}
|
||||
|
||||
.ui.menu .item-right:before{
|
||||
width:0px;
|
||||
.ui.menu .item-right:before {
|
||||
width: 0px;
|
||||
}
|
||||
|
||||
.status.cards .flag {
|
||||
margin-right: 0 !important;
|
||||
}
|
||||
|
||||
.status.cards .header > .info.icon {
|
||||
.status.cards .header>.info.icon {
|
||||
float: right;
|
||||
margin-right: 0;
|
||||
}
|
||||
|
||||
.ui.grid {
|
||||
margin-bottom:-0.5em
|
||||
margin-bottom: -0.5em
|
||||
}
|
||||
|
||||
.ui.card>.content>.header:not(.ui), .ui.cards>.card>.content>.header:not(.ui){
|
||||
.ui.card>.content>.header:not(.ui),
|
||||
.ui.cards>.card>.content>.header:not(.ui) {
|
||||
line-height: 1em;
|
||||
}
|
||||
|
||||
.status.cards .wide.column {
|
||||
padding-top: 0 !important;
|
||||
padding-bottom: 0 !important;
|
||||
height:2.3rem !important;
|
||||
height: 2.3rem !important;
|
||||
}
|
||||
|
||||
.status.cards .wide.column:nth-child(1) {
|
||||
margin-top:1.2rem !important;
|
||||
margin-top: 1.2rem !important;
|
||||
}
|
||||
|
||||
.status.cards .wide.column:nth-child(2) {
|
||||
margin-top:1.2rem !important;
|
||||
margin-top: 1.2rem !important;
|
||||
}
|
||||
|
||||
.status.cards .three.wide.column {
|
||||
text-align: center;
|
||||
width: 22%!important;
|
||||
width: 22% !important;
|
||||
}
|
||||
|
||||
.status.cards .thirteen.wide.column{
|
||||
width: 78%!important;
|
||||
padding-left:0;
|
||||
.status.cards .thirteen.wide.column {
|
||||
width: 78% !important;
|
||||
padding-left: 0;
|
||||
}
|
||||
|
||||
.status.cards .description {
|
||||
padding-bottom:0 !important;
|
||||
padding-bottom: 0 !important;
|
||||
}
|
||||
|
||||
.status.cards .flag {
|
||||
margin-right:0.5rem !important;
|
||||
margin-right: 0.5rem !important;
|
||||
}
|
||||
|
||||
.status.cards .header > .info.icon {
|
||||
.status.cards .header>.info.icon {
|
||||
float: right;
|
||||
margin-right:0 !important;
|
||||
margin-right: 0 !important;
|
||||
}
|
||||
|
||||
.ui.popup:before {
|
||||
display: none;
|
||||
}
|
||||
|
||||
.closePopup{
|
||||
color:rgb(10, 148, 242) !important;
|
||||
.closePopup {
|
||||
color: rgb(10, 148, 242) !important;
|
||||
position: absolute;
|
||||
top: 7px;
|
||||
right: 10px;
|
||||
@@ -150,26 +145,26 @@ td {
|
||||
}
|
||||
|
||||
.ui.content {
|
||||
margin:0 !important;
|
||||
padding:1em !important;
|
||||
margin: 0 !important;
|
||||
padding: 1em !important;
|
||||
}
|
||||
|
||||
.status.cards .ui.content.popup {
|
||||
min-width:calc(100%)!important;
|
||||
line-height:2rem !important;
|
||||
border-radius:5px !important;
|
||||
border:1px solid transparent !important;
|
||||
font-family:Arial,Helvetica,sans-serif !important;
|
||||
min-width: calc(100%) !important;
|
||||
line-height: 2rem !important;
|
||||
border-radius: 5px !important;
|
||||
border: 1px solid transparent !important;
|
||||
font-family: Arial, Helvetica, sans-serif !important;
|
||||
}
|
||||
|
||||
.status.cards .outline.icon {
|
||||
margin-right:1px !important;
|
||||
margin-right: 1px !important;
|
||||
}
|
||||
|
||||
.ui.progress .bar {
|
||||
min-width:1.8em !important;
|
||||
border-radius:5px !important;
|
||||
line-height:1.65em !important;
|
||||
min-width: 1.8em !important;
|
||||
border-radius: 5px !important;
|
||||
line-height: 1.65em !important;
|
||||
text-align: right;
|
||||
padding-right: 0.4em;
|
||||
color: rgba(255, 255, 255, 0.7);
|
||||
@@ -182,7 +177,7 @@ td {
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.service-status .delay-today > i {
|
||||
.service-status .delay-today>i {
|
||||
display: inline-block;
|
||||
width: 1.2em;
|
||||
height: 1.2em;
|
||||
|
||||
+11
@@ -13,7 +13,18 @@ const mixinsVue = {
|
||||
this.isMobile = this.checkIsMobile();
|
||||
this.preferredTemplate = this.getCookie('preferred_theme') ? this.getCookie('preferred_theme') : this.$root.defaultTemplate;
|
||||
},
|
||||
mounted() {
|
||||
this.initDropdown();
|
||||
},
|
||||
methods: {
|
||||
initDropdown() {
|
||||
if(this.isMobile) $('.ui.dropdown').dropdown({
|
||||
action: 'hide',
|
||||
on: 'click',
|
||||
duration: 100,
|
||||
direction: 'direction'
|
||||
});
|
||||
},
|
||||
toggleTemplate(template) {
|
||||
if( template != this.preferredTemplate){
|
||||
this.preferredTemplate = template;
|
||||
|
||||
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
+94
-83
@@ -1,5 +1,6 @@
|
||||
body[theme="dark"] {
|
||||
background: #31363b;
|
||||
font-family: "Helvetica Neue",Helvetica,Arial,sans-serif;
|
||||
background-color: rgba(49, 54, 59, 1);
|
||||
color: #f1f1f1;
|
||||
}
|
||||
|
||||
@@ -7,14 +8,14 @@ body[theme="dark"] .navbar .navbar-brand {
|
||||
color: #ffffff;
|
||||
}
|
||||
|
||||
body[theme="dark"] .navbar .navbar-brand {
|
||||
color: #ffffff;
|
||||
body[theme="dark"] .navbar .navbar-nav li.pc-active a,
|
||||
body[theme="dark"] .navbar .navbar-nav li.m-active a {
|
||||
color: rgba(73, 146, 255, 1);
|
||||
}
|
||||
|
||||
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,31 +25,25 @@ 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,
|
||||
body[theme="dark"] .navbar .navbar-nav > .open > a:hover,
|
||||
body[theme="dark"] .navbar .navbar-nav > .active > a,
|
||||
body[theme="dark"] .navbar .navbar-nav > .open > a{
|
||||
background-image: none !important;
|
||||
box-shadow: none !important;
|
||||
background-color: transparent !important;
|
||||
body[theme="dark"] .navbar .navbar-nav > .open > a {
|
||||
background-image: none;
|
||||
box-shadow: none;
|
||||
background-color: transparent;
|
||||
}
|
||||
|
||||
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;
|
||||
@@ -56,84 +51,86 @@ 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,
|
||||
body[theme="dark"] .table > thead > tr.node-group-cell > th:before{
|
||||
body[theme="dark"] .table > thead > tr.node-group-cell > th:before {
|
||||
background-color: rgba(58, 62, 65, 1);
|
||||
}
|
||||
|
||||
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(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(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: none;
|
||||
background-color: rgba(255, 255, 255, 0.075);
|
||||
}
|
||||
|
||||
body[theme="dark"] .progress-bar {
|
||||
color: #ffffff;
|
||||
}
|
||||
|
||||
body[theme="dark"] .progress-bar-success {
|
||||
background-image: linear-gradient(to right, rgba(77, 133, 58, 1) 0, rgba(54, 126, 54, 1) 100%);
|
||||
}
|
||||
|
||||
body[theme="dark"] .progress-bar-warning {
|
||||
background-image: linear-gradient(to right, rgba(153, 95, 13, 1) 0, rgba(181, 113, 15, 1) 100%);
|
||||
}
|
||||
|
||||
body[theme="dark"] .progress-bar-danger {
|
||||
background-image: linear-gradient(to right, rgba(145, 35, 32, 1) 0, rgba(161, 38, 35, 1) 100%);
|
||||
}
|
||||
|
||||
body[theme="dark"] .progress-offline .progress-bar-success {
|
||||
background-image: linear-gradient(rgba(128, 128, 128, 1) 0, rgba(128, 128, 128, 1) 100%);
|
||||
}
|
||||
|
||||
body[theme="dark"] .progress-offline .progress-bar-warning {
|
||||
background-image: linear-gradient(rgba(128, 128, 128, 1) 0, rgba(128, 128, 128, 1) 100%);
|
||||
}
|
||||
|
||||
body[theme="dark"] .progress-offline .progress-bar-danger {
|
||||
background-image: linear-gradient(rgba(128, 128, 128, 1) 0, rgba(128, 128, 128, 1) 100%);
|
||||
}
|
||||
|
||||
body[theme="dark"] .node-cell.status .status-icon.online {
|
||||
background-image: linear-gradient(#4d853a 0, #367e36 100%);
|
||||
background-image: linear-gradient(rgba(77, 133, 58, 1) 0, rgba(54, 126, 54, 1) 100%);
|
||||
}
|
||||
|
||||
body[theme="dark"] .node-cell.status .status-icon.offline {
|
||||
background-image: linear-gradient(#9b2522 0, #9b2522 100%);
|
||||
background-image: linear-gradient(rgba(155, 37, 34, 1) 0, rgba(161, 38, 35, 1) 100%);
|
||||
}
|
||||
|
||||
body[theme="dark"] .service-status .danger {
|
||||
@@ -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,
|
||||
@@ -158,15 +155,29 @@ body[theme="dark"] footer p a:hover {
|
||||
text-decoration: none;
|
||||
}
|
||||
|
||||
body[theme="dark"] .modal-content{
|
||||
background-color: #1c1d26;
|
||||
body[theme="dark"] .modal-content {
|
||||
background-color: rgba(28, 29, 38, 1);
|
||||
color: #f1f1f1;
|
||||
}
|
||||
|
||||
body[theme="dark"] .modal-header{
|
||||
body[theme="dark"] #earthChartBox .modal-content {
|
||||
background-color: rgba(0, 0, 0, 1);
|
||||
color: #f1f1f1;
|
||||
}
|
||||
|
||||
body[theme="dark"] .modal-header {
|
||||
border-bottom-color:#080808;
|
||||
}
|
||||
|
||||
body[theme="dark"] .modal-header i{
|
||||
body[theme="dark"] .modal-header i.xclose {
|
||||
color: #f1f1f1;
|
||||
}
|
||||
|
||||
body[theme="dark"] .toolbox i {
|
||||
color: rgba(241, 241, 241, 1);
|
||||
background-color: rgba(0, 0, 0, 0.5);
|
||||
}
|
||||
|
||||
body[theme="dark"] .network-box .network-box-header {
|
||||
border-bottom-color: rgba(110, 112, 121, 0.25);
|
||||
}
|
||||
@@ -0,0 +1,123 @@
|
||||
body[theme="dark"]::before {
|
||||
content: "";
|
||||
position: fixed;
|
||||
top: 0;
|
||||
left: 0;
|
||||
width: 100vw;
|
||||
height: 100vh;
|
||||
background-image: url(/static/theme-server-status/img/bg.jpg);
|
||||
background-repeat: no-repeat;
|
||||
background-position: 50% 50%;
|
||||
background-size: cover;
|
||||
z-index: -1;
|
||||
}
|
||||
|
||||
body[theme="dark"] {
|
||||
background-color: rgba(0, 0, 0, 0.8);
|
||||
color: #f1f1f1;
|
||||
}
|
||||
|
||||
body[theme="dark"] .navbar {
|
||||
/** 顶部导航条 背景 **/
|
||||
background-color: rgba(0, 0, 0, 0.8);
|
||||
box-shadow: none;
|
||||
border: none;
|
||||
}
|
||||
|
||||
body[theme="dark"] .navbar .navbar-brand {
|
||||
color: #ffffff;
|
||||
}
|
||||
|
||||
body[theme="dark"] .navbar .dropdown-menu {
|
||||
/** 二级导航下拉 背景 **/
|
||||
background-color: rgba(0, 0, 0, 0.85);
|
||||
border-top: none;
|
||||
border-color: #31363b;
|
||||
box-shadow: rgba(0, 0, 0, 0.18) 0px 6px 12px;
|
||||
}
|
||||
|
||||
body[theme="dark"] .navbar .dropdown-menu > li > a {
|
||||
color: #c8c3bc;
|
||||
}
|
||||
|
||||
body[theme="dark"] .navbar .dropdown-menu > li > a:focus,
|
||||
body[theme="dark"] .navbar .dropdown-menu > li > a:hover {
|
||||
/** 二级导航鼠标悬停选中背景 **/
|
||||
background-color: rgba(0, 0, 0, 0.95);
|
||||
background-image: linear-gradient(#1c1d26 0, #1c1d26 100%);
|
||||
}
|
||||
|
||||
body[theme="dark"] .navbar .navbar-nav .open .dropdown-menu > li > a {
|
||||
color: #f1f1f1;
|
||||
}
|
||||
|
||||
body[theme="dark"] .table,
|
||||
body[theme="dark"] .table-condensed > tbody > tr,
|
||||
body[theme="dark"] .table-hover > tbody > tr,
|
||||
body[theme="dark"] .table-hover > tbody > tr:hover,
|
||||
body[theme="dark"] .table-striped tbody > tr.even,
|
||||
body[theme="dark"] .table-striped tbody > tr.odd,
|
||||
body[theme="dark"] .table-striped tbody > tr.even > td,
|
||||
body[theme="dark"] .table-striped tbody > tr.even > th,
|
||||
body[theme="dark"] .table-striped tbody > tr.odd > td,
|
||||
body[theme="dark"] .table-striped tbody > tr.odd > th,
|
||||
body[theme="dark"] .table-striped tbody > tr.even > td:hover,
|
||||
body[theme="dark"] .table-striped tbody > tr.even > th:hover,
|
||||
body[theme="dark"] .table-striped tbody > tr.odd > td:hover,
|
||||
body[theme="dark"] .table-striped tbody > tr.odd > th:hover,
|
||||
body[theme="dark"] .table-striped tbody > tr.expandRow:hover {
|
||||
background-color: transparent !important;
|
||||
}
|
||||
|
||||
body[theme="dark"] .content {
|
||||
/** 主box 背景 **/
|
||||
background-color: rgba(28, 29, 38, 0.8);
|
||||
border: none;
|
||||
box-shadow: rgba(0, 0, 0, 0.5) 0 0.625em 2em;
|
||||
-webkit-box-shadow: rgba(0, 0, 0, 0.5) 0 0.625em 2em;
|
||||
box-shadow: rgba(0, 0, 0, 0.5) 0 0.625em 2em;
|
||||
}
|
||||
|
||||
body[theme="dark"] .table > thead > tr.node-group-tag > th,
|
||||
body[theme="dark"] .table > thead > tr.node-group-tag > th:before {
|
||||
background: unset;
|
||||
}
|
||||
|
||||
body[theme="dark"] .table > tbody > tr > td:before,
|
||||
body[theme="dark"] .table > tfoot > tr > td:before,
|
||||
body[theme="dark"] .table > thead > tr > td:before,
|
||||
body[theme="dark"] .table > thead > tr.node-group-cell > th:before {
|
||||
/** border-bottom 颜色 **/
|
||||
background-color: rgba(155, 155, 155, 0.1);
|
||||
}
|
||||
|
||||
body[theme="dark"] .table-hover > tbody > tr:not(.expandRow):hover > td {
|
||||
background-color: unset;
|
||||
}
|
||||
|
||||
/* expandRow展开部分样式 */
|
||||
body[theme="dark"] .table > tbody > tr.expandRow.odd > td:before {
|
||||
background-color: unset;
|
||||
}
|
||||
|
||||
body[theme="dark"] .table > tbody > tr.expandRow.even > td:before {
|
||||
background-color: unset;
|
||||
}
|
||||
/* expandRow展开部分样式结束 */
|
||||
|
||||
body[theme="dark"] .progress {
|
||||
background-image: none;
|
||||
background-color: rgba(255, 255, 255, 0.075);
|
||||
}
|
||||
|
||||
body[theme="dark"] .progress-bar-success {
|
||||
background-image: linear-gradient(to right, rgba(77, 133, 58, 0.9) 0, rgba(54, 126, 54, 0.9) 100%);
|
||||
}
|
||||
|
||||
body[theme="dark"] .progress-bar-warning {
|
||||
background-image: linear-gradient(to right, rgba(153, 95, 13, 0.9) 0, rgba(181, 113, 15, 0.9) 100%);
|
||||
}
|
||||
|
||||
body[theme="dark"] .progress-bar-danger {
|
||||
background-image: linear-gradient(to right, rgba(145, 35, 32, 0.8) 0, rgba(161, 38, 35, 0.8) 100%);
|
||||
}
|
||||
+117
-48
File diff suppressed because one or more lines are too long
@@ -0,0 +1,134 @@
|
||||
body[theme="light"]::before {
|
||||
content: "";
|
||||
position: fixed;
|
||||
top: 0;
|
||||
left: 0;
|
||||
width: 100vw;
|
||||
height: 100vh;
|
||||
background-image: url(/static/theme-server-status/img/bg.jpg);
|
||||
background-repeat: no-repeat;
|
||||
background-position: 50% 50%;
|
||||
background-size: cover;
|
||||
z-index: -1;
|
||||
}
|
||||
|
||||
body[theme="light"] {
|
||||
background-image: unset;
|
||||
}
|
||||
|
||||
body[theme="light"]::after {
|
||||
content: "";
|
||||
position: fixed;
|
||||
}
|
||||
|
||||
body[theme="light"] .navbar {
|
||||
/** 顶部导航条 背景 **/
|
||||
background-color: rgba(255, 255, 255, 0.9);
|
||||
border-bottom: 1px #cecece solid;
|
||||
box-shadow: 0 1px 10px rgba(0, 0, 0, 0.1);
|
||||
}
|
||||
|
||||
body[theme="light"] .navbar .dropdown-menu {
|
||||
/** 二级导航下拉 **/
|
||||
background-color: rgba(255, 255, 255, 0.8);
|
||||
border-color: rgba(0, 0, 0, .05);
|
||||
}
|
||||
|
||||
|
||||
body[theme="light"] .navbar .dropdown-menu > li > a:focus,
|
||||
body[theme="light"] .navbar .dropdown-menu > li > a:hover {
|
||||
/** 二级导航鼠标悬停选中背景 **/
|
||||
background-color: rgba(0, 0, 0, .05);
|
||||
}
|
||||
|
||||
body[theme="light"] .navbar .navbar-collapse {
|
||||
border-color:rgba(0, 0, 0, 0.085);
|
||||
}
|
||||
|
||||
body[theme="light"] .navbar .navbar-toggle .icon-bar {
|
||||
background-color: rgba(0, 0, 0, 0.9);
|
||||
}
|
||||
|
||||
body[theme="light"] .navbar .navbar-toggle:focus,
|
||||
body[theme="light"] .navbar .navbar-toggle:hover {
|
||||
background-color: unset;
|
||||
}
|
||||
|
||||
body[theme="light"] .table,
|
||||
body[theme="light"] .table-condensed > tbody > tr,
|
||||
body[theme="light"] .table-hover > tbody > tr,
|
||||
body[theme="light"] .table-hover > tbody > tr:hover,
|
||||
body[theme="light"] .table-striped tbody > tr.even,
|
||||
body[theme="light"] .table-striped tbody > tr.odd,
|
||||
body[theme="light"] .table-striped tbody > tr.even > td,
|
||||
body[theme="light"] .table-striped tbody > tr.even > th,
|
||||
body[theme="light"] .table-striped tbody > tr.odd > td,
|
||||
body[theme="light"] .table-striped tbody > tr.odd > th,
|
||||
body[theme="light"] .table-striped tbody > tr.even > td:hover,
|
||||
body[theme="light"] .table-striped tbody > tr.even > th:hover,
|
||||
body[theme="light"] .table-striped tbody > tr.odd > td:hover,
|
||||
body[theme="light"] .table-striped tbody > tr.odd > th:hover,
|
||||
body[theme="light"] .table-striped tbody > tr.expandRow:hover {
|
||||
background-color: transparent !important;
|
||||
}
|
||||
|
||||
body[theme="light"] .content {
|
||||
/** 主box 背景 **/
|
||||
background-color: rgba(245, 245, 245, 0.8);
|
||||
border: none;
|
||||
box-shadow: rgba(0, 0, 0, 0.5) 0 0.625em 2em;
|
||||
}
|
||||
|
||||
body[theme="light"] .table > thead > tr.node-group-tag > th,
|
||||
body[theme="light"] .table > thead > tr.node-group-tag > th:before {
|
||||
background: unset;
|
||||
}
|
||||
|
||||
body[theme="light"] .table > tbody > tr > td:before,
|
||||
body[theme="light"] .table > tfoot > tr > td:before,
|
||||
body[theme="light"] .table > thead > tr > td:before,
|
||||
body[theme="light"] .table > thead > tr.node-group-cell > th:before {
|
||||
/** border-bottom 颜色 **/
|
||||
background-color: rgba(0, 0, 0, 0.075);
|
||||
}
|
||||
|
||||
body[theme="light"] .table-hover > tbody > tr:not(.expandRow):hover > td {
|
||||
background-color: unset;
|
||||
}
|
||||
|
||||
/* expandRow展开部分样式 */
|
||||
body[theme="light"] .table > tbody > tr.expandRow.odd > td:before {
|
||||
background-color: unset;
|
||||
}
|
||||
|
||||
body[theme="light"] .table > tbody > tr.expandRow.even > td:before {
|
||||
background-color: unset;
|
||||
}
|
||||
/* expandRow展开部分样式结束 */
|
||||
|
||||
body[theme="light"] .progress {
|
||||
background-image: none;
|
||||
background-color: rgba(0, 0, 0, 0.015);
|
||||
}
|
||||
|
||||
body[theme="light"] .progress-bar-success {
|
||||
background-image: linear-gradient(to right, rgba(50, 205, 50, 0.6) 0, rgba(68, 157, 68, 0.6) 100%);
|
||||
}
|
||||
|
||||
body[theme="light"] .progress-bar-warning {
|
||||
background-image: linear-gradient(to right, rgba(236, 151, 31, 0.6) 0, rgba(255, 125, 80, 0.6) 100%);
|
||||
}
|
||||
|
||||
body[theme="light"] .progress-bar-danger {
|
||||
background-image: linear-gradient(to right, rgba(255, 70, 0, 0.6) 0, rgba(201, 48, 44,0.6) 100%);
|
||||
}
|
||||
|
||||
body[theme="light"] .toolbox i {
|
||||
background-color: rgba(255, 255, 255, .55);
|
||||
}
|
||||
|
||||
@media only screen and (max-width: 767px) {
|
||||
body[theme="light"] .navbar .navbar-nav .open .dropdown-menu {
|
||||
background-color: rgba(235, 235, 235, 0.75);
|
||||
}
|
||||
}
|
||||
+250
-51
@@ -3,16 +3,26 @@ body {
|
||||
padding-bottom: 30px !important;
|
||||
}
|
||||
|
||||
.container-fluid{
|
||||
max-width: 1680px;
|
||||
width: 95vw;
|
||||
margin: 0 auto;
|
||||
}
|
||||
|
||||
.container-fluid::-webkit-scrollbar {
|
||||
display: none;
|
||||
}
|
||||
|
||||
.nezha {
|
||||
min-height: calc(100vh - 100px);
|
||||
}
|
||||
|
||||
/* 导航部分 开始*/
|
||||
.navbar {
|
||||
min-height: 40px !important;
|
||||
margin:0 auto;
|
||||
font-size: 14px;
|
||||
}
|
||||
|
||||
.navbar .container{
|
||||
max-width: 95vw;
|
||||
margin: 0 auto;
|
||||
z-index:99999999;
|
||||
}
|
||||
|
||||
.navbar-inverse{
|
||||
@@ -46,8 +56,8 @@ body {
|
||||
word-break: break-all;
|
||||
}
|
||||
|
||||
.navbar .node-cell-expand-label {
|
||||
/*margin-right: 5px;*/
|
||||
.navbar .dropdown a{
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.navbar .dropdown .dropdown-toggle {
|
||||
@@ -59,7 +69,7 @@ body {
|
||||
margin:0px -15px;
|
||||
}
|
||||
|
||||
.navbar .navbar-nav>li>a {
|
||||
.navbar .navbar-nav > li > a {
|
||||
color:#f1f1f1;
|
||||
}
|
||||
|
||||
@@ -70,12 +80,10 @@ body {
|
||||
.navbar .navbar-collapse{
|
||||
max-height: 500px;
|
||||
}
|
||||
|
||||
/* 导航部分 结束 */
|
||||
|
||||
|
||||
/* toolbox 开始 */
|
||||
|
||||
.toolbox {
|
||||
position: fixed;
|
||||
bottom:20px;
|
||||
@@ -116,7 +124,6 @@ body {
|
||||
.toolbox .showGoTop i.goTop {
|
||||
font-size: 1.55rem;
|
||||
}
|
||||
|
||||
/* toolbox 结束 */
|
||||
|
||||
|
||||
@@ -133,6 +140,10 @@ body {
|
||||
border-radius: 3px;
|
||||
}
|
||||
|
||||
.table-responsive{
|
||||
border: none;
|
||||
}
|
||||
|
||||
.table th, .table td {
|
||||
text-align: left;
|
||||
}
|
||||
@@ -162,6 +173,15 @@ body {
|
||||
height: 0.7px;
|
||||
}
|
||||
|
||||
table.monitor-detail{
|
||||
margin:5px 0px 10px 0px;
|
||||
}
|
||||
|
||||
tr.accordion-toggle{
|
||||
cursor:pointer;
|
||||
}
|
||||
|
||||
.table .network-thead-tr th,
|
||||
.table .node-group-tag th{
|
||||
font-size: 18px;
|
||||
padding-bottom:15px;
|
||||
@@ -230,16 +250,44 @@ body {
|
||||
max-width: 50px;
|
||||
}
|
||||
|
||||
.node-cell.remaining {
|
||||
min-width: 65px;
|
||||
max-width: 65px;
|
||||
}
|
||||
|
||||
.node-cell.remaining .additional {
|
||||
position: absolute;
|
||||
left: 0;
|
||||
font-size: 12px;
|
||||
text-align: center;
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.node-cell.remaining .additional small{
|
||||
white-space: nowrap;
|
||||
font-size: 80%;
|
||||
}
|
||||
|
||||
.temp-detail {
|
||||
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
|
||||
}
|
||||
/*正文结束*/
|
||||
|
||||
/* 服务页 正文*/
|
||||
.service-status {
|
||||
|
||||
}
|
||||
.service-status {}
|
||||
|
||||
.service-status .service-status-th{
|
||||
min-width:60px;
|
||||
@@ -285,61 +333,191 @@ body {
|
||||
.service-status .tooltip-inner {
|
||||
max-width: 500px;
|
||||
}
|
||||
|
||||
td.ping-network-quality {
|
||||
width: 600px;
|
||||
}
|
||||
/* 服务页 正文结束 */
|
||||
|
||||
|
||||
/* 服务器分布图 */
|
||||
#mapChartBox{
|
||||
z-index: 999999999;
|
||||
/* 网络页 正文*/
|
||||
.network-box .btn-group.open .dropdown-toggle {
|
||||
box-shadow:unset;
|
||||
}
|
||||
|
||||
.modal-content{
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
pointer-events: auto;
|
||||
.network-box .network-box-header {
|
||||
font-size: 18px;
|
||||
padding: 5px 0px 15px 5px;
|
||||
cursor: pointer;
|
||||
width: 100%;
|
||||
border-bottom-width: 1px;
|
||||
border-bottom-style: solid;
|
||||
}
|
||||
|
||||
.modal-dialog-centered{
|
||||
.network-box .network-box-header .dropdown-menu {
|
||||
max-height: 22.5em;
|
||||
overflow-y: auto;
|
||||
z-index:99999998;
|
||||
min-width: 200px;
|
||||
padding: 0px;
|
||||
}
|
||||
|
||||
.network-box .network-box-header .dropdown-menu::-webkit-scrollbar {
|
||||
display: none;
|
||||
}
|
||||
|
||||
.network-box .network-box-header .dropdown-menu li {
|
||||
height: 2em;
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.network-box .network-box-header .dropdown-menu li.input-group {
|
||||
margin: 0 auto;
|
||||
width: 1134px;
|
||||
max-width: 1134px;
|
||||
display: flex;
|
||||
position: sticky;
|
||||
top: 0;
|
||||
z-index: 99999997;
|
||||
}
|
||||
|
||||
.network-box .network-box-header .dropdown-menu li.input-group input {
|
||||
width: 100%;
|
||||
height: 2em;
|
||||
border-top: none;
|
||||
border-left: none;
|
||||
border-right: none;
|
||||
}
|
||||
|
||||
.network-box .network-box-header .dropdown-menu li a {
|
||||
padding: 5px 5px 5px 15px;
|
||||
}
|
||||
|
||||
.network-box .network-box-header .dropdown-menu li.hidden {
|
||||
height: 0;
|
||||
display: none;
|
||||
visibility:hidden;
|
||||
}
|
||||
|
||||
.network-box .network-box-header .dropdown-menu li.visible {
|
||||
display: block;
|
||||
}
|
||||
|
||||
.network-box .chartTitle {
|
||||
cursor: pointer;
|
||||
text-align: center;
|
||||
font-size: 18px;
|
||||
margin: 18px 0px 15px 0px;
|
||||
}
|
||||
|
||||
.network-box .chartTitle i.chartCountryCode {
|
||||
font-size: 16px;
|
||||
border-radius: 12.5%;
|
||||
}
|
||||
|
||||
/* 网络页 正文结束 */
|
||||
|
||||
/* 地图版服务器分布图 */
|
||||
#mapChartBox {
|
||||
z-index: 999999999;
|
||||
padding-left: 0 !important;
|
||||
}
|
||||
|
||||
.modal-open .modal {
|
||||
overflow-x: hidden;
|
||||
overflow-y: hidden;
|
||||
}
|
||||
|
||||
.modal-dialog-centered {
|
||||
margin: 0 auto;
|
||||
width: 100%;
|
||||
max-width: 100%;
|
||||
display: flex;
|
||||
align-items: center; /* 垂直居中 */
|
||||
justify-content: center; /* 水平居中 */
|
||||
min-height: calc(100%) /* 确保模态框占据整个视口高度 */
|
||||
}
|
||||
|
||||
.modal-header{
|
||||
.modal-content {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
pointer-events: auto;
|
||||
border-radius: unset;
|
||||
border: none;
|
||||
}
|
||||
|
||||
.modal-header {
|
||||
position: relative;
|
||||
}
|
||||
|
||||
.modal-header i{
|
||||
.modal-header h4 {
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.modal-header i.xclose {
|
||||
cursor: pointer;
|
||||
position: absolute;
|
||||
font-size: 2em;
|
||||
top: 13px;
|
||||
right: 13px;
|
||||
font-size: 30px;
|
||||
top: 12px;
|
||||
right: 10px;
|
||||
}
|
||||
|
||||
.modal-body {
|
||||
padding: 0px;
|
||||
}
|
||||
|
||||
a.template-item, a.cdn-item {
|
||||
display: block;
|
||||
}
|
||||
|
||||
footer {
|
||||
padding-bottom: 20px;
|
||||
}
|
||||
|
||||
footer p {
|
||||
text-align: center;
|
||||
font-size: 10px;
|
||||
}
|
||||
|
||||
@media only screen and (max-width: 1200px) {
|
||||
.accordian-body{
|
||||
.accordian-body {
|
||||
margin: 5px 0px 5px 10px;
|
||||
}
|
||||
.table .node-group-tag th{
|
||||
table.monitor-detail {
|
||||
margin:0px 0px 2px 0px;
|
||||
}
|
||||
.table .network-thead-tr th,
|
||||
.table .node-group-tag th {
|
||||
font-size:16px;
|
||||
padding-bottom:6px;
|
||||
}
|
||||
}
|
||||
|
||||
/* 彩虹旗 */
|
||||
.fi-rb {
|
||||
background-image: url(/static/theme-server-status/img/rb.png);
|
||||
}
|
||||
|
||||
@media only screen and (max-width: 767px) {
|
||||
body {
|
||||
font-size: 10px !important;
|
||||
padding-top:60px !important;
|
||||
}
|
||||
.nezha {
|
||||
min-height: calc(100vh - 90px);
|
||||
min-height: calc(var(--vh, 1vh) * 100 - 90px);
|
||||
}
|
||||
#chartbox {
|
||||
min-height: calc(100vh - 170px);
|
||||
min-height: calc(var(--vh, 1vh) * 100 - 170px);
|
||||
}
|
||||
.content {
|
||||
padding: 0;
|
||||
margin-bottom: 10px;
|
||||
}
|
||||
footer {
|
||||
padding-bottom: 10px;
|
||||
}
|
||||
.cycle-transfer-stats .dataTables_wrapper {
|
||||
margin-bottom: -10px;
|
||||
}
|
||||
.navbar .navbar-nav .open .dropdown-menu>li>a {
|
||||
color: #f1f1f1;
|
||||
}
|
||||
@@ -359,57 +537,78 @@ body {
|
||||
}
|
||||
.node-cell.os,
|
||||
.node-cell.uptime,
|
||||
.node-cell.traffic{
|
||||
.node-cell.traffic {
|
||||
display: none;
|
||||
visibility: hidden;
|
||||
}
|
||||
.node-cell.location{
|
||||
.node-cell.location {
|
||||
display: table-cell;
|
||||
visibility: visible;
|
||||
}
|
||||
.node-cell-os-text,.node-cell-location-text{
|
||||
.node-cell-os-text,.node-cell-location-text {
|
||||
display: none;
|
||||
visibility: hidden;
|
||||
}
|
||||
.accordian-body{
|
||||
.node-cell.network {
|
||||
min-width: 95px;
|
||||
max-width: 95px;
|
||||
}
|
||||
.node-cell.remaining {
|
||||
min-width: 75px;
|
||||
max-width: 75px;
|
||||
}
|
||||
.accordian-body {
|
||||
margin: 5px 0px 5px 10px;
|
||||
}
|
||||
.table .node-group-tag th{
|
||||
font-size:16px;
|
||||
table.monitor-detail {
|
||||
margin:0px 0px 2px 0px;
|
||||
}
|
||||
.table .network-thead-tr th,
|
||||
.table .node-group-tag th {
|
||||
font-size:14px;
|
||||
padding-bottom:6px;
|
||||
}
|
||||
.service-status .service-status-th{
|
||||
.table tr.network td {
|
||||
font-size: 12px;
|
||||
}
|
||||
.service-status .service-status-th {
|
||||
min-width:30px;
|
||||
}
|
||||
.service-status .delay-today{
|
||||
.service-status .delay-today {
|
||||
margin-top:4px;
|
||||
justify-content: center;
|
||||
}
|
||||
.service-status .delay-today i{
|
||||
.service-status .delay-today i {
|
||||
margin-right:0px;
|
||||
}
|
||||
.service-status .delay-today-text{
|
||||
.service-status .delay-today-text {
|
||||
display: none;
|
||||
visibility: hidden;
|
||||
}
|
||||
.service-status .service-averagelatency-th{
|
||||
.service-status .service-averagelatency-th {
|
||||
min-width:70px;
|
||||
}
|
||||
.service-status .service-30daysonline-th{
|
||||
.service-status .service-30daysonline-th {
|
||||
min-width:75px;
|
||||
}
|
||||
.toolbox {
|
||||
right: 18px;
|
||||
}
|
||||
.modal-dialog-centered{
|
||||
width: 95%;
|
||||
td.ping-network-quality {
|
||||
width: 110px;
|
||||
}
|
||||
.modal-title{
|
||||
line-height: 0.5;
|
||||
.network-box .network-box-header {
|
||||
margin: 8px 0px 0px 8px;
|
||||
font-size: 16px;
|
||||
border-bottom: none;
|
||||
}
|
||||
.modal-header i{
|
||||
top: 8px;
|
||||
right: 8px;
|
||||
.network-box .chartTitle {
|
||||
font-size: 16px;
|
||||
margin: 10px 0px 10px 0px;
|
||||
}
|
||||
.network-box .chartTitle i.chartCountryCode {
|
||||
font-size: 15px;
|
||||
border-radius: 12.5%;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
BIN
Binary file not shown.
|
After Width: | Height: | Size: 262 KiB |
BIN
Binary file not shown.
|
After Width: | Height: | Size: 5.8 KiB |
-326
@@ -1,326 +0,0 @@
|
||||
/**
|
||||
* @preserve HTML5 Shiv 3.7.3 | @afarkas @jdalton @jon_neal @rem | MIT/GPL2 Licensed
|
||||
*/
|
||||
;(function(window, document) {
|
||||
/*jshint evil:true */
|
||||
/** version */
|
||||
var version = '3.7.3';
|
||||
|
||||
/** Preset options */
|
||||
var options = window.html5 || {};
|
||||
|
||||
/** Used to skip problem elements */
|
||||
var reSkip = /^<|^(?:button|map|select|textarea|object|iframe|option|optgroup)$/i;
|
||||
|
||||
/** Not all elements can be cloned in IE **/
|
||||
var saveClones = /^(?:a|b|code|div|fieldset|h1|h2|h3|h4|h5|h6|i|label|li|ol|p|q|span|strong|style|table|tbody|td|th|tr|ul)$/i;
|
||||
|
||||
/** Detect whether the browser supports default html5 styles */
|
||||
var supportsHtml5Styles;
|
||||
|
||||
/** Name of the expando, to work with multiple documents or to re-shiv one document */
|
||||
var expando = '_html5shiv';
|
||||
|
||||
/** The id for the documents expando */
|
||||
var expanID = 0;
|
||||
|
||||
/** Cached data for each document */
|
||||
var expandoData = {};
|
||||
|
||||
/** Detect whether the browser supports unknown elements */
|
||||
var supportsUnknownElements;
|
||||
|
||||
(function() {
|
||||
try {
|
||||
var a = document.createElement('a');
|
||||
a.innerHTML = '<xyz></xyz>';
|
||||
//if the hidden property is implemented we can assume, that the browser supports basic HTML5 Styles
|
||||
supportsHtml5Styles = ('hidden' in a);
|
||||
|
||||
supportsUnknownElements = a.childNodes.length == 1 || (function() {
|
||||
// assign a false positive if unable to shiv
|
||||
(document.createElement)('a');
|
||||
var frag = document.createDocumentFragment();
|
||||
return (
|
||||
typeof frag.cloneNode == 'undefined' ||
|
||||
typeof frag.createDocumentFragment == 'undefined' ||
|
||||
typeof frag.createElement == 'undefined'
|
||||
);
|
||||
}());
|
||||
} catch(e) {
|
||||
// assign a false positive if detection fails => unable to shiv
|
||||
supportsHtml5Styles = true;
|
||||
supportsUnknownElements = true;
|
||||
}
|
||||
|
||||
}());
|
||||
|
||||
/*--------------------------------------------------------------------------*/
|
||||
|
||||
/**
|
||||
* Creates a style sheet with the given CSS text and adds it to the document.
|
||||
* @private
|
||||
* @param {Document} ownerDocument The document.
|
||||
* @param {String} cssText The CSS text.
|
||||
* @returns {StyleSheet} The style element.
|
||||
*/
|
||||
function addStyleSheet(ownerDocument, cssText) {
|
||||
var p = ownerDocument.createElement('p'),
|
||||
parent = ownerDocument.getElementsByTagName('head')[0] || ownerDocument.documentElement;
|
||||
|
||||
p.innerHTML = 'x<style>' + cssText + '</style>';
|
||||
return parent.insertBefore(p.lastChild, parent.firstChild);
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the value of `html5.elements` as an array.
|
||||
* @private
|
||||
* @returns {Array} An array of shived element node names.
|
||||
*/
|
||||
function getElements() {
|
||||
var elements = html5.elements;
|
||||
return typeof elements == 'string' ? elements.split(' ') : elements;
|
||||
}
|
||||
|
||||
/**
|
||||
* Extends the built-in list of html5 elements
|
||||
* @memberOf html5
|
||||
* @param {String|Array} newElements whitespace separated list or array of new element names to shiv
|
||||
* @param {Document} ownerDocument The context document.
|
||||
*/
|
||||
function addElements(newElements, ownerDocument) {
|
||||
var elements = html5.elements;
|
||||
if(typeof elements != 'string'){
|
||||
elements = elements.join(' ');
|
||||
}
|
||||
if(typeof newElements != 'string'){
|
||||
newElements = newElements.join(' ');
|
||||
}
|
||||
html5.elements = elements +' '+ newElements;
|
||||
shivDocument(ownerDocument);
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the data associated to the given document
|
||||
* @private
|
||||
* @param {Document} ownerDocument The document.
|
||||
* @returns {Object} An object of data.
|
||||
*/
|
||||
function getExpandoData(ownerDocument) {
|
||||
var data = expandoData[ownerDocument[expando]];
|
||||
if (!data) {
|
||||
data = {};
|
||||
expanID++;
|
||||
ownerDocument[expando] = expanID;
|
||||
expandoData[expanID] = data;
|
||||
}
|
||||
return data;
|
||||
}
|
||||
|
||||
/**
|
||||
* returns a shived element for the given nodeName and document
|
||||
* @memberOf html5
|
||||
* @param {String} nodeName name of the element
|
||||
* @param {Document|DocumentFragment} ownerDocument The context document.
|
||||
* @returns {Object} The shived element.
|
||||
*/
|
||||
function createElement(nodeName, ownerDocument, data){
|
||||
if (!ownerDocument) {
|
||||
ownerDocument = document;
|
||||
}
|
||||
if(supportsUnknownElements){
|
||||
return ownerDocument.createElement(nodeName);
|
||||
}
|
||||
if (!data) {
|
||||
data = getExpandoData(ownerDocument);
|
||||
}
|
||||
var node;
|
||||
|
||||
if (data.cache[nodeName]) {
|
||||
node = data.cache[nodeName].cloneNode();
|
||||
} else if (saveClones.test(nodeName)) {
|
||||
node = (data.cache[nodeName] = data.createElem(nodeName)).cloneNode();
|
||||
} else {
|
||||
node = data.createElem(nodeName);
|
||||
}
|
||||
|
||||
// Avoid adding some elements to fragments in IE < 9 because
|
||||
// * Attributes like `name` or `type` cannot be set/changed once an element
|
||||
// is inserted into a document/fragment
|
||||
// * Link elements with `src` attributes that are inaccessible, as with
|
||||
// a 403 response, will cause the tab/window to crash
|
||||
// * Script elements appended to fragments will execute when their `src`
|
||||
// or `text` property is set
|
||||
return node.canHaveChildren && !reSkip.test(nodeName) && !node.tagUrn ? data.frag.appendChild(node) : node;
|
||||
}
|
||||
|
||||
/**
|
||||
* returns a shived DocumentFragment for the given document
|
||||
* @memberOf html5
|
||||
* @param {Document} ownerDocument The context document.
|
||||
* @returns {Object} The shived DocumentFragment.
|
||||
*/
|
||||
function createDocumentFragment(ownerDocument, data){
|
||||
if (!ownerDocument) {
|
||||
ownerDocument = document;
|
||||
}
|
||||
if(supportsUnknownElements){
|
||||
return ownerDocument.createDocumentFragment();
|
||||
}
|
||||
data = data || getExpandoData(ownerDocument);
|
||||
var clone = data.frag.cloneNode(),
|
||||
i = 0,
|
||||
elems = getElements(),
|
||||
l = elems.length;
|
||||
for(;i<l;i++){
|
||||
clone.createElement(elems[i]);
|
||||
}
|
||||
return clone;
|
||||
}
|
||||
|
||||
/**
|
||||
* Shivs the `createElement` and `createDocumentFragment` methods of the document.
|
||||
* @private
|
||||
* @param {Document|DocumentFragment} ownerDocument The document.
|
||||
* @param {Object} data of the document.
|
||||
*/
|
||||
function shivMethods(ownerDocument, data) {
|
||||
if (!data.cache) {
|
||||
data.cache = {};
|
||||
data.createElem = ownerDocument.createElement;
|
||||
data.createFrag = ownerDocument.createDocumentFragment;
|
||||
data.frag = data.createFrag();
|
||||
}
|
||||
|
||||
|
||||
ownerDocument.createElement = function(nodeName) {
|
||||
//abort shiv
|
||||
if (!html5.shivMethods) {
|
||||
return data.createElem(nodeName);
|
||||
}
|
||||
return createElement(nodeName, ownerDocument, data);
|
||||
};
|
||||
|
||||
ownerDocument.createDocumentFragment = Function('h,f', 'return function(){' +
|
||||
'var n=f.cloneNode(),c=n.createElement;' +
|
||||
'h.shivMethods&&(' +
|
||||
// unroll the `createElement` calls
|
||||
getElements().join().replace(/[\w\-:]+/g, function(nodeName) {
|
||||
data.createElem(nodeName);
|
||||
data.frag.createElement(nodeName);
|
||||
return 'c("' + nodeName + '")';
|
||||
}) +
|
||||
');return n}'
|
||||
)(html5, data.frag);
|
||||
}
|
||||
|
||||
/*--------------------------------------------------------------------------*/
|
||||
|
||||
/**
|
||||
* Shivs the given document.
|
||||
* @memberOf html5
|
||||
* @param {Document} ownerDocument The document to shiv.
|
||||
* @returns {Document} The shived document.
|
||||
*/
|
||||
function shivDocument(ownerDocument) {
|
||||
if (!ownerDocument) {
|
||||
ownerDocument = document;
|
||||
}
|
||||
var data = getExpandoData(ownerDocument);
|
||||
|
||||
if (html5.shivCSS && !supportsHtml5Styles && !data.hasCSS) {
|
||||
data.hasCSS = !!addStyleSheet(ownerDocument,
|
||||
// corrects block display not defined in IE6/7/8/9
|
||||
'article,aside,dialog,figcaption,figure,footer,header,hgroup,main,nav,section{display:block}' +
|
||||
// adds styling not present in IE6/7/8/9
|
||||
'mark{background:#FF0;color:#000}' +
|
||||
// hides non-rendered elements
|
||||
'template{display:none}'
|
||||
);
|
||||
}
|
||||
if (!supportsUnknownElements) {
|
||||
shivMethods(ownerDocument, data);
|
||||
}
|
||||
return ownerDocument;
|
||||
}
|
||||
|
||||
/*--------------------------------------------------------------------------*/
|
||||
|
||||
/**
|
||||
* The `html5` object is exposed so that more elements can be shived and
|
||||
* existing shiving can be detected on iframes.
|
||||
* @type Object
|
||||
* @example
|
||||
*
|
||||
* // options can be changed before the script is included
|
||||
* html5 = { 'elements': 'mark section', 'shivCSS': false, 'shivMethods': false };
|
||||
*/
|
||||
var html5 = {
|
||||
|
||||
/**
|
||||
* An array or space separated string of node names of the elements to shiv.
|
||||
* @memberOf html5
|
||||
* @type Array|String
|
||||
*/
|
||||
'elements': options.elements || 'abbr article aside audio bdi canvas data datalist details dialog figcaption figure footer header hgroup main mark meter nav output picture progress section summary template time video',
|
||||
|
||||
/**
|
||||
* current version of html5shiv
|
||||
*/
|
||||
'version': version,
|
||||
|
||||
/**
|
||||
* A flag to indicate that the HTML5 style sheet should be inserted.
|
||||
* @memberOf html5
|
||||
* @type Boolean
|
||||
*/
|
||||
'shivCSS': (options.shivCSS !== false),
|
||||
|
||||
/**
|
||||
* Is equal to true if a browser supports creating unknown/HTML5 elements
|
||||
* @memberOf html5
|
||||
* @type boolean
|
||||
*/
|
||||
'supportsUnknownElements': supportsUnknownElements,
|
||||
|
||||
/**
|
||||
* A flag to indicate that the document's `createElement` and `createDocumentFragment`
|
||||
* methods should be overwritten.
|
||||
* @memberOf html5
|
||||
* @type Boolean
|
||||
*/
|
||||
'shivMethods': (options.shivMethods !== false),
|
||||
|
||||
/**
|
||||
* A string to describe the type of `html5` object ("default" or "default print").
|
||||
* @memberOf html5
|
||||
* @type String
|
||||
*/
|
||||
'type': 'default',
|
||||
|
||||
// shivs the document according to the specified `html5` object options
|
||||
'shivDocument': shivDocument,
|
||||
|
||||
//creates a shived element
|
||||
createElement: createElement,
|
||||
|
||||
//creates a shived documentFragment
|
||||
createDocumentFragment: createDocumentFragment,
|
||||
|
||||
//extends list of elements
|
||||
addElements: addElements
|
||||
};
|
||||
|
||||
/*--------------------------------------------------------------------------*/
|
||||
|
||||
// expose html5
|
||||
window.html5 = html5;
|
||||
|
||||
// shiv the document
|
||||
shivDocument(document);
|
||||
|
||||
if(typeof module == 'object' && module.exports){
|
||||
module.exports = html5;
|
||||
}
|
||||
|
||||
}(typeof window !== "undefined" ? window : this, document));
|
||||
+47
-3
@@ -1,30 +1,49 @@
|
||||
const mixinsVue = {
|
||||
data: {
|
||||
cache: [],
|
||||
isMobile: false,
|
||||
theme: "light",
|
||||
isSystemTheme: false,
|
||||
showGroup: false,
|
||||
showGoTop: false,
|
||||
showTools: false,
|
||||
preferredTemplate: null,
|
||||
isMobile: false,
|
||||
semiTransparent: false,
|
||||
staticUrl: '/static/theme-server-status',
|
||||
adaptedTemplates: [
|
||||
{ key: 'default', name: 'Default', icon: 'th large' },
|
||||
{ key: 'angel-kanade', name: 'AngelKanade', icon: 'square' },
|
||||
{ key: 'server-status', name: 'ServerStatus', icon: 'list' }
|
||||
]
|
||||
],
|
||||
colors: [],
|
||||
colorsDark: ['#4992FF', '#08C091', '#FDDD5F', '#FF6E76', '#58D9F9', '#7CFFB2', '#FF8A44', '#8D48E3', '#DD79FF', '#5470C6', '#3BA272', '#FAC758', '#EE6666', '#72C0DE', '#91CC76', '#FB8352', '#9A60B4', '#EA7BCC'],
|
||||
colorsLight: ['#5470C6', '#3BA272', '#FAC758', '#EE6666', '#72C0DE', '#91CC76', '#FB8352', '#9A60B4', '#EA7BCC', '#4992FF', '#08C091', '#FDDD5F', '#FF6E76', '#58D9F9', '#7CFFB2', '#FF8A44', '#8D48E3', '#DD79FF'],
|
||||
},
|
||||
created() {
|
||||
this.isMobile = this.checkIsMobile();
|
||||
this.theme = this.initTheme();
|
||||
this.showGroup = this.initShowGroup();
|
||||
this.semiTransparent = this.initSemiTransparent();
|
||||
this.preferredTemplate = this.getCookie('preferred_theme') ? this.getCookie('preferred_theme') : this.$root.defaultTemplate;
|
||||
this.colors = this.theme == "dark" ? this.colorsDark : this.colorsLight;
|
||||
this.setBenchmarkHeight();
|
||||
window.addEventListener('scroll', this.handleScroll);
|
||||
window.addEventListener('resize', this.setBenchmarkHeight());
|
||||
},
|
||||
destroyed() {
|
||||
window.removeEventListener('scroll', this.handleScroll);
|
||||
},
|
||||
methods: {
|
||||
toggleTemplate(template) {
|
||||
if( template != this.preferredTemplate){
|
||||
this.preferredTemplate = template;
|
||||
this.updateCookie("preferred_theme", template);
|
||||
window.location.reload();
|
||||
}
|
||||
},
|
||||
toggleShowTools() {
|
||||
this.showTools = !this.showTools;
|
||||
},
|
||||
initTheme() {
|
||||
const storedTheme = localStorage.getItem("theme");
|
||||
const theme = (storedTheme === 'dark' || storedTheme === 'light') ? storedTheme : (window.matchMedia('(prefers-color-scheme: dark)').matches ? 'dark' : 'light');
|
||||
@@ -38,7 +57,7 @@ const mixinsVue = {
|
||||
// 重新赋值全局调色
|
||||
this.colors = this.theme == "dark" ? this.colorsDark : this.colorsLight;
|
||||
|
||||
if(this.$root.page == 'index') {
|
||||
if(this.$root.page == 'index' || this.$root.page == 'network') {
|
||||
this.reloadCharts(); // 重新载入echarts图表
|
||||
}
|
||||
},
|
||||
@@ -57,6 +76,21 @@ const mixinsVue = {
|
||||
this.$root.initTooltip();
|
||||
}
|
||||
},
|
||||
initSemiTransparent() {
|
||||
const storedSemiTransparent = localStorage.getItem("semiTransparent");
|
||||
const semiTransparent = storedSemiTransparent !== null ? JSON.parse(storedSemiTransparent) : false;
|
||||
if (storedSemiTransparent === null) {
|
||||
localStorage.setItem("semiTransparent", semiTransparent);
|
||||
}
|
||||
return semiTransparent;
|
||||
},
|
||||
toggleSemiTransparent(){
|
||||
this.semiTransparent = !this.semiTransparent;
|
||||
localStorage.setItem("semiTransparent", this.semiTransparent);
|
||||
if(this.$root.page == 'index' || this.$root.page == 'network') {
|
||||
this.reloadCharts(); // 重新载入echarts图表
|
||||
}
|
||||
},
|
||||
updateCookie(name, value) {
|
||||
document.cookie = name + "=" + value +"; path=/";
|
||||
},
|
||||
@@ -99,6 +133,7 @@ const mixinsVue = {
|
||||
},
|
||||
handleScroll() {
|
||||
this.showGoTop = window.scrollY >= 100;
|
||||
if(this.showTools) this.showTools = false;
|
||||
},
|
||||
groupingData(data, field) {
|
||||
let map = new Map();
|
||||
@@ -119,6 +154,15 @@ const mixinsVue = {
|
||||
},
|
||||
checkIsMobile() { // 检测设备类型,页面宽度小于768px认为是移动设备
|
||||
return window.innerWidth <= 768;
|
||||
},
|
||||
isMenuActive(page){
|
||||
if(page == this.$root.page) {
|
||||
return this.isMobile ? 'm-active' : 'pc-active';
|
||||
}
|
||||
},
|
||||
setBenchmarkHeight() {
|
||||
let vh = window.innerHeight * 0.01;
|
||||
document.documentElement.style.setProperty('--vh', `${vh}px`);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,5 +0,0 @@
|
||||
/*! Respond.js v1.4.2: min/max-width media query polyfill * Copyright 2013 Scott Jehl
|
||||
* Licensed under https://github.com/scottjehl/Respond/blob/master/LICENSE-MIT
|
||||
* */
|
||||
|
||||
!function(a){"use strict";a.matchMedia=a.matchMedia||function(a){var b,c=a.documentElement,d=c.firstElementChild||c.firstChild,e=a.createElement("body"),f=a.createElement("div");return f.id="mq-test-1",f.style.cssText="position:absolute;top:-100em",e.style.background="none",e.appendChild(f),function(a){return f.innerHTML='­<style media="'+a+'"> #mq-test-1 { width: 42px; }</style>',c.insertBefore(e,d),b=42===f.offsetWidth,c.removeChild(e),{matches:b,media:a}}}(a.document)}(this),function(a){"use strict";function b(){u(!0)}var c={};a.respond=c,c.update=function(){};var d=[],e=function(){var b=!1;try{b=new a.XMLHttpRequest}catch(c){b=new a.ActiveXObject("Microsoft.XMLHTTP")}return function(){return b}}(),f=function(a,b){var c=e();c&&(c.open("GET",a,!0),c.onreadystatechange=function(){4!==c.readyState||200!==c.status&&304!==c.status||b(c.responseText)},4!==c.readyState&&c.send(null))};if(c.ajax=f,c.queue=d,c.regex={media:/@media[^\{]+\{([^\{\}]*\{[^\}\{]*\})+/gi,keyframes:/@(?:\-(?:o|moz|webkit)\-)?keyframes[^\{]+\{(?:[^\{\}]*\{[^\}\{]*\})+[^\}]*\}/gi,urls:/(url\()['"]?([^\/\)'"][^:\)'"]+)['"]?(\))/g,findStyles:/@media *([^\{]+)\{([\S\s]+?)$/,only:/(only\s+)?([a-zA-Z]+)\s?/,minw:/\([\s]*min\-width\s*:[\s]*([\s]*[0-9\.]+)(px|em)[\s]*\)/,maxw:/\([\s]*max\-width\s*:[\s]*([\s]*[0-9\.]+)(px|em)[\s]*\)/},c.mediaQueriesSupported=a.matchMedia&&null!==a.matchMedia("only all")&&a.matchMedia("only all").matches,!c.mediaQueriesSupported){var g,h,i,j=a.document,k=j.documentElement,l=[],m=[],n=[],o={},p=30,q=j.getElementsByTagName("head")[0]||k,r=j.getElementsByTagName("base")[0],s=q.getElementsByTagName("link"),t=function(){var a,b=j.createElement("div"),c=j.body,d=k.style.fontSize,e=c&&c.style.fontSize,f=!1;return b.style.cssText="position:absolute;font-size:1em;width:1em",c||(c=f=j.createElement("body"),c.style.background="none"),k.style.fontSize="100%",c.style.fontSize="100%",c.appendChild(b),f&&k.insertBefore(c,k.firstChild),a=b.offsetWidth,f?k.removeChild(c):c.removeChild(b),k.style.fontSize=d,e&&(c.style.fontSize=e),a=i=parseFloat(a)},u=function(b){var c="clientWidth",d=k[c],e="CSS1Compat"===j.compatMode&&d||j.body[c]||d,f={},o=s[s.length-1],r=(new Date).getTime();if(b&&g&&p>r-g)return a.clearTimeout(h),h=a.setTimeout(u,p),void 0;g=r;for(var v in l)if(l.hasOwnProperty(v)){var w=l[v],x=w.minw,y=w.maxw,z=null===x,A=null===y,B="em";x&&(x=parseFloat(x)*(x.indexOf(B)>-1?i||t():1)),y&&(y=parseFloat(y)*(y.indexOf(B)>-1?i||t():1)),w.hasquery&&(z&&A||!(z||e>=x)||!(A||y>=e))||(f[w.media]||(f[w.media]=[]),f[w.media].push(m[w.rules]))}for(var C in n)n.hasOwnProperty(C)&&n[C]&&n[C].parentNode===q&&q.removeChild(n[C]);n.length=0;for(var D in f)if(f.hasOwnProperty(D)){var E=j.createElement("style"),F=f[D].join("\n");E.type="text/css",E.media=D,q.insertBefore(E,o.nextSibling),E.styleSheet?E.styleSheet.cssText=F:E.appendChild(j.createTextNode(F)),n.push(E)}},v=function(a,b,d){var e=a.replace(c.regex.keyframes,"").match(c.regex.media),f=e&&e.length||0;b=b.substring(0,b.lastIndexOf("/"));var g=function(a){return a.replace(c.regex.urls,"$1"+b+"$2$3")},h=!f&&d;b.length&&(b+="/"),h&&(f=1);for(var i=0;f>i;i++){var j,k,n,o;h?(j=d,m.push(g(a))):(j=e[i].match(c.regex.findStyles)&&RegExp.$1,m.push(RegExp.$2&&g(RegExp.$2))),n=j.split(","),o=n.length;for(var p=0;o>p;p++)k=n[p],l.push({media:k.split("(")[0].match(c.regex.only)&&RegExp.$2||"all",rules:m.length-1,hasquery:k.indexOf("(")>-1,minw:k.match(c.regex.minw)&&parseFloat(RegExp.$1)+(RegExp.$2||""),maxw:k.match(c.regex.maxw)&&parseFloat(RegExp.$1)+(RegExp.$2||"")})}u()},w=function(){if(d.length){var b=d.shift();f(b.href,function(c){v(c,b.href,b.media),o[b.href]=!0,a.setTimeout(function(){w()},0)})}},x=function(){for(var b=0;b<s.length;b++){var c=s[b],e=c.href,f=c.media,g=c.rel&&"stylesheet"===c.rel.toLowerCase();e&&g&&!o[e]&&(c.styleSheet&&c.styleSheet.rawCssText?(v(c.styleSheet.rawCssText,e,f),o[e]=!0):(!/^([a-zA-Z:]*\/\/)/.test(e)&&!r||e.replace(RegExp.$1,"").split("/")[0]===a.location.host)&&("//"===e.substring(0,2)&&(e=a.location.protocol+e),d.push({href:e,media:f})))}w()};x(),c.update=x,c.getEmValue=t,a.addEventListener?a.addEventListener("resize",b,!1):a.attachEvent&&a.attachEvent("onresize",b)}}(this);
|
||||
Vendored
+4
-4
@@ -6,11 +6,11 @@
|
||||
{{.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?v20241011"></script>
|
||||
<script>
|
||||
(function () {
|
||||
updateLang({{.LANG }});
|
||||
|
||||
Vendored
+10
-9
@@ -1,21 +1,22 @@
|
||||
{{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="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" />
|
||||
<link rel="stylesheet" type="text/css" href="https://unpkg.com/[email protected]/dist/semantic.min.css">
|
||||
<link rel="stylesheet" type="text/css" href="https://unpkg.com/[email protected]/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?v20240813">
|
||||
<link rel="stylesheet" type="text/css" href="/static/darkmode.css?v20240813">
|
||||
<script>
|
||||
document.documentElement.setAttribute('nz-theme', window.matchMedia('(prefers-color-scheme: dark)').matches ? 'dark' : 'light')
|
||||
</script>
|
||||
</head>
|
||||
|
||||
{{if ts .CustomCodeDashboard}} {{.CustomCodeDashboard|safe}} {{end}}
|
||||
<body>
|
||||
{{end}}
|
||||
{{end}}
|
||||
Vendored
+2
@@ -9,6 +9,8 @@
|
||||
<a class='item{{if eq .MatchedPath "/monitor"}} active{{end}}' href="/monitor"><i class="rss icon"></i>{{tr "Services"}}</a>
|
||||
<a class='item{{if eq .MatchedPath "/cron"}} active{{end}}' href="/cron"><i class="clock icon"></i>{{tr "Task"}}</a>
|
||||
<a class='item{{if eq .MatchedPath "/notification"}} active{{end}}' href="/notification"><i class="bell icon"></i>{{tr "Notification"}}</a>
|
||||
<a class='item{{if eq .MatchedPath "/ddns"}} active{{end}}' href="/ddns"><i class="globe icon"></i>{{tr "DDNS"}}</a>
|
||||
<a class='item{{if eq .MatchedPath "/nat"}} active{{end}}' href="/nat"><i class="exchange icon"></i>{{tr "NAT"}}</a>
|
||||
<a class='item{{if eq .MatchedPath "/setting"}} active{{end}}' href="/setting">
|
||||
<i class="settings icon"></i>{{tr "Settings"}}
|
||||
</a>
|
||||
|
||||
Vendored
+1
-1
@@ -10,7 +10,7 @@
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
<div class=" actions">
|
||||
<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>
|
||||
|
||||
+1
-1
@@ -60,7 +60,7 @@
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
<div class=" actions">
|
||||
<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>
|
||||
|
||||
+86
@@ -0,0 +1,86 @@
|
||||
{{define "component/ddns"}}
|
||||
<div class="ui tiny ddns modal transition hidden">
|
||||
<div class="header">Add</div>
|
||||
<div class="content">
|
||||
<form id="ddnsForm" class="ui form">
|
||||
<input type="hidden" name="ID">
|
||||
<div class="field">
|
||||
<label>{{tr "Name"}}</label>
|
||||
<input type="text" name="Name">
|
||||
</div>
|
||||
<div class="field">
|
||||
<label>{{tr "DDNSProvider"}}</label>
|
||||
<select name="Provider" class="ui fluid dropdown" id="providerSelect" onchange="toggleFields()">
|
||||
{{ range $provider := .ProviderList }}
|
||||
<option value="{{ $provider.ID }}">
|
||||
{{ $provider.Name }}
|
||||
</option>
|
||||
{{ end }}
|
||||
</select>
|
||||
</div>
|
||||
<div class="field">
|
||||
<label>{{tr "DDNSDomains"}}</label>
|
||||
<input type="text" name="DomainsRaw" placeholder="www.example.com">
|
||||
</div>
|
||||
<div class="field">
|
||||
<label>{{tr "DDNSAccessID"}}</label>
|
||||
<input type="text" name="AccessID" placeholder="{{tr "DDNSTokenID"}}">
|
||||
</div>
|
||||
<div class="field">
|
||||
<label>{{tr "DDNSAccessSecret"}}</label>
|
||||
<input type="text" name="AccessSecret" placeholder="{{tr "DDNSTokenSecret"}}">
|
||||
</div>
|
||||
<div class="field">
|
||||
<label>{{tr "MaxRetries"}}</label>
|
||||
<input type="number" name="MaxRetries" placeholder="3">
|
||||
</div>
|
||||
<div class="field">
|
||||
<label>{{tr "WebhookURL"}}</label>
|
||||
<input type="text" name="WebhookURL" placeholder="https://ddns.example.com/?record=#record#">
|
||||
</div>
|
||||
<div class="field">
|
||||
<label>{{tr "WebhookMethod"}}</label>
|
||||
<select name="WebhookMethod" class="ui fluid dropdown">
|
||||
<option value="1">GET</option>
|
||||
<option value="2">POST</option>
|
||||
<option value="3">PATCH</option>
|
||||
<option value="4">DELETE</option>
|
||||
<option value="5">PUT</option>
|
||||
</select>
|
||||
</div>
|
||||
<div class="field">
|
||||
<label>{{tr "WebhookRequestType"}}</label>
|
||||
<select name="WebhookRequestType" class="ui fluid dropdown">
|
||||
<option value="1">JSON</option>
|
||||
<option value="2">Form</option>
|
||||
</select>
|
||||
</div>
|
||||
<div class="field">
|
||||
<label>{{tr "WebhookHeaders"}}</label>
|
||||
<textarea name="WebhookHeaders" placeholder='{"User-Agent":"Nezha-Agent"}'></textarea>
|
||||
</div>
|
||||
<div class="field">
|
||||
<label>{{tr "WebhookRequestBody"}}</label>
|
||||
<textarea name="WebhookRequestBody" placeholder='{ "ip": #ip#, "domain": "#domain#" }'></textarea>
|
||||
</div>
|
||||
<div class="field">
|
||||
<div class="ui enableipv4 checkbox">
|
||||
<input name="EnableIPv4" type="checkbox" tabindex="0" class="hidden">
|
||||
<label>{{tr "EnableIPv4"}}</label>
|
||||
</div>
|
||||
</div>
|
||||
<div class="field">
|
||||
<div class="ui enableipv6 checkbox">
|
||||
<input name="EnableIPv6" type="checkbox" tabindex="0" class="hidden">
|
||||
<label>{{tr "EnableIPv6"}}</label>
|
||||
</div>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
<div class="actions">
|
||||
<div class="ui negative button">{{tr "Cancel"}}</div>
|
||||
<button class="ui positive nezha-primary-btn right labeled icon button">{{tr "Confirm"}}<i class="checkmark icon"></i>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
{{end}}
|
||||
Vendored
+31
@@ -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>{{tr "LocalService"}}</label>
|
||||
<input type="text" name="Host" placeholder="{{tr "LocalServicePlaceholder"}}">
|
||||
</div>
|
||||
<div class="field">
|
||||
<label>{{tr "BindHostname"}}</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}}
|
||||
+20
-27
@@ -20,42 +20,35 @@
|
||||
<label>{{tr "Secret"}}</label>
|
||||
<input type="text" name="secret">
|
||||
</div>
|
||||
<div class="field">
|
||||
<label>{{tr "DDNSProfiles"}}</label>
|
||||
<div class="ui fluid multiple ddns search selection dropdown">
|
||||
<input type="hidden" name="DDNSProfilesRaw">
|
||||
<i class="dropdown icon ddnsProfiles"></i>
|
||||
<div class="default text">{{tr "EnterIdAndNameToSearch"}}</div>
|
||||
<div class="menu"></div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="field">
|
||||
<div class="ui enableddns checkbox">
|
||||
<input name="EnableDDNS" type="checkbox" tabindex="0" class="hidden" />
|
||||
<label>{{tr "EnableDDNS"}}</label>
|
||||
</div>
|
||||
</div>
|
||||
<div class="field">
|
||||
<div class="ui hideforguest checkbox">
|
||||
<input name="HideForGuest" type="checkbox" tabindex="0" class="hidden" />
|
||||
<label>{{tr "HideForGuest"}}</label>
|
||||
</div>
|
||||
</div>
|
||||
<div class="field">
|
||||
<div class="ui enableddns checkbox">
|
||||
<input name="EnableDDNS" type="checkbox" tabindex="0" />
|
||||
<label>{{tr "EnableDDNS"}}</label>
|
||||
</div>
|
||||
</div>
|
||||
<div class="field">
|
||||
<div class="ui enableipv4 checkbox">
|
||||
<input name="EnableIPv4" type="checkbox" tabindex="0" />
|
||||
<label>{{tr "EnableIPv4"}}</label>
|
||||
</div>
|
||||
</div>
|
||||
<div class="field">
|
||||
<div class="ui enableipv6 checkbox">
|
||||
<input name="EnableIpv6" type="checkbox" tabindex="0" />
|
||||
<label>{{tr "EnableIpv6"}}</label>
|
||||
</div>
|
||||
</div>
|
||||
<div class="field">
|
||||
<label>{{tr "DDNSDomain"}}</label>
|
||||
<input type="text" name="DDNSDomain" placeholder="{{tr "DDNSDomain"}}">
|
||||
</div>
|
||||
<div class="field">
|
||||
<label>{{tr "DDNSProfile"}}</label>
|
||||
<input type="text" name="DDNSProfile" placeholder="{{tr "DDNSProfile"}}">
|
||||
</div>
|
||||
<div class="field">
|
||||
<label>{{tr "Note"}}</label>
|
||||
<textarea name="Note"></textarea>
|
||||
</div>
|
||||
<div class="field">
|
||||
<label>{{tr "PublicNote"}}</label>
|
||||
<textarea name="PublicNote"></textarea>
|
||||
</div>
|
||||
<div class="command field">
|
||||
<label>{{tr "LinuxOneKeyInstall"}}</label>
|
||||
<div class="ui message">
|
||||
@@ -71,7 +64,7 @@
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
<div class=" actions">
|
||||
<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>
|
||||
|
||||
+1
-1
@@ -10,7 +10,7 @@
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
<table class="ui very basic table">
|
||||
<table class="ui basic table">
|
||||
<thead>
|
||||
<tr>
|
||||
<th>{{tr "Token"}}</th>
|
||||
|
||||
+1
-1
@@ -10,7 +10,7 @@
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
<table class="ui very basic table">
|
||||
<table class="ui basic table">
|
||||
<thead>
|
||||
<tr>
|
||||
<th>ID</th>
|
||||
|
||||
+58
@@ -0,0 +1,58 @@
|
||||
{{define "dashboard-default/ddns"}}
|
||||
{{template "common/header" .}}
|
||||
{{template "common/menu" .}}
|
||||
<div class="nb-container">
|
||||
<div class="ui container">
|
||||
<div class="ui grid">
|
||||
<div class="right floated right aligned twelve wide column">
|
||||
<button class="ui right labeled nezha-primary-btn icon button" onclick="addOrEditDDNS()"><i
|
||||
class="add icon"></i> {{tr "AddDDNSProfile"}}
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
<table class="ui basic table">
|
||||
<thead>
|
||||
<tr>
|
||||
<th>ID</th>
|
||||
<th>{{tr "Name"}}</th>
|
||||
<th>{{tr "EnableIPv4"}}</th>
|
||||
<th>{{tr "EnableIPv6"}}</th>
|
||||
<th>{{tr "DDNSProvider"}}</th>
|
||||
<th>{{tr "DDNSDomain"}}</th>
|
||||
<th>{{tr "MaxRetries"}}</th>
|
||||
<th>{{tr "Administration"}}</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{{range $item := .DDNS}}
|
||||
<tr>
|
||||
<td>{{$item.ID}}</td>
|
||||
<td>{{$item.Name}}</td>
|
||||
<td>{{$item.EnableIPv4}}</td>
|
||||
<td>{{$item.EnableIPv6}}</td>
|
||||
<td>{{index $.ProviderMap $item.Provider}}</td>
|
||||
<td>{{$item.DomainsRaw}}</td>
|
||||
<td>{{$item.MaxRetries}}</td>
|
||||
<td>
|
||||
<div class="ui mini icon buttons">
|
||||
<button class="ui button" onclick="addOrEditDDNS({{$item}})">
|
||||
<i class="edit icon"></i>
|
||||
</button>
|
||||
<button class="ui button"
|
||||
onclick="showConfirm('确定删除DDNS配置?','确认删除',deleteRequest,'/api/ddns/'+{{$item.ID}})">
|
||||
<i class="trash alternate outline icon"></i>
|
||||
</button>
|
||||
</div>
|
||||
</td>
|
||||
</tr>
|
||||
{{end}}
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
</div>
|
||||
{{template "component/ddns" .}}
|
||||
{{template "common/footer" .}}
|
||||
<script>
|
||||
$('.checkbox').checkbox()
|
||||
</script>
|
||||
{{end}}
|
||||
+495
@@ -0,0 +1,495 @@
|
||||
{{define "dashboard-default/file"}}
|
||||
<!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>File List</title>
|
||||
<link rel="shortcut icon" type="image/png" href="/static/logo.svg?v20210804" />
|
||||
<link href="https://fonts.googleapis.com/css2?family=Roboto:wght@300;400;500;700&display=swap" rel="stylesheet" />
|
||||
<link rel="stylesheet" href="https://unpkg.com/mdui@2/mdui.css" />
|
||||
<link href="https://fonts.googleapis.com/icon?family=Material+Icons" rel="stylesheet" />
|
||||
<script src="https://unpkg.com/mdui@2/mdui.global.js"></script>
|
||||
</head>
|
||||
<style>
|
||||
body {
|
||||
font-family: 'Roboto', sans-serif;
|
||||
}
|
||||
|
||||
.file-list {
|
||||
list-style-type: none;
|
||||
padding: 0;
|
||||
}
|
||||
|
||||
.file-item {
|
||||
margin: 5px 0;
|
||||
border: 1px solid #ccc;
|
||||
border-radius: 5px;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
display: block;
|
||||
width: 100%;
|
||||
box-sizing: border-box;
|
||||
}
|
||||
|
||||
#top-app-bar {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
padding: 10px;
|
||||
}
|
||||
|
||||
#current-directory {
|
||||
font-size: 1rem;
|
||||
font-weight: normal;
|
||||
margin: 0 15px;
|
||||
white-space: nowrap;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
max-width: calc(100% - 100px);
|
||||
box-sizing: border-box;
|
||||
}
|
||||
</style>
|
||||
|
||||
<body>
|
||||
<div id="top-app-bar">
|
||||
<mdui-dropdown>
|
||||
<mdui-button-icon slot="trigger" icon="menu"></mdui-button-icon>
|
||||
<mdui-menu>
|
||||
<mdui-menu-item id="refresh">{{tr "Refresh"}}</mdui-menu-item>
|
||||
<mdui-menu-item id="copy">{{tr "CopyPath"}}</mdui-menu-item>
|
||||
<mdui-menu-item id="goto">{{tr "Goto"}}</mdui-menu-item>
|
||||
</mdui-menu>
|
||||
</mdui-dropdown>
|
||||
<span id="current-directory"></span>
|
||||
<mdui-button-icon id="upload" icon="upload"></mdui-button-icon>
|
||||
</div>
|
||||
|
||||
<mdui-list id="file-list" class="file-list"></mdui-list>
|
||||
|
||||
<mdui-dialog id="error-dialog" headline="Error"
|
||||
description="{{tr "FMError"}}"></mdui-dialog>
|
||||
|
||||
<mdui-dialog id="upd-modal" class="modal">
|
||||
<mdui-linear-progress id="upd-progress"></mdui-linear-progress>
|
||||
</mdui-dialog>
|
||||
|
||||
<mdui-dialog id="goto-dialog" headline="{{tr "GotoHeadline"}}" close-on-overlay-click>
|
||||
<mdui-text-field id="goto-text" variant="outlined" value=""></mdui-text-field>
|
||||
<mdui-button id="goto-go" slot="action" variant="text">{{tr "GotoGo"}}</mdui-button>
|
||||
<mdui-button id="goto-close" slot="action" variant="tonal">{{tr "GotoClose"}}</mdui-button>
|
||||
</mdui-dialog>
|
||||
|
||||
<script>
|
||||
let currentPath = '/opt/nezha/';
|
||||
let fileName = '';
|
||||
let receivedBuffer = []; // 用于缓存数据块
|
||||
let expectedLength = 0;
|
||||
let receivedLength = 0;
|
||||
let isFirstChunk = true;
|
||||
let isUpCompleted = false;
|
||||
let handleReady = false;
|
||||
let worker;
|
||||
|
||||
function updateDirectoryTitle() {
|
||||
const directoryTitle = document.getElementById('current-directory');
|
||||
directoryTitle.textContent = `${currentPath}`;
|
||||
}
|
||||
|
||||
function updateFileList(items) {
|
||||
const fileListElement = document.getElementById('file-list');
|
||||
fileListElement.innerHTML = '';
|
||||
|
||||
if (currentPath !== '/') {
|
||||
const upItem = document.createElement('mdui-list-item');
|
||||
upItem.className = 'file-item up-directory';
|
||||
upItem.setAttribute('icon', 'arrow_back');
|
||||
upItem.textContent = "..";
|
||||
upItem.onclick = function () {
|
||||
const lastSlashIndex = currentPath.lastIndexOf('/', currentPath.length - 2);
|
||||
currentPath = currentPath.substring(0, lastSlashIndex + 1) || '/';
|
||||
listFile();
|
||||
};
|
||||
fileListElement.appendChild(upItem);
|
||||
}
|
||||
|
||||
items.sort((a, b) => {
|
||||
if (a.fileType === 'dir' && b.fileType !== 'dir') {
|
||||
return -1;
|
||||
}
|
||||
if (a.fileType !== 'dir' && b.fileType === 'dir') {
|
||||
return 1;
|
||||
}
|
||||
return a.name.localeCompare(b.name);
|
||||
});
|
||||
|
||||
items.forEach(item => {
|
||||
const listItem = document.createElement('mdui-list-item');
|
||||
listItem.className = `file-item ${item.fileType.toLowerCase()}`;
|
||||
listItem.setAttribute('nonclickable', 'true');
|
||||
listItem.setAttribute('icon', 'insert_drive_file');
|
||||
listItem.textContent = `${item.name}`;
|
||||
|
||||
if (item.fileType === 'dir') {
|
||||
listItem.setAttribute('nonclickable', 'false');
|
||||
listItem.setAttribute('icon', 'folder');
|
||||
listItem.style.cursor = 'pointer';
|
||||
listItem.onclick = function () {
|
||||
currentPath += `${item.name}/`;
|
||||
listFile();
|
||||
};
|
||||
} else {
|
||||
const downloadButton = document.createElement('mdui-button-icon');
|
||||
downloadButton.setAttribute('slot', 'end-icon');
|
||||
downloadButton.setAttribute('icon', 'download');
|
||||
downloadButton.onclick = function () {
|
||||
const filePath = currentPath + item.name;
|
||||
fileName = item.name;
|
||||
downloadFile(filePath);
|
||||
};
|
||||
listItem.appendChild(downloadButton);
|
||||
}
|
||||
|
||||
fileListElement.appendChild(listItem);
|
||||
});
|
||||
updateDirectoryTitle();
|
||||
}
|
||||
|
||||
function resetUpdState() {
|
||||
receivedBuffer = [];
|
||||
expectedLength = 0;
|
||||
receivedLength = 0;
|
||||
isFirstChunk = true;
|
||||
}
|
||||
|
||||
function downloadFile(filePath) {
|
||||
showUpdModal('d');
|
||||
|
||||
const prefix = new Int8Array([1]); // Request download
|
||||
const filePathMessage = new TextEncoder().encode(filePath);
|
||||
|
||||
const msg = new Int8Array(prefix.length + filePathMessage.length);
|
||||
msg.set(prefix);
|
||||
msg.set(filePathMessage, prefix.length);
|
||||
|
||||
socket.send(msg);
|
||||
}
|
||||
|
||||
function listFile() {
|
||||
const prefix = new Int8Array([0]);
|
||||
const resizeMessage = new TextEncoder().encode(currentPath);
|
||||
|
||||
const msg = new Int8Array(prefix.length + resizeMessage.length);
|
||||
msg.set(prefix);
|
||||
msg.set(resizeMessage, prefix.length);
|
||||
|
||||
socket.send(msg);
|
||||
}
|
||||
|
||||
async function uploadFile(file) {
|
||||
showUpdModal('u');
|
||||
|
||||
const chunkSize = 1048576; // 1MB chunk
|
||||
let offset = 0;
|
||||
|
||||
const filePath = `${currentPath}${file.name}`;
|
||||
const fileSize = file.size;
|
||||
const messageType = 2;
|
||||
|
||||
// Build header (type + file size + path)
|
||||
const filePathBytes = new TextEncoder().encode(filePath);
|
||||
const header = new ArrayBuffer(1 + 8 + filePathBytes.length);
|
||||
const headerView = new DataView(header);
|
||||
|
||||
headerView.setUint8(0, messageType);
|
||||
headerView.setBigUint64(1, BigInt(fileSize), false);
|
||||
|
||||
new Uint8Array(header, 9).set(filePathBytes);
|
||||
|
||||
// Send header
|
||||
socket.send(header);
|
||||
|
||||
// Send data chunks
|
||||
while (offset < fileSize) {
|
||||
const chunk = file.slice(offset, offset + chunkSize);
|
||||
const arrayBuffer = await readFileAsArrayBuffer(chunk);
|
||||
socket.send(arrayBuffer);
|
||||
offset += chunkSize;
|
||||
}
|
||||
|
||||
const checkCompletion = setInterval(() => {
|
||||
if (isUpCompleted) {
|
||||
clearInterval(checkCompletion);
|
||||
hideUpdModal();
|
||||
resetUpdState();
|
||||
listFile();
|
||||
}
|
||||
}, 100);
|
||||
}
|
||||
|
||||
async function parseFileList(arrayBuffer) {
|
||||
const dataView = new DataView(arrayBuffer);
|
||||
const items = [];
|
||||
let offset = 4;
|
||||
|
||||
const pathLength = dataView.getUint32(offset);
|
||||
offset += 4;
|
||||
|
||||
const pathArray = new Uint8Array(arrayBuffer, offset, pathLength);
|
||||
currentPath = new TextDecoder('utf-8').decode(pathArray);
|
||||
offset += pathLength;
|
||||
|
||||
while (offset < dataView.byteLength) {
|
||||
const fileType = dataView.getUint8(offset);
|
||||
offset += 1;
|
||||
|
||||
const nameLength = dataView.getUint8(offset);
|
||||
offset += 1;
|
||||
|
||||
if (offset + nameLength > dataView.byteLength) {
|
||||
console.error('Error: Name length exceeds buffer size');
|
||||
break;
|
||||
}
|
||||
|
||||
const nameArray = new Uint8Array(arrayBuffer, offset, nameLength);
|
||||
const name = new TextDecoder('utf-8').decode(nameArray);
|
||||
offset += nameLength;
|
||||
|
||||
items.push({
|
||||
fileType: fileType === 0x01 ? 'dir' : 'f',
|
||||
name: name,
|
||||
});
|
||||
}
|
||||
|
||||
return { items };
|
||||
}
|
||||
|
||||
function readFileAsArrayBuffer(blob) {
|
||||
return new Promise((resolve, reject) => {
|
||||
const reader = new FileReader();
|
||||
reader.onload = () => resolve(reader.result);
|
||||
reader.onerror = () => reject(reader.error);
|
||||
reader.readAsArrayBuffer(blob);
|
||||
});
|
||||
}
|
||||
|
||||
function concatenateArrayBuffers(buffers) {
|
||||
let totalLength = 0;
|
||||
buffers.forEach(buf => totalLength += buf.byteLength);
|
||||
|
||||
const result = new Uint8Array(totalLength);
|
||||
let offset = 0;
|
||||
buffers.forEach(buf => {
|
||||
result.set(new Uint8Array(buf), offset);
|
||||
offset += buf.byteLength;
|
||||
});
|
||||
|
||||
return result.buffer;
|
||||
}
|
||||
|
||||
function arraysEqual(a, b) {
|
||||
if (a.length !== b.length) return false;
|
||||
for (let i = 0; i < a.length; i++) {
|
||||
if (a[i] !== b[i]) return false;
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
async function copyTextToClipboard(text) {
|
||||
try {
|
||||
await navigator.clipboard.writeText(text);
|
||||
} catch (err) {
|
||||
console.error('Failed to copy text to clipboard: ', err);
|
||||
}
|
||||
}
|
||||
|
||||
async function handleError(errMsg) {
|
||||
try {
|
||||
console.error('Received error: ', errMsg);
|
||||
hideUpdModal();
|
||||
const errorDialog = document.getElementById('error-dialog');
|
||||
errorDialog.open = true;
|
||||
if (socket.readyState === WebSocket.OPEN) {
|
||||
socket.close(1000, 'Closing due to error');
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('Error while handling error and closing WebSocket:', error);
|
||||
}
|
||||
}
|
||||
|
||||
function showUpdModal(operation) {
|
||||
const modal = document.getElementById('upd-modal');
|
||||
modal.open = true;
|
||||
if (operation === 'd') {
|
||||
modal.setAttribute('headline', 'Downloading...');
|
||||
} else if (operation === 'u') {
|
||||
modal.setAttribute('headline', 'Uploading...');
|
||||
}
|
||||
}
|
||||
|
||||
function hideUpdModal() {
|
||||
const modal = document.getElementById('upd-modal');
|
||||
modal.open = false;
|
||||
}
|
||||
|
||||
function waitForHandleReady() {
|
||||
return new Promise(resolve => {
|
||||
const checkReady = () => {
|
||||
if (handleReady) {
|
||||
resolve();
|
||||
} else {
|
||||
setTimeout(checkReady, 10);
|
||||
}
|
||||
};
|
||||
checkReady();
|
||||
});
|
||||
}
|
||||
|
||||
const socket = new WebSocket((window.location.protocol === 'https:' ? 'wss' : 'ws') + '://' + window.location.host + '/file/' + '{{.SessionID}}');
|
||||
socket.binaryType = 'arraybuffer';
|
||||
|
||||
socket.onmessage = async function (event) {
|
||||
try {
|
||||
const arrayBuffer = event.data;
|
||||
|
||||
if (isFirstChunk) {
|
||||
const identifier = new Uint8Array(arrayBuffer, 0, 4);
|
||||
const fileIdentifier = new Uint8Array([0x4E, 0x5A, 0x54, 0x44]); // NZTD
|
||||
const fileNameIdentifier = new Uint8Array([0x4E, 0x5A, 0x46, 0x4E]); // NZFN
|
||||
const errorIdentifier = new Uint8Array([0x4E, 0x45, 0x52, 0x52]); // NERR
|
||||
const completeIdentifier = new Uint8Array([0x4E, 0x5A, 0x55, 0x50]); // NZUP
|
||||
|
||||
if (arraysEqual(identifier, fileIdentifier)) {
|
||||
worker = new Worker('/static/file.js');
|
||||
worker.onmessage = async function (event) {
|
||||
switch (event.data.type) {
|
||||
case 'error':
|
||||
console.error('Error from worker:', event.data.error);
|
||||
break;
|
||||
case 'progress':
|
||||
handleReady = true;
|
||||
break;
|
||||
case 'result':
|
||||
handleReady = false;
|
||||
const url = URL.createObjectURL(event.data.blob);
|
||||
const anchor = document.createElement('a');
|
||||
anchor.href = url;
|
||||
anchor.download = event.data.fileName;
|
||||
anchor.click();
|
||||
URL.revokeObjectURL(url);
|
||||
|
||||
// Delete the file in OPFS
|
||||
window.addEventListener('beforeunload', async () => {
|
||||
await worker.postMessage({ operation: 3, arrayBuffer: null, fileName: event.data.fileName });
|
||||
});
|
||||
|
||||
hideUpdModal();
|
||||
resetUpdState();
|
||||
break;
|
||||
}
|
||||
};
|
||||
await worker.postMessage({ operation: 1, arrayBuffer: arrayBuffer, fileName: fileName });
|
||||
isFirstChunk = false;
|
||||
} else if (arraysEqual(identifier, fileNameIdentifier)) {
|
||||
// List files
|
||||
const { items } = await parseFileList(arrayBuffer);
|
||||
updateFileList(items);
|
||||
return;
|
||||
} else if (arraysEqual(identifier, errorIdentifier)) {
|
||||
// Handle error
|
||||
const errBytes = arrayBuffer.slice(4);
|
||||
const errMsg = new TextDecoder('utf-8').decode(errBytes);
|
||||
await handleError(errMsg);
|
||||
return;
|
||||
} else if (arraysEqual(identifier, completeIdentifier)) {
|
||||
// Upload is completed
|
||||
isUpCompleted = true;
|
||||
return;
|
||||
} else {
|
||||
console.log('Unknown identifier');
|
||||
return;
|
||||
}
|
||||
} else {
|
||||
await waitForHandleReady();
|
||||
await worker.postMessage({ operation: 2, arrayBuffer: arrayBuffer, fileName: fileName });
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('Error processing received data:', error);
|
||||
}
|
||||
};
|
||||
|
||||
socket.onopen = function (event) {
|
||||
listFile();
|
||||
};
|
||||
|
||||
socket.onerror = function (event) {
|
||||
console.error('WebSocket error:', event);
|
||||
};
|
||||
|
||||
socket.onclose = function (event) {
|
||||
console.log('WebSocket connection closed:', event);
|
||||
};
|
||||
|
||||
document.getElementById('refresh').addEventListener('click', listFile);
|
||||
|
||||
document.getElementById('copy').addEventListener('click', async () => {
|
||||
await copyTextToClipboard(currentPath);
|
||||
});
|
||||
|
||||
document.getElementById('goto').addEventListener('click', function () {
|
||||
const dialog = document.getElementById('goto-dialog');
|
||||
const textField = document.getElementById('goto-text');
|
||||
const goButton = document.getElementById('goto-go');
|
||||
const closeButton = document.getElementById('goto-close');
|
||||
|
||||
dialog.open = true;
|
||||
|
||||
// Ensure the path ends with a separator
|
||||
const updateText = function (event) {
|
||||
let text = event.target.value;
|
||||
if (!text.endsWith('/')) {
|
||||
text += '/';
|
||||
}
|
||||
return text;
|
||||
};
|
||||
|
||||
const handleGoClick = function () {
|
||||
let text = updateText({ target: textField });
|
||||
currentPath = text;
|
||||
listFile();
|
||||
dialog.open = false;
|
||||
};
|
||||
|
||||
textField.removeEventListener('change', updateText);
|
||||
textField.addEventListener('change', updateText);
|
||||
|
||||
goButton.removeEventListener('click', handleGoClick);
|
||||
goButton.addEventListener('click', handleGoClick);
|
||||
|
||||
closeButton.addEventListener("click", () => dialog.open = false);
|
||||
});
|
||||
|
||||
document.getElementById('upload').addEventListener('click', async function () {
|
||||
const fileInput = document.createElement('input');
|
||||
fileInput.type = 'file';
|
||||
fileInput.style.display = 'none';
|
||||
|
||||
fileInput.addEventListener('change', async function (event) {
|
||||
const file = event.target.files[0];
|
||||
if (file) {
|
||||
await uploadFile(file);
|
||||
isUpCompleted = false;
|
||||
}
|
||||
});
|
||||
|
||||
document.body.appendChild(fileInput);
|
||||
fileInput.click();
|
||||
document.body.removeChild(fileInput);
|
||||
});
|
||||
</script>
|
||||
</body>
|
||||
|
||||
</html>
|
||||
{{end}}
|
||||
+1
-1
@@ -9,7 +9,7 @@
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
<table class="ui very basic table">
|
||||
<table class="ui basic table">
|
||||
<thead>
|
||||
<tr>
|
||||
<th>ID</th>
|
||||
|
||||
+54
@@ -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 basic table">
|
||||
<thead>
|
||||
<tr>
|
||||
<th>ID</th>
|
||||
<th>{{tr "Name"}}</th>
|
||||
<th>Agent ID</th>
|
||||
<th>{{tr "LocalService"}}</th>
|
||||
<th>{{tr "BindHostname"}}</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}}
|
||||
+2
-2
@@ -10,7 +10,7 @@
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
<table class="ui very basic table">
|
||||
<table class="ui basic table">
|
||||
<thead>
|
||||
<tr>
|
||||
<th>ID</th>
|
||||
@@ -52,7 +52,7 @@
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
<table class="ui very basic table">
|
||||
<table class="ui basic table">
|
||||
<thead>
|
||||
<tr>
|
||||
<th>ID</th>
|
||||
|
||||
@@ -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}}
|
||||
+19
-15
@@ -19,7 +19,7 @@
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
<table class="ui very basic table">
|
||||
<table class="ui basic table">
|
||||
<thead>
|
||||
<tr>
|
||||
<th><button onclick="checkAllServer()" class="ui mini nezha-primary-btn button">{{tr "SelectAll"}}</button></th>
|
||||
@@ -28,14 +28,12 @@
|
||||
<th>{{tr "ServerGroup"}}</th>
|
||||
<th>IP</th>
|
||||
<th>{{tr "VersionNumber"}}</th>
|
||||
<th>{{tr "HideForGuest"}}</th>
|
||||
<th>{{tr "EnableDDNS"}}</th>
|
||||
<th>{{tr "EnableIPv4"}}</th>
|
||||
<th>{{tr "EnableIpv6"}}</th>
|
||||
<th>{{tr "DDNSDomain"}}</th>
|
||||
<th>{{tr "HideForGuest"}}</th>
|
||||
<th>{{tr "Secret"}}</th>
|
||||
<th>{{tr "OneKeyInstall"}}</th>
|
||||
<th>{{tr "Note"}}</th>
|
||||
<th>{{tr "PublicNote"}}</th>
|
||||
<th>{{tr "Administration"}}</th>
|
||||
</tr>
|
||||
</thead>
|
||||
@@ -48,11 +46,8 @@
|
||||
<td>{{$server.Tag}}</td>
|
||||
<td>{{$server.Host.IP}}</td>
|
||||
<td>{{$server.Host.Version}}</td>
|
||||
<td>{{$server.HideForGuest}}</td>
|
||||
<td>{{$server.EnableDDNS}}</td>
|
||||
<td>{{$server.EnableIPv4}}</td>
|
||||
<td>{{$server.EnableIpv6}}</td>
|
||||
<td>{{$server.DDNSDomain}}</td>
|
||||
<td>{{$server.HideForGuest}}</td>
|
||||
<td>
|
||||
<button class="ui icon green mini button" data-clipboard-text="{{$server.Secret}}" data-tooltip="{{tr "ClickToCopy"}}">
|
||||
<i class="copy icon"></i>
|
||||
@@ -60,7 +55,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>
|
||||
@@ -75,13 +70,22 @@
|
||||
<i class="apple icon"></i>
|
||||
</button>
|
||||
</td>
|
||||
<td style="word-break: break-word;white-space: pre-wrap;">{{$server.Note}}</td>
|
||||
<td>
|
||||
{{if $server.Note}}
|
||||
<button class="ui icon green mini button" title="{{ $server.Note }}" onclick="addOrEditServer({{$server.MarshalForDashboard}})"><i class="sticky note icon"></i></button>
|
||||
{{end}}
|
||||
</td>
|
||||
<td>
|
||||
{{if $server.PublicNote}}
|
||||
<button class="ui icon green mini button" title="{{$server.PublicNote}}" onclick="addOrEditServer({{$server.MarshalForDashboard}})"><i class="sticky clipboard icon"></i></button>
|
||||
{{end}}
|
||||
</td>
|
||||
<td>
|
||||
<div class="ui mini icon buttons">
|
||||
<button class="ui button" onclick="connectToServer({{$server.ID}})">
|
||||
<i class="terminal icon"></i>
|
||||
</button>
|
||||
<button class="ui button" onclick="addOrEditServer({{$server.Marshal}})">
|
||||
<button class="ui button" onclick="addOrEditServer({{$server.MarshalForDashboard}})">
|
||||
<i class="edit icon"></i>
|
||||
</button>
|
||||
<button class="ui button"
|
||||
@@ -98,7 +102,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]')
|
||||
@@ -155,8 +159,8 @@
|
||||
}
|
||||
function batchEditServerGroup() {
|
||||
let groupName = prompt('{{tr "InputServerGroupName"}}')
|
||||
if (!groupName) {
|
||||
return;
|
||||
if (groupName === null) {
|
||||
return
|
||||
}
|
||||
const servers = []
|
||||
checkBoxList.forEach(cb => {
|
||||
|
||||
+11
-3
@@ -41,6 +41,10 @@
|
||||
<label>{{tr "CustomCodes"}}</label>
|
||||
<textarea name="CustomCode">{{.Conf.Site.CustomCode}}</textarea>
|
||||
</div>
|
||||
<div class="field">
|
||||
<label>{{tr "CustomCodesDashboard"}}</label>
|
||||
<textarea name="CustomCodeDashboard">{{.Conf.Site.CustomCodeDashboard}}</textarea>
|
||||
</div>
|
||||
<div class="field">
|
||||
<label>{{tr "AccessPassword"}}</label>
|
||||
<input type="text" name="ViewPassword" placeholder="" value="{{.Conf.Site.ViewPassword}}">
|
||||
@@ -49,6 +53,10 @@
|
||||
<label>{{tr "PanelServerDomainAndIP"}}</label>
|
||||
<input type="text" name="GRPCHost" placeholder="" value="{{.Conf.GRPCHost}}">
|
||||
</div>
|
||||
<div class="field">
|
||||
<label>{{tr "CustomNameservers"}}</label>
|
||||
<input type="text" name="CustomNameservers" placeholder="" value="{{.Conf.DNSServers}}">
|
||||
</div>
|
||||
<div class="field">
|
||||
<label>{{tr "IPChangeAlert"}}</label>
|
||||
</div>
|
||||
@@ -128,13 +136,13 @@
|
||||
})
|
||||
$('.checkbox').checkbox()
|
||||
$('#settingForm').find("select[name=Cover]").val({{.Conf.Cover }});
|
||||
{{if .Conf.EnableIPChangeNotification}}
|
||||
{{ if .Conf.EnableIPChangeNotification}}
|
||||
$('.checkbox.ip-change').checkbox('set checked')
|
||||
{{ end }}
|
||||
{{if .Conf.EnablePlainIPInNotification}}
|
||||
{{ if .Conf.EnablePlainIPInNotification}}
|
||||
$('.checkbox.plain-ip').checkbox('set checked')
|
||||
{{ end }}
|
||||
{{if .Conf.DisableSwitchTemplateInFrontend }}
|
||||
{{ if .Conf.DisableSwitchTemplateInFrontend }}
|
||||
$('.checkbox.disable-switch-template').checkbox('set checked')
|
||||
{{ end }}
|
||||
</script>
|
||||
|
||||
+64
-6
@@ -8,9 +8,8 @@
|
||||
<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,
|
||||
@@ -24,18 +23,53 @@
|
||||
body {
|
||||
background-color: black;
|
||||
}
|
||||
|
||||
#file-list-iframe {
|
||||
position: absolute;
|
||||
top: 0;
|
||||
right: 0;
|
||||
width: 30%;
|
||||
height: 100%;
|
||||
border: none;
|
||||
background-color: white;
|
||||
display: none;
|
||||
z-index: 10;
|
||||
}
|
||||
|
||||
#folder-button {
|
||||
position: absolute;
|
||||
bottom: 20px;
|
||||
right: 20px;
|
||||
width: 50px;
|
||||
height: 50px;
|
||||
background-color: #007bff;
|
||||
color: white;
|
||||
border: none;
|
||||
border-radius: 25px;
|
||||
font-size: 24px;
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
cursor: pointer;
|
||||
z-index: 20;
|
||||
}
|
||||
</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>
|
||||
<iframe id="file-list-iframe" src=""></iframe>
|
||||
<button id="folder-button">📁</button>
|
||||
|
||||
<script src="https://unpkg.com/[email protected]/lib/xterm.js"></script>
|
||||
<script src="https://unpkg.com/@xterm/[email protected]/lib/addon-fit.js"></script>
|
||||
<script src="https://unpkg.com/@xterm/[email protected]/lib/addon-web-links.js"></script>
|
||||
<script src="https://unpkg.com/@xterm/[email protected]/lib/addon-attach.js"></script>
|
||||
<script>
|
||||
let sendResizing = false;
|
||||
|
||||
function doResize() {
|
||||
fitAddon.fit()
|
||||
|
||||
const w = fitAddon.proposeDimensions();
|
||||
const prefix = new Int8Array([1]);
|
||||
const resizeMessage = new TextEncoder().encode(JSON.stringify({
|
||||
@@ -73,10 +107,13 @@
|
||||
cursorBlink: true,
|
||||
});
|
||||
const socket = new WebSocket((window.location.protocol == 'https:' ? 'wss' : 'ws') + '://' + window.location.host + '/terminal/' + '{{.SessionID}}');
|
||||
|
||||
const attachAddon = new AttachAddon.AttachAddon(socket);
|
||||
const fitAddon = new FitAddon.FitAddon();
|
||||
term.loadAddon(attachAddon);
|
||||
|
||||
const fitAddon = new FitAddon.FitAddon();
|
||||
term.loadAddon(fitAddon);
|
||||
|
||||
term.open(document.getElementById('terminal-container'));
|
||||
|
||||
socket.onopen = () => {
|
||||
@@ -91,6 +128,27 @@
|
||||
socket.onerror = () => {
|
||||
alert('{{tr "TerminalConnectionFailed"}}')
|
||||
}
|
||||
|
||||
// 处理文件夹按钮点击事件
|
||||
const folderButton = document.getElementById('folder-button');
|
||||
const fileListIframe = document.getElementById('file-list-iframe');
|
||||
let fileListVisible = false;
|
||||
|
||||
folderButton.addEventListener('click', () => {
|
||||
if (!fileListVisible) {
|
||||
// 显示文件列表
|
||||
const params = new URLSearchParams({
|
||||
id: "{{.ServerID}}"
|
||||
}).toString();
|
||||
fileListIframe.src = `/file?${params}`;
|
||||
fileListIframe.style.display = 'block';
|
||||
fileListVisible = true;
|
||||
} else {
|
||||
// 隐藏文件列表
|
||||
fileListIframe.style.display = 'none';
|
||||
fileListVisible = false;
|
||||
}
|
||||
});
|
||||
</script>
|
||||
</body>
|
||||
|
||||
|
||||
+1
-1
@@ -13,7 +13,7 @@
|
||||
<script src="https://lf6-cdn-tos.bytecdntp.com/cdn/expire-1-y/semantic-ui/2.4.1/semantic.min.js"></script>
|
||||
<script src="/static/semantic-ui-alerts.min.js"></script>
|
||||
<script src="https://lf6-cdn-tos.bytecdntp.com/cdn/expire-1-y/vue/2.6.14/vue.min.js"></script>
|
||||
<script src="/static/main.js?v20240330"></script>
|
||||
<script src="/static/main.js?v20241011"></script>
|
||||
<script src="/static/theme-default/js/mixin.js?v20240302"></script>
|
||||
<script>
|
||||
(function () {
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user