mirror of
https://github.com/EasyTier/EasyTier.git
synced 2025-10-04 08:26:37 +08:00
Compare commits
110 Commits
Author | SHA1 | Date | |
---|---|---|---|
![]() |
25ed41caf5 | ||
![]() |
4bb72b5606 | ||
![]() |
c4d8ea4fec | ||
![]() |
8588c9201a | ||
![]() |
dd2236c697 | ||
![]() |
bc7c4d8cd0 | ||
![]() |
aed54f7318 | ||
![]() |
86600c6315 | ||
![]() |
3f47f37470 | ||
![]() |
1324e6163e | ||
![]() |
89093167c6 | ||
![]() |
15ad92aef2 | ||
![]() |
6cdea38284 | ||
![]() |
9d455e22fa | ||
![]() |
4fc3ff8ce8 | ||
![]() |
88e6de9d7e | ||
![]() |
e948dbfcc1 | ||
![]() |
8aca5851f2 | ||
![]() |
18da94bf33 | ||
![]() |
1ac2e1c8e3 | ||
![]() |
a78b759741 | ||
![]() |
b5c3726e67 | ||
![]() |
efee3707da | ||
![]() |
bbd3453f36 | ||
![]() |
0bf42c53cc | ||
![]() |
2134bc9139 | ||
![]() |
4df8d7e976 | ||
![]() |
70708b34cc | ||
![]() |
949003ee1b | ||
![]() |
db9df1df94 | ||
![]() |
4dca25db86 | ||
![]() |
d87a440c04 | ||
![]() |
55efd62798 | ||
![]() |
70a41275c1 | ||
![]() |
dd941681ce | ||
![]() |
9824d0adaa | ||
![]() |
d2291628e0 | ||
![]() |
7ab8cad1af | ||
![]() |
2c017e0fc5 | ||
![]() |
d9453589ac | ||
![]() |
e344372616 | ||
![]() |
63821e56bc | ||
![]() |
1be64223c8 | ||
![]() |
a08a8e7f4c | ||
![]() |
b31996230d | ||
![]() |
1e836501a8 | ||
![]() |
d4e59ffc40 | ||
![]() |
37ceb77bf6 | ||
![]() |
ba3da97ad4 | ||
![]() |
984ed8f6cf | ||
![]() |
c7895963e4 | ||
![]() |
a0ece6ad4d | ||
![]() |
d0a3a40a0f | ||
![]() |
ff5ee8a05e | ||
![]() |
a50bcf3087 | ||
![]() |
e0b364d3e2 | ||
![]() |
2496cf51c3 | ||
![]() |
7b4a01e7fb | ||
![]() |
3f9a1d8f2e | ||
![]() |
0b927bcc91 | ||
![]() |
92397bf7b6 | ||
![]() |
d1e2e1db2b | ||
![]() |
783ba50c9e | ||
![]() |
aca9a0e35b | ||
![]() |
fb8d262554 | ||
![]() |
bd60cfc2a0 | ||
![]() |
06afd221d5 | ||
![]() |
0171fb35a4 | ||
![]() |
99c47813c3 | ||
![]() |
82f5dfd569 | ||
![]() |
6d7edcd486 | ||
![]() |
9f273dc887 | ||
![]() |
ac9cfa5040 | ||
![]() |
1b03223537 | ||
![]() |
0467b0a3dc | ||
![]() |
ba75167238 | ||
![]() |
51e7daa26f | ||
![]() |
2ff653cc6f | ||
![]() |
cfe4d080d5 | ||
![]() |
9b28ecde8e | ||
![]() |
096ed39d23 | ||
![]() |
6ea3adcef8 | ||
![]() |
4342be29d7 | ||
![]() |
1609c97574 | ||
![]() |
f07b3ee9c6 | ||
![]() |
2058dbc470 | ||
![]() |
6964fb71fc | ||
![]() |
a8bb4ee7e5 | ||
![]() |
3fcd74ce4e | ||
![]() |
2b7ff0efc5 | ||
![]() |
5833541a6e | ||
![]() |
54c6418f97 | ||
![]() |
fc9aac42b4 | ||
![]() |
89b43684d8 | ||
![]() |
31b26222d3 | ||
![]() |
e4df03053e | ||
![]() |
833e7eca22 | ||
![]() |
b7d85ad2ff | ||
![]() |
8793560e12 | ||
![]() |
58e0e48d59 | ||
![]() |
ad4cbbea6d | ||
![]() |
db660ee3b1 | ||
![]() |
ae54a872ce | ||
![]() |
2aa686f7ad | ||
![]() |
ce10bf5e60 | ||
![]() |
28ae9c447a | ||
![]() |
ff6da9bbec | ||
![]() |
198c239399 | ||
![]() |
0fbbea963f | ||
![]() |
51165c54f5 |
53
.github/ISSUE_TEMPLATE/bug_report.yml
vendored
Normal file
53
.github/ISSUE_TEMPLATE/bug_report.yml
vendored
Normal file
@@ -0,0 +1,53 @@
|
||||
# Copyright 2024-present Easytier Programme within The Commons Conservancy
|
||||
# SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
name: 🐞 问题报告 / Bug Report
|
||||
title: '[bug] '
|
||||
description: 报告一个问题 / Report a bug
|
||||
labels: ['type: bug', 'status: needs triage']
|
||||
|
||||
body:
|
||||
- type: markdown
|
||||
attributes:
|
||||
value: |
|
||||
## 在提交问题之前 / First of all
|
||||
1. 请先搜索有关此问题的 [现有问题](https://github.com/EasyTier/EasyTier/issues?q=is%3Aissue)。
|
||||
1. Please search for [existing issues](https://github.com/EasyTier/EasyTier/issues?q=is%3Aissue) about this problem first.
|
||||
2. 请确保所使用的 Easytier 版本都是最新的。
|
||||
2. Make sure that all Easytier versions are up-to-date.
|
||||
3. 请确保这是 EasyTier 的问题,而不是你正在使用的其他内容引起的问题。
|
||||
3. Make sure it's an issue with EasyTier and not something else you are using.
|
||||
4. 请记得遵守我们的社区准则并保持友好态度。
|
||||
4. Remember to follow our community guidelines and be friendly.
|
||||
|
||||
- type: textarea
|
||||
id: description
|
||||
attributes:
|
||||
label: 描述问题 / Describe the bug
|
||||
description: 对 bug 的明确描述。如果条件允许,请包括屏幕截图。 / A clear description of what the bug is. Include screenshots if applicable.
|
||||
placeholder: 问题描述 / Bug description
|
||||
validations:
|
||||
required: true
|
||||
|
||||
- type: textarea
|
||||
id: reproduction
|
||||
attributes:
|
||||
label: 重现步骤 / Reproduction
|
||||
description: 能够重现行为的步骤或指向能够复现的存储库链接。 / A link to a reproduction repo or steps to reproduce the behaviour.
|
||||
placeholder: |
|
||||
请提供一个最小化的复现示例或复现步骤,请参考这个指南 https://stackoverflow.com/help/minimal-reproducible-example
|
||||
Please provide a minimal reproduction or steps to reproduce, see this guide https://stackoverflow.com/help/minimal-reproducible-example
|
||||
为什么需要重现(问题)?请参阅这篇文章 https://antfu.me/posts/why-reproductions-are-required
|
||||
Why reproduction is required? see this article https://antfu.me/posts/why-reproductions-are-required
|
||||
|
||||
- type: textarea
|
||||
id: expected-behavior
|
||||
attributes:
|
||||
label: 预期结果 / Expected behavior
|
||||
description: 清楚地描述您期望发生的事情。 / A clear description of what you expected to happen.
|
||||
|
||||
- type: textarea
|
||||
id: context
|
||||
attributes:
|
||||
label: 额外上下文 / Additional context
|
||||
description: 在这里添加关于问题的任何其他上下文。 / Add any other context about the problem here.
|
38
.github/ISSUE_TEMPLATE/feature_request.yml
vendored
Normal file
38
.github/ISSUE_TEMPLATE/feature_request.yml
vendored
Normal file
@@ -0,0 +1,38 @@
|
||||
# Copyright 2024-present Easytier Programme within The Commons Conservancy
|
||||
# SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
name: 💡 新功能请求 / Feature Request
|
||||
title: '[feat] '
|
||||
description: 提出一个想法 / Suggest an idea
|
||||
labels: ['type: feature request']
|
||||
|
||||
body:
|
||||
- type: textarea
|
||||
id: problem
|
||||
attributes:
|
||||
label: 描述问题 / Describe the problem
|
||||
description: 明确描述此功能将解决的问题 / A clear description of the problem this feature would solve
|
||||
placeholder: "我总是在...感觉困惑 / I'm always frustrated when..."
|
||||
validations:
|
||||
required: true
|
||||
|
||||
- type: textarea
|
||||
id: solution
|
||||
attributes:
|
||||
label: "描述您想要的解决方案 / Describe the solution you'd like"
|
||||
description: 明确说明您希望做出的改变 / A clear description of what change you would like
|
||||
placeholder: '我希望... / I would like to...'
|
||||
validations:
|
||||
required: true
|
||||
|
||||
- type: textarea
|
||||
id: alternatives
|
||||
attributes:
|
||||
label: 替代方案 / Alternatives considered
|
||||
description: "您考虑过的任何替代解决方案 / Any alternative solutions you've considered"
|
||||
|
||||
- type: textarea
|
||||
id: context
|
||||
attributes:
|
||||
label: 额外上下文 / Additional context
|
||||
description: 在此处添加有关问题的任何其他上下文。 / Add any other context about the problem here.
|
4
.github/workflows/Dockerfile
vendored
4
.github/workflows/Dockerfile
vendored
@@ -18,9 +18,13 @@ RUN mkdir -p /tmp/output; \
|
||||
|
||||
FROM alpine:latest
|
||||
|
||||
RUN apk add --no-cache tzdata
|
||||
WORKDIR /app
|
||||
COPY --from=builder --chmod=755 /tmp/output/* /usr/local/bin
|
||||
|
||||
# users can use "-e TZ=xxx" to adjust it
|
||||
ENV TZ Asia/Shanghai
|
||||
|
||||
# tcp
|
||||
EXPOSE 11010/tcp
|
||||
# udp
|
||||
|
96
.github/workflows/core.yml
vendored
96
.github/workflows/core.yml
vendored
@@ -2,7 +2,7 @@ name: EasyTier Core
|
||||
|
||||
on:
|
||||
push:
|
||||
branches: ["develop", "main"]
|
||||
branches: ["develop", "main", "releases/**"]
|
||||
pull_request:
|
||||
branches: ["develop", "main"]
|
||||
|
||||
@@ -20,14 +20,16 @@ jobs:
|
||||
runs-on: ubuntu-latest
|
||||
# Map a step output to a job output
|
||||
outputs:
|
||||
should_skip: ${{ steps.skip_check.outputs.should_skip }}
|
||||
# do not skip push on branch starts with releases/
|
||||
should_skip: ${{ steps.skip_check.outputs.should_skip == 'true' && !startsWith(github.ref_name, 'releases/') }}
|
||||
steps:
|
||||
- id: skip_check
|
||||
uses: fkirc/skip-duplicate-actions@v5
|
||||
with:
|
||||
# All of these options are optional, so you can remove them if you are happy with the defaults
|
||||
concurrent_skipping: 'never'
|
||||
concurrent_skipping: 'same_content_newer'
|
||||
skip_after_successful_duplicate: 'true'
|
||||
cancel_others: 'true'
|
||||
paths: '["Cargo.toml", "Cargo.lock", "easytier/**", ".github/workflows/core.yml", ".github/workflows/install_rust.sh"]'
|
||||
build:
|
||||
strategy:
|
||||
@@ -35,28 +37,28 @@ jobs:
|
||||
matrix:
|
||||
include:
|
||||
- TARGET: aarch64-unknown-linux-musl
|
||||
OS: ubuntu-latest
|
||||
OS: ubuntu-22.04
|
||||
ARTIFACT_NAME: linux-aarch64
|
||||
- TARGET: x86_64-unknown-linux-musl
|
||||
OS: ubuntu-latest
|
||||
OS: ubuntu-22.04
|
||||
ARTIFACT_NAME: linux-x86_64
|
||||
- TARGET: mips-unknown-linux-musl
|
||||
OS: ubuntu-latest
|
||||
OS: ubuntu-22.04
|
||||
ARTIFACT_NAME: linux-mips
|
||||
- TARGET: mipsel-unknown-linux-musl
|
||||
OS: ubuntu-latest
|
||||
OS: ubuntu-22.04
|
||||
ARTIFACT_NAME: linux-mipsel
|
||||
- TARGET: armv7-unknown-linux-musleabihf # raspberry pi 2-3-4, not tested
|
||||
OS: ubuntu-latest
|
||||
OS: ubuntu-22.04
|
||||
ARTIFACT_NAME: linux-armv7hf
|
||||
- TARGET: armv7-unknown-linux-musleabi # raspberry pi 2-3-4, not tested
|
||||
OS: ubuntu-latest
|
||||
OS: ubuntu-22.04
|
||||
ARTIFACT_NAME: linux-armv7
|
||||
- TARGET: arm-unknown-linux-musleabihf # raspberry pi 0-1, not tested
|
||||
OS: ubuntu-latest
|
||||
OS: ubuntu-22.04
|
||||
ARTIFACT_NAME: linux-armhf
|
||||
- TARGET: arm-unknown-linux-musleabi # raspberry pi 0-1, not tested
|
||||
OS: ubuntu-latest
|
||||
OS: ubuntu-22.04
|
||||
ARTIFACT_NAME: linux-arm
|
||||
|
||||
- TARGET: x86_64-apple-darwin
|
||||
@@ -70,6 +72,15 @@ jobs:
|
||||
OS: windows-latest
|
||||
ARTIFACT_NAME: windows-x86_64
|
||||
|
||||
- TARGET: aarch64-pc-windows-msvc
|
||||
OS: windows-latest
|
||||
ARTIFACT_NAME: windows-arm64
|
||||
|
||||
- TARGET: x86_64-unknown-freebsd
|
||||
OS: ubuntu-22.04
|
||||
ARTIFACT_NAME: freebsd-13.2-x86_64
|
||||
BSD_VERSION: 13.2
|
||||
|
||||
runs-on: ${{ matrix.OS }}
|
||||
env:
|
||||
NAME: easytier
|
||||
@@ -81,9 +92,9 @@ jobs:
|
||||
steps:
|
||||
- uses: actions/checkout@v3
|
||||
|
||||
- uses: actions/setup-node@v4
|
||||
with:
|
||||
node-version: 21
|
||||
- name: Set current ref as env variable
|
||||
run: |
|
||||
echo "GIT_DESC=$(git log -1 --format=%cd.%h --date=format:%Y-%m-%d_%H:%M:%S)" >> $GITHUB_ENV
|
||||
|
||||
- name: Cargo cache
|
||||
uses: actions/cache@v4
|
||||
@@ -93,9 +104,6 @@ jobs:
|
||||
./target
|
||||
key: ${{ runner.os }}-cargo-${{ hashFiles('**/Cargo.lock') }}
|
||||
|
||||
- name: Install rust target
|
||||
run: bash ./.github/workflows/install_rust.sh
|
||||
|
||||
- name: Setup protoc
|
||||
uses: arduino/setup-protoc@v2
|
||||
with:
|
||||
@@ -103,13 +111,52 @@ jobs:
|
||||
repo-token: ${{ secrets.GITHUB_TOKEN }}
|
||||
|
||||
- name: Build Core & Cli
|
||||
if: ${{ ! endsWith(matrix.TARGET, 'freebsd') }}
|
||||
run: |
|
||||
bash ./.github/workflows/install_rust.sh
|
||||
if [[ $OS =~ ^ubuntu.*$ && $TARGET =~ ^mips.*$ ]]; then
|
||||
cargo +nightly build -r --verbose --target $TARGET -Z build-std=std,panic_abort --no-default-features --features mips
|
||||
cargo +nightly build -r --verbose --target $TARGET -Z build-std=std,panic_abort --no-default-features --features mips --package=easytier
|
||||
else
|
||||
cargo build --release --verbose --target $TARGET
|
||||
fi
|
||||
|
||||
# Copied and slightly modified from @lmq8267 (https://github.com/lmq8267)
|
||||
- name: Build Core & Cli (X86_64 FreeBSD)
|
||||
uses: cross-platform-actions/action@v0.23.0
|
||||
if: ${{ endsWith(matrix.TARGET, 'freebsd') }}
|
||||
env:
|
||||
TARGET: ${{ matrix.TARGET }}
|
||||
with:
|
||||
operating_system: freebsd
|
||||
environment_variables: TARGET
|
||||
architecture: x86-64
|
||||
version: ${{ matrix.BSD_VERSION }}
|
||||
shell: bash
|
||||
memory: 5G
|
||||
cpu_count: 4
|
||||
run: |
|
||||
uname -a
|
||||
echo $SHELL
|
||||
pwd
|
||||
ls -lah
|
||||
whoami
|
||||
env | sort
|
||||
|
||||
sudo pkg install -y git protobuf
|
||||
curl --proto 'https' --tlsv1.2 -sSf https://sh.rustup.rs | sh -s -- -y
|
||||
source $HOME/.cargo/env
|
||||
|
||||
rustup set auto-self-update disable
|
||||
|
||||
rustup install 1.77
|
||||
rustup default 1.77
|
||||
|
||||
export CC=clang
|
||||
export CXX=clang++
|
||||
export CARGO_TERM_COLOR=always
|
||||
|
||||
cargo build --release --verbose --target $TARGET
|
||||
|
||||
- name: Install UPX
|
||||
if: ${{ matrix.OS != 'macos-latest' }}
|
||||
uses: crazy-max/ghaction-upx@v3
|
||||
@@ -121,10 +168,14 @@ jobs:
|
||||
run: |
|
||||
mkdir -p ./artifacts/objects/
|
||||
# windows is the only OS using a different convention for executable file name
|
||||
if [[ $OS =~ ^windows.*$ ]]; then
|
||||
if [[ $OS =~ ^windows.*$ && $TARGET =~ ^x86_64.*$ ]]; then
|
||||
SUFFIX=.exe
|
||||
cp easytier/third_party/Packet.dll ./artifacts/objects/
|
||||
cp easytier/third_party/wintun.dll ./artifacts/objects/
|
||||
elif [[ $OS =~ ^windows.*$ && $TARGET =~ ^aarch64.*$ ]]; then
|
||||
SUFFIX=.exe
|
||||
cp easytier/third_party/arm64/Packet.dll ./artifacts/objects/
|
||||
cp easytier/third_party/arm64/wintun.dll ./artifacts/objects/
|
||||
fi
|
||||
if [[ $GITHUB_REF_TYPE =~ ^tag$ ]]; then
|
||||
TAG=$GITHUB_REF_NAME
|
||||
@@ -132,13 +183,16 @@ jobs:
|
||||
TAG=$GITHUB_SHA
|
||||
fi
|
||||
|
||||
if [[ $OS =~ ^ubuntu.*$ ]]; then
|
||||
if [[ $OS =~ ^ubuntu.*$ && ! $TARGET =~ ^.*freebsd$ ]]; then
|
||||
upx --lzma --best ./target/$TARGET/release/easytier-core"$SUFFIX"
|
||||
upx --lzma --best ./target/$TARGET/release/easytier-cli"$SUFFIX"
|
||||
fi
|
||||
|
||||
mv ./target/$TARGET/release/easytier-core"$SUFFIX" ./artifacts/objects/
|
||||
mv ./target/$TARGET/release/easytier-cli"$SUFFIX" ./artifacts/objects/
|
||||
if [[ ! $TARGET =~ ^mips.*$ ]]; then
|
||||
mv ./target/$TARGET/release/easytier-web"$SUFFIX" ./artifacts/objects/
|
||||
fi
|
||||
|
||||
mv ./artifacts/objects/* ./artifacts/
|
||||
rm -rf ./artifacts/objects/
|
||||
@@ -159,7 +213,7 @@ jobs:
|
||||
endpoint: ${{ secrets.ALIYUN_OSS_ENDPOINT }}
|
||||
bucket: ${{ secrets.ALIYUN_OSS_BUCKET }}
|
||||
local-path: ./artifacts/
|
||||
remote-path: /easytier-releases/${{ github.sha }}/
|
||||
remote-path: /easytier-releases/${{env.GIT_DESC}}/easytier-${{ matrix.ARTIFACT_NAME }}
|
||||
no-delete-remote-files: true
|
||||
retry: 5
|
||||
core-result:
|
||||
|
45
.github/workflows/gui.yml
vendored
45
.github/workflows/gui.yml
vendored
@@ -2,7 +2,7 @@ name: EasyTier GUI
|
||||
|
||||
on:
|
||||
push:
|
||||
branches: ["develop", "main"]
|
||||
branches: ["develop", "main", "releases/**"]
|
||||
pull_request:
|
||||
branches: ["develop", "main"]
|
||||
|
||||
@@ -20,14 +20,15 @@ jobs:
|
||||
runs-on: ubuntu-latest
|
||||
# Map a step output to a job output
|
||||
outputs:
|
||||
should_skip: ${{ steps.skip_check.outputs.should_skip }}
|
||||
should_skip: ${{ steps.skip_check.outputs.should_skip == 'true' && !startsWith(github.ref_name, 'releases/') }}
|
||||
steps:
|
||||
- id: skip_check
|
||||
uses: fkirc/skip-duplicate-actions@v5
|
||||
with:
|
||||
# All of these options are optional, so you can remove them if you are happy with the defaults
|
||||
concurrent_skipping: 'never'
|
||||
concurrent_skipping: 'same_content_newer'
|
||||
skip_after_successful_duplicate: 'true'
|
||||
cancel_others: 'true'
|
||||
paths: '["Cargo.toml", "Cargo.lock", "easytier/**", "easytier-gui/**", ".github/workflows/gui.yml", ".github/workflows/install_rust.sh"]'
|
||||
build-gui:
|
||||
strategy:
|
||||
@@ -35,11 +36,11 @@ jobs:
|
||||
matrix:
|
||||
include:
|
||||
- TARGET: aarch64-unknown-linux-musl
|
||||
OS: ubuntu-latest
|
||||
OS: ubuntu-22.04
|
||||
GUI_TARGET: aarch64-unknown-linux-gnu
|
||||
ARTIFACT_NAME: linux-aarch64
|
||||
- TARGET: x86_64-unknown-linux-musl
|
||||
OS: ubuntu-latest
|
||||
OS: ubuntu-22.04
|
||||
GUI_TARGET: x86_64-unknown-linux-gnu
|
||||
ARTIFACT_NAME: linux-x86_64
|
||||
|
||||
@@ -57,6 +58,11 @@ jobs:
|
||||
GUI_TARGET: x86_64-pc-windows-msvc
|
||||
ARTIFACT_NAME: windows-x86_64
|
||||
|
||||
- TARGET: aarch64-pc-windows-msvc
|
||||
OS: windows-latest
|
||||
GUI_TARGET: aarch64-pc-windows-msvc
|
||||
ARTIFACT_NAME: windows-arm64
|
||||
|
||||
runs-on: ${{ matrix.OS }}
|
||||
env:
|
||||
NAME: easytier
|
||||
@@ -69,6 +75,10 @@ jobs:
|
||||
steps:
|
||||
- uses: actions/checkout@v3
|
||||
|
||||
- name: Set current ref as env variable
|
||||
run: |
|
||||
echo "GIT_DESC=$(git log -1 --format=%cd.%h --date=format:%Y-%m-%d_%H:%M:%S)" >> $GITHUB_ENV
|
||||
|
||||
- uses: actions/setup-node@v4
|
||||
with:
|
||||
node-version: 21
|
||||
@@ -94,8 +104,8 @@ jobs:
|
||||
|
||||
- name: Install frontend dependencies
|
||||
run: |
|
||||
(cd easytier-gui; pnpm install)
|
||||
(cd tauri-plugin-vpnservice; pnpm install; pnpm build)
|
||||
pnpm -r install
|
||||
pnpm -r build
|
||||
|
||||
- name: Cargo cache
|
||||
uses: actions/cache@v4
|
||||
@@ -141,20 +151,29 @@ jobs:
|
||||
echo "deb [arch=armhf,arm64] http://ports.ubuntu.com/ubuntu-ports jammy-security multiverse" | sudo tee -a /etc/apt/sources.list
|
||||
|
||||
sudo dpkg --add-architecture arm64
|
||||
sudo apt-get update && sudo apt-get upgrade -y
|
||||
sudo apt install gcc-aarch64-linux-gnu
|
||||
sudo apt install libwebkit2gtk-4.1-dev:arm64
|
||||
sudo apt install libssl-dev:arm64
|
||||
sudo apt-get update
|
||||
sudo apt-get install -y libgstreamer1.0-0:arm64 gstreamer1.0-plugins-base:arm64 gstreamer1.0-plugins-good:arm64
|
||||
sudo apt-get install -y libgstreamer-gl1.0-0:arm64 libgstreamer-plugins-base1.0-0:arm64 libgstreamer-plugins-good1.0-0:arm64 libwebkit2gtk-4.1-0:arm64
|
||||
sudo apt install -f -o Dpkg::Options::="--force-overwrite" libwebkit2gtk-4.1-dev:arm64 libssl-dev:arm64 gcc-aarch64-linux-gnu
|
||||
echo "PKG_CONFIG_SYSROOT_DIR=/usr/aarch64-linux-gnu/" >> "$GITHUB_ENV"
|
||||
echo "PKG_CONFIG_PATH=/usr/lib/aarch64-linux-gnu/pkgconfig/" >> "$GITHUB_ENV"
|
||||
|
||||
- name: copy correct DLLs
|
||||
if: ${{ matrix.OS == 'windows-latest' }}
|
||||
run: |
|
||||
if [[ $GUI_TARGET =~ ^aarch64.*$ ]]; then
|
||||
cp ./easytier/third_party/arm64/*.dll ./easytier-gui/src-tauri/
|
||||
else
|
||||
cp ./easytier/third_party/*.dll ./easytier-gui/src-tauri/
|
||||
fi
|
||||
|
||||
- name: Build GUI
|
||||
if: ${{ matrix.GUI_TARGET != '' }}
|
||||
uses: tauri-apps/tauri-action@v0
|
||||
with:
|
||||
projectPath: ./easytier-gui
|
||||
# https://tauri.app/v1/guides/building/linux/#cross-compiling-tauri-applications-for-arm-based-devices
|
||||
args: --verbose --target ${{ matrix.GUI_TARGET }} ${{ matrix.OS == 'ubuntu-latest' && contains(matrix.TARGET, 'aarch64') && '--bundles deb' || '' }}
|
||||
args: --verbose --target ${{ matrix.GUI_TARGET }} ${{ matrix.OS == 'ubuntu-22.04' && contains(matrix.TARGET, 'aarch64') && '--bundles deb' || '' }}
|
||||
|
||||
- name: Compress
|
||||
run: |
|
||||
@@ -197,7 +216,7 @@ jobs:
|
||||
endpoint: ${{ secrets.ALIYUN_OSS_ENDPOINT }}
|
||||
bucket: ${{ secrets.ALIYUN_OSS_BUCKET }}
|
||||
local-path: ./artifacts/
|
||||
remote-path: /easytier-releases/${{ github.sha }}/gui
|
||||
remote-path: /easytier-releases/${{env.GIT_DESC}}/easytier-gui-${{ matrix.ARTIFACT_NAME }}
|
||||
no-delete-remote-files: true
|
||||
retry: 5
|
||||
gui-result:
|
||||
|
19
.github/workflows/mobile.yml
vendored
19
.github/workflows/mobile.yml
vendored
@@ -2,7 +2,7 @@ name: EasyTier Mobile
|
||||
|
||||
on:
|
||||
push:
|
||||
branches: ["develop", "main"]
|
||||
branches: ["develop", "main", "releases/**"]
|
||||
pull_request:
|
||||
branches: ["develop", "main"]
|
||||
|
||||
@@ -20,14 +20,15 @@ jobs:
|
||||
runs-on: ubuntu-latest
|
||||
# Map a step output to a job output
|
||||
outputs:
|
||||
should_skip: ${{ steps.skip_check.outputs.should_skip }}
|
||||
should_skip: ${{ steps.skip_check.outputs.should_skip == 'true' && !startsWith(github.ref_name, 'releases/') }}
|
||||
steps:
|
||||
- id: skip_check
|
||||
uses: fkirc/skip-duplicate-actions@v5
|
||||
with:
|
||||
# All of these options are optional, so you can remove them if you are happy with the defaults
|
||||
concurrent_skipping: 'never'
|
||||
concurrent_skipping: 'same_content_newer'
|
||||
skip_after_successful_duplicate: 'true'
|
||||
cancel_others: 'true'
|
||||
paths: '["Cargo.toml", "Cargo.lock", "easytier/**", "easytier-gui/**", "tauri-plugin-vpnservice/**", ".github/workflows/mobile.yml", ".github/workflows/install_rust.sh"]'
|
||||
build-mobile:
|
||||
strategy:
|
||||
@@ -35,7 +36,7 @@ jobs:
|
||||
matrix:
|
||||
include:
|
||||
- TARGET: android
|
||||
OS: ubuntu-latest
|
||||
OS: ubuntu-22.04
|
||||
ARTIFACT_NAME: android
|
||||
runs-on: ${{ matrix.OS }}
|
||||
env:
|
||||
@@ -48,6 +49,10 @@ jobs:
|
||||
steps:
|
||||
- uses: actions/checkout@v3
|
||||
|
||||
- name: Set current ref as env variable
|
||||
run: |
|
||||
echo "GIT_DESC=$(git log -1 --format=%cd.%h --date=format:%Y-%m-%d_%H:%M:%S)" >> $GITHUB_ENV
|
||||
|
||||
- uses: actions/setup-java@v4
|
||||
with:
|
||||
distribution: 'oracle'
|
||||
@@ -90,8 +95,8 @@ jobs:
|
||||
|
||||
- name: Install frontend dependencies
|
||||
run: |
|
||||
(cd easytier-gui; pnpm install)
|
||||
(cd tauri-plugin-vpnservice; pnpm install; pnpm build)
|
||||
pnpm -r install
|
||||
pnpm -r build
|
||||
|
||||
- name: Cargo cache
|
||||
uses: actions/cache@v4
|
||||
@@ -150,7 +155,7 @@ jobs:
|
||||
endpoint: ${{ secrets.ALIYUN_OSS_ENDPOINT }}
|
||||
bucket: ${{ secrets.ALIYUN_OSS_BUCKET }}
|
||||
local-path: ./artifacts/
|
||||
remote-path: /easytier-releases/${{ github.sha }}/mobile
|
||||
remote-path: /easytier-releases/${{env.GIT_DESC}}/easytier-gui-${{ matrix.ARTIFACT_NAME }}
|
||||
no-delete-remote-files: true
|
||||
retry: 5
|
||||
mobile-result:
|
||||
|
15
.github/workflows/release.yml
vendored
15
.github/workflows/release.yml
vendored
@@ -19,9 +19,9 @@ on:
|
||||
default: 10322498555
|
||||
required: true
|
||||
version:
|
||||
description: 'version for this release'
|
||||
description: 'Version for this release'
|
||||
type: string
|
||||
default: 'v1.2.2'
|
||||
default: 'v2.1.0'
|
||||
required: true
|
||||
make_latest:
|
||||
description: 'Mark this release as latest'
|
||||
@@ -55,7 +55,7 @@ jobs:
|
||||
github_token: ${{secrets.GITHUB_TOKEN}}
|
||||
run_id: ${{ inputs.gui_run_id }}
|
||||
repo: EasyTier/EasyTier
|
||||
path: release_assets
|
||||
path: release_assets_nozip
|
||||
|
||||
- name: Download GUI Artifact
|
||||
uses: dawidd6/action-download-artifact@v6
|
||||
@@ -63,17 +63,20 @@ jobs:
|
||||
github_token: ${{secrets.GITHUB_TOKEN}}
|
||||
run_id: ${{ inputs.mobile_run_id }}
|
||||
repo: EasyTier/EasyTier
|
||||
path: release_assets
|
||||
path: release_assets_nozip
|
||||
|
||||
- name: Zip release assets
|
||||
env:
|
||||
VERSION: ${{ inputs.version }}
|
||||
run: |
|
||||
mkdir zipped_assets
|
||||
|
||||
find release_assets_nozip -type f -exec mv {} zipped_assets \;
|
||||
ls -l -R ./zipped_assets
|
||||
|
||||
cd release_assets
|
||||
ls -l -R ./
|
||||
chmod -R 755 .
|
||||
|
||||
mkdir ../zipped_assets
|
||||
for x in `ls`; do
|
||||
zip ../zipped_assets/$x-${VERSION}.zip $x/*;
|
||||
done
|
||||
|
2
.github/workflows/test.yml
vendored
2
.github/workflows/test.yml
vendored
@@ -30,7 +30,7 @@ jobs:
|
||||
skip_after_successful_duplicate: 'true'
|
||||
paths: '["Cargo.toml", "Cargo.lock", "easytier/**", ".github/workflows/test.yml"]'
|
||||
test:
|
||||
runs-on: ubuntu-latest
|
||||
runs-on: ubuntu-22.04
|
||||
needs: pre_job
|
||||
if: needs.pre_job.outputs.should_skip != 'true'
|
||||
steps:
|
||||
|
8
.gitignore
vendored
8
.gitignore
vendored
@@ -11,6 +11,7 @@ target-*/
|
||||
*.pdb
|
||||
|
||||
.vscode
|
||||
/.idea
|
||||
|
||||
# perf & flamegraph
|
||||
perf.data
|
||||
@@ -29,3 +30,10 @@ musl_gcc
|
||||
|
||||
# log
|
||||
easytier-panic.log
|
||||
|
||||
# web
|
||||
node_modules
|
||||
|
||||
.vite
|
||||
|
||||
easytier-gui/src-tauri/*.dll
|
||||
|
3323
Cargo.lock
generated
3323
Cargo.lock
generated
File diff suppressed because it is too large
Load Diff
@@ -1,7 +1,7 @@
|
||||
[workspace]
|
||||
resolver = "2"
|
||||
members = ["easytier", "easytier-gui/src-tauri"]
|
||||
default-members = ["easytier"]
|
||||
members = ["easytier", "easytier-gui/src-tauri", "easytier-rpc-build", "easytier-web"]
|
||||
default-members = ["easytier", "easytier-web"]
|
||||
|
||||
[profile.dev]
|
||||
panic = "unwind"
|
||||
@@ -10,4 +10,3 @@ panic = "unwind"
|
||||
panic = "abort"
|
||||
lto = true
|
||||
codegen-units = 1
|
||||
strip = true
|
||||
|
@@ -4,84 +4,27 @@
|
||||
"path": "."
|
||||
},
|
||||
{
|
||||
"name": "gui",
|
||||
"path": "easytier-gui"
|
||||
},
|
||||
{
|
||||
"name": "core",
|
||||
"path": "easytier"
|
||||
},
|
||||
{
|
||||
"name": "vpnservice",
|
||||
"path": "tauri-plugin-vpnservice"
|
||||
},
|
||||
{
|
||||
"name": "rpc-build",
|
||||
"path": "easytier-rpc-build"
|
||||
}
|
||||
],
|
||||
"settings": {
|
||||
"eslint.experimental.useFlatConfig": true,
|
||||
"i18n-ally.sourceLanguage": "cn",
|
||||
"i18n-ally.keystyle": "nested",
|
||||
"i18n-ally.sortKeys": true,
|
||||
// Disable the default formatter
|
||||
"prettier.enable": false,
|
||||
"editor.formatOnSave": false,
|
||||
"editor.codeActionsOnSave": {
|
||||
"source.fixAll.eslint": "explicit",
|
||||
"source.organizeImports": "never"
|
||||
},
|
||||
"eslint.rules.customizations": [
|
||||
{
|
||||
"rule": "style/*",
|
||||
"severity": "off"
|
||||
},
|
||||
{
|
||||
"rule": "style/eol-last",
|
||||
"severity": "error"
|
||||
},
|
||||
{
|
||||
"rule": "format/*",
|
||||
"severity": "off"
|
||||
},
|
||||
{
|
||||
"rule": "*-indent",
|
||||
"severity": "off"
|
||||
},
|
||||
{
|
||||
"rule": "*-spacing",
|
||||
"severity": "off"
|
||||
},
|
||||
{
|
||||
"rule": "*-spaces",
|
||||
"severity": "off"
|
||||
},
|
||||
{
|
||||
"rule": "*-order",
|
||||
"severity": "off"
|
||||
},
|
||||
{
|
||||
"rule": "*-dangle",
|
||||
"severity": "off"
|
||||
},
|
||||
{
|
||||
"rule": "*-newline",
|
||||
"severity": "off"
|
||||
},
|
||||
{
|
||||
"rule": "*quotes",
|
||||
"severity": "off"
|
||||
},
|
||||
{
|
||||
"rule": "*semi",
|
||||
"severity": "off"
|
||||
}
|
||||
],
|
||||
"eslint.validate": [
|
||||
"code-workspace",
|
||||
"javascript",
|
||||
"javascriptreact",
|
||||
"typescript",
|
||||
"typescriptreact",
|
||||
"vue",
|
||||
"html",
|
||||
"markdown",
|
||||
"json",
|
||||
"jsonc",
|
||||
"yaml",
|
||||
"toml",
|
||||
"gql",
|
||||
"graphql"
|
||||
],
|
||||
"i18n-ally.localesPaths": [
|
||||
"easytier-gui/locales"
|
||||
]
|
||||
}
|
||||
}
|
381
README.md
381
README.md
@@ -1,28 +1,28 @@
|
||||
# EasyTier
|
||||
|
||||
[](https://github.com/EasyTier/EasyTier/blob/main/LICENSE)
|
||||
[](https://github.com/EasyTier/EasyTier/commits/main)
|
||||
[](https://github.com/EasyTier/EasyTier/issues)
|
||||
[](https://github.com/EasyTier/EasyTier/actions/workflows/core.yml)
|
||||
[](https://github.com/EasyTier/EasyTier/actions/workflows/gui.yml)
|
||||
|
||||
# EasyTier
|
||||
|
||||
[](https://github.com/EasyTier/EasyTier/blob/main/LICENSE)
|
||||
[](https://github.com/EasyTier/EasyTier/commits/main)
|
||||
[](https://github.com/EasyTier/EasyTier/issues)
|
||||
[](https://github.com/EasyTier/EasyTier/actions/workflows/core.yml)
|
||||
[](https://github.com/EasyTier/EasyTier/actions/workflows/gui.yml)
|
||||
|
||||
[简体中文](/README_CN.md) | [English](/README.md)
|
||||
|
||||
**Please visit the [EasyTier Official Website](https://www.easytier.top/en/) to view the full documentation.**
|
||||
**Please visit the [EasyTier Official Website](https://www.easytier.top/en/) to view the full documentation.**
|
||||
|
||||
EasyTier is a simple, safe and decentralized VPN networking solution implemented with the Rust language and Tokio framework.
|
||||
EasyTier is a simple, safe and decentralized VPN networking solution implemented with the Rust language and Tokio framework.
|
||||
|
||||
<p align="center">
|
||||
<img src="assets/image-5.png" width="300">
|
||||
<img src="assets/image-4.png" width="300">
|
||||
</p>
|
||||
|
||||
## Features
|
||||
|
||||
|
||||
## Features
|
||||
|
||||
- **Decentralized**: No need to rely on centralized services, nodes are equal and independent.
|
||||
- **Safe**: Use WireGuard protocol to encrypt data.
|
||||
- **High Performance**: Full-link zero-copy, with performance comparable to mainstream networking software.
|
||||
- **Cross-platform**: Supports MacOS/Linux/Windows, will support IOS and Android in the future. The executable file is statically linked, making deployment simple.
|
||||
- **Cross-platform**: Supports MacOS/Linux/Windows/Android, will support IOS in the future. The executable file is statically linked, making deployment simple.
|
||||
- **Networking without public IP**: Supports networking using shared public nodes, refer to [Configuration Guide](#Networking-without-public-IP)
|
||||
- **NAT traversal**: Supports UDP-based NAT traversal, able to establish stable connections even in complex network environments.
|
||||
- **Subnet Proxy (Point-to-Network)**: Nodes can expose accessible network segments as proxies to the VPN subnet, allowing other nodes to access these subnets through the node.
|
||||
@@ -32,170 +32,195 @@
|
||||
- **IPv6 Support**: Supports networking using IPv6.
|
||||
- **Multiple Protocol Types**: Supports communication between nodes using protocols such as WebSocket and QUIC.
|
||||
|
||||
|
||||
## Installation
|
||||
|
||||
1. **Download the precompiled binary file**
|
||||
|
||||
Visit the [GitHub Release page](https://github.com/EasyTier/EasyTier/releases) to download the binary file suitable for your operating system. Release includes both command-line programs and GUI programs in the compressed package.
|
||||
|
||||
2. **Install via crates.io**
|
||||
## Installation
|
||||
|
||||
1. **Download the precompiled binary file**
|
||||
|
||||
Visit the [GitHub Release page](https://github.com/EasyTier/EasyTier/releases) to download the binary file suitable for your operating system. Release includes both command-line programs and GUI programs in the compressed package.
|
||||
|
||||
2. **Install via crates.io**
|
||||
|
||||
```sh
|
||||
cargo install easytier
|
||||
```
|
||||
|
||||
3. **Install from source code**
|
||||
|
||||
3. **Install from source code**
|
||||
|
||||
```sh
|
||||
cargo install --git https://github.com/EasyTier/EasyTier.git
|
||||
cargo install --git https://github.com/EasyTier/EasyTier.git easytier
|
||||
```
|
||||
|
||||
4. **Install by Docker Compose**
|
||||
4. **Install by Docker Compose**
|
||||
|
||||
Please visit the [EasyTier Official Website](https://www.easytier.top/en/) to view the full documentation.
|
||||
Please visit the [EasyTier Official Website](https://www.easytier.top/en/) to view the full documentation.
|
||||
|
||||
5. **Install by script (For Linux Only)**
|
||||
|
||||
5. **Install by script (For Linux Only)**
|
||||
```sh
|
||||
wget -O /tmp/easytier.sh "https://raw.githubusercontent.com/EasyTier/EasyTier/main/script/easytier.sh" && bash /tmp/easytier.sh install
|
||||
wget -O /tmp/easytier.sh "https://raw.githubusercontent.com/EasyTier/EasyTier/main/script/install.sh" && bash /tmp/easytier.sh install
|
||||
```
|
||||
|
||||
|
||||
You can also uninstall/update Easytier by the command "uninstall" or "update" of this script
|
||||
|
||||
## Quick Start
|
||||
6. **Install by Homebrew (For MacOS Only)**
|
||||
|
||||
```sh
|
||||
brew tap brewforge/chinese
|
||||
brew install --cask easytier
|
||||
```
|
||||
|
||||
## Quick Start
|
||||
|
||||
> The following text only describes the use of the command-line tool; the GUI program can be configured by referring to the following concepts.
|
||||
|
||||
Make sure EasyTier is installed according to the [Installation Guide](#Installation), and both easytier-core and easytier-cli commands are available.
|
||||
|
||||
### Two-node Networking
|
||||
|
||||
Assuming the network topology of the two nodes is as follows
|
||||
|
||||
```mermaid
|
||||
flowchart LR
|
||||
|
||||
subgraph Node A IP 22.1.1.1
|
||||
nodea[EasyTier\n10.144.144.1]
|
||||
end
|
||||
|
||||
subgraph Node B
|
||||
nodeb[EasyTier\n10.144.144.2]
|
||||
end
|
||||
|
||||
nodea <-----> nodeb
|
||||
|
||||
```
|
||||
|
||||
1. Execute on Node A:
|
||||
|
||||
> The following text only describes the use of the command-line tool; the GUI program can be configured by referring to the following concepts.
|
||||
|
||||
Make sure EasyTier is installed according to the [Installation Guide](#Installation), and both easytier-core and easytier-cli commands are available.
|
||||
|
||||
### Two-node Networking
|
||||
|
||||
Assuming the network topology of the two nodes is as follows
|
||||
|
||||
```mermaid
|
||||
flowchart LR
|
||||
|
||||
subgraph Node A IP 22.1.1.1
|
||||
nodea[EasyTier\n10.144.144.1]
|
||||
end
|
||||
|
||||
subgraph Node B
|
||||
nodeb[EasyTier\n10.144.144.2]
|
||||
end
|
||||
|
||||
nodea <-----> nodeb
|
||||
|
||||
```
|
||||
|
||||
1. Execute on Node A:
|
||||
```sh
|
||||
sudo easytier-core --ipv4 10.144.144.1
|
||||
```
|
||||
|
||||
Successful execution of the command will print the following.
|
||||
|
||||
|
||||

|
||||
|
||||
2. Execute on Node B
|
||||
|
||||
2. Execute on Node B
|
||||
|
||||
```sh
|
||||
sudo easytier-core --ipv4 10.144.144.2 --peers udp://22.1.1.1:11010
|
||||
```
|
||||
|
||||
3. Test Connectivity
|
||||
|
||||
|
||||
3. Test Connectivity
|
||||
|
||||
The two nodes should connect successfully and be able to communicate within the virtual subnet
|
||||
|
||||
```sh
|
||||
ping 10.144.144.2
|
||||
```
|
||||
|
||||
|
||||
Use easytier-cli to view node information in the subnet
|
||||
|
||||
```sh
|
||||
easytier-cli peer
|
||||
```
|
||||
|
||||

|
||||
|
||||
```sh
|
||||
easytier-cli route
|
||||
```
|
||||
|
||||

|
||||
|
||||
|
||||
```sh
|
||||
easytier-cli node
|
||||
```
|
||||
|
||||

|
||||
|
||||
---
|
||||
|
||||
### Multi-node Networking
|
||||
|
||||
Based on the two-node networking example just now, if more nodes need to join the virtual network, you can use the following command.
|
||||
|
||||
```
|
||||
sudo easytier-core --ipv4 10.144.144.2 --peers udp://22.1.1.1:11010
|
||||
```
|
||||
|
||||
The `--peers` parameter can fill in the listening address of any node already in the virtual network.
|
||||
|
||||
---
|
||||
|
||||
### Subnet Proxy (Point-to-Network) Configuration
|
||||
|
||||
Assuming the network topology is as follows, Node B wants to share its accessible subnet 10.1.1.0/24 with other nodes.
|
||||
|
||||
```mermaid
|
||||
flowchart LR
|
||||
|
||||
subgraph Node A IP 22.1.1.1
|
||||
nodea[EasyTier\n10.144.144.1]
|
||||
end
|
||||
|
||||
subgraph Node B
|
||||
nodeb[EasyTier\n10.144.144.2]
|
||||
end
|
||||
|
||||
id1[[10.1.1.0/24]]
|
||||
|
||||
nodea <--> nodeb <-.-> id1
|
||||
|
||||
```
|
||||
|
||||
Then the startup parameters for Node B's easytier are (new -n parameter)
|
||||
|
||||
```sh
|
||||
sudo easytier-core --ipv4 10.144.144.2 -n 10.1.1.0/24
|
||||
```
|
||||
|
||||
Subnet proxy information will automatically sync to each node in the virtual network, and each node will automatically configure the corresponding route. Node A can check whether the subnet proxy is effective through the following command.
|
||||
|
||||
1. Check whether the routing information has been synchronized, the proxy_cidrs column shows the proxied subnets.
|
||||
|
||||
|
||||
### Multi-node Networking
|
||||
|
||||
Based on the two-node networking example just now, if more nodes need to join the virtual network, you can use the following command.
|
||||
|
||||
```sh
|
||||
sudo easytier-core --ipv4 10.144.144.2 --peers udp://22.1.1.1:11010
|
||||
```
|
||||
|
||||
The `--peers` parameter can fill in the listening address of any node already in the virtual network.
|
||||
|
||||
---
|
||||
|
||||
### Subnet Proxy (Point-to-Network) Configuration
|
||||
|
||||
Assuming the network topology is as follows, Node B wants to share its accessible subnet 10.1.1.0/24 with other nodes.
|
||||
|
||||
```mermaid
|
||||
flowchart LR
|
||||
|
||||
subgraph Node A IP 22.1.1.1
|
||||
nodea[EasyTier\n10.144.144.1]
|
||||
end
|
||||
|
||||
subgraph Node B
|
||||
nodeb[EasyTier\n10.144.144.2]
|
||||
end
|
||||
|
||||
id1[[10.1.1.0/24]]
|
||||
|
||||
nodea <--> nodeb <-.-> id1
|
||||
|
||||
```
|
||||
|
||||
Then the startup parameters for Node B's easytier are (new -n parameter)
|
||||
|
||||
```sh
|
||||
sudo easytier-core --ipv4 10.144.144.2 -n 10.1.1.0/24
|
||||
```
|
||||
|
||||
Subnet proxy information will automatically sync to each node in the virtual network, and each node will automatically configure the corresponding route. Node A can check whether the subnet proxy is effective through the following command.
|
||||
|
||||
1. Check whether the routing information has been synchronized, the proxy_cidrs column shows the proxied subnets.
|
||||
|
||||
```sh
|
||||
easytier-cli route
|
||||
```
|
||||

|
||||
|
||||
|
||||

|
||||
|
||||
2. Test whether Node A can access nodes under the proxied subnet
|
||||
|
||||
|
||||
```sh
|
||||
ping 10.1.1.2
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
### Networking without Public IP
|
||||
|
||||
EasyTier supports networking using shared public nodes. The currently deployed shared public node is ``tcp://easytier.public.kkrainbow.top:11010``.
|
||||
|
||||
When using shared nodes, each node entering the network needs to provide the same ``--network-name`` and ``--network-secret`` parameters as the unique identifier of the network.
|
||||
|
||||
Taking two nodes as an example, Node A executes:
|
||||
|
||||
```sh
|
||||
sudo easytier-core -i 10.144.144.1 --network-name abc --network-secret abc -e tcp://easytier.public.kkrainbow.top:11010
|
||||
```
|
||||
|
||||
Node B executes
|
||||
|
||||
```sh
|
||||
sudo easytier-core --ipv4 10.144.144.2 --network-name abc --network-secret abc -e tcp://easytier.public.kkrainbow.top:11010
|
||||
```
|
||||
|
||||
After the command is successfully executed, Node A can access Node B through the virtual IP 10.144.144.2.
|
||||
|
||||
### Use EasyTier with WireGuard Client
|
||||
|
||||
EasyTier can be used as a WireGuard server to allow any device with WireGuard client installed to access the EasyTier network. For platforms currently unsupported by EasyTier (such as iOS, Android, etc.), this method can be used to connect to the EasyTier network.
|
||||
---
|
||||
|
||||
### Networking without Public IP
|
||||
|
||||
EasyTier supports networking using shared public nodes. The currently deployed shared public node is ``tcp://public.easytier.top:11010``.
|
||||
|
||||
When using shared nodes, each node entering the network needs to provide the same ``--network-name`` and ``--network-secret`` parameters as the unique identifier of the network.
|
||||
|
||||
Taking two nodes as an example, Node A executes:
|
||||
|
||||
```sh
|
||||
sudo easytier-core -i 10.144.144.1 --network-name abc --network-secret abc -e tcp://public.easytier.top:11010
|
||||
```
|
||||
|
||||
Node B executes
|
||||
|
||||
```sh
|
||||
sudo easytier-core --ipv4 10.144.144.2 --network-name abc --network-secret abc -e tcp://public.easytier.top:11010
|
||||
```
|
||||
|
||||
After the command is successfully executed, Node A can access Node B through the virtual IP 10.144.144.2.
|
||||
|
||||
### Use EasyTier with WireGuard Client
|
||||
|
||||
EasyTier can be used as a WireGuard server to allow any device with WireGuard client installed to access the EasyTier network. For platforms currently unsupported by EasyTier (such as iOS, Android, etc.), this method can be used to connect to the EasyTier network.
|
||||
|
||||
Assuming the network topology is as follows:
|
||||
|
||||
@@ -221,14 +246,14 @@ To enable an iPhone to access the EasyTier network through Node A, the following
|
||||
|
||||
Include the --vpn-portal parameter in the easytier-core command on Node A to specify the port that the WireGuard service listens on and the subnet used by the WireGuard network.
|
||||
|
||||
```
|
||||
```sh
|
||||
# The following parameters mean: listen on port 0.0.0.0:11013, and use the 10.14.14.0/24 subnet for WireGuard
|
||||
sudo easytier-core --ipv4 10.144.144.1 --vpn-portal wg://0.0.0.0:11013/10.14.14.0/24
|
||||
```
|
||||
|
||||
After successfully starting easytier-core, use easytier-cli to obtain the WireGuard client configuration.
|
||||
|
||||
```
|
||||
```sh
|
||||
$> easytier-cli vpn-portal
|
||||
portal_name: wireguard
|
||||
|
||||
@@ -252,45 +277,53 @@ connected_clients:
|
||||
|
||||
Before using the Client Config, you need to modify the Interface Address and Peer Endpoint to the client's IP and the IP of the EasyTier node, respectively. Import the configuration file into the WireGuard client to access the EasyTier network.
|
||||
|
||||
# Self-Hosted Public Server
|
||||
### Self-Hosted Public Server
|
||||
|
||||
Each node can act as a relay node for other users' networks. Simply start EasyTier without any parameters.
|
||||
|
||||
### Configurations
|
||||
|
||||
You can use ``easytier-core --help`` to view all configuration items
|
||||
|
||||
|
||||
# Roadmap
|
||||
|
||||
- [ ] Improve documentation and user guides.
|
||||
- [ ] Support features such as encryption, TCP hole punching, etc.
|
||||
- [ ] Support Android, IOS and other mobile platforms.
|
||||
- [ ] Support Web configuration management.
|
||||
|
||||
# Community and Contribution
|
||||
|
||||
We welcome and encourage community contributions! If you want to get involved, please submit a [GitHub PR](https://github.com/EasyTier/EasyTier/pulls). Detailed contribution guidelines can be found in [CONTRIBUTING.md](https://github.com/EasyTier/EasyTier/blob/main/CONTRIBUTING.md).
|
||||
|
||||
# Related Projects and Resources
|
||||
|
||||
- [ZeroTier](https://www.zerotier.com/): A global virtual network for connecting devices.
|
||||
- [TailScale](https://tailscale.com/): A VPN solution aimed at simplifying network configuration.
|
||||
- [vpncloud](https://github.com/dswd/vpncloud): A P2P Mesh VPN
|
||||
- [Candy](https://github.com/lanthora/candy): A reliable, low-latency, and anti-censorship virtual private network
|
||||
|
||||
# License
|
||||
|
||||
EasyTier is released under the [Apache License 2.0](https://github.com/EasyTier/EasyTier/blob/main/LICENSE).
|
||||
|
||||
# Contact
|
||||
|
||||
- Ask questions or report problems: [GitHub Issues](https://github.com/EasyTier/EasyTier/issues)
|
||||
- Discussion and exchange: [GitHub Discussions](https://github.com/EasyTier/EasyTier/discussions)
|
||||
- Telegram:https://t.me/easytier
|
||||
- QQ Group: 949700262
|
||||
Every virtual network (with same network name and secret) can act as a public server cluster. Nodes of other network can connect to arbitrary nodes in public server cluster to discover each other without public IP.
|
||||
|
||||
# Sponsor
|
||||
Run you own public server cluster is exactly same as running an virtual network, except that you can skip config the ipv4 addr.
|
||||
|
||||
You can also join the official public server cluster with following command:
|
||||
|
||||
```
|
||||
sudo easytier-core --network-name easytier --network-secret easytier -p tcp://public.easytier.top:11010
|
||||
```
|
||||
|
||||
|
||||
### Configurations
|
||||
|
||||
You can use ``easytier-core --help`` to view all configuration items
|
||||
|
||||
## Roadmap
|
||||
|
||||
- [ ] Improve documentation and user guides.
|
||||
- [ ] Support features such as encryption, TCP hole punching, etc.
|
||||
- [ ] Support iOS.
|
||||
- [ ] Support Web configuration management.
|
||||
|
||||
## Community and Contribution
|
||||
|
||||
We welcome and encourage community contributions! If you want to get involved, please submit a [GitHub PR](https://github.com/EasyTier/EasyTier/pulls). Detailed contribution guidelines can be found in [CONTRIBUTING.md](https://github.com/EasyTier/EasyTier/blob/main/CONTRIBUTING.md).
|
||||
|
||||
## Related Projects and Resources
|
||||
|
||||
- [ZeroTier](https://www.zerotier.com/): A global virtual network for connecting devices.
|
||||
- [TailScale](https://tailscale.com/): A VPN solution aimed at simplifying network configuration.
|
||||
- [vpncloud](https://github.com/dswd/vpncloud): A P2P Mesh VPN
|
||||
- [Candy](https://github.com/lanthora/candy): A reliable, low-latency, and anti-censorship virtual private network
|
||||
|
||||
## License
|
||||
|
||||
EasyTier is released under the [Apache License 2.0](https://github.com/EasyTier/EasyTier/blob/main/LICENSE).
|
||||
|
||||
## Contact
|
||||
|
||||
- Ask questions or report problems: [GitHub Issues](https://github.com/EasyTier/EasyTier/issues)
|
||||
- Discussion and exchange: [GitHub Discussions](https://github.com/EasyTier/EasyTier/discussions)
|
||||
- Telegram:https://t.me/easytier
|
||||
- QQ Group: 949700262
|
||||
|
||||
## Sponsor
|
||||
|
||||
<img src="assets/image-8.png" width="300">
|
||||
<img src="assets/image-9.png" width="300">
|
||||
<img src="assets/image-9.png" width="300">
|
||||
|
146
README_CN.md
146
README_CN.md
@@ -22,7 +22,7 @@
|
||||
- **去中心化**:无需依赖中心化服务,节点平等且独立。
|
||||
- **安全**:支持利用 WireGuard 加密通信,也支持 AES-GCM 加密保护中转流量。
|
||||
- **高性能**:全链路零拷贝,性能与主流组网软件相当。
|
||||
- **跨平台**:支持 MacOS/Linux/Windows,未来将支持 IOS 和 Android。可执行文件静态链接,部署简单。
|
||||
- **跨平台**:支持 MacOS/Linux/Windows/Android,未来将支持 IOS。可执行文件静态链接,部署简单。
|
||||
- **无公网 IP 组网**:支持利用共享的公网节点组网,可参考 [配置指南](#无公网IP组网)
|
||||
- **NAT 穿透**:支持基于 UDP 的 NAT 穿透,即使在复杂的网络环境下也能建立稳定的连接。
|
||||
- **子网代理(点对网)**:节点可以将可访问的网段作为代理暴露给 VPN 子网,允许其他节点通过该节点访问这些子网。
|
||||
@@ -39,27 +39,35 @@
|
||||
访问 [GitHub Release 页面](https://github.com/EasyTier/EasyTier/releases) 下载适用于您操作系统的二进制文件。Release 压缩包中同时包含命令行程序和图形界面程序。
|
||||
|
||||
2. **通过 crates.io 安装**
|
||||
```sh
|
||||
cargo install easytier
|
||||
```
|
||||
|
||||
```sh
|
||||
cargo install easytier
|
||||
```
|
||||
|
||||
3. **通过源码安装**
|
||||
```sh
|
||||
cargo install --git https://github.com/EasyTier/EasyTier.git
|
||||
```
|
||||
|
||||
```sh
|
||||
cargo install --git https://github.com/EasyTier/EasyTier.git easytier
|
||||
```
|
||||
|
||||
4. **通过Docker Compose安装**
|
||||
|
||||
请访问 [EasyTier 官网](https://www.easytier.top/) 以查看完整的文档。
|
||||
|
||||
请访问 [EasyTier 官网](https://www.easytier.top/) 以查看完整的文档。
|
||||
|
||||
5. **使用一键脚本安装 (仅适用于 Linux)**
|
||||
```sh
|
||||
wget -O /tmp/easytier.sh "https://raw.githubusercontent.com/EasyTier/EasyTier/main/script/easytier.sh" && bash /tmp/easytier.sh install
|
||||
```
|
||||
|
||||
使用本脚本安装的 Easytier 可以使用脚本的 uninstall/update 对其卸载/升级
|
||||
|
||||
```sh
|
||||
wget -O /tmp/easytier.sh "https://raw.githubusercontent.com/EasyTier/EasyTier/main/script/install.sh" && bash /tmp/easytier.sh install
|
||||
```
|
||||
|
||||
使用本脚本安装的 Easytier 可以使用脚本的 uninstall/update 对其卸载/升级
|
||||
|
||||
6. **使用 Homebrew 安装 (仅适用于 MacOS)**
|
||||
|
||||
```sh
|
||||
brew tap brewforge/chinese
|
||||
brew install --cask easytier
|
||||
```
|
||||
|
||||
## 快速开始
|
||||
|
||||
@@ -87,34 +95,48 @@ nodea <-----> nodeb
|
||||
```
|
||||
|
||||
1. 在节点 A 上执行:
|
||||
```sh
|
||||
sudo easytier-core --ipv4 10.144.144.1
|
||||
```
|
||||
命令执行成功会有如下打印。
|
||||
|
||||

|
||||
```sh
|
||||
sudo easytier-core --ipv4 10.144.144.1
|
||||
```
|
||||
|
||||
命令执行成功会有如下打印。
|
||||
|
||||

|
||||
|
||||
2. 在节点 B 执行
|
||||
```sh
|
||||
sudo easytier-core --ipv4 10.144.144.2 --peers udp://22.1.1.1:11010
|
||||
```
|
||||
|
||||
```sh
|
||||
sudo easytier-core --ipv4 10.144.144.2 --peers udp://22.1.1.1:11010
|
||||
```
|
||||
|
||||
3. 测试联通性
|
||||
|
||||
两个节点应成功连接并能够在虚拟子网内通信
|
||||
```sh
|
||||
ping 10.144.144.2
|
||||
```
|
||||
两个节点应成功连接并能够在虚拟子网内通信
|
||||
|
||||
使用 easytier-cli 查看子网中的节点信息
|
||||
```sh
|
||||
easytier-cli peer
|
||||
```
|
||||

|
||||
```sh
|
||||
easytier-cli route
|
||||
```
|
||||

|
||||
```sh
|
||||
ping 10.144.144.2
|
||||
```
|
||||
|
||||
使用 easytier-cli 查看子网中的节点信息
|
||||
|
||||
```sh
|
||||
easytier-cli peer
|
||||
```
|
||||
|
||||

|
||||
|
||||
```sh
|
||||
easytier-cli route
|
||||
```
|
||||
|
||||

|
||||
|
||||
```sh
|
||||
easytier-cli node
|
||||
```
|
||||
|
||||

|
||||
|
||||
---
|
||||
|
||||
@@ -122,11 +144,11 @@ nodea <-----> nodeb
|
||||
|
||||
基于刚才的双节点组网例子,如果有更多的节点需要加入虚拟网络,可以使用如下命令。
|
||||
|
||||
```
|
||||
```sh
|
||||
sudo easytier-core --ipv4 10.144.144.2 --peers udp://22.1.1.1:11010
|
||||
```
|
||||
|
||||
其中 `--peers ` 参数可以填写任意一个已经在虚拟网络中的节点的监听地址。
|
||||
其中 `--peers` 参数可以填写任意一个已经在虚拟网络中的节点的监听地址。
|
||||
|
||||
---
|
||||
|
||||
@@ -161,35 +183,36 @@ sudo easytier-core --ipv4 10.144.144.2 -n 10.1.1.0/24
|
||||
|
||||
1. 检查路由信息是否已经同步,proxy_cidrs 列展示了被代理的子网。
|
||||
|
||||
```sh
|
||||
easytier-cli route
|
||||
```
|
||||

|
||||
```sh
|
||||
easytier-cli route
|
||||
```
|
||||
|
||||

|
||||
|
||||
2. 测试节点 A 是否可访问被代理子网下的节点
|
||||
|
||||
```sh
|
||||
ping 10.1.1.2
|
||||
```
|
||||
```sh
|
||||
ping 10.1.1.2
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
### 无公网IP组网
|
||||
|
||||
EasyTier 支持共享公网节点进行组网。目前已部署共享的公网节点 ``tcp://easytier.public.kkrainbow.top:11010``。
|
||||
EasyTier 支持共享公网节点进行组网。目前已部署共享的公网节点 ``tcp://public.easytier.top:11010``。
|
||||
|
||||
使用共享节点时,需要每个入网节点提供相同的 ``--network-name`` 和 ``--network-secret`` 参数,作为网络的唯一标识。
|
||||
|
||||
以双节点为例,节点 A 执行:
|
||||
|
||||
```sh
|
||||
sudo easytier-core -i 10.144.144.1 --network-name abc --network-secret abc -e tcp://easytier.public.kkrainbow.top:11010
|
||||
sudo easytier-core -i 10.144.144.1 --network-name abc --network-secret abc -e tcp://public.easytier.top:11010
|
||||
```
|
||||
|
||||
节点 B 执行
|
||||
|
||||
```sh
|
||||
sudo easytier-core --ipv4 10.144.144.2 --network-name abc --network-secret abc -e tcp://easytier.public.kkrainbow.top:11010
|
||||
sudo easytier-core --ipv4 10.144.144.2 --network-name abc --network-secret abc -e tcp://public.easytier.top:11010
|
||||
```
|
||||
|
||||
命令执行成功后,节点 A 即可通过虚拟 IP 10.144.144.2 访问节点 B。
|
||||
@@ -224,14 +247,14 @@ ios <-.-> nodea <--> nodeb <-.-> id1
|
||||
|
||||
在节点 A 的 easytier-core 命令中,加入 --vpn-portal 参数,指定 WireGuard 服务监听的端口,以及 WireGuard 网络使用的网段。
|
||||
|
||||
```
|
||||
```sh
|
||||
# 以下参数的含义为: 监听 0.0.0.0:11013 端口,WireGuard 使用 10.14.14.0/24 网段
|
||||
sudo easytier-core --ipv4 10.144.144.1 --vpn-portal wg://0.0.0.0:11013/10.14.14.0/24
|
||||
```
|
||||
|
||||
easytier-core 启动成功后,使用 easytier-cli 获取 WireGuard Client 的配置。
|
||||
|
||||
```
|
||||
```sh
|
||||
$> easytier-cli vpn-portal
|
||||
portal_name: wireguard
|
||||
|
||||
@@ -259,43 +282,50 @@ connected_clients:
|
||||
|
||||
### 自建公共中转服务器
|
||||
|
||||
每个节点都可作为其他用户网络的中转节点。不带任何参数直接启动 EasyTier 即可。
|
||||
每个虚拟网络(通过相同的网络名称和密钥建链)都可以充当公共服务器集群。其他网络的节点可以连接到公共服务器集群中的任意节点,无需公共 IP 即可发现彼此。
|
||||
|
||||
运行自建的公共服务器集群与运行虚拟网络完全相同,不过可以跳过配置 ipv4 地址。
|
||||
|
||||
也可以使用以下命令加入官方公共服务器集群,后续将实现公共服务器集群的节点间负载均衡:
|
||||
|
||||
```
|
||||
sudo easytier-core --network-name easytier --network-secret easytier -p tcp://public.easytier.top:11010
|
||||
```
|
||||
|
||||
### 其他配置
|
||||
|
||||
可使用 ``easytier-core --help`` 查看全部配置项
|
||||
|
||||
|
||||
# 路线图
|
||||
## 路线图
|
||||
|
||||
- [ ] 完善文档和用户指南。
|
||||
- [ ] 支持 TCP 打洞等特性。
|
||||
- [ ] 支持 Android、IOS 等移动平台。
|
||||
- [ ] 支持 iOS。
|
||||
- [ ] 支持 Web 配置管理。
|
||||
|
||||
# 社区和贡献
|
||||
## 社区和贡献
|
||||
|
||||
我们欢迎并鼓励社区贡献!如果你想参与进来,请提交 [GitHub PR](https://github.com/EasyTier/EasyTier/pulls)。详细的贡献指南可以在 [CONTRIBUTING.md](https://github.com/EasyTier/EasyTier/blob/main/CONTRIBUTING.md) 中找到。
|
||||
|
||||
# 相关项目和资源
|
||||
## 相关项目和资源
|
||||
|
||||
- [ZeroTier](https://www.zerotier.com/): 一个全球虚拟网络,用于连接设备。
|
||||
- [TailScale](https://tailscale.com/): 一个旨在简化网络配置的 VPN 解决方案。
|
||||
- [vpncloud](https://github.com/dswd/vpncloud): 一个 P2P Mesh VPN
|
||||
- [Candy](https://github.com/lanthora/candy): 可靠、低延迟、抗审查的虚拟专用网络
|
||||
|
||||
# 许可证
|
||||
## 许可证
|
||||
|
||||
EasyTier 根据 [Apache License 2.0](https://github.com/EasyTier/EasyTier/blob/main/LICENSE) 许可证发布。
|
||||
|
||||
# 联系方式
|
||||
## 联系方式
|
||||
|
||||
- 提问或报告问题:[GitHub Issues](https://github.com/EasyTier/EasyTier/issues)
|
||||
- 讨论和交流:[GitHub Discussions](https://github.com/EasyTier/EasyTier/discussions)
|
||||
- QQ 群: 949700262
|
||||
- Telegram:https://t.me/easytier
|
||||
|
||||
# 赞助
|
||||
## 赞助
|
||||
|
||||
<img src="assets/image-8.png" width="300">
|
||||
<img src="assets/image-9.png" width="300">
|
||||
|
BIN
assets/image-10.png
Normal file
BIN
assets/image-10.png
Normal file
Binary file not shown.
After Width: | Height: | Size: 16 KiB |
@@ -1,2 +0,0 @@
|
||||
shamefully-hoist=true
|
||||
strict-peer-dependencies=false
|
82
easytier-gui/.vscode/settings.json
vendored
82
easytier-gui/.vscode/settings.json
vendored
@@ -1,5 +1,81 @@
|
||||
{
|
||||
"i18n-ally.localesPaths": [
|
||||
"locales"
|
||||
"cSpell.words": [
|
||||
"easytier",
|
||||
"Vite",
|
||||
"vueuse",
|
||||
"pinia",
|
||||
"demi",
|
||||
"antfu",
|
||||
"iconify",
|
||||
"intlify",
|
||||
"vitejs",
|
||||
"unplugin",
|
||||
"pnpm"
|
||||
],
|
||||
"i18n-ally.localesPaths": "locales",
|
||||
"editor.formatOnSave": false,
|
||||
// Auto fix
|
||||
"editor.codeActionsOnSave": {
|
||||
"source.fixAll.eslint": "explicit",
|
||||
"source.organizeImports": "never"
|
||||
},
|
||||
// Silent the stylistic rules in you IDE, but still auto fix them
|
||||
"eslint.rules.customizations": [
|
||||
{
|
||||
"rule": "style/*",
|
||||
"severity": "off"
|
||||
},
|
||||
{
|
||||
"rule": "format/*",
|
||||
"severity": "off"
|
||||
},
|
||||
{
|
||||
"rule": "*-indent",
|
||||
"severity": "off"
|
||||
},
|
||||
{
|
||||
"rule": "*-spacing",
|
||||
"severity": "off"
|
||||
},
|
||||
{
|
||||
"rule": "*-spaces",
|
||||
"severity": "off"
|
||||
},
|
||||
{
|
||||
"rule": "*-order",
|
||||
"severity": "off"
|
||||
},
|
||||
{
|
||||
"rule": "*-dangle",
|
||||
"severity": "off"
|
||||
},
|
||||
{
|
||||
"rule": "*-newline",
|
||||
"severity": "off"
|
||||
},
|
||||
{
|
||||
"rule": "*quotes",
|
||||
"severity": "off"
|
||||
},
|
||||
{
|
||||
"rule": "*semi",
|
||||
"severity": "off"
|
||||
}
|
||||
],
|
||||
// The following is optional.
|
||||
// It's better to put under project setting `.vscode/settings.json`
|
||||
// to avoid conflicts with working with different eslint configs
|
||||
// that does not support all formats.
|
||||
"eslint.validate": [
|
||||
"javascript",
|
||||
"javascriptreact",
|
||||
"typescript",
|
||||
"typescriptreact",
|
||||
"vue",
|
||||
"html",
|
||||
"markdown",
|
||||
"json",
|
||||
"jsonc",
|
||||
"yaml"
|
||||
]
|
||||
}
|
||||
}
|
@@ -14,6 +14,11 @@ npm install -g pnpm
|
||||
### For Desktop (Win/Mac/Linux)
|
||||
|
||||
```
|
||||
cd ../tauri-plugin-vpnservice
|
||||
pnpm install
|
||||
pnpm build
|
||||
|
||||
cd ../easytier-gui
|
||||
pnpm install
|
||||
pnpm tauri build
|
||||
```
|
||||
@@ -34,7 +39,6 @@ rustup target add aarch64-linux-android
|
||||
install java 20
|
||||
```
|
||||
|
||||
|
||||
Java version depend on gradle version specified in (easytier-gui\src-tauri\gen\android\build.gradle.kts)
|
||||
|
||||
See [Gradle compatibility matrix](https://docs.gradle.org/current/userguide/compatibility.html) for detail .
|
||||
@@ -43,4 +47,4 @@ See [Gradle compatibility matrix](https://docs.gradle.org/current/userguide/comp
|
||||
pnpm install
|
||||
pnpm tauri android init
|
||||
pnpm tauri android build
|
||||
```
|
||||
```
|
||||
|
@@ -13,6 +13,7 @@ proxy_cidrs: 子网代理CIDR
|
||||
enable_vpn_portal: 启用VPN门户
|
||||
vpn_portal_listen_port: 监听端口
|
||||
vpn_portal_client_network: 客户端子网
|
||||
dev_name: TUN接口名称
|
||||
advanced_settings: 高级设置
|
||||
basic_settings: 基础设置
|
||||
listener_urls: 监听地址
|
||||
@@ -45,11 +46,13 @@ enable_auto_launch: 开启开机自启
|
||||
exit: 退出
|
||||
chips_placeholder: 例如: {0}, 按回车添加
|
||||
hostname_placeholder: '留空默认为主机名: {0}'
|
||||
dev_name_placeholder: 注意:当多个网络同时使用相同的TUN接口名称时,将会在设置TUN的IP时产生冲突,留空以自动生成随机名称
|
||||
off_text: 点击关闭
|
||||
on_text: 点击开启
|
||||
show_config: 显示配置
|
||||
close: 关闭
|
||||
|
||||
use_latency_first: 延迟优先模式
|
||||
my_node_info: 当前节点信息
|
||||
peer_count: 已连接
|
||||
upload: 上传
|
||||
@@ -66,6 +69,12 @@ upload_bytes: 上传
|
||||
download_bytes: 下载
|
||||
loss_rate: 丢包率
|
||||
|
||||
status:
|
||||
version: 内核版本
|
||||
local: 本机
|
||||
server: 服务器
|
||||
relay: 中继
|
||||
|
||||
run_network: 运行网络
|
||||
stop_network: 停止网络
|
||||
network_running: 运行中
|
||||
@@ -75,3 +84,32 @@ dhcp_experimental_warning: 实验性警告!使用DHCP时如果组网环境中
|
||||
tray:
|
||||
show: 显示 / 隐藏
|
||||
exit: 退出
|
||||
|
||||
about:
|
||||
title: 关于
|
||||
version: 版本
|
||||
author: 作者
|
||||
homepage: 主页
|
||||
license: 许可证
|
||||
description: 一个简单、安全、去中心化的内网穿透 VPN 组网方案,使用 Rust 语言和 Tokio 框架实现。
|
||||
check_update: 检查更新
|
||||
|
||||
event:
|
||||
Unknown: 未知
|
||||
TunDeviceReady: Tun设备就绪
|
||||
TunDeviceError: Tun设备错误
|
||||
PeerAdded: 对端添加
|
||||
PeerRemoved: 对端移除
|
||||
PeerConnAdded: 对端连接添加
|
||||
PeerConnRemoved: 对端连接移除
|
||||
ListenerAdded: 监听器添加
|
||||
ListenerAddFailed: 监听器添加失败
|
||||
ListenerAcceptFailed: 监听器接受连接失败
|
||||
ConnectionAccepted: 连接已接受
|
||||
ConnectionError: 连接错误
|
||||
Connecting: 正在连接
|
||||
ConnectError: 连接错误
|
||||
VpnPortalClientConnected: VPN门户客户端已连接
|
||||
VpnPortalClientDisconnected: VPN门户客户端已断开连接
|
||||
DhcpIpv4Changed: DHCP IPv4地址更改
|
||||
DhcpIpv4Conflicted: DHCP IPv4地址冲突
|
||||
|
@@ -13,6 +13,7 @@ proxy_cidrs: Subnet Proxy CIDRs
|
||||
enable_vpn_portal: Enable VPN Portal
|
||||
vpn_portal_listen_port: VPN Portal Listen Port
|
||||
vpn_portal_client_network: Client Sub Network
|
||||
dev_name: TUN interface name
|
||||
advanced_settings: Advanced Settings
|
||||
basic_settings: Basic Settings
|
||||
listener_urls: Listener URLs
|
||||
@@ -43,9 +44,10 @@ logging_copy_dir: Copy Log Path
|
||||
disable_auto_launch: Disable Launch on Reboot
|
||||
enable_auto_launch: Enable Launch on Reboot
|
||||
exit: Exit
|
||||
|
||||
use_latency_first: Latency First Mode
|
||||
chips_placeholder: 'e.g: {0}, press Enter to add'
|
||||
hostname_placeholder: 'Leave blank and default to host name: {0}'
|
||||
dev_name_placeholder: 'Note: When multiple networks use the same TUN interface name at the same time, there will be a conflict when setting the TUN''s IP. Leave blank to automatically generate a random name.'
|
||||
off_text: Press to disable
|
||||
on_text: Press to enable
|
||||
show_config: Show Config
|
||||
@@ -66,6 +68,12 @@ upload_bytes: Upload
|
||||
download_bytes: Download
|
||||
loss_rate: Loss Rate
|
||||
|
||||
status:
|
||||
version: Version
|
||||
local: Local
|
||||
server: Server
|
||||
relay: Relay
|
||||
|
||||
run_network: Run Network
|
||||
stop_network: Stop Network
|
||||
network_running: running
|
||||
@@ -75,3 +83,32 @@ dhcp_experimental_warning: Experimental warning! if there is an IP conflict in t
|
||||
tray:
|
||||
show: Show / Hide
|
||||
exit: Exit
|
||||
|
||||
about:
|
||||
title: About
|
||||
version: Version
|
||||
author: Author
|
||||
homepage: Homepage
|
||||
license: License
|
||||
description: 'EasyTier is a simple, safe and decentralized VPN networking solution implemented with the Rust language and Tokio framework.'
|
||||
check_update: Check Update
|
||||
|
||||
event:
|
||||
Unknown: Unknown
|
||||
TunDeviceReady: TunDeviceReady
|
||||
TunDeviceError: TunDeviceError
|
||||
PeerAdded: PeerAdded
|
||||
PeerRemoved: PeerRemoved
|
||||
PeerConnAdded: PeerConnAdded
|
||||
PeerConnRemoved: PeerConnRemoved
|
||||
ListenerAdded: ListenerAdded
|
||||
ListenerAddFailed: ListenerAddFailed
|
||||
ListenerAcceptFailed: ListenerAcceptFailed
|
||||
ConnectionAccepted: ConnectionAccepted
|
||||
ConnectionError: ConnectionError
|
||||
Connecting: Connecting
|
||||
ConnectError: ConnectError
|
||||
VpnPortalClientConnected: VpnPortalClientConnected
|
||||
VpnPortalClientDisconnected: VpnPortalClientDisconnected
|
||||
DhcpIpv4Changed: DhcpIpv4Changed
|
||||
DhcpIpv4Conflicted: DhcpIpv4Conflicted
|
||||
|
@@ -1,8 +1,9 @@
|
||||
{
|
||||
"name": "easytier-gui",
|
||||
"type": "module",
|
||||
"version": "1.2.2",
|
||||
"version": "2.1.0",
|
||||
"private": true,
|
||||
"packageManager": "pnpm@9.12.1+sha512.e5a7e52a4183a02d5931057f7a0dbff9d5e9ce3161e33fa68ae392125b79282a8a8a470a51dfc8a0ed86221442eb2fb57019b0990ed24fab519bf0e1bc5ccfc4",
|
||||
"scripts": {
|
||||
"dev": "vite",
|
||||
"build": "vue-tsc --noEmit && vite build",
|
||||
@@ -12,49 +13,49 @@
|
||||
"lint:fix": "eslint . --ignore-pattern src-tauri --fix"
|
||||
},
|
||||
"dependencies": {
|
||||
"@primevue/themes": "^4.0.4",
|
||||
"@tauri-apps/plugin-clipboard-manager": "2.0.0-rc.0",
|
||||
"@tauri-apps/plugin-os": "2.0.0-rc.0",
|
||||
"@tauri-apps/plugin-process": "2.0.0-rc.0",
|
||||
"@tauri-apps/plugin-shell": "2.0.0-rc.0",
|
||||
"aura": "link:@primevue/themes/aura",
|
||||
"pinia": "^2.2.1",
|
||||
"primeflex": "^3.3.1",
|
||||
"primeicons": "^7.0.0",
|
||||
"primevue": "^4.0.4",
|
||||
"tauri-plugin-vpnservice-api": "link:../tauri-plugin-vpnservice",
|
||||
"vue": "^3.4.36",
|
||||
"vue-i18n": "^9.13.1",
|
||||
"vue-router": "^4.4.3"
|
||||
"@primevue/themes": "^4.2.1",
|
||||
"@tauri-apps/plugin-autostart": "2.0.0",
|
||||
"@tauri-apps/plugin-clipboard-manager": "2.0.0",
|
||||
"@tauri-apps/plugin-os": "2.0.0",
|
||||
"@tauri-apps/plugin-process": "2.0.0",
|
||||
"@tauri-apps/plugin-shell": "2.0.1",
|
||||
"@vueuse/core": "^11.2.0",
|
||||
"aura": "link:@primevue\\themes\\aura",
|
||||
"easytier-frontend-lib": "workspace:*",
|
||||
"ip-num": "1.5.1",
|
||||
"pinia": "^2.2.4",
|
||||
"primevue": "^4.2.1",
|
||||
"tauri-plugin-vpnservice-api": "workspace:*",
|
||||
"vue": "^3.5.12",
|
||||
"vue-router": "^4.4.5"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@antfu/eslint-config": "^2.24.1",
|
||||
"@intlify/unplugin-vue-i18n": "^4.0.0",
|
||||
"@primevue/auto-import-resolver": "^4.0.4",
|
||||
"@sveltejs/vite-plugin-svelte": "^3.1.1",
|
||||
"@tauri-apps/api": "2.0.0-rc.0",
|
||||
"@tauri-apps/cli": "2.0.0-rc.1",
|
||||
"@types/node": "^20.14.14",
|
||||
"@types/uuid": "^9.0.8",
|
||||
"@vitejs/plugin-vue": "^5.1.2",
|
||||
"@vue-macros/volar": "^0.19.1",
|
||||
"@antfu/eslint-config": "^3.7.3",
|
||||
"@intlify/unplugin-vue-i18n": "^5.2.0",
|
||||
"@primevue/auto-import-resolver": "^4.1.0",
|
||||
"@tauri-apps/api": "2.1.0",
|
||||
"@tauri-apps/cli": "2.1.0",
|
||||
"@types/node": "^22.7.4",
|
||||
"@types/uuid": "^10.0.0",
|
||||
"@vitejs/plugin-vue": "^5.1.4",
|
||||
"@vue-macros/volar": "0.30.5",
|
||||
"autoprefixer": "^10.4.20",
|
||||
"eslint": "^9.8.0",
|
||||
"eslint": "^9.12.0",
|
||||
"eslint-plugin-format": "^0.1.2",
|
||||
"internal-ip": "^8.0.0",
|
||||
"postcss": "^8.4.41",
|
||||
"tailwindcss": "^3.4.7",
|
||||
"typescript": "^5.5.4",
|
||||
"unplugin-auto-import": "^0.17.8",
|
||||
"unplugin-vue-components": "^0.27.3",
|
||||
"unplugin-vue-macros": "^2.11.4",
|
||||
"postcss": "^8.4.47",
|
||||
"tailwindcss": "^3.4.13",
|
||||
"typescript": "^5.6.2",
|
||||
"unplugin-auto-import": "^0.18.3",
|
||||
"unplugin-vue-components": "^0.27.4",
|
||||
"unplugin-vue-macros": "^2.13.3",
|
||||
"unplugin-vue-markdown": "^0.26.2",
|
||||
"unplugin-vue-router": "^0.8.8",
|
||||
"uuid": "^9.0.1",
|
||||
"vite": "^5.3.5",
|
||||
"vite-plugin-vue-devtools": "^7.3.7",
|
||||
"unplugin-vue-router": "^0.10.8",
|
||||
"uuid": "^10.0.0",
|
||||
"vite": "^5.4.8",
|
||||
"vite-plugin-vue-devtools": "^7.4.6",
|
||||
"vite-plugin-vue-layouts": "^0.11.0",
|
||||
"vue-i18n": "^9.13.1",
|
||||
"vue-tsc": "^2.0.29"
|
||||
"vue-i18n": "^10.0.0",
|
||||
"vue-tsc": "^2.1.10"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
4572
easytier-gui/pnpm-lock.yaml
generated
4572
easytier-gui/pnpm-lock.yaml
generated
File diff suppressed because it is too large
Load Diff
@@ -1,4 +0,0 @@
|
||||
[build]
|
||||
target = "x86_64-unknown-linux-gnu"
|
||||
|
||||
[target]
|
@@ -1,6 +1,6 @@
|
||||
[package]
|
||||
name = "easytier-gui"
|
||||
version = "1.2.2"
|
||||
version = "2.1.0"
|
||||
description = "EasyTier GUI"
|
||||
authors = ["you"]
|
||||
edition = "2021"
|
||||
@@ -15,10 +15,11 @@ crate-type = ["staticlib", "cdylib", "rlib"]
|
||||
tauri-build = { version = "2.0.0-rc", features = [] }
|
||||
|
||||
[dependencies]
|
||||
tauri = { version = "2.0.0-rc", features = [
|
||||
tauri = { version = "2.1", features = [
|
||||
"tray-icon",
|
||||
"image-png",
|
||||
"image-ico",
|
||||
"devtools",
|
||||
] }
|
||||
|
||||
serde = { version = "1", features = ["derive"] }
|
||||
@@ -35,17 +36,20 @@ dashmap = "6.0"
|
||||
privilege = "0.3"
|
||||
gethostname = "0.5"
|
||||
|
||||
auto-launch = "0.5.0"
|
||||
dunce = "1.0.4"
|
||||
|
||||
tauri-plugin-shell = "2.0.0-rc"
|
||||
tauri-plugin-process = "2.0.0-rc"
|
||||
tauri-plugin-clipboard-manager = "2.0.0-rc"
|
||||
tauri-plugin-positioner = { version = "2.0.0-rc", features = ["tray-icon"] }
|
||||
tauri-plugin-shell = "2.0"
|
||||
tauri-plugin-process = "2.0"
|
||||
tauri-plugin-clipboard-manager = "2.0"
|
||||
tauri-plugin-positioner = { version = "2.0", features = ["tray-icon"] }
|
||||
tauri-plugin-vpnservice = { path = "../../tauri-plugin-vpnservice" }
|
||||
tauri-plugin-os = "2.0.0-rc"
|
||||
tauri-plugin-os = "2.0"
|
||||
tauri-plugin-autostart = "2.0"
|
||||
|
||||
|
||||
[features]
|
||||
# This feature is used for production builds or when a dev server is not specified, DO NOT REMOVE!!
|
||||
custom-protocol = ["tauri/custom-protocol"]
|
||||
|
||||
[target.'cfg(not(any(target_os = "android", target_os = "ios")))'.dependencies]
|
||||
tauri-plugin-single-instance = "2.0.0-rc.0"
|
||||
|
Binary file not shown.
@@ -1,34 +1,3 @@
|
||||
fn main() {
|
||||
if !cfg!(debug_assertions) && cfg!(target_os = "windows") {
|
||||
let mut windows = tauri_build::WindowsAttributes::new();
|
||||
windows = windows.app_manifest(
|
||||
r#"
|
||||
<assembly xmlns="urn:schemas-microsoft-com:asm.v1" manifestVersion="1.0">
|
||||
<dependency>
|
||||
<dependentAssembly>
|
||||
<assemblyIdentity
|
||||
type="win32"
|
||||
name="Microsoft.Windows.Common-Controls"
|
||||
version="6.0.0.0"
|
||||
processorArchitecture="*"
|
||||
publicKeyToken="6595b64144ccf1df"
|
||||
language="*"
|
||||
/>
|
||||
</dependentAssembly>
|
||||
</dependency>
|
||||
<trustInfo xmlns="urn:schemas-microsoft-com:asm.v3">
|
||||
<security>
|
||||
<requestedPrivileges>
|
||||
<requestedExecutionLevel level="requireAdministrator" uiAccess="false" />
|
||||
</requestedPrivileges>
|
||||
</security>
|
||||
</trustInfo>
|
||||
</assembly>
|
||||
"#,
|
||||
);
|
||||
tauri_build::try_build(tauri_build::Attributes::new().windows_attributes(windows))
|
||||
.expect("failed to run build script");
|
||||
} else {
|
||||
tauri_build::build();
|
||||
}
|
||||
}
|
||||
fn main() {
|
||||
tauri_build::build();
|
||||
}
|
||||
|
@@ -1,4 +1,5 @@
|
||||
{
|
||||
"$schema": "../gen/schemas/desktop-schema.json",
|
||||
"identifier": "migrated",
|
||||
"description": "permissions that were migrated from v1",
|
||||
"local": true,
|
||||
@@ -13,6 +14,7 @@
|
||||
"core:window:allow-show",
|
||||
"core:window:allow-hide",
|
||||
"core:window:allow-set-focus",
|
||||
"core:window:allow-set-title",
|
||||
"core:app:default",
|
||||
"core:resources:default",
|
||||
"core:menu:default",
|
||||
@@ -24,7 +26,6 @@
|
||||
"shell:default",
|
||||
"process:default",
|
||||
"clipboard-manager:default",
|
||||
"core:tray:default",
|
||||
"core:tray:allow-new",
|
||||
"core:tray:allow-set-menu",
|
||||
"core:tray:allow-set-title",
|
||||
@@ -44,6 +45,10 @@
|
||||
"os:allow-arch",
|
||||
"os:allow-hostname",
|
||||
"os:allow-platform",
|
||||
"os:allow-locale"
|
||||
"os:allow-locale",
|
||||
"autostart:default",
|
||||
"autostart:allow-disable",
|
||||
"autostart:allow-enable",
|
||||
"autostart:allow-is-enabled"
|
||||
]
|
||||
}
|
@@ -3,175 +3,38 @@
|
||||
|
||||
use std::collections::BTreeMap;
|
||||
|
||||
use anyhow::Context;
|
||||
#[cfg(not(target_os = "android"))]
|
||||
use auto_launch::AutoLaunchBuilder;
|
||||
use dashmap::DashMap;
|
||||
use easytier::{
|
||||
common::config::{
|
||||
ConfigLoader, FileLoggerConfig, NetworkIdentity, PeerConfig, TomlConfigLoader,
|
||||
VpnPortalConfig,
|
||||
},
|
||||
launcher::{NetworkInstance, NetworkInstanceRunningInfo},
|
||||
common::config::{ConfigLoader, FileLoggerConfig, TomlConfigLoader},
|
||||
launcher::{NetworkConfig, NetworkInstance, NetworkInstanceRunningInfo},
|
||||
utils::{self, NewFilterSender},
|
||||
};
|
||||
use serde::{Deserialize, Serialize};
|
||||
|
||||
use tauri::Manager as _;
|
||||
|
||||
|
||||
#[derive(Deserialize, Serialize, PartialEq, Debug)]
|
||||
enum NetworkingMethod {
|
||||
PublicServer,
|
||||
Manual,
|
||||
Standalone,
|
||||
}
|
||||
|
||||
impl Default for NetworkingMethod {
|
||||
fn default() -> Self {
|
||||
NetworkingMethod::PublicServer
|
||||
}
|
||||
}
|
||||
pub const AUTOSTART_ARG: &str = "--autostart";
|
||||
|
||||
#[cfg(not(target_os = "android"))]
|
||||
use tauri::tray::{MouseButton, MouseButtonState, TrayIconBuilder, TrayIconEvent};
|
||||
|
||||
#[derive(Deserialize, Serialize, Debug, Default)]
|
||||
struct NetworkConfig {
|
||||
instance_id: String,
|
||||
|
||||
dhcp: bool,
|
||||
virtual_ipv4: String,
|
||||
hostname: Option<String>,
|
||||
network_name: String,
|
||||
network_secret: String,
|
||||
networking_method: NetworkingMethod,
|
||||
|
||||
public_server_url: String,
|
||||
peer_urls: Vec<String>,
|
||||
|
||||
proxy_cidrs: Vec<String>,
|
||||
|
||||
enable_vpn_portal: bool,
|
||||
vpn_portal_listen_port: i32,
|
||||
vpn_portal_client_network_addr: String,
|
||||
vpn_portal_client_network_len: i32,
|
||||
|
||||
advanced_settings: bool,
|
||||
|
||||
listener_urls: Vec<String>,
|
||||
rpc_port: i32,
|
||||
}
|
||||
|
||||
impl NetworkConfig {
|
||||
fn gen_config(&self) -> Result<TomlConfigLoader, anyhow::Error> {
|
||||
let cfg = TomlConfigLoader::default();
|
||||
cfg.set_id(
|
||||
self.instance_id
|
||||
.parse()
|
||||
.with_context(|| format!("failed to parse instance id: {}", self.instance_id))?,
|
||||
);
|
||||
cfg.set_hostname(self.hostname.clone());
|
||||
cfg.set_dhcp(self.dhcp);
|
||||
cfg.set_inst_name(self.network_name.clone());
|
||||
cfg.set_network_identity(NetworkIdentity::new(
|
||||
self.network_name.clone(),
|
||||
self.network_secret.clone(),
|
||||
));
|
||||
|
||||
if !self.dhcp {
|
||||
if self.virtual_ipv4.len() > 0 {
|
||||
cfg.set_ipv4(Some(self.virtual_ipv4.parse().with_context(|| {
|
||||
format!("failed to parse ipv4 address: {}", self.virtual_ipv4)
|
||||
})?))
|
||||
}
|
||||
}
|
||||
|
||||
match self.networking_method {
|
||||
NetworkingMethod::PublicServer => {
|
||||
cfg.set_peers(vec![PeerConfig {
|
||||
uri: self.public_server_url.parse().with_context(|| {
|
||||
format!(
|
||||
"failed to parse public server uri: {}",
|
||||
self.public_server_url
|
||||
)
|
||||
})?,
|
||||
}]);
|
||||
}
|
||||
NetworkingMethod::Manual => {
|
||||
let mut peers = vec![];
|
||||
for peer_url in self.peer_urls.iter() {
|
||||
if peer_url.is_empty() {
|
||||
continue;
|
||||
}
|
||||
peers.push(PeerConfig {
|
||||
uri: peer_url
|
||||
.parse()
|
||||
.with_context(|| format!("failed to parse peer uri: {}", peer_url))?,
|
||||
});
|
||||
}
|
||||
|
||||
cfg.set_peers(peers);
|
||||
}
|
||||
NetworkingMethod::Standalone => {}
|
||||
}
|
||||
|
||||
let mut listener_urls = vec![];
|
||||
for listener_url in self.listener_urls.iter() {
|
||||
if listener_url.is_empty() {
|
||||
continue;
|
||||
}
|
||||
listener_urls.push(
|
||||
listener_url
|
||||
.parse()
|
||||
.with_context(|| format!("failed to parse listener uri: {}", listener_url))?,
|
||||
);
|
||||
}
|
||||
cfg.set_listeners(listener_urls);
|
||||
|
||||
for n in self.proxy_cidrs.iter() {
|
||||
cfg.add_proxy_cidr(
|
||||
n.parse()
|
||||
.with_context(|| format!("failed to parse proxy network: {}", n))?,
|
||||
);
|
||||
}
|
||||
|
||||
cfg.set_rpc_portal(
|
||||
format!("127.0.0.1:{}", self.rpc_port)
|
||||
.parse()
|
||||
.with_context(|| format!("failed to parse rpc portal port: {}", self.rpc_port))?,
|
||||
);
|
||||
|
||||
if self.enable_vpn_portal {
|
||||
let cidr = format!(
|
||||
"{}/{}",
|
||||
self.vpn_portal_client_network_addr, self.vpn_portal_client_network_len
|
||||
);
|
||||
cfg.set_vpn_portal_config(VpnPortalConfig {
|
||||
client_cidr: cidr
|
||||
.parse()
|
||||
.with_context(|| format!("failed to parse vpn portal client cidr: {}", cidr))?,
|
||||
wireguard_listen: format!("0.0.0.0:{}", self.vpn_portal_listen_port)
|
||||
.parse()
|
||||
.with_context(|| {
|
||||
format!(
|
||||
"failed to parse vpn portal wireguard listen port. {}",
|
||||
self.vpn_portal_listen_port
|
||||
)
|
||||
})?,
|
||||
});
|
||||
}
|
||||
|
||||
Ok(cfg)
|
||||
}
|
||||
}
|
||||
|
||||
static INSTANCE_MAP: once_cell::sync::Lazy<DashMap<String, NetworkInstance>> =
|
||||
once_cell::sync::Lazy::new(DashMap::new);
|
||||
|
||||
static mut LOGGER_LEVEL_SENDER: once_cell::sync::Lazy<Option<NewFilterSender>> =
|
||||
once_cell::sync::Lazy::new(Default::default);
|
||||
|
||||
#[tauri::command]
|
||||
fn easytier_version() -> Result<String, String> {
|
||||
Ok(easytier::VERSION.to_string())
|
||||
}
|
||||
|
||||
#[tauri::command]
|
||||
fn is_autostart() -> Result<bool, String> {
|
||||
let args: Vec<String> = std::env::args().collect();
|
||||
println!("{:?}", args);
|
||||
Ok(args.contains(&AUTOSTART_ARG.to_owned()))
|
||||
}
|
||||
|
||||
// Learn more about Tauri commands at https://tauri.app/v1/guides/features/command
|
||||
#[tauri::command]
|
||||
fn parse_network_config(cfg: NetworkConfig) -> Result<String, String> {
|
||||
@@ -181,10 +44,10 @@ fn parse_network_config(cfg: NetworkConfig) -> Result<String, String> {
|
||||
|
||||
#[tauri::command]
|
||||
fn run_network_instance(cfg: NetworkConfig) -> Result<(), String> {
|
||||
if INSTANCE_MAP.contains_key(&cfg.instance_id) {
|
||||
if INSTANCE_MAP.contains_key(cfg.instance_id()) {
|
||||
return Err("instance already exists".to_string());
|
||||
}
|
||||
let instance_id = cfg.instance_id.clone();
|
||||
let instance_id = cfg.instance_id().to_string();
|
||||
|
||||
let cfg = cfg.gen_config().map_err(|e| e.to_string())?;
|
||||
let mut instance = NetworkInstance::new(cfg);
|
||||
@@ -224,11 +87,6 @@ fn get_os_hostname() -> Result<String, String> {
|
||||
Ok(gethostname::gethostname().to_string_lossy().to_string())
|
||||
}
|
||||
|
||||
#[tauri::command]
|
||||
fn set_auto_launch_status(app_handle: tauri::AppHandle, enable: bool) -> Result<bool, String> {
|
||||
Ok(init_launch(&app_handle, enable).map_err(|e| e.to_string())?)
|
||||
}
|
||||
|
||||
#[tauri::command]
|
||||
fn set_logging_level(level: String) -> Result<(), String> {
|
||||
let sender = unsafe { LOGGER_LEVEL_SENDER.as_ref().unwrap() };
|
||||
@@ -262,82 +120,19 @@ fn check_sudo() -> bool {
|
||||
use std::env::current_exe;
|
||||
let is_elevated = privilege::user::privileged();
|
||||
if !is_elevated {
|
||||
let Ok(my_exe) = current_exe() else {
|
||||
let Ok(exe) = current_exe() else {
|
||||
return true;
|
||||
};
|
||||
let mut elevated_cmd = privilege::runas::Command::new(my_exe);
|
||||
let _ = elevated_cmd.force_prompt(true).gui(true).run();
|
||||
let args: Vec<String> = std::env::args().collect();
|
||||
let mut elevated_cmd = privilege::runas::Command::new(exe);
|
||||
if args.contains(&AUTOSTART_ARG.to_owned()) {
|
||||
elevated_cmd.arg(AUTOSTART_ARG);
|
||||
}
|
||||
let _ = elevated_cmd.force_prompt(true).hide(true).gui(true).run();
|
||||
}
|
||||
is_elevated
|
||||
}
|
||||
|
||||
#[cfg(target_os = "android")]
|
||||
pub fn init_launch(_app_handle: &tauri::AppHandle, _enable: bool) -> Result<bool, anyhow::Error> {
|
||||
Ok(false)
|
||||
}
|
||||
|
||||
/// init the auto launch
|
||||
#[cfg(not(target_os = "android"))]
|
||||
pub fn init_launch(_app_handle: &tauri::AppHandle, enable: bool) -> Result<bool, anyhow::Error> {
|
||||
use std::env::current_exe;
|
||||
let app_exe = current_exe()?;
|
||||
let app_exe = dunce::canonicalize(app_exe)?;
|
||||
let app_name = app_exe
|
||||
.file_stem()
|
||||
.and_then(|f| f.to_str())
|
||||
.ok_or(anyhow::anyhow!("failed to get file stem"))?;
|
||||
|
||||
let app_path = app_exe
|
||||
.as_os_str()
|
||||
.to_str()
|
||||
.ok_or(anyhow::anyhow!("failed to get app_path"))?
|
||||
.to_string();
|
||||
|
||||
#[cfg(target_os = "windows")]
|
||||
let app_path = format!("\"{app_path}\"");
|
||||
|
||||
// use the /Applications/easytier-gui.app
|
||||
#[cfg(target_os = "macos")]
|
||||
let app_path = (|| -> Option<String> {
|
||||
let path = std::path::PathBuf::from(&app_path);
|
||||
let path = path.parent()?.parent()?.parent()?;
|
||||
let extension = path.extension()?.to_str()?;
|
||||
match extension == "app" {
|
||||
true => Some(path.as_os_str().to_str()?.to_string()),
|
||||
false => None,
|
||||
}
|
||||
})()
|
||||
.unwrap_or(app_path);
|
||||
|
||||
#[cfg(target_os = "linux")]
|
||||
let app_path = {
|
||||
let appimage = _app_handle.env().appimage;
|
||||
appimage
|
||||
.and_then(|p| p.to_str().map(|s| s.to_string()))
|
||||
.unwrap_or(app_path)
|
||||
};
|
||||
|
||||
let auto = AutoLaunchBuilder::new()
|
||||
.set_app_name(app_name)
|
||||
.set_app_path(&app_path)
|
||||
.build()
|
||||
.with_context(|| "failed to build auto launch")?;
|
||||
|
||||
if enable && !auto.is_enabled().unwrap_or(false) {
|
||||
// 避免重复设置登录项
|
||||
let _ = auto.disable();
|
||||
auto.enable()
|
||||
.with_context(|| "failed to enable auto launch")?
|
||||
} else if !enable {
|
||||
let _ = auto.disable();
|
||||
}
|
||||
|
||||
let enabled = auto.is_enabled()?;
|
||||
|
||||
Ok(enabled)
|
||||
}
|
||||
|
||||
|
||||
#[cfg_attr(mobile, tauri::mobile_entry_point)]
|
||||
pub fn run() {
|
||||
#[cfg(not(target_os = "android"))]
|
||||
@@ -345,12 +140,41 @@ pub fn run() {
|
||||
use std::process;
|
||||
process::exit(0);
|
||||
}
|
||||
tauri::Builder::default()
|
||||
|
||||
#[cfg(not(target_os = "android"))]
|
||||
utils::setup_panic_handler();
|
||||
|
||||
let mut builder = tauri::Builder::default();
|
||||
|
||||
#[cfg(not(target_os = "android"))]
|
||||
{
|
||||
use tauri_plugin_autostart::MacosLauncher;
|
||||
builder = builder.plugin(tauri_plugin_autostart::init(
|
||||
MacosLauncher::LaunchAgent,
|
||||
Some(vec![AUTOSTART_ARG]),
|
||||
));
|
||||
}
|
||||
|
||||
#[cfg(not(any(target_os = "android", target_os = "ios")))]
|
||||
{
|
||||
builder = builder.plugin(tauri_plugin_single_instance::init(|app, _args, _cwd| {
|
||||
app.webview_windows()
|
||||
.values()
|
||||
.next()
|
||||
.expect("Sorry, no window found")
|
||||
.set_focus()
|
||||
.expect("Can't Bring Window to Focus");
|
||||
}));
|
||||
}
|
||||
|
||||
builder = builder
|
||||
.plugin(tauri_plugin_os::init())
|
||||
.plugin(tauri_plugin_clipboard_manager::init())
|
||||
.plugin(tauri_plugin_process::init())
|
||||
.plugin(tauri_plugin_shell::init())
|
||||
.plugin(tauri_plugin_vpnservice::init())
|
||||
.plugin(tauri_plugin_vpnservice::init());
|
||||
|
||||
let app = builder
|
||||
.setup(|app| {
|
||||
// for logging config
|
||||
let Ok(log_dir) = app.path().app_log_dir() else {
|
||||
@@ -382,7 +206,7 @@ pub fn run() {
|
||||
toggle_window_visibility(app);
|
||||
}
|
||||
})
|
||||
.icon(tauri::image::Image::from_bytes(include_bytes!(
|
||||
.icon(tauri::image::Image::from_bytes(include_bytes!(
|
||||
"../icons/icon.png"
|
||||
))?)
|
||||
.icon_as_template(false)
|
||||
@@ -396,9 +220,10 @@ pub fn run() {
|
||||
retain_network_instance,
|
||||
collect_network_infos,
|
||||
get_os_hostname,
|
||||
set_auto_launch_status,
|
||||
set_logging_level,
|
||||
set_tun_fd
|
||||
set_tun_fd,
|
||||
is_autostart,
|
||||
easytier_version
|
||||
])
|
||||
.on_window_event(|_win, event| match event {
|
||||
#[cfg(not(target_os = "android"))]
|
||||
@@ -408,6 +233,20 @@ pub fn run() {
|
||||
}
|
||||
_ => {}
|
||||
})
|
||||
.run(tauri::generate_context!())
|
||||
.expect("error while running tauri application");
|
||||
.build(tauri::generate_context!())
|
||||
.unwrap();
|
||||
|
||||
#[cfg(not(target_os = "macos"))]
|
||||
app.run(|_app, _event| {});
|
||||
|
||||
#[cfg(target_os = "macos")]
|
||||
{
|
||||
use tauri::RunEvent;
|
||||
app.run(|app, event| match event {
|
||||
RunEvent::Reopen { .. } => {
|
||||
toggle_window_visibility(app);
|
||||
}
|
||||
_ => {}
|
||||
});
|
||||
}
|
||||
}
|
||||
|
@@ -17,7 +17,7 @@
|
||||
"createUpdaterArtifacts": false
|
||||
},
|
||||
"productName": "easytier-gui",
|
||||
"version": "1.2.2",
|
||||
"version": "2.1.0",
|
||||
"identifier": "com.kkrainbow.easytier",
|
||||
"plugins": {},
|
||||
"app": {
|
||||
|
Binary file not shown.
@@ -1,3 +1,12 @@
|
||||
<script setup lang="ts">
|
||||
import { getCurrentWindow } from '@tauri-apps/api/window'
|
||||
import pkg from '~/../package.json'
|
||||
|
||||
onBeforeMount(async () => {
|
||||
await getCurrentWindow().setTitle(`Easytier GUI: v${pkg.version}`)
|
||||
})
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<RouterView />
|
||||
</template>
|
||||
|
38
easytier-gui/src/auto-imports.d.ts
vendored
38
easytier-gui/src/auto-imports.d.ts
vendored
@@ -3,6 +3,7 @@
|
||||
// @ts-nocheck
|
||||
// noinspection JSUnusedGlobalSymbols
|
||||
// Generated by unplugin-auto-import
|
||||
// biome-ignore lint: disable
|
||||
export {}
|
||||
declare global {
|
||||
const EffectScope: typeof import('vue')['EffectScope']
|
||||
@@ -20,15 +21,18 @@ declare global {
|
||||
const definePage: typeof import('unplugin-vue-router/runtime')['definePage']
|
||||
const defineStore: typeof import('pinia')['defineStore']
|
||||
const effectScope: typeof import('vue')['effectScope']
|
||||
const event2human: typeof import('./composables/utils')['event2human']
|
||||
const generateMenuItem: typeof import('./composables/tray')['generateMenuItem']
|
||||
const getActivePinia: typeof import('pinia')['getActivePinia']
|
||||
const getCurrentInstance: typeof import('vue')['getCurrentInstance']
|
||||
const getCurrentScope: typeof import('vue')['getCurrentScope']
|
||||
const getEasytierVersion: typeof import('./composables/network')['getEasytierVersion']
|
||||
const getOsHostname: typeof import('./composables/network')['getOsHostname']
|
||||
const h: typeof import('vue')['h']
|
||||
const initMobileService: typeof import('./composables/mobile_vpn')['initMobileService']
|
||||
const initMobileVpnService: typeof import('./composables/mobile_vpn')['initMobileVpnService']
|
||||
const inject: typeof import('vue')['inject']
|
||||
const isAutostart: typeof import('./composables/network')['isAutostart']
|
||||
const isProxy: typeof import('vue')['isProxy']
|
||||
const isReactive: typeof import('vue')['isReactive']
|
||||
const isReadonly: typeof import('vue')['isReadonly']
|
||||
@@ -41,10 +45,12 @@ declare global {
|
||||
const mapWritableState: typeof import('pinia')['mapWritableState']
|
||||
const markRaw: typeof import('vue')['markRaw']
|
||||
const nextTick: typeof import('vue')['nextTick']
|
||||
const num2ipv4: typeof import('./composables/utils')['num2ipv4']
|
||||
const num2ipv6: typeof import('./composables/utils')['num2ipv6']
|
||||
const onActivated: typeof import('vue')['onActivated']
|
||||
const onBeforeMount: typeof import('vue')['onBeforeMount']
|
||||
const onBeforeRouteLeave: typeof import('vue-router/auto')['onBeforeRouteLeave']
|
||||
const onBeforeRouteUpdate: typeof import('vue-router/auto')['onBeforeRouteUpdate']
|
||||
const onBeforeRouteLeave: typeof import('vue-router')['onBeforeRouteLeave']
|
||||
const onBeforeRouteUpdate: typeof import('vue-router')['onBeforeRouteUpdate']
|
||||
const onBeforeUnmount: typeof import('vue')['onBeforeUnmount']
|
||||
const onBeforeUpdate: typeof import('vue')['onBeforeUpdate']
|
||||
const onDeactivated: typeof import('vue')['onDeactivated']
|
||||
@@ -56,6 +62,7 @@ declare global {
|
||||
const onServerPrefetch: typeof import('vue')['onServerPrefetch']
|
||||
const onUnmounted: typeof import('vue')['onUnmounted']
|
||||
const onUpdated: typeof import('vue')['onUpdated']
|
||||
const onWatcherCleanup: typeof import('vue')['onWatcherCleanup']
|
||||
const parseNetworkConfig: typeof import('./composables/network')['parseNetworkConfig']
|
||||
const prepareVpnService: typeof import('./composables/mobile_vpn')['prepareVpnService']
|
||||
const provide: typeof import('vue')['provide']
|
||||
@@ -77,6 +84,7 @@ declare global {
|
||||
const shallowReadonly: typeof import('vue')['shallowReadonly']
|
||||
const shallowRef: typeof import('vue')['shallowRef']
|
||||
const storeToRefs: typeof import('pinia')['storeToRefs']
|
||||
const timeAgoCn: typeof import('./composables/utils')['timeAgoCn']
|
||||
const toRaw: typeof import('vue')['toRaw']
|
||||
const toRef: typeof import('vue')['toRef']
|
||||
const toRefs: typeof import('vue')['toRefs']
|
||||
@@ -87,11 +95,14 @@ declare global {
|
||||
const useCssModule: typeof import('vue')['useCssModule']
|
||||
const useCssVars: typeof import('vue')['useCssVars']
|
||||
const useI18n: typeof import('vue-i18n')['useI18n']
|
||||
const useId: typeof import('vue')['useId']
|
||||
const useLink: typeof import('vue-router/auto')['useLink']
|
||||
const useModel: typeof import('vue')['useModel']
|
||||
const useNetworkStore: typeof import('./stores/network')['useNetworkStore']
|
||||
const useRoute: typeof import('vue-router/auto')['useRoute']
|
||||
const useRouter: typeof import('vue-router/auto')['useRouter']
|
||||
const useRoute: typeof import('vue-router')['useRoute']
|
||||
const useRouter: typeof import('vue-router')['useRouter']
|
||||
const useSlots: typeof import('vue')['useSlots']
|
||||
const useTemplateRef: typeof import('vue')['useTemplateRef']
|
||||
const useTray: typeof import('./composables/tray')['useTray']
|
||||
const watch: typeof import('vue')['watch']
|
||||
const watchEffect: typeof import('vue')['watchEffect']
|
||||
@@ -101,7 +112,7 @@ declare global {
|
||||
// for type re-export
|
||||
declare global {
|
||||
// @ts-ignore
|
||||
export type { Component, ComponentPublicInstance, ComputedRef, ExtractDefaultPropTypes, ExtractPropTypes, ExtractPublicPropTypes, InjectionKey, PropType, Ref, VNode, WritableComputedRef } from 'vue'
|
||||
export type { Component, ComponentPublicInstance, ComputedRef, DirectiveBinding, ExtractDefaultPropTypes, ExtractPropTypes, ExtractPublicPropTypes, InjectionKey, PropType, Ref, MaybeRef, MaybeRefOrGetter, VNode, WritableComputedRef } from 'vue'
|
||||
import('vue')
|
||||
}
|
||||
// for vue template auto import
|
||||
@@ -120,22 +131,22 @@ declare module 'vue' {
|
||||
readonly customRef: UnwrapRef<typeof import('vue')['customRef']>
|
||||
readonly defineAsyncComponent: UnwrapRef<typeof import('vue')['defineAsyncComponent']>
|
||||
readonly defineComponent: UnwrapRef<typeof import('vue')['defineComponent']>
|
||||
readonly definePage: UnwrapRef<typeof import('unplugin-vue-router/runtime')['definePage']>
|
||||
readonly defineStore: UnwrapRef<typeof import('pinia')['defineStore']>
|
||||
readonly effectScope: UnwrapRef<typeof import('vue')['effectScope']>
|
||||
readonly generateMenuItem: UnwrapRef<typeof import('./composables/tray')['generateMenuItem']>
|
||||
readonly getActivePinia: UnwrapRef<typeof import('pinia')['getActivePinia']>
|
||||
readonly getCurrentInstance: UnwrapRef<typeof import('vue')['getCurrentInstance']>
|
||||
readonly getCurrentScope: UnwrapRef<typeof import('vue')['getCurrentScope']>
|
||||
readonly getEasytierVersion: UnwrapRef<typeof import('./composables/network')['getEasytierVersion']>
|
||||
readonly getOsHostname: UnwrapRef<typeof import('./composables/network')['getOsHostname']>
|
||||
readonly h: UnwrapRef<typeof import('vue')['h']>
|
||||
readonly initMobileVpnService: UnwrapRef<typeof import('./composables/mobile_vpn')['initMobileVpnService']>
|
||||
readonly inject: UnwrapRef<typeof import('vue')['inject']>
|
||||
readonly isAutostart: UnwrapRef<typeof import('./composables/network')['isAutostart']>
|
||||
readonly isProxy: UnwrapRef<typeof import('vue')['isProxy']>
|
||||
readonly isReactive: UnwrapRef<typeof import('vue')['isReactive']>
|
||||
readonly isReadonly: UnwrapRef<typeof import('vue')['isReadonly']>
|
||||
readonly isRef: UnwrapRef<typeof import('vue')['isRef']>
|
||||
readonly loadRunningInstanceIdsFromLocalStorage: UnwrapRef<typeof import('./stores/network')['loadRunningInstanceIdsFromLocalStorage']>
|
||||
readonly mapActions: UnwrapRef<typeof import('pinia')['mapActions']>
|
||||
readonly mapGetters: UnwrapRef<typeof import('pinia')['mapGetters']>
|
||||
readonly mapState: UnwrapRef<typeof import('pinia')['mapState']>
|
||||
@@ -145,8 +156,8 @@ declare module 'vue' {
|
||||
readonly nextTick: UnwrapRef<typeof import('vue')['nextTick']>
|
||||
readonly onActivated: UnwrapRef<typeof import('vue')['onActivated']>
|
||||
readonly onBeforeMount: UnwrapRef<typeof import('vue')['onBeforeMount']>
|
||||
readonly onBeforeRouteLeave: UnwrapRef<typeof import('vue-router/auto')['onBeforeRouteLeave']>
|
||||
readonly onBeforeRouteUpdate: UnwrapRef<typeof import('vue-router/auto')['onBeforeRouteUpdate']>
|
||||
readonly onBeforeRouteLeave: UnwrapRef<typeof import('vue-router')['onBeforeRouteLeave']>
|
||||
readonly onBeforeRouteUpdate: UnwrapRef<typeof import('vue-router')['onBeforeRouteUpdate']>
|
||||
readonly onBeforeUnmount: UnwrapRef<typeof import('vue')['onBeforeUnmount']>
|
||||
readonly onBeforeUpdate: UnwrapRef<typeof import('vue')['onBeforeUpdate']>
|
||||
readonly onDeactivated: UnwrapRef<typeof import('vue')['onDeactivated']>
|
||||
@@ -158,6 +169,7 @@ declare module 'vue' {
|
||||
readonly onServerPrefetch: UnwrapRef<typeof import('vue')['onServerPrefetch']>
|
||||
readonly onUnmounted: UnwrapRef<typeof import('vue')['onUnmounted']>
|
||||
readonly onUpdated: UnwrapRef<typeof import('vue')['onUpdated']>
|
||||
readonly onWatcherCleanup: UnwrapRef<typeof import('vue')['onWatcherCleanup']>
|
||||
readonly parseNetworkConfig: UnwrapRef<typeof import('./composables/network')['parseNetworkConfig']>
|
||||
readonly prepareVpnService: UnwrapRef<typeof import('./composables/mobile_vpn')['prepareVpnService']>
|
||||
readonly provide: UnwrapRef<typeof import('vue')['provide']>
|
||||
@@ -168,7 +180,6 @@ declare module 'vue' {
|
||||
readonly retainNetworkInstance: UnwrapRef<typeof import('./composables/network')['retainNetworkInstance']>
|
||||
readonly runNetworkInstance: UnwrapRef<typeof import('./composables/network')['runNetworkInstance']>
|
||||
readonly setActivePinia: UnwrapRef<typeof import('pinia')['setActivePinia']>
|
||||
readonly setAutoLaunchStatus: UnwrapRef<typeof import('./composables/network')['setAutoLaunchStatus']>
|
||||
readonly setLoggingLevel: UnwrapRef<typeof import('./composables/network')['setLoggingLevel']>
|
||||
readonly setMapStoreSuffix: UnwrapRef<typeof import('pinia')['setMapStoreSuffix']>
|
||||
readonly setTrayMenu: UnwrapRef<typeof import('./composables/tray')['setTrayMenu']>
|
||||
@@ -189,11 +200,14 @@ declare module 'vue' {
|
||||
readonly useCssModule: UnwrapRef<typeof import('vue')['useCssModule']>
|
||||
readonly useCssVars: UnwrapRef<typeof import('vue')['useCssVars']>
|
||||
readonly useI18n: UnwrapRef<typeof import('vue-i18n')['useI18n']>
|
||||
readonly useId: UnwrapRef<typeof import('vue')['useId']>
|
||||
readonly useLink: UnwrapRef<typeof import('vue-router/auto')['useLink']>
|
||||
readonly useModel: UnwrapRef<typeof import('vue')['useModel']>
|
||||
readonly useNetworkStore: UnwrapRef<typeof import('./stores/network')['useNetworkStore']>
|
||||
readonly useRoute: UnwrapRef<typeof import('vue-router/auto')['useRoute']>
|
||||
readonly useRouter: UnwrapRef<typeof import('vue-router/auto')['useRouter']>
|
||||
readonly useRoute: UnwrapRef<typeof import('vue-router')['useRoute']>
|
||||
readonly useRouter: UnwrapRef<typeof import('vue-router')['useRouter']>
|
||||
readonly useSlots: UnwrapRef<typeof import('vue')['useSlots']>
|
||||
readonly useTemplateRef: UnwrapRef<typeof import('vue')['useTemplateRef']>
|
||||
readonly useTray: UnwrapRef<typeof import('./composables/tray')['useTray']>
|
||||
readonly watch: UnwrapRef<typeof import('vue')['watch']>
|
||||
readonly watchEffect: UnwrapRef<typeof import('vue')['watchEffect']>
|
||||
|
27
easytier-gui/src/components/About.vue
Normal file
27
easytier-gui/src/components/About.vue
Normal file
@@ -0,0 +1,27 @@
|
||||
<script setup lang="ts">
|
||||
import { getEasytierVersion } from '~/composables/network'
|
||||
|
||||
const { t } = useI18n()
|
||||
|
||||
const etVersion = ref('')
|
||||
|
||||
onMounted(async () => {
|
||||
etVersion.value = await getEasytierVersion()
|
||||
})
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<Card>
|
||||
<template #title>
|
||||
Easytier - {{ t('about.version') }}: {{ etVersion }}
|
||||
</template>
|
||||
<template #content>
|
||||
<p class="mb-1">
|
||||
{{ t('about.description') }}
|
||||
</p>
|
||||
</template>
|
||||
</Card>
|
||||
</template>
|
||||
|
||||
<style scoped lang="postcss">
|
||||
</style>
|
@@ -1,262 +0,0 @@
|
||||
<script setup lang="ts">
|
||||
import InputGroup from 'primevue/inputgroup'
|
||||
import InputGroupAddon from 'primevue/inputgroupaddon'
|
||||
import { getOsHostname } from '~/composables/network'
|
||||
import { NetworkingMethod } from '~/types/network'
|
||||
const { t } = useI18n()
|
||||
|
||||
import { ping } from 'tauri-plugin-vpnservice-api'
|
||||
|
||||
const props = defineProps<{
|
||||
configInvalid?: boolean
|
||||
instanceId?: string
|
||||
}>()
|
||||
|
||||
defineEmits(['runNetwork'])
|
||||
|
||||
const networking_methods = ref([
|
||||
{ value: NetworkingMethod.PublicServer, label: () => t('public_server') },
|
||||
{ value: NetworkingMethod.Manual, label: () => t('manual') },
|
||||
{ value: NetworkingMethod.Standalone, label: () => t('standalone') },
|
||||
])
|
||||
|
||||
const networkStore = useNetworkStore()
|
||||
const curNetwork = computed(() => {
|
||||
if (props.instanceId) {
|
||||
// console.log('instanceId', props.instanceId)
|
||||
const c = networkStore.networkList.find(n => n.instance_id === props.instanceId)
|
||||
if (c !== undefined)
|
||||
return c
|
||||
}
|
||||
|
||||
return networkStore.curNetwork
|
||||
})
|
||||
|
||||
const protos:{ [proto: string] : number; } = {'tcp': 11010, 'udp': 11010, 'wg':11011, 'ws': 11011, 'wss': 11012}
|
||||
|
||||
function searchUrlSuggestions(e: { query: string }): string[] {
|
||||
const query = e.query
|
||||
let ret = []
|
||||
// if query match "^\w+:.*", then no proto prefix
|
||||
if (query.match(/^\w+:.*/)) {
|
||||
// if query is a valid url, then add to suggestions
|
||||
try {
|
||||
new URL(query)
|
||||
ret.push(query)
|
||||
} catch (e) {}
|
||||
} else {
|
||||
for (let proto in protos) {
|
||||
let item = proto + '://' + query
|
||||
// if query match ":\d+$", then no port suffix
|
||||
if (!query.match(/:\d+$/)) {
|
||||
item += ':' + protos[proto]
|
||||
}
|
||||
ret.push(item)
|
||||
}
|
||||
}
|
||||
|
||||
return ret
|
||||
}
|
||||
|
||||
|
||||
const publicServerSuggestions = ref([''])
|
||||
|
||||
const searchPresetPublicServers = (e: { query: string }) => {
|
||||
const presetPublicServers = [
|
||||
'tcp://easytier.public.kkrainbow.top:11010',
|
||||
]
|
||||
|
||||
let query = e.query
|
||||
// if query is sub string of presetPublicServers, add to suggestions
|
||||
let ret = presetPublicServers.filter((item) => item.includes(query))
|
||||
// add additional suggestions
|
||||
if (query.length > 0) {
|
||||
ret = ret.concat(searchUrlSuggestions(e))
|
||||
}
|
||||
|
||||
publicServerSuggestions.value = ret
|
||||
}
|
||||
|
||||
const peerSuggestions = ref([''])
|
||||
|
||||
const searchPeerSuggestions = (e: { query: string }) => {
|
||||
peerSuggestions.value = searchUrlSuggestions(e)
|
||||
}
|
||||
|
||||
const listenerSuggestions = ref([''])
|
||||
|
||||
const searchListenerSuggestiong = (e: { query: string }) => {
|
||||
let ret = []
|
||||
|
||||
for (let proto in protos) {
|
||||
let item = proto + '://0.0.0.0:';
|
||||
// if query is a number, use it as port
|
||||
if (e.query.match(/^\d+$/)) {
|
||||
item += e.query
|
||||
} else {
|
||||
item += protos[proto]
|
||||
}
|
||||
|
||||
if (item.includes(e.query)) {
|
||||
ret.push(item)
|
||||
}
|
||||
}
|
||||
|
||||
if (ret.length === 0) {
|
||||
ret.push(e.query)
|
||||
}
|
||||
|
||||
listenerSuggestions.value = ret
|
||||
}
|
||||
|
||||
function validateHostname() {
|
||||
if (curNetwork.value.hostname) {
|
||||
// eslint no-useless-escape
|
||||
let name = curNetwork.value.hostname!.replaceAll(/[^\u4E00-\u9FA5a-zA-Z0-9\-]*/g, '')
|
||||
if (name.length > 32)
|
||||
name = name.substring(0, 32)
|
||||
|
||||
if (curNetwork.value.hostname !== name)
|
||||
curNetwork.value.hostname = name
|
||||
}
|
||||
}
|
||||
|
||||
const osHostname = ref<string>('')
|
||||
|
||||
onMounted(async () => {
|
||||
osHostname.value = await getOsHostname()
|
||||
osHostname.value = await ping('ffdklsajflkdsjl') || ''
|
||||
})
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div class="flex flex-column h-full">
|
||||
<div class="flex flex-column">
|
||||
<div class="w-10/12 self-center ">
|
||||
<Message severity="warn">
|
||||
{{ t('dhcp_experimental_warning') }}
|
||||
</Message>
|
||||
</div>
|
||||
<div class="w-10/12 self-center ">
|
||||
<Panel :header="t('basic_settings')">
|
||||
<div class="flex flex-column gap-y-2">
|
||||
<div class="flex flex-row gap-x-9 flex-wrap">
|
||||
<div class="flex flex-column gap-2 basis-5/12 grow">
|
||||
<div class="flex align-items-center" for="virtual_ip">
|
||||
<label class="mr-2"> {{ t('virtual_ipv4') }} </label>
|
||||
<Checkbox v-model="curNetwork.dhcp" input-id="virtual_ip_auto" :binary="true" />
|
||||
|
||||
<label for="virtual_ip_auto" class="ml-2">
|
||||
{{ t('virtual_ipv4_dhcp') }}
|
||||
</label>
|
||||
</div>
|
||||
<InputGroup>
|
||||
<InputText id="virtual_ip" v-model="curNetwork.virtual_ipv4" :disabled="curNetwork.dhcp"
|
||||
aria-describedby="virtual_ipv4-help" />
|
||||
<InputGroupAddon>
|
||||
<span>/24</span>
|
||||
</InputGroupAddon>
|
||||
</InputGroup>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="flex flex-row gap-x-9 flex-wrap">
|
||||
<div class="flex flex-column gap-2 basis-5/12 grow">
|
||||
<label for="network_name">{{ t('network_name') }}</label>
|
||||
<InputText id="network_name" v-model="curNetwork.network_name" aria-describedby="network_name-help" />
|
||||
</div>
|
||||
<div class="flex flex-column gap-2 basis-5/12 grow">
|
||||
<label for="network_secret">{{ t('network_secret') }}</label>
|
||||
<InputText id="network_secret" v-model="curNetwork.network_secret"
|
||||
aria-describedby=" network_secret-help" />
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="flex flex-row gap-x-9 flex-wrap">
|
||||
<div class="flex flex-column gap-2 basis-5/12 grow">
|
||||
<label for="nm">{{ t('networking_method') }}</label>
|
||||
<SelectButton v-model="curNetwork.networking_method" :options="networking_methods" :option-label="(v) => v.label()" option-value="value"></SelectButton>
|
||||
<div class="items-center flex flex-row p-fluid gap-x-1">
|
||||
<AutoComplete v-if="curNetwork.networking_method === NetworkingMethod.Manual" id="chips"
|
||||
v-model="curNetwork.peer_urls" :placeholder="t('chips_placeholder', ['tcp://8.8.8.8:11010'])"
|
||||
class="grow" multiple fluid :suggestions="peerSuggestions" @complete="searchPeerSuggestions"/>
|
||||
|
||||
<AutoComplete v-if="curNetwork.networking_method === NetworkingMethod.PublicServer" :suggestions="publicServerSuggestions"
|
||||
:virtualScrollerOptions="{ itemSize: 38 }" class="grow" dropdown @complete="searchPresetPublicServers" :completeOnFocus="true"
|
||||
v-model="curNetwork.public_server_url"/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</Panel>
|
||||
|
||||
<Divider />
|
||||
|
||||
<Panel :header="t('advanced_settings')" toggleable collapsed>
|
||||
<div class="flex flex-column gap-y-2">
|
||||
<div class="flex flex-row gap-x-9 flex-wrap">
|
||||
<div class="flex flex-column gap-2 basis-5/12 grow">
|
||||
<label for="hostname">{{ t('hostname') }}</label>
|
||||
<InputText id="hostname" v-model="curNetwork.hostname" aria-describedby="hostname-help" :format="true"
|
||||
:placeholder="t('hostname_placeholder', [osHostname])" @blur="validateHostname" />
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="flex flex-row gap-x-9 flex-wrap w-full">
|
||||
<div class="flex flex-column gap-2 grow p-fluid">
|
||||
<label for="username">{{ t('proxy_cidrs') }}</label>
|
||||
<Chips id="chips" v-model="curNetwork.proxy_cidrs"
|
||||
:placeholder="t('chips_placeholder', ['10.0.0.0/24'])" separator=" " class="w-full" />
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="flex flex-row gap-x-9 flex-wrap ">
|
||||
<div class="flex flex-column gap-2 grow">
|
||||
<label for="username">VPN Portal</label>
|
||||
<ToggleButton v-model="curNetwork.enable_vpn_portal" on-icon="pi pi-check" off-icon="pi pi-times"
|
||||
:on-label="t('off_text')" :off-label="t('on_text')" class="w-48"/>
|
||||
<div class="items-center flex flex-row gap-x-4" v-if="curNetwork.enable_vpn_portal">
|
||||
<div class="min-w-64">
|
||||
<InputGroup>
|
||||
<InputText v-model="curNetwork.vpn_portal_client_network_addr"
|
||||
:placeholder="t('vpn_portal_client_network')" />
|
||||
<InputGroupAddon>
|
||||
<span>/{{ curNetwork.vpn_portal_client_network_len }}</span>
|
||||
</InputGroupAddon>
|
||||
</InputGroup>
|
||||
|
||||
<InputNumber v-model="curNetwork.vpn_portal_listen_port" :allow-empty="false"
|
||||
:format="false" :min="0" :max="65535" class="w-8" fluid/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="flex flex-row gap-x-9 flex-wrap">
|
||||
<div class="flex flex-column gap-2 grow p-fluid">
|
||||
<label for="listener_urls">{{ t('listener_urls') }}</label>
|
||||
<AutoComplete id="listener_urls" :suggestions="listenerSuggestions"
|
||||
class="w-full" dropdown @complete="searchListenerSuggestiong" :completeOnFocus="true"
|
||||
:placeholder="t('chips_placeholder', ['tcp://1.1.1.1:11010'])"
|
||||
v-model="curNetwork.listener_urls" multiple/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="flex flex-row gap-x-9 flex-wrap">
|
||||
<div class="flex flex-column gap-2 basis-5/12 grow">
|
||||
<label for="rpc_port">{{ t('rpc_port') }}</label>
|
||||
<InputNumber id="rpc_port" v-model="curNetwork.rpc_port" aria-describedby="username-help"
|
||||
:format="false" :min="0" :max="65535" />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</Panel>
|
||||
|
||||
<div class="flex pt-4 justify-content-center">
|
||||
<Button :label="t('run_network')" icon="pi pi-arrow-right" icon-pos="right" :disabled="configInvalid"
|
||||
@click="$emit('runNetwork', curNetwork)" />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
@@ -1,183 +1,192 @@
|
||||
import { addPluginListener } from '@tauri-apps/api/core';
|
||||
import { prepare_vpn, start_vpn, stop_vpn } from 'tauri-plugin-vpnservice-api';
|
||||
import { Route } from '~/types/network';
|
||||
import type { NetworkTypes } from 'easytier-frontend-lib'
|
||||
import { addPluginListener } from '@tauri-apps/api/core'
|
||||
import { Utils } from 'easytier-frontend-lib'
|
||||
import { prepare_vpn, start_vpn, stop_vpn } from 'tauri-plugin-vpnservice-api'
|
||||
|
||||
type Route = NetworkTypes.Route
|
||||
|
||||
const networkStore = useNetworkStore()
|
||||
|
||||
interface vpnStatus {
|
||||
running: boolean
|
||||
ipv4Addr: string | null | undefined
|
||||
ipv4Cidr: number | null | undefined
|
||||
routes: string[]
|
||||
running: boolean
|
||||
ipv4Addr: string | null | undefined
|
||||
ipv4Cidr: number | null | undefined
|
||||
routes: string[]
|
||||
}
|
||||
|
||||
var curVpnStatus: vpnStatus = {
|
||||
running: false,
|
||||
ipv4Addr: undefined,
|
||||
ipv4Cidr: undefined,
|
||||
routes: []
|
||||
const curVpnStatus: vpnStatus = {
|
||||
running: false,
|
||||
ipv4Addr: undefined,
|
||||
ipv4Cidr: undefined,
|
||||
routes: [],
|
||||
}
|
||||
|
||||
async function waitVpnStatus(target_status: boolean, timeout_sec: number) {
|
||||
let start_time = Date.now()
|
||||
while (curVpnStatus.running !== target_status) {
|
||||
if (Date.now() - start_time > timeout_sec * 1000) {
|
||||
throw new Error('wait vpn status timeout')
|
||||
}
|
||||
await new Promise(r => setTimeout(r, 50))
|
||||
const start_time = Date.now()
|
||||
while (curVpnStatus.running !== target_status) {
|
||||
if (Date.now() - start_time > timeout_sec * 1000) {
|
||||
throw new Error('wait vpn status timeout')
|
||||
}
|
||||
|
||||
await new Promise(r => setTimeout(r, 50))
|
||||
}
|
||||
}
|
||||
|
||||
async function doStopVpn() {
|
||||
if (!curVpnStatus.running) {
|
||||
return
|
||||
}
|
||||
console.log('stop vpn')
|
||||
let stop_ret = await stop_vpn()
|
||||
console.log('stop vpn', JSON.stringify((stop_ret)))
|
||||
await waitVpnStatus(false, 3)
|
||||
if (!curVpnStatus.running) {
|
||||
return
|
||||
}
|
||||
console.log('stop vpn')
|
||||
const stop_ret = await stop_vpn()
|
||||
console.log('stop vpn', JSON.stringify((stop_ret)))
|
||||
await waitVpnStatus(false, 3)
|
||||
|
||||
curVpnStatus.ipv4Addr = undefined
|
||||
curVpnStatus.routes = []
|
||||
curVpnStatus.ipv4Addr = undefined
|
||||
curVpnStatus.routes = []
|
||||
}
|
||||
|
||||
async function doStartVpn(ipv4Addr: string, cidr: number, routes: string[]) {
|
||||
if (curVpnStatus.running) {
|
||||
return
|
||||
}
|
||||
if (curVpnStatus.running) {
|
||||
return
|
||||
}
|
||||
|
||||
console.log('start vpn')
|
||||
let start_ret = await start_vpn({
|
||||
"ipv4Addr": ipv4Addr + '/' + cidr,
|
||||
"routes": routes,
|
||||
"disallowedApplications": ["com.kkrainbow.easytier"],
|
||||
"mtu": 1300,
|
||||
});
|
||||
if (start_ret?.errorMsg?.length) {
|
||||
throw new Error(start_ret.errorMsg)
|
||||
}
|
||||
await waitVpnStatus(true, 3)
|
||||
console.log('start vpn')
|
||||
const start_ret = await start_vpn({
|
||||
ipv4Addr: `${ipv4Addr}`,
|
||||
routes,
|
||||
disallowedApplications: ['com.kkrainbow.easytier'],
|
||||
mtu: 1300,
|
||||
})
|
||||
if (start_ret?.errorMsg?.length) {
|
||||
throw new Error(start_ret.errorMsg)
|
||||
}
|
||||
await waitVpnStatus(true, 3)
|
||||
|
||||
curVpnStatus.ipv4Addr = ipv4Addr
|
||||
curVpnStatus.routes = routes
|
||||
curVpnStatus.ipv4Addr = ipv4Addr
|
||||
curVpnStatus.routes = routes
|
||||
}
|
||||
|
||||
async function onVpnServiceStart(payload: any) {
|
||||
console.log('vpn service start', JSON.stringify(payload))
|
||||
curVpnStatus.running = true
|
||||
if (payload.fd) {
|
||||
setTunFd(networkStore.networkInstanceIds[0], payload.fd)
|
||||
}
|
||||
console.log('vpn service start', JSON.stringify(payload))
|
||||
curVpnStatus.running = true
|
||||
if (payload.fd) {
|
||||
setTunFd(networkStore.networkInstanceIds[0], payload.fd)
|
||||
}
|
||||
}
|
||||
|
||||
async function onVpnServiceStop(payload: any) {
|
||||
console.log('vpn service stop', JSON.stringify(payload))
|
||||
curVpnStatus.running = false
|
||||
console.log('vpn service stop', JSON.stringify(payload))
|
||||
curVpnStatus.running = false
|
||||
}
|
||||
|
||||
async function registerVpnServiceListener() {
|
||||
console.log('register vpn service listener')
|
||||
await addPluginListener(
|
||||
'vpnservice',
|
||||
'vpn_service_start',
|
||||
onVpnServiceStart
|
||||
)
|
||||
console.log('register vpn service listener')
|
||||
await addPluginListener(
|
||||
'vpnservice',
|
||||
'vpn_service_start',
|
||||
onVpnServiceStart,
|
||||
)
|
||||
|
||||
await addPluginListener(
|
||||
'vpnservice',
|
||||
'vpn_service_stop',
|
||||
onVpnServiceStop
|
||||
)
|
||||
await addPluginListener(
|
||||
'vpnservice',
|
||||
'vpn_service_stop',
|
||||
onVpnServiceStop,
|
||||
)
|
||||
}
|
||||
|
||||
function getRoutesForVpn(routes: Route[]): string[] {
|
||||
if (!routes) {
|
||||
return []
|
||||
}
|
||||
if (!routes) {
|
||||
return []
|
||||
}
|
||||
|
||||
let ret = []
|
||||
for (let r of routes) {
|
||||
for (let cidr of r.proxy_cidrs) {
|
||||
if (cidr.indexOf('/') === -1) {
|
||||
cidr += '/32'
|
||||
}
|
||||
ret.push(cidr)
|
||||
}
|
||||
const ret = []
|
||||
for (const r of routes) {
|
||||
for (let cidr of r.proxy_cidrs) {
|
||||
if (!cidr.includes('/')) {
|
||||
cidr += '/32'
|
||||
}
|
||||
ret.push(cidr)
|
||||
}
|
||||
}
|
||||
|
||||
// sort and dedup
|
||||
return Array.from(new Set(ret)).sort()
|
||||
// sort and dedup
|
||||
return Array.from(new Set(ret)).sort()
|
||||
}
|
||||
|
||||
async function onNetworkInstanceChange() {
|
||||
let insts = networkStore.networkInstanceIds
|
||||
if (!insts) {
|
||||
await doStopVpn()
|
||||
return
|
||||
const insts = networkStore.networkInstanceIds
|
||||
if (!insts) {
|
||||
await doStopVpn()
|
||||
return
|
||||
}
|
||||
|
||||
const curNetworkInfo = networkStore.networkInfos[insts[0]]
|
||||
if (!curNetworkInfo || curNetworkInfo?.error_msg?.length) {
|
||||
await doStopVpn()
|
||||
return
|
||||
}
|
||||
|
||||
const virtual_ip = Utils.ipv4ToString(curNetworkInfo?.my_node_info?.virtual_ipv4.address)
|
||||
if (!virtual_ip || !virtual_ip.length) {
|
||||
await doStopVpn()
|
||||
return
|
||||
}
|
||||
|
||||
let network_length = curNetworkInfo?.my_node_info?.virtual_ipv4.network_length
|
||||
if (!network_length) {
|
||||
network_length = 24
|
||||
}
|
||||
|
||||
const routes = getRoutesForVpn(curNetworkInfo?.routes)
|
||||
|
||||
const ipChanged = virtual_ip !== curVpnStatus.ipv4Addr
|
||||
const routesChanged = JSON.stringify(routes) !== JSON.stringify(curVpnStatus.routes)
|
||||
|
||||
if (ipChanged || routesChanged) {
|
||||
console.log('virtual ip changed', JSON.stringify(curVpnStatus), virtual_ip)
|
||||
try {
|
||||
await doStopVpn()
|
||||
}
|
||||
catch (e) {
|
||||
console.error(e)
|
||||
}
|
||||
|
||||
const curNetworkInfo = networkStore.networkInfos[insts[0]]
|
||||
if (!curNetworkInfo || curNetworkInfo?.error_msg?.length) {
|
||||
await doStopVpn()
|
||||
return
|
||||
try {
|
||||
await doStartVpn(virtual_ip, 24, routes)
|
||||
}
|
||||
|
||||
const virtual_ip = curNetworkInfo?.node_info?.virtual_ipv4
|
||||
if (!virtual_ip || !virtual_ip.length) {
|
||||
await doStopVpn()
|
||||
return
|
||||
}
|
||||
|
||||
const routes = getRoutesForVpn(curNetworkInfo?.routes)
|
||||
|
||||
var ipChanged = virtual_ip !== curVpnStatus.ipv4Addr
|
||||
var routesChanged = JSON.stringify(routes) !== JSON.stringify(curVpnStatus.routes)
|
||||
|
||||
if (ipChanged || routesChanged) {
|
||||
console.log('virtual ip changed', JSON.stringify(curVpnStatus), virtual_ip)
|
||||
try {
|
||||
await doStopVpn()
|
||||
} catch (e) {
|
||||
console.error(e)
|
||||
}
|
||||
|
||||
try {
|
||||
await doStartVpn(virtual_ip, 24, routes)
|
||||
} catch (e) {
|
||||
console.error("start vpn failed, clear all network insts.", e)
|
||||
networkStore.clearNetworkInstances()
|
||||
await retainNetworkInstance(networkStore.networkInstanceIds)
|
||||
}
|
||||
return
|
||||
catch (e) {
|
||||
console.error('start vpn failed, clear all network insts.', e)
|
||||
networkStore.clearNetworkInstances()
|
||||
await retainNetworkInstance(networkStore.networkInstanceIds)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
async function watchNetworkInstance() {
|
||||
var subscribe_running = false
|
||||
networkStore.$subscribe(async () => {
|
||||
if (subscribe_running) {
|
||||
return
|
||||
}
|
||||
subscribe_running = true
|
||||
try {
|
||||
await onNetworkInstanceChange()
|
||||
} catch (_) {
|
||||
}
|
||||
subscribe_running = false
|
||||
})
|
||||
let subscribe_running = false
|
||||
networkStore.$subscribe(async () => {
|
||||
if (subscribe_running) {
|
||||
return
|
||||
}
|
||||
subscribe_running = true
|
||||
try {
|
||||
await onNetworkInstanceChange()
|
||||
}
|
||||
catch (_) {
|
||||
}
|
||||
subscribe_running = false
|
||||
})
|
||||
}
|
||||
|
||||
export async function initMobileVpnService() {
|
||||
await registerVpnServiceListener()
|
||||
await watchNetworkInstance()
|
||||
await registerVpnServiceListener()
|
||||
await watchNetworkInstance()
|
||||
}
|
||||
|
||||
export async function prepareVpnService() {
|
||||
console.log('prepare vpn')
|
||||
let prepare_ret = await prepare_vpn()
|
||||
console.log('prepare vpn', JSON.stringify((prepare_ret)))
|
||||
if (prepare_ret?.errorMsg?.length) {
|
||||
throw new Error(prepare_ret.errorMsg)
|
||||
}
|
||||
console.log('prepare vpn')
|
||||
const prepare_ret = await prepare_vpn()
|
||||
console.log('prepare vpn', JSON.stringify((prepare_ret)))
|
||||
if (prepare_ret?.errorMsg?.length) {
|
||||
throw new Error(prepare_ret.errorMsg)
|
||||
}
|
||||
}
|
||||
|
@@ -1,6 +1,8 @@
|
||||
import { invoke } from "@tauri-apps/api/core"
|
||||
import type { NetworkTypes } from 'easytier-frontend-lib'
|
||||
import { invoke } from '@tauri-apps/api/core'
|
||||
|
||||
import type { NetworkConfig, NetworkInstanceRunningInfo } from '~/types/network'
|
||||
type NetworkConfig = NetworkTypes.NetworkConfig
|
||||
type NetworkInstanceRunningInfo = NetworkTypes.NetworkInstanceRunningInfo
|
||||
|
||||
export async function parseNetworkConfig(cfg: NetworkConfig) {
|
||||
return invoke<string>('parse_network_config', { cfg })
|
||||
@@ -22,8 +24,8 @@ export async function getOsHostname() {
|
||||
return await invoke<string>('get_os_hostname')
|
||||
}
|
||||
|
||||
export async function setAutoLaunchStatus(enable: boolean) {
|
||||
return await invoke<boolean>('set_auto_launch_status', { enable })
|
||||
export async function isAutostart() {
|
||||
return await invoke<boolean>('is_autostart')
|
||||
}
|
||||
|
||||
export async function setLoggingLevel(level: string) {
|
||||
@@ -33,3 +35,7 @@ export async function setLoggingLevel(level: string) {
|
||||
export async function setTunFd(instanceId: string, fd: number) {
|
||||
return await invoke('set_tun_fd', { instanceId, fd })
|
||||
}
|
||||
|
||||
export async function getEasytierVersion() {
|
||||
return await invoke<string>('easytier_version')
|
||||
}
|
||||
|
@@ -1,6 +1,6 @@
|
||||
import { getCurrentWindow } from '@tauri-apps/api/window'
|
||||
import { Menu, MenuItem, PredefinedMenuItem } from '@tauri-apps/api/menu'
|
||||
import { TrayIcon } from '@tauri-apps/api/tray'
|
||||
import { getCurrentWindow } from '@tauri-apps/api/window'
|
||||
import pkg from '~/../package.json'
|
||||
|
||||
const DEFAULT_TRAY_NAME = 'main'
|
||||
@@ -8,14 +8,15 @@ const DEFAULT_TRAY_NAME = 'main'
|
||||
async function toggleVisibility() {
|
||||
if (await getCurrentWindow().isVisible()) {
|
||||
await getCurrentWindow().hide()
|
||||
} else {
|
||||
}
|
||||
else {
|
||||
await getCurrentWindow().show()
|
||||
await getCurrentWindow().setFocus()
|
||||
}
|
||||
}
|
||||
|
||||
export async function useTray(init: boolean = false) {
|
||||
let tray;
|
||||
let tray
|
||||
try {
|
||||
tray = await TrayIcon.getById(DEFAULT_TRAY_NAME)
|
||||
if (!tray) {
|
||||
@@ -29,17 +30,18 @@ export async function useTray(init: boolean = false) {
|
||||
}),
|
||||
action: async () => {
|
||||
toggleVisibility()
|
||||
}
|
||||
},
|
||||
})
|
||||
}
|
||||
} catch (error) {
|
||||
}
|
||||
catch (error) {
|
||||
console.warn('Error while creating tray icon:', error)
|
||||
return null
|
||||
}
|
||||
|
||||
if (init) {
|
||||
tray.setTooltip(`EasyTier\n${pkg.version}`)
|
||||
tray.setMenuOnLeftClick(false);
|
||||
tray.setMenuOnLeftClick(false)
|
||||
tray.setMenu(await Menu.new({
|
||||
id: 'main',
|
||||
items: await generateMenuItem(),
|
||||
@@ -59,7 +61,7 @@ export async function generateMenuItem() {
|
||||
|
||||
export async function MenuItemExit(text: string) {
|
||||
return await PredefinedMenuItem.new({
|
||||
text: text,
|
||||
text,
|
||||
item: 'Quit',
|
||||
})
|
||||
}
|
||||
@@ -69,14 +71,15 @@ export async function MenuItemShow(text: string) {
|
||||
id: 'show',
|
||||
text,
|
||||
action: async () => {
|
||||
await toggleVisibility();
|
||||
await toggleVisibility()
|
||||
},
|
||||
})
|
||||
}
|
||||
|
||||
export async function setTrayMenu(items: (MenuItem | PredefinedMenuItem)[] | undefined = undefined) {
|
||||
const tray = await useTray()
|
||||
if (!tray) return
|
||||
if (!tray)
|
||||
return
|
||||
const menu = await Menu.new({
|
||||
id: 'main',
|
||||
items: items || await generateMenuItem(),
|
||||
@@ -86,15 +89,17 @@ export async function setTrayMenu(items: (MenuItem | PredefinedMenuItem)[] | und
|
||||
|
||||
export async function setTrayRunState(isRunning: boolean = false) {
|
||||
const tray = await useTray()
|
||||
if (!tray) return
|
||||
if (!tray)
|
||||
return
|
||||
tray.setIcon(isRunning ? 'icons/icon-inactive.ico' : 'icons/icon.ico')
|
||||
}
|
||||
|
||||
export async function setTrayTooltip(tooltip: string) {
|
||||
if (tooltip) {
|
||||
const tray = await useTray()
|
||||
if (!tray) return
|
||||
if (!tray)
|
||||
return
|
||||
tray.setTooltip(`EasyTier\n${pkg.version}\n${tooltip}`)
|
||||
tray.setTitle(`EasyTier\n${pkg.version}\n${tooltip}`)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@@ -1,16 +1,15 @@
|
||||
import { setupLayouts } from 'virtual:generated-layouts'
|
||||
import { createRouter, createWebHistory } from 'vue-router/auto'
|
||||
|
||||
import Aura from '@primevue/themes/aura'
|
||||
import PrimeVue from 'primevue/config'
|
||||
import ToastService from 'primevue/toastservice'
|
||||
import App from '~/App.vue'
|
||||
|
||||
import { createRouter, createWebHistory } from 'vue-router/auto'
|
||||
import { routes } from 'vue-router/auto-routes'
|
||||
import App from '~/App.vue'
|
||||
import EasyTierFrontendLib, { I18nUtils } from 'easytier-frontend-lib'
|
||||
|
||||
import { getAutoLaunchStatusAsync, loadAutoLaunchStatusAsync } from './modules/auto_launch'
|
||||
import '~/styles.css'
|
||||
import Aura from '@primevue/themes/aura'
|
||||
import 'primeicons/primeicons.css'
|
||||
import 'primeflex/primeflex.css'
|
||||
import { i18n, loadLanguageAsync } from '~/modules/i18n'
|
||||
import { loadAutoLaunchStatusAsync, getAutoLaunchStatusAsync } from './modules/auto_launch'
|
||||
import 'easytier-frontend-lib/style.css'
|
||||
|
||||
if (import.meta.env.PROD) {
|
||||
document.addEventListener('keydown', (event) => {
|
||||
@@ -18,8 +17,9 @@ if (import.meta.env.PROD) {
|
||||
event.key === 'F5'
|
||||
|| (event.ctrlKey && event.key === 'r')
|
||||
|| (event.metaKey && event.key === 'r')
|
||||
)
|
||||
) {
|
||||
event.preventDefault()
|
||||
}
|
||||
})
|
||||
|
||||
document.addEventListener('contextmenu', (event) => {
|
||||
@@ -28,29 +28,34 @@ if (import.meta.env.PROD) {
|
||||
}
|
||||
|
||||
async function main() {
|
||||
await loadLanguageAsync(localStorage.getItem('lang') || 'en')
|
||||
await I18nUtils.loadLanguageAsync(localStorage.getItem('lang') || 'en')
|
||||
await loadAutoLaunchStatusAsync(getAutoLaunchStatusAsync())
|
||||
|
||||
const app = createApp(App)
|
||||
|
||||
const router = createRouter({
|
||||
history: createWebHistory(),
|
||||
extendRoutes: routes => setupLayouts(routes),
|
||||
routes,
|
||||
})
|
||||
|
||||
app.use(router)
|
||||
app.use(createPinia())
|
||||
app.use(i18n, { useScope: 'global' })
|
||||
app.use(EasyTierFrontendLib)
|
||||
// app.use(i18n, { useScope: 'global' })
|
||||
app.use(PrimeVue, {
|
||||
theme: {
|
||||
preset: Aura,
|
||||
options: {
|
||||
prefix: 'p',
|
||||
darkModeSelector: 'system',
|
||||
cssLayer: false
|
||||
}
|
||||
}})
|
||||
app.use(ToastService)
|
||||
prefix: 'p',
|
||||
darkModeSelector: 'system',
|
||||
cssLayer: {
|
||||
name: 'primevue',
|
||||
order: 'tailwind-base, primevue, tailwind-utilities'
|
||||
}
|
||||
},
|
||||
},
|
||||
})
|
||||
app.use(ToastService as any)
|
||||
app.mount('#app')
|
||||
}
|
||||
|
||||
|
@@ -1,16 +1,26 @@
|
||||
import { setAutoLaunchStatus } from "~/composables/network"
|
||||
import { disable, enable, isEnabled } from '@tauri-apps/plugin-autostart'
|
||||
|
||||
export async function loadAutoLaunchStatusAsync(enable: boolean): Promise<boolean> {
|
||||
try {
|
||||
const ret = await setAutoLaunchStatus(enable)
|
||||
localStorage.setItem('auto_launch', JSON.stringify(ret))
|
||||
return ret
|
||||
} catch (e) {
|
||||
console.error(e)
|
||||
return false
|
||||
export async function loadAutoLaunchStatusAsync(target_enable: boolean): Promise<boolean> {
|
||||
try {
|
||||
if (target_enable) {
|
||||
await enable()
|
||||
}
|
||||
else {
|
||||
// 消除没有配置自启动时进行关闭操作报错
|
||||
try {
|
||||
await disable()
|
||||
}
|
||||
catch { }
|
||||
}
|
||||
localStorage.setItem('auto_launch', JSON.stringify(await isEnabled()))
|
||||
return isEnabled()
|
||||
}
|
||||
catch (e) {
|
||||
console.error(e)
|
||||
return false
|
||||
}
|
||||
}
|
||||
|
||||
export function getAutoLaunchStatusAsync(): boolean {
|
||||
return localStorage.getItem('auto_launch') === 'true'
|
||||
return localStorage.getItem('auto_launch') === 'true'
|
||||
}
|
||||
|
@@ -1,24 +1,22 @@
|
||||
<script setup lang="ts">
|
||||
import { useToast } from 'primevue/usetoast'
|
||||
|
||||
import { exit } from '@tauri-apps/plugin-process';
|
||||
import Config from '~/components/Config.vue'
|
||||
import Status from '~/components/Status.vue'
|
||||
|
||||
import type { NetworkConfig } from '~/types/network'
|
||||
import { loadLanguageAsync } from '~/modules/i18n'
|
||||
import { getAutoLaunchStatusAsync as getAutoLaunchStatus, loadAutoLaunchStatusAsync } from '~/modules/auto_launch'
|
||||
import { loadRunningInstanceIdsFromLocalStorage } from '~/stores/network'
|
||||
import { setLoggingLevel } from '~/composables/network'
|
||||
import TieredMenu from 'primevue/tieredmenu'
|
||||
import { open } from '@tauri-apps/plugin-shell';
|
||||
import { appLogDir } from '@tauri-apps/api/path'
|
||||
import { writeText } from '@tauri-apps/plugin-clipboard-manager';
|
||||
import { useTray } from '~/composables/tray';
|
||||
import { type } from '@tauri-apps/plugin-os';
|
||||
|
||||
import { getCurrentWindow } from '@tauri-apps/api/window'
|
||||
import { writeText } from '@tauri-apps/plugin-clipboard-manager'
|
||||
import { type } from '@tauri-apps/plugin-os'
|
||||
import { exit } from '@tauri-apps/plugin-process'
|
||||
import { open } from '@tauri-apps/plugin-shell'
|
||||
import TieredMenu from 'primevue/tieredmenu'
|
||||
import { useToast } from 'primevue/usetoast'
|
||||
import { NetworkTypes, Config, Status, Utils, I18nUtils } from 'easytier-frontend-lib'
|
||||
|
||||
import { isAutostart, setLoggingLevel } from '~/composables/network'
|
||||
import { useTray } from '~/composables/tray'
|
||||
import { getAutoLaunchStatusAsync as getAutoLaunchStatus, loadAutoLaunchStatusAsync } from '~/modules/auto_launch'
|
||||
|
||||
const { t, locale } = useI18n()
|
||||
const visible = ref(false)
|
||||
const aboutVisible = ref(false)
|
||||
const tomlConfig = ref('')
|
||||
|
||||
useTray(true)
|
||||
@@ -64,6 +62,27 @@ const toast = useToast()
|
||||
|
||||
const networkStore = useNetworkStore()
|
||||
|
||||
const curNetworkConfig = computed(() => {
|
||||
if (networkStore.curNetworkId) {
|
||||
// console.log('instanceId', props.instanceId)
|
||||
const c = networkStore.networkList.find(n => n.instance_id === networkStore.curNetworkId)
|
||||
if (c !== undefined)
|
||||
return c
|
||||
}
|
||||
|
||||
return networkStore.curNetwork
|
||||
})
|
||||
|
||||
const curNetworkInst = computed<NetworkTypes.NetworkInstance | null>(() => {
|
||||
let ret = networkStore.networkInstances.find(n => n.instance_id === curNetworkConfig.value.instance_id)
|
||||
console.log('curNetworkInst', ret)
|
||||
if (ret === undefined) {
|
||||
return null;
|
||||
} else {
|
||||
return ret;
|
||||
}
|
||||
})
|
||||
|
||||
function addNewNetwork() {
|
||||
networkStore.addNewNetwork()
|
||||
networkStore.curNetwork = networkStore.lastNetwork
|
||||
@@ -71,7 +90,6 @@ function addNewNetwork() {
|
||||
|
||||
networkStore.$subscribe(async () => {
|
||||
networkStore.saveToLocalStorage()
|
||||
networkStore.saveRunningInstanceIdsToLocalStorage()
|
||||
try {
|
||||
await parseNetworkConfig(networkStore.curNetwork)
|
||||
messageBarSeverity.value = Severity.None
|
||||
@@ -82,11 +100,12 @@ networkStore.$subscribe(async () => {
|
||||
}
|
||||
})
|
||||
|
||||
async function runNetworkCb(cfg: NetworkConfig, cb: () => void) {
|
||||
async function runNetworkCb(cfg: NetworkTypes.NetworkConfig, cb: () => void) {
|
||||
if (type() === 'android') {
|
||||
await prepareVpnService()
|
||||
networkStore.clearNetworkInstances()
|
||||
} else {
|
||||
}
|
||||
else {
|
||||
networkStore.removeNetworkInstance(cfg.instance_id)
|
||||
}
|
||||
|
||||
@@ -95,6 +114,7 @@ async function runNetworkCb(cfg: NetworkConfig, cb: () => void) {
|
||||
|
||||
try {
|
||||
await runNetworkInstance(cfg)
|
||||
networkStore.addAutoStartInstId(cfg.instance_id)
|
||||
}
|
||||
catch (e: any) {
|
||||
// console.error(e)
|
||||
@@ -104,11 +124,12 @@ async function runNetworkCb(cfg: NetworkConfig, cb: () => void) {
|
||||
cb()
|
||||
}
|
||||
|
||||
async function stopNetworkCb(cfg: NetworkConfig, cb: () => void) {
|
||||
async function stopNetworkCb(cfg: NetworkTypes.NetworkConfig, cb: () => void) {
|
||||
// console.log('stopNetworkCb', cfg, cb)
|
||||
cb()
|
||||
networkStore.removeNetworkInstance(cfg.instance_id)
|
||||
await retainNetworkInstance(networkStore.networkInstanceIds)
|
||||
networkStore.removeAutoStartInstId(cfg.instance_id)
|
||||
}
|
||||
|
||||
async function updateNetworkInfos() {
|
||||
@@ -120,10 +141,13 @@ onMounted(async () => {
|
||||
intervalId = window.setInterval(async () => {
|
||||
await updateNetworkInfos()
|
||||
}, 500)
|
||||
await setTrayMenu([
|
||||
await MenuItemExit(t('tray.exit')),
|
||||
await MenuItemShow(t('tray.show'))
|
||||
])
|
||||
|
||||
window.setTimeout(async () => {
|
||||
await setTrayMenu([
|
||||
await MenuItemExit(t('tray.exit')),
|
||||
await MenuItemShow(t('tray.show')),
|
||||
])
|
||||
}, 1000)
|
||||
})
|
||||
onUnmounted(() => clearInterval(intervalId))
|
||||
|
||||
@@ -139,10 +163,10 @@ const setting_menu_items = ref([
|
||||
label: () => t('exchange_language'),
|
||||
icon: 'pi pi-language',
|
||||
command: async () => {
|
||||
await loadLanguageAsync((locale.value === 'en' ? 'cn' : 'en'))
|
||||
await I18nUtils.loadLanguageAsync((locale.value === 'en' ? 'cn' : 'en'))
|
||||
await setTrayMenu([
|
||||
await MenuItemExit(t('tray.exit')),
|
||||
await MenuItemShow(t('tray.show'))
|
||||
await MenuItemShow(t('tray.show')),
|
||||
])
|
||||
},
|
||||
},
|
||||
@@ -158,10 +182,10 @@ const setting_menu_items = ref([
|
||||
icon: 'pi pi-file',
|
||||
items: (function () {
|
||||
const levels = ['off', 'warn', 'info', 'debug', 'trace']
|
||||
let items = []
|
||||
for (let level of levels) {
|
||||
const items = []
|
||||
for (const level of levels) {
|
||||
items.push({
|
||||
label: () => t("logging_level_" + level) + (current_log_level === level ? ' ✓' : ''),
|
||||
label: () => t(`logging_level_${level}`) + (current_log_level === level ? ' ✓' : ''),
|
||||
command: async () => {
|
||||
current_log_level = level
|
||||
await setLoggingLevel(level)
|
||||
@@ -175,7 +199,7 @@ const setting_menu_items = ref([
|
||||
label: () => t('logging_open_dir'),
|
||||
icon: 'pi pi-folder-open',
|
||||
command: async () => {
|
||||
console.log("open log dir", await appLogDir())
|
||||
// console.log('open log dir', await appLogDir())
|
||||
await open(await appLogDir())
|
||||
},
|
||||
})
|
||||
@@ -187,7 +211,14 @@ const setting_menu_items = ref([
|
||||
},
|
||||
})
|
||||
return items
|
||||
})()
|
||||
})(),
|
||||
},
|
||||
{
|
||||
label: () => t('about.title'),
|
||||
icon: 'pi pi-at',
|
||||
command: async () => {
|
||||
aboutVisible.value = true
|
||||
},
|
||||
},
|
||||
{
|
||||
label: () => t('exit'),
|
||||
@@ -202,18 +233,22 @@ function toggle_setting_menu(event: any) {
|
||||
setting_menu.value.toggle(event)
|
||||
}
|
||||
|
||||
onMounted(async () => {
|
||||
onBeforeMount(async () => {
|
||||
networkStore.loadFromLocalStorage()
|
||||
if (getAutoLaunchStatus()) {
|
||||
let prev_running_ids = loadRunningInstanceIdsFromLocalStorage()
|
||||
for (let id of prev_running_ids) {
|
||||
let cfg = networkStore.networkList.find((item) => item.instance_id === id)
|
||||
if (type() !== 'android' && getAutoLaunchStatus() && await isAutostart()) {
|
||||
getCurrentWindow().hide()
|
||||
const autoStartIds = networkStore.autoStartInstIds
|
||||
for (const id of autoStartIds) {
|
||||
const cfg = networkStore.networkList.find((item: NetworkTypes.NetworkConfig) => item.instance_id === id)
|
||||
if (cfg) {
|
||||
networkStore.addNetworkInstance(cfg.instance_id)
|
||||
await runNetworkInstance(cfg)
|
||||
}
|
||||
}
|
||||
}
|
||||
})
|
||||
|
||||
onMounted(async () => {
|
||||
if (type() === 'android') {
|
||||
await initMobileVpnService()
|
||||
}
|
||||
@@ -222,14 +257,13 @@ onMounted(async () => {
|
||||
function isRunning(id: string) {
|
||||
return networkStore.networkInstanceIds.includes(id)
|
||||
}
|
||||
|
||||
</script>
|
||||
|
||||
<script lang="ts">
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div id="root" class="flex flex-column">
|
||||
<div id="root" class="flex flex-col">
|
||||
<Dialog v-model:visible="visible" modal header="Config File" :style="{ width: '70%' }">
|
||||
<Panel>
|
||||
<ScrollPanel style="width: 100%; height: 300px">
|
||||
@@ -237,50 +271,61 @@ function isRunning(id: string) {
|
||||
</ScrollPanel>
|
||||
</Panel>
|
||||
<Divider />
|
||||
<div class="flex justify-content-end gap-2">
|
||||
<div class="flex gap-2 justify-end">
|
||||
<Button type="button" :label="t('close')" @click="visible = false" />
|
||||
</div>
|
||||
</Dialog>
|
||||
|
||||
<Dialog v-model:visible="aboutVisible" modal :header="t('about.title')" :style="{ width: '70%' }">
|
||||
<About />
|
||||
</Dialog>
|
||||
|
||||
<div>
|
||||
<Toolbar>
|
||||
<template #start>
|
||||
<div class="flex align-items-center">
|
||||
<div class="flex items-center">
|
||||
<Button icon="pi pi-plus" severity="primary" :label="t('add_new_network')" @click="addNewNetwork" />
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<template #center>
|
||||
<div class="min-w-40">
|
||||
<Dropdown v-model="networkStore.curNetwork" :options="networkStore.networkList" :highlight-on-select="false"
|
||||
<Select v-model="networkStore.curNetwork" :options="networkStore.networkList" :highlight-on-select="false"
|
||||
:placeholder="t('select_network')" class="w-full">
|
||||
<template #value="slotProps">
|
||||
<div class="flex items-start content-center">
|
||||
<div class="mr-3 flex-column">
|
||||
<div class="mr-4 flex-col">
|
||||
<span>{{ slotProps.value.network_name }}</span>
|
||||
</div>
|
||||
<Tag class="my-auto" :severity="isRunning(slotProps.value.instance_id) ? 'success' : 'info'"
|
||||
<Tag class="my-auto leading-3" :severity="isRunning(slotProps.value.instance_id) ? 'success' : 'info'"
|
||||
:value="t(isRunning(slotProps.value.instance_id) ? 'network_running' : 'network_stopped')" />
|
||||
</div>
|
||||
</template>
|
||||
<template #option="slotProps">
|
||||
<div class="flex flex-col items-start content-center">
|
||||
<div class="flex flex-col items-start content-center max-w-full">
|
||||
<div class="flex">
|
||||
<div class="mr-3">
|
||||
<div class="mr-4">
|
||||
{{ t('network_name') }}: {{ slotProps.option.network_name }}
|
||||
</div>
|
||||
<Tag class="my-auto" :severity="isRunning(slotProps.option.instance_id) ? 'success' : 'info'"
|
||||
<Tag class="my-auto leading-3"
|
||||
:severity="isRunning(slotProps.option.instance_id) ? 'success' : 'info'"
|
||||
:value="t(isRunning(slotProps.option.instance_id) ? 'network_running' : 'network_stopped')" />
|
||||
</div>
|
||||
<div>{{ slotProps.option.public_server_url }}</div>
|
||||
<div v-if="slotProps.option.networking_method !== NetworkTypes.NetworkingMethod.Standalone"
|
||||
class="max-w-full overflow-hidden text-ellipsis">
|
||||
{{ slotProps.option.networking_method === NetworkTypes.NetworkingMethod.Manual
|
||||
? slotProps.option.peer_urls.join(', ')
|
||||
: slotProps.option.public_server_url }}
|
||||
</div>
|
||||
<div
|
||||
v-if="isRunning(slotProps.option.instance_id) && networkStore.instances[slotProps.option.instance_id].detail && (networkStore.instances[slotProps.option.instance_id].detail?.my_node_info.virtual_ipv4 !== '')">
|
||||
{{ networkStore.instances[slotProps.option.instance_id].detail
|
||||
? networkStore.instances[slotProps.option.instance_id].detail?.my_node_info.virtual_ipv4 : '' }}
|
||||
v-if="isRunning(slotProps.option.instance_id) && networkStore.instances[slotProps.option.instance_id].detail && (!!networkStore.instances[slotProps.option.instance_id].detail?.my_node_info.virtual_ipv4)">
|
||||
{{
|
||||
Utils.ipv4InetToString(networkStore.instances[slotProps.option.instance_id].detail?.my_node_info.virtual_ipv4)
|
||||
}}
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
</Dropdown>
|
||||
</Select>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
@@ -295,19 +340,23 @@ function isRunning(id: string) {
|
||||
<Panel class="h-full overflow-y-auto">
|
||||
<Stepper :value="activeStep">
|
||||
<StepList value="1">
|
||||
<Step value="1">{{ t('config_network') }}</Step>
|
||||
<Step value="2">{{ t('running') }}</Step>
|
||||
<Step value="1">
|
||||
{{ t('config_network') }}
|
||||
</Step>
|
||||
<Step value="2">
|
||||
{{ t('running') }}
|
||||
</Step>
|
||||
</StepList>
|
||||
<StepPanels value="1">
|
||||
<StepPanel v-slot="{ activateCallback = (s: string) => { } } = {}" value="1">
|
||||
<Config :instance-id="networkStore.curNetworkId" :config-invalid="messageBarSeverity !== Severity.None"
|
||||
@run-network="runNetworkCb($event, () => activateCallback('2'))" />
|
||||
:cur-network="curNetworkConfig" @run-network="runNetworkCb($event, () => activateCallback('2'))" />
|
||||
</StepPanel>
|
||||
<StepPanel v-slot="{ activateCallback = (s: string) => { } } = {}" value="2">
|
||||
<div class="flex flex-column">
|
||||
<Status :instance-id="networkStore.curNetworkId" />
|
||||
<div class="flex flex-col">
|
||||
<Status :cur-network-inst="curNetworkInst" />
|
||||
</div>
|
||||
<div class="flex pt-4 justify-content-center">
|
||||
<div class="flex pt-6 justify-center">
|
||||
<Button :label="t('stop_network')" severity="danger" icon="pi pi-arrow-left"
|
||||
@click="stopNetworkCb(networkStore.curNetwork, () => activateCallback('1'))" />
|
||||
</div>
|
||||
@@ -349,6 +398,10 @@ body {
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
.p-select-overlay {
|
||||
max-width: calc(100% - 2rem);
|
||||
}
|
||||
|
||||
/*
|
||||
|
||||
.p-tabview-panel {
|
||||
|
@@ -1,24 +1,25 @@
|
||||
import type { NetworkConfig, NetworkInstance, NetworkInstanceRunningInfo } from '~/types/network'
|
||||
import { DEFAULT_NETWORK_CONFIG } from '~/types/network'
|
||||
import { NetworkTypes } from 'easytier-frontend-lib'
|
||||
|
||||
export const useNetworkStore = defineStore('networkStore', {
|
||||
state: () => {
|
||||
const networkList = [DEFAULT_NETWORK_CONFIG()]
|
||||
const networkList = [NetworkTypes.DEFAULT_NETWORK_CONFIG()]
|
||||
return {
|
||||
// for initially empty lists
|
||||
networkList: networkList as NetworkConfig[],
|
||||
networkList: networkList as NetworkTypes.NetworkConfig[],
|
||||
// for data that is not yet loaded
|
||||
curNetwork: networkList[0],
|
||||
|
||||
// uuid -> instance
|
||||
instances: {} as Record<string, NetworkInstance>,
|
||||
instances: {} as Record<string, NetworkTypes.NetworkInstance>,
|
||||
|
||||
networkInfos: {} as Record<string, NetworkInstanceRunningInfo>,
|
||||
networkInfos: {} as Record<string, NetworkTypes.NetworkInstanceRunningInfo>,
|
||||
|
||||
autoStartInstIds: [] as string[],
|
||||
}
|
||||
},
|
||||
|
||||
getters: {
|
||||
lastNetwork(): NetworkConfig {
|
||||
lastNetwork(): NetworkTypes.NetworkConfig {
|
||||
return this.networkList[this.networkList.length - 1]
|
||||
},
|
||||
|
||||
@@ -26,7 +27,7 @@ export const useNetworkStore = defineStore('networkStore', {
|
||||
return this.curNetwork.instance_id
|
||||
},
|
||||
|
||||
networkInstances(): Array<NetworkInstance> {
|
||||
networkInstances(): Array<NetworkTypes.NetworkInstance> {
|
||||
return Object.values(this.instances)
|
||||
},
|
||||
|
||||
@@ -37,7 +38,7 @@ export const useNetworkStore = defineStore('networkStore', {
|
||||
|
||||
actions: {
|
||||
addNewNetwork() {
|
||||
this.networkList.push(DEFAULT_NETWORK_CONFIG())
|
||||
this.networkList.push(NetworkTypes.DEFAULT_NETWORK_CONFIG())
|
||||
},
|
||||
|
||||
delCurNetwork() {
|
||||
@@ -64,7 +65,7 @@ export const useNetworkStore = defineStore('networkStore', {
|
||||
this.instances = {}
|
||||
},
|
||||
|
||||
updateWithNetworkInfos(networkInfos: Record<string, NetworkInstanceRunningInfo>) {
|
||||
updateWithNetworkInfos(networkInfos: Record<string, NetworkTypes.NetworkInstanceRunningInfo>) {
|
||||
this.networkInfos = networkInfos
|
||||
for (const [instanceId, info] of Object.entries(networkInfos)) {
|
||||
if (this.instances[instanceId] === undefined)
|
||||
@@ -74,45 +75,61 @@ export const useNetworkStore = defineStore('networkStore', {
|
||||
this.instances[instanceId].error_msg = info.error_msg || ''
|
||||
this.instances[instanceId].detail = info
|
||||
}
|
||||
this.saveRunningInstanceIdsToLocalStorage()
|
||||
},
|
||||
|
||||
loadFromLocalStorage() {
|
||||
let networkList: NetworkConfig[]
|
||||
let networkList: NetworkTypes.NetworkConfig[]
|
||||
|
||||
// if localStorage default is [{}], instanceId will be undefined
|
||||
networkList = JSON.parse(localStorage.getItem('networkList') || '[]')
|
||||
networkList = networkList.map((cfg) => {
|
||||
return { ...DEFAULT_NETWORK_CONFIG(), ...cfg } as NetworkConfig
|
||||
return { ...NetworkTypes.DEFAULT_NETWORK_CONFIG(), ...cfg } as NetworkTypes.NetworkConfig
|
||||
})
|
||||
|
||||
// prevent a empty list from localStorage, should not happen
|
||||
if (networkList.length === 0)
|
||||
networkList = [DEFAULT_NETWORK_CONFIG()]
|
||||
networkList = [NetworkTypes.DEFAULT_NETWORK_CONFIG()]
|
||||
|
||||
this.networkList = networkList
|
||||
this.curNetwork = this.networkList[0]
|
||||
|
||||
this.loadAutoStartInstIdsFromLocalStorage()
|
||||
},
|
||||
|
||||
saveToLocalStorage() {
|
||||
localStorage.setItem('networkList', JSON.stringify(this.networkList))
|
||||
},
|
||||
|
||||
saveRunningInstanceIdsToLocalStorage() {
|
||||
let instance_ids = Object.keys(this.instances).filter((instanceId) => this.instances[instanceId].running)
|
||||
localStorage.setItem('runningInstanceIds', JSON.stringify(instance_ids))
|
||||
}
|
||||
saveAutoStartInstIdsToLocalStorage() {
|
||||
localStorage.setItem('autoStartInstIds', JSON.stringify(this.autoStartInstIds))
|
||||
},
|
||||
|
||||
loadAutoStartInstIdsFromLocalStorage() {
|
||||
try {
|
||||
this.autoStartInstIds = JSON.parse(localStorage.getItem('autoStartInstIds') || '[]')
|
||||
}
|
||||
catch (e) {
|
||||
console.error(e)
|
||||
this.autoStartInstIds = []
|
||||
}
|
||||
},
|
||||
|
||||
addAutoStartInstId(instanceId: string) {
|
||||
if (!this.autoStartInstIds.includes(instanceId)) {
|
||||
this.autoStartInstIds.push(instanceId)
|
||||
}
|
||||
this.saveAutoStartInstIdsToLocalStorage()
|
||||
},
|
||||
|
||||
removeAutoStartInstId(instanceId: string) {
|
||||
const idx = this.autoStartInstIds.indexOf(instanceId)
|
||||
if (idx !== -1) {
|
||||
this.autoStartInstIds.splice(idx, 1)
|
||||
}
|
||||
this.saveAutoStartInstIdsToLocalStorage()
|
||||
},
|
||||
},
|
||||
})
|
||||
|
||||
if (import.meta.hot)
|
||||
import.meta.hot.accept(acceptHMRUpdate(useNetworkStore as any, import.meta.hot))
|
||||
|
||||
export function loadRunningInstanceIdsFromLocalStorage(): string[] {
|
||||
try {
|
||||
return JSON.parse(localStorage.getItem('runningInstanceIds') || '[]')
|
||||
} catch (e) {
|
||||
console.error(e)
|
||||
return []
|
||||
}
|
||||
}
|
||||
|
@@ -16,7 +16,6 @@
|
||||
font-weight: 400;
|
||||
|
||||
color: #0f0f0f;
|
||||
background-color: white;
|
||||
|
||||
font-synthesis: none;
|
||||
text-rendering: optimizeLegibility;
|
||||
|
2
easytier-gui/src/typed-router.d.ts
vendored
2
easytier-gui/src/typed-router.d.ts
vendored
@@ -12,7 +12,7 @@ declare module 'vue-router/auto-routes' {
|
||||
ParamValueOneOrMore,
|
||||
ParamValueZeroOrMore,
|
||||
ParamValueZeroOrOne,
|
||||
} from 'unplugin-vue-router/types'
|
||||
} from 'vue-router'
|
||||
|
||||
/**
|
||||
* Route name map generated by unplugin-vue-router
|
||||
|
@@ -1,19 +1,19 @@
|
||||
import path from 'node:path'
|
||||
import { defineConfig } from 'vite'
|
||||
import Vue from '@vitejs/plugin-vue'
|
||||
import Layouts from 'vite-plugin-vue-layouts'
|
||||
import Components from 'unplugin-vue-components/vite'
|
||||
import AutoImport from 'unplugin-auto-import/vite'
|
||||
import VueMacros from 'unplugin-vue-macros/vite'
|
||||
import process from 'node:process'
|
||||
import VueI18n from '@intlify/unplugin-vue-i18n/vite'
|
||||
import VueDevTools from 'vite-plugin-vue-devtools'
|
||||
import VueRouter from 'unplugin-vue-router/vite'
|
||||
import { PrimeVueResolver } from '@primevue/auto-import-resolver'
|
||||
import Vue from '@vitejs/plugin-vue'
|
||||
import { internalIpV4Sync } from 'internal-ip'
|
||||
import AutoImport from 'unplugin-auto-import/vite'
|
||||
import Components from 'unplugin-vue-components/vite'
|
||||
import VueMacros from 'unplugin-vue-macros/vite'
|
||||
import { VueRouterAutoImports } from 'unplugin-vue-router'
|
||||
import { PrimeVueResolver } from '@primevue/auto-import-resolver';
|
||||
import { svelte } from '@sveltejs/vite-plugin-svelte';
|
||||
import { internalIpV4Sync } from 'internal-ip';
|
||||
import VueRouter from 'unplugin-vue-router/vite'
|
||||
import { defineConfig } from 'vite'
|
||||
import VueDevTools from 'vite-plugin-vue-devtools'
|
||||
import Layouts from 'vite-plugin-vue-layouts'
|
||||
|
||||
const host = process.env.TAURI_DEV_HOST;
|
||||
const host = process.env.TAURI_DEV_HOST
|
||||
|
||||
// https://vitejs.dev/config/
|
||||
export default defineConfig(async () => ({
|
||||
@@ -23,7 +23,6 @@ export default defineConfig(async () => ({
|
||||
},
|
||||
},
|
||||
plugins: [
|
||||
svelte(),
|
||||
VueMacros({
|
||||
plugins: {
|
||||
vue: Vue({
|
||||
@@ -100,10 +99,10 @@ export default defineConfig(async () => ({
|
||||
},
|
||||
hmr: host
|
||||
? {
|
||||
protocol: 'ws',
|
||||
host: internalIpV4Sync(),
|
||||
port: 1430,
|
||||
}
|
||||
protocol: 'ws',
|
||||
host: internalIpV4Sync(),
|
||||
port: 1430,
|
||||
}
|
||||
: undefined,
|
||||
},
|
||||
}))
|
||||
|
21
easytier-rpc-build/Cargo.toml
Normal file
21
easytier-rpc-build/Cargo.toml
Normal file
@@ -0,0 +1,21 @@
|
||||
[package]
|
||||
name = "easytier-rpc-build"
|
||||
description = "Protobuf RPC Service Generator for EasyTier"
|
||||
version = "0.1.0"
|
||||
edition = "2021"
|
||||
homepage = "https://github.com/EasyTier/EasyTier"
|
||||
repository = "https://github.com/EasyTier/EasyTier"
|
||||
authors = ["kkrainbow"]
|
||||
keywords = ["vpn", "p2p", "network", "easytier"]
|
||||
categories = ["network-programming", "command-line-utilities"]
|
||||
rust-version = "1.77.0"
|
||||
license-file = "LICENSE"
|
||||
readme = "README.md"
|
||||
|
||||
[dependencies]
|
||||
heck = "0.5"
|
||||
prost-build = "0.13"
|
||||
|
||||
[features]
|
||||
default = []
|
||||
internal-namespace = []
|
1
easytier-rpc-build/LICENSE
Symbolic link
1
easytier-rpc-build/LICENSE
Symbolic link
@@ -0,0 +1 @@
|
||||
../LICENSE
|
3
easytier-rpc-build/README.md
Normal file
3
easytier-rpc-build/README.md
Normal file
@@ -0,0 +1,3 @@
|
||||
# Introduction
|
||||
|
||||
This is a protobuf rpc service stub generator for [EasyTier](https://github.com/EasyTier/EasyTier) project.
|
387
easytier-rpc-build/src/lib.rs
Normal file
387
easytier-rpc-build/src/lib.rs
Normal file
@@ -0,0 +1,387 @@
|
||||
extern crate heck;
|
||||
extern crate prost_build;
|
||||
|
||||
use std::fmt;
|
||||
|
||||
#[cfg(feature = "internal-namespace")]
|
||||
const NAMESPACE: &str = "crate::proto::rpc_types";
|
||||
|
||||
#[cfg(not(feature = "internal-namespace"))]
|
||||
const NAMESPACE: &str = "easytier::proto::rpc_types";
|
||||
|
||||
/// The service generator to be used with `prost-build` to generate RPC implementations for
|
||||
/// `prost-simple-rpc`.
|
||||
///
|
||||
/// See the crate-level documentation for more info.
|
||||
#[allow(missing_copy_implementations)]
|
||||
#[derive(Clone, Debug)]
|
||||
pub struct ServiceGenerator {
|
||||
_private: (),
|
||||
}
|
||||
|
||||
impl ServiceGenerator {
|
||||
/// Create a new `ServiceGenerator` instance with the default options set.
|
||||
pub fn new() -> ServiceGenerator {
|
||||
ServiceGenerator { _private: () }
|
||||
}
|
||||
}
|
||||
|
||||
impl prost_build::ServiceGenerator for ServiceGenerator {
|
||||
fn generate(&mut self, service: prost_build::Service, mut buf: &mut String) {
|
||||
use std::fmt::Write;
|
||||
|
||||
let descriptor_name = format!("{}Descriptor", service.name);
|
||||
let server_name = format!("{}Server", service.name);
|
||||
let client_name = format!("{}Client", service.name);
|
||||
let method_descriptor_name = format!("{}MethodDescriptor", service.name);
|
||||
|
||||
let mut trait_methods = String::new();
|
||||
let mut enum_methods = String::new();
|
||||
let mut list_enum_methods = String::new();
|
||||
let mut client_methods = String::new();
|
||||
let mut client_own_methods = String::new();
|
||||
let mut match_name_methods = String::new();
|
||||
let mut match_proto_name_methods = String::new();
|
||||
let mut match_input_type_methods = String::new();
|
||||
let mut match_input_proto_type_methods = String::new();
|
||||
let mut match_output_type_methods = String::new();
|
||||
let mut match_output_proto_type_methods = String::new();
|
||||
let mut match_handle_methods = String::new();
|
||||
|
||||
let mut match_method_try_from = String::new();
|
||||
|
||||
for (idx, method) in service.methods.iter().enumerate() {
|
||||
assert!(
|
||||
!method.client_streaming,
|
||||
"Client streaming not yet supported for method {}",
|
||||
method.proto_name
|
||||
);
|
||||
assert!(
|
||||
!method.server_streaming,
|
||||
"Server streaming not yet supported for method {}",
|
||||
method.proto_name
|
||||
);
|
||||
|
||||
ServiceGenerator::write_comments(&mut trait_methods, 4, &method.comments).unwrap();
|
||||
writeln!(
|
||||
trait_methods,
|
||||
r#" async fn {name}(&self, ctrl: Self::Controller, input: {input_type}) -> {namespace}::error::Result<{output_type}>;"#,
|
||||
name = method.name,
|
||||
input_type = method.input_type,
|
||||
output_type = method.output_type,
|
||||
namespace = NAMESPACE,
|
||||
)
|
||||
.unwrap();
|
||||
|
||||
ServiceGenerator::write_comments(&mut enum_methods, 4, &method.comments).unwrap();
|
||||
writeln!(
|
||||
enum_methods,
|
||||
" {name} = {index},",
|
||||
name = method.proto_name,
|
||||
index = format!("{}", idx + 1)
|
||||
)
|
||||
.unwrap();
|
||||
|
||||
writeln!(
|
||||
match_method_try_from,
|
||||
" {index} => Ok({service_name}MethodDescriptor::{name}),",
|
||||
service_name = service.name,
|
||||
name = method.proto_name,
|
||||
index = format!("{}", idx + 1),
|
||||
)
|
||||
.unwrap();
|
||||
|
||||
writeln!(
|
||||
list_enum_methods,
|
||||
" {service_name}MethodDescriptor::{name},",
|
||||
service_name = service.name,
|
||||
name = method.proto_name
|
||||
)
|
||||
.unwrap();
|
||||
|
||||
writeln!(
|
||||
client_methods,
|
||||
r#" async fn {name}(&self, ctrl: H::Controller, input: {input_type}) -> {namespace}::error::Result<{output_type}> {{
|
||||
{client_name}::{name}_inner(self.0.clone(), ctrl, input).await
|
||||
}}"#,
|
||||
name = method.name,
|
||||
input_type = method.input_type,
|
||||
output_type = method.output_type,
|
||||
client_name = format!("{}Client", service.name),
|
||||
namespace = NAMESPACE,
|
||||
)
|
||||
.unwrap();
|
||||
|
||||
writeln!(
|
||||
client_own_methods,
|
||||
r#" async fn {name}_inner(handler: H, ctrl: H::Controller, input: {input_type}) -> {namespace}::error::Result<{output_type}> {{
|
||||
{namespace}::__rt::call_method(handler, ctrl, {method_descriptor_name}::{proto_name}, input).await
|
||||
}}"#,
|
||||
name = method.name,
|
||||
method_descriptor_name = method_descriptor_name,
|
||||
proto_name = method.proto_name,
|
||||
input_type = method.input_type,
|
||||
output_type = method.output_type,
|
||||
namespace = NAMESPACE,
|
||||
).unwrap();
|
||||
|
||||
let case = format!(
|
||||
" {service_name}MethodDescriptor::{proto_name} => ",
|
||||
service_name = service.name,
|
||||
proto_name = method.proto_name
|
||||
);
|
||||
|
||||
writeln!(match_name_methods, "{}{:?},", case, method.name).unwrap();
|
||||
writeln!(match_proto_name_methods, "{}{:?},", case, method.proto_name).unwrap();
|
||||
writeln!(
|
||||
match_input_type_methods,
|
||||
"{}::std::any::TypeId::of::<{}>(),",
|
||||
case, method.input_type
|
||||
)
|
||||
.unwrap();
|
||||
writeln!(
|
||||
match_input_proto_type_methods,
|
||||
"{}{:?},",
|
||||
case, method.input_proto_type
|
||||
)
|
||||
.unwrap();
|
||||
writeln!(
|
||||
match_output_type_methods,
|
||||
"{}::std::any::TypeId::of::<{}>(),",
|
||||
case, method.output_type
|
||||
)
|
||||
.unwrap();
|
||||
writeln!(
|
||||
match_output_proto_type_methods,
|
||||
"{}{:?},",
|
||||
case, method.output_proto_type
|
||||
)
|
||||
.unwrap();
|
||||
write!(
|
||||
match_handle_methods,
|
||||
r#"{} {{
|
||||
let decoded: {input_type} = {namespace}::__rt::decode(input)?;
|
||||
let ret = service.{name}(ctrl, decoded).await?;
|
||||
{namespace}::__rt::encode(ret)
|
||||
}}
|
||||
"#,
|
||||
case,
|
||||
input_type = method.input_type,
|
||||
name = method.name,
|
||||
namespace = NAMESPACE,
|
||||
)
|
||||
.unwrap();
|
||||
}
|
||||
|
||||
ServiceGenerator::write_comments(&mut buf, 0, &service.comments).unwrap();
|
||||
write!(
|
||||
buf,
|
||||
r#"
|
||||
#[async_trait::async_trait]
|
||||
#[auto_impl::auto_impl(&, Arc, Box)]
|
||||
pub trait {name} {{
|
||||
type Controller: {namespace}::controller::Controller;
|
||||
|
||||
{trait_methods}
|
||||
}}
|
||||
|
||||
/// A service descriptor for a `{name}`.
|
||||
#[derive(Clone, Debug, Eq, Ord, PartialEq, PartialOrd, Default)]
|
||||
pub struct {descriptor_name};
|
||||
|
||||
/// Methods available on a `{name}`.
|
||||
///
|
||||
/// This can be used as a key when routing requests for servers/clients of a `{name}`.
|
||||
#[derive(Clone, Copy, Debug, Eq, Ord, PartialEq, PartialOrd)]
|
||||
#[repr(u8)]
|
||||
pub enum {method_descriptor_name} {{
|
||||
{enum_methods}
|
||||
}}
|
||||
|
||||
impl std::convert::TryFrom<u8> for {method_descriptor_name} {{
|
||||
type Error = {namespace}::error::Error;
|
||||
fn try_from(value: u8) -> {namespace}::error::Result<Self> {{
|
||||
match value {{
|
||||
{match_method_try_from}
|
||||
_ => Err({namespace}::error::Error::InvalidMethodIndex(value, "{name}".to_string())),
|
||||
}}
|
||||
}}
|
||||
}}
|
||||
|
||||
/// A client for a `{name}`.
|
||||
///
|
||||
/// This implements the `{name}` trait by dispatching all method calls to the supplied `Handler`.
|
||||
#[derive(Clone, Debug)]
|
||||
pub struct {client_name}<H>(H) where H: {namespace}::handler::Handler;
|
||||
|
||||
impl<H> {client_name}<H> where H: {namespace}::handler::Handler<Descriptor = {descriptor_name}> {{
|
||||
/// Creates a new client instance that delegates all method calls to the supplied handler.
|
||||
pub fn new(handler: H) -> {client_name}<H> {{
|
||||
{client_name}(handler)
|
||||
}}
|
||||
}}
|
||||
|
||||
impl<H> {client_name}<H> where H: {namespace}::handler::Handler<Descriptor = {descriptor_name}> {{
|
||||
{client_own_methods}
|
||||
}}
|
||||
|
||||
#[async_trait::async_trait]
|
||||
impl<H> {name} for {client_name}<H> where H: {namespace}::handler::Handler<Descriptor = {descriptor_name}> {{
|
||||
type Controller = H::Controller;
|
||||
|
||||
{client_methods}
|
||||
}}
|
||||
|
||||
pub struct {client_name}Factory<C: {namespace}::controller::Controller>(std::marker::PhantomData<C>);
|
||||
|
||||
impl<C: {namespace}::controller::Controller> Clone for {client_name}Factory<C> {{
|
||||
fn clone(&self) -> Self {{
|
||||
Self(std::marker::PhantomData)
|
||||
}}
|
||||
}}
|
||||
|
||||
impl<C> {namespace}::__rt::RpcClientFactory for {client_name}Factory<C> where C: {namespace}::controller::Controller {{
|
||||
type Descriptor = {descriptor_name};
|
||||
type ClientImpl = Box<dyn {name}<Controller = C> + Send + 'static>;
|
||||
type Controller = C;
|
||||
|
||||
fn new(handler: impl {namespace}::handler::Handler<Descriptor = Self::Descriptor, Controller = Self::Controller>) -> Self::ClientImpl {{
|
||||
Box::new({client_name}::new(handler))
|
||||
}}
|
||||
}}
|
||||
|
||||
/// A server for a `{name}`.
|
||||
///
|
||||
/// This implements the `Server` trait by handling requests and dispatch them to methods on the
|
||||
/// supplied `{name}`.
|
||||
#[derive(Clone, Debug)]
|
||||
pub struct {server_name}<A>(A) where A: {name} + Clone + Send + 'static;
|
||||
|
||||
impl<A> {server_name}<A> where A: {name} + Clone + Send + 'static {{
|
||||
/// Creates a new server instance that dispatches all calls to the supplied service.
|
||||
pub fn new(service: A) -> {server_name}<A> {{
|
||||
{server_name}(service)
|
||||
}}
|
||||
|
||||
async fn call_inner(
|
||||
service: A,
|
||||
method: {method_descriptor_name},
|
||||
ctrl: A::Controller,
|
||||
input: ::bytes::Bytes)
|
||||
-> {namespace}::error::Result<::bytes::Bytes> {{
|
||||
match method {{
|
||||
{match_handle_methods}
|
||||
}}
|
||||
}}
|
||||
}}
|
||||
|
||||
impl {namespace}::descriptor::ServiceDescriptor for {descriptor_name} {{
|
||||
type Method = {method_descriptor_name};
|
||||
fn name(&self) -> &'static str {{ {name:?} }}
|
||||
fn proto_name(&self) -> &'static str {{ {proto_name:?} }}
|
||||
fn package(&self) -> &'static str {{ {package:?} }}
|
||||
fn methods(&self) -> &'static [Self::Method] {{
|
||||
&[ {list_enum_methods} ]
|
||||
}}
|
||||
}}
|
||||
|
||||
#[async_trait::async_trait]
|
||||
impl<A> {namespace}::handler::Handler for {server_name}<A>
|
||||
where
|
||||
A: {name} + Clone + Send + Sync + 'static {{
|
||||
type Descriptor = {descriptor_name};
|
||||
type Controller = A::Controller;
|
||||
|
||||
async fn call(
|
||||
&self,
|
||||
ctrl: A::Controller,
|
||||
method: {method_descriptor_name},
|
||||
input: ::bytes::Bytes)
|
||||
-> {namespace}::error::Result<::bytes::Bytes> {{
|
||||
{server_name}::call_inner(self.0.clone(), method, ctrl, input).await
|
||||
}}
|
||||
}}
|
||||
|
||||
impl {namespace}::descriptor::MethodDescriptor for {method_descriptor_name} {{
|
||||
fn name(&self) -> &'static str {{
|
||||
match *self {{
|
||||
{match_name_methods}
|
||||
}}
|
||||
}}
|
||||
|
||||
fn proto_name(&self) -> &'static str {{
|
||||
match *self {{
|
||||
{match_proto_name_methods}
|
||||
}}
|
||||
}}
|
||||
|
||||
fn input_type(&self) -> ::std::any::TypeId {{
|
||||
match *self {{
|
||||
{match_input_type_methods}
|
||||
}}
|
||||
}}
|
||||
|
||||
fn input_proto_type(&self) -> &'static str {{
|
||||
match *self {{
|
||||
{match_input_proto_type_methods}
|
||||
}}
|
||||
}}
|
||||
|
||||
fn output_type(&self) -> ::std::any::TypeId {{
|
||||
match *self {{
|
||||
{match_output_type_methods}
|
||||
}}
|
||||
}}
|
||||
|
||||
fn output_proto_type(&self) -> &'static str {{
|
||||
match *self {{
|
||||
{match_output_proto_type_methods}
|
||||
}}
|
||||
}}
|
||||
|
||||
fn index(&self) -> u8 {{
|
||||
*self as u8
|
||||
}}
|
||||
}}
|
||||
"#,
|
||||
name = service.name,
|
||||
descriptor_name = descriptor_name,
|
||||
server_name = server_name,
|
||||
client_name = client_name,
|
||||
method_descriptor_name = method_descriptor_name,
|
||||
proto_name = service.proto_name,
|
||||
package = service.package,
|
||||
trait_methods = trait_methods,
|
||||
enum_methods = enum_methods,
|
||||
list_enum_methods = list_enum_methods,
|
||||
client_own_methods = client_own_methods,
|
||||
client_methods = client_methods,
|
||||
match_name_methods = match_name_methods,
|
||||
match_proto_name_methods = match_proto_name_methods,
|
||||
match_input_type_methods = match_input_type_methods,
|
||||
match_input_proto_type_methods = match_input_proto_type_methods,
|
||||
match_output_type_methods = match_output_type_methods,
|
||||
match_output_proto_type_methods = match_output_proto_type_methods,
|
||||
match_handle_methods = match_handle_methods,
|
||||
namespace = NAMESPACE,
|
||||
).unwrap();
|
||||
}
|
||||
}
|
||||
|
||||
impl ServiceGenerator {
|
||||
fn write_comments<W>(
|
||||
mut write: W,
|
||||
indent: usize,
|
||||
comments: &prost_build::Comments,
|
||||
) -> fmt::Result
|
||||
where
|
||||
W: fmt::Write,
|
||||
{
|
||||
for comment in &comments.leading {
|
||||
for line in comment.lines().filter(|s| !s.is_empty()) {
|
||||
writeln!(write, "{}///{}", " ".repeat(indent), line)?;
|
||||
}
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
}
|
52
easytier-web/Cargo.toml
Normal file
52
easytier-web/Cargo.toml
Normal file
@@ -0,0 +1,52 @@
|
||||
[package]
|
||||
name = "easytier-web"
|
||||
version = "0.1.0"
|
||||
edition = "2021"
|
||||
|
||||
[dependencies]
|
||||
easytier = { path = "../easytier" }
|
||||
tracing = { version = "0.1", features = ["log"] }
|
||||
anyhow = { version = "1.0" }
|
||||
thiserror = "1.0"
|
||||
tokio = { version = "1", features = ["full"] }
|
||||
dashmap = "6.1"
|
||||
url = "2.2"
|
||||
async-trait = "0.1"
|
||||
|
||||
axum = { version = "0.7", features = ["macros"] }
|
||||
axum-login = { version = "0.16" }
|
||||
password-auth = { version = "1.0.0" }
|
||||
axum-messages = "0.7.0"
|
||||
tower-sessions-sqlx-store = { version = "0.14.1", features = ["sqlite"] }
|
||||
tower-sessions = { version = "0.13.0", default-features = false, features = [
|
||||
"signed",
|
||||
] }
|
||||
tower-http = { version = "0.6", features = ["cors", "compression-full"] }
|
||||
sqlx = { version = "0.8", features = ["sqlite"] }
|
||||
sea-orm = { version = "1.1", features = [ "sqlx-sqlite", "runtime-tokio-rustls", "macros" ] }
|
||||
sea-orm-migration = { version = "1.1" }
|
||||
|
||||
|
||||
# for captcha
|
||||
rust-embed = { version = "8.5.0", features = ["debug-embed"] }
|
||||
base64 = "0.22"
|
||||
rand = "0.8"
|
||||
image = {version="0.24", default-features = false, features = ["png"]}
|
||||
rusttype = "0.9.3"
|
||||
imageproc = "0.23.0"
|
||||
|
||||
|
||||
clap = { version = "4.4.8", features = [
|
||||
"string",
|
||||
"unicode",
|
||||
"derive",
|
||||
"wrap_help",
|
||||
] }
|
||||
serde = { version = "1.0", features = ["derive"] }
|
||||
serde_json = "1.0"
|
||||
uuid = { version = "1.5.0", features = [
|
||||
"v4",
|
||||
"fast-rng",
|
||||
"macro-diagnostics",
|
||||
"serde",
|
||||
] }
|
24
easytier-web/frontend-lib/.gitignore
vendored
Normal file
24
easytier-web/frontend-lib/.gitignore
vendored
Normal file
@@ -0,0 +1,24 @@
|
||||
# Logs
|
||||
logs
|
||||
*.log
|
||||
npm-debug.log*
|
||||
yarn-debug.log*
|
||||
yarn-error.log*
|
||||
pnpm-debug.log*
|
||||
lerna-debug.log*
|
||||
|
||||
node_modules
|
||||
dist
|
||||
dist-ssr
|
||||
*.local
|
||||
|
||||
# Editor directories and files
|
||||
.vscode/*
|
||||
!.vscode/extensions.json
|
||||
.idea
|
||||
.DS_Store
|
||||
*.suo
|
||||
*.ntvs*
|
||||
*.njsproj
|
||||
*.sln
|
||||
*.sw?
|
5
easytier-web/frontend-lib/README.md
Normal file
5
easytier-web/frontend-lib/README.md
Normal file
@@ -0,0 +1,5 @@
|
||||
# Vue 3 + TypeScript + Vite
|
||||
|
||||
This template should help get you started developing with Vue 3 and TypeScript in Vite. The template uses Vue 3 `<script setup>` SFCs, check out the [script setup docs](https://v3.vuejs.org/api/sfc-script-setup.html#sfc-script-setup) to learn more.
|
||||
|
||||
Learn more about the recommended Project Setup and IDE Support in the [Vue Docs TypeScript Guide](https://vuejs.org/guide/typescript/overview.html#project-setup).
|
13
easytier-web/frontend-lib/index.html
Normal file
13
easytier-web/frontend-lib/index.html
Normal file
@@ -0,0 +1,13 @@
|
||||
<!doctype html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="UTF-8" />
|
||||
<link rel="icon" type="image/svg+xml" href="/vite.svg" />
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
||||
<title>Vite + Vue + TS</title>
|
||||
</head>
|
||||
<body>
|
||||
<div id="app"></div>
|
||||
<script type="module" src="/src/main.ts"></script>
|
||||
</body>
|
||||
</html>
|
49
easytier-web/frontend-lib/package.json
Normal file
49
easytier-web/frontend-lib/package.json
Normal file
@@ -0,0 +1,49 @@
|
||||
{
|
||||
"name": "easytier-frontend-lib",
|
||||
"private": true,
|
||||
"version": "0.0.0",
|
||||
"type": "module",
|
||||
"main": "./dist/easytier-frontend-lib.umd.cjs",
|
||||
"module": "./dist/easytier-frontend-lib.js",
|
||||
"exports": {
|
||||
".": {
|
||||
"import": "./dist/easytier-frontend-lib.js",
|
||||
"require": "./dist/easytier-frontend-lib.umd.cjs"
|
||||
},
|
||||
"./*.css": "./dist/*.css"
|
||||
},
|
||||
"scripts": {
|
||||
"dev": "vite",
|
||||
"build": "vue-tsc -b && vite build",
|
||||
"preview": "vite preview"
|
||||
},
|
||||
"dependencies": {
|
||||
"@primevue/themes": "^4.2.1",
|
||||
"@vueuse/core": "^11.1.0",
|
||||
"aura": "link:@primevue\\themes\\aura",
|
||||
"axios": "^1.7.7",
|
||||
"floating-vue": "^5.2",
|
||||
"ip-num": "1.5.1",
|
||||
"primeicons": "^7.0.0",
|
||||
"primevue": "^4.2.1",
|
||||
"tailwindcss-primeui": "^0.3.4",
|
||||
"ts-md5": "^1.3.1",
|
||||
"uuid": "^11.0.2",
|
||||
"vue": "^3.5.12",
|
||||
"vue-i18n": "^10.0.4"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@modyfi/vite-plugin-yaml": "^1.1.0",
|
||||
"@types/node": "^22.8.6",
|
||||
"@vitejs/plugin-vue": "^5.1.4",
|
||||
"autoprefixer": "^10.4.20",
|
||||
"postcss": "^8.4.47",
|
||||
"postcss-import": "^16.1.0",
|
||||
"postcss-nested": "^7.0.2",
|
||||
"tailwindcss": "^3.4.14",
|
||||
"typescript": "~5.6.3",
|
||||
"vite": "^5.4.10",
|
||||
"vite-plugin-dts": "^4.3.0",
|
||||
"vue-tsc": "^2.1.10"
|
||||
}
|
||||
}
|
820
easytier-web/frontend-lib/pnpm-lock.yaml
generated
Normal file
820
easytier-web/frontend-lib/pnpm-lock.yaml
generated
Normal file
@@ -0,0 +1,820 @@
|
||||
lockfileVersion: '9.0'
|
||||
|
||||
settings:
|
||||
autoInstallPeers: true
|
||||
excludeLinksFromLockfile: false
|
||||
|
||||
importers:
|
||||
|
||||
.:
|
||||
dependencies:
|
||||
vue:
|
||||
specifier: ^3.5.12
|
||||
version: 3.5.12(typescript@5.6.3)
|
||||
devDependencies:
|
||||
'@vitejs/plugin-vue':
|
||||
specifier: ^5.1.4
|
||||
version: 5.1.4(vite@5.4.10)(vue@3.5.12(typescript@5.6.3))
|
||||
typescript:
|
||||
specifier: ~5.6.2
|
||||
version: 5.6.3
|
||||
vite:
|
||||
specifier: ^5.4.10
|
||||
version: 5.4.10
|
||||
vue-tsc:
|
||||
specifier: ^2.1.8
|
||||
version: 2.1.10(typescript@5.6.3)
|
||||
|
||||
packages:
|
||||
|
||||
'@babel/helper-string-parser@7.25.9':
|
||||
resolution: {integrity: sha512-4A/SCr/2KLd5jrtOMFzaKjVtAei3+2r/NChoBNoZ3EyP/+GlhoaEGoWOZUmFmoITP7zOJyHIMm+DYRd8o3PvHA==}
|
||||
engines: {node: '>=6.9.0'}
|
||||
|
||||
'@babel/helper-validator-identifier@7.25.9':
|
||||
resolution: {integrity: sha512-Ed61U6XJc3CVRfkERJWDz4dJwKe7iLmmJsbOGu9wSloNSFttHV0I8g6UAgb7qnK5ly5bGLPd4oXZlxCdANBOWQ==}
|
||||
engines: {node: '>=6.9.0'}
|
||||
|
||||
'@babel/parser@7.26.2':
|
||||
resolution: {integrity: sha512-DWMCZH9WA4Maitz2q21SRKHo9QXZxkDsbNZoVD62gusNtNBBqDg9i7uOhASfTfIGNzW+O+r7+jAlM8dwphcJKQ==}
|
||||
engines: {node: '>=6.0.0'}
|
||||
hasBin: true
|
||||
|
||||
'@babel/types@7.26.0':
|
||||
resolution: {integrity: sha512-Z/yiTPj+lDVnF7lWeKCIJzaIkI0vYO87dMpZ4bg4TDrFe4XXLFWL1TbXU27gBP3QccxV9mZICCrnjnYlJjXHOA==}
|
||||
engines: {node: '>=6.9.0'}
|
||||
|
||||
'@esbuild/aix-ppc64@0.21.5':
|
||||
resolution: {integrity: sha512-1SDgH6ZSPTlggy1yI6+Dbkiz8xzpHJEVAlF/AM1tHPLsf5STom9rwtjE4hKAF20FfXXNTFqEYXyJNWh1GiZedQ==}
|
||||
engines: {node: '>=12'}
|
||||
cpu: [ppc64]
|
||||
os: [aix]
|
||||
|
||||
'@esbuild/android-arm64@0.21.5':
|
||||
resolution: {integrity: sha512-c0uX9VAUBQ7dTDCjq+wdyGLowMdtR/GoC2U5IYk/7D1H1JYC0qseD7+11iMP2mRLN9RcCMRcjC4YMclCzGwS/A==}
|
||||
engines: {node: '>=12'}
|
||||
cpu: [arm64]
|
||||
os: [android]
|
||||
|
||||
'@esbuild/android-arm@0.21.5':
|
||||
resolution: {integrity: sha512-vCPvzSjpPHEi1siZdlvAlsPxXl7WbOVUBBAowWug4rJHb68Ox8KualB+1ocNvT5fjv6wpkX6o/iEpbDrf68zcg==}
|
||||
engines: {node: '>=12'}
|
||||
cpu: [arm]
|
||||
os: [android]
|
||||
|
||||
'@esbuild/android-x64@0.21.5':
|
||||
resolution: {integrity: sha512-D7aPRUUNHRBwHxzxRvp856rjUHRFW1SdQATKXH2hqA0kAZb1hKmi02OpYRacl0TxIGz/ZmXWlbZgjwWYaCakTA==}
|
||||
engines: {node: '>=12'}
|
||||
cpu: [x64]
|
||||
os: [android]
|
||||
|
||||
'@esbuild/darwin-arm64@0.21.5':
|
||||
resolution: {integrity: sha512-DwqXqZyuk5AiWWf3UfLiRDJ5EDd49zg6O9wclZ7kUMv2WRFr4HKjXp/5t8JZ11QbQfUS6/cRCKGwYhtNAY88kQ==}
|
||||
engines: {node: '>=12'}
|
||||
cpu: [arm64]
|
||||
os: [darwin]
|
||||
|
||||
'@esbuild/darwin-x64@0.21.5':
|
||||
resolution: {integrity: sha512-se/JjF8NlmKVG4kNIuyWMV/22ZaerB+qaSi5MdrXtd6R08kvs2qCN4C09miupktDitvh8jRFflwGFBQcxZRjbw==}
|
||||
engines: {node: '>=12'}
|
||||
cpu: [x64]
|
||||
os: [darwin]
|
||||
|
||||
'@esbuild/freebsd-arm64@0.21.5':
|
||||
resolution: {integrity: sha512-5JcRxxRDUJLX8JXp/wcBCy3pENnCgBR9bN6JsY4OmhfUtIHe3ZW0mawA7+RDAcMLrMIZaf03NlQiX9DGyB8h4g==}
|
||||
engines: {node: '>=12'}
|
||||
cpu: [arm64]
|
||||
os: [freebsd]
|
||||
|
||||
'@esbuild/freebsd-x64@0.21.5':
|
||||
resolution: {integrity: sha512-J95kNBj1zkbMXtHVH29bBriQygMXqoVQOQYA+ISs0/2l3T9/kj42ow2mpqerRBxDJnmkUDCaQT/dfNXWX/ZZCQ==}
|
||||
engines: {node: '>=12'}
|
||||
cpu: [x64]
|
||||
os: [freebsd]
|
||||
|
||||
'@esbuild/linux-arm64@0.21.5':
|
||||
resolution: {integrity: sha512-ibKvmyYzKsBeX8d8I7MH/TMfWDXBF3db4qM6sy+7re0YXya+K1cem3on9XgdT2EQGMu4hQyZhan7TeQ8XkGp4Q==}
|
||||
engines: {node: '>=12'}
|
||||
cpu: [arm64]
|
||||
os: [linux]
|
||||
|
||||
'@esbuild/linux-arm@0.21.5':
|
||||
resolution: {integrity: sha512-bPb5AHZtbeNGjCKVZ9UGqGwo8EUu4cLq68E95A53KlxAPRmUyYv2D6F0uUI65XisGOL1hBP5mTronbgo+0bFcA==}
|
||||
engines: {node: '>=12'}
|
||||
cpu: [arm]
|
||||
os: [linux]
|
||||
|
||||
'@esbuild/linux-ia32@0.21.5':
|
||||
resolution: {integrity: sha512-YvjXDqLRqPDl2dvRODYmmhz4rPeVKYvppfGYKSNGdyZkA01046pLWyRKKI3ax8fbJoK5QbxblURkwK/MWY18Tg==}
|
||||
engines: {node: '>=12'}
|
||||
cpu: [ia32]
|
||||
os: [linux]
|
||||
|
||||
'@esbuild/linux-loong64@0.21.5':
|
||||
resolution: {integrity: sha512-uHf1BmMG8qEvzdrzAqg2SIG/02+4/DHB6a9Kbya0XDvwDEKCoC8ZRWI5JJvNdUjtciBGFQ5PuBlpEOXQj+JQSg==}
|
||||
engines: {node: '>=12'}
|
||||
cpu: [loong64]
|
||||
os: [linux]
|
||||
|
||||
'@esbuild/linux-mips64el@0.21.5':
|
||||
resolution: {integrity: sha512-IajOmO+KJK23bj52dFSNCMsz1QP1DqM6cwLUv3W1QwyxkyIWecfafnI555fvSGqEKwjMXVLokcV5ygHW5b3Jbg==}
|
||||
engines: {node: '>=12'}
|
||||
cpu: [mips64el]
|
||||
os: [linux]
|
||||
|
||||
'@esbuild/linux-ppc64@0.21.5':
|
||||
resolution: {integrity: sha512-1hHV/Z4OEfMwpLO8rp7CvlhBDnjsC3CttJXIhBi+5Aj5r+MBvy4egg7wCbe//hSsT+RvDAG7s81tAvpL2XAE4w==}
|
||||
engines: {node: '>=12'}
|
||||
cpu: [ppc64]
|
||||
os: [linux]
|
||||
|
||||
'@esbuild/linux-riscv64@0.21.5':
|
||||
resolution: {integrity: sha512-2HdXDMd9GMgTGrPWnJzP2ALSokE/0O5HhTUvWIbD3YdjME8JwvSCnNGBnTThKGEB91OZhzrJ4qIIxk/SBmyDDA==}
|
||||
engines: {node: '>=12'}
|
||||
cpu: [riscv64]
|
||||
os: [linux]
|
||||
|
||||
'@esbuild/linux-s390x@0.21.5':
|
||||
resolution: {integrity: sha512-zus5sxzqBJD3eXxwvjN1yQkRepANgxE9lgOW2qLnmr8ikMTphkjgXu1HR01K4FJg8h1kEEDAqDcZQtbrRnB41A==}
|
||||
engines: {node: '>=12'}
|
||||
cpu: [s390x]
|
||||
os: [linux]
|
||||
|
||||
'@esbuild/linux-x64@0.21.5':
|
||||
resolution: {integrity: sha512-1rYdTpyv03iycF1+BhzrzQJCdOuAOtaqHTWJZCWvijKD2N5Xu0TtVC8/+1faWqcP9iBCWOmjmhoH94dH82BxPQ==}
|
||||
engines: {node: '>=12'}
|
||||
cpu: [x64]
|
||||
os: [linux]
|
||||
|
||||
'@esbuild/netbsd-x64@0.21.5':
|
||||
resolution: {integrity: sha512-Woi2MXzXjMULccIwMnLciyZH4nCIMpWQAs049KEeMvOcNADVxo0UBIQPfSmxB3CWKedngg7sWZdLvLczpe0tLg==}
|
||||
engines: {node: '>=12'}
|
||||
cpu: [x64]
|
||||
os: [netbsd]
|
||||
|
||||
'@esbuild/openbsd-x64@0.21.5':
|
||||
resolution: {integrity: sha512-HLNNw99xsvx12lFBUwoT8EVCsSvRNDVxNpjZ7bPn947b8gJPzeHWyNVhFsaerc0n3TsbOINvRP2byTZ5LKezow==}
|
||||
engines: {node: '>=12'}
|
||||
cpu: [x64]
|
||||
os: [openbsd]
|
||||
|
||||
'@esbuild/sunos-x64@0.21.5':
|
||||
resolution: {integrity: sha512-6+gjmFpfy0BHU5Tpptkuh8+uw3mnrvgs+dSPQXQOv3ekbordwnzTVEb4qnIvQcYXq6gzkyTnoZ9dZG+D4garKg==}
|
||||
engines: {node: '>=12'}
|
||||
cpu: [x64]
|
||||
os: [sunos]
|
||||
|
||||
'@esbuild/win32-arm64@0.21.5':
|
||||
resolution: {integrity: sha512-Z0gOTd75VvXqyq7nsl93zwahcTROgqvuAcYDUr+vOv8uHhNSKROyU961kgtCD1e95IqPKSQKH7tBTslnS3tA8A==}
|
||||
engines: {node: '>=12'}
|
||||
cpu: [arm64]
|
||||
os: [win32]
|
||||
|
||||
'@esbuild/win32-ia32@0.21.5':
|
||||
resolution: {integrity: sha512-SWXFF1CL2RVNMaVs+BBClwtfZSvDgtL//G/smwAc5oVK/UPu2Gu9tIaRgFmYFFKrmg3SyAjSrElf0TiJ1v8fYA==}
|
||||
engines: {node: '>=12'}
|
||||
cpu: [ia32]
|
||||
os: [win32]
|
||||
|
||||
'@esbuild/win32-x64@0.21.5':
|
||||
resolution: {integrity: sha512-tQd/1efJuzPC6rCFwEvLtci/xNFcTZknmXs98FYDfGE4wP9ClFV98nyKrzJKVPMhdDnjzLhdUyMX4PsQAPjwIw==}
|
||||
engines: {node: '>=12'}
|
||||
cpu: [x64]
|
||||
os: [win32]
|
||||
|
||||
'@jridgewell/sourcemap-codec@1.5.0':
|
||||
resolution: {integrity: sha512-gv3ZRaISU3fjPAgNsriBRqGWQL6quFx04YMPW/zD8XMLsU32mhCCbfbO6KZFLjvYpCZ8zyDEgqsgf+PwPaM7GQ==}
|
||||
|
||||
'@rollup/rollup-android-arm-eabi@4.24.3':
|
||||
resolution: {integrity: sha512-ufb2CH2KfBWPJok95frEZZ82LtDl0A6QKTa8MoM+cWwDZvVGl5/jNb79pIhRvAalUu+7LD91VYR0nwRD799HkQ==}
|
||||
cpu: [arm]
|
||||
os: [android]
|
||||
|
||||
'@rollup/rollup-android-arm64@4.24.3':
|
||||
resolution: {integrity: sha512-iAHpft/eQk9vkWIV5t22V77d90CRofgR2006UiCjHcHJFVI1E0oBkQIAbz+pLtthFw3hWEmVB4ilxGyBf48i2Q==}
|
||||
cpu: [arm64]
|
||||
os: [android]
|
||||
|
||||
'@rollup/rollup-darwin-arm64@4.24.3':
|
||||
resolution: {integrity: sha512-QPW2YmkWLlvqmOa2OwrfqLJqkHm7kJCIMq9kOz40Zo9Ipi40kf9ONG5Sz76zszrmIZZ4hgRIkez69YnTHgEz1w==}
|
||||
cpu: [arm64]
|
||||
os: [darwin]
|
||||
|
||||
'@rollup/rollup-darwin-x64@4.24.3':
|
||||
resolution: {integrity: sha512-KO0pN5x3+uZm1ZXeIfDqwcvnQ9UEGN8JX5ufhmgH5Lz4ujjZMAnxQygZAVGemFWn+ZZC0FQopruV4lqmGMshow==}
|
||||
cpu: [x64]
|
||||
os: [darwin]
|
||||
|
||||
'@rollup/rollup-freebsd-arm64@4.24.3':
|
||||
resolution: {integrity: sha512-CsC+ZdIiZCZbBI+aRlWpYJMSWvVssPuWqrDy/zi9YfnatKKSLFCe6fjna1grHuo/nVaHG+kiglpRhyBQYRTK4A==}
|
||||
cpu: [arm64]
|
||||
os: [freebsd]
|
||||
|
||||
'@rollup/rollup-freebsd-x64@4.24.3':
|
||||
resolution: {integrity: sha512-F0nqiLThcfKvRQhZEzMIXOQG4EeX61im61VYL1jo4eBxv4aZRmpin6crnBJQ/nWnCsjH5F6J3W6Stdm0mBNqBg==}
|
||||
cpu: [x64]
|
||||
os: [freebsd]
|
||||
|
||||
'@rollup/rollup-linux-arm-gnueabihf@4.24.3':
|
||||
resolution: {integrity: sha512-KRSFHyE/RdxQ1CSeOIBVIAxStFC/hnBgVcaiCkQaVC+EYDtTe4X7z5tBkFyRoBgUGtB6Xg6t9t2kulnX6wJc6A==}
|
||||
cpu: [arm]
|
||||
os: [linux]
|
||||
|
||||
'@rollup/rollup-linux-arm-musleabihf@4.24.3':
|
||||
resolution: {integrity: sha512-h6Q8MT+e05zP5BxEKz0vi0DhthLdrNEnspdLzkoFqGwnmOzakEHSlXfVyA4HJ322QtFy7biUAVFPvIDEDQa6rw==}
|
||||
cpu: [arm]
|
||||
os: [linux]
|
||||
|
||||
'@rollup/rollup-linux-arm64-gnu@4.24.3':
|
||||
resolution: {integrity: sha512-fKElSyXhXIJ9pqiYRqisfirIo2Z5pTTve5K438URf08fsypXrEkVmShkSfM8GJ1aUyvjakT+fn2W7Czlpd/0FQ==}
|
||||
cpu: [arm64]
|
||||
os: [linux]
|
||||
|
||||
'@rollup/rollup-linux-arm64-musl@4.24.3':
|
||||
resolution: {integrity: sha512-YlddZSUk8G0px9/+V9PVilVDC6ydMz7WquxozToozSnfFK6wa6ne1ATUjUvjin09jp34p84milxlY5ikueoenw==}
|
||||
cpu: [arm64]
|
||||
os: [linux]
|
||||
|
||||
'@rollup/rollup-linux-powerpc64le-gnu@4.24.3':
|
||||
resolution: {integrity: sha512-yNaWw+GAO8JjVx3s3cMeG5Esz1cKVzz8PkTJSfYzE5u7A+NvGmbVFEHP+BikTIyYWuz0+DX9kaA3pH9Sqxp69g==}
|
||||
cpu: [ppc64]
|
||||
os: [linux]
|
||||
|
||||
'@rollup/rollup-linux-riscv64-gnu@4.24.3':
|
||||
resolution: {integrity: sha512-lWKNQfsbpv14ZCtM/HkjCTm4oWTKTfxPmr7iPfp3AHSqyoTz5AgLemYkWLwOBWc+XxBbrU9SCokZP0WlBZM9lA==}
|
||||
cpu: [riscv64]
|
||||
os: [linux]
|
||||
|
||||
'@rollup/rollup-linux-s390x-gnu@4.24.3':
|
||||
resolution: {integrity: sha512-HoojGXTC2CgCcq0Woc/dn12wQUlkNyfH0I1ABK4Ni9YXyFQa86Fkt2Q0nqgLfbhkyfQ6003i3qQk9pLh/SpAYw==}
|
||||
cpu: [s390x]
|
||||
os: [linux]
|
||||
|
||||
'@rollup/rollup-linux-x64-gnu@4.24.3':
|
||||
resolution: {integrity: sha512-mnEOh4iE4USSccBOtcrjF5nj+5/zm6NcNhbSEfR3Ot0pxBwvEn5QVUXcuOwwPkapDtGZ6pT02xLoPaNv06w7KQ==}
|
||||
cpu: [x64]
|
||||
os: [linux]
|
||||
|
||||
'@rollup/rollup-linux-x64-musl@4.24.3':
|
||||
resolution: {integrity: sha512-rMTzawBPimBQkG9NKpNHvquIUTQPzrnPxPbCY1Xt+mFkW7pshvyIS5kYgcf74goxXOQk0CP3EoOC1zcEezKXhw==}
|
||||
cpu: [x64]
|
||||
os: [linux]
|
||||
|
||||
'@rollup/rollup-win32-arm64-msvc@4.24.3':
|
||||
resolution: {integrity: sha512-2lg1CE305xNvnH3SyiKwPVsTVLCg4TmNCF1z7PSHX2uZY2VbUpdkgAllVoISD7JO7zu+YynpWNSKAtOrX3AiuA==}
|
||||
cpu: [arm64]
|
||||
os: [win32]
|
||||
|
||||
'@rollup/rollup-win32-ia32-msvc@4.24.3':
|
||||
resolution: {integrity: sha512-9SjYp1sPyxJsPWuhOCX6F4jUMXGbVVd5obVpoVEi8ClZqo52ViZewA6eFz85y8ezuOA+uJMP5A5zo6Oz4S5rVQ==}
|
||||
cpu: [ia32]
|
||||
os: [win32]
|
||||
|
||||
'@rollup/rollup-win32-x64-msvc@4.24.3':
|
||||
resolution: {integrity: sha512-HGZgRFFYrMrP3TJlq58nR1xy8zHKId25vhmm5S9jETEfDf6xybPxsavFTJaufe2zgOGYJBskGlj49CwtEuFhWQ==}
|
||||
cpu: [x64]
|
||||
os: [win32]
|
||||
|
||||
'@types/estree@1.0.6':
|
||||
resolution: {integrity: sha512-AYnb1nQyY49te+VRAVgmzfcgjYS91mY5P0TKUDCLEM+gNnA+3T6rWITXRLYCpahpqSQbN5cE+gHpnPyXjHWxcw==}
|
||||
|
||||
'@vitejs/plugin-vue@5.1.4':
|
||||
resolution: {integrity: sha512-N2XSI2n3sQqp5w7Y/AN/L2XDjBIRGqXko+eDp42sydYSBeJuSm5a1sLf8zakmo8u7tA8NmBgoDLA1HeOESjp9A==}
|
||||
engines: {node: ^18.0.0 || >=20.0.0}
|
||||
peerDependencies:
|
||||
vite: ^5.0.0
|
||||
vue: ^3.2.25
|
||||
|
||||
'@volar/language-core@2.4.8':
|
||||
resolution: {integrity: sha512-K/GxMOXGq997bO00cdFhTNuR85xPxj0BEEAy+BaqqayTmy9Tmhfgmq2wpJcVspRhcwfgPoE2/mEJa26emUhG/g==}
|
||||
|
||||
'@volar/source-map@2.4.8':
|
||||
resolution: {integrity: sha512-jeWJBkC/WivdelMwxKkpFL811uH/jJ1kVxa+c7OvG48DXc3VrP7pplSWPP2W1dLMqBxD+awRlg55FQQfiup4cA==}
|
||||
|
||||
'@volar/typescript@2.4.8':
|
||||
resolution: {integrity: sha512-6xkIYJ5xxghVBhVywMoPMidDDAFT1OoQeXwa27HSgJ6AiIKRe61RXLoik+14Z7r0JvnblXVsjsRLmCr42SGzqg==}
|
||||
|
||||
'@vue/compiler-core@3.5.12':
|
||||
resolution: {integrity: sha512-ISyBTRMmMYagUxhcpyEH0hpXRd/KqDU4ymofPgl2XAkY9ZhQ+h0ovEZJIiPop13UmR/54oA2cgMDjgroRelaEw==}
|
||||
|
||||
'@vue/compiler-dom@3.5.12':
|
||||
resolution: {integrity: sha512-9G6PbJ03uwxLHKQ3P42cMTi85lDRvGLB2rSGOiQqtXELat6uI4n8cNz9yjfVHRPIu+MsK6TE418Giruvgptckg==}
|
||||
|
||||
'@vue/compiler-sfc@3.5.12':
|
||||
resolution: {integrity: sha512-2k973OGo2JuAa5+ZlekuQJtitI5CgLMOwgl94BzMCsKZCX/xiqzJYzapl4opFogKHqwJk34vfsaKpfEhd1k5nw==}
|
||||
|
||||
'@vue/compiler-ssr@3.5.12':
|
||||
resolution: {integrity: sha512-eLwc7v6bfGBSM7wZOGPmRavSWzNFF6+PdRhE+VFJhNCgHiF8AM7ccoqcv5kBXA2eWUfigD7byekvf/JsOfKvPA==}
|
||||
|
||||
'@vue/compiler-vue2@2.7.16':
|
||||
resolution: {integrity: sha512-qYC3Psj9S/mfu9uVi5WvNZIzq+xnXMhOwbTFKKDD7b1lhpnn71jXSFdTQ+WsIEk0ONCd7VV2IMm7ONl6tbQ86A==}
|
||||
|
||||
'@vue/language-core@2.1.10':
|
||||
resolution: {integrity: sha512-DAI289d0K3AB5TUG3xDp9OuQ71CnrujQwJrQnfuZDwo6eGNf0UoRlPuaVNO+Zrn65PC3j0oB2i7mNmVPggeGeQ==}
|
||||
peerDependencies:
|
||||
typescript: '*'
|
||||
peerDependenciesMeta:
|
||||
typescript:
|
||||
optional: true
|
||||
|
||||
'@vue/reactivity@3.5.12':
|
||||
resolution: {integrity: sha512-UzaN3Da7xnJXdz4Okb/BGbAaomRHc3RdoWqTzlvd9+WBR5m3J39J1fGcHes7U3za0ruYn/iYy/a1euhMEHvTAg==}
|
||||
|
||||
'@vue/runtime-core@3.5.12':
|
||||
resolution: {integrity: sha512-hrMUYV6tpocr3TL3Ad8DqxOdpDe4zuQY4HPY3X/VRh+L2myQO8MFXPAMarIOSGNu0bFAjh1yBkMPXZBqCk62Uw==}
|
||||
|
||||
'@vue/runtime-dom@3.5.12':
|
||||
resolution: {integrity: sha512-q8VFxR9A2MRfBr6/55Q3umyoN7ya836FzRXajPB6/Vvuv0zOPL+qltd9rIMzG/DbRLAIlREmnLsplEF/kotXKA==}
|
||||
|
||||
'@vue/server-renderer@3.5.12':
|
||||
resolution: {integrity: sha512-I3QoeDDeEPZm8yR28JtY+rk880Oqmj43hreIBVTicisFTx/Dl7JpG72g/X7YF8hnQD3IFhkky5i2bPonwrTVPg==}
|
||||
peerDependencies:
|
||||
vue: 3.5.12
|
||||
|
||||
'@vue/shared@3.5.12':
|
||||
resolution: {integrity: sha512-L2RPSAwUFbgZH20etwrXyVyCBu9OxRSi8T/38QsvnkJyvq2LufW2lDCOzm7t/U9C1mkhJGWYfCuFBCmIuNivrg==}
|
||||
|
||||
alien-signals@0.2.0:
|
||||
resolution: {integrity: sha512-StlonZhBBrsPPwrDjiPAiVTf/rolxffLxVPT60Qv/t88BZ81BvUVzHgGqEFvJ1ii8HXtm1+zU2Icr59tfWEcag==}
|
||||
|
||||
balanced-match@1.0.2:
|
||||
resolution: {integrity: sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==}
|
||||
|
||||
brace-expansion@2.0.1:
|
||||
resolution: {integrity: sha512-XnAIvQ8eM+kC6aULx6wuQiwVsnzsi9d3WxzV3FpWTGA19F621kwdbsAcFKXgKUHZWsy+mY6iL1sHTxWEFCytDA==}
|
||||
|
||||
csstype@3.1.3:
|
||||
resolution: {integrity: sha512-M1uQkMl8rQK/szD0LNhtqxIPLpimGm8sOBwU7lLnCpSbTyY3yeU1Vc7l4KT5zT4s/yOxHH5O7tIuuLOCnLADRw==}
|
||||
|
||||
de-indent@1.0.2:
|
||||
resolution: {integrity: sha512-e/1zu3xH5MQryN2zdVaF0OrdNLUbvWxzMbi+iNA6Bky7l1RoP8a2fIbRocyHclXt/arDrrR6lL3TqFD9pMQTsg==}
|
||||
|
||||
entities@4.5.0:
|
||||
resolution: {integrity: sha512-V0hjH4dGPh9Ao5p0MoRY6BVqtwCjhz6vI5LT8AJ55H+4g9/4vbHx1I54fS0XuclLhDHArPQCiMjDxjaL8fPxhw==}
|
||||
engines: {node: '>=0.12'}
|
||||
|
||||
esbuild@0.21.5:
|
||||
resolution: {integrity: sha512-mg3OPMV4hXywwpoDxu3Qda5xCKQi+vCTZq8S9J/EpkhB2HzKXq4SNFZE3+NK93JYxc8VMSep+lOUSC/RVKaBqw==}
|
||||
engines: {node: '>=12'}
|
||||
hasBin: true
|
||||
|
||||
estree-walker@2.0.2:
|
||||
resolution: {integrity: sha512-Rfkk/Mp/DL7JVje3u18FxFujQlTNR2q6QfMSMB7AvCBx91NGj/ba3kCfza0f6dVDbw7YlRf/nDrn7pQrCCyQ/w==}
|
||||
|
||||
fsevents@2.3.3:
|
||||
resolution: {integrity: sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw==}
|
||||
engines: {node: ^8.16.0 || ^10.6.0 || >=11.0.0}
|
||||
os: [darwin]
|
||||
|
||||
he@1.2.0:
|
||||
resolution: {integrity: sha512-F/1DnUGPopORZi0ni+CvrCgHQ5FyEAHRLSApuYWMmrbSwoN2Mn/7k+Gl38gJnR7yyDZk6WLXwiGod1JOWNDKGw==}
|
||||
hasBin: true
|
||||
|
||||
magic-string@0.30.12:
|
||||
resolution: {integrity: sha512-Ea8I3sQMVXr8JhN4z+H/d8zwo+tYDgHE9+5G4Wnrwhs0gaK9fXTKx0Tw5Xwsd/bCPTTZNRAdpyzvoeORe9LYpw==}
|
||||
|
||||
minimatch@9.0.5:
|
||||
resolution: {integrity: sha512-G6T0ZX48xgozx7587koeX9Ys2NYy6Gmv//P89sEte9V9whIapMNF4idKxnW2QtCcLiTWlb/wfCabAtAFWhhBow==}
|
||||
engines: {node: '>=16 || 14 >=14.17'}
|
||||
|
||||
muggle-string@0.4.1:
|
||||
resolution: {integrity: sha512-VNTrAak/KhO2i8dqqnqnAHOa3cYBwXEZe9h+D5h/1ZqFSTEFHdM65lR7RoIqq3tBBYavsOXV84NoHXZ0AkPyqQ==}
|
||||
|
||||
nanoid@3.3.7:
|
||||
resolution: {integrity: sha512-eSRppjcPIatRIMC1U6UngP8XFcz8MQWGQdt1MTBQ7NaAmvXDfvNxbvWV3x2y6CdEUciCSsDHDQZbhYaB8QEo2g==}
|
||||
engines: {node: ^10 || ^12 || ^13.7 || ^14 || >=15.0.1}
|
||||
hasBin: true
|
||||
|
||||
path-browserify@1.0.1:
|
||||
resolution: {integrity: sha512-b7uo2UCUOYZcnF/3ID0lulOJi/bafxa1xPe7ZPsammBSpjSWQkjNxlt635YGS2MiR9GjvuXCtz2emr3jbsz98g==}
|
||||
|
||||
picocolors@1.1.1:
|
||||
resolution: {integrity: sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA==}
|
||||
|
||||
postcss@8.4.47:
|
||||
resolution: {integrity: sha512-56rxCq7G/XfB4EkXq9Egn5GCqugWvDFjafDOThIdMBsI15iqPqR5r15TfSr1YPYeEI19YeaXMCbY6u88Y76GLQ==}
|
||||
engines: {node: ^10 || ^12 || >=14}
|
||||
|
||||
rollup@4.24.3:
|
||||
resolution: {integrity: sha512-HBW896xR5HGmoksbi3JBDtmVzWiPAYqp7wip50hjQ67JbDz61nyoMPdqu1DvVW9asYb2M65Z20ZHsyJCMqMyDg==}
|
||||
engines: {node: '>=18.0.0', npm: '>=8.0.0'}
|
||||
hasBin: true
|
||||
|
||||
semver@7.6.3:
|
||||
resolution: {integrity: sha512-oVekP1cKtI+CTDvHWYFUcMtsK/00wmAEfyqKfNdARm8u1wNVhSgaX7A8d4UuIlUI5e84iEwOhs7ZPYRmzU9U6A==}
|
||||
engines: {node: '>=10'}
|
||||
hasBin: true
|
||||
|
||||
source-map-js@1.2.1:
|
||||
resolution: {integrity: sha512-UXWMKhLOwVKb728IUtQPXxfYU+usdybtUrK/8uGE8CQMvrhOpwvzDBwj0QhSL7MQc7vIsISBG8VQ8+IDQxpfQA==}
|
||||
engines: {node: '>=0.10.0'}
|
||||
|
||||
typescript@5.6.3:
|
||||
resolution: {integrity: sha512-hjcS1mhfuyi4WW8IWtjP7brDrG2cuDZukyrYrSauoXGNgx0S7zceP07adYkJycEr56BOUTNPzbInooiN3fn1qw==}
|
||||
engines: {node: '>=14.17'}
|
||||
hasBin: true
|
||||
|
||||
vite@5.4.10:
|
||||
resolution: {integrity: sha512-1hvaPshuPUtxeQ0hsVH3Mud0ZanOLwVTneA1EgbAM5LhaZEqyPWGRQ7BtaMvUrTDeEaC8pxtj6a6jku3x4z6SQ==}
|
||||
engines: {node: ^18.0.0 || >=20.0.0}
|
||||
hasBin: true
|
||||
peerDependencies:
|
||||
'@types/node': ^18.0.0 || >=20.0.0
|
||||
less: '*'
|
||||
lightningcss: ^1.21.0
|
||||
sass: '*'
|
||||
sass-embedded: '*'
|
||||
stylus: '*'
|
||||
sugarss: '*'
|
||||
terser: ^5.4.0
|
||||
peerDependenciesMeta:
|
||||
'@types/node':
|
||||
optional: true
|
||||
less:
|
||||
optional: true
|
||||
lightningcss:
|
||||
optional: true
|
||||
sass:
|
||||
optional: true
|
||||
sass-embedded:
|
||||
optional: true
|
||||
stylus:
|
||||
optional: true
|
||||
sugarss:
|
||||
optional: true
|
||||
terser:
|
||||
optional: true
|
||||
|
||||
vscode-uri@3.0.8:
|
||||
resolution: {integrity: sha512-AyFQ0EVmsOZOlAnxoFOGOq1SQDWAB7C6aqMGS23svWAllfOaxbuFvcT8D1i8z3Gyn8fraVeZNNmN6e9bxxXkKw==}
|
||||
|
||||
vue-tsc@2.1.10:
|
||||
resolution: {integrity: sha512-RBNSfaaRHcN5uqVqJSZh++Gy/YUzryuv9u1aFWhsammDJXNtUiJMNoJ747lZcQ68wUQFx6E73y4FY3D8E7FGMA==}
|
||||
hasBin: true
|
||||
peerDependencies:
|
||||
typescript: '>=5.0.0'
|
||||
|
||||
vue@3.5.12:
|
||||
resolution: {integrity: sha512-CLVZtXtn2ItBIi/zHZ0Sg1Xkb7+PU32bJJ8Bmy7ts3jxXTcbfsEfBivFYYWz1Hur+lalqGAh65Coin0r+HRUfg==}
|
||||
peerDependencies:
|
||||
typescript: '*'
|
||||
peerDependenciesMeta:
|
||||
typescript:
|
||||
optional: true
|
||||
|
||||
snapshots:
|
||||
|
||||
'@babel/helper-string-parser@7.25.9': {}
|
||||
|
||||
'@babel/helper-validator-identifier@7.25.9': {}
|
||||
|
||||
'@babel/parser@7.26.2':
|
||||
dependencies:
|
||||
'@babel/types': 7.26.0
|
||||
|
||||
'@babel/types@7.26.0':
|
||||
dependencies:
|
||||
'@babel/helper-string-parser': 7.25.9
|
||||
'@babel/helper-validator-identifier': 7.25.9
|
||||
|
||||
'@esbuild/aix-ppc64@0.21.5':
|
||||
optional: true
|
||||
|
||||
'@esbuild/android-arm64@0.21.5':
|
||||
optional: true
|
||||
|
||||
'@esbuild/android-arm@0.21.5':
|
||||
optional: true
|
||||
|
||||
'@esbuild/android-x64@0.21.5':
|
||||
optional: true
|
||||
|
||||
'@esbuild/darwin-arm64@0.21.5':
|
||||
optional: true
|
||||
|
||||
'@esbuild/darwin-x64@0.21.5':
|
||||
optional: true
|
||||
|
||||
'@esbuild/freebsd-arm64@0.21.5':
|
||||
optional: true
|
||||
|
||||
'@esbuild/freebsd-x64@0.21.5':
|
||||
optional: true
|
||||
|
||||
'@esbuild/linux-arm64@0.21.5':
|
||||
optional: true
|
||||
|
||||
'@esbuild/linux-arm@0.21.5':
|
||||
optional: true
|
||||
|
||||
'@esbuild/linux-ia32@0.21.5':
|
||||
optional: true
|
||||
|
||||
'@esbuild/linux-loong64@0.21.5':
|
||||
optional: true
|
||||
|
||||
'@esbuild/linux-mips64el@0.21.5':
|
||||
optional: true
|
||||
|
||||
'@esbuild/linux-ppc64@0.21.5':
|
||||
optional: true
|
||||
|
||||
'@esbuild/linux-riscv64@0.21.5':
|
||||
optional: true
|
||||
|
||||
'@esbuild/linux-s390x@0.21.5':
|
||||
optional: true
|
||||
|
||||
'@esbuild/linux-x64@0.21.5':
|
||||
optional: true
|
||||
|
||||
'@esbuild/netbsd-x64@0.21.5':
|
||||
optional: true
|
||||
|
||||
'@esbuild/openbsd-x64@0.21.5':
|
||||
optional: true
|
||||
|
||||
'@esbuild/sunos-x64@0.21.5':
|
||||
optional: true
|
||||
|
||||
'@esbuild/win32-arm64@0.21.5':
|
||||
optional: true
|
||||
|
||||
'@esbuild/win32-ia32@0.21.5':
|
||||
optional: true
|
||||
|
||||
'@esbuild/win32-x64@0.21.5':
|
||||
optional: true
|
||||
|
||||
'@jridgewell/sourcemap-codec@1.5.0': {}
|
||||
|
||||
'@rollup/rollup-android-arm-eabi@4.24.3':
|
||||
optional: true
|
||||
|
||||
'@rollup/rollup-android-arm64@4.24.3':
|
||||
optional: true
|
||||
|
||||
'@rollup/rollup-darwin-arm64@4.24.3':
|
||||
optional: true
|
||||
|
||||
'@rollup/rollup-darwin-x64@4.24.3':
|
||||
optional: true
|
||||
|
||||
'@rollup/rollup-freebsd-arm64@4.24.3':
|
||||
optional: true
|
||||
|
||||
'@rollup/rollup-freebsd-x64@4.24.3':
|
||||
optional: true
|
||||
|
||||
'@rollup/rollup-linux-arm-gnueabihf@4.24.3':
|
||||
optional: true
|
||||
|
||||
'@rollup/rollup-linux-arm-musleabihf@4.24.3':
|
||||
optional: true
|
||||
|
||||
'@rollup/rollup-linux-arm64-gnu@4.24.3':
|
||||
optional: true
|
||||
|
||||
'@rollup/rollup-linux-arm64-musl@4.24.3':
|
||||
optional: true
|
||||
|
||||
'@rollup/rollup-linux-powerpc64le-gnu@4.24.3':
|
||||
optional: true
|
||||
|
||||
'@rollup/rollup-linux-riscv64-gnu@4.24.3':
|
||||
optional: true
|
||||
|
||||
'@rollup/rollup-linux-s390x-gnu@4.24.3':
|
||||
optional: true
|
||||
|
||||
'@rollup/rollup-linux-x64-gnu@4.24.3':
|
||||
optional: true
|
||||
|
||||
'@rollup/rollup-linux-x64-musl@4.24.3':
|
||||
optional: true
|
||||
|
||||
'@rollup/rollup-win32-arm64-msvc@4.24.3':
|
||||
optional: true
|
||||
|
||||
'@rollup/rollup-win32-ia32-msvc@4.24.3':
|
||||
optional: true
|
||||
|
||||
'@rollup/rollup-win32-x64-msvc@4.24.3':
|
||||
optional: true
|
||||
|
||||
'@types/estree@1.0.6': {}
|
||||
|
||||
'@vitejs/plugin-vue@5.1.4(vite@5.4.10)(vue@3.5.12(typescript@5.6.3))':
|
||||
dependencies:
|
||||
vite: 5.4.10
|
||||
vue: 3.5.12(typescript@5.6.3)
|
||||
|
||||
'@volar/language-core@2.4.8':
|
||||
dependencies:
|
||||
'@volar/source-map': 2.4.8
|
||||
|
||||
'@volar/source-map@2.4.8': {}
|
||||
|
||||
'@volar/typescript@2.4.8':
|
||||
dependencies:
|
||||
'@volar/language-core': 2.4.8
|
||||
path-browserify: 1.0.1
|
||||
vscode-uri: 3.0.8
|
||||
|
||||
'@vue/compiler-core@3.5.12':
|
||||
dependencies:
|
||||
'@babel/parser': 7.26.2
|
||||
'@vue/shared': 3.5.12
|
||||
entities: 4.5.0
|
||||
estree-walker: 2.0.2
|
||||
source-map-js: 1.2.1
|
||||
|
||||
'@vue/compiler-dom@3.5.12':
|
||||
dependencies:
|
||||
'@vue/compiler-core': 3.5.12
|
||||
'@vue/shared': 3.5.12
|
||||
|
||||
'@vue/compiler-sfc@3.5.12':
|
||||
dependencies:
|
||||
'@babel/parser': 7.26.2
|
||||
'@vue/compiler-core': 3.5.12
|
||||
'@vue/compiler-dom': 3.5.12
|
||||
'@vue/compiler-ssr': 3.5.12
|
||||
'@vue/shared': 3.5.12
|
||||
estree-walker: 2.0.2
|
||||
magic-string: 0.30.12
|
||||
postcss: 8.4.47
|
||||
source-map-js: 1.2.1
|
||||
|
||||
'@vue/compiler-ssr@3.5.12':
|
||||
dependencies:
|
||||
'@vue/compiler-dom': 3.5.12
|
||||
'@vue/shared': 3.5.12
|
||||
|
||||
'@vue/compiler-vue2@2.7.16':
|
||||
dependencies:
|
||||
de-indent: 1.0.2
|
||||
he: 1.2.0
|
||||
|
||||
'@vue/language-core@2.1.10(typescript@5.6.3)':
|
||||
dependencies:
|
||||
'@volar/language-core': 2.4.8
|
||||
'@vue/compiler-dom': 3.5.12
|
||||
'@vue/compiler-vue2': 2.7.16
|
||||
'@vue/shared': 3.5.12
|
||||
alien-signals: 0.2.0
|
||||
minimatch: 9.0.5
|
||||
muggle-string: 0.4.1
|
||||
path-browserify: 1.0.1
|
||||
optionalDependencies:
|
||||
typescript: 5.6.3
|
||||
|
||||
'@vue/reactivity@3.5.12':
|
||||
dependencies:
|
||||
'@vue/shared': 3.5.12
|
||||
|
||||
'@vue/runtime-core@3.5.12':
|
||||
dependencies:
|
||||
'@vue/reactivity': 3.5.12
|
||||
'@vue/shared': 3.5.12
|
||||
|
||||
'@vue/runtime-dom@3.5.12':
|
||||
dependencies:
|
||||
'@vue/reactivity': 3.5.12
|
||||
'@vue/runtime-core': 3.5.12
|
||||
'@vue/shared': 3.5.12
|
||||
csstype: 3.1.3
|
||||
|
||||
'@vue/server-renderer@3.5.12(vue@3.5.12(typescript@5.6.3))':
|
||||
dependencies:
|
||||
'@vue/compiler-ssr': 3.5.12
|
||||
'@vue/shared': 3.5.12
|
||||
vue: 3.5.12(typescript@5.6.3)
|
||||
|
||||
'@vue/shared@3.5.12': {}
|
||||
|
||||
alien-signals@0.2.0: {}
|
||||
|
||||
balanced-match@1.0.2: {}
|
||||
|
||||
brace-expansion@2.0.1:
|
||||
dependencies:
|
||||
balanced-match: 1.0.2
|
||||
|
||||
csstype@3.1.3: {}
|
||||
|
||||
de-indent@1.0.2: {}
|
||||
|
||||
entities@4.5.0: {}
|
||||
|
||||
esbuild@0.21.5:
|
||||
optionalDependencies:
|
||||
'@esbuild/aix-ppc64': 0.21.5
|
||||
'@esbuild/android-arm': 0.21.5
|
||||
'@esbuild/android-arm64': 0.21.5
|
||||
'@esbuild/android-x64': 0.21.5
|
||||
'@esbuild/darwin-arm64': 0.21.5
|
||||
'@esbuild/darwin-x64': 0.21.5
|
||||
'@esbuild/freebsd-arm64': 0.21.5
|
||||
'@esbuild/freebsd-x64': 0.21.5
|
||||
'@esbuild/linux-arm': 0.21.5
|
||||
'@esbuild/linux-arm64': 0.21.5
|
||||
'@esbuild/linux-ia32': 0.21.5
|
||||
'@esbuild/linux-loong64': 0.21.5
|
||||
'@esbuild/linux-mips64el': 0.21.5
|
||||
'@esbuild/linux-ppc64': 0.21.5
|
||||
'@esbuild/linux-riscv64': 0.21.5
|
||||
'@esbuild/linux-s390x': 0.21.5
|
||||
'@esbuild/linux-x64': 0.21.5
|
||||
'@esbuild/netbsd-x64': 0.21.5
|
||||
'@esbuild/openbsd-x64': 0.21.5
|
||||
'@esbuild/sunos-x64': 0.21.5
|
||||
'@esbuild/win32-arm64': 0.21.5
|
||||
'@esbuild/win32-ia32': 0.21.5
|
||||
'@esbuild/win32-x64': 0.21.5
|
||||
|
||||
estree-walker@2.0.2: {}
|
||||
|
||||
fsevents@2.3.3:
|
||||
optional: true
|
||||
|
||||
he@1.2.0: {}
|
||||
|
||||
magic-string@0.30.12:
|
||||
dependencies:
|
||||
'@jridgewell/sourcemap-codec': 1.5.0
|
||||
|
||||
minimatch@9.0.5:
|
||||
dependencies:
|
||||
brace-expansion: 2.0.1
|
||||
|
||||
muggle-string@0.4.1: {}
|
||||
|
||||
nanoid@3.3.7: {}
|
||||
|
||||
path-browserify@1.0.1: {}
|
||||
|
||||
picocolors@1.1.1: {}
|
||||
|
||||
postcss@8.4.47:
|
||||
dependencies:
|
||||
nanoid: 3.3.7
|
||||
picocolors: 1.1.1
|
||||
source-map-js: 1.2.1
|
||||
|
||||
rollup@4.24.3:
|
||||
dependencies:
|
||||
'@types/estree': 1.0.6
|
||||
optionalDependencies:
|
||||
'@rollup/rollup-android-arm-eabi': 4.24.3
|
||||
'@rollup/rollup-android-arm64': 4.24.3
|
||||
'@rollup/rollup-darwin-arm64': 4.24.3
|
||||
'@rollup/rollup-darwin-x64': 4.24.3
|
||||
'@rollup/rollup-freebsd-arm64': 4.24.3
|
||||
'@rollup/rollup-freebsd-x64': 4.24.3
|
||||
'@rollup/rollup-linux-arm-gnueabihf': 4.24.3
|
||||
'@rollup/rollup-linux-arm-musleabihf': 4.24.3
|
||||
'@rollup/rollup-linux-arm64-gnu': 4.24.3
|
||||
'@rollup/rollup-linux-arm64-musl': 4.24.3
|
||||
'@rollup/rollup-linux-powerpc64le-gnu': 4.24.3
|
||||
'@rollup/rollup-linux-riscv64-gnu': 4.24.3
|
||||
'@rollup/rollup-linux-s390x-gnu': 4.24.3
|
||||
'@rollup/rollup-linux-x64-gnu': 4.24.3
|
||||
'@rollup/rollup-linux-x64-musl': 4.24.3
|
||||
'@rollup/rollup-win32-arm64-msvc': 4.24.3
|
||||
'@rollup/rollup-win32-ia32-msvc': 4.24.3
|
||||
'@rollup/rollup-win32-x64-msvc': 4.24.3
|
||||
fsevents: 2.3.3
|
||||
|
||||
semver@7.6.3: {}
|
||||
|
||||
source-map-js@1.2.1: {}
|
||||
|
||||
typescript@5.6.3: {}
|
||||
|
||||
vite@5.4.10:
|
||||
dependencies:
|
||||
esbuild: 0.21.5
|
||||
postcss: 8.4.47
|
||||
rollup: 4.24.3
|
||||
optionalDependencies:
|
||||
fsevents: 2.3.3
|
||||
|
||||
vscode-uri@3.0.8: {}
|
||||
|
||||
vue-tsc@2.1.10(typescript@5.6.3):
|
||||
dependencies:
|
||||
'@volar/typescript': 2.4.8
|
||||
'@vue/language-core': 2.1.10(typescript@5.6.3)
|
||||
semver: 7.6.3
|
||||
typescript: 5.6.3
|
||||
|
||||
vue@3.5.12(typescript@5.6.3):
|
||||
dependencies:
|
||||
'@vue/compiler-dom': 3.5.12
|
||||
'@vue/compiler-sfc': 3.5.12
|
||||
'@vue/runtime-dom': 3.5.12
|
||||
'@vue/server-renderer': 3.5.12(vue@3.5.12(typescript@5.6.3))
|
||||
'@vue/shared': 3.5.12
|
||||
optionalDependencies:
|
||||
typescript: 5.6.3
|
7
easytier-web/frontend-lib/postcss.config.js
Normal file
7
easytier-web/frontend-lib/postcss.config.js
Normal file
@@ -0,0 +1,7 @@
|
||||
export default {
|
||||
plugins: {
|
||||
tailwindcss: {},
|
||||
autoprefixer: {},
|
||||
"postcss-nested": {},
|
||||
},
|
||||
}
|
1
easytier-web/frontend-lib/public/vite.svg
Normal file
1
easytier-web/frontend-lib/public/vite.svg
Normal file
@@ -0,0 +1 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" aria-hidden="true" role="img" class="iconify iconify--logos" width="31.88" height="32" preserveAspectRatio="xMidYMid meet" viewBox="0 0 256 257"><defs><linearGradient id="IconifyId1813088fe1fbc01fb466" x1="-.828%" x2="57.636%" y1="7.652%" y2="78.411%"><stop offset="0%" stop-color="#41D1FF"></stop><stop offset="100%" stop-color="#BD34FE"></stop></linearGradient><linearGradient id="IconifyId1813088fe1fbc01fb467" x1="43.376%" x2="50.316%" y1="2.242%" y2="89.03%"><stop offset="0%" stop-color="#FFEA83"></stop><stop offset="8.333%" stop-color="#FFDD35"></stop><stop offset="100%" stop-color="#FFA800"></stop></linearGradient></defs><path fill="url(#IconifyId1813088fe1fbc01fb466)" d="M255.153 37.938L134.897 252.976c-2.483 4.44-8.862 4.466-11.382.048L.875 37.958c-2.746-4.814 1.371-10.646 6.827-9.67l120.385 21.517a6.537 6.537 0 0 0 2.322-.004l117.867-21.483c5.438-.991 9.574 4.796 6.877 9.62Z"></path><path fill="url(#IconifyId1813088fe1fbc01fb467)" d="M185.432.063L96.44 17.501a3.268 3.268 0 0 0-2.634 3.014l-5.474 92.456a3.268 3.268 0 0 0 3.997 3.378l24.777-5.718c2.318-.535 4.413 1.507 3.936 3.838l-7.361 36.047c-.495 2.426 1.782 4.5 4.151 3.78l15.304-4.649c2.372-.72 4.652 1.36 4.15 3.788l-11.698 56.621c-.732 3.542 3.979 5.473 5.943 2.437l1.313-2.028l72.516-144.72c1.215-2.423-.88-5.186-3.54-4.672l-25.505 4.922c-2.396.462-4.435-1.77-3.759-4.114l16.646-57.705c.677-2.35-1.37-4.583-3.769-4.113Z"></path></svg>
|
After Width: | Height: | Size: 1.5 KiB |
1
easytier-web/frontend-lib/src/assets/vue.svg
Normal file
1
easytier-web/frontend-lib/src/assets/vue.svg
Normal file
@@ -0,0 +1 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" aria-hidden="true" role="img" class="iconify iconify--logos" width="37.07" height="36" preserveAspectRatio="xMidYMid meet" viewBox="0 0 256 198"><path fill="#41B883" d="M204.8 0H256L128 220.8L0 0h97.92L128 51.2L157.44 0h47.36Z"></path><path fill="#41B883" d="m0 0l128 220.8L256 0h-51.2L128 132.48L50.56 0H0Z"></path><path fill="#35495E" d="M50.56 0L128 133.12L204.8 0h-47.36L128 51.2L97.92 0H50.56Z"></path></svg>
|
After Width: | Height: | Size: 496 B |
275
easytier-web/frontend-lib/src/components/Config.vue
Normal file
275
easytier-web/frontend-lib/src/components/Config.vue
Normal file
@@ -0,0 +1,275 @@
|
||||
<script setup lang="ts">
|
||||
import InputGroup from 'primevue/inputgroup'
|
||||
import InputGroupAddon from 'primevue/inputgroupaddon'
|
||||
import { SelectButton, Checkbox, InputText, InputNumber, AutoComplete, Panel, Divider, ToggleButton, Button } from 'primevue'
|
||||
import { DEFAULT_NETWORK_CONFIG, NetworkConfig, NetworkingMethod } from '../types/network'
|
||||
import { defineProps, defineEmits, ref, } from 'vue'
|
||||
import { useI18n } from 'vue-i18n'
|
||||
|
||||
const props = defineProps<{
|
||||
configInvalid?: boolean
|
||||
hostname?: string
|
||||
}>()
|
||||
|
||||
defineEmits(['runNetwork'])
|
||||
|
||||
const curNetwork = defineModel('curNetwork', {
|
||||
type: Object as () => NetworkConfig,
|
||||
default: DEFAULT_NETWORK_CONFIG,
|
||||
})
|
||||
|
||||
const { t } = useI18n()
|
||||
|
||||
const networking_methods = ref([
|
||||
{ value: NetworkingMethod.PublicServer, label: () => t('public_server') },
|
||||
{ value: NetworkingMethod.Manual, label: () => t('manual') },
|
||||
{ value: NetworkingMethod.Standalone, label: () => t('standalone') },
|
||||
])
|
||||
|
||||
const protos: { [proto: string]: number } = { tcp: 11010, udp: 11010, wg: 11011, ws: 11011, wss: 11012 }
|
||||
|
||||
function searchUrlSuggestions(e: { query: string }): string[] {
|
||||
const query = e.query
|
||||
const ret = []
|
||||
// if query match "^\w+:.*", then no proto prefix
|
||||
if (query.match(/^\w+:.*/)) {
|
||||
// if query is a valid url, then add to suggestions
|
||||
try {
|
||||
// eslint-disable-next-line no-new
|
||||
new URL(query)
|
||||
ret.push(query)
|
||||
}
|
||||
catch { }
|
||||
}
|
||||
else {
|
||||
for (const proto in protos) {
|
||||
let item = `${proto}://${query}`
|
||||
// if query match ":\d+$", then no port suffix
|
||||
if (!query.match(/:\d+$/)) {
|
||||
item += `:${protos[proto]}`
|
||||
}
|
||||
ret.push(item)
|
||||
}
|
||||
}
|
||||
|
||||
return ret
|
||||
}
|
||||
|
||||
const publicServerSuggestions = ref([''])
|
||||
|
||||
function searchPresetPublicServers(e: { query: string }) {
|
||||
const presetPublicServers = [
|
||||
'tcp://public.easytier.top:11010',
|
||||
]
|
||||
|
||||
const query = e.query
|
||||
// if query is sub string of presetPublicServers, add to suggestions
|
||||
let ret = presetPublicServers.filter(item => item.includes(query))
|
||||
// add additional suggestions
|
||||
if (query.length > 0) {
|
||||
ret = ret.concat(searchUrlSuggestions(e))
|
||||
}
|
||||
|
||||
publicServerSuggestions.value = ret
|
||||
}
|
||||
|
||||
const peerSuggestions = ref([''])
|
||||
|
||||
function searchPeerSuggestions(e: { query: string }) {
|
||||
peerSuggestions.value = searchUrlSuggestions(e)
|
||||
}
|
||||
|
||||
const inetSuggestions = ref([''])
|
||||
|
||||
function searchInetSuggestions(e: { query: string }) {
|
||||
if (e.query.search('/') >= 0) {
|
||||
inetSuggestions.value = [e.query]
|
||||
} else {
|
||||
const ret = []
|
||||
for (let i = 0; i < 32; i++) {
|
||||
ret.push(`${e.query}/${i}`)
|
||||
}
|
||||
inetSuggestions.value = ret
|
||||
}
|
||||
}
|
||||
|
||||
const listenerSuggestions = ref([''])
|
||||
|
||||
function searchListenerSuggestions(e: { query: string }) {
|
||||
const ret = []
|
||||
|
||||
for (const proto in protos) {
|
||||
let item = `${proto}://0.0.0.0:`
|
||||
// if query is a number, use it as port
|
||||
if (e.query.match(/^\d+$/)) {
|
||||
item += e.query
|
||||
}
|
||||
else {
|
||||
item += protos[proto]
|
||||
}
|
||||
|
||||
if (item.includes(e.query)) {
|
||||
ret.push(item)
|
||||
}
|
||||
}
|
||||
|
||||
if (ret.length === 0) {
|
||||
ret.push(e.query)
|
||||
}
|
||||
|
||||
listenerSuggestions.value = ret
|
||||
}
|
||||
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div class="frontend-lib">
|
||||
<div class="flex flex-col h-full">
|
||||
<div class="flex flex-col">
|
||||
<div class="w-10/12 self-center ">
|
||||
<Panel :header="t('basic_settings')">
|
||||
<div class="flex flex-col gap-y-2">
|
||||
<div class="flex flex-row gap-x-9 flex-wrap">
|
||||
<div class="flex flex-col gap-2 basis-5/12 grow">
|
||||
<div class="flex items-center" for="virtual_ip">
|
||||
<label class="mr-2"> {{ t('virtual_ipv4') }} </label>
|
||||
<Checkbox v-model="curNetwork.dhcp" input-id="virtual_ip_auto" :binary="true" />
|
||||
|
||||
<label for="virtual_ip_auto" class="ml-2">
|
||||
{{ t('virtual_ipv4_dhcp') }}
|
||||
</label>
|
||||
</div>
|
||||
<InputGroup>
|
||||
<InputText id="virtual_ip" v-model="curNetwork.virtual_ipv4" :disabled="curNetwork.dhcp"
|
||||
aria-describedby="virtual_ipv4-help" />
|
||||
<InputGroupAddon>
|
||||
<span>/</span>
|
||||
</InputGroupAddon>
|
||||
<InputNumber v-model="curNetwork.network_length" :disabled="curNetwork.dhcp"
|
||||
inputId="horizontal-buttons" showButtons :step="1" mode="decimal" :min="1" :max="32" fluid
|
||||
class="max-w-20" />
|
||||
</InputGroup>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="flex flex-row gap-x-9 flex-wrap">
|
||||
<div class="flex flex-col gap-2 basis-5/12 grow">
|
||||
<label for="network_name">{{ t('network_name') }}</label>
|
||||
<InputText id="network_name" v-model="curNetwork.network_name" aria-describedby="network_name-help" />
|
||||
</div>
|
||||
<div class="flex flex-col gap-2 basis-5/12 grow">
|
||||
<label for="network_secret">{{ t('network_secret') }}</label>
|
||||
<InputText id="network_secret" v-model="curNetwork.network_secret"
|
||||
aria-describedby="network_secret-help" />
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="flex flex-row gap-x-9 flex-wrap">
|
||||
<div class="flex flex-col gap-2 basis-5/12 grow">
|
||||
<label for="nm">{{ t('networking_method') }}</label>
|
||||
<SelectButton v-model="curNetwork.networking_method" :options="networking_methods"
|
||||
:option-label="(v) => v.label()" option-value="value" />
|
||||
<div class="items-center flex flex-row p-fluid gap-x-1">
|
||||
<AutoComplete v-if="curNetwork.networking_method === NetworkingMethod.Manual" id="chips"
|
||||
v-model="curNetwork.peer_urls" :placeholder="t('chips_placeholder', ['tcp://8.8.8.8:11010'])"
|
||||
class="grow" multiple fluid :suggestions="peerSuggestions" @complete="searchPeerSuggestions" />
|
||||
|
||||
<AutoComplete v-if="curNetwork.networking_method === NetworkingMethod.PublicServer"
|
||||
v-model="curNetwork.public_server_url" :suggestions="publicServerSuggestions"
|
||||
:virtual-scroller-options="{ itemSize: 38 }" class="grow" dropdown :complete-on-focus="true"
|
||||
@complete="searchPresetPublicServers" />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</Panel>
|
||||
|
||||
<Divider />
|
||||
|
||||
<Panel :header="t('advanced_settings')" toggleable collapsed>
|
||||
<div class="flex flex-col gap-y-2">
|
||||
<div class="flex flex-row gap-x-9 flex-wrap">
|
||||
<div class="flex flex-col gap-2 basis-5/12 grow">
|
||||
<div class="flex items-center">
|
||||
<Checkbox v-model="curNetwork.latency_first" input-id="use_latency_first" :binary="true" />
|
||||
<label for="use_latency_first" class="ml-2"> {{ t('use_latency_first') }} </label>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="flex flex-row gap-x-9 flex-wrap">
|
||||
<div class="flex flex-col gap-2 basis-5/12 grow">
|
||||
<label for="hostname">{{ t('hostname') }}</label>
|
||||
<InputText id="hostname" v-model="curNetwork.hostname" aria-describedby="hostname-help" :format="true"
|
||||
:placeholder="t('hostname_placeholder', [props.hostname])" />
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="flex flex-row gap-x-9 flex-wrap w-full">
|
||||
<div class="flex flex-col gap-2 grow p-fluid">
|
||||
<label for="username">{{ t('proxy_cidrs') }}</label>
|
||||
<AutoComplete id="subnet-proxy" v-model="curNetwork.proxy_cidrs"
|
||||
:placeholder="t('chips_placeholder', ['10.0.0.0/24'])" class="w-full" multiple fluid
|
||||
:suggestions="inetSuggestions" @complete="searchInetSuggestions" />
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="flex flex-row gap-x-9 flex-wrap ">
|
||||
<div class="flex flex-col gap-2 grow">
|
||||
<label for="username">VPN Portal</label>
|
||||
<ToggleButton v-model="curNetwork.enable_vpn_portal" on-icon="pi pi-check" off-icon="pi pi-times"
|
||||
:on-label="t('off_text')" :off-label="t('on_text')" class="w-48" />
|
||||
<div v-if="curNetwork.enable_vpn_portal" class="items-center flex flex-row gap-x-4">
|
||||
<div class="min-w-64">
|
||||
<InputGroup>
|
||||
<InputText v-model="curNetwork.vpn_portal_client_network_addr"
|
||||
:placeholder="t('vpn_portal_client_network')" />
|
||||
<InputGroupAddon>
|
||||
<span>/{{ curNetwork.vpn_portal_client_network_len }}</span>
|
||||
</InputGroupAddon>
|
||||
</InputGroup>
|
||||
|
||||
<InputNumber v-model="curNetwork.vpn_portal_listen_port" :allow-empty="false" :format="false"
|
||||
:min="0" :max="65535" class="w-8/12" fluid />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="flex flex-row gap-x-9 flex-wrap">
|
||||
<div class="flex flex-col gap-2 grow p-fluid">
|
||||
<label for="listener_urls">{{ t('listener_urls') }}</label>
|
||||
<AutoComplete id="listener_urls" v-model="curNetwork.listener_urls" :suggestions="listenerSuggestions"
|
||||
class="w-full" dropdown :complete-on-focus="true"
|
||||
:placeholder="t('chips_placeholder', ['tcp://1.1.1.1:11010'])" multiple
|
||||
@complete="searchListenerSuggestions" />
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="flex flex-row gap-x-9 flex-wrap">
|
||||
<div class="flex flex-col gap-2 basis-5/12 grow">
|
||||
<label for="rpc_port">{{ t('rpc_port') }}</label>
|
||||
<InputNumber id="rpc_port" v-model="curNetwork.rpc_port" aria-describedby="rpc_port-help"
|
||||
:format="false" :min="0" :max="65535" />
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="flex flex-row gap-x-9 flex-wrap">
|
||||
<div class="flex flex-col gap-2 basis-5/12 grow">
|
||||
<label for="dev_name">{{ t('dev_name') }}</label>
|
||||
<InputText id="dev_name" v-model="curNetwork.dev_name" aria-describedby="dev_name-help" :format="true"
|
||||
:placeholder="t('dev_name_placeholder')" />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</Panel>
|
||||
|
||||
<div class="flex pt-6 justify-center">
|
||||
<Button :label="t('run_network')" icon="pi pi-arrow-right" icon-pos="right" :disabled="configInvalid"
|
||||
@click="$emit('runNetwork', curNetwork)" />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
35
easytier-web/frontend-lib/src/components/HumanEvent.vue
Normal file
35
easytier-web/frontend-lib/src/components/HumanEvent.vue
Normal file
@@ -0,0 +1,35 @@
|
||||
<script setup lang="ts">
|
||||
import { useI18n } from 'vue-i18n';
|
||||
import { EventType } from '../types/network'
|
||||
import { computed } from 'vue';
|
||||
import { Fieldset } from 'primevue';
|
||||
|
||||
const props = defineProps<{
|
||||
event: {
|
||||
[key: string]: any
|
||||
}
|
||||
}>()
|
||||
const { t } = useI18n()
|
||||
|
||||
const eventKey = computed(() => {
|
||||
const key = Object.keys(props.event)[0]
|
||||
return Object.keys(EventType).includes(key) ? key : 'Unknown'
|
||||
})
|
||||
|
||||
const eventValue = computed(() => {
|
||||
const value = props.event[eventKey.value]
|
||||
return typeof value === 'object' ? value : value
|
||||
})
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<Fieldset :legend="t(`event.${eventKey}`)">
|
||||
<template v-if="eventKey !== 'Unknown'">
|
||||
<div v-if="event.DhcpIpv4Changed">
|
||||
{{ `${eventValue[0]} -> ${eventValue[1]}` }}
|
||||
</div>
|
||||
<pre v-else>{{ eventValue }}</pre>
|
||||
</template>
|
||||
<pre v-else>{{ eventValue }}</pre>
|
||||
</Fieldset>
|
||||
</template>
|
@@ -1,31 +1,29 @@
|
||||
<script setup lang="ts">
|
||||
import type { NodeInfo } from '~/types/network'
|
||||
const { t } = useI18n()
|
||||
import { useTimeAgo } from '@vueuse/core'
|
||||
import { IPv4 } from 'ip-num/IPNumber'
|
||||
import { NetworkInstance, type NodeInfo, type PeerRoutePair } from '../types/network'
|
||||
import { useI18n } from 'vue-i18n';
|
||||
import { computed, onMounted, onUnmounted, ref } from 'vue';
|
||||
import { ipv4InetToString, ipv4ToString, ipv6ToString } from '../modules/utils';
|
||||
import { DataTable, Column, Tag, Chip, Button, Dialog, ScrollPanel, Timeline, Divider, Card, } from 'primevue';
|
||||
|
||||
const props = defineProps<{
|
||||
instanceId?: string
|
||||
curNetworkInst: NetworkInstance | null,
|
||||
}>()
|
||||
|
||||
const networkStore = useNetworkStore()
|
||||
|
||||
const curNetwork = computed(() => {
|
||||
if (props.instanceId) {
|
||||
// console.log('instanceId', props.instanceId)
|
||||
const c = networkStore.networkList.find(n => n.instance_id === props.instanceId)
|
||||
if (c !== undefined)
|
||||
return c
|
||||
}
|
||||
|
||||
return networkStore.curNetwork
|
||||
})
|
||||
|
||||
const curNetworkInst = computed(() => {
|
||||
return networkStore.networkInstances.find(n => n.instance_id === curNetwork.value.instance_id)
|
||||
})
|
||||
const { t } = useI18n()
|
||||
|
||||
const peerRouteInfos = computed(() => {
|
||||
if (curNetworkInst.value)
|
||||
return curNetworkInst.value.detail?.peer_route_pairs || []
|
||||
if (props.curNetworkInst) {
|
||||
const my_node_info = props.curNetworkInst.detail?.my_node_info
|
||||
return [{
|
||||
route: {
|
||||
ipv4_addr: my_node_info?.virtual_ipv4,
|
||||
hostname: my_node_info?.hostname,
|
||||
version: my_node_info?.version,
|
||||
},
|
||||
}, ...(props.curNetworkInst.detail?.peer_route_pairs || [])]
|
||||
}
|
||||
|
||||
return []
|
||||
})
|
||||
@@ -33,8 +31,9 @@ const peerRouteInfos = computed(() => {
|
||||
function routeCost(info: any) {
|
||||
if (info.route) {
|
||||
const cost = info.route.cost
|
||||
return cost === 1 ? 'p2p' : `relay(${cost})`
|
||||
return cost ? cost === 1 ? 'p2p' : `relay(${cost})` : t('status.local')
|
||||
}
|
||||
|
||||
return '?'
|
||||
}
|
||||
|
||||
@@ -73,34 +72,45 @@ function humanFileSize(bytes: number, si = false, dp = 1) {
|
||||
return `${bytes.toFixed(dp)} ${units[u]}`
|
||||
}
|
||||
|
||||
function latencyMs(info: any) {
|
||||
function latencyMs(info: PeerRoutePair) {
|
||||
let lat_us_sum = statsCommon(info, 'stats.latency_us')
|
||||
if (lat_us_sum === undefined)
|
||||
return ''
|
||||
lat_us_sum = lat_us_sum / 1000 / info.peer.conns.length
|
||||
lat_us_sum = lat_us_sum / 1000 / info.peer!.conns.length
|
||||
return `${lat_us_sum % 1 > 0 ? Math.round(lat_us_sum) + 1 : Math.round(lat_us_sum)}ms`
|
||||
}
|
||||
|
||||
function txBytes(info: any) {
|
||||
function txBytes(info: PeerRoutePair) {
|
||||
const tx = statsCommon(info, 'stats.tx_bytes')
|
||||
return tx ? humanFileSize(tx) : ''
|
||||
}
|
||||
|
||||
function rxBytes(info: any) {
|
||||
function rxBytes(info: PeerRoutePair) {
|
||||
const rx = statsCommon(info, 'stats.rx_bytes')
|
||||
return rx ? humanFileSize(rx) : ''
|
||||
}
|
||||
|
||||
function lossRate(info: any) {
|
||||
function lossRate(info: PeerRoutePair) {
|
||||
const lossRate = statsCommon(info, 'loss_rate')
|
||||
return lossRate !== undefined ? `${Math.round(lossRate * 100)}%` : ''
|
||||
}
|
||||
|
||||
function version(info: PeerRoutePair) {
|
||||
return info.route.version === '' ? 'unknown' : info.route.version
|
||||
}
|
||||
|
||||
function ipFormat(info: PeerRoutePair) {
|
||||
const ip = info.route.ipv4_addr
|
||||
if (typeof ip === 'string')
|
||||
return ip
|
||||
return ip ? `${IPv4.fromNumber(ip.address.addr)}/${ip.network_length}` : ''
|
||||
}
|
||||
|
||||
const myNodeInfo = computed(() => {
|
||||
if (!curNetworkInst.value)
|
||||
if (!props.curNetworkInst)
|
||||
return {} as NodeInfo
|
||||
|
||||
return curNetworkInst.value.detail?.my_node_info
|
||||
return props.curNetworkInst.detail?.my_node_info
|
||||
})
|
||||
|
||||
interface Chip {
|
||||
@@ -109,18 +119,26 @@ interface Chip {
|
||||
}
|
||||
|
||||
const myNodeInfoChips = computed(() => {
|
||||
if (!curNetworkInst.value)
|
||||
if (!props.curNetworkInst)
|
||||
return []
|
||||
|
||||
const chips: Array<Chip> = []
|
||||
const my_node_info = curNetworkInst.value.detail?.my_node_info
|
||||
const my_node_info = props.curNetworkInst.detail?.my_node_info
|
||||
if (!my_node_info)
|
||||
return chips
|
||||
|
||||
// virtual ipv4
|
||||
// TUN Device Name
|
||||
const dev_name = props.curNetworkInst.detail?.dev_name
|
||||
if (dev_name) {
|
||||
chips.push({
|
||||
label: `TUN Device Name: ${dev_name}`,
|
||||
icon: '',
|
||||
} as Chip)
|
||||
}
|
||||
|
||||
// virtual ipv4
|
||||
chips.push({
|
||||
label: `Virtual IPv4: ${my_node_info.virtual_ipv4}`,
|
||||
label: `Virtual IPv4: ${ipv4InetToString(my_node_info.virtual_ipv4)}`,
|
||||
icon: '',
|
||||
} as Chip)
|
||||
|
||||
@@ -128,7 +146,7 @@ const myNodeInfoChips = computed(() => {
|
||||
const local_ipv4s = my_node_info.ips?.interface_ipv4s
|
||||
for (const [idx, ip] of local_ipv4s?.entries()) {
|
||||
chips.push({
|
||||
label: `Local IPv4 ${idx}: ${ip}`,
|
||||
label: `Local IPv4 ${idx}: ${ipv4ToString(ip)}`,
|
||||
icon: '',
|
||||
} as Chip)
|
||||
}
|
||||
@@ -137,7 +155,7 @@ const myNodeInfoChips = computed(() => {
|
||||
const local_ipv6s = my_node_info.ips?.interface_ipv6s
|
||||
for (const [idx, ip] of local_ipv6s?.entries()) {
|
||||
chips.push({
|
||||
label: `Local IPv6 ${idx}: ${ip}`,
|
||||
label: `Local IPv6 ${idx}: ${ipv6ToString(ip)}`,
|
||||
icon: '',
|
||||
} as Chip)
|
||||
}
|
||||
@@ -146,7 +164,15 @@ const myNodeInfoChips = computed(() => {
|
||||
const public_ip = my_node_info.ips?.public_ipv4
|
||||
if (public_ip) {
|
||||
chips.push({
|
||||
label: `Public IP: ${public_ip}`,
|
||||
label: `Public IP: ${IPv4.fromNumber(public_ip.addr)}`,
|
||||
icon: '',
|
||||
} as Chip)
|
||||
}
|
||||
|
||||
const public_ipv6 = my_node_info.ips?.public_ipv6
|
||||
if (public_ipv6) {
|
||||
chips.push({
|
||||
label: `Public IPv6: ${ipv6ToString(public_ipv6)}`,
|
||||
icon: '',
|
||||
} as Chip)
|
||||
}
|
||||
@@ -155,7 +181,7 @@ const myNodeInfoChips = computed(() => {
|
||||
const listeners = my_node_info.listeners
|
||||
for (const [idx, listener] of listeners?.entries()) {
|
||||
chips.push({
|
||||
label: `Listener ${idx}: ${listener}`,
|
||||
label: `Listener ${idx}: ${listener.url}`,
|
||||
icon: '',
|
||||
} as Chip)
|
||||
}
|
||||
@@ -171,6 +197,8 @@ const myNodeInfoChips = computed(() => {
|
||||
PortRestricted = 5,
|
||||
Symmetric = 6,
|
||||
SymUdpFirewall = 7,
|
||||
SymmetricEasyInc = 8,
|
||||
SymmetricEasyDec = 9,
|
||||
};
|
||||
const udpNatType: NatType = my_node_info.stun_info?.udp_nat_type
|
||||
if (udpNatType !== undefined) {
|
||||
@@ -183,6 +211,8 @@ const myNodeInfoChips = computed(() => {
|
||||
[NatType.PortRestricted]: 'Port Restricted',
|
||||
[NatType.Symmetric]: 'Symmetric',
|
||||
[NatType.SymUdpFirewall]: 'Symmetric UDP Firewall',
|
||||
[NatType.SymmetricEasyInc]: 'Symmetric Easy Inc',
|
||||
[NatType.SymmetricEasyDec]: 'Symmetric Easy Dec',
|
||||
}
|
||||
|
||||
chips.push({
|
||||
@@ -261,28 +291,31 @@ function showVpnPortalConfig() {
|
||||
}
|
||||
|
||||
function showEventLogs() {
|
||||
const detail = curNetworkInst.value?.detail
|
||||
const detail = props.curNetworkInst?.detail
|
||||
if (!detail)
|
||||
return
|
||||
|
||||
dialogContent.value = detail.events
|
||||
dialogContent.value = detail.events.map((event: string) => JSON.parse(event))
|
||||
dialogHeader.value = 'event_log'
|
||||
dialogVisible.value = true
|
||||
}
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div>
|
||||
<Dialog v-model:visible="dialogVisible" modal :header="t(dialogHeader)" :style="{ width: '70%' }">
|
||||
<Panel>
|
||||
<ScrollPanel style="width: 100%; height: 400px">
|
||||
<pre>{{ dialogContent }}</pre>
|
||||
</ScrollPanel>
|
||||
</Panel>
|
||||
<Divider />
|
||||
<div class="flex justify-content-end gap-2">
|
||||
<Button type="button" :label="t('close')" @click="dialogVisible = false" />
|
||||
</div>
|
||||
<div class="frontend-lib">
|
||||
<Dialog v-model:visible="dialogVisible" modal :header="t(dialogHeader)" class="w-2/3 h-auto">
|
||||
<ScrollPanel v-if="dialogHeader === 'vpn_portal_config'">
|
||||
<pre>{{ dialogContent }}</pre>
|
||||
</ScrollPanel>
|
||||
<Timeline v-else :value="dialogContent">
|
||||
<template #opposite="slotProps">
|
||||
<small class="text-surface-500 dark:text-surface-400">{{ useTimeAgo(Date.parse(slotProps.item.time))
|
||||
}}</small>
|
||||
</template>
|
||||
<template #content="slotProps">
|
||||
<HumanEvent :event="slotProps.item.event" />
|
||||
</template>
|
||||
</Timeline>
|
||||
</Dialog>
|
||||
|
||||
<Card v-if="curNetworkInst?.error_msg">
|
||||
@@ -290,7 +323,7 @@ function showEventLogs() {
|
||||
Run Network Error
|
||||
</template>
|
||||
<template #content>
|
||||
<div class="flex flex-column gap-y-5">
|
||||
<div class="flex flex-col gap-y-5">
|
||||
<div class="text-red-500">
|
||||
{{ curNetworkInst.error_msg }}
|
||||
</div>
|
||||
@@ -304,12 +337,9 @@ function showEventLogs() {
|
||||
{{ t('my_node_info') }}
|
||||
</template>
|
||||
<template #content>
|
||||
<div class="flex w-full flex-column gap-y-5">
|
||||
<div class="flex w-full flex-col gap-y-5">
|
||||
<div class="m-0 flex flex-row justify-center gap-x-5">
|
||||
<div
|
||||
class="rounded-full w-32 h-32 flex flex-column align-items-center pt-4"
|
||||
style="border: 1px solid green"
|
||||
>
|
||||
<div class="rounded-full w-32 h-32 flex flex-col items-center pt-6" style="border: 1px solid green">
|
||||
<div class="font-bold">
|
||||
{{ t('peer_count') }}
|
||||
</div>
|
||||
@@ -318,10 +348,7 @@ function showEventLogs() {
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div
|
||||
class="rounded-full w-32 h-32 flex flex-column align-items-center pt-4"
|
||||
style="border: 1px solid purple"
|
||||
>
|
||||
<div class="rounded-full w-32 h-32 flex flex-col items-center pt-6" style="border: 1px solid purple">
|
||||
<div class="font-bold">
|
||||
{{ t('upload') }}
|
||||
</div>
|
||||
@@ -330,10 +357,7 @@ function showEventLogs() {
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div
|
||||
class="rounded-full w-32 h-32 flex flex-column align-items-center pt-4"
|
||||
style="border: 1px solid fuchsia"
|
||||
>
|
||||
<div class="rounded-full w-32 h-32 flex flex-col items-center pt-6" style="border: 1px solid fuchsia">
|
||||
<div class="font-bold">
|
||||
{{ t('download') }}
|
||||
</div>
|
||||
@@ -343,11 +367,9 @@ function showEventLogs() {
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="flex flex-row align-items-center flex-wrap w-full max-h-40 overflow-scroll">
|
||||
<Chip
|
||||
v-for="(chip, i) in myNodeInfoChips" :key="i" :label="chip.label" :icon="chip.icon"
|
||||
class="mr-2 mt-2 text-sm"
|
||||
/>
|
||||
<div class="flex flex-row items-center flex-wrap w-full max-h-40 overflow-scroll">
|
||||
<Chip v-for="(chip, i) in myNodeInfoChips" :key="i" :label="chip.label" :icon="chip.icon"
|
||||
class="mr-2 mt-2 text-sm" />
|
||||
</div>
|
||||
|
||||
<div v-if="myNodeInfo" class="m-0 flex flex-row justify-center gap-x-5 text-sm">
|
||||
@@ -365,17 +387,44 @@ function showEventLogs() {
|
||||
{{ t('peer_info') }}
|
||||
</template>
|
||||
<template #content>
|
||||
<DataTable :value="peerRouteInfos" column-resize-mode="fit" table-style="width: 100%">
|
||||
<Column field="route.ipv4_addr" style="width: 100px;" :header="t('virtual_ipv4')" />
|
||||
<Column field="route.hostname" style="max-width: 250px;" :header="t('hostname')" />
|
||||
<Column :field="routeCost" style="width: 100px;" :header="t('route_cost')" />
|
||||
<Column :field="latencyMs" style="width: 80px;" :header="t('latency')" />
|
||||
<Column :field="txBytes" style="width: 80px;" :header="t('upload_bytes')" />
|
||||
<Column :field="rxBytes" style="width: 80px;" :header="t('download_bytes')" />
|
||||
<Column :field="lossRate" style="width: 100px;" :header="t('loss_rate')" />
|
||||
<DataTable :value="peerRouteInfos" column-resize-mode="fit" table-class="w-full">
|
||||
<Column :field="ipFormat" :header="t('virtual_ipv4')" />
|
||||
<Column :header="t('hostname')">
|
||||
<template #body="slotProps">
|
||||
<div v-if="!slotProps.data.route.cost || !slotProps.data.route.feature_flag.is_public_server"
|
||||
v-tooltip="slotProps.data.route.hostname">
|
||||
{{
|
||||
slotProps.data.route.hostname }}
|
||||
</div>
|
||||
<div v-else v-tooltip="slotProps.data.route.hostname" class="space-x-1">
|
||||
<Tag v-if="slotProps.data.route.feature_flag.is_public_server" severity="info" value="Info">
|
||||
{{ t('status.server') }}
|
||||
</Tag>
|
||||
<Tag v-if="slotProps.data.route.feature_flag.avoid_relay_data" severity="warn" value="Warn">
|
||||
{{ t('status.relay') }}
|
||||
</Tag>
|
||||
</div>
|
||||
</template>
|
||||
</Column>
|
||||
<Column :field="routeCost" :header="t('route_cost')" />
|
||||
<Column :field="latencyMs" :header="t('latency')" />
|
||||
<Column :field="txBytes" :header="t('upload_bytes')" />
|
||||
<Column :field="rxBytes" :header="t('download_bytes')" />
|
||||
<Column :field="lossRate" :header="t('loss_rate')" />
|
||||
<Column :header="t('status.version')">
|
||||
<template #body="slotProps">
|
||||
<span>{{ version(slotProps.data) }}</span>
|
||||
</template>
|
||||
</Column>
|
||||
</DataTable>
|
||||
</template>
|
||||
</Card>
|
||||
</template>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<style lang="postcss" scoped>
|
||||
.p-timeline :deep(.p-timeline-event-opposite) {
|
||||
@apply flex-none;
|
||||
}
|
||||
</style>
|
2
easytier-web/frontend-lib/src/components/index.ts
Normal file
2
easytier-web/frontend-lib/src/components/index.ts
Normal file
@@ -0,0 +1,2 @@
|
||||
export { default as Config } from './Config.vue';
|
||||
export { default as Status } from './Status.vue';
|
50
easytier-web/frontend-lib/src/easytier-frontend-lib.ts
Normal file
50
easytier-web/frontend-lib/src/easytier-frontend-lib.ts
Normal file
@@ -0,0 +1,50 @@
|
||||
import './style.css'
|
||||
|
||||
import type { App } from 'vue';
|
||||
import { Config, Status } from "./components";
|
||||
import Aura from '@primevue/themes/aura'
|
||||
import PrimeVue from 'primevue/config'
|
||||
|
||||
import I18nUtils from './modules/i18n'
|
||||
import * as NetworkTypes from './types/network'
|
||||
import HumanEvent from './components/HumanEvent.vue';
|
||||
|
||||
// do not use primevue tooltip, it has serious memory leak issue
|
||||
// https://github.com/primefaces/primevue/issues/5856
|
||||
// import Tooltip from 'primevue/tooltip';
|
||||
import { vTooltip } from 'floating-vue';
|
||||
|
||||
import * as Api from './modules/api';
|
||||
import * as Utils from './modules/utils';
|
||||
|
||||
export default {
|
||||
install: (app: App): void => {
|
||||
app.use(I18nUtils.i18n, { useScope: 'global' })
|
||||
app.use(PrimeVue, {
|
||||
theme: {
|
||||
preset: Aura,
|
||||
options: {
|
||||
prefix: 'p',
|
||||
darkModeSelector: 'system',
|
||||
cssLayer: {
|
||||
name: 'primevue',
|
||||
order: 'tailwind-base, primevue, tailwind-utilities'
|
||||
}
|
||||
},
|
||||
},
|
||||
zIndex: {
|
||||
modal: 1100, //dialog, drawer
|
||||
overlay: 1200, //select, popover
|
||||
menu: 1300, //overlay menus
|
||||
tooltip: 1400 //tooltip
|
||||
}
|
||||
});
|
||||
|
||||
app.component('Config', Config);
|
||||
app.component('Status', Status);
|
||||
app.component('HumanEvent', HumanEvent);
|
||||
app.directive('tooltip', vTooltip as any);
|
||||
}
|
||||
};
|
||||
|
||||
export { Config, Status, I18nUtils, NetworkTypes, Api, Utils };
|
115
easytier-web/frontend-lib/src/locales/cn.yaml
Normal file
115
easytier-web/frontend-lib/src/locales/cn.yaml
Normal file
@@ -0,0 +1,115 @@
|
||||
network: 网络
|
||||
networking_method: 网络方式
|
||||
public_server: 公共服务器
|
||||
manual: 手动
|
||||
standalone: 独立
|
||||
virtual_ipv4: 虚拟IPv4地址
|
||||
virtual_ipv4_dhcp: DHCP
|
||||
network_name: 网络名称
|
||||
network_secret: 网络密码
|
||||
public_server_url: 公共服务器地址
|
||||
peer_urls: 对等节点地址
|
||||
proxy_cidrs: 子网代理CIDR
|
||||
enable_vpn_portal: 启用VPN门户
|
||||
vpn_portal_listen_port: 监听端口
|
||||
vpn_portal_client_network: 客户端子网
|
||||
dev_name: TUN接口名称
|
||||
advanced_settings: 高级设置
|
||||
basic_settings: 基础设置
|
||||
listener_urls: 监听地址
|
||||
rpc_port: RPC端口
|
||||
config_network: 配置网络
|
||||
running: 运行中
|
||||
error_msg: 错误信息
|
||||
detail: 详情
|
||||
add_new_network: 添加新网络
|
||||
del_cur_network: 删除当前网络
|
||||
select_network: 选择网络
|
||||
network_instances: 网络实例
|
||||
instance_id: 实例ID
|
||||
network_infos: 网络信息
|
||||
parse_network_config: 解析网络配置
|
||||
retain_network_instance: 保留网络实例
|
||||
collect_network_infos: 收集网络信息
|
||||
settings: 设置
|
||||
exchange_language: Switch to English
|
||||
logging: 日志
|
||||
logging_level_info: 信息
|
||||
logging_level_debug: 调试
|
||||
logging_level_warn: 警告
|
||||
logging_level_trace: 跟踪
|
||||
logging_level_off: 关闭
|
||||
logging_open_dir: 打开日志目录
|
||||
logging_copy_dir: 复制日志路径
|
||||
disable_auto_launch: 关闭开机自启
|
||||
enable_auto_launch: 开启开机自启
|
||||
exit: 退出
|
||||
chips_placeholder: 例如: {0}, 按回车添加
|
||||
hostname_placeholder: '留空默认为主机名: {0}'
|
||||
dev_name_placeholder: 注意:当多个网络同时使用相同的TUN接口名称时,将会在设置TUN的IP时产生冲突,留空以自动生成随机名称
|
||||
off_text: 点击关闭
|
||||
on_text: 点击开启
|
||||
show_config: 显示配置
|
||||
close: 关闭
|
||||
|
||||
use_latency_first: 延迟优先模式
|
||||
my_node_info: 当前节点信息
|
||||
peer_count: 已连接
|
||||
upload: 上传
|
||||
download: 下载
|
||||
show_vpn_portal_config: 显示VPN门户配置
|
||||
vpn_portal_config: VPN门户配置
|
||||
show_event_log: 显示事件日志
|
||||
event_log: 事件日志
|
||||
peer_info: 节点信息
|
||||
hostname: 主机名
|
||||
route_cost: 路由
|
||||
latency: 延迟
|
||||
upload_bytes: 上传
|
||||
download_bytes: 下载
|
||||
loss_rate: 丢包率
|
||||
|
||||
status:
|
||||
version: 内核版本
|
||||
local: 本机
|
||||
server: 服务器
|
||||
relay: 中继
|
||||
|
||||
run_network: 运行网络
|
||||
stop_network: 停止网络
|
||||
network_running: 运行中
|
||||
network_stopped: 已停止
|
||||
dhcp_experimental_warning: 实验性警告!使用DHCP时如果组网环境中发生IP冲突,将自动更改IP。
|
||||
|
||||
tray:
|
||||
show: 显示 / 隐藏
|
||||
exit: 退出
|
||||
|
||||
about:
|
||||
title: 关于
|
||||
version: 版本
|
||||
author: 作者
|
||||
homepage: 主页
|
||||
license: 许可证
|
||||
description: 一个简单、安全、去中心化的内网穿透 VPN 组网方案,使用 Rust 语言和 Tokio 框架实现。
|
||||
check_update: 检查更新
|
||||
|
||||
event:
|
||||
Unknown: 未知
|
||||
TunDeviceReady: Tun设备就绪
|
||||
TunDeviceError: Tun设备错误
|
||||
PeerAdded: 对端添加
|
||||
PeerRemoved: 对端移除
|
||||
PeerConnAdded: 对端连接添加
|
||||
PeerConnRemoved: 对端连接移除
|
||||
ListenerAdded: 监听器添加
|
||||
ListenerAddFailed: 监听器添加失败
|
||||
ListenerAcceptFailed: 监听器接受连接失败
|
||||
ConnectionAccepted: 连接已接受
|
||||
ConnectionError: 连接错误
|
||||
Connecting: 正在连接
|
||||
ConnectError: 连接错误
|
||||
VpnPortalClientConnected: VPN门户客户端已连接
|
||||
VpnPortalClientDisconnected: VPN门户客户端已断开连接
|
||||
DhcpIpv4Changed: DHCP IPv4地址更改
|
||||
DhcpIpv4Conflicted: DHCP IPv4地址冲突
|
114
easytier-web/frontend-lib/src/locales/en.yaml
Normal file
114
easytier-web/frontend-lib/src/locales/en.yaml
Normal file
@@ -0,0 +1,114 @@
|
||||
network: Network
|
||||
networking_method: Networking Method
|
||||
public_server: Public Server
|
||||
manual: Manual
|
||||
standalone: Standalone
|
||||
virtual_ipv4: Virtual IPv4
|
||||
virtual_ipv4_dhcp: DHCP
|
||||
network_name: Network Name
|
||||
network_secret: Network Secret
|
||||
public_server_url: Public Server URL
|
||||
peer_urls: Peer URLs
|
||||
proxy_cidrs: Subnet Proxy CIDRs
|
||||
enable_vpn_portal: Enable VPN Portal
|
||||
vpn_portal_listen_port: VPN Portal Listen Port
|
||||
vpn_portal_client_network: Client Sub Network
|
||||
dev_name: TUN interface name
|
||||
advanced_settings: Advanced Settings
|
||||
basic_settings: Basic Settings
|
||||
listener_urls: Listener URLs
|
||||
rpc_port: RPC Port
|
||||
config_network: Config Network
|
||||
running: Running
|
||||
error_msg: Error Message
|
||||
detail: Detail
|
||||
add_new_network: New Network
|
||||
del_cur_network: Delete Current Network
|
||||
select_network: Select Network
|
||||
network_instances: Network Instances
|
||||
instance_id: Instance ID
|
||||
network_infos: Network Infos
|
||||
parse_network_config: Parse Network Config
|
||||
retain_network_instance: Retain Network Instance
|
||||
collect_network_infos: Collect Network Infos
|
||||
settings: Settings
|
||||
exchange_language: 切换中文
|
||||
logging: Logging
|
||||
logging_level_info: Info
|
||||
logging_level_debug: Debug
|
||||
logging_level_warn: Warn
|
||||
logging_level_trace: Trace
|
||||
logging_level_off: Off
|
||||
logging_open_dir: Open Log Directory
|
||||
logging_copy_dir: Copy Log Path
|
||||
disable_auto_launch: Disable Launch on Reboot
|
||||
enable_auto_launch: Enable Launch on Reboot
|
||||
exit: Exit
|
||||
use_latency_first: Latency First Mode
|
||||
chips_placeholder: 'e.g: {0}, press Enter to add'
|
||||
hostname_placeholder: 'Leave blank and default to host name: {0}'
|
||||
dev_name_placeholder: 'Note: When multiple networks use the same TUN interface name at the same time, there will be a conflict when setting the TUN''s IP. Leave blank to automatically generate a random name.'
|
||||
off_text: Press to disable
|
||||
on_text: Press to enable
|
||||
show_config: Show Config
|
||||
close: Close
|
||||
my_node_info: My Node Info
|
||||
peer_count: Connected
|
||||
upload: Upload
|
||||
download: Download
|
||||
show_vpn_portal_config: Show VPN Portal Config
|
||||
vpn_portal_config: VPN Portal Config
|
||||
show_event_log: Show Event Log
|
||||
event_log: Event Log
|
||||
peer_info: Peer Info
|
||||
route_cost: Route Cost
|
||||
hostname: Hostname
|
||||
latency: Latency
|
||||
upload_bytes: Upload
|
||||
download_bytes: Download
|
||||
loss_rate: Loss Rate
|
||||
|
||||
status:
|
||||
version: Version
|
||||
local: Local
|
||||
server: Server
|
||||
relay: Relay
|
||||
|
||||
run_network: Run Network
|
||||
stop_network: Stop Network
|
||||
network_running: running
|
||||
network_stopped: stopped
|
||||
dhcp_experimental_warning: Experimental warning! if there is an IP conflict in the network when using DHCP, the IP will be automatically changed.
|
||||
|
||||
tray:
|
||||
show: Show / Hide
|
||||
exit: Exit
|
||||
|
||||
about:
|
||||
title: About
|
||||
version: Version
|
||||
author: Author
|
||||
homepage: Homepage
|
||||
license: License
|
||||
description: 'EasyTier is a simple, safe and decentralized VPN networking solution implemented with the Rust language and Tokio framework.'
|
||||
check_update: Check Update
|
||||
|
||||
event:
|
||||
Unknown: Unknown
|
||||
TunDeviceReady: TunDeviceReady
|
||||
TunDeviceError: TunDeviceError
|
||||
PeerAdded: PeerAdded
|
||||
PeerRemoved: PeerRemoved
|
||||
PeerConnAdded: PeerConnAdded
|
||||
PeerConnRemoved: PeerConnRemoved
|
||||
ListenerAdded: ListenerAdded
|
||||
ListenerAddFailed: ListenerAddFailed
|
||||
ListenerAcceptFailed: ListenerAcceptFailed
|
||||
ConnectionAccepted: ConnectionAccepted
|
||||
ConnectionError: ConnectionError
|
||||
Connecting: Connecting
|
||||
ConnectError: ConnectError
|
||||
VpnPortalClientConnected: VpnPortalClientConnected
|
||||
VpnPortalClientDisconnected: VpnPortalClientDisconnected
|
||||
DhcpIpv4Changed: DhcpIpv4Changed
|
||||
DhcpIpv4Conflicted: DhcpIpv4Conflicted
|
181
easytier-web/frontend-lib/src/modules/api.ts
Normal file
181
easytier-web/frontend-lib/src/modules/api.ts
Normal file
@@ -0,0 +1,181 @@
|
||||
import axios, { AxiosError, AxiosInstance, AxiosResponse, InternalAxiosRequestConfig } from 'axios';
|
||||
import { Md5 } from 'ts-md5'
|
||||
|
||||
export interface ValidateConfigResponse {
|
||||
toml_config: string;
|
||||
}
|
||||
|
||||
// 定义接口返回的数据结构
|
||||
export interface LoginResponse {
|
||||
success: boolean;
|
||||
message: string;
|
||||
}
|
||||
|
||||
export interface RegisterResponse {
|
||||
success: boolean;
|
||||
message: string;
|
||||
}
|
||||
|
||||
// 定义请求体数据结构
|
||||
export interface Credential {
|
||||
username: string;
|
||||
password: string;
|
||||
}
|
||||
|
||||
export interface RegisterData {
|
||||
credentials: Credential;
|
||||
captcha: string;
|
||||
}
|
||||
|
||||
export interface Summary {
|
||||
device_count: number;
|
||||
}
|
||||
|
||||
export class ApiClient {
|
||||
private client: AxiosInstance;
|
||||
private authFailedCb: Function | undefined;
|
||||
|
||||
constructor(baseUrl: string, authFailedCb: Function | undefined = undefined) {
|
||||
this.client = axios.create({
|
||||
baseURL: baseUrl + '/api/v1',
|
||||
withCredentials: true, // 如果需要支持跨域携带cookie
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
},
|
||||
});
|
||||
this.authFailedCb = authFailedCb;
|
||||
|
||||
// 添加请求拦截器
|
||||
this.client.interceptors.request.use((config: InternalAxiosRequestConfig) => {
|
||||
return config;
|
||||
}, (error: any) => {
|
||||
return Promise.reject(error);
|
||||
});
|
||||
|
||||
// 添加响应拦截器
|
||||
this.client.interceptors.response.use((response: AxiosResponse) => {
|
||||
console.debug('Axios Response:', response);
|
||||
return response.data; // 假设服务器返回的数据都在data属性中
|
||||
}, (error: any) => {
|
||||
if (error.response) {
|
||||
let response: AxiosResponse = error.response;
|
||||
if (response.status == 401 && this.authFailedCb) {
|
||||
console.error('Unauthorized:', response.data);
|
||||
this.authFailedCb();
|
||||
} else {
|
||||
// 请求已发出,但是服务器响应的状态码不在2xx范围
|
||||
console.error('Response Error:', error.response.data);
|
||||
}
|
||||
} else if (error.request) {
|
||||
// 请求已发出,但是没有收到响应
|
||||
console.error('Request Error:', error.request);
|
||||
} else {
|
||||
// 发生了一些问题导致请求未发出
|
||||
console.error('Error:', error.message);
|
||||
}
|
||||
return Promise.reject(error);
|
||||
});
|
||||
}
|
||||
|
||||
// 注册
|
||||
public async register(data: RegisterData): Promise<RegisterResponse> {
|
||||
try {
|
||||
data.credentials.password = Md5.hashStr(data.credentials.password);
|
||||
const response = await this.client.post<RegisterResponse>('/auth/register', data);
|
||||
console.log("register response:", response);
|
||||
return { success: true, message: 'Register success', };
|
||||
} catch (error) {
|
||||
if (error instanceof AxiosError) {
|
||||
return { success: false, message: 'Failed to register, error: ' + JSON.stringify(error.response?.data), };
|
||||
}
|
||||
return { success: false, message: 'Unknown error, error: ' + error, };
|
||||
}
|
||||
}
|
||||
|
||||
// 登录
|
||||
public async login(data: Credential): Promise<LoginResponse> {
|
||||
try {
|
||||
data.password = Md5.hashStr(data.password);
|
||||
const response = await this.client.post<any>('/auth/login', data);
|
||||
console.log("login response:", response);
|
||||
return { success: true, message: 'Login success', };
|
||||
} catch (error) {
|
||||
if (error instanceof AxiosError) {
|
||||
if (error.response?.status === 401) {
|
||||
return { success: false, message: 'Invalid username or password', };
|
||||
} else {
|
||||
return { success: false, message: 'Unknown error, status code: ' + error.response?.status, };
|
||||
}
|
||||
}
|
||||
return { success: false, message: 'Unknown error, error: ' + error, };
|
||||
}
|
||||
}
|
||||
|
||||
public async logout() {
|
||||
await this.client.get('/auth/logout');
|
||||
if (this.authFailedCb) {
|
||||
this.authFailedCb();
|
||||
}
|
||||
}
|
||||
|
||||
public async change_password(new_password: string) {
|
||||
await this.client.put('/auth/password', { new_password: Md5.hashStr(new_password) });
|
||||
}
|
||||
|
||||
public async check_login_status() {
|
||||
try {
|
||||
await this.client.get('/auth/check_login_status');
|
||||
return true;
|
||||
} catch (error) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
public async list_session() {
|
||||
const response = await this.client.get('/sessions');
|
||||
return response;
|
||||
}
|
||||
|
||||
public async list_machines(): Promise<Array<any>> {
|
||||
const response = await this.client.get<any, Record<string, Array<any>>>('/machines');
|
||||
return response.machines;
|
||||
}
|
||||
|
||||
public async get_network_info(machine_id: string, inst_id: string): Promise<any> {
|
||||
const response = await this.client.get<any, Record<string, any>>('/machines/' + machine_id + '/networks/info/' + inst_id);
|
||||
return response.info.map;
|
||||
}
|
||||
|
||||
public async get_network_config(machine_id: string, inst_id: string): Promise<any> {
|
||||
const response = await this.client.get<any, Record<string, any>>('/machines/' + machine_id + '/networks/config/' + inst_id);
|
||||
return response;
|
||||
}
|
||||
|
||||
public async validate_config(machine_id: string, config: any): Promise<ValidateConfigResponse> {
|
||||
const response = await this.client.post<any, ValidateConfigResponse>(`/machines/${machine_id}/validate-config`, {
|
||||
config: config,
|
||||
});
|
||||
return response;
|
||||
}
|
||||
|
||||
public async run_network(machine_id: string, config: any): Promise<undefined> {
|
||||
await this.client.post<string>(`/machines/${machine_id}/networks`, {
|
||||
config: config,
|
||||
});
|
||||
}
|
||||
|
||||
public async delete_network(machine_id: string, inst_id: string): Promise<undefined> {
|
||||
await this.client.delete<string>(`/machines/${machine_id}/networks/${inst_id}`);
|
||||
}
|
||||
|
||||
public async get_summary(): Promise<Summary> {
|
||||
const response = await this.client.get<any, Summary>('/summary');
|
||||
return response;
|
||||
}
|
||||
|
||||
public captcha_url() {
|
||||
return this.client.defaults.baseURL + '/auth/captcha';
|
||||
}
|
||||
}
|
||||
|
||||
export default ApiClient;
|
@@ -1,5 +1,8 @@
|
||||
import type { Locale } from 'vue-i18n'
|
||||
import { createI18n } from 'vue-i18n'
|
||||
import type { Locale } from 'vue-i18n'
|
||||
|
||||
import EnLocale from '../locales/en.yaml'
|
||||
import CnLocale from '../locales/cn.yaml'
|
||||
|
||||
// Import i18n resources
|
||||
// https://vitejs.dev/guide/features.html#glob-import
|
||||
@@ -10,10 +13,10 @@ export const i18n = createI18n({
|
||||
messages: {},
|
||||
})
|
||||
|
||||
const localesMap = Object.fromEntries(
|
||||
Object.entries(import.meta.glob('../../locales/*.yml'))
|
||||
.map(([path, loadLocale]) => [path.match(/([\w-]*)\.yml$/)?.[1], loadLocale]),
|
||||
) as Record<Locale, () => Promise<{ default: Record<string, string> }>>
|
||||
const localesMap = {
|
||||
"en": EnLocale,
|
||||
"cn": CnLocale,
|
||||
} as Record<string, any>
|
||||
|
||||
export const availableLocales = Object.keys(localesMap)
|
||||
|
||||
@@ -38,13 +41,19 @@ export async function loadLanguageAsync(lang: string): Promise<Locale> {
|
||||
let messages
|
||||
|
||||
try {
|
||||
messages = await localesMap[lang]()
|
||||
messages = localesMap[lang]
|
||||
}
|
||||
catch {
|
||||
messages = await localesMap.en()
|
||||
messages = localesMap.en
|
||||
}
|
||||
|
||||
i18n.global.setLocaleMessage(lang, messages.default)
|
||||
i18n.global.setLocaleMessage(lang, messages)
|
||||
loadedLanguages.push(lang)
|
||||
return setI18nLanguage(lang)
|
||||
}
|
||||
|
||||
export default {
|
||||
i18n,
|
||||
localesMap,
|
||||
loadLanguageAsync,
|
||||
}
|
108
easytier-web/frontend-lib/src/modules/utils.ts
Normal file
108
easytier-web/frontend-lib/src/modules/utils.ts
Normal file
@@ -0,0 +1,108 @@
|
||||
import { IPv4, IPv6 } from 'ip-num/IPNumber'
|
||||
import { Ipv4Addr, Ipv4Inet, Ipv6Addr } from '../types/network'
|
||||
|
||||
export function ipv4ToString(ip: Ipv4Addr) {
|
||||
return IPv4.fromNumber(ip.addr).toString()
|
||||
}
|
||||
|
||||
export function ipv4InetToString(ip: Ipv4Inet | undefined) {
|
||||
if (ip?.address === undefined) {
|
||||
return 'undefined'
|
||||
}
|
||||
return `${ipv4ToString(ip.address)}/${ip.network_length}`
|
||||
}
|
||||
|
||||
export function ipv6ToString(ip: Ipv6Addr) {
|
||||
return IPv6.fromBigInt(
|
||||
(BigInt(ip.part1) << BigInt(96))
|
||||
+ (BigInt(ip.part2) << BigInt(64))
|
||||
+ (BigInt(ip.part3) << BigInt(32))
|
||||
+ BigInt(ip.part4),
|
||||
)
|
||||
}
|
||||
|
||||
function toHexString(uint64: bigint, padding = 9): string {
|
||||
let hexString = uint64.toString(16);
|
||||
while (hexString.length < padding) {
|
||||
hexString = '0' + hexString;
|
||||
}
|
||||
return hexString;
|
||||
}
|
||||
|
||||
function uint32ToUuid(part1: number, part2: number, part3: number, part4: number): string {
|
||||
// 将两个 uint64 转换为 16 进制字符串
|
||||
const part1Hex = toHexString(BigInt(part1), 8);
|
||||
const part2Hex = toHexString(BigInt(part2), 8);
|
||||
const part3Hex = toHexString(BigInt(part3), 8);
|
||||
const part4Hex = toHexString(BigInt(part4), 8);
|
||||
|
||||
// 构造 UUID 格式字符串
|
||||
const uuid = `${part1Hex.substring(0, 8)}-${part2Hex.substring(0, 4)}-${part2Hex.substring(4, 8)}-${part3Hex.substring(0, 4)}-${part3Hex.substring(4, 8)}${part4Hex.substring(0, 12)}`;
|
||||
|
||||
return uuid;
|
||||
}
|
||||
|
||||
export interface UUID {
|
||||
part1: number;
|
||||
part2: number;
|
||||
part3: number;
|
||||
part4: number;
|
||||
}
|
||||
|
||||
export function UuidToStr(uuid: UUID): string {
|
||||
return uint32ToUuid(uuid.part1, uuid.part2, uuid.part3, uuid.part4);
|
||||
}
|
||||
|
||||
export interface DeviceInfo {
|
||||
hostname: string;
|
||||
public_ip: string;
|
||||
running_network_count: number;
|
||||
report_time: string;
|
||||
easytier_version: string;
|
||||
running_network_instances?: Array<string>;
|
||||
machine_id: string;
|
||||
}
|
||||
|
||||
export function buildDeviceInfo(device: any): DeviceInfo {
|
||||
let dev_info: DeviceInfo = {
|
||||
hostname: device.info?.hostname,
|
||||
public_ip: device.client_url,
|
||||
running_network_instances: device.info?.running_network_instances.map((instance: any) => UuidToStr(instance)),
|
||||
running_network_count: device.info?.running_network_instances.length,
|
||||
report_time: device.info?.report_time,
|
||||
easytier_version: device.info?.easytier_version,
|
||||
machine_id: UuidToStr(device.info?.machine_id),
|
||||
};
|
||||
|
||||
return dev_info;
|
||||
}
|
||||
|
||||
// write a class to run a function periodically and can be stopped by calling stop(), use setTimeout to trigger the function
|
||||
export class PeriodicTask {
|
||||
private interval: number;
|
||||
private task: (() => Promise<void>) | undefined;
|
||||
private timer: any;
|
||||
|
||||
constructor(task: () => Promise<void>, interval: number) {
|
||||
this.interval = interval;
|
||||
this.task = task;
|
||||
}
|
||||
|
||||
_runTaskHelper(nextInterval: number) {
|
||||
this.timer = setTimeout(async () => {
|
||||
if (this.task) {
|
||||
await this.task();
|
||||
this._runTaskHelper(this.interval);
|
||||
}
|
||||
}, nextInterval);
|
||||
}
|
||||
|
||||
start() {
|
||||
this._runTaskHelper(0);
|
||||
}
|
||||
|
||||
stop() {
|
||||
this.task = undefined;
|
||||
clearTimeout(this.timer);
|
||||
}
|
||||
}
|
54
easytier-web/frontend-lib/src/style.css
Normal file
54
easytier-web/frontend-lib/src/style.css
Normal file
@@ -0,0 +1,54 @@
|
||||
@import 'primeicons/primeicons.css';
|
||||
@import 'floating-vue/dist/style.css';
|
||||
|
||||
.frontend-lib {
|
||||
|
||||
@layer tailwind-base, primevue, tailwind-utilities;
|
||||
|
||||
@layer tailwind-base {
|
||||
@tailwind base;
|
||||
}
|
||||
|
||||
@layer tailwind-utilities {
|
||||
@tailwind components;
|
||||
@tailwind utilities;
|
||||
}
|
||||
|
||||
:root {
|
||||
font-family: Inter, Avenir, Helvetica, Arial, sans-serif;
|
||||
font-size: 12px;
|
||||
line-height: 24px;
|
||||
font-weight: 400;
|
||||
|
||||
color: #0f0f0f;
|
||||
|
||||
font-synthesis: none;
|
||||
text-rendering: optimizeLegibility;
|
||||
-webkit-font-smoothing: antialiased;
|
||||
-moz-osx-font-smoothing: grayscale;
|
||||
-webkit-text-size-adjust: 100%;
|
||||
}
|
||||
|
||||
.card {
|
||||
background: var(--surface-card);
|
||||
padding: 2rem;
|
||||
border-radius: 10px;
|
||||
margin-bottom: 1rem;
|
||||
}
|
||||
|
||||
::-webkit-scrollbar {
|
||||
width: 4px;
|
||||
height: 4px;
|
||||
border-radius: 4px;
|
||||
}
|
||||
|
||||
::-webkit-scrollbar-track {
|
||||
border-radius: 4px;
|
||||
}
|
||||
|
||||
::-webkit-scrollbar-thumb {
|
||||
border-radius: 4px;
|
||||
background-color: #0000005d;
|
||||
}
|
||||
|
||||
}
|
@@ -1,9 +1,9 @@
|
||||
import { v4 as uuidv4 } from 'uuid'
|
||||
|
||||
export enum NetworkingMethod {
|
||||
PublicServer = 'PublicServer',
|
||||
Manual = 'Manual',
|
||||
Standalone = 'Standalone',
|
||||
PublicServer = 0,
|
||||
Manual = 1,
|
||||
Standalone = 2,
|
||||
}
|
||||
|
||||
export interface NetworkConfig {
|
||||
@@ -11,6 +11,7 @@ export interface NetworkConfig {
|
||||
|
||||
dhcp: boolean
|
||||
virtual_ipv4: string
|
||||
network_length: number
|
||||
hostname?: string
|
||||
network_name: string
|
||||
network_secret: string
|
||||
@@ -31,6 +32,9 @@ export interface NetworkConfig {
|
||||
|
||||
listener_urls: string[]
|
||||
rpc_port: number
|
||||
latency_first: boolean
|
||||
|
||||
dev_name: string
|
||||
}
|
||||
|
||||
export function DEFAULT_NETWORK_CONFIG(): NetworkConfig {
|
||||
@@ -39,12 +43,13 @@ export function DEFAULT_NETWORK_CONFIG(): NetworkConfig {
|
||||
|
||||
dhcp: true,
|
||||
virtual_ipv4: '',
|
||||
network_length: 24,
|
||||
network_name: 'easytier',
|
||||
network_secret: '',
|
||||
|
||||
networking_method: NetworkingMethod.PublicServer,
|
||||
|
||||
public_server_url: 'tcp://easytier.public.kkrainbow.top:11010',
|
||||
public_server_url: 'tcp://public.easytier.top:11010',
|
||||
peer_urls: [],
|
||||
|
||||
proxy_cidrs: [],
|
||||
@@ -62,6 +67,8 @@ export function DEFAULT_NETWORK_CONFIG(): NetworkConfig {
|
||||
'wg://0.0.0.0:11011',
|
||||
],
|
||||
rpc_port: 0,
|
||||
latency_first: true,
|
||||
dev_name: '',
|
||||
}
|
||||
}
|
||||
|
||||
@@ -75,9 +82,9 @@ export interface NetworkInstance {
|
||||
}
|
||||
|
||||
export interface NetworkInstanceRunningInfo {
|
||||
dev_name: string
|
||||
my_node_info: NodeInfo
|
||||
events: Record<string, any>
|
||||
node_info: NodeInfo
|
||||
events: Array<string>,
|
||||
routes: Route[]
|
||||
peers: PeerInfo[]
|
||||
peer_route_pairs: PeerRoutePair[]
|
||||
@@ -85,13 +92,35 @@ export interface NetworkInstanceRunningInfo {
|
||||
error_msg?: string
|
||||
}
|
||||
|
||||
export interface Ipv4Addr {
|
||||
addr: number
|
||||
}
|
||||
|
||||
export interface Ipv4Inet {
|
||||
address: Ipv4Addr
|
||||
network_length: number
|
||||
}
|
||||
|
||||
export interface Ipv6Addr {
|
||||
part1: number
|
||||
part2: number
|
||||
part3: number
|
||||
part4: number
|
||||
}
|
||||
|
||||
export interface Url {
|
||||
url: string
|
||||
}
|
||||
|
||||
export interface NodeInfo {
|
||||
virtual_ipv4: string
|
||||
virtual_ipv4: Ipv4Inet,
|
||||
hostname: string
|
||||
version: string
|
||||
ips: {
|
||||
public_ipv4: string
|
||||
interface_ipv4s: string[]
|
||||
public_ipv6: string
|
||||
interface_ipv6s: string[]
|
||||
public_ipv4: Ipv4Addr
|
||||
interface_ipv4s: Ipv4Addr[]
|
||||
public_ipv6: Ipv6Addr
|
||||
interface_ipv6s: Ipv6Addr[]
|
||||
listeners: {
|
||||
serialization: string
|
||||
scheme_end: number
|
||||
@@ -106,7 +135,7 @@ export interface NodeInfo {
|
||||
}[]
|
||||
}
|
||||
stun_info: StunInfo
|
||||
listeners: string[]
|
||||
listeners: Url[]
|
||||
vpn_portal_cfg?: string
|
||||
}
|
||||
|
||||
@@ -118,13 +147,14 @@ export interface StunInfo {
|
||||
|
||||
export interface Route {
|
||||
peer_id: number
|
||||
ipv4_addr: string
|
||||
ipv4_addr: Ipv4Inet | string | null
|
||||
next_hop_peer_id: number
|
||||
cost: number
|
||||
proxy_cidrs: string[]
|
||||
hostname: string
|
||||
stun_info?: StunInfo
|
||||
inst_id: string
|
||||
version: string
|
||||
}
|
||||
|
||||
export interface PeerInfo {
|
||||
@@ -135,6 +165,7 @@ export interface PeerInfo {
|
||||
export interface PeerConnInfo {
|
||||
conn_id: string
|
||||
my_peer_id: number
|
||||
is_client: boolean
|
||||
peer_id: number
|
||||
features: string[]
|
||||
tunnel?: TunnelInfo
|
||||
@@ -160,3 +191,28 @@ export interface PeerConnStats {
|
||||
tx_packets: number
|
||||
latency_us: number
|
||||
}
|
||||
|
||||
export enum EventType {
|
||||
TunDeviceReady = 'TunDeviceReady', // string
|
||||
TunDeviceError = 'TunDeviceError', // string
|
||||
|
||||
PeerAdded = 'PeerAdded', // number
|
||||
PeerRemoved = 'PeerRemoved', // number
|
||||
PeerConnAdded = 'PeerConnAdded', // PeerConnInfo
|
||||
PeerConnRemoved = 'PeerConnRemoved', // PeerConnInfo
|
||||
|
||||
ListenerAdded = 'ListenerAdded', // any
|
||||
ListenerAddFailed = 'ListenerAddFailed', // any, string
|
||||
ListenerAcceptFailed = 'ListenerAcceptFailed', // any, string
|
||||
ConnectionAccepted = 'ConnectionAccepted', // string, string
|
||||
ConnectionError = 'ConnectionError', // string, string, string
|
||||
|
||||
Connecting = 'Connecting', // any
|
||||
ConnectError = 'ConnectError', // string, string, string
|
||||
|
||||
VpnPortalClientConnected = 'VpnPortalClientConnected', // string, string
|
||||
VpnPortalClientDisconnected = 'VpnPortalClientDisconnected', // string, string, string
|
||||
|
||||
DhcpIpv4Changed = 'DhcpIpv4Changed', // ipv4 | null, ipv4 | null
|
||||
DhcpIpv4Conflicted = 'DhcpIpv4Conflicted', // ipv4 | null
|
||||
}
|
1
easytier-web/frontend-lib/src/vite-env.d.ts
vendored
Normal file
1
easytier-web/frontend-lib/src/vite-env.d.ts
vendored
Normal file
@@ -0,0 +1 @@
|
||||
/// <reference types="vite/client" />
|
11
easytier-web/frontend-lib/tailwind.config.js
Normal file
11
easytier-web/frontend-lib/tailwind.config.js
Normal file
@@ -0,0 +1,11 @@
|
||||
/** @type {import('tailwindcss').Config} */
|
||||
export default {
|
||||
content: [
|
||||
'./index.html',
|
||||
'./src/**/*.{vue,js,ts,jsx,tsx}',
|
||||
],
|
||||
theme: {
|
||||
extend: {},
|
||||
},
|
||||
plugins: [require('tailwindcss-primeui')],
|
||||
}
|
31
easytier-web/frontend-lib/tsconfig.app.json
Normal file
31
easytier-web/frontend-lib/tsconfig.app.json
Normal file
@@ -0,0 +1,31 @@
|
||||
{
|
||||
"compilerOptions": {
|
||||
"tsBuildInfoFile": "./node_modules/.tmp/tsconfig.app.tsbuildinfo",
|
||||
"target": "ES2020",
|
||||
"useDefineForClassFields": true,
|
||||
"module": "ESNext",
|
||||
"lib": ["ES2020", "DOM", "DOM.Iterable"],
|
||||
"skipLibCheck": true,
|
||||
|
||||
"allowSyntheticDefaultImports": true,
|
||||
|
||||
/* Bundler mode */
|
||||
"moduleResolution": "Bundler",
|
||||
"allowImportingTsExtensions": true,
|
||||
"isolatedModules": true,
|
||||
"moduleDetection": "force",
|
||||
"noEmit": true,
|
||||
"jsx": "preserve",
|
||||
|
||||
/* Linting */
|
||||
"strict": true,
|
||||
"noUnusedLocals": true,
|
||||
"noUnusedParameters": true,
|
||||
"noFallthroughCasesInSwitch": true,
|
||||
"noUncheckedSideEffectImports": true,
|
||||
"types": [
|
||||
"@modyfi/vite-plugin-yaml/modules"
|
||||
],
|
||||
},
|
||||
"include": ["src/**/*.ts", "src/**/*.tsx", "src/**/*.vue"]
|
||||
}
|
7
easytier-web/frontend-lib/tsconfig.json
Normal file
7
easytier-web/frontend-lib/tsconfig.json
Normal file
@@ -0,0 +1,7 @@
|
||||
{
|
||||
"files": [],
|
||||
"references": [
|
||||
{ "path": "./tsconfig.app.json" },
|
||||
{ "path": "./tsconfig.node.json" }
|
||||
]
|
||||
}
|
24
easytier-web/frontend-lib/tsconfig.node.json
Normal file
24
easytier-web/frontend-lib/tsconfig.node.json
Normal file
@@ -0,0 +1,24 @@
|
||||
{
|
||||
"compilerOptions": {
|
||||
"tsBuildInfoFile": "./node_modules/.tmp/tsconfig.node.tsbuildinfo",
|
||||
"target": "ES2022",
|
||||
"lib": ["ES2023"],
|
||||
"module": "ESNext",
|
||||
"skipLibCheck": true,
|
||||
|
||||
/* Bundler mode */
|
||||
"moduleResolution": "Bundler",
|
||||
"allowImportingTsExtensions": true,
|
||||
"isolatedModules": true,
|
||||
"moduleDetection": "force",
|
||||
"noEmit": true,
|
||||
|
||||
/* Linting */
|
||||
"strict": true,
|
||||
"noUnusedLocals": true,
|
||||
"noUnusedParameters": true,
|
||||
"noFallthroughCasesInSwitch": true,
|
||||
"noUncheckedSideEffectImports": true
|
||||
},
|
||||
"include": ["vite.config.ts"]
|
||||
}
|
38
easytier-web/frontend-lib/vite.config.ts
Normal file
38
easytier-web/frontend-lib/vite.config.ts
Normal file
@@ -0,0 +1,38 @@
|
||||
import { resolve } from 'path'
|
||||
import { defineConfig } from 'vite'
|
||||
import vue from '@vitejs/plugin-vue'
|
||||
import dts from "vite-plugin-dts"
|
||||
import ViteYaml from '@modyfi/vite-plugin-yaml';
|
||||
|
||||
// https://vite.dev/config/
|
||||
export default defineConfig({
|
||||
plugins: [vue(), dts({
|
||||
tsconfigPath: './tsconfig.app.json',
|
||||
}), ViteYaml()],
|
||||
build: {
|
||||
lib: {
|
||||
// Could also be a dictionary or array of multiple entry points
|
||||
entry: resolve(__dirname, 'src/index.ts'),
|
||||
name: 'easytier-frontend-lib',
|
||||
// the proper extensions will be added
|
||||
fileName: 'easytier-frontend-lib',
|
||||
formats: ["es", "umd", "cjs"],
|
||||
},
|
||||
rollupOptions: {
|
||||
input: {
|
||||
main: resolve(__dirname, "src/easytier-frontend-lib.ts")
|
||||
},
|
||||
// make sure to externalize deps that shouldn't be bundled
|
||||
// into your library
|
||||
external: ['vue'],
|
||||
output: {
|
||||
// Provide global variables to use in the UMD build
|
||||
// for externalized deps
|
||||
globals: {
|
||||
vue: 'Vue',
|
||||
},
|
||||
exports: "named"
|
||||
},
|
||||
},
|
||||
},
|
||||
})
|
24
easytier-web/frontend/.gitignore
vendored
Normal file
24
easytier-web/frontend/.gitignore
vendored
Normal file
@@ -0,0 +1,24 @@
|
||||
# Logs
|
||||
logs
|
||||
*.log
|
||||
npm-debug.log*
|
||||
yarn-debug.log*
|
||||
yarn-error.log*
|
||||
pnpm-debug.log*
|
||||
lerna-debug.log*
|
||||
|
||||
node_modules
|
||||
dist
|
||||
dist-ssr
|
||||
*.local
|
||||
|
||||
# Editor directories and files
|
||||
.vscode/*
|
||||
!.vscode/extensions.json
|
||||
.idea
|
||||
.DS_Store
|
||||
*.suo
|
||||
*.ntvs*
|
||||
*.njsproj
|
||||
*.sln
|
||||
*.sw?
|
5
easytier-web/frontend/README.md
Normal file
5
easytier-web/frontend/README.md
Normal file
@@ -0,0 +1,5 @@
|
||||
# Vue 3 + TypeScript + Vite
|
||||
|
||||
This template should help get you started developing with Vue 3 and TypeScript in Vite. The template uses Vue 3 `<script setup>` SFCs, check out the [script setup docs](https://v3.vuejs.org/api/sfc-script-setup.html#sfc-script-setup) to learn more.
|
||||
|
||||
Learn more about the recommended Project Setup and IDE Support in the [Vue Docs TypeScript Guide](https://vuejs.org/guide/typescript/overview.html#project-setup).
|
13
easytier-web/frontend/index.html
Normal file
13
easytier-web/frontend/index.html
Normal file
@@ -0,0 +1,13 @@
|
||||
<!doctype html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="UTF-8" />
|
||||
<link rel="icon" type="image/png" href="/easytier.png" />
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
||||
<title>EasyTier Dashboard</title>
|
||||
</head>
|
||||
<body>
|
||||
<div id="app"></div>
|
||||
<script type="module" src="/src/main.ts"></script>
|
||||
</body>
|
||||
</html>
|
32
easytier-web/frontend/package.json
Normal file
32
easytier-web/frontend/package.json
Normal file
@@ -0,0 +1,32 @@
|
||||
{
|
||||
"name": "easytier-frontend",
|
||||
"private": true,
|
||||
"version": "0.0.0",
|
||||
"type": "module",
|
||||
"scripts": {
|
||||
"dev": "vite",
|
||||
"build": "vue-tsc -b && vite build",
|
||||
"preview": "vite preview"
|
||||
},
|
||||
"dependencies": {
|
||||
"@primevue/themes": "^4.2.1",
|
||||
"aura": "link:@primevue/themes/aura",
|
||||
"axios": "^1.7.7",
|
||||
"easytier-frontend-lib": "workspace:*",
|
||||
"primevue": "^4.2.1",
|
||||
"tailwindcss-primeui": "^0.3.4",
|
||||
"vue": "^3.5.12",
|
||||
"vue-router": "4"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@types/node": "^22.8.6",
|
||||
"@vitejs/plugin-vue": "^5.1.4",
|
||||
"autoprefixer": "^10.4.20",
|
||||
"postcss": "^8.4.47",
|
||||
"tailwindcss": "^3.4.14",
|
||||
"typescript": "~5.6.2",
|
||||
"vite": "^5.4.10",
|
||||
"vite-plugin-singlefile": "^2.0.3",
|
||||
"vue-tsc": "^2.1.10"
|
||||
}
|
||||
}
|
6
easytier-web/frontend/postcss.config.js
Normal file
6
easytier-web/frontend/postcss.config.js
Normal file
@@ -0,0 +1,6 @@
|
||||
export default {
|
||||
plugins: {
|
||||
tailwindcss: {},
|
||||
autoprefixer: {},
|
||||
}
|
||||
}
|
BIN
easytier-web/frontend/public/easytier.png
Normal file
BIN
easytier-web/frontend/public/easytier.png
Normal file
Binary file not shown.
After Width: | Height: | Size: 21 KiB |
27
easytier-web/frontend/src/App.vue
Normal file
27
easytier-web/frontend/src/App.vue
Normal file
@@ -0,0 +1,27 @@
|
||||
<script setup lang="ts">
|
||||
|
||||
import { I18nUtils } from 'easytier-frontend-lib'
|
||||
import { onMounted } from 'vue';
|
||||
import { Toast, DynamicDialog } from 'primevue';
|
||||
|
||||
onMounted(async () => {
|
||||
await I18nUtils.loadLanguageAsync('cn')
|
||||
});
|
||||
|
||||
</script>
|
||||
|
||||
<!-- https://flowbite.com/docs/components/sidebar/#sidebar-with-navbar -->
|
||||
|
||||
<template>
|
||||
<Toast position="bottom-right" />
|
||||
<DynamicDialog />
|
||||
|
||||
<RouterView />
|
||||
</template>
|
||||
|
||||
<style scoped>
|
||||
button {
|
||||
text-align: left;
|
||||
justify-content: left;
|
||||
}
|
||||
</style>
|
BIN
easytier-web/frontend/src/assets/easytier.png
Normal file
BIN
easytier-web/frontend/src/assets/easytier.png
Normal file
Binary file not shown.
After Width: | Height: | Size: 21 KiB |
33
easytier-web/frontend/src/components/ChangePassword.vue
Normal file
33
easytier-web/frontend/src/components/ChangePassword.vue
Normal file
@@ -0,0 +1,33 @@
|
||||
<script lang="ts" setup>
|
||||
import { computed, inject, ref } from 'vue';
|
||||
import { Card, Password, Button } from 'primevue';
|
||||
import { Api } from 'easytier-frontend-lib';
|
||||
|
||||
const dialogRef = inject<any>('dialogRef');
|
||||
|
||||
const api = computed<Api.ApiClient>(() => dialogRef.value.data.api);
|
||||
|
||||
const password = ref('');
|
||||
|
||||
const changePassword = async () => {
|
||||
await api.value.change_password(password.value);
|
||||
dialogRef.value.close();
|
||||
}
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div class="flex items-center justify-center">
|
||||
<Card class="w-full max-w-md p-6">
|
||||
<template #header>
|
||||
<h2 class="text-2xl font-semibold text-center">Change Password
|
||||
</h2>
|
||||
</template>
|
||||
<template #content>
|
||||
<div class="flex flex-col space-y-4">
|
||||
<Password v-model="password" placeholder="New Password" :feedback="false" toggleMask />
|
||||
<Button @click="changePassword" label="Ok" />
|
||||
</div>
|
||||
</template>
|
||||
</Card>
|
||||
</div>
|
||||
</template>
|
65
easytier-web/frontend/src/components/Dashboard.vue
Normal file
65
easytier-web/frontend/src/components/Dashboard.vue
Normal file
@@ -0,0 +1,65 @@
|
||||
<script setup lang="ts">
|
||||
import { Card, useToast } from 'primevue';
|
||||
import { computed, onMounted, onUnmounted, ref } from 'vue';
|
||||
import { Api, Utils } from 'easytier-frontend-lib';
|
||||
|
||||
const props = defineProps({
|
||||
api: Api.ApiClient,
|
||||
});
|
||||
|
||||
const toast = useToast();
|
||||
|
||||
const summary = ref<Api.Summary | undefined>(undefined);
|
||||
|
||||
const loadSummary = async () => {
|
||||
const resp = await props.api?.get_summary();
|
||||
summary.value = resp;
|
||||
};
|
||||
|
||||
const periodFunc = new Utils.PeriodicTask(async () => {
|
||||
try {
|
||||
await loadSummary();
|
||||
} catch (e) {
|
||||
toast.add({ severity: 'error', summary: 'Load Summary Failed', detail: e, life: 2000 });
|
||||
console.error(e);
|
||||
}
|
||||
}, 1000);
|
||||
|
||||
onMounted(async () => {
|
||||
periodFunc.start();
|
||||
});
|
||||
|
||||
onUnmounted(() => {
|
||||
periodFunc.stop();
|
||||
});
|
||||
|
||||
const deviceCount = computed<number | undefined>(
|
||||
() => {
|
||||
return summary.value?.device_count;
|
||||
},
|
||||
);
|
||||
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div class="grid grid-cols-3 gap-4">
|
||||
<Card class="h-full">
|
||||
<template #title>Device Count</template>
|
||||
<template #content>
|
||||
<div class="w-full flex justify-center text-7xl font-bold text-green-800 mt-4">
|
||||
{{ deviceCount }}
|
||||
</div>
|
||||
</template>
|
||||
</Card>
|
||||
<div class="flex items-center justify-center rounded bg-gray-50 dark:bg-gray-800">
|
||||
<p class="text-2xl text-gray-400 dark:text-gray-500">
|
||||
<!-- <svg class="w-3.5 h-3.5" aria-hidden="true" xmlns="http://www.w3.org/2000/svg" fill="none"
|
||||
viewBox="0 0 18 18">
|
||||
<path stroke="currentColor" stroke-linecap="round" stroke-linejoin="round" stroke-width="2"
|
||||
d="M9 1v16M1 9h16" />
|
||||
</svg> -->
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
</template>
|
110
easytier-web/frontend/src/components/DeviceList.vue
Normal file
110
easytier-web/frontend/src/components/DeviceList.vue
Normal file
@@ -0,0 +1,110 @@
|
||||
<script setup lang="ts">
|
||||
import { computed, onMounted, onUnmounted, ref } from 'vue';
|
||||
import { Button, Column, DataTable, Drawer, ProgressSpinner, useToast } from 'primevue';
|
||||
import { useRoute, useRouter } from 'vue-router';
|
||||
import { Api, Utils } from 'easytier-frontend-lib';
|
||||
|
||||
const props = defineProps({
|
||||
api: Api.ApiClient,
|
||||
});
|
||||
|
||||
const api = props.api;
|
||||
|
||||
const deviceList = ref<Array<Utils.DeviceInfo> | undefined>(undefined);
|
||||
|
||||
const selectedDeviceId = computed<string | undefined>(() => route.params.deviceId as string);
|
||||
|
||||
const route = useRoute();
|
||||
const router = useRouter();
|
||||
const toast = useToast();
|
||||
|
||||
const loadDevices = async () => {
|
||||
const resp = await api?.list_machines();
|
||||
let devices: Array<Utils.DeviceInfo> = [];
|
||||
for (const device of (resp || [])) {
|
||||
devices.push({
|
||||
hostname: device.info?.hostname,
|
||||
public_ip: device.client_url,
|
||||
running_network_instances: device.info?.running_network_instances.map((instance: any) => Utils.UuidToStr(instance)),
|
||||
running_network_count: device.info?.running_network_instances.length,
|
||||
report_time: device.info?.report_time,
|
||||
easytier_version: device.info?.easytier_version,
|
||||
machine_id: Utils.UuidToStr(device.info?.machine_id),
|
||||
});
|
||||
}
|
||||
console.debug("device list", deviceList.value);
|
||||
deviceList.value = devices;
|
||||
};
|
||||
|
||||
const periodFunc = new Utils.PeriodicTask(async () => {
|
||||
try {
|
||||
await loadDevices();
|
||||
} catch (e) {
|
||||
toast.add({ severity: 'error', summary: 'Load Device List Failed', detail: e, life: 2000 });
|
||||
console.error(e);
|
||||
}
|
||||
}, 1000);
|
||||
|
||||
onMounted(async () => {
|
||||
periodFunc.start();
|
||||
});
|
||||
|
||||
onUnmounted(() => {
|
||||
periodFunc.stop();
|
||||
});
|
||||
|
||||
const deviceManageVisible = computed<boolean>({
|
||||
get: () => !!selectedDeviceId.value,
|
||||
set: (value) => {
|
||||
if (!value) {
|
||||
router.push({ name: 'deviceList', params: { deviceId: undefined } });
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
const selectedDeviceHostname = computed<string | undefined>(() => {
|
||||
return deviceList.value?.find((device) => device.machine_id === selectedDeviceId.value)?.hostname;
|
||||
});
|
||||
|
||||
</script>
|
||||
|
||||
<style scoped></style>
|
||||
|
||||
<template>
|
||||
<div v-if="deviceList === undefined" class="w-full flex justify-center">
|
||||
<ProgressSpinner />
|
||||
</div>
|
||||
|
||||
<DataTable :value="deviceList" tableStyle="min-width: 50rem" :metaKeySelection="true" sortField="hostname"
|
||||
:sortOrder="-1" v-if="deviceList !== undefined">
|
||||
<template #header>
|
||||
<div class="text-xl font-bold">Device List</div>
|
||||
</template>
|
||||
|
||||
<Column field="hostname" header="Hostname" sortable style="width: 180px"></Column>
|
||||
<Column field="public_ip" header="Public IP" style="width: 150px"></Column>
|
||||
<Column field="running_network_count" header="Running Network Count" sortable style="width: 150px"></Column>
|
||||
<Column field="report_time" header="Report Time" sortable style="width: 150px"></Column>
|
||||
<Column field="easytier_version" header="EasyTier Version" sortable style="width: 150px"></Column>
|
||||
<Column class="w-24 !text-end">
|
||||
<template #body="{ data }">
|
||||
<Button icon="pi pi-cog"
|
||||
@click="router.push({ name: 'deviceManagement', params: { deviceId: data.machine_id, instanceId: data.running_network_instances[0] } })"
|
||||
severity="secondary" rounded></Button>
|
||||
</template>
|
||||
</Column>
|
||||
|
||||
<template #footer>
|
||||
<div class="flex justify-end">
|
||||
<Button icon="pi pi-refresh" label="Reload" severity="info" @click="loadDevices" />
|
||||
</div>
|
||||
</template>
|
||||
</DataTable>
|
||||
|
||||
<Drawer v-model:visible="deviceManageVisible" :header="`Manage ${selectedDeviceHostname}`" position="right"
|
||||
class="w-1/2 min-w-96">
|
||||
<RouterView v-slot="{ Component }">
|
||||
<component :is="Component" :api="api" :deviceList="deviceList" @update="loadDevices" />
|
||||
</RouterView>
|
||||
</Drawer>
|
||||
</template>
|
197
easytier-web/frontend/src/components/DeviceManagement.vue
Normal file
197
easytier-web/frontend/src/components/DeviceManagement.vue
Normal file
@@ -0,0 +1,197 @@
|
||||
<script setup lang="ts">
|
||||
import { Toolbar, IftaLabel, Select, Button, ConfirmPopup, Dialog, useConfirm, useToast } from 'primevue';
|
||||
import { NetworkTypes, Status, Utils, Api, } from 'easytier-frontend-lib';
|
||||
import { computed, onMounted, onUnmounted, ref } from 'vue';
|
||||
import { useRoute, useRouter } from 'vue-router';
|
||||
|
||||
const props = defineProps<{
|
||||
api: Api.ApiClient;
|
||||
deviceList: Array<Utils.DeviceInfo> | undefined;
|
||||
}>();
|
||||
|
||||
const emits = defineEmits(['update']);
|
||||
|
||||
const route = useRoute();
|
||||
const router = useRouter();
|
||||
const toast = useToast();
|
||||
|
||||
const deviceId = computed<string>(() => {
|
||||
return route.params.deviceId as string;
|
||||
});
|
||||
|
||||
const instanceId = computed<string>(() => {
|
||||
return route.params.instanceId as string;
|
||||
});
|
||||
|
||||
const deviceInfo = computed<Utils.DeviceInfo | undefined | null>(() => {
|
||||
return deviceId.value ? props.deviceList?.find((device) => device.machine_id === deviceId.value) : null;
|
||||
});
|
||||
|
||||
const curNetworkInfo = ref<NetworkTypes.NetworkInstance | null>(null);
|
||||
|
||||
const isEditing = ref(false);
|
||||
const showCreateNetworkDialog = ref(false);
|
||||
const newNetworkConfig = ref<NetworkTypes.NetworkConfig>(NetworkTypes.DEFAULT_NETWORK_CONFIG());
|
||||
|
||||
const instanceIdList = computed(() => {
|
||||
let insts = deviceInfo.value?.running_network_instances || [];
|
||||
let options = insts.map((instance: string) => {
|
||||
return { uuid: instance };
|
||||
});
|
||||
return options;
|
||||
});
|
||||
|
||||
const selectedInstanceId = computed({
|
||||
get() {
|
||||
return instanceIdList.value.find((instance) => instance.uuid === instanceId.value);
|
||||
},
|
||||
set(value: any) {
|
||||
console.log("set instanceId", value);
|
||||
router.push({ name: 'deviceManagement', params: { deviceId: deviceId.value, instanceId: value.uuid } });
|
||||
}
|
||||
});
|
||||
|
||||
const confirm = useConfirm();
|
||||
const confirmDeleteNetwork = (event: any) => {
|
||||
confirm.require({
|
||||
target: event.currentTarget,
|
||||
message: 'Do you want to delete this network?',
|
||||
icon: 'pi pi-info-circle',
|
||||
rejectProps: {
|
||||
label: 'Cancel',
|
||||
severity: 'secondary',
|
||||
outlined: true
|
||||
},
|
||||
acceptProps: {
|
||||
label: 'Delete',
|
||||
severity: 'danger'
|
||||
},
|
||||
accept: async () => {
|
||||
try {
|
||||
await props.api?.delete_network(deviceId.value, instanceId.value);
|
||||
} catch (e) {
|
||||
console.error(e);
|
||||
}
|
||||
emits('update');
|
||||
},
|
||||
reject: () => {
|
||||
return;
|
||||
}
|
||||
});
|
||||
};
|
||||
|
||||
// const verifyNetworkConfig = async (): Promise<ValidateConfigResponse | undefined> => {
|
||||
// let ret = await props.api?.validate_config(deviceId.value, newNetworkConfig.value);
|
||||
// console.log("verifyNetworkConfig", ret);
|
||||
// return ret;
|
||||
// }
|
||||
|
||||
const createNewNetwork = async () => {
|
||||
try {
|
||||
if (isEditing.value) {
|
||||
await props.api?.delete_network(deviceId.value, instanceId.value);
|
||||
}
|
||||
let ret = await props.api?.run_network(deviceId.value, newNetworkConfig.value);
|
||||
console.debug("createNewNetwork", ret);
|
||||
} catch (e: any) {
|
||||
console.error(e);
|
||||
toast.add({ severity: 'error', summary: 'Error', detail: 'Failed to create network, error: ' + JSON.stringify(e.response.data), life: 2000 });
|
||||
return;
|
||||
}
|
||||
emits('update');
|
||||
showCreateNetworkDialog.value = false;
|
||||
}
|
||||
|
||||
const newNetwork = () => {
|
||||
newNetworkConfig.value = NetworkTypes.DEFAULT_NETWORK_CONFIG();
|
||||
isEditing.value = false;
|
||||
showCreateNetworkDialog.value = true;
|
||||
}
|
||||
|
||||
const editNetwork = async () => {
|
||||
if (!deviceId.value || !instanceId.value) {
|
||||
toast.add({ severity: 'error', summary: 'Error', detail: 'No network instance selected', life: 2000 });
|
||||
return;
|
||||
}
|
||||
|
||||
isEditing.value = true;
|
||||
|
||||
try {
|
||||
let ret = await props.api?.get_network_config(deviceId.value, instanceId.value);
|
||||
console.debug("editNetwork", ret);
|
||||
newNetworkConfig.value = ret;
|
||||
showCreateNetworkDialog.value = true;
|
||||
} catch (e: any) {
|
||||
console.error(e);
|
||||
toast.add({ severity: 'error', summary: 'Error', detail: 'Failed to edit network, error: ' + JSON.stringify(e.response.data), life: 2000 });
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
const loadDeviceInfo = async () => {
|
||||
if (!deviceId.value || !instanceId.value) {
|
||||
return;
|
||||
}
|
||||
|
||||
let ret = await props.api?.get_network_info(deviceId.value, instanceId.value);
|
||||
let device_info = ret[instanceId.value];
|
||||
|
||||
curNetworkInfo.value = {
|
||||
instance_id: instanceId.value,
|
||||
running: device_info.running,
|
||||
error_msg: device_info.error_msg,
|
||||
detail: device_info,
|
||||
} as NetworkTypes.NetworkInstance;
|
||||
}
|
||||
|
||||
let periodFunc = new Utils.PeriodicTask(async () => {
|
||||
try {
|
||||
await loadDeviceInfo();
|
||||
} catch (e) {
|
||||
console.debug(e);
|
||||
}
|
||||
}, 1000);
|
||||
|
||||
onMounted(async () => {
|
||||
periodFunc.start();
|
||||
});
|
||||
|
||||
onUnmounted(() => {
|
||||
periodFunc.stop();
|
||||
});
|
||||
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<ConfirmPopup></ConfirmPopup>
|
||||
<Dialog v-model:visible="showCreateNetworkDialog" modal :header="!isEditing ? 'Create New Network' : 'Edit Network'"
|
||||
:style="{ width: '55rem' }">
|
||||
<Config :cur-network="newNetworkConfig" @run-network="createNewNetwork"></Config>
|
||||
</Dialog>
|
||||
|
||||
<Toolbar>
|
||||
<template #start>
|
||||
<IftaLabel>
|
||||
<Select v-model="selectedInstanceId" :options="instanceIdList" optionLabel="uuid" inputId="dd-inst-id"
|
||||
placeholder="Select Instance" />
|
||||
<label class="mr-3" for="dd-inst-id">Network</label>
|
||||
</IftaLabel>
|
||||
</template>
|
||||
|
||||
<template #end>
|
||||
<div class="gap-x-3 flex">
|
||||
<Button @click="confirmDeleteNetwork($event)" icon="pi pi-minus" severity="danger" label="Delete"
|
||||
iconPos="right" />
|
||||
<Button @click="editNetwork" icon="pi pi-pen-to-square" label="Edit" iconPos="right" severity="info" />
|
||||
<Button @click="newNetwork" icon="pi pi-plus" label="Create" iconPos="right" />
|
||||
</div>
|
||||
</template>
|
||||
</Toolbar>
|
||||
|
||||
<Status v-bind:cur-network-inst="curNetworkInfo" v-if="!!selectedInstanceId">
|
||||
</Status>
|
||||
|
||||
<div class="grid grid-cols-1 gap-4 place-content-center h-full" v-if="!selectedInstanceId">
|
||||
<div class="text-center text-xl"> Select or create a network instance to manage </div>
|
||||
</div>
|
||||
</template>
|
122
easytier-web/frontend/src/components/Login.vue
Normal file
122
easytier-web/frontend/src/components/Login.vue
Normal file
@@ -0,0 +1,122 @@
|
||||
<script setup lang="ts">
|
||||
import { computed, ref } from 'vue';
|
||||
import { Card, InputText, Password, Button, AutoComplete } from 'primevue';
|
||||
import { useRouter } from 'vue-router';
|
||||
import { useToast } from 'primevue/usetoast';
|
||||
import { Api } from 'easytier-frontend-lib';
|
||||
|
||||
defineProps<{
|
||||
isRegistering: boolean;
|
||||
}>();
|
||||
|
||||
const api = computed<Api.ApiClient>(() => new Api.ApiClient(apiHost.value));
|
||||
const router = useRouter();
|
||||
const toast = useToast();
|
||||
|
||||
const username = ref('');
|
||||
const password = ref('');
|
||||
const registerUsername = ref('');
|
||||
const registerPassword = ref('');
|
||||
const captcha = ref('');
|
||||
const captchaSrc = computed(() => api.value.captcha_url());
|
||||
|
||||
const onSubmit = async () => {
|
||||
// Add your login logic here
|
||||
const credential: Api.Credential = { username: username.value, password: password.value, };
|
||||
let ret = await api.value?.login(credential);
|
||||
if (ret.success) {
|
||||
localStorage.setItem('apiHost', btoa(apiHost.value));
|
||||
router.push({
|
||||
name: 'dashboard',
|
||||
params: { apiHost: btoa(apiHost.value) },
|
||||
});
|
||||
} else {
|
||||
toast.add({ severity: 'error', summary: 'Login Failed', detail: ret.message, life: 2000 });
|
||||
}
|
||||
};
|
||||
|
||||
const onRegister = async () => {
|
||||
const credential: Api.Credential = { username: registerUsername.value, password: registerPassword.value };
|
||||
const registerReq: Api.RegisterData = { credentials: credential, captcha: captcha.value };
|
||||
let ret = await api.value?.register(registerReq);
|
||||
if (ret.success) {
|
||||
toast.add({ severity: 'success', summary: 'Register Success', detail: ret.message, life: 2000 });
|
||||
router.push({ name: 'login' });
|
||||
} else {
|
||||
toast.add({ severity: 'error', summary: 'Register Failed', detail: ret.message, life: 2000 });
|
||||
}
|
||||
};
|
||||
|
||||
const defaultApiHost = 'https://config-server.easytier.cn'
|
||||
const apiHost = ref<string>(defaultApiHost)
|
||||
const apiHostSuggestions = ref<Array<string>>([])
|
||||
const apiHostSearch = async (event: { query: string }) => {
|
||||
apiHostSuggestions.value = [];
|
||||
if (event.query) {
|
||||
apiHostSuggestions.value.push(event.query);
|
||||
}
|
||||
apiHostSuggestions.value.push(defaultApiHost);
|
||||
}
|
||||
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div class="flex items-center justify-center min-h-screen">
|
||||
<Card class="w-full max-w-md p-6">
|
||||
<template #header>
|
||||
<h2 class="text-2xl font-semibold text-center">{{ isRegistering ? 'Register' : 'Login' }}
|
||||
</h2>
|
||||
</template>
|
||||
<template #content>
|
||||
<div class="p-field mb-4">
|
||||
<label for="api-host" class="block text-sm font-medium">Api Host</label>
|
||||
<AutoComplete id="api-host" v-model="apiHost" dropdown :suggestions="apiHostSuggestions"
|
||||
@complete="apiHostSearch" class="w-full" />
|
||||
</div>
|
||||
<form v-if="!isRegistering" @submit.prevent="onSubmit" class="space-y-4">
|
||||
<div class="p-field">
|
||||
<label for="username" class="block text-sm font-medium">Username</label>
|
||||
<InputText id="username" v-model="username" required class="w-full" />
|
||||
</div>
|
||||
<div class="p-field">
|
||||
<label for="password" class="block text-sm font-medium">Password</label>
|
||||
<Password id="password" v-model="password" required toggleMask :feedback="false" />
|
||||
</div>
|
||||
<div class="flex items-center justify-between">
|
||||
<Button label="Login" type="submit" class="w-full" />
|
||||
</div>
|
||||
<div class="flex items-center justify-between">
|
||||
<Button label="Register" type="button" class="w-full"
|
||||
@click="$router.replace({ name: 'register' })" severity="secondary" />
|
||||
</div>
|
||||
</form>
|
||||
|
||||
<form v-else @submit.prevent="onRegister" class="space-y-4">
|
||||
<div class="p-field">
|
||||
<label for="register-username" class="block text-sm font-medium">Username</label>
|
||||
<InputText id="register-username" v-model="registerUsername" required class="w-full" />
|
||||
</div>
|
||||
<div class="p-field">
|
||||
<label for="register-password" class="block text-sm font-medium">Password</label>
|
||||
<Password id="register-password" v-model="registerPassword" required toggleMask
|
||||
:feedback="false" class="w-full" />
|
||||
</div>
|
||||
<div class="p-field">
|
||||
<label for="captcha" class="block text-sm font-medium">Captcha</label>
|
||||
<InputText id="captcha" v-model="captcha" required class="w-full" />
|
||||
<img :src="captchaSrc" alt="Captcha" class="mt-2 mb-2" />
|
||||
</div>
|
||||
<div class="flex items-center justify-between">
|
||||
<Button label="Register" type="submit" class="w-full" />
|
||||
</div>
|
||||
<div class="flex items-center justify-between">
|
||||
<Button label="Back to Login" type="button" class="w-full"
|
||||
@click="$router.replace({ name: 'login' })" severity="secondary" />
|
||||
</div>
|
||||
</form>
|
||||
</template>
|
||||
</Card>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<style scoped></style>
|
173
easytier-web/frontend/src/components/MainPage.vue
Normal file
173
easytier-web/frontend/src/components/MainPage.vue
Normal file
@@ -0,0 +1,173 @@
|
||||
<script setup lang="ts">
|
||||
import { Api, I18nUtils } from 'easytier-frontend-lib'
|
||||
import { computed, onMounted, ref } from 'vue';
|
||||
import { Button, TieredMenu } from 'primevue';
|
||||
import { useRoute, useRouter } from 'vue-router';
|
||||
import { useDialog } from 'primevue/usedialog';
|
||||
import ChangePassword from './ChangePassword.vue';
|
||||
import Icon from '../assets/easytier.png'
|
||||
|
||||
const route = useRoute();
|
||||
const router = useRouter();
|
||||
const api = computed<Api.ApiClient | undefined>(() => {
|
||||
try {
|
||||
return new Api.ApiClient(atob(route.params.apiHost as string), () => {
|
||||
router.push({ name: 'login' });
|
||||
})
|
||||
} catch (e) {
|
||||
router.push({ name: 'login' });
|
||||
}
|
||||
});
|
||||
|
||||
const dialog = useDialog();
|
||||
|
||||
onMounted(async () => {
|
||||
await I18nUtils.loadLanguageAsync('cn')
|
||||
});
|
||||
|
||||
const userMenu = ref();
|
||||
const userMenuItems = ref([
|
||||
{
|
||||
label: 'Change Password',
|
||||
icon: 'pi pi-key',
|
||||
command: () => {
|
||||
console.log('File');
|
||||
let ret = dialog.open(ChangePassword, {
|
||||
props: {
|
||||
modal: true,
|
||||
},
|
||||
data: {
|
||||
api: api.value,
|
||||
}
|
||||
});
|
||||
|
||||
console.log("return", ret)
|
||||
},
|
||||
},
|
||||
{
|
||||
label: 'Logout',
|
||||
icon: 'pi pi-sign-out',
|
||||
command: async () => {
|
||||
try {
|
||||
await api.value?.logout();
|
||||
} catch (e) {
|
||||
console.error("logout failed", e);
|
||||
}
|
||||
router.push({ name: 'login' });
|
||||
},
|
||||
},
|
||||
])
|
||||
|
||||
const forceShowSideBar = ref(false)
|
||||
|
||||
</script>
|
||||
|
||||
<!-- https://flowbite.com/docs/components/sidebar/#sidebar-with-navbar -->
|
||||
<template>
|
||||
<nav class="fixed top-0 z-50 w-full bg-white border-b border-gray-200 dark:bg-gray-800 dark:border-gray-700">
|
||||
<div class="px-3 py-3 lg:px-5 lg:pl-3">
|
||||
<div class="flex items-center justify-between">
|
||||
<div class="flex items-center justify-start rtl:justify-end">
|
||||
<div class="sm:hidden">
|
||||
<Button type="button" aria-haspopup="true" icon="pi pi-list" variant="text" size="large"
|
||||
severity="contrast" @click="forceShowSideBar = !forceShowSideBar" />
|
||||
</div>
|
||||
<a href="https://easytier.top" class="flex ms-2 md:me-24">
|
||||
<img :src="Icon" class="h-9 me-3" alt="FlowBite Logo" />
|
||||
<span
|
||||
class="self-center text-xl font-semibold sm:text-2xl whitespace-nowrap dark:text-white">EasyTier</span>
|
||||
</a>
|
||||
</div>
|
||||
<div class="flex items-center">
|
||||
<div class="flex items-center ms-3">
|
||||
<div>
|
||||
<Button type="button" @click="userMenu.toggle($event)" aria-haspopup="true"
|
||||
aria-controls="user-menu" icon="pi pi-user" raised rounded />
|
||||
<TieredMenu ref="userMenu" id="user-menu" :model="userMenuItems" popup />
|
||||
</div>
|
||||
<div class="z-50 hidden my-4 text-base list-none bg-white divide-y divide-gray-100 rounded shadow dark:bg-gray-700 dark:divide-gray-600"
|
||||
id="dropdown-user">
|
||||
<div class="px-4 py-3" role="none">
|
||||
<p class="text-sm text-gray-900 dark:text-white" role="none">
|
||||
Neil Sims
|
||||
</p>
|
||||
<p class="text-sm font-medium text-gray-900 truncate dark:text-gray-300" role="none">
|
||||
neil.sims@flowbite.com
|
||||
</p>
|
||||
</div>
|
||||
<ul class="py-1" role="none">
|
||||
<li>
|
||||
<a href="#"
|
||||
class="block px-4 py-2 text-sm text-gray-700 hover:bg-gray-100 dark:text-gray-300 dark:hover:bg-gray-600 dark:hover:text-white"
|
||||
role="menuitem">Dashboard</a>
|
||||
</li>
|
||||
<li>
|
||||
<a href="#"
|
||||
class="block px-4 py-2 text-sm text-gray-700 hover:bg-gray-100 dark:text-gray-300 dark:hover:bg-gray-600 dark:hover:text-white"
|
||||
role="menuitem">Settings</a>
|
||||
</li>
|
||||
<li>
|
||||
<a href="#"
|
||||
class="block px-4 py-2 text-sm text-gray-700 hover:bg-gray-100 dark:text-gray-300 dark:hover:bg-gray-600 dark:hover:text-white"
|
||||
role="menuitem">Earnings</a>
|
||||
</li>
|
||||
<li>
|
||||
<a href="#"
|
||||
class="block px-4 py-2 text-sm text-gray-700 hover:bg-gray-100 dark:text-gray-300 dark:hover:bg-gray-600 dark:hover:text-white"
|
||||
role="menuitem">Sign out</a>
|
||||
</li>
|
||||
</ul>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</nav>
|
||||
|
||||
<aside id="logo-sidebar"
|
||||
class="fixed top-1 left-0 z-40 w-64 h-screen pt-20 transition-transform bg-white border-r border-gray-201 sm:translate-x-0 dark:bg-gray-800 dark:border-gray-700"
|
||||
:class="{ '-translate-x-full': !forceShowSideBar }" aria-label="Sidebar">
|
||||
<div class="h-full px-3 pb-4 overflow-y-auto bg-white dark:bg-gray-800">
|
||||
<ul class="space-y-2 font-medium">
|
||||
<li>
|
||||
<Button variant="text" class="w-full justify-start gap-x-3 pl-1.5 sidebar-button"
|
||||
severity="contrast" @click="router.push({ name: 'dashboard' })">
|
||||
<i class="pi pi-chart-pie text-xl"></i>
|
||||
<span class="mb-0.5">DashBoard</span>
|
||||
</Button>
|
||||
</li>
|
||||
<li>
|
||||
<Button variant="text" class="w-full justify-start gap-x-3 pl-1.5 sidebar-button"
|
||||
severity="contrast" @click="router.push({ name: 'deviceList' })">
|
||||
<i class="pi pi-server text-xl"></i>
|
||||
<span class="mb-0.5">Devices</span>
|
||||
</Button>
|
||||
</li>
|
||||
<li>
|
||||
<Button variant="text" class="w-full justify-start gap-x-3 pl-1.5 sidebar-button"
|
||||
severity="contrast" @click="router.push({ name: 'login' })">
|
||||
<i class="pi pi-sign-in text-xl"></i>
|
||||
<span class="mb-0.5">Login Page</span>
|
||||
</Button>
|
||||
</li>
|
||||
</ul>
|
||||
</div>
|
||||
</aside>
|
||||
|
||||
<div class="p-4 sm:ml-64">
|
||||
<div class="p-4 border-2 border-gray-200 border-dashed rounded-lg dark:border-gray-700 mt-14">
|
||||
<div class="grid grid-cols-1 gap-4">
|
||||
<RouterView v-slot="{ Component }">
|
||||
<component :is="Component" :api="api" />
|
||||
</RouterView>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<style scoped>
|
||||
.sidebar-button {
|
||||
text-align: left;
|
||||
justify-content: left;
|
||||
}
|
||||
</style>
|
90
easytier-web/frontend/src/main.ts
Normal file
90
easytier-web/frontend/src/main.ts
Normal file
@@ -0,0 +1,90 @@
|
||||
import { createApp } from 'vue'
|
||||
import './style.css'
|
||||
import 'easytier-frontend-lib/style.css'
|
||||
import App from './App.vue'
|
||||
import EasytierFrontendLib from 'easytier-frontend-lib'
|
||||
import PrimeVue from 'primevue/config'
|
||||
import Aura from '@primevue/themes/aura'
|
||||
import ConfirmationService from 'primevue/confirmationservice';
|
||||
|
||||
import { createRouter, createWebHashHistory } from 'vue-router'
|
||||
import MainPage from './components/MainPage.vue'
|
||||
import Login from './components/Login.vue'
|
||||
import DeviceList from './components/DeviceList.vue'
|
||||
import DeviceManagement from './components/DeviceManagement.vue'
|
||||
import Dashboard from './components/Dashboard.vue'
|
||||
import DialogService from 'primevue/dialogservice';
|
||||
import ToastService from 'primevue/toastservice';
|
||||
|
||||
const routes = [
|
||||
{
|
||||
path: '/auth', children: [
|
||||
{
|
||||
name: 'login',
|
||||
path: '',
|
||||
component: Login,
|
||||
alias: 'login',
|
||||
props: { isRegistering: false }
|
||||
},
|
||||
{
|
||||
name: 'register',
|
||||
path: 'register',
|
||||
component: Login,
|
||||
props: { isRegistering: true }
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
path: '/h/:apiHost', component: MainPage, children: [
|
||||
{
|
||||
path: '',
|
||||
alias: 'dashboard',
|
||||
name: 'dashboard',
|
||||
component: Dashboard,
|
||||
},
|
||||
{
|
||||
path: 'deviceList',
|
||||
name: 'deviceList',
|
||||
component: DeviceList,
|
||||
children: [
|
||||
{
|
||||
path: 'device/:deviceId/:instanceId?',
|
||||
name: 'deviceManagement',
|
||||
component: DeviceManagement,
|
||||
}
|
||||
]
|
||||
},
|
||||
]
|
||||
},
|
||||
{
|
||||
path: '/:pathMatch(.*)*', name: 'notFound', redirect: () => {
|
||||
let apiHost = localStorage.getItem('apiHost');
|
||||
if (apiHost) {
|
||||
return { name: 'dashboard', params: { apiHost: apiHost } }
|
||||
} else {
|
||||
return { name: 'login' }
|
||||
}
|
||||
}
|
||||
},
|
||||
]
|
||||
|
||||
const router = createRouter({
|
||||
history: createWebHashHistory(),
|
||||
routes,
|
||||
})
|
||||
|
||||
createApp(App).use(PrimeVue,
|
||||
{
|
||||
theme: {
|
||||
preset: Aura,
|
||||
options: {
|
||||
prefix: 'p',
|
||||
darkModeSelector: 'system',
|
||||
cssLayer: {
|
||||
name: 'primevue',
|
||||
order: 'tailwind-base, primevue, tailwind-utilities'
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
).use(ToastService as any).use(DialogService as any).use(router).use(ConfirmationService as any).use(EasytierFrontendLib).mount('#app')
|
33
easytier-web/frontend/src/style.css
Normal file
33
easytier-web/frontend/src/style.css
Normal file
@@ -0,0 +1,33 @@
|
||||
@layer tailwind-base, primevue, tailwind-utilities;
|
||||
|
||||
@layer tailwind-base {
|
||||
@tailwind base;
|
||||
}
|
||||
|
||||
@layer tailwind-utilities {
|
||||
@tailwind components;
|
||||
@tailwind utilities;
|
||||
}
|
||||
|
||||
.p-password {
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.p-password>input {
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
:root {
|
||||
font-family: Inter, Avenir, Helvetica, Arial, sans-serif;
|
||||
font-size: 0.9rem;
|
||||
line-height: 24px;
|
||||
font-weight: 400;
|
||||
|
||||
color: #0f0f0f;
|
||||
|
||||
font-synthesis: none;
|
||||
text-rendering: optimizeLegibility;
|
||||
-webkit-font-smoothing: antialiased;
|
||||
-moz-osx-font-smoothing: grayscale;
|
||||
-webkit-text-size-adjust: 100%;
|
||||
}
|
1
easytier-web/frontend/src/vite-env.d.ts
vendored
Normal file
1
easytier-web/frontend/src/vite-env.d.ts
vendored
Normal file
@@ -0,0 +1 @@
|
||||
/// <reference types="vite/client" />
|
11
easytier-web/frontend/tailwind.config.js
Normal file
11
easytier-web/frontend/tailwind.config.js
Normal file
@@ -0,0 +1,11 @@
|
||||
/** @type {import('tailwindcss').Config} */
|
||||
export default {
|
||||
content: [
|
||||
'./index.html',
|
||||
'./src/**/*.{vue,js,ts,jsx,tsx}',
|
||||
],
|
||||
theme: {
|
||||
extend: {},
|
||||
},
|
||||
plugins: [require('tailwindcss-primeui')],
|
||||
}
|
26
easytier-web/frontend/tsconfig.app.json
Normal file
26
easytier-web/frontend/tsconfig.app.json
Normal file
@@ -0,0 +1,26 @@
|
||||
{
|
||||
"compilerOptions": {
|
||||
"tsBuildInfoFile": "./node_modules/.tmp/tsconfig.app.tsbuildinfo",
|
||||
"target": "ES2020",
|
||||
"useDefineForClassFields": true,
|
||||
"module": "ESNext",
|
||||
"lib": ["ES2020", "DOM", "DOM.Iterable"],
|
||||
"skipLibCheck": true,
|
||||
|
||||
/* Bundler mode */
|
||||
"moduleResolution": "Bundler",
|
||||
"allowImportingTsExtensions": true,
|
||||
"isolatedModules": true,
|
||||
"moduleDetection": "force",
|
||||
"noEmit": true,
|
||||
"jsx": "preserve",
|
||||
|
||||
/* Linting */
|
||||
"strict": true,
|
||||
"noUnusedLocals": true,
|
||||
"noUnusedParameters": true,
|
||||
"noFallthroughCasesInSwitch": true,
|
||||
"noUncheckedSideEffectImports": true
|
||||
},
|
||||
"include": ["src/**/*.ts", "src/**/*.tsx", "src/**/*.vue", "../frontend-lib/src/modules/api.ts"]
|
||||
}
|
7
easytier-web/frontend/tsconfig.json
Normal file
7
easytier-web/frontend/tsconfig.json
Normal file
@@ -0,0 +1,7 @@
|
||||
{
|
||||
"files": [],
|
||||
"references": [
|
||||
{ "path": "./tsconfig.app.json" },
|
||||
{ "path": "./tsconfig.node.json" }
|
||||
]
|
||||
}
|
24
easytier-web/frontend/tsconfig.node.json
Normal file
24
easytier-web/frontend/tsconfig.node.json
Normal file
@@ -0,0 +1,24 @@
|
||||
{
|
||||
"compilerOptions": {
|
||||
"tsBuildInfoFile": "./node_modules/.tmp/tsconfig.node.tsbuildinfo",
|
||||
"target": "ES2022",
|
||||
"lib": ["ES2023"],
|
||||
"module": "ESNext",
|
||||
"skipLibCheck": true,
|
||||
|
||||
/* Bundler mode */
|
||||
"moduleResolution": "Bundler",
|
||||
"allowImportingTsExtensions": true,
|
||||
"isolatedModules": true,
|
||||
"moduleDetection": "force",
|
||||
"noEmit": true,
|
||||
|
||||
/* Linting */
|
||||
"strict": true,
|
||||
"noUnusedLocals": true,
|
||||
"noUnusedParameters": true,
|
||||
"noFallthroughCasesInSwitch": true,
|
||||
"noUncheckedSideEffectImports": true
|
||||
},
|
||||
"include": ["vite.config.ts"]
|
||||
}
|
9
easytier-web/frontend/vite.config.ts
Normal file
9
easytier-web/frontend/vite.config.ts
Normal file
@@ -0,0 +1,9 @@
|
||||
import { defineConfig } from 'vite'
|
||||
import vue from '@vitejs/plugin-vue'
|
||||
import { viteSingleFile } from "vite-plugin-singlefile"
|
||||
|
||||
// https://vite.dev/config/
|
||||
export default defineConfig({
|
||||
base: '',
|
||||
plugins: [vue(), viteSingleFile()],
|
||||
})
|
85
easytier-web/migrations/20241026_init.sql
Normal file
85
easytier-web/migrations/20241026_init.sql
Normal file
@@ -0,0 +1,85 @@
|
||||
-- # Entity schema.
|
||||
|
||||
-- Create `users` table.
|
||||
create table if not exists users (
|
||||
id integer primary key autoincrement,
|
||||
username text not null unique,
|
||||
password text not null
|
||||
);
|
||||
|
||||
-- Create `groups` table.
|
||||
create table if not exists groups (
|
||||
id integer primary key autoincrement,
|
||||
name text not null unique
|
||||
);
|
||||
|
||||
-- Create `permissions` table.
|
||||
create table if not exists permissions (
|
||||
id integer primary key autoincrement,
|
||||
name text not null unique
|
||||
);
|
||||
|
||||
|
||||
-- # Join tables.
|
||||
|
||||
-- Create `users_groups` table for many-to-many relationships between users and groups.
|
||||
create table if not exists users_groups (
|
||||
user_id integer references users(id),
|
||||
group_id integer references groups(id),
|
||||
primary key (user_id, group_id)
|
||||
);
|
||||
|
||||
-- Create `groups_permissions` table for many-to-many relationships between groups and permissions.
|
||||
create table if not exists groups_permissions (
|
||||
group_id integer references groups(id),
|
||||
permission_id integer references permissions(id),
|
||||
primary key (group_id, permission_id)
|
||||
);
|
||||
|
||||
|
||||
-- # Fixture hydration.
|
||||
|
||||
-- Insert "user" user. password: "user"
|
||||
insert into users (username, password)
|
||||
values (
|
||||
'user',
|
||||
'$argon2i$v=19$m=16,t=2,p=1$dHJ5dXZkYmZkYXM$UkrNqWz0BbSVBq4ykLSuJw'
|
||||
);
|
||||
|
||||
-- Insert "admin" user. password: "admin"
|
||||
insert into users (username, password)
|
||||
values (
|
||||
'admin',
|
||||
'$argon2i$v=19$m=16,t=2,p=1$Ymd1Y2FlcnQ$x0q4oZinW9S1ZB9BcaHEpQ'
|
||||
);
|
||||
|
||||
-- Insert "users" and "superusers" groups.
|
||||
insert into groups (name) values ('users');
|
||||
insert into groups (name) values ('superusers');
|
||||
|
||||
-- Insert individual permissions.
|
||||
insert into permissions (name) values ('sessions');
|
||||
insert into permissions (name) values ('devices');
|
||||
|
||||
-- Insert group permissions.
|
||||
insert into groups_permissions (group_id, permission_id)
|
||||
values (
|
||||
(select id from groups where name = 'users'),
|
||||
(select id from permissions where name = 'devices')
|
||||
), (
|
||||
(select id from groups where name = 'superusers'),
|
||||
(select id from permissions where name = 'sessions')
|
||||
);
|
||||
|
||||
-- Insert users into groups.
|
||||
insert into users_groups (user_id, group_id)
|
||||
values (
|
||||
(select id from users where username = 'user'),
|
||||
(select id from groups where name = 'users')
|
||||
), (
|
||||
(select id from users where username = 'admin'),
|
||||
(select id from groups where name = 'users')
|
||||
), (
|
||||
(select id from users where username = 'admin'),
|
||||
(select id from groups where name = 'superusers')
|
||||
);
|
BIN
easytier-web/resources/robot.ttf
Normal file
BIN
easytier-web/resources/robot.ttf
Normal file
Binary file not shown.
152
easytier-web/src/client_manager/mod.rs
Normal file
152
easytier-web/src/client_manager/mod.rs
Normal file
@@ -0,0 +1,152 @@
|
||||
pub mod session;
|
||||
pub mod storage;
|
||||
|
||||
use std::sync::Arc;
|
||||
|
||||
use dashmap::DashMap;
|
||||
use easytier::{
|
||||
common::scoped_task::ScopedTask, proto::web::HeartbeatRequest, tunnel::TunnelListener,
|
||||
};
|
||||
use session::Session;
|
||||
use storage::{Storage, StorageToken};
|
||||
|
||||
use crate::db::Db;
|
||||
|
||||
#[derive(Debug)]
|
||||
pub struct ClientManager {
|
||||
accept_task: Option<ScopedTask<()>>,
|
||||
clear_task: Option<ScopedTask<()>>,
|
||||
|
||||
client_sessions: Arc<DashMap<url::Url, Arc<Session>>>,
|
||||
storage: Storage,
|
||||
}
|
||||
|
||||
impl ClientManager {
|
||||
pub fn new(db: Db) -> Self {
|
||||
ClientManager {
|
||||
accept_task: None,
|
||||
clear_task: None,
|
||||
|
||||
client_sessions: Arc::new(DashMap::new()),
|
||||
storage: Storage::new(db),
|
||||
}
|
||||
}
|
||||
|
||||
pub async fn serve<L: TunnelListener + 'static>(
|
||||
&mut self,
|
||||
mut listener: L,
|
||||
) -> Result<(), anyhow::Error> {
|
||||
listener.listen().await?;
|
||||
|
||||
let sessions = self.client_sessions.clone();
|
||||
let storage = self.storage.weak_ref();
|
||||
let task = tokio::spawn(async move {
|
||||
while let Ok(tunnel) = listener.accept().await {
|
||||
let info = tunnel.info().unwrap();
|
||||
let client_url: url::Url = info.remote_addr.unwrap().into();
|
||||
println!("New session from {:?}", tunnel.info());
|
||||
let mut session = Session::new(storage.clone(), client_url.clone());
|
||||
session.serve(tunnel).await;
|
||||
sessions.insert(client_url, Arc::new(session));
|
||||
}
|
||||
});
|
||||
|
||||
self.accept_task = Some(ScopedTask::from(task));
|
||||
|
||||
let sessions = self.client_sessions.clone();
|
||||
let task = tokio::spawn(async move {
|
||||
loop {
|
||||
tokio::time::sleep(std::time::Duration::from_secs(15)).await;
|
||||
sessions.retain(|_, session| session.is_running());
|
||||
}
|
||||
});
|
||||
self.clear_task = Some(ScopedTask::from(task));
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub fn is_running(&self) -> bool {
|
||||
self.accept_task.is_some() && self.clear_task.is_some()
|
||||
}
|
||||
|
||||
pub async fn list_sessions(&self) -> Vec<StorageToken> {
|
||||
let sessions = self
|
||||
.client_sessions
|
||||
.iter()
|
||||
.map(|item| item.value().clone())
|
||||
.collect::<Vec<_>>();
|
||||
|
||||
let mut ret: Vec<StorageToken> = vec![];
|
||||
for s in sessions {
|
||||
if let Some(t) = s.get_token().await {
|
||||
ret.push(t);
|
||||
}
|
||||
}
|
||||
|
||||
ret
|
||||
}
|
||||
|
||||
pub fn get_session_by_machine_id(&self, machine_id: &uuid::Uuid) -> Option<Arc<Session>> {
|
||||
let c_url = self.storage.get_client_url_by_machine_id(machine_id)?;
|
||||
self.client_sessions
|
||||
.get(&c_url)
|
||||
.map(|item| item.value().clone())
|
||||
}
|
||||
|
||||
pub fn list_machine_by_token(&self, token: String) -> Vec<url::Url> {
|
||||
self.storage.list_token_clients(&token)
|
||||
}
|
||||
|
||||
pub async fn get_heartbeat_requests(&self, client_url: &url::Url) -> Option<HeartbeatRequest> {
|
||||
let s = self.client_sessions.get(client_url)?.clone();
|
||||
s.data().read().await.req()
|
||||
}
|
||||
|
||||
pub fn db(&self) -> &Db {
|
||||
self.storage.db()
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use std::time::Duration;
|
||||
|
||||
use easytier::{
|
||||
tunnel::{
|
||||
common::tests::wait_for_condition,
|
||||
udp::{UdpTunnelConnector, UdpTunnelListener},
|
||||
},
|
||||
web_client::WebClient,
|
||||
};
|
||||
|
||||
use crate::{client_manager::ClientManager, db::Db};
|
||||
|
||||
#[tokio::test]
|
||||
async fn test_client() {
|
||||
let listener = UdpTunnelListener::new("udp://0.0.0.0:54333".parse().unwrap());
|
||||
let mut mgr = ClientManager::new(Db::memory_db().await);
|
||||
mgr.serve(Box::new(listener)).await.unwrap();
|
||||
|
||||
let connector = UdpTunnelConnector::new("udp://127.0.0.1:54333".parse().unwrap());
|
||||
let _c = WebClient::new(connector, "test");
|
||||
|
||||
wait_for_condition(
|
||||
|| async { mgr.client_sessions.len() == 1 },
|
||||
Duration::from_secs(6),
|
||||
)
|
||||
.await;
|
||||
|
||||
let mut a = mgr
|
||||
.client_sessions
|
||||
.iter()
|
||||
.next()
|
||||
.unwrap()
|
||||
.data()
|
||||
.read()
|
||||
.await
|
||||
.heartbeat_waiter();
|
||||
let req = a.recv().await.unwrap();
|
||||
println!("{:?}", req);
|
||||
println!("{:?}", mgr);
|
||||
}
|
||||
}
|
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user