mirror of
https://github.com/donaldzou/WGDashboard.git
synced 2026-04-14 17:06:17 +00:00
Compare commits
261 Commits
v4.3.0.1
...
dependabot
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
e7726658f9 | ||
|
|
ebd538e2d8 | ||
|
|
b36b0469e1 | ||
|
|
2b9a39f124 | ||
|
|
cd709c2807 | ||
|
|
6bc54e5d65 | ||
|
|
65c2e69ac6 | ||
|
|
96acc10153 | ||
|
|
1a667a60c1 | ||
|
|
bbc85d6a2d | ||
|
|
3995a5db47 | ||
|
|
b8750860a8 | ||
|
|
4711c750cc | ||
|
|
51bee91c32 | ||
|
|
5fcfe97fab | ||
|
|
74bee75df3 | ||
|
|
1aa1b8a51b | ||
|
|
833ee88340 | ||
|
|
6cb4bac673 | ||
|
|
e87c52b6f2 | ||
|
|
f53da6eacb | ||
|
|
8d35887950 | ||
|
|
ebf37c879d | ||
|
|
21bea56426 | ||
|
|
0a33132d26 | ||
|
|
b15bbce9bc | ||
|
|
ddaada8e4f | ||
|
|
920f832297 | ||
|
|
22df8a1b1f | ||
|
|
468d1c7642 | ||
|
|
f58226e398 | ||
|
|
eeedf705aa | ||
|
|
b9c271ff4c | ||
|
|
ba4ac7c1ec | ||
|
|
06097316ec | ||
|
|
e71179a2d4 | ||
|
|
bb18c55003 | ||
|
|
8e08d20497 | ||
|
|
bb5d45a66d | ||
|
|
2cdc0265b8 | ||
|
|
df295bf470 | ||
|
|
dfb75d77b8 | ||
|
|
18ef023817 | ||
|
|
12da167b8c | ||
|
|
db78f42a04 | ||
|
|
f742f9de49 | ||
|
|
686fc139ec | ||
|
|
78a7211680 | ||
|
|
1bcb48a50e | ||
|
|
a56297bf87 | ||
|
|
2e01ecc455 | ||
|
|
80b1b8875d | ||
|
|
ebd13b4df9 | ||
|
|
8ca0363fd0 | ||
|
|
07534b6f66 | ||
|
|
e4d441388d | ||
|
|
a8bf35b0c0 | ||
|
|
82f170cd57 | ||
|
|
6beda22f39 | ||
|
|
6b1d2b4b50 | ||
|
|
3f23c0f949 | ||
|
|
abaaff809a | ||
|
|
eebb2e39cc | ||
|
|
fd2daa2934 | ||
|
|
cc253a6a11 | ||
|
|
06ce640f56 | ||
|
|
8c8746cb9b | ||
|
|
97d92d5310 | ||
|
|
a19ab85ba9 | ||
|
|
6705705e3d | ||
|
|
8f973021fc | ||
|
|
23256a873c | ||
|
|
20847a0954 | ||
|
|
515bd0f806 | ||
|
|
27742d65c6 | ||
|
|
ddf378d617 | ||
|
|
35c45b159b | ||
|
|
654bf6d779 | ||
|
|
fb66fd2dda | ||
|
|
0f98bb2537 | ||
|
|
d5026a4081 | ||
|
|
aeed7cf322 | ||
|
|
29ebe65587 | ||
|
|
ce553de03b | ||
|
|
44d274a998 | ||
|
|
b6e2f084e6 | ||
|
|
cb0619036f | ||
|
|
3a7e16ca9e | ||
|
|
601ccb355d | ||
|
|
2347c76812 | ||
|
|
9ffba5e33a | ||
|
|
18018d3079 | ||
|
|
2b703d6ab8 | ||
|
|
d506fdf2c1 | ||
|
|
e5dc6086fa | ||
|
|
64506c9088 | ||
|
|
d2b05b77c8 | ||
|
|
17e23685ec | ||
|
|
9a7b23748d | ||
|
|
6239b8237a | ||
|
|
dd6ffa694f | ||
|
|
febc8818b0 | ||
|
|
8b7d372dcb | ||
|
|
66dd5c4f9f | ||
|
|
682587183f | ||
|
|
a3da48be8c | ||
|
|
e398a07f32 | ||
|
|
0745d39a50 | ||
|
|
87bdc5dde9 | ||
|
|
126c200fa2 | ||
|
|
5486b3aaf6 | ||
|
|
c8967b449b | ||
|
|
72effeecf0 | ||
|
|
dcaf5eff32 | ||
|
|
540a085821 | ||
|
|
06cdb0b59e | ||
|
|
faf9e663b3 | ||
|
|
174fd527cc | ||
|
|
a3058d2a28 | ||
|
|
0d70d13d0f | ||
|
|
a0bbace7ee | ||
|
|
53f7a32202 | ||
|
|
c35f06b262 | ||
|
|
6de90410d0 | ||
|
|
87ff8afefe | ||
|
|
9ae5fea7b4 | ||
|
|
40e2dddc00 | ||
|
|
a584bacdba | ||
|
|
e6dcef2b01 | ||
|
|
f58e572558 | ||
|
|
721ac89420 | ||
|
|
9e9f07408d | ||
|
|
92651729eb | ||
|
|
d51c543346 | ||
|
|
3adca64624 | ||
|
|
b1d7199e84 | ||
|
|
0a64b1b377 | ||
|
|
bc20057de6 | ||
|
|
c7c9d62e80 | ||
|
|
f5ccbdf9d3 | ||
|
|
a09342ed2a | ||
|
|
306e17eb4d | ||
|
|
d75c7ad418 | ||
|
|
26171ee091 | ||
|
|
82e10659b6 | ||
|
|
b6f25ac817 | ||
|
|
48481ab992 | ||
|
|
d56a3cb5d6 | ||
|
|
d6a930c04b | ||
|
|
6fe257fa4a | ||
|
|
e5d1a64d7d | ||
|
|
da4cec60e5 | ||
|
|
a4d471df4f | ||
|
|
f00940b73c | ||
|
|
cb9cb4a0b6 | ||
|
|
b695ccc5f9 | ||
|
|
16cd4da5fc | ||
|
|
7be830a34e | ||
|
|
eb65d6fb2a | ||
|
|
6fee7da633 | ||
|
|
2e8d2fe400 | ||
|
|
da3c34c250 | ||
|
|
d864d12793 | ||
|
|
36390a4d71 | ||
|
|
5db2baf1dc | ||
|
|
dc10da607d | ||
|
|
9b4abf19fa | ||
|
|
bf823e6cf8 | ||
|
|
880c94b8a3 | ||
|
|
656eba3a57 | ||
|
|
4154a27f4d | ||
|
|
785d0d6b06 | ||
|
|
9af2983a7b | ||
|
|
83b4753d5f | ||
|
|
f4f0ab27ef | ||
|
|
d51815e0f7 | ||
|
|
413b65e976 | ||
|
|
b1ea6d3c1d | ||
|
|
c7bed91626 | ||
|
|
b9e09bf91b | ||
|
|
839f686d73 | ||
|
|
1ead86dacc | ||
|
|
c4265b31a9 | ||
|
|
63e6c2040f | ||
|
|
35de3eb45a | ||
|
|
7c35f81af1 | ||
|
|
1a653bfe32 | ||
|
|
024072688f | ||
|
|
fa59de4a09 | ||
|
|
4756556c82 | ||
|
|
66ffc12864 | ||
|
|
9f609b01de | ||
|
|
a40df0481f | ||
|
|
2de46d97cf | ||
|
|
bd6d27f282 | ||
|
|
61c9f5aab7 | ||
|
|
aba3c5da64 | ||
|
|
2ee1b3b138 | ||
|
|
876a079be3 | ||
|
|
1fe03e1664 | ||
|
|
d55770c385 | ||
|
|
cd11e0495a | ||
|
|
24367db1f5 | ||
|
|
bfc3e285db | ||
|
|
58dc5a70dc | ||
|
|
c94b8a17bc | ||
|
|
b18ceca439 | ||
|
|
b794c342b1 | ||
|
|
230fa166b8 | ||
|
|
0ed5640499 | ||
|
|
931d547046 | ||
|
|
30dd9f3e54 | ||
|
|
c98a83e5b1 | ||
|
|
10c9b5635f | ||
|
|
9dd5341fd9 | ||
|
|
d763867566 | ||
|
|
18531b71c7 | ||
|
|
bbeae93c05 | ||
|
|
3c297cbb96 | ||
|
|
954f99b354 | ||
|
|
3f75c0aa3b | ||
|
|
33d8a1f991 | ||
|
|
616238735a | ||
|
|
649c1ad292 | ||
|
|
145b18c28c | ||
|
|
34e43de1a1 | ||
|
|
b7ed934331 | ||
|
|
d5a7632bb6 | ||
|
|
6621c5f910 | ||
|
|
09a3fddd87 | ||
|
|
45510165a2 | ||
|
|
508dbf8dc2 | ||
|
|
eb1d52ffba | ||
|
|
5db7351f8c | ||
|
|
5ae3a56337 | ||
|
|
e7068b472e | ||
|
|
3dc94a35a1 | ||
|
|
f82abd71a3 | ||
|
|
602238d794 | ||
|
|
4d4a15740b | ||
|
|
524d50ee07 | ||
|
|
fc591b7fe8 | ||
|
|
c2f06193d0 | ||
|
|
f2ead12315 | ||
|
|
ca8700ac2a | ||
|
|
10a8d22efd | ||
|
|
fc3ec61373 | ||
|
|
094d1c0718 | ||
|
|
0d814ec03c | ||
|
|
5ccfe07e12 | ||
|
|
101ac5e985 | ||
|
|
113a780eec | ||
|
|
cf77610a56 | ||
|
|
84675fe521 | ||
|
|
5db5b35311 | ||
|
|
ff345c9609 | ||
|
|
6cccfec923 | ||
|
|
8231dd1463 | ||
|
|
d8ff020d8c | ||
|
|
238fb91360 | ||
|
|
9ecc16fcc1 |
@@ -2,4 +2,8 @@
|
||||
.github
|
||||
*.md
|
||||
tests/
|
||||
docs/
|
||||
docs/
|
||||
src/db
|
||||
src/wg-dashboard.ini
|
||||
src/static/app
|
||||
src/static/client
|
||||
|
||||
4
.github/FUNDING.yml
vendored
4
.github/FUNDING.yml
vendored
@@ -1,4 +1,4 @@
|
||||
# These are supported funding model platforms
|
||||
|
||||
github: [donaldzou]
|
||||
patreon: DonaldDonnyZou
|
||||
github: [WGDashboard]
|
||||
open_collective: wgdashboard
|
||||
|
||||
22
.github/ISSUE_TEMPLATE/bug_report.md
vendored
22
.github/ISSUE_TEMPLATE/bug_report.md
vendored
@@ -1,6 +1,6 @@
|
||||
---
|
||||
name: Bug report
|
||||
about: Create a report to help us improve
|
||||
about: Create a report to help us improve the project!
|
||||
title: ''
|
||||
labels: bug
|
||||
assignees: ''
|
||||
@@ -11,6 +11,7 @@ assignees: ''
|
||||
A clear and concise description of what the bug is.
|
||||
|
||||
**Expected Error / Traceback**
|
||||
|
||||
```
|
||||
Please provide the error traceback here
|
||||
```
|
||||
@@ -20,9 +21,22 @@ Please provide how you run the dashboard
|
||||
|
||||
**OS Information:**
|
||||
- OS: [e.g. Ubuntu 18.02]
|
||||
- Python Version: [e.g v3.7]
|
||||
- Docker: [e.g. Yes or No]
|
||||
- Python Version: [e.g v37]
|
||||
|
||||
**Docker Details**
|
||||
If using docker, provide the following:
|
||||
- Image used: [e.g. ghcr.io/wgdashboard/wgdashboard:latest]
|
||||
- Compose: [e.g. Yes or No]
|
||||
If Docker Compose is used:
|
||||
|
||||
```yaml
|
||||
**Your Docker Compose**
|
||||
```
|
||||
|
||||
**Sample of your `.conf` file**
|
||||
```
|
||||
Please provide a sample of your configuration file that you are having problem with. You can replace your public key and private key to ABCD...
|
||||
|
||||
```ini
|
||||
Please provide a sample of your configuration file that you are having problem with.
|
||||
You should replace your public key and private key to ABCD...
|
||||
```
|
||||
|
||||
14
.github/ISSUE_TEMPLATE/config.yml
vendored
Normal file
14
.github/ISSUE_TEMPLATE/config.yml
vendored
Normal file
@@ -0,0 +1,14 @@
|
||||
blank_issues_enabled: false
|
||||
contact_links:
|
||||
- name: WGDashboard Community Support (Github)
|
||||
url: https://github.com/orgs/WGDashboard/discussions
|
||||
about: Please ask and answer questions here.
|
||||
|
||||
- name: WGDashboard Community Support (Discord)
|
||||
url: https://discord.gg/72TwzjeuWm
|
||||
about: Discord Server about WGDashboard
|
||||
|
||||
- name: WGDashboard Issue reporting (Bugs and Feature Requests (FR))
|
||||
url: https://github.com/WGDashboard/WGDashboard/issues
|
||||
about: Please report bugs or feature requests here so they can be tracked
|
||||
|
||||
10
.github/ISSUE_TEMPLATE/feature_request.md
vendored
10
.github/ISSUE_TEMPLATE/feature_request.md
vendored
@@ -1,6 +1,6 @@
|
||||
---
|
||||
name: Feature request
|
||||
about: Suggest an idea for this project
|
||||
name: Feature Request (FR)
|
||||
about: Suggest an idea to extend or improve the usage of the project
|
||||
title: ''
|
||||
labels: enhancement
|
||||
assignees: ''
|
||||
@@ -10,5 +10,11 @@ assignees: ''
|
||||
**Is your feature request related to a problem? Please describe.**
|
||||
A clear and concise description of what the problem is. Ex. I'm always frustrated when [...]
|
||||
|
||||
**Related/Adjacent Issues**
|
||||
Paste a Github link to possible issues that could be solved if they exist.
|
||||
|
||||
**What have you done yourself?**
|
||||
What have you already tried to do or considered to fix this issue?
|
||||
|
||||
**Describe the solution you'd like**
|
||||
A clear and concise description of what you want to happen.
|
||||
|
||||
23
.github/dependabot.yml
vendored
23
.github/dependabot.yml
vendored
@@ -7,25 +7,36 @@ version: 2
|
||||
updates:
|
||||
- package-ecosystem: "pip"
|
||||
directory: "/src"
|
||||
target-branch: "development"
|
||||
schedule:
|
||||
interval: "daily"
|
||||
interval: "weekly"
|
||||
|
||||
- package-ecosystem: "npm"
|
||||
directory: "/"
|
||||
directory: "/src/static/app"
|
||||
target-branch: "development"
|
||||
schedule:
|
||||
interval: "daily"
|
||||
interval: "weekly"
|
||||
|
||||
- package-ecosystem: "npm"
|
||||
directory: "/src/static/client"
|
||||
target-branch: "development"
|
||||
schedule:
|
||||
interval: "weekly"
|
||||
|
||||
- package-ecosystem: "github-actions"
|
||||
directory: "/.github"
|
||||
target-branch: "development"
|
||||
schedule:
|
||||
interval: "daily"
|
||||
interval: "weekly"
|
||||
|
||||
- package-ecosystem: "docker"
|
||||
directory: "/docker"
|
||||
target-branch: "development"
|
||||
schedule:
|
||||
interval: "daily"
|
||||
interval: "weekly"
|
||||
|
||||
- package-ecosystem: "docker-compose"
|
||||
directory: "/docker"
|
||||
target-branch: "development"
|
||||
schedule:
|
||||
interval: "daily"
|
||||
interval: "weekly"
|
||||
|
||||
8
.github/workflows/codeql-analyze.yaml
vendored
8
.github/workflows/codeql-analyze.yaml
vendored
@@ -39,11 +39,11 @@ jobs:
|
||||
|
||||
steps:
|
||||
- name: Checkout repository
|
||||
uses: actions/checkout@v3
|
||||
uses: actions/checkout@v6
|
||||
|
||||
# Initializes the CodeQL tools for scanning.
|
||||
- name: Initialize CodeQL
|
||||
uses: github/codeql-action/init@v3
|
||||
uses: github/codeql-action/init@v4
|
||||
with:
|
||||
languages: ${{ matrix.language }}
|
||||
# If you wish to specify custom queries, you can do so here or in a config file.
|
||||
@@ -54,7 +54,7 @@ jobs:
|
||||
# Autobuild attempts to build any compiled languages (C/C++, C#, or Java).
|
||||
# If this step fails, then you should remove it and run the build manually (see below)
|
||||
- name: Autobuild
|
||||
uses: github/codeql-action/autobuild@v3
|
||||
uses: github/codeql-action/autobuild@v4
|
||||
|
||||
# ℹ️ Command-line programs to run using the OS shell.
|
||||
# 📚 https://git.io/JvXDl
|
||||
@@ -68,4 +68,4 @@ jobs:
|
||||
# make release
|
||||
|
||||
- name: Perform CodeQL Analysis
|
||||
uses: github/codeql-action/analyze@v3
|
||||
uses: github/codeql-action/analyze@v4
|
||||
|
||||
17
.github/workflows/docker.yml
vendored
17
.github/workflows/docker.yml
vendored
@@ -5,8 +5,13 @@ on:
|
||||
push:
|
||||
branches:
|
||||
- 'main'
|
||||
- 'development'
|
||||
- '**dev'
|
||||
tags:
|
||||
- '*'
|
||||
paths:
|
||||
- 'src/**'
|
||||
- 'docker/**'
|
||||
release:
|
||||
types: [ published ]
|
||||
|
||||
@@ -44,15 +49,12 @@ jobs:
|
||||
- name: Set up QEMU
|
||||
uses: docker/setup-qemu-action@v3
|
||||
with:
|
||||
platforms: |
|
||||
- linux/amd64
|
||||
- linux/arm64
|
||||
- linux/arm/v7
|
||||
platforms: linux/amd64,linux/arm64,linux/arm/v7
|
||||
|
||||
- name: Set up Docker Buildx
|
||||
uses: docker/setup-buildx-action@v3
|
||||
|
||||
- name: Docker meta by docs https://github.com/docker/metadata-action
|
||||
- name: Extract Docker metadata from environment
|
||||
id: meta
|
||||
uses: docker/metadata-action@v5
|
||||
with:
|
||||
@@ -64,7 +66,7 @@ jobs:
|
||||
type=ref,event=tag
|
||||
type=sha,format=short,prefix=
|
||||
|
||||
- name: Build and export (multi-arch)
|
||||
- name: Build and export Docker image
|
||||
uses: docker/build-push-action@v6
|
||||
with:
|
||||
context: .
|
||||
@@ -85,7 +87,7 @@ jobs:
|
||||
registry: ${{ env.DOCKERHUB_PREFIX }}
|
||||
username: ${{ secrets.DOCKER_HUB_USERNAME }}
|
||||
password: ${{ secrets.DOCKER_HUB_PASSWORD }}
|
||||
|
||||
|
||||
- name: Log in to GitHub Container Registry
|
||||
uses: docker/login-action@v3
|
||||
with:
|
||||
@@ -102,7 +104,6 @@ jobs:
|
||||
only-fixed: true
|
||||
write-comment: true
|
||||
github-token: ${{ secrets.GITHUB_TOKEN }}
|
||||
exit-code: true
|
||||
|
||||
- name: Docker Scout Compare
|
||||
uses: docker/scout-action@v1
|
||||
|
||||
7
.gitignore
vendored
7
.gitignore
vendored
@@ -1,3 +1,4 @@
|
||||
*.tar
|
||||
.vscode
|
||||
.DS_Store
|
||||
.idea
|
||||
@@ -10,7 +11,7 @@ src/static/pic.xd
|
||||
*.conf
|
||||
private_key.txt
|
||||
public_key.txt
|
||||
venv/**
|
||||
*venv*
|
||||
log/**
|
||||
release/*
|
||||
src/db/wgdashboard.db
|
||||
@@ -19,6 +20,8 @@ node_modules/**
|
||||
*/proxy.js
|
||||
src/static/app/proxy.js
|
||||
.secrets
|
||||
*.ini
|
||||
*.pid
|
||||
|
||||
# Logs
|
||||
logs
|
||||
@@ -49,4 +52,4 @@ coverage
|
||||
*.sw?
|
||||
|
||||
*.tsbuildinfo
|
||||
.vite/*
|
||||
.vite/*
|
||||
|
||||
21
README.md
21
README.md
@@ -1,9 +1,8 @@
|
||||
> [!WARNING]
|
||||
> All users running WGDashboard v4.2.x or later and hosted on the public internet are strongly advised to update to the latest release immediately. For more information: [v4.3.2 Release](https://github.com/WGDashboard/WGDashboard/releases/tag/v4.3.2)
|
||||
|
||||
> [!TIP]
|
||||
> 🎉 I'm excited to announce that WGDashboard is officially listed on DigitalOcean's Marketplace! For more information, please visit [Host WGDashboard & WireGuard with DigitalOcean](https://docs.wgdashboard.dev/host-wgdashboard-wireguard-with-digitalocean.html) for more information!
|
||||
|
||||
> [!NOTE]
|
||||
> **Help Wanted 🎉**: Localizing WGDashboard to other languages! If you're willing to help, please visit https://github.com/WGDashboard/WGDashboard/issues/397. Many thanks!
|
||||
|
||||
> 🎉 To help us better understand and improve WGDashboard’s performance, we’re launching the **WGDashboard Testing Program**. As part of this program, participants will receive free WireGuard VPN access to our server in Toronto, Canada, valid for **24 hours** or up to **1GB of total traffic**—whichever comes first. If you’d like to join, visit [https://wg.wgdashboard.dev/](https://wg.wgdashboard.dev/) for more details!
|
||||
|
||||
|
||||

|
||||
@@ -23,9 +22,11 @@
|
||||
|
||||
<p align="center">
|
||||
<a href="https://github.com/WGDashboard/WGDashboard/releases/latest"><img src="https://img.shields.io/github/v/release/donaldzou/wireguard-dashboard?style=for-the-badge"></a>
|
||||
<a href="https://wakatime.com/badge/github/donaldzou/WGDashboard"><img src="https://wakatime.com/badge/github/donaldzou/WGDashboard.svg?style=for-the-badge" alt="wakatime"></a>
|
||||
<a href="https://wakatime.com/badge/github/donaldzou/WGDashboard"><img src="https://wakatime.com/badge/user/45f53c7c-9da9-4cb0-85d6-17bd38cc748b/project/5334ae20-e9a6-4c55-9fea-52d4eb9dfba6.svg?style=for-the-badge" alt="wakatime"></a>
|
||||
<a href="https://hitscounter.dev"><img src="https://hitscounter.dev/api/hit?url=https%3A%2F%2Fgithub.com%2Fdonaldzou%2FWGDashboard&label=Visitor&icon=github&color=%230a58ca&style=for-the-badge"></a>
|
||||
<img src="https://img.shields.io/docker/pulls/donaldzou/wgdashboard?logo=docker&label=Docker%20Image%20Pulls&labelColor=ffffff&style=for-the-badge">
|
||||
<img src="https://github.com/WGDashboard/WGDashboard/actions/workflows/docker.yml/badge.svg?style=for-the-badge">
|
||||
<img src="https://github.com/WGDashboard/WGDashboard/actions/workflows/codeql-analyze.yaml/badge.svg">
|
||||
</p>
|
||||
<p align="center"><b>This project is supported by</b></p>
|
||||
<p align="center">
|
||||
@@ -33,10 +34,10 @@
|
||||
<img src="https://opensource.nyc3.cdn.digitaloceanspaces.com/attribution/assets/SVG/DO_Logo_horizontal_blue.svg" width="201px">
|
||||
</a>
|
||||
</p>
|
||||
<p align="center">Monitoring WireGuard is not convenient, in most case, you'll need to login to your server and type <code>wg show</code>. That's why this project is being created, to view and manage all WireGuard configurations in a easy way.</p>
|
||||
<p align="center">With all these awesome features, while keeping it <b>easy to install and use</b></p>
|
||||
<p align="center">Monitoring WireGuard is not convenient, in most case, you'll need to login to your server and type <code>wg show</code>. That's why this project is being created, to view and manage all WireGuard configurations in an easy way.</p>
|
||||
<p align="center">Though all these awesome features are present, we are still striving to make it <b>easy to install and use</b></p>
|
||||
|
||||
<p align="center"><b><i>This project is not affiliate to the official WireGuard Project</i></b></p>
|
||||
<p align="center"><b><i>This project is not affiliated to the official WireGuard Project</i></b></p>
|
||||
|
||||
<h3 align="center">Looking for help or want to chat about this project?</h4>
|
||||
<p align="center">
|
||||
@@ -52,7 +53,7 @@
|
||||
You can support via <br>
|
||||
</p>
|
||||
<p align="center">
|
||||
<a align="center" href="https://github.com/sponsors/donaldzou" target="_blank"><img src="https://img.shields.io/badge/GitHub%20Sponsor-2e9a40?style=for-the-badge&logo=github"></a>
|
||||
<a align="center" href="https://github.com/sponsors/WGDashboard" target="_blank"><img src="https://img.shields.io/badge/GitHub%20Sponsor-2e9a40?style=for-the-badge&logo=github"></a>
|
||||
<a align="center" href="https://buymeacoffee.com/donaldzou" target="_blank"><img src="https://img.shields.io/badge/Buy%20me%20a%20coffee-ffdd00?style=for-the-badge&logo=buymeacoffee&logoColor=000000"></a>
|
||||
<a align="center" href="https://patreon.com/c/DonaldDonnyZou/membership" target="_blank"><img src="https://img.shields.io/badge/Patreon-000000?style=for-the-badge&logo=patreon&logoColor=ffffff"></a>
|
||||
</p>
|
||||
|
||||
@@ -4,7 +4,5 @@
|
||||
|
||||
| Version | Supported |
|
||||
| ------- | ------------------ |
|
||||
| 5.1.x | :white_check_mark: |
|
||||
| 5.0.x | :x: |
|
||||
| 4.0.x | :white_check_mark: |
|
||||
| < 4.0 | :x: |
|
||||
| 4.3 | :white_check_mark: |
|
||||
| < 4.3 | :x: |
|
||||
|
||||
@@ -3,8 +3,10 @@
|
||||
# Base: Alpine
|
||||
#
|
||||
|
||||
FROM golang:1.25-alpine AS awg-go
|
||||
# Pull the current golang-alpine image.
|
||||
FROM golang:1.26-alpine3.23 AS awg-go
|
||||
|
||||
# Install build-dependencies.
|
||||
RUN apk add --no-cache \
|
||||
git \
|
||||
gcc \
|
||||
@@ -14,18 +16,23 @@ RUN apk add --no-cache \
|
||||
RUN mkdir -p /workspace && \
|
||||
git clone https://github.com/WGDashboard/amneziawg-go /workspace/awg
|
||||
|
||||
# Enable CGO compilation for AmneziaWG
|
||||
ENV CGO_ENABLED=1
|
||||
|
||||
# Change directory
|
||||
WORKDIR /workspace/awg
|
||||
RUN go mod download && \
|
||||
# Compile the binaries
|
||||
RUN go version && \
|
||||
go mod download && \
|
||||
go mod verify && \
|
||||
go build -ldflags '-linkmode external -extldflags "-fno-PIC -static"' -v -o /usr/bin
|
||||
#
|
||||
# AWG TOOLS BUILDING STAGE
|
||||
# Base: Debian
|
||||
# Base: Alpine
|
||||
#
|
||||
FROM alpine:latest AS awg-tools
|
||||
FROM alpine:3.23 AS awg-tools
|
||||
|
||||
# Install needed dependencies.
|
||||
RUN apk add --no-cache \
|
||||
make \
|
||||
git \
|
||||
@@ -33,31 +40,47 @@ RUN apk add --no-cache \
|
||||
linux-headers \
|
||||
ca-certificates
|
||||
|
||||
# Get the workspace ready
|
||||
RUN mkdir -p /workspace && \
|
||||
git clone https://github.com/WGDashboard/amneziawg-tools /workspace/awg-tools
|
||||
|
||||
# Change directory
|
||||
WORKDIR /workspace/awg-tools/src
|
||||
# Compile and change permissions
|
||||
RUN make && chmod +x wg*
|
||||
|
||||
#
|
||||
# PIP DEPENDENCY BUILDING
|
||||
# Base: Alpine
|
||||
#
|
||||
FROM python:3.13-alpine AS pip-builder
|
||||
|
||||
# Use the python-alpine image for building pip dependencies
|
||||
FROM python:3.14-alpine3.23 AS pip-builder
|
||||
|
||||
ARG TARGETPLATFORM
|
||||
|
||||
# Add the build dependencies and create a Python virtual environment.
|
||||
RUN apk add --no-cache \
|
||||
build-base \
|
||||
pkgconfig \
|
||||
python3-dev \
|
||||
postgresql-dev \
|
||||
libffi-dev \
|
||||
libpq \
|
||||
linux-headers \
|
||||
rust \
|
||||
cargo \
|
||||
&& mkdir -p /opt/wgdashboard/src \
|
||||
&& python3 -m venv /opt/wgdashboard/src/venv
|
||||
|
||||
# Copy the requirements file into the build layer.
|
||||
COPY ./src/requirements.txt /opt/wgdashboard/src
|
||||
RUN if [ "$TARGETPLATFORM" = "linux/arm/v7" ]; then \
|
||||
sed -i 's|psycopg\[binary\]|psycopg[c]|' /opt/wgdashboard/src/requirements.txt; \
|
||||
fi; \
|
||||
cat /opt/wgdashboard/src/requirements.txt
|
||||
|
||||
# Install the pip packages
|
||||
RUN . /opt/wgdashboard/src/venv/bin/activate && \
|
||||
pip3 install --upgrade pip && \
|
||||
pip3 install -r /opt/wgdashboard/src/requirements.txt
|
||||
@@ -66,59 +89,57 @@ RUN . /opt/wgdashboard/src/venv/bin/activate && \
|
||||
# WGDashboard RUNNING STAGE
|
||||
# Base: Alpine
|
||||
#
|
||||
FROM python:3.13-alpine AS final
|
||||
|
||||
# Running with the python-alpine image.
|
||||
FROM python:3.14-alpine3.23 AS final
|
||||
LABEL maintainer="dselen@nerthus.nl"
|
||||
|
||||
# Install only the runtime dependencies
|
||||
RUN apk add --no-cache \
|
||||
iproute2 iptables \
|
||||
bash curl \
|
||||
wget unzip \
|
||||
procps sudo \
|
||||
tzdata wireguard-tools \
|
||||
openresolv openrc
|
||||
bash curl procps openrc \
|
||||
tzdata wireguard-tools envsubst
|
||||
SHELL ["/bin/bash", "-o", "pipefail", "-c"]
|
||||
|
||||
# Copy only the final binaries from the builders
|
||||
# Copy only the final binaries from the AWG builder stages
|
||||
COPY --from=awg-go /usr/bin/amneziawg-go /usr/bin/amneziawg-go
|
||||
COPY --from=awg-tools /workspace/awg-tools/src/wg /usr/bin/awg
|
||||
COPY --from=awg-tools /workspace/awg-tools/src/wg-quick/linux.bash /usr/bin/awg-quick
|
||||
|
||||
# Environment variables
|
||||
ARG wg_net="10.0.0.1"
|
||||
ARG wg_subn="24"
|
||||
ARG wg_port="51820"
|
||||
ENV TZ="Europe/Amsterdam" \
|
||||
global_dns="9.9.9.9" \
|
||||
wgd_port="10086" \
|
||||
public_ip="" \
|
||||
WGDASH=/opt/wgdashboard
|
||||
WGDASH=/opt/wgdashboard \
|
||||
dynamic_config="true"
|
||||
|
||||
# Create directories
|
||||
RUN mkdir /data /configs -p ${WGDASH}/src /etc/amnezia/amneziawg
|
||||
# Create directories needed for operation
|
||||
RUN mkdir /data /configs -p ${WGDASH}/src /etc/amnezia/amneziawg \
|
||||
&& echo "name_servers=${global_dns}" >> /etc/resolvconf.conf
|
||||
|
||||
# Copy app source and prebuilt venv only (no pip cache)
|
||||
# Copy the venv and source files from local compiled locations or repos
|
||||
COPY ./src ${WGDASH}/src
|
||||
COPY --from=pip-builder /opt/wgdashboard/src/venv /opt/wgdashboard/src/venv
|
||||
COPY ./docker/wg0.conf.template /tmp/wg0.conf.template
|
||||
COPY ./docker/wg-dashboard-oidc-providers.json.template /tmp/wg-dashboard-oidc-providers.json.template
|
||||
# Copy in the runtime script, essential.
|
||||
COPY ./docker/entrypoint.sh /entrypoint.sh
|
||||
|
||||
# WireGuard interface template
|
||||
SHELL ["/bin/bash", "-o", "pipefail", "-c"]
|
||||
RUN out_adapt=$(ip -o -4 route show to default | awk '{print $NF}') \
|
||||
&& echo -e "[Interface]\n\
|
||||
Address = ${wg_net}/24\n\
|
||||
PrivateKey =\n\
|
||||
PostUp = iptables -t nat -I POSTROUTING 1 -s ${wg_net}/24 -o ${out_adapt} -j MASQUERADE\n\
|
||||
PostUp = iptables -I FORWARD -i wg0 -o wg0 -j DROP\n\
|
||||
PreDown = iptables -t nat -D POSTROUTING -s ${wg_net}/24 -o ${out_adapt} -j MASQUERADE\n\
|
||||
PreDown = iptables -D FORWARD -i wg0 -o wg0 -j DROP\n\
|
||||
ListenPort = ${wg_port}\n\
|
||||
SaveConfig = true\n\
|
||||
DNS = ${global_dns}" > /configs/wg0.conf.template \
|
||||
&& chmod 600 /configs/wg0.conf.template
|
||||
# First WireGuard interface template
|
||||
RUN export out_adapt=$(ip -o -4 route show to default | awk '{print $NF}') \
|
||||
&& envsubst < /tmp/wg0.conf.template > /configs/wg0.conf.template \
|
||||
&& chmod 600 /configs/wg0.conf.template \
|
||||
&& cat /configs/wg0.conf.template
|
||||
|
||||
# Set a healthcheck to determine the container its health
|
||||
HEALTHCHECK --interval=30s --timeout=10s --start-period=5s --retries=3 \
|
||||
CMD sh -c 'pgrep gunicorn > /dev/null && pgrep tail > /dev/null' || exit 1
|
||||
|
||||
COPY ./docker/entrypoint.sh /entrypoint.sh
|
||||
|
||||
# Expose ports on the container
|
||||
EXPOSE 10086
|
||||
WORKDIR $WGDASH/src
|
||||
|
||||
|
||||
@@ -3,8 +3,8 @@ Author: @DaanSelen<br>
|
||||
|
||||
This document delves into how the WGDashboard Docker container has been built.<br>
|
||||
Of course there are two stages (simply said), one before run-time and one at/after run-time.<br>
|
||||
The `Dockerfile` describes how the container image is made, and the `entrypoint.sh` is executed after running the container. <br>
|
||||
In this example, WireGuard is integrated into the container itself, so it should be a run-and-go(/out-of-the-box).<br>
|
||||
The `Dockerfile` describes how the container image is made, and the `entrypoint.sh` is executed after the container is started. <br>
|
||||
In this example, [WireGuard](https://www.wireguard.com/) is integrated into the container itself, so it should be a run-and-go(/out-of-the-box) experience.<br>
|
||||
For more details on the source-code specific to this Docker image, refer to the source files, they have lots of comments.
|
||||
|
||||
<br>
|
||||
@@ -18,20 +18,24 @@ For more details on the source-code specific to this Docker image, refer to the
|
||||
/>
|
||||
<br>
|
||||
|
||||
To get the container running you either pull the image from the repository, (docker.io)`donaldzou/wgdashboard:latest`.<br>
|
||||
From there either use the environment variables describe below as parameters or use the Docker Compose file: `compose.yaml`.<br>
|
||||
Be careful, the default generated WireGuard configuration file uses port 51820/udp. So use this port if you want to use it out of the box.<br>
|
||||
Otherwise edit the configuration file in `/etc/wireguard/wg0.conf`.
|
||||
To get the container running you either pull the pre-made image from a remote repository, there are 2 official options.<br>
|
||||
|
||||
- ghcr.io/wgdashboard/wgdashboard:<tag>
|
||||
- docker.io/donaldzou/wgdashboard:<tag>
|
||||
|
||||
> tags should be either: latest, main, <version>, <branch-name> (if built) or <commit-sha>.
|
||||
|
||||
From there either use the environment variables described below as parameters or use the Docker Compose file: `compose.yaml`.<br>
|
||||
Be careful, the default generated WireGuard configuration file uses port 51820/udp. So make sure to use this port if you want to use it out of the box.<br>
|
||||
Otherwise edit the configuration file in WGDashboard under `Configuration Settings` -> `Edit Raw Configuration File`.
|
||||
|
||||
> Otherwise you need to enter the container and edit: `/etc/wireguard/wg0.conf`.
|
||||
|
||||
# WGDashboard: 🐳 Docker Deployment Guide
|
||||
|
||||
To run the container, you can either pull the image from Docker Hub or build it yourself. The image is available at:
|
||||
To run the container, you can either pull the image from the Github Container Registry (ghcr.io), Docker Hub (docker.io) or build it yourself. The image is available at:
|
||||
|
||||
```
|
||||
docker.io/donaldzou/wgdashboard:latest
|
||||
```
|
||||
|
||||
> `docker.io` is in most cases automatically resolved by the Docker application.
|
||||
> `docker.io` is in most cases automatically resolved by the Docker application. Therefor you can ofter specify: `donaldzou/wgdashboard:latest`
|
||||
|
||||
### 🔧 Quick Docker Run Command
|
||||
|
||||
@@ -44,37 +48,37 @@ docker run -d \
|
||||
-p 10086:10086/tcp \
|
||||
-p 51820:51820/udp \
|
||||
--cap-add NET_ADMIN \
|
||||
donaldzou/wgdashboard:latest
|
||||
ghcr.io/wgdashboard/wgdashboard:latest
|
||||
```
|
||||
|
||||
> ⚠️ The default WireGuard port is `51820/udp`. If you change this, update the `/etc/wireguard/wg0.conf` accordingly.
|
||||
|
||||
---
|
||||
|
||||
### 📦 Docker Compose Alternative
|
||||
### 📦 Docker Compose Alternative (see the [compose file](./compose.yaml))
|
||||
|
||||
You can also use Docker Compose for easier configuration:
|
||||
|
||||
```yaml
|
||||
services:
|
||||
wgdashboard:
|
||||
image: donaldzou/wgdashboard:latest
|
||||
image: ghcr.io/wgdashboard/wgdashboard:latest
|
||||
restart: unless-stopped
|
||||
container_name: wgdashboard
|
||||
environment:
|
||||
# - tz=Europe/Amsterdam
|
||||
# - global_dns=1.1.1.1
|
||||
# - public_ip=YOUR_PUBLIC_IP
|
||||
ports:
|
||||
- 10086:10086/tcp
|
||||
- 51820:51820/udp
|
||||
|
||||
volumes:
|
||||
- aconf:/etc/amnezia/amneziawg
|
||||
- conf:/etc/wireguard
|
||||
- data:/data
|
||||
|
||||
cap_add:
|
||||
- NET_ADMIN
|
||||
|
||||
volumes:
|
||||
aconf:
|
||||
conf:
|
||||
data:
|
||||
```
|
||||
@@ -85,7 +89,7 @@ volumes:
|
||||
|
||||
## 🔄 Updating the Container
|
||||
|
||||
Updating WGDashboard is currently in **alpha** stage. While the update process may work, it's still under testing.
|
||||
Updating the WGDashboard container should be through 'The Docker Way' - by pulling the newest/newer image and replacing this old one.
|
||||
|
||||
---
|
||||
|
||||
@@ -93,6 +97,7 @@ Updating WGDashboard is currently in **alpha** stage. While the update process m
|
||||
|
||||
| Variable | Accepted Values | Default | Example | Description |
|
||||
| ------------------ | ---------------------------------------- | ----------------------- | --------------------- | ----------------------------------------------------------------------- |
|
||||
| `dynamic_config` | true, yes, false, no | `true` | `true` or `no` | Turns on or off the dynamic configuration feature, on by default for Docker |
|
||||
| `tz` | Timezone | `Europe/Amsterdam` | `America/New_York` | Sets the container's timezone. Useful for accurate logs and scheduling. |
|
||||
| `global_dns` | IPv4 and IPv6 addresses | `9.9.9.9` | `8.8.8.8`, `1.1.1.1` | Default DNS for WireGuard clients. |
|
||||
| `public_ip` | Public IP address | Retrieved automatically | `253.162.134.73` | Used to generate accurate client configs. Needed if container is NAT’d. |
|
||||
@@ -108,6 +113,11 @@ Updating WGDashboard is currently in **alpha** stage. While the update process m
|
||||
| `email_password` | Any non-empty string | `-` | `app_password` | Password for SMTP authentication. |
|
||||
| `email_from` | Valid email address | `-` | `noreply@example.com` | Email address used as the sender for notifications. |
|
||||
| `email_template` | Path to template file | `-` | `your-template` | Custom template for email notifications. |
|
||||
| `database_type` | `sqlite`, `postgresql`, `mariadb+mariadbconnector`, etc. | `-` | `postgresql` | Type of [sqlalchemy database engine](https://docs.sqlalchemy.org/en/21/core/engines.html). |
|
||||
| `database_host` | Any non-empty string | `-` | `localhost` | IP-Address or hostname of the SQL-database server. |
|
||||
| `database_port` | Any non-empty string (or int for port) | `-` | `5432` | Port for the database communication. |
|
||||
| `database_username`| Valid database username | `-` | `database_user` | Database user username. |
|
||||
| `database_password`| Valid database password | `-` | `database_password` | Database user password. |
|
||||
|
||||
---
|
||||
|
||||
@@ -205,4 +215,4 @@ ENTRYPOINT ["/bin/bash", "/entrypoint.sh"]
|
||||
|
||||
## Closing remarks:
|
||||
|
||||
For feedback please submit an issue to the repository. Or message dselen@nerthus.nl.
|
||||
For feedback please submit an issue to the repository. Or message dselen@nerthus.nl.
|
||||
|
||||
@@ -1,22 +1,42 @@
|
||||
services:
|
||||
wireguard-dashboard:
|
||||
image: donaldzou/wgdashboard:latest
|
||||
wgdashboard:
|
||||
# Since the github organisation we recommend the ghcr.io.
|
||||
# Alternatively we also still push to docker.io under donaldzou/wgdashboard.
|
||||
# Both share the exact same tags. So they should be interchangable.
|
||||
image: ghcr.io/wgdashboard/wgdashboard:latest
|
||||
|
||||
# Make sure to set the restart policy. Because for a VPN its important to come back IF it crashes.
|
||||
restart: unless-stopped
|
||||
container_name: wgdashboard
|
||||
|
||||
# Environment variables can be used to configure certain values at startup. Without having to configure it from the dashboard.
|
||||
# By default its all disabled, but uncomment the following lines to apply these. (uncommenting is removing the # character)
|
||||
# Refer to the documentation on https://wgdashboard.dev/ for more info on what everything means.
|
||||
#environment:
|
||||
#- wg_autostart=wg0
|
||||
#- tz= # <--- Set container timezone, default: Europe/Amsterdam.
|
||||
#- public_ip= # <--- Set public IP to ensure the correct one is chosen, defaulting to the IP give by ifconfig.me.
|
||||
#- wgd_port= # <--- Set the port WGDashboard will use for its web-server.
|
||||
|
||||
# The following section, ports is very important for exposing more than one Wireguard/AmneziaWireguard interfaces.
|
||||
# Once you create a new configuration and assign a port in the dashboard, don't forget to add it to the ports as well.
|
||||
# Quick-tip: most Wireguard VPN tunnels use UDP. WGDashboard uses HTTP, so tcp.
|
||||
ports:
|
||||
- 10086:10086/tcp
|
||||
- 51820:51820/udp
|
||||
|
||||
# Volumes can be configured however you'd like. The default is using docker volumes.
|
||||
# If you want to use local paths, replace the path before the : with your path.
|
||||
volumes:
|
||||
- aconf:/etc/amnezia/amneziawg
|
||||
- conf:/etc/wireguard
|
||||
- data:/data
|
||||
|
||||
# Needed for network administration.
|
||||
cap_add:
|
||||
- NET_ADMIN
|
||||
|
||||
# The following configuration is linked to the above default volumes.
|
||||
volumes:
|
||||
aconf:
|
||||
conf:
|
||||
|
||||
@@ -1,28 +1,29 @@
|
||||
#!/bin/bash
|
||||
|
||||
config_file="/data/wg-dashboard.ini"
|
||||
runtime_pid=""
|
||||
|
||||
trap 'stop_service' SIGTERM
|
||||
|
||||
# Hash password with bcrypt
|
||||
hash_password() {
|
||||
python3 -c "import bcrypt; print(bcrypt.hashpw('$1'.encode(), bcrypt.gensalt(12)).decode())"
|
||||
${WGDASH}/src/venv/bin/python3 -c "import bcrypt; print(bcrypt.hashpw('$1'.encode(), bcrypt.gensalt(12)).decode())"
|
||||
}
|
||||
|
||||
# Function to set or update section/key/value in the INI file
|
||||
set_ini() {
|
||||
local section="$1" key="$2" value="$3"
|
||||
local current_value
|
||||
|
||||
|
||||
# Add section if it doesn't exist
|
||||
grep -q "^\[${section}\]" "$config_file" \
|
||||
|| printf "\n[%s]\n" "${section}" >> "$config_file"
|
||||
|
||||
|
||||
# Check current value if key exists
|
||||
if grep -q "^[[:space:]]*${key}[[:space:]]*=" "$config_file"; then
|
||||
current_value=$(grep "^[[:space:]]*${key}[[:space:]]*=" "$config_file" | cut -d= -f2- | xargs)
|
||||
|
||||
# Don't display actual value if it's a password field
|
||||
|
||||
# Dont display actual value if it's a password field
|
||||
if [[ "$key" == *"password"* ]]; then
|
||||
if [ "$current_value" = "$value" ]; then
|
||||
echo "- $key is already set correctly (value hidden)"
|
||||
@@ -40,7 +41,7 @@ set_ini() {
|
||||
fi
|
||||
else
|
||||
sed -i "/^\[${section}\]/a ${key} = ${value}" "$config_file"
|
||||
|
||||
|
||||
# Don't display actual value if it's a password field
|
||||
if [[ "$key" == *"password"* ]]; then
|
||||
echo "- Added new setting $key (value hidden)"
|
||||
@@ -52,7 +53,31 @@ set_ini() {
|
||||
|
||||
stop_service() {
|
||||
echo "[WGDashboard] Stopping WGDashboard..."
|
||||
/bin/bash ./wgd.sh stop
|
||||
|
||||
local max_rounds="10"
|
||||
local round="0"
|
||||
local runtime_pid=""
|
||||
|
||||
while true; do
|
||||
round=$((round + 1))
|
||||
|
||||
if [[ -f ${WGDASH}/src/gunicorn.pid ]]; then
|
||||
runtime_pid=$(cat ${WGDASH}/src/gunicorn.pid)
|
||||
|
||||
echo "Running as PID: ${runtime_pid}"
|
||||
return 0
|
||||
fi
|
||||
|
||||
if [[ $round -eq $max_rounds ]]; then
|
||||
echo "Reached breaking point!"
|
||||
return 1
|
||||
|
||||
fi
|
||||
|
||||
sleep 0.5s
|
||||
done
|
||||
|
||||
kill $runtime_pid
|
||||
exit 0
|
||||
}
|
||||
|
||||
@@ -60,59 +85,77 @@ echo "------------------------- START ----------------------------"
|
||||
echo "Starting the WGDashboard Docker container."
|
||||
|
||||
ensure_installation() {
|
||||
echo "Quick-installing..."
|
||||
|
||||
# Make the wgd.sh script executable.
|
||||
chmod +x "${WGDASH}"/src/wgd.sh
|
||||
cd "${WGDASH}"/src || exit
|
||||
|
||||
|
||||
# Github issue: https://github.com/donaldzou/WGDashboard/issues/723
|
||||
echo "Checking for stale pids..."
|
||||
if [[ -f ${WGDASH}/src/gunicorn.pid ]]; then
|
||||
echo "Found stale pid, removing..."
|
||||
rm ${WGDASH}/src/gunicorn.pid
|
||||
fi
|
||||
|
||||
|
||||
# Removing clear shell command from the wgd.sh script to enhance docker logging.
|
||||
echo "Removing clear command from wgd.sh for better Docker logging."
|
||||
sed -i '/clear/d' ./wgd.sh
|
||||
|
||||
|
||||
# PERSISTENCE FOR databases directory
|
||||
# Create required directories and links
|
||||
if [ ! -d "/data/db" ]; then
|
||||
echo "Creating database dir"
|
||||
mkdir -p /data/db
|
||||
fi
|
||||
|
||||
if [ ! -d "${WGDASH}/src/db" ]; then
|
||||
ln -s /data/db "${WGDASH}/src/db"
|
||||
|
||||
if [[ ! -L "${WGDASH}/src/db" ]] && [[ -d "${WGDASH}/src/db" ]]; then
|
||||
echo "Removing ${WGDASH}/src/db since its not a symbolic link."
|
||||
rm -rfv "${WGDASH}/src/db"
|
||||
fi
|
||||
|
||||
if [[ -L "${WGDASH}/src/db" ]]; then
|
||||
echo "${WGDASH}/src/db is a symbolic link."
|
||||
else
|
||||
ln -sv /data/db "${WGDASH}/src/db"
|
||||
fi
|
||||
|
||||
# PERSISTENCE FOR wg-dashboard-oidc-providers.json
|
||||
if [ ! -f "/data/wg-dashboard-oidc-providers.json" ]; then
|
||||
echo "Creating wg-dashboard-oidc-providers.json file"
|
||||
cp -v /tmp/wg-dashboard-oidc-providers.json.template /data/wg-dashboard-oidc-providers.json
|
||||
fi
|
||||
if [[ ! -L "${WGDASH}/src/wg-dashboard-oidc-providers.json" ]] && [[ -f "${WGDASH}/src/wg-dashboard-oidc-providers.json" ]]; then
|
||||
echo "Removing ${WGDASH}/src/wg-dashboard-oidc-providers.json since its not a symbolic link."
|
||||
rm -fv "${WGDASH}/src/wg-dashboard-oidc-providers.json"
|
||||
fi
|
||||
if [[ -L "${WGDASH}/src/wg-dashboard-oidc-providers.json" ]]; then
|
||||
echo "${WGDASH}/src/wg-dashboard-oidc-providers.json is a symbolic link."
|
||||
else
|
||||
ln -sv /data/wg-dashboard-oidc-providers.json "${WGDASH}/src/wg-dashboard-oidc-providers.json"
|
||||
fi
|
||||
|
||||
# PERSISTENCE FOR wg-dashboard.ini
|
||||
if [ ! -f "${config_file}" ]; then
|
||||
echo "Creating wg-dashboard.ini file"
|
||||
touch "${config_file}"
|
||||
fi
|
||||
|
||||
if [ ! -f "${WGDASH}/src/wg-dashboard.ini" ]; then
|
||||
ln -s "${config_file}" "${WGDASH}/src/wg-dashboard.ini"
|
||||
if [[ ! -L "${WGDASH}/src/wg-dashboard.ini" ]] && [[ -f "${WGDASH}/src/wg-dashboard.ini" ]]; then
|
||||
echo "Removing ${WGDASH}/src/wg-dashboard.ini since its not a symbolic link."
|
||||
rm -fv "${WGDASH}/src/wg-dashboard.ini"
|
||||
fi
|
||||
if [[ -L "${WGDASH}/src/wg-dashboard.ini" ]]; then
|
||||
echo "${WGDASH}/src/wg-dashboard.ini is a symbolic link."
|
||||
else
|
||||
ln -sv "${config_file}" "${WGDASH}/src/wg-dashboard.ini"
|
||||
fi
|
||||
|
||||
# Create the Python virtual environment.
|
||||
. "${WGDASH}/src/venv/bin/activate"
|
||||
|
||||
# Use the bash interpreter to install WGDashboard according to the wgd.sh script.
|
||||
/bin/bash ./wgd.sh install
|
||||
|
||||
echo "Looks like the installation succeeded. Moving on."
|
||||
|
||||
# Setup WireGuard if needed
|
||||
if [ ! -f "/etc/wireguard/wg0.conf" ]; then
|
||||
if [ -z "$(ls -A /etc/wireguard)" ]; then
|
||||
cp -a "/configs/wg0.conf.template" "/etc/wireguard/wg0.conf"
|
||||
|
||||
|
||||
echo "Setting a secure private key."
|
||||
local privateKey
|
||||
privateKey=$(wg genkey)
|
||||
sed -i "s|^PrivateKey *=.*$|PrivateKey = ${privateKey}|g" /etc/wireguard/wg0.conf
|
||||
|
||||
|
||||
echo "Done setting template."
|
||||
else
|
||||
echo "Existing wg0 configuration file found, using that."
|
||||
@@ -121,51 +164,80 @@ ensure_installation() {
|
||||
|
||||
set_envvars() {
|
||||
printf "\n------------- SETTING ENVIRONMENT VARIABLES ----------------\n"
|
||||
|
||||
|
||||
# Check if config file is empty
|
||||
if [ ! -s "${config_file}" ]; then
|
||||
echo "Config file is empty. Creating initial structure."
|
||||
elif [[ ${dynamic_config,,} =~ ^(false|no)$ ]]; then
|
||||
echo "Dynamic configuration feature turned off, not changing anything"
|
||||
return
|
||||
fi
|
||||
|
||||
|
||||
echo "Checking basic configuration:"
|
||||
set_ini Peers peer_global_dns "${global_dns}"
|
||||
|
||||
|
||||
if [ -z "${public_ip}" ]; then
|
||||
public_ip=$(curl -s ifconfig.me)
|
||||
echo "Automatically detected public IP: ${public_ip}"
|
||||
public_ip=$(curl -s https://ifconfig.me)
|
||||
if [ -z "${public_ip}" ]; then
|
||||
echo "Using fallback public IP resolution website"
|
||||
public_ip=$(curl -s https://api.ipify.org)
|
||||
fi
|
||||
if [ -z "${public_ip}" ]; then
|
||||
echo "Failed to resolve publicly. Using private address."
|
||||
public_ip=$(hostname -i)
|
||||
fi
|
||||
echo "Automatically detected public IP: ${public_ip}"
|
||||
fi
|
||||
|
||||
|
||||
set_ini Peers remote_endpoint "${public_ip}"
|
||||
set_ini Server app_port "${wgd_port}"
|
||||
|
||||
|
||||
# Account settings - process all parameters
|
||||
[[ -n "$username" ]] && echo "Configuring user account:"
|
||||
# Basic account variables
|
||||
[[ -n "$username" ]] && set_ini Account username "${username}"
|
||||
|
||||
|
||||
if [[ -n "$password" ]]; then
|
||||
echo "- Setting password"
|
||||
set_ini Account password "$(hash_password "${password}")"
|
||||
fi
|
||||
|
||||
|
||||
# Additional account variables
|
||||
[[ -n "$enable_totp" ]] && set_ini Account enable_totp "${enable_totp}"
|
||||
[[ -n "$totp_verified" ]] && set_ini Account totp_verified "${totp_verified}"
|
||||
[[ -n "$totp_key" ]] && set_ini Account totp_key "${totp_key}"
|
||||
|
||||
|
||||
# Welcome session
|
||||
[[ -n "$welcome_session" ]] && set_ini Other welcome_session "${welcome_session}"
|
||||
# If username and password are set but welcome_session isn't, disable it
|
||||
if [[ -n "$username" && -n "$password" && -z "$welcome_session" ]]; then
|
||||
set_ini Other welcome_session "false"
|
||||
fi
|
||||
|
||||
|
||||
# Autostart WireGuard
|
||||
if [[ -n "$wg_autostart" ]]; then
|
||||
echo "Configuring WireGuard autostart:"
|
||||
set_ini WireGuardConfiguration autostart "${wg_autostart}"
|
||||
fi
|
||||
|
||||
|
||||
# Database (check if any settings need to be configured)
|
||||
database_vars=("database_type" "database_host" "database_port" "database_username" "database_password")
|
||||
for var in "${database_vars[@]}"; do
|
||||
if [ -n "${!var}" ]; then
|
||||
echo "Configuring database settings:"
|
||||
break
|
||||
fi
|
||||
done
|
||||
|
||||
# Database (iterate through all possible fields)
|
||||
database_fields=("type:database_type" "host:database_host" "port:database_port"
|
||||
"username:database_username" "password:database_password")
|
||||
|
||||
for field_pair in "${database_fields[@]}"; do
|
||||
IFS=: read -r field var <<< "$field_pair"
|
||||
[[ -n "${!var}" ]] && set_ini Database "$field" "${!var}"
|
||||
done
|
||||
|
||||
# Email (check if any settings need to be configured)
|
||||
email_vars=("email_server" "email_port" "email_encryption" "email_username" "email_password" "email_from" "email_template")
|
||||
for var in "${email_vars[@]}"; do
|
||||
@@ -174,12 +246,12 @@ set_envvars() {
|
||||
break
|
||||
fi
|
||||
done
|
||||
|
||||
|
||||
# Email (iterate through all possible fields)
|
||||
email_fields=("server:email_server" "port:email_port" "encryption:email_encryption"
|
||||
"username:email_username" "email_password:email_password"
|
||||
"send_from:email_from" "email_template:email_template")
|
||||
|
||||
|
||||
for field_pair in "${email_fields[@]}"; do
|
||||
IFS=: read -r field var <<< "$field_pair"
|
||||
[[ -n "${!var}" ]] && set_ini Email "$field" "${!var}"
|
||||
@@ -189,36 +261,74 @@ set_envvars() {
|
||||
# Start service and monitor logs
|
||||
start_and_monitor() {
|
||||
printf "\n---------------------- STARTING CORE -----------------------\n"
|
||||
|
||||
|
||||
# Due to resolvconf resetting the DNS we echo back the one we defined (or fallback to default).
|
||||
resolvconf -u
|
||||
|
||||
# Due to some instances complaining about this, making sure its there every time.
|
||||
mkdir -p /dev/net
|
||||
mknod /dev/net/tun c 10 200
|
||||
chmod 600 /dev/net/tun
|
||||
|
||||
# Actually starting WGDashboard
|
||||
echo "Activating Python venv and executing the WireGuard Dashboard service."
|
||||
bash ./wgd.sh start
|
||||
|
||||
echo "Starting WGDashboard directly with Gunicorn..."
|
||||
|
||||
[[ ! -d ${WGDASH}/src/log ]] && mkdir ${WGDASH}/src/log
|
||||
[[ ! -d ${WGDASH}/src/download ]] && mkdir ${WGDASH}/src/download
|
||||
|
||||
${WGDASH}/src/venv/bin/gunicorn --config ${WGDASH}/src/gunicorn.conf.py
|
||||
|
||||
cp /etc/resolv.conf /etc/resolv.conf.docker
|
||||
/usr/sbin/resolvconf -u
|
||||
cat /etc/resolv.conf.docker | resolvconf -a docker.inet
|
||||
|
||||
if [ $? -ne 0 ]; then
|
||||
echo "Loading WGDashboard failed... Look above for details."
|
||||
fi
|
||||
|
||||
# Wait a second before continuing, to give the python program some time to get ready.
|
||||
sleep 1
|
||||
echo -e "\nEnsuring container continuation."
|
||||
|
||||
# Find and monitor log file
|
||||
local logdir="${WGDASH}/src/log"
|
||||
latestErrLog=$(find "$logdir" -name "error_*.log" -type f -print | sort -r | head -n 1)
|
||||
|
||||
# Only tail the logs if they are found
|
||||
if [ -n "$latestErrLog" ]; then
|
||||
tail -f "$latestErrLog" &
|
||||
# Wait for the tail process to end.
|
||||
wait $!
|
||||
else
|
||||
echo "No log files found to tail. Something went wrong, exiting..."
|
||||
|
||||
local max_rounds="10"
|
||||
local round="0"
|
||||
|
||||
# Hang in there for 10s for Gunicorn to get ready
|
||||
while true; do
|
||||
round=$((round + 1))
|
||||
|
||||
local latest_error=$(ls -t ${WGDASH}/src/log/error_*.log 2> /dev/null | head -n 1)
|
||||
|
||||
if [[ $round -eq $max_rounds ]]; then
|
||||
echo "Reached breaking point!"
|
||||
break
|
||||
|
||||
fi
|
||||
|
||||
if [[ -z $latest_error ]]; then
|
||||
echo -e "Logs not yet present! Retrying in 1 second!"
|
||||
sleep 1s
|
||||
|
||||
else
|
||||
break
|
||||
|
||||
fi
|
||||
|
||||
done
|
||||
|
||||
if [[ -z $latest_error ]]; then
|
||||
echo -e "No error logs founds... Please investigate.\nExiting in 3 minutes..."
|
||||
sleep 180s
|
||||
exit 1
|
||||
|
||||
else
|
||||
tail -f "$latest_error" &
|
||||
tail_pid=$!
|
||||
|
||||
wait $tail_pid
|
||||
fi
|
||||
}
|
||||
|
||||
# Main execution flow
|
||||
ensure_installation
|
||||
set_envvars
|
||||
start_and_monitor
|
||||
start_and_monitor
|
||||
|
||||
16
docker/wg-dashboard-oidc-providers.json.template
Normal file
16
docker/wg-dashboard-oidc-providers.json.template
Normal file
@@ -0,0 +1,16 @@
|
||||
{
|
||||
"Admin": {
|
||||
"Provider": {
|
||||
"client_id": "",
|
||||
"client_secret": "",
|
||||
"issuer": ""
|
||||
}
|
||||
},
|
||||
"Client": {
|
||||
"Provider": {
|
||||
"client_id": "",
|
||||
"client_secret": "",
|
||||
"issuer": ""
|
||||
}
|
||||
}
|
||||
}
|
||||
8
docker/wg0.conf.template
Normal file
8
docker/wg0.conf.template
Normal file
@@ -0,0 +1,8 @@
|
||||
[Interface]
|
||||
Address = ${wg_net}/24
|
||||
PrivateKey =
|
||||
PostUp = iptables -t nat -I POSTROUTING 1 -s ${wg_net}/24 -o ${out_adapt} -j MASQUERADE; iptables -I FORWARD -i wg0 -o wg0 -j DROP
|
||||
PreDown = iptables -t nat -D POSTROUTING -s ${wg_net}/24 -o ${out_adapt} -j MASQUERADE; iptables -D FORWARD -i wg0 -o wg0 -j DROP
|
||||
ListenPort = ${wg_port}
|
||||
SaveConfig = true
|
||||
DNS = ${global_dns}
|
||||
@@ -1,6 +0,0 @@
|
||||
version: "1.0"
|
||||
linter: jetbrains/qodana-python:2024.3
|
||||
profile:
|
||||
name: qodana.recommended
|
||||
include:
|
||||
- name: CheckDependencyLicenses
|
||||
@@ -4,8 +4,9 @@ from tzlocal import get_localzone
|
||||
|
||||
from functools import wraps
|
||||
|
||||
from flask import Blueprint, render_template, abort, request, Flask, current_app, session, redirect, url_for
|
||||
from flask import Blueprint, render_template, abort, request, Flask, current_app, session, redirect, url_for, send_from_directory
|
||||
import os
|
||||
import mimetypes
|
||||
|
||||
from modules.WireguardConfiguration import WireguardConfiguration
|
||||
from modules.DashboardConfig import DashboardConfig
|
||||
@@ -53,6 +54,8 @@ def createClientBlueprint(wireguardConfigurations: dict[WireguardConfiguration],
|
||||
|
||||
@client.post(f'{prefix}/api/signup')
|
||||
def ClientAPI_SignUp():
|
||||
if not dashboardConfig.GetConfig("Clients", "sign_up")[1]:
|
||||
abort(404)
|
||||
data = request.get_json()
|
||||
status, msg = dashboardClients.SignUp(**data)
|
||||
return ResponseObject(status, msg)
|
||||
@@ -192,14 +195,26 @@ def createClientBlueprint(wireguardConfigurations: dict[WireguardConfiguration],
|
||||
})
|
||||
return ResponseObject(status, msg)
|
||||
|
||||
@client.get(f'{prefix}/assets/<path:filename>')
|
||||
@client.get(f'{prefix}/img/<path:filename>')
|
||||
def serve_client_static(filename):
|
||||
client_dist_folder = os.path.abspath("./static/dist/WGDashboardClient")
|
||||
mimetype = mimetypes.guess_type(filename)[0]
|
||||
subfolder = 'assets' if 'assets' in request.path else 'img'
|
||||
return send_from_directory(os.path.join(client_dist_folder, subfolder), os.path.basename(filename), mimetype=mimetype)
|
||||
|
||||
@client.get(prefix)
|
||||
def ClientIndex():
|
||||
return render_template('client.html')
|
||||
app_prefix = dashboardConfig.GetConfig("Server", "app_prefix")[1]
|
||||
return render_template('client.html', APP_PREFIX=app_prefix)
|
||||
|
||||
@client.get(f'{prefix}/api/serverInformation')
|
||||
def ClientAPI_ServerInformation():
|
||||
return ResponseObject(data={
|
||||
"ServerTimezone": str(get_localzone())
|
||||
"ServerTimezone": str(get_localzone()),
|
||||
"SignUp": {
|
||||
"enable": dashboardConfig.GetConfig("Clients", "sign_up")[1]
|
||||
}
|
||||
})
|
||||
|
||||
@client.get(f'{prefix}/api/validateAuthentication')
|
||||
|
||||
217
src/dashboard.py
217
src/dashboard.py
@@ -8,7 +8,7 @@ from datetime import datetime, timedelta
|
||||
|
||||
import sqlalchemy
|
||||
from jinja2 import Template
|
||||
from flask import Flask, request, render_template, session, send_file
|
||||
from flask import Flask, request, render_template, session, send_file, current_app
|
||||
from flask_cors import CORS
|
||||
from icmplib import ping, traceroute
|
||||
from flask.json.provider import DefaultJSONProvider
|
||||
@@ -17,8 +17,7 @@ from itertools import islice
|
||||
from sqlalchemy import RowMapping
|
||||
|
||||
from modules.Utilities import (
|
||||
RegexMatch, StringToBoolean,
|
||||
ValidateIPAddressesWithRange, ValidateDNSAddress,
|
||||
RegexMatch, StringToBoolean, ValidateDNSAddress,
|
||||
GenerateWireguardPublicKey, GenerateWireguardPrivateKey
|
||||
)
|
||||
from packaging import version
|
||||
@@ -30,7 +29,7 @@ from modules.PeerShareLinks import PeerShareLinks
|
||||
from modules.PeerJobs import PeerJobs
|
||||
from modules.DashboardConfig import DashboardConfig
|
||||
from modules.WireguardConfiguration import WireguardConfiguration
|
||||
from modules.AmneziaWireguardConfiguration import AmneziaWireguardConfiguration
|
||||
from modules.AmneziaConfiguration import AmneziaConfiguration
|
||||
|
||||
from client import createClientBlueprint
|
||||
|
||||
@@ -72,7 +71,11 @@ def ResponseObject(status=True, message=None, data=None, status_code = 200) -> F
|
||||
'''
|
||||
Flask App
|
||||
'''
|
||||
app = Flask("WGDashboard", template_folder=os.path.abspath("./static/dist/WGDashboardAdmin"))
|
||||
_, APP_PREFIX_INIT = DashboardConfig().GetConfig("Server", "app_prefix")
|
||||
app = Flask("WGDashboard",
|
||||
template_folder=os.path.abspath("./static/dist/WGDashboardAdmin"),
|
||||
static_folder=os.path.abspath("./static/dist/WGDashboardAdmin"),
|
||||
static_url_path=APP_PREFIX_INIT if APP_PREFIX_INIT else '')
|
||||
|
||||
def peerInformationBackgroundThread():
|
||||
global WireguardConfigurations
|
||||
@@ -92,9 +95,13 @@ def peerInformationBackgroundThread():
|
||||
c.getPeersTransfer()
|
||||
c.getPeersEndpoint()
|
||||
c.getPeers()
|
||||
if delay == 6:
|
||||
c.logPeersTraffic()
|
||||
c.logPeersHistoryEndpoint()
|
||||
if DashboardConfig.GetConfig('WireGuardConfiguration', 'peer_tracking')[1] is True:
|
||||
print("[WGDashboard] Tracking Peers")
|
||||
if delay == 6:
|
||||
if c.configurationInfo.PeerTrafficTracking:
|
||||
c.logPeersTraffic()
|
||||
if c.configurationInfo.PeerHistoricalEndpointTracking:
|
||||
c.logPeersHistoryEndpoint()
|
||||
c.getRestrictedPeersList()
|
||||
except Exception as e:
|
||||
app.logger.error(f"[WGDashboard] Background Thread #1 Error", e)
|
||||
@@ -159,10 +166,10 @@ def InitWireguardConfigurationsList(startup: bool = False):
|
||||
if i in WireguardConfigurations.keys():
|
||||
if WireguardConfigurations[i].configurationFileChanged():
|
||||
with app.app_context():
|
||||
WireguardConfigurations[i] = AmneziaWireguardConfiguration(DashboardConfig, AllPeerJobs, AllPeerShareLinks, DashboardWebHooks, i)
|
||||
WireguardConfigurations[i] = AmneziaConfiguration(DashboardConfig, AllPeerJobs, AllPeerShareLinks, DashboardWebHooks, i)
|
||||
else:
|
||||
with app.app_context():
|
||||
WireguardConfigurations[i] = AmneziaWireguardConfiguration(DashboardConfig, AllPeerJobs, AllPeerShareLinks, DashboardWebHooks, i, startup=startup)
|
||||
WireguardConfigurations[i] = AmneziaConfiguration(DashboardConfig, AllPeerJobs, AllPeerShareLinks, DashboardWebHooks, i, startup=startup)
|
||||
except WireguardConfiguration.InvalidConfigurationFileException as e:
|
||||
app.logger.error(f"{i} have an invalid configuration file.")
|
||||
|
||||
@@ -194,7 +201,7 @@ with app.app_context():
|
||||
DashboardConfig = DashboardConfig()
|
||||
EmailSender = EmailSender(DashboardConfig)
|
||||
AllPeerShareLinks: PeerShareLinks = PeerShareLinks(DashboardConfig, WireguardConfigurations)
|
||||
AllPeerJobs: PeerJobs = PeerJobs(DashboardConfig, WireguardConfigurations)
|
||||
AllPeerJobs: PeerJobs = PeerJobs(DashboardConfig, WireguardConfigurations, AllPeerShareLinks)
|
||||
DashboardLogger: DashboardLogger = DashboardLogger()
|
||||
DashboardPlugins: DashboardPlugins = DashboardPlugins(app, WireguardConfigurations)
|
||||
DashboardWebHooks: DashboardWebHooks = DashboardWebHooks(DashboardConfig)
|
||||
@@ -222,14 +229,7 @@ def auth_req():
|
||||
if request.method.lower() == 'options':
|
||||
return ResponseObject(True)
|
||||
|
||||
DashboardConfig.APIAccessed = False
|
||||
if "api" in request.path:
|
||||
if str(request.method) == "GET":
|
||||
DashboardLogger.log(str(request.url), str(request.remote_addr), Message=str(request.args))
|
||||
elif str(request.method) == "POST":
|
||||
DashboardLogger.log(str(request.url), str(request.remote_addr), Message=f"Request Args: {str(request.args)} Body:{str(request.get_json())}")
|
||||
|
||||
|
||||
DashboardConfig.APIAccessed = False
|
||||
authenticationRequired = DashboardConfig.GetConfig("Server", "auth_req")[1]
|
||||
d = request.headers
|
||||
if authenticationRequired:
|
||||
@@ -251,17 +251,28 @@ def auth_req():
|
||||
DashboardConfig.APIAccessed = True
|
||||
else:
|
||||
DashboardConfig.APIAccessed = False
|
||||
appPrefix = APP_PREFIX if len(APP_PREFIX) > 0 else ''
|
||||
whiteList = [
|
||||
'/static/', 'validateAuthentication', 'authenticate', 'getDashboardConfiguration',
|
||||
'getDashboardTheme', 'getDashboardVersion', 'sharePeer/get', 'isTotpEnabled', 'locale',
|
||||
'/fileDownload',
|
||||
'/client'
|
||||
# f'/static/',
|
||||
f'{appPrefix}/api/validateAuthentication',
|
||||
f'{appPrefix}/api/authenticate',
|
||||
# f'{appPrefix}/api/getDashboardConfiguration',
|
||||
f'{appPrefix}/api/getDashboardTheme',
|
||||
f'{appPrefix}/api/getDashboardVersion',
|
||||
f'{appPrefix}/api/sharePeer/get',
|
||||
f'{appPrefix}/api/isTotpEnabled',
|
||||
f'{appPrefix}/api/locale',
|
||||
]
|
||||
|
||||
if (("username" not in session or session.get("role") != "admin")
|
||||
and (f"{(APP_PREFIX if len(APP_PREFIX) > 0 else '')}/" != request.path
|
||||
and f"{(APP_PREFIX if len(APP_PREFIX) > 0 else '')}" != request.path)
|
||||
and len(list(filter(lambda x : x not in request.path, whiteList))) == len(whiteList)
|
||||
|
||||
|
||||
if (
|
||||
("username" not in session or session.get("role") != "admin")
|
||||
and (f"{appPrefix}/" != request.path and f"{appPrefix}" != request.path)
|
||||
and not request.path.startswith(f'{appPrefix}/client')
|
||||
and not request.path.startswith(f'{appPrefix}/img')
|
||||
and not request.path.startswith(f'{appPrefix}/json')
|
||||
and not request.path.startswith(f'{appPrefix}/assets')
|
||||
and request.path not in whiteList
|
||||
):
|
||||
response = Flask.make_response(app, {
|
||||
"status": False,
|
||||
@@ -418,11 +429,11 @@ def API_addWireguardConfiguration():
|
||||
)
|
||||
WireguardConfigurations[data['ConfigurationName']] = (
|
||||
WireguardConfiguration(DashboardConfig, AllPeerJobs, AllPeerShareLinks, data=data, name=data['ConfigurationName'])) if protocol == 'wg' else (
|
||||
AmneziaWireguardConfiguration(DashboardConfig, AllPeerJobs, AllPeerShareLinks, DashboardWebHooks, data=data, name=data['ConfigurationName']))
|
||||
AmneziaConfiguration(DashboardConfig, AllPeerJobs, AllPeerShareLinks, DashboardWebHooks, data=data, name=data['ConfigurationName']))
|
||||
else:
|
||||
WireguardConfigurations[data['ConfigurationName']] = (
|
||||
WireguardConfiguration(DashboardConfig, AllPeerJobs, AllPeerShareLinks, DashboardWebHooks, data=data)) if data.get('Protocol') == 'wg' else (
|
||||
AmneziaWireguardConfiguration(DashboardConfig, AllPeerJobs, AllPeerShareLinks, DashboardWebHooks, data=data))
|
||||
AmneziaConfiguration(DashboardConfig, AllPeerJobs, AllPeerShareLinks, DashboardWebHooks, data=data))
|
||||
return ResponseObject()
|
||||
|
||||
@app.get(f'{APP_PREFIX}/api/toggleWireguardConfiguration')
|
||||
@@ -519,7 +530,7 @@ def API_renameWireguardConfiguration():
|
||||
|
||||
status, message = rc.renameConfiguration(data.get("NewConfigurationName"))
|
||||
if status:
|
||||
WireguardConfigurations[data.get("NewConfigurationName")] = (WireguardConfiguration(DashboardConfig, AllPeerJobs, AllPeerShareLinks, DashboardWebHooks, data.get("NewConfigurationName")) if rc.Protocol == 'wg' else AmneziaWireguardConfiguration(DashboardConfig, AllPeerJobs, AllPeerShareLinks, DashboardWebHooks, data.get("NewConfigurationName")))
|
||||
WireguardConfigurations[data.get("NewConfigurationName")] = (WireguardConfiguration(DashboardConfig, AllPeerJobs, AllPeerShareLinks, DashboardWebHooks, data.get("NewConfigurationName")) if rc.Protocol == 'wg' else AmneziaConfiguration(DashboardConfig, AllPeerJobs, AllPeerShareLinks, DashboardWebHooks, data.get("NewConfigurationName")))
|
||||
else:
|
||||
WireguardConfigurations[data.get("ConfigurationName")] = rc
|
||||
return ResponseObject(status, message)
|
||||
@@ -558,8 +569,8 @@ def API_getAllWireguardConfigurationBackup():
|
||||
files.sort(key=lambda x: x[1], reverse=True)
|
||||
|
||||
for f, ct in files:
|
||||
if RegexMatch(r"^(.*)_(.*)\.(conf)$", f):
|
||||
s = re.search(r"^(.*)_(.*)\.(conf)$", f)
|
||||
if RegexMatch(r"^(.+)_(\d+)\.(conf)$", f):
|
||||
s = re.search(r"^(.+)_(\d+)\.(conf)$", f)
|
||||
name = s.group(1)
|
||||
if name not in existingConfiguration:
|
||||
if name not in data['NonExistingConfigurations'].keys():
|
||||
@@ -606,12 +617,19 @@ def API_deleteWireguardConfigurationBackup():
|
||||
|
||||
@app.get(f'{APP_PREFIX}/api/downloadWireguardConfigurationBackup')
|
||||
def API_downloadWireguardConfigurationBackup():
|
||||
configurationName = request.args.get('configurationName')
|
||||
backupFileName = request.args.get('backupFileName')
|
||||
configurationName = os.path.basename(request.args.get('configurationName'))
|
||||
backupFileName = os.path.basename(request.args.get('backupFileName'))
|
||||
|
||||
if configurationName is None or configurationName not in WireguardConfigurations.keys():
|
||||
return ResponseObject(False, "Configuration does not exist", status_code=404)
|
||||
|
||||
status, zip = WireguardConfigurations[configurationName].downloadBackup(backupFileName)
|
||||
return ResponseObject(status, data=zip, status_code=(200 if status else 404))
|
||||
|
||||
if not status:
|
||||
current_app.logger.error(f"Failed to download a requested backup.\nConfiguration Name: {configurationName}\nBackup File Name: {backupFileName}")
|
||||
return ResponseObject(False, "Internal server error", status_code=500)
|
||||
|
||||
return send_file(os.path.join('download', zip), as_attachment=True)
|
||||
|
||||
@app.post(f'{APP_PREFIX}/api/restoreWireguardConfigurationBackup')
|
||||
def API_restoreWireguardConfigurationBackup():
|
||||
@@ -695,15 +713,30 @@ def API_updatePeerSettings(configName):
|
||||
preshared_key = data['preshared_key']
|
||||
mtu = data['mtu']
|
||||
keepalive = data['keepalive']
|
||||
notes = data.get('notes', '')
|
||||
wireguardConfig = WireguardConfigurations[configName]
|
||||
foundPeer, peer = wireguardConfig.searchPeer(id)
|
||||
if foundPeer:
|
||||
if wireguardConfig.Protocol == 'wg':
|
||||
status, msg = peer.updatePeer(name, private_key, preshared_key, dns_addresses,
|
||||
allowed_ip, endpoint_allowed_ip, mtu, keepalive)
|
||||
status, msg = peer.updatePeer(name,
|
||||
private_key,
|
||||
preshared_key,
|
||||
dns_addresses,
|
||||
allowed_ip,
|
||||
endpoint_allowed_ip,
|
||||
mtu,
|
||||
keepalive,
|
||||
notes)
|
||||
else:
|
||||
status, msg = peer.updatePeer(name, private_key, preshared_key, dns_addresses,
|
||||
allowed_ip, endpoint_allowed_ip, mtu, keepalive, "off")
|
||||
status, msg = peer.updatePeer(name,
|
||||
private_key,
|
||||
preshared_key,
|
||||
dns_addresses,
|
||||
allowed_ip,
|
||||
endpoint_allowed_ip,
|
||||
mtu,
|
||||
keepalive,
|
||||
notes)
|
||||
wireguardConfig.getPeers()
|
||||
DashboardWebHooks.RunWebHook('peer_updated', {
|
||||
"configuration": wireguardConfig.Name,
|
||||
@@ -853,6 +886,7 @@ def API_addPeers(configName):
|
||||
|
||||
mtu: int = data.get('mtu', None)
|
||||
keep_alive: int = data.get('keepalive', None)
|
||||
notes: str = data.get('notes', '')
|
||||
preshared_key: str = data.get('preshared_key', "")
|
||||
|
||||
if type(mtu) is not int or mtu < 0 or mtu > 1460:
|
||||
@@ -908,7 +942,7 @@ def API_addPeers(configName):
|
||||
"endpoint_allowed_ip": endpoint_allowed_ip,
|
||||
"mtu": mtu,
|
||||
"keepalive": keep_alive,
|
||||
"advanced_security": "off"
|
||||
"notes": ""
|
||||
})
|
||||
if addedCount == bulkAddAmount:
|
||||
break
|
||||
@@ -951,8 +985,11 @@ def API_addPeers(configName):
|
||||
for i in allowed_ips:
|
||||
found = False
|
||||
for subnet in availableIps.keys():
|
||||
network = ipaddress.ip_network(subnet, False)
|
||||
ap = ipaddress.ip_network(i)
|
||||
try:
|
||||
network = ipaddress.ip_network(subnet, False)
|
||||
ap = ipaddress.ip_network(i)
|
||||
except ValueError as e:
|
||||
return ResponseObject(False, str(e))
|
||||
if network.version == ap.version and ap.subnet_of(network):
|
||||
found = True
|
||||
|
||||
@@ -970,14 +1007,13 @@ def API_addPeers(configName):
|
||||
"DNS": dns_addresses,
|
||||
"mtu": mtu,
|
||||
"keepalive": keep_alive,
|
||||
"advanced_security": "off"
|
||||
"notes": notes
|
||||
}]
|
||||
)
|
||||
return ResponseObject(status=status, message=message, data=addedPeers)
|
||||
except Exception as e:
|
||||
app.logger.error("Add peers failed", e)
|
||||
return ResponseObject(False,
|
||||
f"Add peers failed. Reason: {message}")
|
||||
return ResponseObject(False, f"Add peers failed.")
|
||||
|
||||
return ResponseObject(False, "Configuration does not exist")
|
||||
|
||||
@@ -1090,12 +1126,10 @@ def API_GetPeerTraffics():
|
||||
interval = request.args.get('interval', 30)
|
||||
startDate = request.args.get('startDate', None)
|
||||
endDate = request.args.get('endDate', None)
|
||||
|
||||
if type(interval) is str:
|
||||
if not interval.isdigit():
|
||||
return ResponseObject(False, "Interval must be integers in minutes")
|
||||
interval = int(interval)
|
||||
|
||||
if startDate is None:
|
||||
endDate = None
|
||||
else:
|
||||
@@ -1113,6 +1147,55 @@ def API_GetPeerTraffics():
|
||||
return ResponseObject(data=p.getTraffics(interval, startDate, endDate))
|
||||
return ResponseObject(False, "Peer does not exist")
|
||||
|
||||
@app.get(f'{APP_PREFIX}/api/getPeerTrackingTableCounts')
|
||||
def API_GetPeerTrackingTableCounts():
|
||||
configurationName = request.args.get("configurationName")
|
||||
if configurationName and configurationName not in WireguardConfigurations.keys():
|
||||
return ResponseObject(False, "Configuration does not exist")
|
||||
|
||||
if configurationName:
|
||||
c = WireguardConfigurations.get(configurationName)
|
||||
return ResponseObject(data={
|
||||
"TrafficTrackingTableSize": c.getTransferTableSize(),
|
||||
"HistoricalTrackingTableSize": c.getHistoricalEndpointTableSize()
|
||||
})
|
||||
|
||||
d = {}
|
||||
for i in WireguardConfigurations.keys():
|
||||
c = WireguardConfigurations.get(i)
|
||||
d[i] = {
|
||||
"TrafficTrackingTableSize": c.getTransferTableSize(),
|
||||
"HistoricalTrackingTableSize": c.getHistoricalEndpointTableSize()
|
||||
}
|
||||
return ResponseObject(data=d)
|
||||
|
||||
@app.get(f'{APP_PREFIX}/api/downloadPeerTrackingTable')
|
||||
def API_DownloadPeerTackingTable():
|
||||
configurationName = request.args.get("configurationName")
|
||||
table = request.args.get('table')
|
||||
if configurationName not in WireguardConfigurations.keys():
|
||||
return ResponseObject(False, "Configuration does not exist")
|
||||
if table not in ['TrafficTrackingTable', 'HistoricalTrackingTable']:
|
||||
return ResponseObject(False, "Table does not exist")
|
||||
c = WireguardConfigurations.get(configurationName)
|
||||
return ResponseObject(
|
||||
data=c.downloadTransferTable() if table == 'TrafficTrackingTable'
|
||||
else c.downloadHistoricalEndpointTable())
|
||||
|
||||
@app.post(f'{APP_PREFIX}/api/deletePeerTrackingTable')
|
||||
def API_DeletePeerTrackingTable():
|
||||
data = request.get_json()
|
||||
configurationName = data.get('configurationName')
|
||||
table = data.get('table')
|
||||
if not configurationName or configurationName not in WireguardConfigurations.keys():
|
||||
return ResponseObject(False, "Configuration does not exist")
|
||||
if not table or table not in ['TrafficTrackingTable', 'HistoricalTrackingTable']:
|
||||
return ResponseObject(False, "Table does not exist")
|
||||
c = WireguardConfigurations.get(configurationName)
|
||||
return ResponseObject(
|
||||
status=c.deleteTransferTable() if table == 'TrafficTrackingTable'
|
||||
else c.deleteHistoryEndpointTable())
|
||||
|
||||
@app.get(f'{APP_PREFIX}/api/getDashboardTheme')
|
||||
def API_getDashboardTheme():
|
||||
return ResponseObject(data=DashboardConfig.GetConfig("Server", "dashboard_theme")[1])
|
||||
@@ -1176,20 +1259,6 @@ def API_getPeerScheduleJobLogs(configName):
|
||||
requestAll = True
|
||||
return ResponseObject(data=AllPeerJobs.getPeerJobLogs(configName))
|
||||
|
||||
'''
|
||||
File Download
|
||||
'''
|
||||
@app.get(f'{APP_PREFIX}/fileDownload')
|
||||
def API_download():
|
||||
file = request.args.get('file')
|
||||
if file is None or len(file) == 0:
|
||||
return ResponseObject(False, "Please specify a file")
|
||||
if os.path.exists(os.path.join('download', file)):
|
||||
return send_file(os.path.join('download', file), as_attachment=True)
|
||||
else:
|
||||
return ResponseObject(False, "File does not exist")
|
||||
|
||||
|
||||
'''
|
||||
Tools
|
||||
'''
|
||||
@@ -1207,8 +1276,9 @@ def API_ping_getAllPeersIpAddress():
|
||||
ip = ipaddress.ip_network(x, strict=False)
|
||||
except ValueError as e:
|
||||
app.logger.error(f"Failed to parse IP address of {p.id} - {c.Name}")
|
||||
if len(list(ip.hosts())) == 1:
|
||||
parsed.append(str(ip.hosts()[0]))
|
||||
host = list(ip.hosts())
|
||||
if len(host) == 1:
|
||||
parsed.append(str(host[0]))
|
||||
endpoint = p.endpoint.replace(" ", "").replace("(none)", "")
|
||||
if len(p.name) > 0:
|
||||
cips[f"{p.name} - {p.id}"] = {
|
||||
@@ -1291,12 +1361,17 @@ def API_traceroute_execute():
|
||||
data=json.dumps([x['ip'] for x in result]))
|
||||
d = r.json()
|
||||
for i in range(len(result)):
|
||||
result[i]['geo'] = d[i]
|
||||
result[i]['geo'] = d[i]
|
||||
|
||||
return ResponseObject(data=result)
|
||||
|
||||
except Exception as e:
|
||||
app.logger.error(f"Failed to gather the geolocation data: {e}")
|
||||
return ResponseObject(data=result, message="Failed to request IP address geolocation")
|
||||
return ResponseObject(data=result)
|
||||
except Exception as exp:
|
||||
return ResponseObject(False, exp)
|
||||
|
||||
except Exception as e:
|
||||
app.logger.error(f"Failed to execute the traceroute: {e}")
|
||||
return ResponseObject(data=[], message="Failed to traceroute the given parameter")
|
||||
else:
|
||||
return ResponseObject(False, "Please provide ipAddress")
|
||||
|
||||
@@ -1412,7 +1487,7 @@ def API_Locale_Update():
|
||||
|
||||
@app.get(f'{APP_PREFIX}/api/email/ready')
|
||||
def API_Email_Ready():
|
||||
return ResponseObject(EmailSender.ready())
|
||||
return ResponseObject(EmailSender.is_ready())
|
||||
|
||||
@app.post(f'{APP_PREFIX}/api/email/send')
|
||||
def API_Email_Send():
|
||||
@@ -1666,9 +1741,9 @@ Index Page
|
||||
|
||||
@app.get(f'{APP_PREFIX}/')
|
||||
def index():
|
||||
return render_template('index.html')
|
||||
return render_template('index.html', APP_PREFIX=APP_PREFIX)
|
||||
|
||||
if __name__ == "__main__":
|
||||
startThreads()
|
||||
DashboardPlugins.startThreads()
|
||||
app.run(host=app_ip, debug=False, port=app_port)
|
||||
app.run(host=app_ip, debug=False, port=app_port)
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
import dashboard
|
||||
import os
|
||||
from datetime import datetime
|
||||
global sqldb, cursor, DashboardConfig, WireguardConfigurations, AllPeerJobs, JobLogger, Dash
|
||||
app_host, app_port = dashboard.gunicornConfig()
|
||||
@@ -16,11 +17,11 @@ daemon = True
|
||||
pidfile = './gunicorn.pid'
|
||||
wsgi_app = "dashboard:app"
|
||||
accesslog = f"./log/access_{date}.log"
|
||||
loglevel = "info"
|
||||
loglevel = os.environ['log_level'] if 'log_level' in os.environ else 'info'
|
||||
capture_output = True
|
||||
errorlog = f"./log/error_{date}.log"
|
||||
pythonpath = "., ./modules"
|
||||
|
||||
print(f"[Gunicorn] WGDashboard w/ Gunicorn will be running on {bind}", flush=True)
|
||||
print(f"[Gunicorn] Access log file is at {accesslog}", flush=True)
|
||||
print(f"[Gunicorn] Error log file is at {errorlog}", flush=True)
|
||||
print(f"[Gunicorn] Error log file is at {errorlog}", flush=True)
|
||||
|
||||
@@ -4,28 +4,39 @@ AmneziaWG Configuration
|
||||
import random, sqlalchemy, os, subprocess, re, uuid
|
||||
from flask import current_app
|
||||
from .PeerJobs import PeerJobs
|
||||
from .AmneziaWGPeer import AmneziaWGPeer
|
||||
from .AmneziaPeer import AmneziaPeer
|
||||
from .PeerShareLinks import PeerShareLinks
|
||||
from .Utilities import RegexMatch
|
||||
from .Utilities import RegexMatch, CheckAddress, CheckPeerKey
|
||||
from .WireguardConfiguration import WireguardConfiguration
|
||||
from .DashboardWebHooks import DashboardWebHooks
|
||||
|
||||
|
||||
class AmneziaWireguardConfiguration(WireguardConfiguration):
|
||||
def __init__(self, DashboardConfig,
|
||||
class AmneziaConfiguration(WireguardConfiguration):
|
||||
def __init__(self,
|
||||
DashboardConfig,
|
||||
AllPeerJobs: PeerJobs,
|
||||
AllPeerShareLinks: PeerShareLinks,
|
||||
DashboardWebHooks: DashboardWebHooks,
|
||||
name: str = None, data: dict = None, backup: dict = None, startup: bool = False):
|
||||
name: str = None,
|
||||
data: dict = None,
|
||||
backup: dict = None,
|
||||
startup: bool = False):
|
||||
self.Jc = 0
|
||||
self.Jmin = 0
|
||||
self.Jmax = 0
|
||||
self.S1 = 0
|
||||
self.S2 = 0
|
||||
self.S3 = 0
|
||||
self.S4 = 0
|
||||
self.H1 = 1
|
||||
self.H2 = 2
|
||||
self.H3 = 3
|
||||
self.H4 = 4
|
||||
self.I1 = "0"
|
||||
self.I2 = "0"
|
||||
self.I3 = "0"
|
||||
self.I4 = "0"
|
||||
self.I5 = "0"
|
||||
|
||||
super().__init__(DashboardConfig, AllPeerJobs, AllPeerShareLinks, DashboardWebHooks, name, data, backup, startup, wg=False)
|
||||
|
||||
@@ -58,65 +69,64 @@ class AmneziaWireguardConfiguration(WireguardConfiguration):
|
||||
"Jmax": self.Jmax,
|
||||
"S1": self.S1,
|
||||
"S2": self.S2,
|
||||
"S3": self.S3,
|
||||
"S4": self.S4,
|
||||
"H1": self.H1,
|
||||
"H2": self.H2,
|
||||
"H3": self.H3,
|
||||
"H4": self.H4
|
||||
"H4": self.H4,
|
||||
"I1": self.I1,
|
||||
"I2": self.I2,
|
||||
"I3": self.I3,
|
||||
"I4": self.I4,
|
||||
"I5": self.I5
|
||||
}
|
||||
|
||||
def createDatabase(self, dbName = None):
|
||||
def generate_column_obj():
|
||||
return [
|
||||
sqlalchemy.Column('id', sqlalchemy.String(255), nullable=False, primary_key=True),
|
||||
sqlalchemy.Column('private_key', sqlalchemy.String(255)),
|
||||
sqlalchemy.Column('DNS', sqlalchemy.Text),
|
||||
sqlalchemy.Column('endpoint_allowed_ip', sqlalchemy.Text),
|
||||
sqlalchemy.Column('name', sqlalchemy.Text),
|
||||
sqlalchemy.Column('total_receive', sqlalchemy.Float),
|
||||
sqlalchemy.Column('total_sent', sqlalchemy.Float),
|
||||
sqlalchemy.Column('total_data', sqlalchemy.Float),
|
||||
sqlalchemy.Column('endpoint', sqlalchemy.String(255)),
|
||||
sqlalchemy.Column('status', sqlalchemy.String(255)),
|
||||
sqlalchemy.Column('latest_handshake', sqlalchemy.String(255)),
|
||||
sqlalchemy.Column('allowed_ip', sqlalchemy.String(255)),
|
||||
sqlalchemy.Column('cumu_receive', sqlalchemy.Float),
|
||||
sqlalchemy.Column('cumu_sent', sqlalchemy.Float),
|
||||
sqlalchemy.Column('cumu_data', sqlalchemy.Float),
|
||||
sqlalchemy.Column('mtu', sqlalchemy.Integer),
|
||||
sqlalchemy.Column('keepalive', sqlalchemy.Integer),
|
||||
sqlalchemy.Column('notes', sqlalchemy.Text),
|
||||
sqlalchemy.Column('remote_endpoint', sqlalchemy.String(255)),
|
||||
sqlalchemy.Column('preshared_key', sqlalchemy.String(255))
|
||||
]
|
||||
|
||||
if dbName is None:
|
||||
dbName = self.Name
|
||||
|
||||
|
||||
self.peersTable = sqlalchemy.Table(
|
||||
dbName, self.metadata,
|
||||
sqlalchemy.Column('id', sqlalchemy.String(255), nullable=False, primary_key=True),
|
||||
sqlalchemy.Column('private_key', sqlalchemy.String(255)),
|
||||
sqlalchemy.Column('DNS', sqlalchemy.Text),
|
||||
sqlalchemy.Column('advanced_security', sqlalchemy.String(255)),
|
||||
sqlalchemy.Column('endpoint_allowed_ip', sqlalchemy.Text),
|
||||
sqlalchemy.Column('name', sqlalchemy.Text),
|
||||
sqlalchemy.Column('total_receive', sqlalchemy.Float),
|
||||
sqlalchemy.Column('total_sent', sqlalchemy.Float),
|
||||
sqlalchemy.Column('total_data', sqlalchemy.Float),
|
||||
sqlalchemy.Column('endpoint', sqlalchemy.String(255)),
|
||||
sqlalchemy.Column('status', sqlalchemy.String(255)),
|
||||
sqlalchemy.Column('latest_handshake', sqlalchemy.String(255)),
|
||||
sqlalchemy.Column('allowed_ip', sqlalchemy.String(255)),
|
||||
sqlalchemy.Column('cumu_receive', sqlalchemy.Float),
|
||||
sqlalchemy.Column('cumu_sent', sqlalchemy.Float),
|
||||
sqlalchemy.Column('cumu_data', sqlalchemy.Float),
|
||||
sqlalchemy.Column('mtu', sqlalchemy.Integer),
|
||||
sqlalchemy.Column('keepalive', sqlalchemy.Integer),
|
||||
sqlalchemy.Column('remote_endpoint', sqlalchemy.String(255)),
|
||||
sqlalchemy.Column('preshared_key', sqlalchemy.String(255)),
|
||||
extend_existing=True
|
||||
f'{dbName}', self.metadata, *generate_column_obj(), extend_existing=True
|
||||
)
|
||||
|
||||
self.peersRestrictedTable = sqlalchemy.Table(
|
||||
f'{dbName}_restrict_access', self.metadata,
|
||||
sqlalchemy.Column('id', sqlalchemy.String(255), nullable=False, primary_key=True),
|
||||
sqlalchemy.Column('private_key', sqlalchemy.String(255)),
|
||||
sqlalchemy.Column('DNS', sqlalchemy.Text),
|
||||
sqlalchemy.Column('advanced_security', sqlalchemy.String(255)),
|
||||
sqlalchemy.Column('endpoint_allowed_ip', sqlalchemy.Text),
|
||||
sqlalchemy.Column('name', sqlalchemy.Text),
|
||||
sqlalchemy.Column('total_receive', sqlalchemy.Float),
|
||||
sqlalchemy.Column('total_sent', sqlalchemy.Float),
|
||||
sqlalchemy.Column('total_data', sqlalchemy.Float),
|
||||
sqlalchemy.Column('endpoint', sqlalchemy.String(255)),
|
||||
sqlalchemy.Column('status', sqlalchemy.String(255)),
|
||||
sqlalchemy.Column('latest_handshake', sqlalchemy.String(255)),
|
||||
sqlalchemy.Column('allowed_ip', sqlalchemy.String(255)),
|
||||
sqlalchemy.Column('cumu_receive', sqlalchemy.Float),
|
||||
sqlalchemy.Column('cumu_sent', sqlalchemy.Float),
|
||||
sqlalchemy.Column('cumu_data', sqlalchemy.Float),
|
||||
sqlalchemy.Column('mtu', sqlalchemy.Integer),
|
||||
sqlalchemy.Column('keepalive', sqlalchemy.Integer),
|
||||
sqlalchemy.Column('remote_endpoint', sqlalchemy.String(255)),
|
||||
sqlalchemy.Column('preshared_key', sqlalchemy.String(255)),
|
||||
extend_existing=True
|
||||
f'{dbName}_restrict_access', self.metadata, *generate_column_obj(), extend_existing=True
|
||||
)
|
||||
|
||||
self.peersDeletedTable = sqlalchemy.Table(
|
||||
f'{dbName}_deleted', self.metadata, *generate_column_obj(), extend_existing=True
|
||||
)
|
||||
|
||||
if self.DashboardConfig.GetConfig("Database", "type")[1] == 'sqlite':
|
||||
time_col_type = sqlalchemy.DATETIME
|
||||
else:
|
||||
time_col_type = sqlalchemy.TIMESTAMP
|
||||
|
||||
self.peersTransferTable = sqlalchemy.Table(
|
||||
f'{dbName}_transfer', self.metadata,
|
||||
sqlalchemy.Column('id', sqlalchemy.String(255), nullable=False),
|
||||
@@ -126,38 +136,7 @@ class AmneziaWireguardConfiguration(WireguardConfiguration):
|
||||
sqlalchemy.Column('cumu_receive', sqlalchemy.Float),
|
||||
sqlalchemy.Column('cumu_sent', sqlalchemy.Float),
|
||||
sqlalchemy.Column('cumu_data', sqlalchemy.Float),
|
||||
sqlalchemy.Column('time', (sqlalchemy.DATETIME if self.DashboardConfig.GetConfig("Database", "type")[1] == 'sqlite' else sqlalchemy.TIMESTAMP),
|
||||
server_default=sqlalchemy.func.now()),
|
||||
extend_existing=True
|
||||
)
|
||||
self.peersDeletedTable = sqlalchemy.Table(
|
||||
f'{dbName}_deleted', self.metadata,
|
||||
sqlalchemy.Column('id', sqlalchemy.String(255), nullable=False),
|
||||
sqlalchemy.Column('private_key', sqlalchemy.String(255)),
|
||||
sqlalchemy.Column('DNS', sqlalchemy.Text),
|
||||
sqlalchemy.Column('advanced_security', sqlalchemy.String(255)),
|
||||
sqlalchemy.Column('endpoint_allowed_ip', sqlalchemy.Text),
|
||||
sqlalchemy.Column('name', sqlalchemy.Text),
|
||||
sqlalchemy.Column('total_receive', sqlalchemy.Float),
|
||||
sqlalchemy.Column('total_sent', sqlalchemy.Float),
|
||||
sqlalchemy.Column('total_data', sqlalchemy.Float),
|
||||
sqlalchemy.Column('endpoint', sqlalchemy.String(255)),
|
||||
sqlalchemy.Column('status', sqlalchemy.String(255)),
|
||||
sqlalchemy.Column('latest_handshake', sqlalchemy.String(255)),
|
||||
sqlalchemy.Column('allowed_ip', sqlalchemy.String(255)),
|
||||
sqlalchemy.Column('cumu_receive', sqlalchemy.Float),
|
||||
sqlalchemy.Column('cumu_sent', sqlalchemy.Float),
|
||||
sqlalchemy.Column('cumu_data', sqlalchemy.Float),
|
||||
sqlalchemy.Column('mtu', sqlalchemy.Integer),
|
||||
sqlalchemy.Column('keepalive', sqlalchemy.Integer),
|
||||
sqlalchemy.Column('remote_endpoint', sqlalchemy.String(255)),
|
||||
sqlalchemy.Column('preshared_key', sqlalchemy.String(255)),
|
||||
extend_existing=True
|
||||
)
|
||||
self.infoTable = sqlalchemy.Table(
|
||||
'ConfigurationsInfo', self.metadata,
|
||||
sqlalchemy.Column('ID', sqlalchemy.String(255), primary_key=True),
|
||||
sqlalchemy.Column('Info', sqlalchemy.Text),
|
||||
sqlalchemy.Column('time', time_col_type, server_default=sqlalchemy.func.now()),
|
||||
extend_existing=True
|
||||
)
|
||||
|
||||
@@ -165,8 +144,13 @@ class AmneziaWireguardConfiguration(WireguardConfiguration):
|
||||
f'{dbName}_history_endpoint', self.metadata,
|
||||
sqlalchemy.Column('id', sqlalchemy.String(255), nullable=False),
|
||||
sqlalchemy.Column('endpoint', sqlalchemy.String(255), nullable=False),
|
||||
sqlalchemy.Column('time',
|
||||
(sqlalchemy.DATETIME if self.DashboardConfig.GetConfig("Database", "type")[1] == 'sqlite' else sqlalchemy.TIMESTAMP)),
|
||||
sqlalchemy.Column('time', time_col_type)
|
||||
)
|
||||
|
||||
self.infoTable = sqlalchemy.Table(
|
||||
'ConfigurationsInfo', self.metadata,
|
||||
sqlalchemy.Column('ID', sqlalchemy.String(255), primary_key=True),
|
||||
sqlalchemy.Column('Info', sqlalchemy.Text),
|
||||
extend_existing=True
|
||||
)
|
||||
|
||||
@@ -174,8 +158,6 @@ class AmneziaWireguardConfiguration(WireguardConfiguration):
|
||||
|
||||
def getPeers(self):
|
||||
self.Peers.clear()
|
||||
current_app.logger.info(f"Refreshing {self.Name} peer list")
|
||||
|
||||
if self.configurationFileChanged():
|
||||
with open(self.configPath, 'r') as configFile:
|
||||
p = []
|
||||
@@ -213,11 +195,9 @@ class AmneziaWireguardConfiguration(WireguardConfiguration):
|
||||
if tempPeer is None:
|
||||
tempPeer = {
|
||||
"id": i['PublicKey'],
|
||||
"advanced_security": i.get('AdvancedSecurity', 'off'),
|
||||
"private_key": "",
|
||||
"DNS": self.DashboardConfig.GetConfig("Peers", "peer_global_DNS")[1],
|
||||
"endpoint_allowed_ip": self.DashboardConfig.GetConfig("Peers", "peer_endpoint_allowed_ip")[
|
||||
1],
|
||||
"endpoint_allowed_ip": self.DashboardConfig.GetConfig("Peers", "peer_endpoint_allowed_ip")[1],
|
||||
"name": i.get("name"),
|
||||
"total_receive": 0,
|
||||
"total_sent": 0,
|
||||
@@ -231,6 +211,7 @@ class AmneziaWireguardConfiguration(WireguardConfiguration):
|
||||
"cumu_data": 0,
|
||||
"mtu": self.DashboardConfig.GetConfig("Peers", "peer_mtu")[1],
|
||||
"keepalive": self.DashboardConfig.GetConfig("Peers", "peer_keep_alive")[1],
|
||||
"notes": "",
|
||||
"remote_endpoint": self.DashboardConfig.GetConfig("Peers", "remote_endpoint")[1],
|
||||
"preshared_key": i["PresharedKey"] if "PresharedKey" in i.keys() else ""
|
||||
}
|
||||
@@ -245,14 +226,14 @@ class AmneziaWireguardConfiguration(WireguardConfiguration):
|
||||
self.peersTable.columns.id == i['PublicKey']
|
||||
)
|
||||
)
|
||||
self.Peers.append(AmneziaWGPeer(tempPeer, self))
|
||||
self.Peers.append(AmneziaPeer(tempPeer, self))
|
||||
except Exception as e:
|
||||
current_app.logger.error(f"{self.Name} getPeers() Error", e)
|
||||
else:
|
||||
with self.engine.connect() as conn:
|
||||
existingPeers = conn.execute(self.peersTable.select()).mappings().fetchall()
|
||||
for i in existingPeers:
|
||||
self.Peers.append(AmneziaWGPeer(i, self))
|
||||
self.Peers.append(AmneziaPeer(i, self))
|
||||
|
||||
def addPeers(self, peers: list) -> tuple[bool, list, str]:
|
||||
result = {
|
||||
@@ -260,6 +241,15 @@ class AmneziaWireguardConfiguration(WireguardConfiguration):
|
||||
"peers": []
|
||||
}
|
||||
try:
|
||||
cleanedAllowedIPs = {}
|
||||
for p in peers:
|
||||
newAllowedIPs = p['allowed_ip'].replace(" ", "")
|
||||
if not CheckAddress(newAllowedIPs):
|
||||
return False, [], "Allowed IPs entry format is incorrect"
|
||||
if not CheckPeerKey(p["id"]):
|
||||
return False, [], "Peer key format is incorrect"
|
||||
cleanedAllowedIPs[p["id"]] = newAllowedIPs
|
||||
|
||||
with self.engine.begin() as conn:
|
||||
for i in peers:
|
||||
newPeer = {
|
||||
@@ -280,9 +270,9 @@ class AmneziaWireguardConfiguration(WireguardConfiguration):
|
||||
"cumu_data": 0,
|
||||
"mtu": i['mtu'],
|
||||
"keepalive": i['keepalive'],
|
||||
"notes": i.get('notes', ''),
|
||||
"remote_endpoint": self.DashboardConfig.GetConfig("Peers", "remote_endpoint")[1],
|
||||
"preshared_key": i["preshared_key"],
|
||||
"advanced_security": i['advanced_security']
|
||||
"preshared_key": i["preshared_key"]
|
||||
}
|
||||
conn.execute(
|
||||
self.peersTable.insert().values(newPeer)
|
||||
@@ -295,13 +285,15 @@ class AmneziaWireguardConfiguration(WireguardConfiguration):
|
||||
with open(uid, "w+") as f:
|
||||
f.write(p['preshared_key'])
|
||||
|
||||
subprocess.check_output(
|
||||
f"{self.Protocol} set {self.Name} peer {p['id']} allowed-ips {p['allowed_ip'].replace(' ', '')}{f' preshared-key {uid}' if presharedKeyExist else ''}",
|
||||
shell=True, stderr=subprocess.STDOUT)
|
||||
command = [self.Protocol, "set", self.Name, "peer", p['id'], "allowed-ips", cleanedAllowedIPs[p["id"]], "preshared-key", uid if presharedKeyExist else "/dev/null"]
|
||||
subprocess.check_output(command, stderr=subprocess.STDOUT)
|
||||
|
||||
if presharedKeyExist:
|
||||
os.remove(uid)
|
||||
subprocess.check_output(
|
||||
f"{self.Protocol}-quick save {self.Name}", shell=True, stderr=subprocess.STDOUT)
|
||||
|
||||
command = [f"{self.Protocol}-quick", "save", self.Name]
|
||||
subprocess.check_output(command, stderr=subprocess.STDOUT)
|
||||
|
||||
self.getPeers()
|
||||
for p in peers:
|
||||
p = self.searchPeer(p['id'])
|
||||
@@ -313,7 +305,7 @@ class AmneziaWireguardConfiguration(WireguardConfiguration):
|
||||
})
|
||||
except Exception as e:
|
||||
current_app.logger.error("Add peers error", e)
|
||||
return False, [], str(e)
|
||||
return False, [], "Internal server error"
|
||||
return True, result['peers'], ""
|
||||
|
||||
def getRestrictedPeers(self):
|
||||
@@ -321,4 +313,4 @@ class AmneziaWireguardConfiguration(WireguardConfiguration):
|
||||
with self.engine.connect() as conn:
|
||||
restricted = conn.execute(self.peersRestrictedTable.select()).mappings().fetchall()
|
||||
for i in restricted:
|
||||
self.RestrictedPeers.append(AmneziaWGPeer(i, self))
|
||||
self.RestrictedPeers.append(AmneziaPeer(i, self))
|
||||
120
src/modules/AmneziaPeer.py
Normal file
120
src/modules/AmneziaPeer.py
Normal file
@@ -0,0 +1,120 @@
|
||||
import os
|
||||
from flask import current_app
|
||||
import random
|
||||
import re
|
||||
import subprocess
|
||||
import uuid
|
||||
|
||||
from flask import current_app
|
||||
from .Peer import Peer
|
||||
from .Utilities import CheckAddress, ValidateDNSAddress, GenerateWireguardPublicKey
|
||||
|
||||
|
||||
class AmneziaPeer(Peer):
|
||||
def __init__(self, tableData, configuration):
|
||||
super().__init__(tableData, configuration)
|
||||
|
||||
|
||||
def updatePeer(self, name: str, private_key: str,
|
||||
preshared_key: str,
|
||||
dns_addresses: str,
|
||||
allowed_ip: str,
|
||||
endpoint_allowed_ip: str,
|
||||
mtu: int,
|
||||
keepalive: int,
|
||||
notes: str
|
||||
) -> tuple[bool, str | None]:
|
||||
|
||||
if not self.configuration.getStatus():
|
||||
self.configuration.toggleConfiguration()
|
||||
|
||||
# Before we do any compute, let us check if the given endpoint allowed ip is valid at all
|
||||
if not CheckAddress(endpoint_allowed_ip):
|
||||
return False, f"Endpoint Allowed IPs format is incorrect"
|
||||
|
||||
peers = []
|
||||
for peer in self.configuration.getPeersList():
|
||||
# Make sure to exclude your own data when updating since its not really relevant
|
||||
if peer.id != self.id:
|
||||
continue
|
||||
peers.append(peer)
|
||||
|
||||
used_allowed_ips = []
|
||||
for peer in peers:
|
||||
ips = peer.allowed_ip.split(',')
|
||||
ips = [ip.strip() for ip in ips]
|
||||
used_allowed_ips.append(ips)
|
||||
|
||||
if allowed_ip in used_allowed_ips:
|
||||
return False, "Allowed IP already taken by another peer"
|
||||
|
||||
if not ValidateDNSAddress(dns_addresses):
|
||||
return False, f"DNS IP-Address or FQDN is incorrect"
|
||||
|
||||
if isinstance(mtu, str):
|
||||
mtu = 0
|
||||
|
||||
if isinstance(keepalive, str):
|
||||
keepalive = 0
|
||||
|
||||
if mtu not in range(0, 1461):
|
||||
return False, "MTU format is not correct"
|
||||
|
||||
if keepalive < 0:
|
||||
return False, "Persistent Keepalive format is not correct"
|
||||
|
||||
if len(private_key) > 0:
|
||||
pubKey = GenerateWireguardPublicKey(private_key)
|
||||
if not pubKey[0] or pubKey[1] != self.id:
|
||||
return False, "Private key does not match with the public key"
|
||||
|
||||
try:
|
||||
rand = random.Random()
|
||||
uid = str(uuid.UUID(int=rand.getrandbits(128), version=4))
|
||||
psk_exist = len(preshared_key) > 0
|
||||
|
||||
if psk_exist:
|
||||
with open(uid, "w+") as f:
|
||||
f.write(preshared_key)
|
||||
|
||||
newAllowedIPs = allowed_ip.replace(" ", "")
|
||||
if not CheckAddress(newAllowedIPs):
|
||||
return False, "Allowed IPs entry format is incorrect"
|
||||
|
||||
command = [self.configuration.Protocol, "set", self.configuration.Name, "peer", self.id, "allowed-ips", newAllowedIPs, "preshared-key", uid if psk_exist else "/dev/null"]
|
||||
|
||||
updateAllowedIp = subprocess.check_output(command, stderr=subprocess.STDOUT)
|
||||
|
||||
if psk_exist: os.remove(uid)
|
||||
|
||||
if len(updateAllowedIp.decode().strip("\n")) != 0:
|
||||
current_app.logger.error(f"Update peer failed when updating Allowed IPs.\nInput: {newAllowedIPs}\nOutput: {updateAllowedIp.decode().strip('\n')}")
|
||||
return False, "Internal server error"
|
||||
|
||||
command = [f"{self.configuration.Protocol}-quick", "save", self.configuration.Name]
|
||||
saveConfig = subprocess.check_output(command, stderr=subprocess.STDOUT)
|
||||
|
||||
if f"wg showconf {self.configuration.Name}" not in saveConfig.decode().strip('\n'):
|
||||
current_app.logger.error("Update peer failed when saving the configuration")
|
||||
return False, "Internal server error"
|
||||
|
||||
with self.configuration.engine.begin() as conn:
|
||||
conn.execute(
|
||||
self.configuration.peersTable.update().values({
|
||||
"name": name,
|
||||
"private_key": private_key,
|
||||
"DNS": dns_addresses,
|
||||
"endpoint_allowed_ip": endpoint_allowed_ip,
|
||||
"mtu": mtu,
|
||||
"keepalive": keepalive,
|
||||
"notes": notes,
|
||||
"preshared_key": preshared_key
|
||||
}).where(
|
||||
self.configuration.peersTable.c.id == self.id
|
||||
)
|
||||
)
|
||||
self.configuration.getPeers()
|
||||
return True, None
|
||||
except subprocess.CalledProcessError as exc:
|
||||
current_app.logger.error(f"Subprocess call failed:\n{exc.output.decode("UTF-8")}")
|
||||
return False, "Internal server error"
|
||||
@@ -1,92 +0,0 @@
|
||||
import os
|
||||
import random
|
||||
import re
|
||||
import subprocess
|
||||
import uuid
|
||||
|
||||
from .Peer import Peer
|
||||
from .Utilities import ValidateIPAddressesWithRange, ValidateDNSAddress, GenerateWireguardPublicKey
|
||||
|
||||
|
||||
class AmneziaWGPeer(Peer):
|
||||
def __init__(self, tableData, configuration):
|
||||
self.advanced_security = tableData["advanced_security"]
|
||||
super().__init__(tableData, configuration)
|
||||
|
||||
|
||||
def updatePeer(self, name: str, private_key: str,
|
||||
preshared_key: str,
|
||||
dns_addresses: str, allowed_ip: str, endpoint_allowed_ip: str, mtu: int,
|
||||
keepalive: int, advanced_security: str) -> tuple[bool, str] or tuple[bool, None]:
|
||||
if not self.configuration.getStatus():
|
||||
self.configuration.toggleConfiguration()
|
||||
|
||||
existingAllowedIps = [item for row in list(
|
||||
map(lambda x: [q.strip() for q in x.split(',')],
|
||||
map(lambda y: y.allowed_ip,
|
||||
list(filter(lambda k: k.id != self.id, self.configuration.getPeersList()))))) for item in row]
|
||||
|
||||
if allowed_ip in existingAllowedIps:
|
||||
return False, "Allowed IP already taken by another peer"
|
||||
if not ValidateIPAddressesWithRange(endpoint_allowed_ip):
|
||||
return False, f"Endpoint Allowed IPs format is incorrect"
|
||||
if len(dns_addresses) > 0 and not ValidateDNSAddress(dns_addresses):
|
||||
return False, f"DNS format is incorrect"
|
||||
|
||||
if type(mtu) is str:
|
||||
mtu = 0
|
||||
|
||||
if type(keepalive) is str:
|
||||
keepalive = 0
|
||||
|
||||
if mtu < 0 or mtu > 1460:
|
||||
return False, "MTU format is not correct"
|
||||
if keepalive < 0:
|
||||
return False, "Persistent Keepalive format is not correct"
|
||||
if advanced_security != "on" and advanced_security != "off":
|
||||
return False, "Advanced Security can only be on or off"
|
||||
if len(private_key) > 0:
|
||||
pubKey = GenerateWireguardPublicKey(private_key)
|
||||
if not pubKey[0] or pubKey[1] != self.id:
|
||||
return False, "Private key does not match with the public key"
|
||||
try:
|
||||
rd = random.Random()
|
||||
uid = str(uuid.UUID(int=rd.getrandbits(128), version=4))
|
||||
pskExist = len(preshared_key) > 0
|
||||
|
||||
if pskExist:
|
||||
with open(uid, "w+") as f:
|
||||
f.write(preshared_key)
|
||||
newAllowedIPs = allowed_ip.replace(" ", "")
|
||||
updateAllowedIp = subprocess.check_output(
|
||||
f"{self.configuration.Protocol} set {self.configuration.Name} peer {self.id} allowed-ips {newAllowedIPs} {f'preshared-key {uid}' if pskExist else 'preshared-key /dev/null'}",
|
||||
shell=True, stderr=subprocess.STDOUT)
|
||||
|
||||
if pskExist: os.remove(uid)
|
||||
|
||||
if len(updateAllowedIp.decode().strip("\n")) != 0:
|
||||
return False, "Update peer failed when updating Allowed IPs"
|
||||
saveConfig = subprocess.check_output(f"{self.configuration.Protocol}-quick save {self.configuration.Name}",
|
||||
shell=True, stderr=subprocess.STDOUT)
|
||||
if f"wg showconf {self.configuration.Name}" not in saveConfig.decode().strip('\n'):
|
||||
return False, "Update peer failed when saving the configuration"
|
||||
|
||||
with self.configuration.engine.begin() as conn:
|
||||
conn.execute(
|
||||
self.configuration.peersTable.update().values({
|
||||
"name": name,
|
||||
"private_key": private_key,
|
||||
"DNS": dns_addresses,
|
||||
"endpoint_allowed_ip": endpoint_allowed_ip,
|
||||
"mtu": mtu,
|
||||
"keepalive": keepalive,
|
||||
"preshared_key": preshared_key,
|
||||
"advanced_security": advanced_security
|
||||
}).where(
|
||||
self.configuration.peersTable.c.id == self.id
|
||||
)
|
||||
)
|
||||
self.configuration.getPeers()
|
||||
return True, None
|
||||
except subprocess.CalledProcessError as exc:
|
||||
return False, exc.output.decode("UTF-8").strip()
|
||||
@@ -8,7 +8,7 @@ import pyotp
|
||||
import sqlalchemy as db
|
||||
import requests
|
||||
|
||||
from .ConnectionString import ConnectionString
|
||||
from .DatabaseConnection import ConnectionString
|
||||
from .DashboardClientsPeerAssignment import DashboardClientsPeerAssignment
|
||||
from .DashboardClientsTOTP import DashboardClientsTOTP
|
||||
from .DashboardOIDC import DashboardOIDC
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import datetime
|
||||
import uuid
|
||||
|
||||
from .ConnectionString import ConnectionString
|
||||
from .DatabaseConnection import ConnectionString
|
||||
from .DashboardLogger import DashboardLogger
|
||||
import sqlalchemy as db
|
||||
from .WireguardConfiguration import WireguardConfiguration
|
||||
|
||||
@@ -3,7 +3,7 @@ import hashlib
|
||||
import uuid
|
||||
|
||||
import sqlalchemy as db
|
||||
from .ConnectionString import ConnectionString
|
||||
from .DatabaseConnection import ConnectionString
|
||||
|
||||
|
||||
class DashboardClientsTOTP:
|
||||
|
||||
@@ -7,19 +7,15 @@ import sqlalchemy as db
|
||||
from datetime import datetime
|
||||
from typing import Any
|
||||
from flask import current_app
|
||||
from .ConnectionString import ConnectionString
|
||||
from .Utilities import (
|
||||
GetRemoteEndpoint, ValidateDNSAddress
|
||||
)
|
||||
from .DatabaseConnection import ConnectionString
|
||||
from .Utilities import (GetRemoteEndpoint, ValidateDNSAddress)
|
||||
from .DashboardAPIKey import DashboardAPIKey
|
||||
|
||||
|
||||
|
||||
class DashboardConfig:
|
||||
DashboardVersion = 'v4.3.0.1'
|
||||
DashboardVersion = 'v4.3.3'
|
||||
ConfigurationPath = os.getenv('CONFIGURATION_PATH', '.')
|
||||
ConfigurationFilePath = os.path.join(ConfigurationPath, 'wg-dashboard.ini')
|
||||
|
||||
|
||||
def __init__(self):
|
||||
if not os.path.exists(DashboardConfig.ConfigurationFilePath):
|
||||
open(DashboardConfig.ConfigurationFilePath, "x")
|
||||
@@ -83,9 +79,11 @@ class DashboardConfig:
|
||||
},
|
||||
"Clients": {
|
||||
"enable": "true",
|
||||
"sign_up": "true"
|
||||
},
|
||||
"WireGuardConfiguration": {
|
||||
"autostart": ""
|
||||
"autostart": "",
|
||||
"peer_tracking": "false"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -102,6 +100,54 @@ class DashboardConfig:
|
||||
self.APIAccessed = False
|
||||
self.SetConfig("Server", "version", DashboardConfig.DashboardVersion)
|
||||
|
||||
def EnsureDatabaseIntegrity(self, wireguardConfigurations):
|
||||
expected_columns = {
|
||||
'id': db.String(255),
|
||||
'private_key': db.String(255),
|
||||
'DNS': db.Text,
|
||||
'endpoint_allowed_ip': db.Text,
|
||||
'name': db.Text,
|
||||
'total_receive': db.Float,
|
||||
'total_sent': db.Float,
|
||||
'total_data': db.Float,
|
||||
'endpoint': db.String(255),
|
||||
'status': db.String(255),
|
||||
'latest_handshake': db.String(255),
|
||||
'allowed_ip': db.String(255),
|
||||
'cumu_receive': db.Float,
|
||||
'cumu_sent': db.Float,
|
||||
'cumu_data': db.Float,
|
||||
'mtu': db.Integer,
|
||||
'keepalive': db.Integer,
|
||||
'notes': db.Text,
|
||||
'remote_endpoint': db.String(255),
|
||||
'preshared_key': db.String(255)
|
||||
}
|
||||
|
||||
inspector = db.inspect(self.engine)
|
||||
|
||||
with self.engine.begin() as conn:
|
||||
for cfg_name, cfg_obj in wireguardConfigurations.items():
|
||||
tables_to_check = [
|
||||
cfg_name,
|
||||
f'{cfg_name}_restrict_access',
|
||||
f'{cfg_name}_deleted'
|
||||
]
|
||||
|
||||
for table_name in tables_to_check:
|
||||
if not table_name:
|
||||
continue
|
||||
if not inspector.has_table(table_name):
|
||||
continue
|
||||
|
||||
existing_columns = [c['name'] for c in inspector.get_columns(table_name)]
|
||||
|
||||
for col_name, col_type in expected_columns.items():
|
||||
if col_name not in existing_columns:
|
||||
type_str = col_type().compile(dialect=self.engine.dialect)
|
||||
current_app.logger.info(f"Adding missing column '{col_name}' to table '{table_name}'")
|
||||
conn.execute(db.text(f'ALTER TABLE "{table_name}" ADD COLUMN "{col_name}" {type_str}'))
|
||||
|
||||
def getConnectionString(self, database) -> str or None:
|
||||
sqlitePath = os.path.join(DashboardConfig.ConfigurationPath, "db")
|
||||
|
||||
@@ -116,7 +162,7 @@ class DashboardConfig:
|
||||
cn = f'sqlite:///{os.path.join(sqlitePath, f"{database}.db")}'
|
||||
if not database_exists(cn):
|
||||
create_database(cn)
|
||||
return cn
|
||||
return cn
|
||||
|
||||
def __createAPIKeyTable(self):
|
||||
self.apiKeyTable = db.Table('DashboardAPIKeys', self.dbMetadata,
|
||||
|
||||
@@ -4,7 +4,7 @@ Dashboard Logger Class
|
||||
import uuid
|
||||
import sqlalchemy as db
|
||||
from flask import current_app
|
||||
from .ConnectionString import ConnectionString
|
||||
from .DatabaseConnection import ConnectionString
|
||||
|
||||
|
||||
class DashboardLogger:
|
||||
|
||||
@@ -8,7 +8,7 @@ from datetime import datetime, timedelta
|
||||
import requests
|
||||
from pydantic import BaseModel, field_serializer
|
||||
import sqlalchemy as db
|
||||
from .ConnectionString import ConnectionString
|
||||
from .DatabaseConnection import ConnectionString
|
||||
from flask import current_app
|
||||
|
||||
WebHookActions = ['peer_created', 'peer_deleted', 'peer_updated']
|
||||
|
||||
@@ -1,14 +1,15 @@
|
||||
import configparser
|
||||
import os
|
||||
from sqlalchemy_utils import database_exists, create_database
|
||||
from flask import current_app
|
||||
|
||||
def ConnectionString(database) -> str:
|
||||
parser = configparser.ConfigParser(strict=False)
|
||||
parser.read_file(open('wg-dashboard.ini', "r+"))
|
||||
|
||||
sqlitePath = os.path.join("db")
|
||||
if not os.path.isdir(sqlitePath):
|
||||
os.mkdir(sqlitePath)
|
||||
|
||||
if parser.get("Database", "type") == "postgresql":
|
||||
cn = f'postgresql+psycopg://{parser.get("Database", "username")}:{parser.get("Database", "password")}@{parser.get("Database", "host")}/{database}'
|
||||
elif parser.get("Database", "type") == "mysql":
|
||||
@@ -19,7 +20,6 @@ def ConnectionString(database) -> str:
|
||||
if not database_exists(cn):
|
||||
create_database(cn)
|
||||
except Exception as e:
|
||||
current_app.logger.error("Database error. Terminating...", e)
|
||||
exit(1)
|
||||
|
||||
|
||||
return cn
|
||||
@@ -1,76 +1,101 @@
|
||||
import os.path
|
||||
import ssl
|
||||
import smtplib
|
||||
|
||||
# Email libaries
|
||||
from email import encoders
|
||||
from email.header import Header
|
||||
from email.mime.base import MIMEBase
|
||||
from email.mime.multipart import MIMEMultipart
|
||||
from email.mime.text import MIMEText
|
||||
from email.utils import formataddr
|
||||
from email.utils import formatdate
|
||||
|
||||
class EmailSender:
|
||||
def __init__(self, DashboardConfig):
|
||||
self.smtp = None
|
||||
self.DashboardConfig = DashboardConfig
|
||||
|
||||
if not os.path.exists('./attachments'):
|
||||
os.mkdir('./attachments')
|
||||
|
||||
def Server(self):
|
||||
return self.DashboardConfig.GetConfig("Email", "server")[1]
|
||||
|
||||
def Port(self):
|
||||
return self.DashboardConfig.GetConfig("Email", "port")[1]
|
||||
|
||||
def Encryption(self):
|
||||
return self.DashboardConfig.GetConfig("Email", "encryption")[1]
|
||||
|
||||
def Username(self):
|
||||
return self.DashboardConfig.GetConfig("Email", "username")[1]
|
||||
|
||||
def Password(self):
|
||||
return self.DashboardConfig.GetConfig("Email", "email_password")[1]
|
||||
|
||||
def SendFrom(self):
|
||||
return self.DashboardConfig.GetConfig("Email", "send_from")[1]
|
||||
|
||||
# Thank you, @gdeeble from GitHub
|
||||
def AuthenticationRequired(self):
|
||||
return self.DashboardConfig.GetConfig("Email", "authentication_required")[1]
|
||||
|
||||
def ready(self):
|
||||
if self.AuthenticationRequired():
|
||||
return all([self.Server(), self.Port(), self.Encryption(), self.Username(), self.Password(), self.SendFrom()])
|
||||
return all([self.Server(), self.Port(), self.Encryption(), self.SendFrom()])
|
||||
self.refresh_vals()
|
||||
|
||||
def send(self, receiver, subject, body, includeAttachment = False, attachmentName = "") -> tuple[bool, str] | tuple[bool, None]:
|
||||
if self.ready():
|
||||
try:
|
||||
self.smtp = smtplib.SMTP(self.Server(), port=int(self.Port()))
|
||||
self.smtp.ehlo()
|
||||
if self.Encryption() == "STARTTLS":
|
||||
self.smtp.starttls()
|
||||
if self.AuthenticationRequired():
|
||||
self.smtp.login(self.Username(), self.Password())
|
||||
message = MIMEMultipart()
|
||||
message['Subject'] = subject
|
||||
message['From'] = self.SendFrom()
|
||||
message["To"] = receiver
|
||||
message.attach(MIMEText(body, "plain"))
|
||||
def refresh_vals(self) -> None:
|
||||
self.Server = self.DashboardConfig.GetConfig("Email", "server")[1]
|
||||
self.Port = self.DashboardConfig.GetConfig("Email", "port")[1]
|
||||
|
||||
if includeAttachment and len(attachmentName) > 0:
|
||||
attachmentPath = os.path.join('./attachments', attachmentName)
|
||||
if os.path.exists(attachmentPath):
|
||||
attachment = MIMEBase("application", "octet-stream")
|
||||
with open(os.path.join('./attachments', attachmentName), 'rb') as f:
|
||||
attachment.set_payload(f.read())
|
||||
encoders.encode_base64(attachment)
|
||||
attachment.add_header("Content-Disposition", f"attachment; filename= {attachmentName}",)
|
||||
message.attach(attachment)
|
||||
else:
|
||||
self.smtp.close()
|
||||
return False, "Attachment does not exist"
|
||||
self.smtp.sendmail(self.SendFrom(), receiver, message.as_string())
|
||||
self.smtp.close()
|
||||
return True, None
|
||||
except Exception as e:
|
||||
return False, f"Send failed | Reason: {e}"
|
||||
return False, "SMTP not configured"
|
||||
self.Encryption = self.DashboardConfig.GetConfig("Email", "encryption")[1]
|
||||
self.AuthRequired = self.DashboardConfig.GetConfig("Email", "authentication_required")[1]
|
||||
self.Username = self.DashboardConfig.GetConfig("Email", "username")[1]
|
||||
self.Password = self.DashboardConfig.GetConfig("Email", "email_password")[1]
|
||||
|
||||
self.SendFrom = self.DashboardConfig.GetConfig("Email", "send_from")[1]
|
||||
|
||||
def is_ready(self) -> bool:
|
||||
self.refresh_vals()
|
||||
|
||||
if self.AuthRequired:
|
||||
ready = all([
|
||||
self.Server, self.Port, self.Encryption,
|
||||
self.Username, self.Password, self.SendFrom
|
||||
])
|
||||
else:
|
||||
ready = all([
|
||||
self.Server, self.Port, self.Encryption, self.SendFrom
|
||||
])
|
||||
return ready
|
||||
|
||||
def send(self, receiver, subject, body, includeAttachment: bool = False, attachmentName: str = "") -> tuple[bool, str | None]:
|
||||
if not self.is_ready():
|
||||
return False, "SMTP not configured"
|
||||
|
||||
message = MIMEMultipart()
|
||||
message['Subject'] = subject
|
||||
message['From'] = self.SendFrom
|
||||
message["To"] = receiver
|
||||
message["Date"] = formatdate(localtime=True)
|
||||
message.attach(MIMEText(body, "plain"))
|
||||
|
||||
if includeAttachment and len(attachmentName) > 0:
|
||||
attachmentPath = os.path.join('./attachments', attachmentName)
|
||||
|
||||
if not os.path.exists(attachmentPath):
|
||||
return False, "Attachment does not exist"
|
||||
|
||||
attachment = MIMEBase("application", "octet-stream")
|
||||
with open(os.path.join('./attachments', attachmentName), 'rb') as f:
|
||||
attachment.set_payload(f.read())
|
||||
|
||||
encoders.encode_base64(attachment)
|
||||
attachment.add_header("Content-Disposition", f"attachment; filename= {attachmentName}",)
|
||||
message.attach(attachment)
|
||||
|
||||
smtp = None
|
||||
try:
|
||||
context = ssl.create_default_context()
|
||||
if self.Encryption == "IMPLICITTLS":
|
||||
smtp = smtplib.SMTP_SSL(self.Server, port=int(self.Port), context=context)
|
||||
else:
|
||||
smtp = smtplib.SMTP(self.Server, port=int(self.Port))
|
||||
smtp.ehlo()
|
||||
|
||||
# Configure SMTP encryption type
|
||||
if self.Encryption == "STARTTLS":
|
||||
smtp.starttls(context=context)
|
||||
smtp.ehlo()
|
||||
|
||||
# Log into the SMTP server if required
|
||||
if self.AuthRequired:
|
||||
smtp.login(self.Username, self.Password)
|
||||
|
||||
# Send the actual email from the SMTP object
|
||||
smtp.sendmail(self.SendFrom, receiver, message.as_string())
|
||||
return True, None
|
||||
|
||||
except Exception as e:
|
||||
return False, f"Send failed | Reason: {e}"
|
||||
|
||||
finally:
|
||||
if smtp:
|
||||
try:
|
||||
smtp.quit()
|
||||
except Exception:
|
||||
pass
|
||||
|
||||
@@ -2,7 +2,7 @@ import uuid
|
||||
|
||||
from pydantic import BaseModel, field_serializer
|
||||
import sqlalchemy as db
|
||||
from .ConnectionString import ConnectionString
|
||||
from .DatabaseConnection import ConnectionString
|
||||
|
||||
|
||||
class NewConfigurationTemplate(BaseModel):
|
||||
|
||||
@@ -10,8 +10,9 @@ from datetime import timedelta
|
||||
import jinja2
|
||||
import sqlalchemy as db
|
||||
from .PeerJob import PeerJob
|
||||
from flask import current_app
|
||||
from .PeerShareLink import PeerShareLink
|
||||
from .Utilities import GenerateWireguardPublicKey, ValidateIPAddressesWithRange, ValidateDNSAddress
|
||||
from .Utilities import GenerateWireguardPublicKey, CheckAddress, ValidateDNSAddress
|
||||
|
||||
|
||||
class Peer:
|
||||
@@ -34,6 +35,7 @@ class Peer:
|
||||
self.cumu_data = tableData["cumu_data"]
|
||||
self.mtu = tableData["mtu"]
|
||||
self.keepalive = tableData["keepalive"]
|
||||
self.notes = tableData.get("notes", "")
|
||||
self.remote_endpoint = tableData["remote_endpoint"]
|
||||
self.preshared_key = tableData["preshared_key"]
|
||||
self.jobs: list[PeerJob] = []
|
||||
@@ -49,62 +51,89 @@ class Peer:
|
||||
def __repr__(self):
|
||||
return str(self.toJson())
|
||||
|
||||
def updatePeer(self, name: str, private_key: str,
|
||||
def updatePeer(self, name: str,
|
||||
private_key: str,
|
||||
preshared_key: str,
|
||||
dns_addresses: str, allowed_ip: str, endpoint_allowed_ip: str, mtu: int,
|
||||
keepalive: int) -> tuple[bool, str] or tuple[bool, None]:
|
||||
dns_addresses: str,
|
||||
allowed_ip: str,
|
||||
endpoint_allowed_ip: str,
|
||||
mtu: int,
|
||||
keepalive: int,
|
||||
notes: str
|
||||
) -> tuple[bool, str | None]:
|
||||
|
||||
if not self.configuration.getStatus():
|
||||
self.configuration.toggleConfiguration()
|
||||
|
||||
existingAllowedIps = [item for row in list(
|
||||
map(lambda x: [q.strip() for q in x.split(',')],
|
||||
map(lambda y: y.allowed_ip,
|
||||
list(filter(lambda k: k.id != self.id, self.configuration.getPeersList()))))) for item in row]
|
||||
|
||||
if allowed_ip in existingAllowedIps:
|
||||
return False, "Allowed IP already taken by another peer"
|
||||
|
||||
if not ValidateIPAddressesWithRange(endpoint_allowed_ip):
|
||||
# Before we do any compute, let us check if the given endpoint allowed ip is valid at all
|
||||
if not CheckAddress(endpoint_allowed_ip):
|
||||
return False, f"Endpoint Allowed IPs format is incorrect"
|
||||
|
||||
if len(dns_addresses) > 0 and not ValidateDNSAddress(dns_addresses):
|
||||
return False, f"DNS format is incorrect"
|
||||
|
||||
if type(mtu) is str or mtu is None:
|
||||
|
||||
peers = []
|
||||
for peer in self.configuration.getPeersList():
|
||||
# Make sure to exclude your own data when updating since its not really relevant
|
||||
if peer.id == self.id:
|
||||
continue
|
||||
peers.append(peer)
|
||||
|
||||
used_allowed_ips = []
|
||||
for peer in peers:
|
||||
ips = peer.allowed_ip.split(',')
|
||||
ips = [ip.strip() for ip in ips]
|
||||
used_allowed_ips.append(ips)
|
||||
|
||||
if allowed_ip in used_allowed_ips:
|
||||
return False, "Allowed IP already taken by another peer"
|
||||
|
||||
if not ValidateDNSAddress(dns_addresses):
|
||||
return False, f"DNS IP-Address or FQDN is incorrect"
|
||||
|
||||
if isinstance(mtu, str):
|
||||
mtu = 0
|
||||
|
||||
if mtu < 0 or mtu > 1460:
|
||||
return False, "MTU format is not correct"
|
||||
|
||||
if type(keepalive) is str or keepalive is None:
|
||||
|
||||
if isinstance(keepalive, str):
|
||||
keepalive = 0
|
||||
|
||||
|
||||
if mtu not in range(0, 1461):
|
||||
return False, "MTU format is not correct"
|
||||
|
||||
if keepalive < 0:
|
||||
return False, "Persistent Keepalive format is not correct"
|
||||
|
||||
if len(private_key) > 0:
|
||||
pubKey = GenerateWireguardPublicKey(private_key)
|
||||
if not pubKey[0] or pubKey[1] != self.id:
|
||||
return False, "Private key does not match with the public key"
|
||||
try:
|
||||
rd = random.Random()
|
||||
uid = str(uuid.UUID(int=rd.getrandbits(128), version=4))
|
||||
pskExist = len(preshared_key) > 0
|
||||
|
||||
if pskExist:
|
||||
try:
|
||||
rand = random.Random()
|
||||
uid = str(uuid.UUID(int=rand.getrandbits(128), version=4))
|
||||
psk_exist = len(preshared_key) > 0
|
||||
|
||||
if psk_exist:
|
||||
with open(uid, "w+") as f:
|
||||
f.write(preshared_key)
|
||||
newAllowedIPs = allowed_ip.replace(" ", "")
|
||||
updateAllowedIp = subprocess.check_output(
|
||||
f"{self.configuration.Protocol} set {self.configuration.Name} peer {self.id} allowed-ips {newAllowedIPs} {f'preshared-key {uid}' if pskExist else 'preshared-key /dev/null'}",
|
||||
shell=True, stderr=subprocess.STDOUT)
|
||||
|
||||
if pskExist: os.remove(uid)
|
||||
newAllowedIPs = allowed_ip.replace(" ", "")
|
||||
if not CheckAddress(newAllowedIPs):
|
||||
return False, "Allowed IPs entry format is incorrect"
|
||||
|
||||
command = [self.configuration.Protocol, "set", self.configuration.Name, "peer", self.id, "allowed-ips", newAllowedIPs, "preshared-key", uid if psk_exist else "/dev/null"]
|
||||
updateAllowedIp = subprocess.check_output(command, stderr=subprocess.STDOUT)
|
||||
|
||||
if psk_exist: os.remove(uid)
|
||||
|
||||
if len(updateAllowedIp.decode().strip("\n")) != 0:
|
||||
return False, "Update peer failed when updating Allowed IPs"
|
||||
saveConfig = subprocess.check_output(f"{self.configuration.Protocol}-quick save {self.configuration.Name}",
|
||||
shell=True, stderr=subprocess.STDOUT)
|
||||
current_app.logger.error("Update peer failed when updating Allowed IPs")
|
||||
return False, "Internal server error"
|
||||
|
||||
command = [f"{self.configuration.Protocol}-quick", "save", self.configuration.Name]
|
||||
saveConfig = subprocess.check_output(command, stderr=subprocess.STDOUT)
|
||||
|
||||
if f"wg showconf {self.configuration.Name}" not in saveConfig.decode().strip('\n'):
|
||||
return False, "Update peer failed when saving the configuration"
|
||||
current_app.logger.error("Update peer failed when saving the configuration")
|
||||
return False, "Internal server error"
|
||||
|
||||
with self.configuration.engine.begin() as conn:
|
||||
conn.execute(
|
||||
self.configuration.peersTable.update().values({
|
||||
@@ -114,6 +143,7 @@ class Peer:
|
||||
"endpoint_allowed_ip": endpoint_allowed_ip,
|
||||
"mtu": mtu,
|
||||
"keepalive": keepalive,
|
||||
"notes": notes,
|
||||
"preshared_key": preshared_key
|
||||
}).where(
|
||||
self.configuration.peersTable.c.id == self.id
|
||||
@@ -121,7 +151,8 @@ class Peer:
|
||||
)
|
||||
return True, None
|
||||
except subprocess.CalledProcessError as exc:
|
||||
return False, exc.output.decode("UTF-8").strip()
|
||||
current_app.logger.error(f"Subprocess call failed:\n{exc.output.decode("UTF-8")}")
|
||||
return False, "Internal server error"
|
||||
|
||||
def downloadPeer(self) -> dict[str, str]:
|
||||
final = {
|
||||
@@ -132,17 +163,19 @@ class Peer:
|
||||
if len(filename) == 0:
|
||||
filename = "UntitledPeer"
|
||||
filename = "".join(filename.split(' '))
|
||||
filename = f"{filename}"
|
||||
illegal_filename = [".", ",", "/", "?", "<", ">", "\\", ":", "*", '|' '\"', "com1", "com2", "com3",
|
||||
"com4", "com5", "com6", "com7", "com8", "com9", "lpt1", "lpt2", "lpt3", "lpt4",
|
||||
"lpt5", "lpt6", "lpt7", "lpt8", "lpt9", "con", "nul", "prn"]
|
||||
for i in illegal_filename:
|
||||
filename = filename.replace(i, "")
|
||||
|
||||
# use previous filtering code if code below is insufficient or faulty
|
||||
filename = re.sub(r'[.,/?<>\\:*|"]', '', filename).rstrip(". ") # remove special characters
|
||||
|
||||
reserved_pattern = r"^(CON|PRN|AUX|NUL|COM[1-9]|LPT[1-9])(\..*)?$" # match com1-9, lpt1-9, con, nul, prn, aux, nul
|
||||
|
||||
if re.match(reserved_pattern, filename, re.IGNORECASE):
|
||||
filename = f"file_{filename}" # prepend "file_" if it matches
|
||||
|
||||
for i in filename:
|
||||
if re.match("^[a-zA-Z0-9_=+.-]$", i):
|
||||
final["fileName"] += i
|
||||
|
||||
|
||||
interfaceSection = {
|
||||
"PrivateKey": self.private_key,
|
||||
"Address": self.allowed_ip,
|
||||
@@ -155,7 +188,7 @@ class Peer:
|
||||
if self.configuration.configurationInfo.OverridePeerSettings.DNS else self.DNS
|
||||
)
|
||||
}
|
||||
|
||||
|
||||
if self.configuration.Protocol == "awg":
|
||||
interfaceSection.update({
|
||||
"Jc": self.configuration.Jc,
|
||||
@@ -163,12 +196,19 @@ class Peer:
|
||||
"Jmax": self.configuration.Jmax,
|
||||
"S1": self.configuration.S1,
|
||||
"S2": self.configuration.S2,
|
||||
"S3": self.configuration.S3,
|
||||
"S4": self.configuration.S4,
|
||||
"H1": self.configuration.H1,
|
||||
"H2": self.configuration.H2,
|
||||
"H3": self.configuration.H3,
|
||||
"H4": self.configuration.H4
|
||||
"H4": self.configuration.H4,
|
||||
"I1": self.configuration.I1,
|
||||
"I2": self.configuration.I2,
|
||||
"I3": self.configuration.I3,
|
||||
"I4": self.configuration.I4,
|
||||
"I5": self.configuration.I5
|
||||
})
|
||||
|
||||
|
||||
peerSection = {
|
||||
"PublicKey": self.configuration.PublicKey,
|
||||
"AllowedIPs": (
|
||||
@@ -192,7 +232,7 @@ class Peer:
|
||||
for (key, val) in combine[s]:
|
||||
if val is not None and ((type(val) is str and len(val) > 0) or (type(val) is int and val > 0)):
|
||||
final["file"] += f"{key} = {val}\n"
|
||||
|
||||
|
||||
final["file"] = jinja2.Template(final["file"]).render(configuration=self.configuration)
|
||||
|
||||
|
||||
@@ -351,4 +391,4 @@ class Peer:
|
||||
|
||||
hours, remainder = divmod(delta.total_seconds(), 3600)
|
||||
minutes, seconds = divmod(remainder, 60)
|
||||
return f"{int(hours):02}:{int(minutes):02}:{int(seconds):02}"
|
||||
return f"{int(hours):02}:{int(minutes):02}:{int(seconds):02}"
|
||||
|
||||
@@ -2,9 +2,13 @@
|
||||
Peer Job Logger
|
||||
"""
|
||||
import uuid
|
||||
from typing import Sequence
|
||||
|
||||
import sqlalchemy as db
|
||||
from flask import current_app
|
||||
from .ConnectionString import ConnectionString
|
||||
from sqlalchemy import RowMapping
|
||||
|
||||
from .DatabaseConnection import ConnectionString
|
||||
from .Log import Log
|
||||
|
||||
class PeerJobLogger:
|
||||
@@ -56,4 +60,42 @@ class PeerJobLogger:
|
||||
except Exception as e:
|
||||
current_app.logger.error(f"Getting Peer Job Log Error", e)
|
||||
return logs
|
||||
return logs
|
||||
return logs
|
||||
|
||||
def getFailingJobs(self) -> Sequence[RowMapping]:
|
||||
with self.engine.connect() as conn:
|
||||
table = conn.execute(
|
||||
db.select(
|
||||
self.jobLogTable.c.JobID
|
||||
).where(
|
||||
(db.or_(
|
||||
self.jobLogTable.c.Status == 'false',
|
||||
self.jobLogTable.c.Status == 0
|
||||
) if conn.dialect.name == 'sqlite' else self.jobLogTable.c.Status == 'false')
|
||||
).group_by(
|
||||
self.jobLogTable.c.JobID
|
||||
).having(
|
||||
db.func.count(
|
||||
self.jobLogTable.c.JobID
|
||||
) > 10
|
||||
)
|
||||
).mappings().fetchall()
|
||||
return table
|
||||
|
||||
def deleteLogs(self, LogID = None, JobID = None):
|
||||
with self.engine.begin() as conn:
|
||||
print(f"[WGDashboard] Deleted stale logs of JobID: {JobID}")
|
||||
conn.execute(
|
||||
self.jobLogTable.delete().where(
|
||||
db.and_(
|
||||
(self.jobLogTable.c.LogID == LogID if LogID is not None else True),
|
||||
(self.jobLogTable.c.JobID == JobID if JobID is not None else True),
|
||||
)
|
||||
)
|
||||
)
|
||||
|
||||
def vacuum(self):
|
||||
with self.engine.begin() as conn:
|
||||
if conn.dialect.name == 'sqlite':
|
||||
print("[WGDashboard] SQLite Vacuuming PeerJobLogs Database")
|
||||
conn.execute(db.text('VACUUM;'))
|
||||
@@ -1,7 +1,9 @@
|
||||
"""
|
||||
Peer Jobs
|
||||
"""
|
||||
from .ConnectionString import ConnectionString
|
||||
import sqlalchemy
|
||||
|
||||
from .DatabaseConnection import ConnectionString
|
||||
from .PeerJob import PeerJob
|
||||
from .PeerJobLogger import PeerJobLogger
|
||||
import sqlalchemy as db
|
||||
@@ -9,7 +11,7 @@ from datetime import datetime
|
||||
from flask import current_app
|
||||
|
||||
class PeerJobs:
|
||||
def __init__(self, DashboardConfig, WireguardConfigurations):
|
||||
def __init__(self, DashboardConfig, WireguardConfigurations, AllPeerShareLinks):
|
||||
self.Jobs: list[PeerJob] = []
|
||||
self.engine = db.create_engine(ConnectionString('wgdashboard_job'))
|
||||
self.metadata = db.MetaData()
|
||||
@@ -28,6 +30,8 @@ class PeerJobs:
|
||||
self.__getJobs()
|
||||
self.JobLogger: PeerJobLogger = PeerJobLogger(self, DashboardConfig)
|
||||
self.WireguardConfigurations = WireguardConfigurations
|
||||
self.AllPeerShareLinks = AllPeerShareLinks
|
||||
self.cleanJob(init=True)
|
||||
|
||||
def __getJobs(self):
|
||||
self.Jobs.clear()
|
||||
@@ -116,7 +120,7 @@ class PeerJobs:
|
||||
}
|
||||
).where(self.peerJobTable.columns.JobID == Job.JobID)
|
||||
)
|
||||
self.JobLogger.log(Job.JobID, Message=f"Job is removed due to being deleted or finshed.")
|
||||
self.JobLogger.log(Job.JobID, Message=f"Job is removed due to being deleted or finished.")
|
||||
self.__getJobs()
|
||||
self.WireguardConfigurations.get(Job.Configuration).searchPeer(Job.Peer)[1].getJobs()
|
||||
return True, None
|
||||
@@ -141,7 +145,7 @@ class PeerJobs:
|
||||
|
||||
|
||||
def runJob(self):
|
||||
current_app.logger.info("Running scheduled jobs")
|
||||
self.cleanJob()
|
||||
needToDelete = []
|
||||
self.__getJobs()
|
||||
for job in self.Jobs:
|
||||
@@ -162,7 +166,7 @@ class PeerJobs:
|
||||
if job.Action == "restrict":
|
||||
s, msg = c.restrictPeers([fp.id])
|
||||
elif job.Action == "delete":
|
||||
s, msg = c.deletePeers([fp.id])
|
||||
s, msg = c.deletePeers([fp.id], self, self.AllPeerShareLinks)
|
||||
elif job.Action == "reset_total_data_usage":
|
||||
s = fp.resetDataUsage("total")
|
||||
c.restrictPeers([fp.id])
|
||||
@@ -171,25 +175,41 @@ class PeerJobs:
|
||||
self.JobLogger.log(job.JobID, s,
|
||||
f"Peer {fp.id} from {c.Name} is successfully {job.Action}ed."
|
||||
)
|
||||
current_app.logger.info(f"Peer {fp.id} from {c.Name} is successfully {job.Action}ed.")
|
||||
needToDelete.append(job)
|
||||
else:
|
||||
current_app.logger.info(f"Peer {fp.id} from {c.Name} is failed {job.Action}ed.")
|
||||
self.JobLogger.log(job.JobID, s,
|
||||
f"Peer {fp.id} from {c.Name} failed {job.Action}ed."
|
||||
)
|
||||
else:
|
||||
current_app.logger.warning(f"Somehow can't find this peer {job.Peer} from {c.Name} failed {job.Action}ed.")
|
||||
self.JobLogger.log(job.JobID, False,
|
||||
f"Somehow can't find this peer {job.Peer} from {c.Name} failed {job.Action}ed."
|
||||
)
|
||||
else:
|
||||
current_app.logger.warning(f"Somehow can't find this peer {job.Peer} from {c.Name} failed {job.Action}ed.")
|
||||
self.JobLogger.log(job.JobID, False,
|
||||
f"Somehow can't find this peer {job.Peer} from {job.Configuration} failed {job.Action}ed."
|
||||
)
|
||||
for j in needToDelete:
|
||||
self.deleteJob(j)
|
||||
|
||||
def cleanJob(self, init = False):
|
||||
failingJobs = self.JobLogger.getFailingJobs()
|
||||
with self.engine.begin() as conn:
|
||||
for job in failingJobs:
|
||||
conn.execute(
|
||||
self.peerJobTable.update().values(
|
||||
{
|
||||
"ExpireDate": datetime.now()
|
||||
}
|
||||
).where(self.peerJobTable.columns.JobID == job.get('JobID'))
|
||||
)
|
||||
self.JobLogger.deleteLogs(JobID=job.get('JobID'))
|
||||
self.JobLogger.log(job.get('JobID'), Message=f"Job is removed due to being stale.")
|
||||
|
||||
with self.engine.connect() as conn:
|
||||
if init and conn.dialect.name == 'sqlite':
|
||||
print("[WGDashboard] SQLite Vacuuming PeerJobs Database")
|
||||
self.JobLogger.vacuum()
|
||||
conn.execute(sqlalchemy.text('VACUUM;'))
|
||||
|
||||
def __runJob_Compare(self, x: float | datetime, y: float | datetime, operator: str):
|
||||
if operator == "eq":
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
from .ConnectionString import ConnectionString
|
||||
from .DatabaseConnection import ConnectionString
|
||||
from .PeerShareLink import PeerShareLink
|
||||
import sqlalchemy as db
|
||||
from datetime import datetime
|
||||
|
||||
@@ -160,18 +160,41 @@ class Processes:
|
||||
self.CPU_Top_10_Processes: list[Process] = []
|
||||
self.Memory_Top_10_Processes: list[Process] = []
|
||||
def getData(self):
|
||||
while True:
|
||||
try:
|
||||
processes = list(psutil.process_iter())
|
||||
self.CPU_Top_10_Processes = sorted(
|
||||
list(map(lambda x : Process(x.name(), " ".join(x.cmdline()), x.pid, x.cpu_percent()), processes)),
|
||||
key=lambda x : x.percent, reverse=True)[:20]
|
||||
self.Memory_Top_10_Processes = sorted(
|
||||
list(map(lambda x : Process(x.name(), " ".join(x.cmdline()), x.pid, x.memory_percent()), processes)),
|
||||
key=lambda x : x.percent, reverse=True)[:20]
|
||||
break
|
||||
except Exception as e:
|
||||
current_app.logger.error("Get processes error", e)
|
||||
try:
|
||||
processes = list(psutil.process_iter())
|
||||
|
||||
cpu_processes = []
|
||||
memory_processes = []
|
||||
|
||||
for proc in processes:
|
||||
try:
|
||||
name = proc.name()
|
||||
cmdline = " ".join(proc.cmdline())
|
||||
pid = proc.pid
|
||||
cpu_percent = proc.cpu_percent()
|
||||
mem_percent = proc.memory_percent()
|
||||
|
||||
# Create Process object for CPU and memory tracking
|
||||
cpu_process = Process(name, cmdline, pid, cpu_percent)
|
||||
mem_process = Process(name, cmdline, pid, mem_percent)
|
||||
|
||||
cpu_processes.append(cpu_process)
|
||||
memory_processes.append(mem_process)
|
||||
|
||||
except (psutil.NoSuchProcess, psutil.AccessDenied, psutil.ZombieProcess):
|
||||
# Skip processes we can’t access or that no longer exist
|
||||
continue
|
||||
|
||||
# Sort by CPU and memory usage (descending order)
|
||||
cpu_sorted = sorted(cpu_processes, key=lambda p: p.percent, reverse=True)
|
||||
mem_sorted = sorted(memory_processes, key=lambda p: p.percent, reverse=True)
|
||||
|
||||
# Get top 20 processes for each
|
||||
self.CPU_Top_10_Processes = cpu_sorted[:20]
|
||||
self.Memory_Top_10_Processes = mem_sorted[:20]
|
||||
|
||||
except Exception as e:
|
||||
current_app.logger.error("Get processes error", e)
|
||||
|
||||
def toJson(self):
|
||||
self.getData()
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import re, ipaddress
|
||||
import subprocess
|
||||
|
||||
import sqlalchemy
|
||||
|
||||
def RegexMatch(regex, text) -> bool:
|
||||
"""
|
||||
@@ -18,10 +18,18 @@ def GetRemoteEndpoint() -> str:
|
||||
@return:
|
||||
"""
|
||||
import socket
|
||||
with socket.socket(socket.AF_INET, socket.SOCK_DGRAM) as s:
|
||||
s.connect(("1.1.1.1", 80)) # Connecting to a public IP
|
||||
try:
|
||||
with socket.socket(socket.AF_INET, socket.SOCK_DGRAM) as s:
|
||||
s.connect(("1.1.1.1", 80)) # Connecting to a public IP
|
||||
wgd_remote_endpoint = s.getsockname()[0]
|
||||
return str(wgd_remote_endpoint)
|
||||
except (socket.error, OSError):
|
||||
pass
|
||||
try:
|
||||
return socket.gethostbyname(socket.gethostname())
|
||||
except (socket.error, OSError):
|
||||
pass
|
||||
return "127.0.0.1"
|
||||
|
||||
|
||||
def StringToBoolean(value: str):
|
||||
@@ -33,31 +41,35 @@ def StringToBoolean(value: str):
|
||||
return (value.strip().replace(" ", "").lower() in
|
||||
("yes", "true", "t", "1", 1))
|
||||
|
||||
def ValidateIPAddressesWithRange(ips: str) -> bool:
|
||||
s = ips.replace(" ", "").split(",")
|
||||
for ip in s:
|
||||
def CheckAddress(ips_str: str) -> bool:
|
||||
if len(ips_str) == 0:
|
||||
return False
|
||||
|
||||
for ip in ips_str.split(','):
|
||||
stripped_ip = ip.strip()
|
||||
try:
|
||||
ipaddress.ip_network(ip)
|
||||
except ValueError as e:
|
||||
# Verify the IP-address, with the strict flag as false also allows for /32 and /128
|
||||
ipaddress.ip_network(stripped_ip, strict=False)
|
||||
except ValueError:
|
||||
return False
|
||||
return True
|
||||
|
||||
def ValidateIPAddresses(ips) -> bool:
|
||||
s = ips.replace(" ", "").split(",")
|
||||
for ip in s:
|
||||
try:
|
||||
ipaddress.ip_address(ip)
|
||||
except ValueError as e:
|
||||
return False
|
||||
return True
|
||||
def CheckPeerKey(peer_key: str) -> bool:
|
||||
return re.match(r"^[A-Za-z0-9+/]{43}=$", peer_key)
|
||||
|
||||
def ValidateDNSAddress(addresses_str: str) -> tuple[bool, str | None]:
|
||||
if len(addresses_str) == 0:
|
||||
return False, "Got an empty list/string to check for valid DNS-addresses"
|
||||
|
||||
addresses = addresses_str.split(',')
|
||||
for address in addresses:
|
||||
stripped_address = address.strip()
|
||||
|
||||
if not CheckAddress(stripped_address) and not RegexMatch(r"(?:[a-z0-9](?:[a-z0-9-]{0,61}[a-z0-9])?\.)+[a-z][a-z]{0,61}[a-z]", stripped_address):
|
||||
return False, f"{stripped_address} does not appear to be a valid IP-address or FQDN"
|
||||
|
||||
return True, None
|
||||
|
||||
def ValidateDNSAddress(addresses) -> tuple[bool, str]:
|
||||
s = addresses.replace(" ", "").split(",")
|
||||
for address in s:
|
||||
if not ValidateIPAddresses(address) and not RegexMatch(
|
||||
r"(?:[a-z0-9](?:[a-z0-9-]{0,61}[a-z0-9])?\.)+[a-z][a-z]{0,61}[a-z]", address):
|
||||
return False, f"{address} does not appear to be an valid DNS address"
|
||||
return True, ""
|
||||
|
||||
def ValidateEndpointAllowedIPs(IPs) -> tuple[bool, str] | tuple[bool, None]:
|
||||
ips = IPs.replace(" ", "").split(",")
|
||||
@@ -101,4 +113,4 @@ def ValidatePasswordStrength(password: str) -> tuple[bool, str] | tuple[bool, No
|
||||
if not re.search(r'[$&+,:;=?@#|\'<>.\-^*()%!~_-]', password):
|
||||
return False, "Password must contain at least 1 special character from $&+,:;=?@#|'<>.-^*()%!~_-"
|
||||
|
||||
return True, None
|
||||
return True, None
|
||||
|
||||
@@ -10,13 +10,18 @@ from datetime import datetime, timedelta
|
||||
from itertools import islice
|
||||
from flask import current_app
|
||||
|
||||
from .ConnectionString import ConnectionString
|
||||
from .DatabaseConnection import ConnectionString
|
||||
from .DashboardConfig import DashboardConfig
|
||||
from .Peer import Peer
|
||||
from .PeerJobs import PeerJobs
|
||||
from .PeerShareLinks import PeerShareLinks
|
||||
from .Utilities import StringToBoolean, GenerateWireguardPublicKey, RegexMatch, ValidateDNSAddress, \
|
||||
ValidateEndpointAllowedIPs
|
||||
from .Utilities import StringToBoolean, \
|
||||
GenerateWireguardPublicKey, \
|
||||
RegexMatch, \
|
||||
ValidateDNSAddress, \
|
||||
ValidateEndpointAllowedIPs, \
|
||||
CheckAddress, \
|
||||
CheckPeerKey
|
||||
from .WireguardConfigurationInfo import WireguardConfigurationInfo, PeerGroupsClass
|
||||
from .DashboardWebHooks import DashboardWebHooks
|
||||
|
||||
@@ -61,13 +66,14 @@ class WireguardConfiguration:
|
||||
self.Protocol = "wg" if wg else "awg"
|
||||
self.AllPeerJobs = AllPeerJobs
|
||||
self.DashboardConfig = DashboardConfig
|
||||
self.DashboardConfig.EnsureDatabaseIntegrity({self.Name: self})
|
||||
self.AllPeerShareLinks = AllPeerShareLinks
|
||||
self.DashboardWebHooks = DashboardWebHooks
|
||||
self.configPath = os.path.join(self.__getProtocolPath(), f'{self.Name}.conf')
|
||||
self.engine: sqlalchemy.Engine = sqlalchemy.create_engine(ConnectionString("wgdashboard"))
|
||||
self.metadata: sqlalchemy.MetaData = sqlalchemy.MetaData()
|
||||
self.dbType = self.DashboardConfig.GetConfig("Database", "type")[1]
|
||||
|
||||
|
||||
if name is not None:
|
||||
if data is not None and "Backup" in data.keys():
|
||||
db = self.__importDatabase(
|
||||
@@ -109,10 +115,17 @@ class WireguardConfiguration:
|
||||
self.__parser["Interface"]["Jmax"] = self.Jmax
|
||||
self.__parser["Interface"]["S1"] = self.S1
|
||||
self.__parser["Interface"]["S2"] = self.S2
|
||||
self.__parser["Interface"]["S3"] = self.S3
|
||||
self.__parser["Interface"]["S4"] = self.S4
|
||||
self.__parser["Interface"]["H1"] = self.H1
|
||||
self.__parser["Interface"]["H2"] = self.H2
|
||||
self.__parser["Interface"]["H3"] = self.H3
|
||||
self.__parser["Interface"]["H4"] = self.H4
|
||||
self.__parser["Interface"]["I1"] = self.I1
|
||||
self.__parser["Interface"]["I2"] = self.I2
|
||||
self.__parser["Interface"]["I3"] = self.I3
|
||||
self.__parser["Interface"]["I4"] = self.I4
|
||||
self.__parser["Interface"]["I5"] = self.I5
|
||||
|
||||
if "Backup" not in data.keys():
|
||||
self.createDatabase()
|
||||
@@ -127,8 +140,11 @@ class WireguardConfiguration:
|
||||
current_app.logger.info(f"Initialized Configuration: {name}")
|
||||
self.__dumpDatabase()
|
||||
if self.getAutostartStatus() and not self.getStatus() and startup:
|
||||
self.toggleConfiguration()
|
||||
current_app.logger.info(f"Autostart Configuration: {name}")
|
||||
status, ext = self.toggleConfiguration()
|
||||
if not status:
|
||||
current_app.logger.error(f"Failed to autostart configuration: {name}. Reason: {ext}")
|
||||
else:
|
||||
current_app.logger.info(f"Autostart Configuration: {name}")
|
||||
|
||||
self.configurationInfo: WireguardConfigurationInfo | None = None
|
||||
configurationInfoJson = self.readConfigurationInfo()
|
||||
@@ -140,7 +156,6 @@ class WireguardConfiguration:
|
||||
|
||||
if self.Status:
|
||||
self.addAutostart()
|
||||
|
||||
|
||||
def __getProtocolPath(self) -> str:
|
||||
_, path = self.DashboardConfig.GetConfig("Server", "wg_conf_path") if self.Protocol == "wg" \
|
||||
@@ -232,54 +247,50 @@ class WireguardConfiguration:
|
||||
return True
|
||||
|
||||
def createDatabase(self, dbName = None):
|
||||
def generate_column_obj():
|
||||
return [
|
||||
sqlalchemy.Column('id', sqlalchemy.String(255), nullable=False, primary_key=True),
|
||||
sqlalchemy.Column('private_key', sqlalchemy.String(255)),
|
||||
sqlalchemy.Column('DNS', sqlalchemy.Text),
|
||||
sqlalchemy.Column('endpoint_allowed_ip', sqlalchemy.Text),
|
||||
sqlalchemy.Column('name', sqlalchemy.Text),
|
||||
sqlalchemy.Column('total_receive', sqlalchemy.Float),
|
||||
sqlalchemy.Column('total_sent', sqlalchemy.Float),
|
||||
sqlalchemy.Column('total_data', sqlalchemy.Float),
|
||||
sqlalchemy.Column('endpoint', sqlalchemy.String(255)),
|
||||
sqlalchemy.Column('status', sqlalchemy.String(255)),
|
||||
sqlalchemy.Column('latest_handshake', sqlalchemy.String(255)),
|
||||
sqlalchemy.Column('allowed_ip', sqlalchemy.String(255)),
|
||||
sqlalchemy.Column('cumu_receive', sqlalchemy.Float),
|
||||
sqlalchemy.Column('cumu_sent', sqlalchemy.Float),
|
||||
sqlalchemy.Column('cumu_data', sqlalchemy.Float),
|
||||
sqlalchemy.Column('mtu', sqlalchemy.Integer),
|
||||
sqlalchemy.Column('keepalive', sqlalchemy.Integer),
|
||||
sqlalchemy.Column('notes', sqlalchemy.Text),
|
||||
sqlalchemy.Column('remote_endpoint', sqlalchemy.String(255)),
|
||||
sqlalchemy.Column('preshared_key', sqlalchemy.String(255))
|
||||
]
|
||||
|
||||
if dbName is None:
|
||||
dbName = self.Name
|
||||
|
||||
self.peersTable = sqlalchemy.Table(
|
||||
dbName, self.metadata,
|
||||
sqlalchemy.Column('id', sqlalchemy.String(255), nullable=False, primary_key=True),
|
||||
sqlalchemy.Column('private_key', sqlalchemy.String(255)),
|
||||
sqlalchemy.Column('DNS', sqlalchemy.Text),
|
||||
sqlalchemy.Column('endpoint_allowed_ip', sqlalchemy.Text),
|
||||
sqlalchemy.Column('name', sqlalchemy.Text),
|
||||
sqlalchemy.Column('total_receive', sqlalchemy.Float),
|
||||
sqlalchemy.Column('total_sent', sqlalchemy.Float),
|
||||
sqlalchemy.Column('total_data', sqlalchemy.Float),
|
||||
sqlalchemy.Column('endpoint', sqlalchemy.String(255)),
|
||||
sqlalchemy.Column('status', sqlalchemy.String(255)),
|
||||
sqlalchemy.Column('latest_handshake', sqlalchemy.String(255)),
|
||||
sqlalchemy.Column('allowed_ip', sqlalchemy.String(255)),
|
||||
sqlalchemy.Column('cumu_receive', sqlalchemy.Float),
|
||||
sqlalchemy.Column('cumu_sent', sqlalchemy.Float),
|
||||
sqlalchemy.Column('cumu_data', sqlalchemy.Float),
|
||||
sqlalchemy.Column('mtu', sqlalchemy.Integer),
|
||||
sqlalchemy.Column('keepalive', sqlalchemy.Integer),
|
||||
sqlalchemy.Column('remote_endpoint', sqlalchemy.String(255)),
|
||||
sqlalchemy.Column('preshared_key', sqlalchemy.String(255)),
|
||||
extend_existing=True
|
||||
f'{dbName}', self.metadata, *generate_column_obj(), extend_existing=True
|
||||
)
|
||||
|
||||
self.peersRestrictedTable = sqlalchemy.Table(
|
||||
f'{dbName}_restrict_access', self.metadata,
|
||||
sqlalchemy.Column('id', sqlalchemy.String(255), nullable=False, primary_key=True),
|
||||
sqlalchemy.Column('private_key', sqlalchemy.String(255)),
|
||||
sqlalchemy.Column('DNS', sqlalchemy.Text),
|
||||
sqlalchemy.Column('endpoint_allowed_ip', sqlalchemy.Text),
|
||||
sqlalchemy.Column('name', sqlalchemy.Text),
|
||||
sqlalchemy.Column('total_receive', sqlalchemy.Float),
|
||||
sqlalchemy.Column('total_sent', sqlalchemy.Float),
|
||||
sqlalchemy.Column('total_data', sqlalchemy.Float),
|
||||
sqlalchemy.Column('endpoint', sqlalchemy.String(255)),
|
||||
sqlalchemy.Column('status', sqlalchemy.String(255)),
|
||||
sqlalchemy.Column('latest_handshake', sqlalchemy.String(255)),
|
||||
sqlalchemy.Column('allowed_ip', sqlalchemy.String(255)),
|
||||
sqlalchemy.Column('cumu_receive', sqlalchemy.Float),
|
||||
sqlalchemy.Column('cumu_sent', sqlalchemy.Float),
|
||||
sqlalchemy.Column('cumu_data', sqlalchemy.Float),
|
||||
sqlalchemy.Column('mtu', sqlalchemy.Integer),
|
||||
sqlalchemy.Column('keepalive', sqlalchemy.Integer),
|
||||
sqlalchemy.Column('remote_endpoint', sqlalchemy.String(255)),
|
||||
sqlalchemy.Column('preshared_key', sqlalchemy.String(255)),
|
||||
extend_existing=True
|
||||
f'{dbName}_restrict_access', self.metadata, *generate_column_obj(), extend_existing=True
|
||||
)
|
||||
|
||||
self.peersDeletedTable = sqlalchemy.Table(
|
||||
f'{dbName}_deleted', self.metadata, *generate_column_obj(), extend_existing=True
|
||||
)
|
||||
|
||||
if self.DashboardConfig.GetConfig("Database", "type")[1] == 'sqlite':
|
||||
time_col_type = sqlalchemy.DATETIME
|
||||
else:
|
||||
time_col_type = sqlalchemy.TIMESTAMP
|
||||
|
||||
self.peersTransferTable = sqlalchemy.Table(
|
||||
f'{dbName}_transfer', self.metadata,
|
||||
sqlalchemy.Column('id', sqlalchemy.String(255), nullable=False),
|
||||
@@ -289,8 +300,7 @@ class WireguardConfiguration:
|
||||
sqlalchemy.Column('cumu_receive', sqlalchemy.Float),
|
||||
sqlalchemy.Column('cumu_sent', sqlalchemy.Float),
|
||||
sqlalchemy.Column('cumu_data', sqlalchemy.Float),
|
||||
sqlalchemy.Column('time', (sqlalchemy.DATETIME if self.DashboardConfig.GetConfig("Database", "type")[1] == 'sqlite' else sqlalchemy.TIMESTAMP),
|
||||
server_default=sqlalchemy.func.now()),
|
||||
sqlalchemy.Column('time', time_col_type, server_default=sqlalchemy.func.now()),
|
||||
extend_existing=True
|
||||
)
|
||||
|
||||
@@ -298,34 +308,9 @@ class WireguardConfiguration:
|
||||
f'{dbName}_history_endpoint', self.metadata,
|
||||
sqlalchemy.Column('id', sqlalchemy.String(255), nullable=False),
|
||||
sqlalchemy.Column('endpoint', sqlalchemy.String(255), nullable=False),
|
||||
sqlalchemy.Column('time',
|
||||
(sqlalchemy.DATETIME if self.DashboardConfig.GetConfig("Database", "type")[1] == 'sqlite' else sqlalchemy.TIMESTAMP)),
|
||||
extend_existing=True
|
||||
sqlalchemy.Column('time', time_col_type)
|
||||
)
|
||||
|
||||
self.peersDeletedTable = sqlalchemy.Table(
|
||||
f'{dbName}_deleted', self.metadata,
|
||||
sqlalchemy.Column('id', sqlalchemy.String(255), nullable=False, primary_key=True),
|
||||
sqlalchemy.Column('private_key', sqlalchemy.String(255)),
|
||||
sqlalchemy.Column('DNS', sqlalchemy.Text),
|
||||
sqlalchemy.Column('endpoint_allowed_ip', sqlalchemy.Text),
|
||||
sqlalchemy.Column('name', sqlalchemy.Text),
|
||||
sqlalchemy.Column('total_receive', sqlalchemy.Float),
|
||||
sqlalchemy.Column('total_sent', sqlalchemy.Float),
|
||||
sqlalchemy.Column('total_data', sqlalchemy.Float),
|
||||
sqlalchemy.Column('endpoint', sqlalchemy.String(255)),
|
||||
sqlalchemy.Column('status', sqlalchemy.String(255)),
|
||||
sqlalchemy.Column('latest_handshake', sqlalchemy.String(255)),
|
||||
sqlalchemy.Column('allowed_ip', sqlalchemy.String(255)),
|
||||
sqlalchemy.Column('cumu_receive', sqlalchemy.Float),
|
||||
sqlalchemy.Column('cumu_sent', sqlalchemy.Float),
|
||||
sqlalchemy.Column('cumu_data', sqlalchemy.Float),
|
||||
sqlalchemy.Column('mtu', sqlalchemy.Integer),
|
||||
sqlalchemy.Column('keepalive', sqlalchemy.Integer),
|
||||
sqlalchemy.Column('remote_endpoint', sqlalchemy.String(255)),
|
||||
sqlalchemy.Column('preshared_key', sqlalchemy.String(255)),
|
||||
extend_existing=True
|
||||
)
|
||||
self.infoTable = sqlalchemy.Table(
|
||||
'ConfigurationsInfo', self.metadata,
|
||||
sqlalchemy.Column('ID', sqlalchemy.String(255), primary_key=True),
|
||||
@@ -395,9 +380,7 @@ class WireguardConfiguration:
|
||||
return changed
|
||||
|
||||
def getPeers(self):
|
||||
tmpList = []
|
||||
current_app.logger.info(f"Refreshing {self.Name} peer list")
|
||||
|
||||
tmpList = []
|
||||
if self.configurationFileChanged():
|
||||
with open(self.configPath, 'r') as configFile:
|
||||
p = []
|
||||
@@ -406,6 +389,7 @@ class WireguardConfiguration:
|
||||
try:
|
||||
if "[Peer]" not in content:
|
||||
current_app.logger.info(f"{self.Name} config has no [Peer] section")
|
||||
self.Peers = []
|
||||
return
|
||||
|
||||
peerStarts = content.index("[Peer]")
|
||||
@@ -441,8 +425,7 @@ class WireguardConfiguration:
|
||||
"id": i['PublicKey'],
|
||||
"private_key": "",
|
||||
"DNS": self.DashboardConfig.GetConfig("Peers", "peer_global_DNS")[1],
|
||||
"endpoint_allowed_ip": self.DashboardConfig.GetConfig("Peers", "peer_endpoint_allowed_ip")[
|
||||
1],
|
||||
"endpoint_allowed_ip": self.DashboardConfig.GetConfig("Peers", "peer_endpoint_allowed_ip")[1],
|
||||
"name": i.get("name"),
|
||||
"total_receive": 0,
|
||||
"total_sent": 0,
|
||||
@@ -456,6 +439,7 @@ class WireguardConfiguration:
|
||||
"cumu_data": 0,
|
||||
"mtu": self.DashboardConfig.GetConfig("Peers", "peer_mtu")[1] if len(self.DashboardConfig.GetConfig("Peers", "peer_mtu")[1]) > 0 else None,
|
||||
"keepalive": self.DashboardConfig.GetConfig("Peers", "peer_keep_alive")[1] if len(self.DashboardConfig.GetConfig("Peers", "peer_keep_alive")[1]) > 0 else None,
|
||||
"notes": "",
|
||||
"remote_endpoint": self.DashboardConfig.GetConfig("Peers", "remote_endpoint")[1],
|
||||
"preshared_key": i["PresharedKey"] if "PresharedKey" in i.keys() else ""
|
||||
}
|
||||
@@ -503,15 +487,24 @@ class WireguardConfiguration:
|
||||
with self.engine.begin() as conn:
|
||||
for tempPeer in self.Peers:
|
||||
if tempPeer.status == "running":
|
||||
endpoint = tempPeer.endpoint.rsplit(":", 1)
|
||||
endpoint = tempPeer.endpoint.rsplit(":", 1)
|
||||
if len(endpoint) == 2 and len(endpoint[0]) > 0:
|
||||
conn.execute(
|
||||
self.peersHistoryEndpointTable.insert().values({
|
||||
"id": tempPeer.id,
|
||||
"endpoint": endpoint[0],
|
||||
"time": datetime.now()
|
||||
})
|
||||
)
|
||||
exist = conn.execute(
|
||||
self.peersHistoryEndpointTable.select().where(
|
||||
sqlalchemy.and_(
|
||||
self.peersHistoryEndpointTable.c.id == tempPeer.id,
|
||||
self.peersHistoryEndpointTable.c.endpoint == endpoint[0]
|
||||
)
|
||||
)
|
||||
).mappings().fetchone()
|
||||
if not exist:
|
||||
conn.execute(
|
||||
self.peersHistoryEndpointTable.insert().values({
|
||||
"id": tempPeer.id,
|
||||
"endpoint": endpoint[0],
|
||||
"time": datetime.now()
|
||||
})
|
||||
)
|
||||
|
||||
def addPeers(self, peers: list) -> tuple[bool, list, str]:
|
||||
result = {
|
||||
@@ -519,6 +512,15 @@ class WireguardConfiguration:
|
||||
"peers": []
|
||||
}
|
||||
try:
|
||||
cleanedAllowedIPs = {}
|
||||
for p in peers:
|
||||
newAllowedIPs = p['allowed_ip'].replace(" ", "")
|
||||
if not CheckAddress(newAllowedIPs):
|
||||
return False, [], "Allowed IPs entry format is incorrect"
|
||||
if not CheckPeerKey(p["id"]):
|
||||
return False, [], "Peer key format is incorrect"
|
||||
cleanedAllowedIPs[p["id"]] = newAllowedIPs
|
||||
|
||||
with self.engine.begin() as conn:
|
||||
for i in peers:
|
||||
newPeer = {
|
||||
@@ -539,6 +541,7 @@ class WireguardConfiguration:
|
||||
"cumu_data": 0,
|
||||
"mtu": i['mtu'],
|
||||
"keepalive": i['keepalive'],
|
||||
"notes": i.get("notes", ""),
|
||||
"remote_endpoint": self.DashboardConfig.GetConfig("Peers", "remote_endpoint")[1],
|
||||
"preshared_key": i["preshared_key"]
|
||||
}
|
||||
@@ -553,12 +556,15 @@ class WireguardConfiguration:
|
||||
with open(uid, "w+") as f:
|
||||
f.write(p['preshared_key'])
|
||||
|
||||
subprocess.check_output(f"{self.Protocol} set {self.Name} peer {p['id']} allowed-ips {p['allowed_ip'].replace(' ', '')}{f' preshared-key {uid}' if presharedKeyExist else ''}",
|
||||
shell=True, stderr=subprocess.STDOUT)
|
||||
command = [self.Protocol, "set", self.Name, "peer", p['id'], "allowed-ips", cleanedAllowedIPs[p["id"]], "preshared-key", uid if presharedKeyExist else "/dev/null"]
|
||||
subprocess.check_output(command, stderr=subprocess.STDOUT)
|
||||
|
||||
if presharedKeyExist:
|
||||
os.remove(uid)
|
||||
subprocess.check_output(
|
||||
f"{self.Protocol}-quick save {self.Name}", shell=True, stderr=subprocess.STDOUT)
|
||||
|
||||
command = [f"{self.Protocol}-quick", "save", self.Name]
|
||||
subprocess.check_output(command, stderr=subprocess.STDOUT)
|
||||
|
||||
self.getPeers()
|
||||
for p in peers:
|
||||
p = self.searchPeer(p['id'])
|
||||
@@ -570,7 +576,7 @@ class WireguardConfiguration:
|
||||
})
|
||||
except Exception as e:
|
||||
current_app.logger.error("Add peers error", e)
|
||||
return False, [], str(e)
|
||||
return False, [], "Internal server error"
|
||||
return True, result['peers'], ""
|
||||
|
||||
def searchPeer(self, publicKey):
|
||||
@@ -608,8 +614,16 @@ class WireguardConfiguration:
|
||||
with open(uid, "w+") as f:
|
||||
f.write(restrictedPeer['preshared_key'])
|
||||
|
||||
subprocess.check_output(f"{self.Protocol} set {self.Name} peer {restrictedPeer['id']} allowed-ips {restrictedPeer['allowed_ip'].replace(' ', '')}{f' preshared-key {uid}' if presharedKeyExist else ''}",
|
||||
shell=True, stderr=subprocess.STDOUT)
|
||||
newAllowedIPs = restrictedPeer['allowed_ip'].replace(" ", "")
|
||||
if not CheckAddress(newAllowedIPs):
|
||||
return False, "Allowed IPs entry format is incorrect"
|
||||
|
||||
if not CheckPeerKey(restrictedPeer["id"]):
|
||||
return False, "Peer key format is incorrect"
|
||||
|
||||
command = [self.Protocol, "set", self.Name, "peer", restrictedPeer["id"], "allowed-ips", newAllowedIPs, "preshared-key", uid if presharedKeyExist else "/dev/null"]
|
||||
subprocess.check_output(command, stderr=subprocess.STDOUT)
|
||||
|
||||
if presharedKeyExist: os.remove(uid)
|
||||
else:
|
||||
return False, "Failed to allow access of peer " + i
|
||||
@@ -629,8 +643,9 @@ class WireguardConfiguration:
|
||||
found, pf = self.searchPeer(p)
|
||||
if found:
|
||||
try:
|
||||
subprocess.check_output(f"{self.Protocol} set {self.Name} peer {pf.id} remove",
|
||||
shell=True, stderr=subprocess.STDOUT)
|
||||
command = [self.Protocol, "set", self.Name, "peer", pf.id, "remove"]
|
||||
subprocess.check_output(command, stderr=subprocess.STDOUT)
|
||||
|
||||
conn.execute(
|
||||
self.peersRestrictedTable.insert().from_select(
|
||||
[c.name for c in self.peersTable.columns],
|
||||
@@ -658,9 +673,8 @@ class WireguardConfiguration:
|
||||
|
||||
if not self.__wgSave():
|
||||
return False, "Failed to save configuration through WireGuard"
|
||||
|
||||
self.getRestrictedPeers()
|
||||
self.getPeers()
|
||||
|
||||
if numOfRestrictedPeers == len(listOfPublicKeys):
|
||||
return True, f"Restricted {numOfRestrictedPeers} peer(s)"
|
||||
return False, f"Restricted {numOfRestrictedPeers} peer(s) successfully. Failed to restrict {numOfFailedToRestrictPeers} peer(s)"
|
||||
@@ -712,17 +726,20 @@ class WireguardConfiguration:
|
||||
|
||||
def __wgSave(self) -> tuple[bool, str] | tuple[bool, None]:
|
||||
try:
|
||||
subprocess.check_output(f"{self.Protocol}-quick save {self.Name}", shell=True, stderr=subprocess.STDOUT)
|
||||
command = [f"{self.Protocol}-quick", "save", self.Name]
|
||||
subprocess.check_output(command, stderr=subprocess.STDOUT)
|
||||
|
||||
return True, None
|
||||
except subprocess.CalledProcessError as e:
|
||||
return False, str(e)
|
||||
current_app.logger.error(f"Failed to process command:\n{str(e)}")
|
||||
return False, "Internal server error"
|
||||
|
||||
def getPeersLatestHandshake(self):
|
||||
if not self.getStatus():
|
||||
self.toggleConfiguration()
|
||||
try:
|
||||
latestHandshake = subprocess.check_output(f"{self.Protocol} show {self.Name} latest-handshakes",
|
||||
shell=True, stderr=subprocess.STDOUT)
|
||||
command = [self.Protocol, "show", self.Name, "latest-handshakes"]
|
||||
latestHandshake = subprocess.check_output(command, stderr=subprocess.STDOUT)
|
||||
except subprocess.CalledProcessError:
|
||||
return "stopped"
|
||||
latestHandshake = latestHandshake.decode("UTF-8").split()
|
||||
@@ -761,8 +778,9 @@ class WireguardConfiguration:
|
||||
if not self.getStatus():
|
||||
self.toggleConfiguration()
|
||||
# try:
|
||||
data_usage = subprocess.check_output(f"{self.Protocol} show {self.Name} transfer",
|
||||
shell=True, stderr=subprocess.STDOUT)
|
||||
command = [self.Protocol, "show", self.Name, "transfer"]
|
||||
data_usage = subprocess.check_output(command, stderr=subprocess.STDOUT)
|
||||
|
||||
data_usage = data_usage.decode("UTF-8").split("\n")
|
||||
|
||||
data_usage = [p.split("\t") for p in data_usage]
|
||||
@@ -776,15 +794,13 @@ class WireguardConfiguration:
|
||||
)
|
||||
).mappings().fetchone()
|
||||
if cur_i is not None:
|
||||
# print(cur_i is None)
|
||||
total_sent = cur_i['total_sent']
|
||||
# print(cur_i is None)
|
||||
total_receive = cur_i['total_receive']
|
||||
cur_total_sent = float(data_usage[i][2]) / (1024 ** 3)
|
||||
cur_total_receive = float(data_usage[i][1]) / (1024 ** 3)
|
||||
cumulative_receive = cur_i['cumu_receive'] + total_receive
|
||||
cumulative_sent = cur_i['cumu_sent'] + total_sent
|
||||
if total_sent <= cur_total_sent and total_receive <= cur_total_receive:
|
||||
if (total_sent * 0.999 ) <= cur_total_sent and (total_receive * 0.999) <= cur_total_receive: # An accuracy of 1K ppm is sufficient
|
||||
total_sent = cur_total_sent
|
||||
total_receive = cur_total_receive
|
||||
else:
|
||||
@@ -819,10 +835,11 @@ class WireguardConfiguration:
|
||||
if not self.getStatus():
|
||||
self.toggleConfiguration()
|
||||
try:
|
||||
data_usage = subprocess.check_output(f"{self.Protocol} show {self.Name} endpoints",
|
||||
shell=True, stderr=subprocess.STDOUT)
|
||||
command = [self.Protocol, "show", self.Name, "endpoints"]
|
||||
data_usage = subprocess.check_output(command, stderr=subprocess.STDOUT)
|
||||
except subprocess.CalledProcessError:
|
||||
return "stopped"
|
||||
|
||||
data_usage = data_usage.decode("UTF-8").split()
|
||||
count = 0
|
||||
with self.engine.begin() as conn:
|
||||
@@ -840,14 +857,17 @@ class WireguardConfiguration:
|
||||
self.getStatus()
|
||||
if self.Status:
|
||||
try:
|
||||
check = subprocess.check_output(f"{self.Protocol}-quick down {self.Name}",
|
||||
shell=True, stderr=subprocess.STDOUT)
|
||||
command = [f"{self.Protocol}-quick", "down", self.Name]
|
||||
check = subprocess.check_output(command, stderr=subprocess.STDOUT)
|
||||
|
||||
self.removeAutostart()
|
||||
except subprocess.CalledProcessError as exc:
|
||||
return False, str(exc.output.strip().decode("utf-8"))
|
||||
else:
|
||||
try:
|
||||
check = subprocess.check_output(f"{self.Protocol}-quick up {self.Name}", shell=True, stderr=subprocess.STDOUT)
|
||||
command = [f"{self.Protocol}-quick", "up", self.Name]
|
||||
check = subprocess.check_output(command, stderr=subprocess.STDOUT)
|
||||
|
||||
self.addAutostart()
|
||||
except subprocess.CalledProcessError as exc:
|
||||
return False, str(exc.output.strip().decode("utf-8"))
|
||||
@@ -914,8 +934,8 @@ class WireguardConfiguration:
|
||||
files.sort(key=lambda x: x[1], reverse=True)
|
||||
|
||||
for f, ct in files:
|
||||
if RegexMatch(f"^({self.Name})_(.*)\\.(conf)$", f):
|
||||
s = re.search(f"^({self.Name})_(.*)\\.(conf)$", f)
|
||||
if RegexMatch(rf"^({self.Name})_(\d+)\\.(conf)$", f):
|
||||
s = re.search(rf"^({self.Name})_(\d+)\\.(conf)$", f)
|
||||
date = s.group(2)
|
||||
d = {
|
||||
"filename": f,
|
||||
@@ -988,7 +1008,7 @@ class WireguardConfiguration:
|
||||
original = [l.rstrip("\n") for l in f.readlines()]
|
||||
allowEdit = ["Address", "PreUp", "PostUp", "PreDown", "PostDown", "ListenPort", "Table"]
|
||||
if self.Protocol == 'awg':
|
||||
allowEdit += ["Jc", "Jmin", "Jmax", "S1", "S2", "H1", "H2", "H3", "H4"]
|
||||
allowEdit += ["Jc", "Jmin", "Jmax", "S1", "S2", "S3", "S4", "H1", "H2", "H3", "H4", "I1", "I2", "I3", "I4", "I5"]
|
||||
start = original.index("[Interface]")
|
||||
try:
|
||||
end = original.index("[Peer]")
|
||||
@@ -1026,31 +1046,33 @@ class WireguardConfiguration:
|
||||
return True
|
||||
|
||||
def renameConfiguration(self, newConfigurationName) -> tuple[bool, str]:
|
||||
newConfigurationName = os.path.basename(newConfigurationName)
|
||||
|
||||
if len(newConfigurationName) > 15 or not re.match(r'^[a-zA-Z0-9_=\+\.\-]{1,15}$', newConfigurationName):
|
||||
return False, "Configuration name is either too long or contains an illegal character"
|
||||
|
||||
newConfigurationName = newConfigurationName.replace("`", "") # double check
|
||||
|
||||
try:
|
||||
if self.getStatus():
|
||||
self.toggleConfiguration()
|
||||
self.createDatabase(newConfigurationName)
|
||||
with self.engine.begin() as conn:
|
||||
conn.execute(
|
||||
sqlalchemy.text(
|
||||
f'INSERT INTO "{newConfigurationName}" SELECT * FROM "{self.Name}"'
|
||||
def doRenameStatement(suffix):
|
||||
newConfig = f"{newConfigurationName}{suffix}"
|
||||
oldConfig = f"{self.Name}{suffix}"
|
||||
|
||||
conn.execute(
|
||||
sqlalchemy.text(
|
||||
f'INSERT INTO `{newConfig}` SELECT * FROM `{oldConfig}`'
|
||||
)
|
||||
)
|
||||
)
|
||||
conn.execute(
|
||||
sqlalchemy.text(
|
||||
f'INSERT INTO "{newConfigurationName}_restrict_access" SELECT * FROM "{self.Name}_restrict_access"'
|
||||
)
|
||||
)
|
||||
conn.execute(
|
||||
sqlalchemy.text(
|
||||
f'INSERT INTO "{newConfigurationName}_deleted" SELECT * FROM "{self.Name}_deleted"'
|
||||
)
|
||||
)
|
||||
conn.execute(
|
||||
sqlalchemy.text(
|
||||
f'INSERT INTO "{newConfigurationName}_transfer" SELECT * FROM "{self.Name}_transfer"'
|
||||
)
|
||||
)
|
||||
|
||||
doRenameStatement("")
|
||||
doRenameStatement("_restrict_access")
|
||||
doRenameStatement("_deleted")
|
||||
doRenameStatement("_transfer")
|
||||
|
||||
self.AllPeerJobs.updateJobConfigurationName(self.Name, newConfigurationName)
|
||||
shutil.copy(
|
||||
self.configPath,
|
||||
@@ -1058,8 +1080,8 @@ class WireguardConfiguration:
|
||||
)
|
||||
self.deleteConfiguration()
|
||||
except Exception as e:
|
||||
traceback.print_stack()
|
||||
return False, str(e)
|
||||
current_app.logger.error(f"Failed to rename configuration.\nNew Configuration Name: {newConfigurationName}\nError: {str(e)}")
|
||||
return False, "Internal server error"
|
||||
return True, None
|
||||
|
||||
def getNumberOfAvailableIP(self):
|
||||
@@ -1188,7 +1210,7 @@ class WireguardConfiguration:
|
||||
except Exception as e:
|
||||
return False
|
||||
|
||||
def updateConfigurationInfo(self, key: str, value: str | dict[str, str] | dict[str, dict]) -> tuple[bool, Any, str] | tuple[
|
||||
def updateConfigurationInfo(self, key: str, value: str | dict[str, str] | dict[str, dict] | bool) -> tuple[bool, Any, str] | tuple[
|
||||
bool, str, None] | tuple[bool, None, None]:
|
||||
if key == "Description":
|
||||
self.configurationInfo.Description = value
|
||||
@@ -1207,16 +1229,18 @@ class WireguardConfiguration:
|
||||
for name, data in value.items():
|
||||
peerGroups[name] = PeerGroupsClass(**data)
|
||||
self.configurationInfo.PeerGroups = peerGroups
|
||||
elif key == "PeerTrafficTracking":
|
||||
self.configurationInfo.PeerTrafficTracking = value
|
||||
elif key == "PeerHistoricalEndpointTracking":
|
||||
self.configurationInfo.PeerHistoricalEndpointTracking = value
|
||||
else:
|
||||
return False, "Key does not exist", None
|
||||
|
||||
self.storeConfigurationInfo()
|
||||
return True, None, None
|
||||
|
||||
def __validateOverridePeerSettings(self, key: str, value: str | int) -> tuple[bool, None] | tuple[bool, str]:
|
||||
status = True
|
||||
msg = None
|
||||
print(value)
|
||||
if key == "DNS" and value:
|
||||
status, msg = ValidateDNSAddress(value)
|
||||
elif key == "EndpointAllowedIPs" and value:
|
||||
@@ -1226,4 +1250,59 @@ class WireguardConfiguration:
|
||||
status = False
|
||||
msg = "Listen Port must be >= 1 and <= 65535"
|
||||
return status, msg
|
||||
|
||||
|
||||
def getTransferTableSize(self):
|
||||
with self.engine.connect() as db:
|
||||
row_count = db.execute(
|
||||
sqlalchemy.select(sqlalchemy.func.count()).select_from(self.peersTransferTable)
|
||||
).scalar()
|
||||
return int(row_count)
|
||||
|
||||
def getHistoricalEndpointTableSize(self):
|
||||
with self.engine.connect() as db:
|
||||
row_count = db.execute(
|
||||
sqlalchemy.select(sqlalchemy.func.count()).select_from(self.peersHistoryEndpointTable)
|
||||
).scalar()
|
||||
return int(row_count)
|
||||
|
||||
def downloadTransferTable(self):
|
||||
with self.engine.connect() as db:
|
||||
data = db.execute(
|
||||
self.peersTransferTable.select()
|
||||
).mappings().fetchall()
|
||||
return data
|
||||
|
||||
def downloadHistoricalEndpointTable(self):
|
||||
with self.engine.connect() as db:
|
||||
data = db.execute(
|
||||
self.peersHistoryEndpointTable.select()
|
||||
).mappings().fetchall()
|
||||
return data
|
||||
|
||||
def deleteTransferTable(self):
|
||||
try:
|
||||
with self.engine.begin() as db:
|
||||
db.execute(
|
||||
self.peersTransferTable.delete()
|
||||
)
|
||||
with self.engine.connect() as conn:
|
||||
if conn.dialect.name == 'sqlite':
|
||||
print("[WGDashboard] SQLite Vacuuming Database")
|
||||
conn.execute(sqlalchemy.text('VACUUM;'))
|
||||
except Exception as e:
|
||||
return False
|
||||
return True
|
||||
|
||||
def deleteHistoryEndpointTable(self):
|
||||
try:
|
||||
with self.engine.begin() as db:
|
||||
db.execute(
|
||||
self.peersHistoryEndpointTable.delete()
|
||||
)
|
||||
with self.engine.connect() as conn:
|
||||
if conn.dialect.name == 'sqlite':
|
||||
print("[WGDashboard] SQLite Vacuuming Database")
|
||||
conn.execute(sqlalchemy.text('VACUUM;'))
|
||||
except Exception as e:
|
||||
return False
|
||||
return True
|
||||
|
||||
@@ -18,4 +18,6 @@ class PeerGroupsClass(BaseModel):
|
||||
class WireguardConfigurationInfo(BaseModel):
|
||||
Description: str = ''
|
||||
OverridePeerSettings: OverridePeerSettingsClass = OverridePeerSettingsClass(**{})
|
||||
PeerGroups: dict[str, PeerGroupsClass] = {}
|
||||
PeerGroups: dict[str, PeerGroupsClass] = {}
|
||||
PeerTrafficTracking: bool = True
|
||||
PeerHistoricalEndpointTracking: bool = True
|
||||
@@ -1,17 +1,17 @@
|
||||
bcrypt
|
||||
ifcfg
|
||||
psutil
|
||||
pyotp
|
||||
Flask
|
||||
flask-cors
|
||||
icmplib
|
||||
gunicorn
|
||||
requests
|
||||
tcconfig
|
||||
sqlalchemy
|
||||
sqlalchemy_utils
|
||||
psycopg
|
||||
PyMySQL
|
||||
tzlocal
|
||||
python-jose
|
||||
pydantic
|
||||
bcrypt==5.0.0
|
||||
ifcfg==0.24
|
||||
psutil==7.2.2
|
||||
pyotp==2.9.0
|
||||
Flask==3.1.2
|
||||
flask-cors==6.0.2
|
||||
icmplib==3.0.4
|
||||
gunicorn==25.0.3
|
||||
requests==2.32.5
|
||||
tcconfig==0.30.1
|
||||
sqlalchemy==2.0.46
|
||||
sqlalchemy_utils==0.42.1
|
||||
psycopg[binary]==3.3.3
|
||||
PyMySQL==1.1.2
|
||||
tzlocal==5.3.1
|
||||
python-jose==3.5.0
|
||||
pydantic==2.12.5
|
||||
|
||||
@@ -6,14 +6,21 @@
|
||||
<meta name="apple-mobile-web-app-capable" content="yes">
|
||||
<meta name="application-name" content="WGDashboard">
|
||||
<meta name="apple-mobile-web-app-title" content="WGDashboard">
|
||||
<link rel="manifest" href="/json/manifest.json">
|
||||
<link rel="icon" href="/img/Logo-2-512x512.png">
|
||||
<link rel="manifest" href="./json/manifest.json">
|
||||
<link rel="icon" href="./img/Logo-2-512x512.png">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<title>WGDashboard</title>
|
||||
<base href="./">
|
||||
<script>
|
||||
const isViteDevMode = document.querySelector('script[src*="@vite/client"]') !== null;
|
||||
const base = document.querySelector('base');
|
||||
if (base && isViteDevMode) {
|
||||
base.href = '/';
|
||||
}
|
||||
</script>
|
||||
</head>
|
||||
<body>
|
||||
<div id="app"></div>
|
||||
|
||||
<script type="module" src="./src/main.js"></script>
|
||||
</body>
|
||||
</html>
|
||||
|
||||
5442
src/static/app/package-lock.json
generated
5442
src/static/app/package-lock.json
generated
File diff suppressed because it is too large
Load Diff
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "app",
|
||||
"version": "4.3.0.1",
|
||||
"version": "4.3.3",
|
||||
"private": true,
|
||||
"type": "module",
|
||||
"module": "es2022",
|
||||
@@ -12,35 +12,38 @@
|
||||
"preview": "vite preview"
|
||||
},
|
||||
"dependencies": {
|
||||
"@volar/language-server": "2.4.23",
|
||||
"@vue/language-server": "3.0.5",
|
||||
"@vuepic/vue-datepicker": "^11.0.2",
|
||||
"@vueuse/core": "^13.5.0",
|
||||
"@vueuse/shared": "^13.5.0",
|
||||
"@volar/language-server": "2.4.28",
|
||||
"@vue/language-server": "3.2.4",
|
||||
"@vuepic/vue-datepicker": "^12.1.0",
|
||||
"@vueuse/core": "^14.2.1",
|
||||
"@vueuse/shared": "^14.1.0",
|
||||
"animate.css": "^4.1.1",
|
||||
"bootstrap": "^5.3.2",
|
||||
"bootstrap-icons": "^1.11.3",
|
||||
"cidr-tools": "^11.0.3",
|
||||
"cidr-tools": "^11.3.2",
|
||||
"css-color-converter": "^2.0.0",
|
||||
"dayjs": "^1.11.12",
|
||||
"electron-builder": "^26.0.12",
|
||||
"dayjs": "^1.11.19",
|
||||
"electron-builder": "^26.7.0",
|
||||
"fuse.js": "^7.0.0",
|
||||
"i": "^0.3.7",
|
||||
"is-cidr": "^5.0.3",
|
||||
"npm": "^10.5.0",
|
||||
"ol": "^10.2.1",
|
||||
"pinia": "^3.0.3",
|
||||
"pinia-plugin-persistedstate": "^4.5.0",
|
||||
"is-cidr": "^6.0.3",
|
||||
"npm": "^11.8.0",
|
||||
"ol": "^10.7.0",
|
||||
"pinia": "^3.0.4",
|
||||
"pinia-plugin-persistedstate": "^4.7.1",
|
||||
"qrcode": "^1.5.3",
|
||||
"qrcodejs": "^1.0.0",
|
||||
"simple-code-editor": "^2.0.9",
|
||||
"uuid": "^11.1.0",
|
||||
"vue": "^3.5.17",
|
||||
"vue-chartjs": "^5.3.0",
|
||||
"vue-router": "^4.2.5"
|
||||
"uuid": "^13.0.0",
|
||||
"vue": "^3.5.31",
|
||||
"vue-chartjs": "^5.3.3",
|
||||
"vue-router": "^5.0.4"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@vitejs/plugin-vue": "^6.0.0",
|
||||
"vite": "^7.0.5"
|
||||
"@vitejs/plugin-vue": "^6.0.4",
|
||||
"vite": "^7.3.1"
|
||||
},
|
||||
"overrides": {
|
||||
"tar": "^7.5.6"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
import { ref, reactive } from "vue"
|
||||
import LocaleText from "@/components/text/localeText.vue";
|
||||
import OidcSettings from "@/components/clientComponents/clientSettingComponents/oidcSettings.vue";
|
||||
import { fetchGet } from "@/utilities/fetch.js"
|
||||
import { fetchGet, fetchPost } from "@/utilities/fetch.js"
|
||||
const emits = defineEmits(['close'])
|
||||
import { DashboardConfigurationStore } from "@/stores/DashboardConfigurationStore"
|
||||
const dashboardConfigurationStore = DashboardConfigurationStore()
|
||||
@@ -12,12 +12,16 @@ const values = reactive({
|
||||
})
|
||||
|
||||
const toggling = ref(false)
|
||||
const toggleClientSideApp = async () => {
|
||||
const updateSettings = async (key: string) => {
|
||||
toggling.value = true
|
||||
await fetchGet("/api/clients/toggleStatus", {}, (res) => {
|
||||
values.enableClients = res.data
|
||||
await fetchPost("/api/updateDashboardConfigurationItem", {
|
||||
section: "Clients",
|
||||
key: key,
|
||||
value: dashboardConfigurationStore.Configuration.Clients[key]
|
||||
}, async (res) => {
|
||||
await dashboardConfigurationStore.getConfiguration()
|
||||
toggling.value = false
|
||||
})
|
||||
toggling.value = false
|
||||
}
|
||||
</script>
|
||||
|
||||
@@ -37,16 +41,43 @@ const toggleClientSideApp = async () => {
|
||||
</h6>
|
||||
<div class="form-check form-switch ms-auto">
|
||||
<label class="form-check-label" for="oidc_switch">
|
||||
<LocaleText :t="values.enableClients ? 'Enabled':'Disabled'"></LocaleText>
|
||||
<LocaleText :t="dashboardConfigurationStore.Configuration.Clients.enable ? 'Enabled':'Disabled'"></LocaleText>
|
||||
</label>
|
||||
<input
|
||||
:disabled="oidcStatusLoading"
|
||||
v-model="values.enableClients"
|
||||
@change="toggleClientSideApp()"
|
||||
:disabled="toggling"
|
||||
v-model="dashboardConfigurationStore.Configuration.Clients.enable"
|
||||
@change="updateSettings('enable')"
|
||||
class="form-check-input" type="checkbox" role="switch" id="oidc_switch">
|
||||
</div>
|
||||
</div>
|
||||
<OidcSettings mode="Client"></OidcSettings>
|
||||
<hr>
|
||||
<div>
|
||||
<div class="d-flex align-items-center">
|
||||
<h6 class="mb-0">
|
||||
<LocaleText t="Sign Up as Local Client"></LocaleText>
|
||||
</h6>
|
||||
<div class="form-check form-switch ms-auto">
|
||||
<label class="form-check-label" for="sign_up_switch">
|
||||
<LocaleText :t="dashboardConfigurationStore.Configuration.Clients.sign_up ? 'Enabled':'Disabled'"></LocaleText>
|
||||
</label>
|
||||
<input
|
||||
:disabled="toggling"
|
||||
v-model="dashboardConfigurationStore.Configuration.Clients.sign_up"
|
||||
@change="updateSettings('sign_up')"
|
||||
class="form-check-input" type="checkbox" role="switch" id="sign_up_switch">
|
||||
</div>
|
||||
</div>
|
||||
<small class="text-muted mb-0">
|
||||
<LocaleText t="Allow clients to sign up with Email and Password"></LocaleText>
|
||||
</small>
|
||||
</div>
|
||||
<div>
|
||||
<OidcSettings mode="Client"></OidcSettings>
|
||||
<small class="text-muted mb-0">
|
||||
<LocaleText t="Allow clients to access with OpenID"></LocaleText>
|
||||
</small>
|
||||
</div>
|
||||
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -46,14 +46,7 @@ const restoreBackup = () => {
|
||||
}
|
||||
|
||||
const downloadBackup = () => {
|
||||
fetchGet("/api/downloadWireguardConfigurationBackup", {
|
||||
configurationName: route.params.id,
|
||||
backupFileName: props.b.filename
|
||||
}, (res) => {
|
||||
if (res.status){
|
||||
window.open(getUrl(`/fileDownload?file=${res.data}`), '_blank')
|
||||
}
|
||||
})
|
||||
window.location.href = getUrl(`/api/downloadWireguardConfigurationBackup?configurationName=${route.params.id}&backupFileName=${props.b.filename}`);
|
||||
}
|
||||
|
||||
const delaySeconds = computed(() => {
|
||||
|
||||
@@ -77,11 +77,11 @@ const emits = defineEmits(["backup", "close"])
|
||||
</div>
|
||||
<div v-else-if="backups.length > 0">
|
||||
<i class="bi bi-check-circle-fill me-2"></i>
|
||||
<LocaleText :t="'This configuration have ' + backups.length + ' backups'"></LocaleText>
|
||||
<LocaleText :t="'This configuration has ' + backups.length + ' backups'"></LocaleText>
|
||||
</div>
|
||||
<div v-else class="d-flex align-items-center gap-2">
|
||||
<i class="bi bi-x-circle-fill me-2"></i>
|
||||
<LocaleText t="This configuration have no backup"></LocaleText>
|
||||
<LocaleText t="This configuration has no backup"></LocaleText>
|
||||
<a role="button"
|
||||
@click="emits('backup')"
|
||||
class="ms-auto btn btn-sm btn-primary rounded-3">
|
||||
@@ -119,4 +119,4 @@ const emits = defineEmits(["backup", "close"])
|
||||
|
||||
<style scoped>
|
||||
|
||||
</style>
|
||||
</style>
|
||||
|
||||
@@ -197,14 +197,14 @@ const deleteConfigurationModal = ref(false)
|
||||
v-model="data[key]"
|
||||
:id="'configuration_' + key">
|
||||
</div>
|
||||
<div v-for="key in ['Jc', 'Jmin', 'Jmax', 'S1', 'S2', 'H1', 'H2', 'H3', 'H4']"
|
||||
<div v-for="key in ['Jc', 'Jmin', 'Jmax', 'S1', 'S2', 'S3', 'S4', 'H1', 'H2', 'H3', 'H4', 'I1', 'I2', 'I3', 'I4', 'I5']"
|
||||
v-if="configurationInfo.Protocol === 'awg'">
|
||||
<label :for="'configuration_' + key" class="form-label">
|
||||
<small class="text-muted">
|
||||
<LocaleText :t="key"></LocaleText>
|
||||
</small>
|
||||
</label>
|
||||
<input type="number" class="form-control form-control-sm rounded-3"
|
||||
<input type="text" class="form-control form-control-sm rounded-3"
|
||||
:disabled="saving"
|
||||
v-model="data[key]"
|
||||
:id="'configuration_' + key">
|
||||
|
||||
@@ -0,0 +1,31 @@
|
||||
<script>
|
||||
import LocaleText from "@/components/text/localeText.vue";
|
||||
|
||||
export default {
|
||||
name: "notesInput",
|
||||
components: {LocaleText},
|
||||
props: {
|
||||
bulk: Boolean,
|
||||
data: Object,
|
||||
saving: Boolean
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div :class="{inactiveField: this.bulk}">
|
||||
<label for="peer_notes_textbox" class="form-label">
|
||||
<small class="text-muted">
|
||||
<LocaleText t="Notes"></LocaleText>
|
||||
</small>
|
||||
</label>
|
||||
<input type="text" class="form-control form-control-sm rounded-3"
|
||||
:disabled="this.saving || this.bulk"
|
||||
v-model="this.data.notes"
|
||||
id="peer_notes_textbox" placeholder="">
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<style scoped>
|
||||
|
||||
</style>
|
||||
@@ -134,12 +134,17 @@ export default {
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="card-footer" role="button" @click="$emit('details')">
|
||||
<div class="card-footer" role="button" @click="$emit('details')" v-if="!this.Peer.restricted">
|
||||
<small class="d-flex align-items-center">
|
||||
<LocaleText t="Details"></LocaleText>
|
||||
<i class="bi bi-chevron-right ms-auto"></i>
|
||||
</small>
|
||||
</div>
|
||||
<div class="card-footer" v-else>
|
||||
<small class="d-flex align-items-center text-muted">
|
||||
<LocaleText t="Allow access to view details"></LocaleText>
|
||||
</small>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
|
||||
@@ -15,6 +15,7 @@ import MtuInput from "@/components/configurationComponents/newPeersComponents/mt
|
||||
import PersistentKeepAliveInput
|
||||
from "@/components/configurationComponents/newPeersComponents/persistentKeepAliveInput.vue";
|
||||
import {WireguardConfigurationsStore} from "@/stores/WireguardConfigurationsStore.js";
|
||||
import NotesInput from "./newPeersComponents/notesInput.vue";
|
||||
|
||||
const dashboardStore = DashboardConfigurationStore()
|
||||
const wireguardStore = WireguardConfigurationsStore()
|
||||
@@ -27,11 +28,11 @@ const peerData = ref({
|
||||
public_key: "",
|
||||
DNS: dashboardStore.Configuration.Peers.peer_global_dns,
|
||||
endpoint_allowed_ip: dashboardStore.Configuration.Peers.peer_endpoint_allowed_ip,
|
||||
notes: "",
|
||||
keepalive: parseInt(dashboardStore.Configuration.Peers.peer_keep_alive),
|
||||
mtu: parseInt(dashboardStore.Configuration.Peers.peer_mtu),
|
||||
preshared_key: "",
|
||||
preshared_key_bulkAdd: false,
|
||||
advanced_security: "off",
|
||||
allowed_ips_validation: true,
|
||||
})
|
||||
const availableIp = ref([])
|
||||
@@ -105,6 +106,7 @@ watch(() => {
|
||||
<template v-if="!peerData.bulkAdd">
|
||||
<hr class="mb-0 mt-2">
|
||||
<NameInput :saving="saving" :data="peerData"></NameInput>
|
||||
<NotesInput :saving="saving" :data="peerData"></NotesInput>
|
||||
<PrivatePublicKeyInput :saving="saving" :data="peerData"></PrivatePublicKeyInput>
|
||||
<AllowedIPsInput :availableIp="availableIp" :saving="saving" :data="peerData"></AllowedIPsInput>
|
||||
</template>
|
||||
@@ -118,7 +120,7 @@ watch(() => {
|
||||
<LocaleText t="Advanced Options"></LocaleText>
|
||||
</button>
|
||||
</h2>
|
||||
<div id="peerAddModalAccordionAdvancedOptions"
|
||||
<div id="peerAddModalAccordionAdvancedOptions"
|
||||
class="accordion-collapse collapse collapsed" data-bs-parent="#peerAddModalAccordion">
|
||||
<div class="accordion-body rounded-bottom-3">
|
||||
<div class="d-flex flex-column gap-2">
|
||||
|
||||
@@ -37,7 +37,6 @@ export default {
|
||||
mtu: parseInt(this.dashboardStore.Configuration.Peers.peer_mtu),
|
||||
preshared_key: "",
|
||||
preshared_key_bulkAdd: false,
|
||||
advanced_security: "off",
|
||||
},
|
||||
availableIp: undefined,
|
||||
availableIpSearchString: "",
|
||||
|
||||
@@ -32,6 +32,7 @@ Chart.register(
|
||||
import PeerSessions from "@/components/peerDetailsModalComponents/peerSessions.vue";
|
||||
import PeerTraffics from "@/components/peerDetailsModalComponents/peerTraffics.vue";
|
||||
import PeerEndpoints from "@/components/peerDetailsModalComponents/peerEndpoints.vue";
|
||||
import { GetLocale } from "@/utilities/locale"
|
||||
const props = defineProps(['selectedPeer'])
|
||||
const selectedDate = ref(undefined)
|
||||
defineEmits(['close'])
|
||||
@@ -49,13 +50,18 @@ defineEmits(['close'])
|
||||
<button type="button" class="btn-close ms-auto" @click="$emit('close')"></button>
|
||||
</div>
|
||||
<div class="card-body px-4">
|
||||
<div>
|
||||
<p class="mb-0 text-muted"><small>
|
||||
<LocaleText t="Peer"></LocaleText>
|
||||
</small></p>
|
||||
<h2>
|
||||
{{ selectedPeer.name }}
|
||||
</h2>
|
||||
<div class="d-flex justify-content-between align-items-start mb-2 flex-column flex-md-row">
|
||||
<div>
|
||||
<p class="mb-0 text-muted"><small><LocaleText t="Peer" /></small></p>
|
||||
<h2 :class="{'text-muted': selectedPeer.name.length === 0 }">
|
||||
{{ selectedPeer.name.length > 0 ? selectedPeer.name : GetLocale("Untitled Peer") }}
|
||||
</h2>
|
||||
</div>
|
||||
|
||||
<div v-if="selectedPeer.notes" class="text-start text-md-end">
|
||||
<p class="mb-0 text-muted"><small><LocaleText t="Notes" /></small></p>
|
||||
<p class="mb-0" style="white-space: pre-wrap">{{ selectedPeer.notes }}</p>
|
||||
</div>
|
||||
</div>
|
||||
<div class="row mt-3 gy-2 gx-2 mb-2">
|
||||
<div class="col-12 col-lg-3">
|
||||
|
||||
@@ -146,8 +146,8 @@ export default {
|
||||
<td v-if="showLogID"><samp class="text-muted">{{log.LogID}}</samp></td>
|
||||
<td v-if="showJobID"><samp class="text-muted">{{log.JobID}}</samp></td>
|
||||
<td>
|
||||
<span class="badge" :class="[log.Status === '1' ? 'text-success-emphasis bg-success-subtle':'text-danger-emphasis bg-danger-subtle']">
|
||||
{{log.Status === "1" ? 'Success': 'Failed'}}
|
||||
<span class="badge" :class="[(log.Status === '1' || log.Status === 'true') ? 'text-success-emphasis bg-success-subtle':'text-danger-emphasis bg-danger-subtle']">
|
||||
{{(log.Status === "1" || log.Status === "true") ? 'Success': 'Failed'}}
|
||||
</span>
|
||||
</td>
|
||||
<td>{{log.Message}}</td>
|
||||
|
||||
@@ -3,7 +3,7 @@ import ScheduleDropdown from "@/components/configurationComponents/peerScheduleJ
|
||||
import {ref} from "vue";
|
||||
import {DashboardConfigurationStore} from "@/stores/DashboardConfigurationStore.js";
|
||||
import {fetchPost} from "@/utilities/fetch.js";
|
||||
import VueDatePicker from "@vuepic/vue-datepicker";
|
||||
import { VueDatePicker } from "@vuepic/vue-datepicker";
|
||||
import dayjs from "dayjs";
|
||||
import LocaleText from "@/components/text/localeText.vue";
|
||||
|
||||
|
||||
@@ -99,6 +99,17 @@ export default {
|
||||
v-model="this.data.name"
|
||||
id="peer_name_textbox" placeholder="">
|
||||
</div>
|
||||
<div>
|
||||
<label for="peer_notes_textbox" class="form-label">
|
||||
<small class="text-muted">
|
||||
<LocaleText t="Notes"></LocaleText>
|
||||
</small>
|
||||
</label>
|
||||
<input type="text" class="form-control form-control-sm rounded-3"
|
||||
:disabled="this.saving"
|
||||
v-model="this.data.notes"
|
||||
id="peer_notes_textbox" placeholder="">
|
||||
</div>
|
||||
<div>
|
||||
<div class="d-flex position-relative">
|
||||
<label for="peer_private_key_textbox" class="form-label">
|
||||
|
||||
@@ -3,7 +3,7 @@ import {WireguardConfigurationsStore} from "@/stores/WireguardConfigurationsStor
|
||||
import {fetchPost} from "@/utilities/fetch.js";
|
||||
import dayjs from "dayjs";
|
||||
import {DashboardConfigurationStore} from "@/stores/DashboardConfigurationStore.js";
|
||||
import VueDatePicker from '@vuepic/vue-datepicker';
|
||||
import { VueDatePicker } from '@vuepic/vue-datepicker';
|
||||
import LocaleText from "@/components/text/localeText.vue";
|
||||
import PeerShareWithEmail from "@/components/configurationComponents/peerShareLinkComponents/peerShareWithEmail.vue";
|
||||
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
import dayjs from "dayjs";
|
||||
import {fetchPost} from "@/utilities/fetch.js";
|
||||
import {DashboardConfigurationStore} from "@/stores/DashboardConfigurationStore.js";
|
||||
import VueDatePicker from "@vuepic/vue-datepicker";
|
||||
import { VueDatePicker } from "@vuepic/vue-datepicker";
|
||||
import LocaleText from "@/components/text/localeText.vue";
|
||||
|
||||
export default {
|
||||
|
||||
@@ -61,7 +61,7 @@ const sendTestEmail = async () => {
|
||||
<div class="card-header">
|
||||
<h6 class="my-2 d-flex">
|
||||
<i class="bi bi-envelope-fill me-2"></i>
|
||||
<LocaleText t="Email Account"></LocaleText>
|
||||
<LocaleText t="Email Server Settings"></LocaleText>
|
||||
<span class="text-success ms-auto" v-if="emailIsReady">
|
||||
<i class="bi bi-check-circle-fill me-2"></i>
|
||||
<LocaleText t="Ready"></LocaleText>
|
||||
@@ -115,6 +115,9 @@ const sendTestEmail = async () => {
|
||||
<select class="form-select rounded-3"
|
||||
v-model="store.Configuration.Email.encryption"
|
||||
id="encryption">
|
||||
<option value="IMPLICITTLS">
|
||||
IMPLICIT TLS
|
||||
</option>
|
||||
<option value="STARTTLS">
|
||||
STARTTLS
|
||||
</option>
|
||||
@@ -208,4 +211,4 @@ const sendTestEmail = async () => {
|
||||
|
||||
<style scoped>
|
||||
|
||||
</style>
|
||||
</style>
|
||||
|
||||
@@ -0,0 +1,76 @@
|
||||
<script setup>
|
||||
|
||||
import LocaleText from "@/components/text/localeText.vue";
|
||||
import ConfigurationTracking
|
||||
from "@/components/settingsComponent/dashboardWireguardConfigurationTrackingComponents/configurationTracking.vue";
|
||||
import {WireguardConfigurationsStore} from "@/stores/WireguardConfigurationsStore.js";
|
||||
import {onMounted, ref, watch} from "vue";
|
||||
import {fetchGet, fetchPost} from "@/utilities/fetch.js";
|
||||
import {DashboardConfigurationStore} from "@/stores/DashboardConfigurationStore.js";
|
||||
|
||||
const store = WireguardConfigurationsStore()
|
||||
const dashboardStore = DashboardConfigurationStore()
|
||||
const peerTrackingStatus = ref(dashboardStore.Configuration.WireGuardConfiguration.peer_tracking)
|
||||
const loaded = ref(false)
|
||||
const trackingData = ref({})
|
||||
onMounted(async () => {
|
||||
if (peerTrackingStatus.value){
|
||||
await loadData()
|
||||
}
|
||||
})
|
||||
|
||||
const loadData = async () => {
|
||||
await fetchGet("/api/getPeerTrackingTableCounts", {}, (ref) => {
|
||||
if (ref.status){
|
||||
trackingData.value = ref.data
|
||||
}
|
||||
loaded.value = true
|
||||
})
|
||||
}
|
||||
|
||||
watch(peerTrackingStatus, async (newVal) => {
|
||||
await fetchPost("/api/updateDashboardConfigurationItem", {
|
||||
section: "WireGuardConfiguration",
|
||||
key: "peer_tracking",
|
||||
value: newVal
|
||||
}, async (res) => {
|
||||
if (res.status){
|
||||
dashboardStore.newMessage("Server", newVal ? "Peer tracking enabled" : "Peer tracking disabled", "success")
|
||||
if (newVal) await loadData()
|
||||
}
|
||||
})
|
||||
})
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div class="card">
|
||||
<div class="card-header d-flex align-items-center">
|
||||
<h6 class="my-2">
|
||||
<LocaleText t="Peer Tracking"></LocaleText>
|
||||
</h6>
|
||||
<div class="form-check form-switch ms-auto">
|
||||
<input class="form-check-input"
|
||||
v-model="peerTrackingStatus"
|
||||
type="checkbox" role="switch" id="peerTrackingStatus">
|
||||
<label class="form-check-label" for="peerTrackingStatus">
|
||||
<LocaleText t="Enabled" v-if="peerTrackingStatus"></LocaleText>
|
||||
<LocaleText t="Disabled" v-else></LocaleText>
|
||||
</label>
|
||||
</div>
|
||||
</div>
|
||||
<div class="card-body d-flex flex-column gap-3" v-if="peerTrackingStatus">
|
||||
<template v-if="!loaded">
|
||||
<div class="spinner-border text-body m-auto"></div>
|
||||
</template>
|
||||
<template v-else>
|
||||
<ConfigurationTracking :configuration="configuration"
|
||||
:trackingData="trackingData"
|
||||
v-for="configuration in store.Configurations"/>
|
||||
</template>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<style scoped>
|
||||
|
||||
</style>
|
||||
@@ -0,0 +1,190 @@
|
||||
<script setup>
|
||||
import {fetchGet, fetchPost} from "@/utilities/fetch.js";
|
||||
import {onMounted, ref, watch} from "vue";
|
||||
import LocaleText from "@/components/text/localeText.vue";
|
||||
import {DashboardConfigurationStore} from "@/stores/DashboardConfigurationStore.js";
|
||||
|
||||
const props = defineProps(['configuration', 'trackingData'])
|
||||
const sizes = ref({
|
||||
HistoricalTrackingTableSize: 0,
|
||||
TrafficTrackingTableSize: 0
|
||||
})
|
||||
const sizeDataLoaded = ref(false)
|
||||
const toggling = ref(false)
|
||||
|
||||
await onMounted(async () => {
|
||||
sizes.value = props.trackingData[props.configuration.Name]
|
||||
})
|
||||
|
||||
const loadSizeData = async () => {
|
||||
|
||||
await fetchGet("/api/getPeerTrackingTableCounts", {
|
||||
configurationName: props.configuration.Name
|
||||
}, (res) => {
|
||||
sizes.value = res.data;
|
||||
})
|
||||
}
|
||||
|
||||
const updateToggle = async (key) => {
|
||||
toggling.value = true;
|
||||
await fetchPost('/api/updateWireguardConfigurationInfo', {
|
||||
Name: props.configuration.Name,
|
||||
Key: key,
|
||||
Value: props.configuration.Info[key]
|
||||
}, (res) => {
|
||||
console.log(res)
|
||||
toggling.value = false;
|
||||
})
|
||||
}
|
||||
const downloading = ref(undefined);
|
||||
const download = async (key) => {
|
||||
downloading.value = key;
|
||||
await fetchGet("/api/downloadPeerTrackingTable", {
|
||||
configurationName: props.configuration.Name,
|
||||
table: key
|
||||
}, (res) => {
|
||||
if (res.status){
|
||||
const s = JSON.stringify(res.data, null, 2)
|
||||
const b = new Blob([s], {
|
||||
type: "application/json"
|
||||
})
|
||||
const url = URL.createObjectURL(b)
|
||||
const a = document.createElement('a')
|
||||
a.href = url;
|
||||
a.download = `${props.configuration.Name}_${key}.json`
|
||||
a.click()
|
||||
downloading.value = undefined;
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
const confirmDelete = ref("")
|
||||
const deleting = ref(undefined)
|
||||
const dashboardStore = DashboardConfigurationStore()
|
||||
const deleteRecord = async (key) => {
|
||||
deleting.value = true;
|
||||
await fetchPost('/api/deletePeerTrackingTable', {
|
||||
configurationName: props.configuration.Name,
|
||||
table: key
|
||||
}, async(res) => {
|
||||
if (res.status){
|
||||
dashboardStore.newMessage('Server', 'Record deleted', 'success')
|
||||
}else{
|
||||
dashboardStore.newMessage('Server', 'Record delete failed', 'danger')
|
||||
}
|
||||
await loadSizeData()
|
||||
deleting.value = false;
|
||||
confirmDelete.value = ''
|
||||
})
|
||||
}
|
||||
|
||||
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div class="card">
|
||||
<div class="card-header">
|
||||
{{ configuration.Name }}
|
||||
</div>
|
||||
<div class="card-body">
|
||||
<div class="row gy-2">
|
||||
<div class="col-sm">
|
||||
<small class="text-muted fw-bold">Peer Traffic Tracking</small>
|
||||
<div class="form-check form-switch">
|
||||
<input class="form-check-input" type="checkbox"
|
||||
:disabled="toggling"
|
||||
@change="updateToggle('PeerTrafficTracking')"
|
||||
v-model="configuration.Info.PeerTrafficTracking" :id="configuration.Name + '_traffic_tracking'">
|
||||
<label class="form-check-label" :for="configuration.Name + '_traffic_tracking'">
|
||||
{{ configuration.Info.PeerTrafficTracking ? 'On':'Off'}}
|
||||
</label>
|
||||
</div>
|
||||
<hr />
|
||||
<div class="d-flex align-items-start align-items-md-center flex-column flex-md-row gap-2">
|
||||
<h6 class="mb-0">
|
||||
{{ sizes.TrafficTrackingTableSize }} <span class="text-muted fw-normal"><LocaleText t="Records"></LocaleText></span>
|
||||
</h6>
|
||||
<div class="ms-md-auto d-flex gap-2" v-if="confirmDelete !== 'TrafficTrackingTable'">
|
||||
<button class="btn btn-sm bg-primary-subtle text-primary-emphasis rounded-3"
|
||||
:class="{disabled: downloading === 'TrafficTrackingTable'}"
|
||||
@click="download('TrafficTrackingTable')"
|
||||
>
|
||||
<i class="bi bi-download me-2 "></i>
|
||||
<LocaleText :t="downloading === 'TrafficTrackingTable' ? 'Downloading...':'Download'"></LocaleText>
|
||||
</button>
|
||||
<button class="btn btn-sm bg-danger-subtle text-danger-emphasis rounded-3" @click="confirmDelete = 'TrafficTrackingTable'">
|
||||
<i class="bi bi-trash me-2 "></i>Delete
|
||||
</button>
|
||||
</div>
|
||||
<div class="ms-md-auto d-flex gap-2 align-items-center" v-else-if="confirmDelete === 'TrafficTrackingTable'">
|
||||
<small>
|
||||
<LocaleText t="Are you sure to delete?"></LocaleText>
|
||||
</small>
|
||||
<button class="btn btn-sm bg-danger-subtle text-danger-emphasis rounded-3"
|
||||
:class="{disabled: deleting}"
|
||||
@click="deleteRecord('TrafficTrackingTable')">
|
||||
<i class="bi bi-check me-2 "></i>Yes
|
||||
</button>
|
||||
<button
|
||||
:class="{disabled: deleting}"
|
||||
class="btn btn-sm bg-secondary-subtle text-secondary-emphasis rounded-3" @click="confirmDelete = ''">
|
||||
<i class="bi bi-x me-2 "></i>No
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="col-sm">
|
||||
<small class="text-muted fw-bold">Peer Historical Endpoint Tracking</small>
|
||||
<div class="form-check form-switch">
|
||||
<input class="form-check-input"
|
||||
:disabled="toggling"
|
||||
@change="updateToggle('PeerHistoricalEndpointTracking')"
|
||||
type="checkbox" v-model="configuration.Info.PeerHistoricalEndpointTracking" :id="configuration.Name + '_historicalEndpoint_tracking'">
|
||||
<label class="form-check-label" :for="configuration.Name + '_historicalEndpoint_tracking'">
|
||||
{{ configuration.Info.PeerHistoricalEndpointTracking ? 'On':'Off'}}
|
||||
</label>
|
||||
</div>
|
||||
<hr />
|
||||
<div class="d-flex align-items-start align-items-md-center flex-column flex-md-row gap-2">
|
||||
<div>
|
||||
<h6 class="mb-0">
|
||||
{{ sizes.HistoricalTrackingTableSize }} <span class="text-muted fw-normal"><LocaleText t="Records"></LocaleText></span>
|
||||
</h6>
|
||||
</div>
|
||||
<div class="ms-md-auto d-flex gap-2" v-if="confirmDelete !== 'HistoricalTrackingTable'">
|
||||
<button
|
||||
@click="download('HistoricalTrackingTable')"
|
||||
:class="{disabled: downloading === 'HistoricalTrackingTable'}"
|
||||
class="btn btn-sm bg-primary-subtle text-primary-emphasis rounded-3">
|
||||
<i class="bi bi-download me-2 "></i>
|
||||
<LocaleText :t="downloading === 'HistoricalTrackingTable' ? 'Downloading...':'Download'"></LocaleText>
|
||||
</button>
|
||||
<button class="btn btn-sm bg-danger-subtle text-danger-emphasis rounded-3" @click="confirmDelete = 'HistoricalTrackingTable'">
|
||||
<i class="bi bi-trash me-2 "></i>Delete
|
||||
</button>
|
||||
</div>
|
||||
<div class="ms-md-auto d-flex gap-2 align-items-center" v-else-if="confirmDelete === 'HistoricalTrackingTable'">
|
||||
<small>
|
||||
<LocaleText t="Are you sure to delete?"></LocaleText>
|
||||
</small>
|
||||
<button class="btn btn-sm bg-danger-subtle text-danger-emphasis rounded-3"
|
||||
:class="{disabled: deleting}"
|
||||
@click="deleteRecord('HistoricalTrackingTable')">
|
||||
<i class="bi bi-check me-2 "></i>Yes
|
||||
</button>
|
||||
<button
|
||||
:class="{disabled: deleting}"
|
||||
class="btn btn-sm bg-secondary-subtle text-secondary-emphasis rounded-3" @click="confirmDelete = ''">
|
||||
<i class="bi bi-x me-2 "></i>No
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<style scoped>
|
||||
|
||||
</style>
|
||||
@@ -4,6 +4,8 @@ import DashboardSettingsInputWireguardConfigurationPath
|
||||
from "@/components/settingsComponent/dashboardSettingsInputWireguardConfigurationPath.vue";
|
||||
import DashboardSettingsWireguardConfigurationAutostart
|
||||
from "@/components/settingsComponent/dashboardSettingsWireguardConfigurationAutostart.vue";
|
||||
import DashboardWireguardConfigurationTracking
|
||||
from "@/components/settingsComponent/dashboardWireguardConfigurationTracking.vue";
|
||||
</script>
|
||||
|
||||
<template>
|
||||
@@ -16,6 +18,7 @@ import DashboardSettingsWireguardConfigurationAutostart
|
||||
>
|
||||
</DashboardSettingsInputWireguardConfigurationPath>
|
||||
<DashboardSettingsWireguardConfigurationAutostart></DashboardSettingsWireguardConfigurationAutostart>
|
||||
<DashboardWireguardConfigurationTracking/>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
|
||||
@@ -20,7 +20,6 @@ const squareHeight = computed(() => {
|
||||
|
||||
<template>
|
||||
<div class="flex-grow-1 square rounded-3 border position-relative p-2"
|
||||
|
||||
@mouseenter="show = true"
|
||||
@mouseleave="show = false"
|
||||
:style="{'background-color': `rgb(13 110 253 / ${percentage*10}%)`}">
|
||||
|
||||
@@ -49,7 +49,7 @@ const data = computed(() => {
|
||||
<div class="progress" role="progressbar" style="height: 6px">
|
||||
<div class="progress-bar" :style="{width: `${data?.CPU.cpu_percent}%` }"></div>
|
||||
</div>
|
||||
<div class="d-flex mt-2 gap-1">
|
||||
<div class="d-grid mt-2 gap-1" style="grid-template-columns: repeat(10, 1fr)">
|
||||
<CpuCore
|
||||
v-for="(cpu, count) in data?.CPU.cpu_percent_per_cpu"
|
||||
:key="count"
|
||||
@@ -74,7 +74,7 @@ const data = computed(() => {
|
||||
<div class="progress" role="progressbar" style="height: 6px">
|
||||
<div class="progress-bar bg-success" :style="{width: `${data?.Disks.find(x => x.mountPoint === '/') ? data?.Disks.find(x => x.mountPoint === '/').percent : data?.Disks[0].percent}%` }"></div>
|
||||
</div>
|
||||
<div class="d-flex mt-2 gap-1">
|
||||
<div class="d-grid mt-2 gap-1" style="grid-template-columns: repeat(10, 1fr)">
|
||||
<StorageMount v-for="(disk, count) in data?.Disks"
|
||||
v-if="data"
|
||||
:key="disk.mountPoint"
|
||||
|
||||
@@ -27,8 +27,11 @@ export const getUrl = (url) => {
|
||||
if (apiKey){
|
||||
return `${apiKey.host}${url}`
|
||||
}
|
||||
return import.meta.env.MODE === 'development' ? url
|
||||
: `${window.location.protocol}//${(window.location.host + window.location.pathname + url).replace(/\/\//g, '/')}`
|
||||
if (import.meta.env.MODE === 'development') {
|
||||
return url;
|
||||
}
|
||||
// const appPrefix = window.APP_PREFIX || '';
|
||||
return `./.${url}`;
|
||||
}
|
||||
|
||||
export const fetchGet = async (url, params=undefined, callback=undefined) => {
|
||||
|
||||
@@ -43,10 +43,17 @@ export default {
|
||||
Jmax: 998,
|
||||
S1: 17,
|
||||
S2: 110,
|
||||
S3: 1,
|
||||
S4: 2,
|
||||
H1: 0,
|
||||
H2: 0,
|
||||
H3: 0,
|
||||
H4: 0
|
||||
H4: 0,
|
||||
I1: "0",
|
||||
I2: "0",
|
||||
I3: "0",
|
||||
I4: "0",
|
||||
I5: "0"
|
||||
},
|
||||
numberOfAvailableIPs: "0",
|
||||
error: false,
|
||||
@@ -59,14 +66,16 @@ export default {
|
||||
},
|
||||
created() {
|
||||
this.wireguardGenerateKeypair();
|
||||
let hValue = []
|
||||
while ([...new Set(hValue)].length !== 4){
|
||||
hValue = [this.rand(1, (2**31) - 1), this.rand(1, (2**31) - 1), this.rand(1, (2**31) - 1), this.rand(1, (2**31) - 1)]
|
||||
}
|
||||
this.newConfiguration.H1 = hValue[0]
|
||||
this.newConfiguration.H2 = hValue[1]
|
||||
this.newConfiguration.H3 = hValue[2]
|
||||
this.newConfiguration.H4 = hValue[3]
|
||||
|
||||
// Generate 4 random numbers for H1, H2, H3, H4
|
||||
['H1', 'H2', 'H3', 'H4'].forEach(key => {
|
||||
this.newConfiguration[key] = this.rand(1, 2**31);
|
||||
});
|
||||
|
||||
// Initialize I1 to I5 as "0"
|
||||
['I1', 'I2', 'I3', 'I4', 'I5'].forEach(key => {
|
||||
this.newConfiguration[key] = "0";
|
||||
});
|
||||
},
|
||||
methods: {
|
||||
rand(min, max){
|
||||
@@ -379,7 +388,7 @@ export default {
|
||||
|
||||
<div class="card rounded-3"
|
||||
v-if="this.newConfiguration.Protocol === 'awg'"
|
||||
v-for="key in ['Jc', 'Jmin', 'Jmax', 'S1', 'S2', 'H1', 'H2', 'H3', 'H4']">
|
||||
v-for="key in ['Jc', 'Jmin', 'Jmax', 'S1', 'S2', 'S3', 'S4', 'H1', 'H2', 'H3', 'H4', 'I1', 'I2', 'I3', 'I4', 'I5']">
|
||||
<div class="card-header">{{ key }}</div>
|
||||
<div class="card-body">
|
||||
<input type="text"
|
||||
|
||||
@@ -176,7 +176,10 @@ export default {
|
||||
<input
|
||||
v-model="this.store.CrossServerConfiguration.Enable"
|
||||
:disabled="loading"
|
||||
class="form-check-input" type="checkbox" role="switch" id="flexSwitchCheckChecked">
|
||||
class="form-check-input"
|
||||
type="checkbox"
|
||||
role="switch"
|
||||
id="flexSwitchCheckChecked">
|
||||
<label class="form-check-label" for="flexSwitchCheckChecked">
|
||||
<LocaleText t="Access Remote Server"></LocaleText>
|
||||
</label>
|
||||
@@ -184,10 +187,20 @@ export default {
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<small class="text-muted pb-3 d-block w-100 text-center mt-3">
|
||||
WGDashboard {{ this.version }} | Developed with ❤️ by
|
||||
<a href="https://github.com/donaldzou" target="_blank"><strong>Donald Zou</strong></a>
|
||||
</small>
|
||||
<div class="d-flex container-fluid align-items-center my-1 w-100">
|
||||
<small class="text-muted">
|
||||
WGDashboard <strong>{{ this.version }}</strong> | Made with ❤️ by
|
||||
<a href="https://github.com/WGDashboard"
|
||||
class="text-decoration-none text-body"
|
||||
target="_blank"><strong>WGDashboard</strong></a>
|
||||
</small>
|
||||
<a href="./client" target="_blank"
|
||||
class="text-decoration-none ms-auto text-body"
|
||||
style="white-space: nowrap">
|
||||
<small><i class="bi bi-box-arrow-up-right me-1"></i>
|
||||
<LocaleText t="Client App" /></small>
|
||||
</a>
|
||||
</div>
|
||||
<div class="messageCentre text-body position-absolute d-flex">
|
||||
<TransitionGroup name="message" tag="div"
|
||||
class="position-relative flex-sm-grow-0 flex-grow-1 d-flex align-items-end ms-sm-auto flex-column gap-2">
|
||||
|
||||
@@ -176,7 +176,7 @@ const memoryHistoricalChartData = computed(() => {
|
||||
<div class="card rounded-3 h-100 shadow">
|
||||
<div class="card-body p-4">
|
||||
<div class="d-flex flex-column gap-3">
|
||||
<div class="d-flex flex-column gap-3" style="height: 130px">
|
||||
<div class="d-flex flex-column gap-3" style="min-height: 130px">
|
||||
<div class="d-flex align-items-center">
|
||||
<h3 class="text-muted mb-0">
|
||||
<i class="bi bi-cpu-fill me-2"></i>
|
||||
@@ -192,7 +192,7 @@ const memoryHistoricalChartData = computed(() => {
|
||||
<div class="progress" role="progressbar" style="height: 10px">
|
||||
<div class="progress-bar" :style="{width: `${data?.CPU.cpu_percent}%` }"></div>
|
||||
</div>
|
||||
<div class="d-flex gap-1">
|
||||
<div class="d-grid gap-1" style="grid-template-columns: repeat(10, 1fr)">
|
||||
<CpuCore
|
||||
v-for="(cpu, count) in data?.CPU.cpu_percent_per_cpu"
|
||||
:square="true"
|
||||
|
||||
@@ -32,7 +32,7 @@ export default defineConfig(({mode}) => {
|
||||
}
|
||||
|
||||
return {
|
||||
base: "/static/dist/WGDashboardAdmin",
|
||||
base: "./",
|
||||
plugins: [
|
||||
vue(),
|
||||
],
|
||||
|
||||
@@ -2,9 +2,10 @@
|
||||
<html lang="">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<link rel="icon" href="/img/Logo-2-128x128.png">
|
||||
<link rel="icon" href="./img/Logo-2-128x128.png">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<title>WGDashboard Client</title>
|
||||
<base href="./client/">
|
||||
<style>
|
||||
*{
|
||||
font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Helvetica, Arial, sans-serif, "Apple Color Emoji", "Segoe UI Emoji", "Segoe UI Symbol";
|
||||
@@ -28,15 +29,22 @@
|
||||
}
|
||||
}
|
||||
</style>
|
||||
<script>
|
||||
const isViteDevMode = document.querySelector('script[src*="@vite/client"]') !== null;
|
||||
const base = document.querySelector('base');
|
||||
if (base && isViteDevMode) {
|
||||
base.href = '/';
|
||||
}
|
||||
</script>
|
||||
</head>
|
||||
<body>
|
||||
<div id="app">
|
||||
<div id="preloader">
|
||||
<div id="preloader_placeholder">
|
||||
<img style="width: 100%" src="/img/Logo-2-128x128.png" alt="WGDashboard Client" />
|
||||
<img style="width: 100%" src="./img/Logo-2-128x128.png" alt="WGDashboard Client" />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<script type="module" src="/src/main.js"></script>
|
||||
<script type="module" src="./src/main.js"></script>
|
||||
</body>
|
||||
</html>
|
||||
|
||||
1560
src/static/client/package-lock.json
generated
1560
src/static/client/package-lock.json
generated
File diff suppressed because it is too large
Load Diff
@@ -23,6 +23,6 @@
|
||||
"devDependencies": {
|
||||
"@vitejs/plugin-vue": "^5.2.3",
|
||||
"vite": "^6.2.4",
|
||||
"vite-plugin-vue-devtools": "^7.7.2"
|
||||
"vite-plugin-vue-devtools": "^8.1.1"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,20 +1,12 @@
|
||||
<script setup async>
|
||||
<script setup>
|
||||
import './assets/main.css'
|
||||
import NotificationList from "@/components/Notification/notificationList.vue";
|
||||
import {clientStore} from "@/stores/clientStore.js";
|
||||
|
||||
const store = clientStore()
|
||||
fetch("/client/api/serverInformation")
|
||||
.then(res => res.json())
|
||||
.then(res => store.serverInformation = res.data)
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div data-bs-theme="dark" class="text-body bg-body vw-100 vh-100 bg-body">
|
||||
<div class="d-flex vw-100 p-sm-4 overflow-y-scroll innerContainer d-flex flex-column">
|
||||
<div class="mx-auto my-sm-auto position-relative"
|
||||
id="listContainer"
|
||||
>
|
||||
<div class="mx-auto my-sm-auto position-relative" id="listContainer">
|
||||
<Suspense>
|
||||
<RouterView v-slot="{ Component }">
|
||||
<Transition name="app" type="transition" mode="out-in">
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
import {computed, ref} from "vue";
|
||||
import ConfigurationQRCode from "@/components/Configuration/configurationQRCode.vue";
|
||||
import dayjs from "dayjs";
|
||||
import Duration from 'dayjs/plugin/Duration'
|
||||
import Duration from 'dayjs/plugin/duration'
|
||||
dayjs.extend(Duration);
|
||||
const props = defineProps([
|
||||
'config'
|
||||
@@ -113,4 +113,4 @@ const emits = defineEmits(['select'])
|
||||
background-color: #28a745 !important;
|
||||
box-shadow: 0 0 0 .2rem #28a74545;
|
||||
}
|
||||
</style>
|
||||
</style>
|
||||
|
||||
@@ -96,7 +96,7 @@ if (route.query.Email){
|
||||
</span>
|
||||
</button>
|
||||
</form>
|
||||
<div>
|
||||
<div v-if="store.serverInformation.SignUp.enable">
|
||||
<hr class="my-4">
|
||||
<div class="d-flex align-items-center">
|
||||
<span class="text-muted">
|
||||
|
||||
@@ -6,43 +6,43 @@ import router from "@/router/router.js";
|
||||
import {createPinia} from "pinia";
|
||||
|
||||
import 'bootstrap/dist/js/bootstrap.bundle.js'
|
||||
import {axiosPost} from "@/utilities/request.js";
|
||||
import {axiosGet, axiosPost} from "@/utilities/request.js";
|
||||
import {clientStore} from "@/stores/clientStore.js";
|
||||
|
||||
const params = new URLSearchParams(window.location.search)
|
||||
const state = params.get('state')
|
||||
const code = params.get('code')
|
||||
|
||||
const initApp = () => {
|
||||
const initApp = async () => {
|
||||
const app = createApp(App)
|
||||
const serverInformation = await axiosGet("/api/serverInformation", {})
|
||||
app.use(createPinia())
|
||||
if (serverInformation){
|
||||
const store = clientStore()
|
||||
store.serverInformation = serverInformation.data;
|
||||
}
|
||||
app.use(router)
|
||||
app.mount("#app")
|
||||
}
|
||||
|
||||
function removeSearchString() {
|
||||
let url = new URL(window.location.href);
|
||||
url.search = ''; // Remove all query parameters
|
||||
history.replaceState({}, document.title, url.toString());
|
||||
}
|
||||
|
||||
if (state && code){
|
||||
axiosPost("/api/signin/oidc", {
|
||||
await axiosPost("/api/signin/oidc", {
|
||||
provider: state,
|
||||
code: code,
|
||||
redirect_uri: window.location.protocol + '//' + window.location.host + window.location.pathname
|
||||
}).then(data => {
|
||||
}).then(async (data) => {
|
||||
let url = new URL(window.location.href);
|
||||
url.search = '';
|
||||
history.replaceState({}, document.title, url.toString());
|
||||
|
||||
initApp()
|
||||
await initApp()
|
||||
if (!data.status){
|
||||
const store = clientStore()
|
||||
store.newNotification(data.message, 'danger')
|
||||
}
|
||||
})
|
||||
}else{
|
||||
initApp()
|
||||
await initApp()
|
||||
}
|
||||
|
||||
|
||||
|
||||
@@ -50,6 +50,11 @@ const router = createRouter({
|
||||
})
|
||||
router.beforeEach(async (to, from, next) => {
|
||||
const store = clientStore()
|
||||
if (to.path === "/signup" && !store.serverInformation.SignUp.enable){
|
||||
next('/signin')
|
||||
store.newNotification("Sign up is disabled. Please contact administrator for more information", "warning")
|
||||
}
|
||||
|
||||
if (to.path === '/signout'){
|
||||
await axios.get(requestURl('/api/signout')).then(() => {
|
||||
next('/signin')
|
||||
|
||||
@@ -1,23 +1,18 @@
|
||||
import axios from "axios";
|
||||
import {useRouter} from "vue-router";
|
||||
|
||||
export const requestURl = (url) => {
|
||||
return import.meta.env.MODE === 'development' ? '/client' + url
|
||||
: `${window.location.protocol}//${(window.location.host + window.location.pathname + url).replace(/\/\//g, '/')}`
|
||||
if (import.meta.env.MODE === 'development') {
|
||||
return '/client' + url;
|
||||
}
|
||||
return `./.${url}`;
|
||||
}
|
||||
|
||||
// const router = useRouter()
|
||||
|
||||
export const axiosPost = async (URL, body = {}) => {
|
||||
try{
|
||||
const res = await axios.post(requestURl(URL), body)
|
||||
return res.data
|
||||
} catch (error){
|
||||
console.log(error)
|
||||
// if (error.status === 401){
|
||||
// await router.push('/signin')
|
||||
// }
|
||||
|
||||
return undefined
|
||||
}
|
||||
}
|
||||
@@ -28,9 +23,6 @@ export const axiosGet = async (URL, query = {}) => {
|
||||
return res.data
|
||||
} catch (error){
|
||||
console.log(error)
|
||||
// if (error.status === 401){
|
||||
// await router.push('/signin')
|
||||
// }
|
||||
return undefined
|
||||
}
|
||||
}
|
||||
@@ -40,5 +40,5 @@ export default defineConfig({
|
||||
}
|
||||
}
|
||||
},
|
||||
base: '/static/dist/WGDashboardClient'
|
||||
base: './'
|
||||
})
|
||||
|
||||
@@ -1 +1 @@
|
||||
import{a7 as A,r as n,D as S,g as l,z as v}from"./index-BQYwwrw6.js";const b=A("DashboardClientAssignmentStore",()=>{const f=n({}),d=n([]),o=n({}),c=n([]),g=n(!1),r=n(""),i=S(),w=async()=>{await l("/api/clients/allClients",{},s=>{o.value=s.data})},y=async()=>{await l("/api/clients/allClientsRaw",{},s=>{c.value=s.data,console.log(c.value)})},m=s=>Object.values(o.value).flat().find(e=>e.ClientID===s),u=async(s,e)=>{await l("/api/clients/assignedClients",{ConfigurationName:s,Peer:e},a=>{d.value=a.data})};return{assignments:d,getAssignedClients:u,getClients:w,getClientsRaw:y,clients:o,unassignClient:async(s,e,a)=>{g.value=!0,await v("/api/clients/unassignClient",{AssignmentID:a},async t=>{t.status?(i.newMessage("Server","Unassign successfully!","success"),s&&e&&await u(s,e)):(i.newMessage("Server","Unassign Failed. Reason: "+t.message,"success"),console.error("Unassign Failed. Reason: "+t.message)),g.value=!1})},assignClient:async(s,e,a,t=!0)=>{r.value=a,await v("/api/clients/assignClient",{ConfigurationName:s,Peer:e,ClientID:a},async C=>{C.status?(i.newMessage("Server","Assign successfully!","success"),t&&await u(s,e)):(i.newMessage("Server","Assign Failed. Reason: "+C.message,"success"),console.error("Assign Failed. Reason: "+C.message)),r.value=""})},getClientById:m,unassigning:g,assigning:r,clientsRaw:c,allConfigurationsPeers:f,getAllConfigurationsPeers:async()=>{await l("/api/clients/allConfigurationsPeers",{},s=>{f.value=s.data})}}});export{b as D};
|
||||
import{a5 as A,r as n,D as S,g as l,z as v}from"./index-DXzxfcZW.js";const b=A("DashboardClientAssignmentStore",()=>{const f=n({}),d=n([]),o=n({}),c=n([]),g=n(!1),r=n(""),i=S(),w=async()=>{await l("/api/clients/allClients",{},s=>{o.value=s.data})},y=async()=>{await l("/api/clients/allClientsRaw",{},s=>{c.value=s.data,console.log(c.value)})},m=s=>Object.values(o.value).flat().find(e=>e.ClientID===s),u=async(s,e)=>{await l("/api/clients/assignedClients",{ConfigurationName:s,Peer:e},a=>{d.value=a.data})};return{assignments:d,getAssignedClients:u,getClients:w,getClientsRaw:y,clients:o,unassignClient:async(s,e,a)=>{g.value=!0,await v("/api/clients/unassignClient",{AssignmentID:a},async t=>{t.status?(i.newMessage("Server","Unassign successfully!","success"),s&&e&&await u(s,e)):(i.newMessage("Server","Unassign Failed. Reason: "+t.message,"success"),console.error("Unassign Failed. Reason: "+t.message)),g.value=!1})},assignClient:async(s,e,a,t=!0)=>{r.value=a,await v("/api/clients/assignClient",{ConfigurationName:s,Peer:e,ClientID:a},async C=>{C.status?(i.newMessage("Server","Assign successfully!","success"),t&&await u(s,e)):(i.newMessage("Server","Assign Failed. Reason: "+C.message,"success"),console.error("Assign Failed. Reason: "+C.message)),r.value=""})},getClientById:m,unassigning:g,assigning:r,clientsRaw:c,allConfigurationsPeers:f,getAllConfigurationsPeers:async()=>{await l("/api/clients/allConfigurationsPeers",{},s=>{f.value=s.data})}}});export{b as D};
|
||||
7
src/static/dist/WGDashboardAdmin/assets/Vector-CuSZivra.js
vendored
Normal file
7
src/static/dist/WGDashboardAdmin/assets/Vector-CuSZivra.js
vendored
Normal file
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
1
src/static/dist/WGDashboardAdmin/assets/clientViewer-zQHAiseB.js
vendored
Normal file
1
src/static/dist/WGDashboardAdmin/assets/clientViewer-zQHAiseB.js
vendored
Normal file
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
1
src/static/dist/WGDashboardAdmin/assets/clients-dPj1ZA29.js
vendored
Normal file
1
src/static/dist/WGDashboardAdmin/assets/clients-dPj1ZA29.js
vendored
Normal file
File diff suppressed because one or more lines are too long
@@ -1 +0,0 @@
|
||||
import{_ as r,c as i,a as e,d as o,w as t,k as l,j as a,l as _,S as d,h as u}from"./index-BQYwwrw6.js";const m={name:"configuration"},p={class:"mt-md-5 mt-3 text-body"};function f(h,k,x,w,$,v){const n=u("RouterView");return e(),i("div",p,[o(n,null,{default:t(({Component:s,route:c})=>[o(l,{name:"fade2",mode:"out-in"},{default:t(()=>[(e(),a(d,null,{default:t(()=>[(e(),a(_(s),{key:c.path,class:"z-1"}))]),_:2},1024))]),_:2},1024)]),_:1})])}const B=r(m,[["render",f]]);export{B as default};
|
||||
1
src/static/dist/WGDashboardAdmin/assets/configuration-CwxFz-wp.js
vendored
Normal file
1
src/static/dist/WGDashboardAdmin/assets/configuration-CwxFz-wp.js
vendored
Normal file
@@ -0,0 +1 @@
|
||||
import{_ as r,c as i,b as o,w as e,k as l,j as a,l as _,S as u,h as d,f as t}from"./index-DXzxfcZW.js";const m={name:"configuration"},f={class:"mt-md-5 mt-3 text-body"};function p(h,k,x,w,$,v){const n=d("RouterView");return t(),i("div",f,[o(n,null,{default:e(({Component:s,route:c})=>[o(l,{name:"fade2",mode:"out-in"},{default:e(()=>[(t(),a(u,null,{default:e(()=>[(t(),a(_(s),{key:c.path,class:"z-1"}))]),_:2},1024))]),_:2},1024)]),_:1})])}const B=r(m,[["render",p]]);export{B as default};
|
||||
File diff suppressed because one or more lines are too long
1
src/static/dist/WGDashboardAdmin/assets/configurationList-BcQfpCge.js
vendored
Normal file
1
src/static/dist/WGDashboardAdmin/assets/configurationList-BcQfpCge.js
vendored
Normal file
File diff suppressed because one or more lines are too long
1
src/static/dist/WGDashboardAdmin/assets/configurationList-CG9tP7oL.css
vendored
Normal file
1
src/static/dist/WGDashboardAdmin/assets/configurationList-CG9tP7oL.css
vendored
Normal file
@@ -0,0 +1 @@
|
||||
.fade-enter-active[data-v-9f596f5e]{transition-delay:var(--v0d365bfc)!important}.progress-bar[data-v-01ef60a9]{width:0;transition:all 1s cubic-bezier(.42,0,.22,1)}.filter a[data-v-7ed053f0]{text-decoration:none}
|
||||
@@ -1 +0,0 @@
|
||||
.fade-enter-active[data-v-9f596f5e]{transition-delay:var(--0d365bfc)!important}.progress-bar[data-v-851170e4]{width:0;transition:all 1s cubic-bezier(.42,0,.22,1)}.filter a[data-v-7ed053f0]{text-decoration:none}
|
||||
File diff suppressed because one or more lines are too long
@@ -1 +1 @@
|
||||
import{_ as f,c as i,a as n,b as t,d as u,h as w,e as k,m as x,y,n as p,t as v,z as _,D as m,W as b,A as S,r as D,q as $,F as W,i as V}from"./index-BQYwwrw6.js";import{L as C}from"./localeText-C3GiyveB.js";const F={name:"dashboardSettingsInputWireguardConfigurationPath",components:{LocaleText:C},props:{targetData:String,title:String,warning:!1,warningText:""},setup(){const o=m(),s=b(),r=`input_${S()}`;return{store:o,uuid:r,WireguardConfigurationStore:s}},data(){return{value:"",invalidFeedback:"",showInvalidFeedback:!1,isValid:!1,timeout:void 0,changed:!1,updating:!1}},mounted(){this.value=this.store.Configuration.Server[this.targetData]},methods:{async useValidation(){this.changed&&(this.updating=!0,await _("/api/updateDashboardConfigurationItem",{section:"Server",key:this.targetData,value:this.value},o=>{o.status?(this.isValid=!0,this.showInvalidFeedback=!1,this.store.Configuration.Account[this.targetData]=this.value,clearTimeout(this.timeout),this.timeout=setTimeout(()=>this.isValid=!1,5e3),this.WireguardConfigurationStore.getConfigurations(),this.store.newMessage("Server","WireGuard configuration path saved","success")):(this.isValid=!1,this.showInvalidFeedback=!0,this.invalidFeedback=o.message),this.changed=!1,this.updating=!1}))}}},I={class:"card"},T={class:"card-header"},A={class:"my-2"},L={class:"card-body"},M={class:"form-group"},N=["for"],P={class:"d-flex gap-2 align-items-start"},B={class:"flex-grow-1"},G=["id","disabled"],z={class:"invalid-feedback fw-bold"},U=["disabled"],q={key:0,class:"bi bi-save2-fill"},E={key:1,class:"spinner-border spinner-border-sm"},K={key:0,class:"px-2 py-1 text-warning-emphasis bg-warning-subtle border border-warning-subtle rounded-2 d-inline-block mt-1 mb-2"};function j(o,s,r,a,c,g){const d=w("LocaleText");return n(),i("div",I,[t("div",T,[t("h6",A,[u(d,{t:"Path"})])]),t("div",L,[t("div",M,[t("label",{for:this.uuid,class:"text-muted mb-1"},[t("strong",null,[t("small",null,[u(d,{t:this.title},null,8,["t"])])])],8,N),t("div",P,[t("div",B,[x(t("input",{type:"text",class:p(["form-control rounded-3",{"is-invalid":this.showInvalidFeedback,"is-valid":this.isValid}]),id:this.uuid,"onUpdate:modelValue":s[0]||(s[0]=e=>this.value=e),onKeydown:s[1]||(s[1]=e=>this.changed=!0),disabled:this.updating},null,42,G),[[y,this.value]]),t("div",z,v(this.invalidFeedback),1)]),t("button",{onClick:s[2]||(s[2]=e=>this.useValidation()),disabled:!this.changed,class:"ms-auto btn rounded-3 border-success-subtle bg-success-subtle text-success-emphasis"},[this.updating?(n(),i("span",E)):(n(),i("i",q))],8,U)]),r.warning?(n(),i("div",K,[t("small",null,[s[3]||(s[3]=t("i",{class:"bi bi-exclamation-triangle-fill me-2"},null,-1)),u(d,{t:r.warningText},null,8,["t"])])])):k("",!0)])])])}const et=f(F,[["render",j]]),H={class:"card rounded-3"},J={class:"card-header"},O={class:"my-2"},Q={class:"card-body d-flex gap-2"},R={class:"list-group w-100"},X=["onClick"],Y={__name:"dashboardSettingsWireguardConfigurationAutostart",setup(o){const s=m(),r=b(),a=D(s.Configuration.WireGuardConfiguration.autostart),c=$(()=>r.Configurations.map(e=>e.Name)),g=async()=>{await _("/api/updateDashboardConfigurationItem",{section:"WireGuardConfiguration",key:"autostart",value:a.value},async e=>{e.status?(s.newMessage("Server","Start up configurations saved","success"),a.value=e.data):s.newMessage("Server","Start up configurations failed to save","danger")})},d=e=>{a.value.includes(e)?a.value=a.value.filter(h=>h!==e):a.value.push(e),g()};return(e,h)=>(n(),i("div",H,[t("div",J,[t("h6",O,[u(C,{t:"Toggle When Start Up"})])]),t("div",Q,[t("div",R,[(n(!0),i(W,null,V(c.value,l=>(n(),i("button",{type:"button",key:l,onClick:Z=>d(l),class:"list-group-item list-group-item-action py-2 w-100 d-flex align-items-center"},[t("samp",null,v(l),1),t("i",{class:p(["ms-auto",[a.value.includes(l)?"bi-check-circle-fill":"bi-circle"]])},null,2)],8,X))),128))])])]))}},at=f(Y,[["__scopeId","data-v-4aa2aed9"]]);export{et as D,at as a};
|
||||
import{_ as f,c as i,a as t,b as u,h as w,d as k,m as x,y,n as p,t as v,z as _,D as m,W as b,A as S,f as n,r as D,q as $,F as W,i as V}from"./index-DXzxfcZW.js";import{L as C}from"./localeText-Dmcj5qqx.js";const F={name:"dashboardSettingsInputWireguardConfigurationPath",components:{LocaleText:C},props:{targetData:String,title:String,warning:!1,warningText:""},setup(){const o=m(),s=b(),r=`input_${S()}`;return{store:o,uuid:r,WireguardConfigurationStore:s}},data(){return{value:"",invalidFeedback:"",showInvalidFeedback:!1,isValid:!1,timeout:void 0,changed:!1,updating:!1}},mounted(){this.value=this.store.Configuration.Server[this.targetData]},methods:{async useValidation(){this.changed&&(this.updating=!0,await _("/api/updateDashboardConfigurationItem",{section:"Server",key:this.targetData,value:this.value},o=>{o.status?(this.isValid=!0,this.showInvalidFeedback=!1,this.store.Configuration.Account[this.targetData]=this.value,clearTimeout(this.timeout),this.timeout=setTimeout(()=>this.isValid=!1,5e3),this.WireguardConfigurationStore.getConfigurations(),this.store.newMessage("Server","WireGuard configuration path saved","success")):(this.isValid=!1,this.showInvalidFeedback=!0,this.invalidFeedback=o.message),this.changed=!1,this.updating=!1}))}}},I={class:"card"},T={class:"card-header"},A={class:"my-2"},L={class:"card-body"},M={class:"form-group"},N=["for"],P={class:"d-flex gap-2 align-items-start"},B={class:"flex-grow-1"},G=["id","disabled"],z={class:"invalid-feedback fw-bold"},U=["disabled"],q={key:0,class:"bi bi-save2-fill"},E={key:1,class:"spinner-border spinner-border-sm"},K={key:0,class:"px-2 py-1 text-warning-emphasis bg-warning-subtle border border-warning-subtle rounded-2 d-inline-block mt-1 mb-2"};function j(o,s,r,a,c,g){const d=w("LocaleText");return n(),i("div",I,[t("div",T,[t("h6",A,[u(d,{t:"Path"})])]),t("div",L,[t("div",M,[t("label",{for:this.uuid,class:"text-muted mb-1"},[t("strong",null,[t("small",null,[u(d,{t:this.title},null,8,["t"])])])],8,N),t("div",P,[t("div",B,[x(t("input",{type:"text",class:p(["form-control rounded-3",{"is-invalid":this.showInvalidFeedback,"is-valid":this.isValid}]),id:this.uuid,"onUpdate:modelValue":s[0]||(s[0]=e=>this.value=e),onKeydown:s[1]||(s[1]=e=>this.changed=!0),disabled:this.updating},null,42,G),[[y,this.value]]),t("div",z,v(this.invalidFeedback),1)]),t("button",{onClick:s[2]||(s[2]=e=>this.useValidation()),disabled:!this.changed,class:"ms-auto btn rounded-3 border-success-subtle bg-success-subtle text-success-emphasis"},[this.updating?(n(),i("span",E)):(n(),i("i",q))],8,U)]),r.warning?(n(),i("div",K,[t("small",null,[s[3]||(s[3]=t("i",{class:"bi bi-exclamation-triangle-fill me-2"},null,-1)),u(d,{t:r.warningText},null,8,["t"])])])):k("",!0)])])])}const et=f(F,[["render",j]]),H={class:"card rounded-3"},J={class:"card-header"},O={class:"my-2"},Q={class:"card-body d-flex gap-2"},R={class:"list-group w-100"},X=["onClick"],Y={__name:"dashboardSettingsWireguardConfigurationAutostart",setup(o){const s=m(),r=b(),a=D(s.Configuration.WireGuardConfiguration.autostart),c=$(()=>r.Configurations.map(e=>e.Name)),g=async()=>{await _("/api/updateDashboardConfigurationItem",{section:"WireGuardConfiguration",key:"autostart",value:a.value},async e=>{e.status?(s.newMessage("Server","Start up configurations saved","success"),a.value=e.data):s.newMessage("Server","Start up configurations failed to save","danger")})},d=e=>{a.value.includes(e)?a.value=a.value.filter(h=>h!==e):a.value.push(e),g()};return(e,h)=>(n(),i("div",H,[t("div",J,[t("h6",O,[u(C,{t:"Toggle When Start Up"})])]),t("div",Q,[t("div",R,[(n(!0),i(W,null,V(c.value,l=>(n(),i("button",{type:"button",key:l,onClick:Z=>d(l),class:"list-group-item list-group-item-action py-2 w-100 d-flex align-items-center"},[t("samp",null,v(l),1),t("i",{class:p(["ms-auto",[a.value.includes(l)?"bi-check-circle-fill":"bi-circle"]])},null,2)],8,X))),128))])])]))}},at=f(Y,[["__scopeId","data-v-4aa2aed9"]]);export{et as D,at as a};
|
||||
1
src/static/dist/WGDashboardAdmin/assets/dashboardWebHooks-BrixRm6N.js
vendored
Normal file
1
src/static/dist/WGDashboardAdmin/assets/dashboardWebHooks-BrixRm6N.js
vendored
Normal file
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user