diff --git a/.dockerignore b/.dockerignore index e2c608c2..7aaadbd9 100644 --- a/.dockerignore +++ b/.dockerignore @@ -2,4 +2,8 @@ .github *.md tests/ -docs/ \ No newline at end of file +docs/ +src/db +src/wg-dashboard.ini +src/static/app +src/static/client diff --git a/.github/workflows/docker.yml b/.github/workflows/docker.yml index b7604428..8d09c180 100644 --- a/.github/workflows/docker.yml +++ b/.github/workflows/docker.yml @@ -103,7 +103,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 diff --git a/README.md b/README.md index 8e76da71..eddc2dcf 100644 --- a/README.md +++ b/README.md @@ -5,7 +5,6 @@ > 🎉 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! - ![](https://wgdashboard-resources.tor1.cdn.digitaloceanspaces.com/Posters/Banner.png) diff --git a/docker/Dockerfile b/docker/Dockerfile index e0b0b3b4..b6630c23 100644 --- a/docker/Dockerfile +++ b/docker/Dockerfile @@ -4,7 +4,7 @@ # # Pull the current golang-alpine image. -FROM golang:1.25-alpine AS awg-go +FROM golang:1.26-alpine3.23 AS awg-go # Install build-dependencies. RUN apk add --no-cache \ @@ -30,7 +30,7 @@ RUN go version && \ # AWG TOOLS BUILDING STAGE # Base: Alpine # -FROM alpine:latest AS awg-tools +FROM alpine:3.23 AS awg-tools # Install needed dependencies. RUN apk add --no-cache \ @@ -55,7 +55,7 @@ RUN make && chmod +x wg* # # Use the python-alpine image for building pip dependencies -FROM python:3.14-alpine AS pip-builder +FROM python:3.14-alpine3.23 AS pip-builder ARG TARGETPLATFORM @@ -91,7 +91,7 @@ RUN . /opt/wgdashboard/src/venv/bin/activate && \ # # Running with the python-alpine image. -FROM python:3.14-alpine AS final +FROM python:3.14-alpine3.23 AS final LABEL maintainer="dselen@nerthus.nl" # Install only the runtime dependencies @@ -114,15 +114,18 @@ 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 needed for operation -RUN mkdir /data /configs -p ${WGDASH}/src /etc/amnezia/amneziawg +RUN mkdir /data /configs -p ${WGDASH}/src /etc/amnezia/amneziawg \ + && echo "name_servers=${global_dns}" >> /etc/resolvconf.conf # 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 diff --git a/docker/README.md b/docker/README.md index 9a39a07c..b6966712 100644 --- a/docker/README.md +++ b/docker/README.md @@ -23,7 +23,7 @@ To get the container running you either pull the pre-made image from a remote re - ghcr.io/wgdashboard/wgdashboard: - docker.io/donaldzou/wgdashboard: -> tags should be either: latest, main, or . +> tags should be either: latest, main, , (if built) or . From there either use the environment variables described below as parameters or use the Docker Compose file: `compose.yaml`.
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.
@@ -95,23 +95,29 @@ Updating the WGDashboard container should be through 'The Docker Way' - by pulli ## ⚙️ Environment Variables -| Variable | Accepted Values | Default | Example | Description | -| ------------------ | ---------------------------------------- | ----------------------- | ------------------------ | ----------------------------------------------------------------------- | -| `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. | -| `wgd_port` | Any port that is allowed for the process | `10086` | `443` | This port is used to set the WGDashboard web port. | -| `username` | Any non‐empty string | `-` | `admin` | Username for the WGDashboard web interface account. | -| `password` | Any non‐empty string | `-` | `s3cr3tP@ss` | Password for the WGDashboard web interface account (stored hashed). | -| `enable_totp` | `true`, `false` | `true` | `false` | Enable TOTP‐based two‐factor authentication for the account. | -| `wg_autostart` | Wireguard interface name | `-` | `wg0` or `wg0\|\|wg1\|\|wg2` | Auto‐start the WireGuard interface when the container launches. | -| `email_server` | SMTP server address | `-` | `smtp.gmail.com` | SMTP server for sending email notifications. | -| `email_port` | SMTP port number | `-` | `587` | Port for connecting to the SMTP server. | -| `email_encryption` | `TLS`, `SSL`, etc. | `-` | `TLS` | Encryption method for email communication. | -| `email_username` | Any non-empty string | `-` | `user@example.com` | Username for SMTP authentication. | -| `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. | +| 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. | +| `wgd_port` | Any port that is allowed for the process | `10086` | `443` | This port is used to set the WGDashboard web port. | +| `username` | Any non‐empty string | `-` | `admin` | Username for the WGDashboard web interface account. | +| `password` | Any non‐empty string | `-` | `s3cr3tP@ss` | Password for the WGDashboard web interface account (stored hashed). | +| `enable_totp` | `true`, `false` | `true` | `false` | Enable TOTP‐based two‐factor authentication for the account. | +| `wg_autostart` | Wireguard interface name | `false` | `true` | Auto‐start the WireGuard client when the container launches. | +| `email_server` | SMTP server address | `-` | `smtp.gmail.com` | SMTP server for sending email notifications. | +| `email_port` | SMTP port number | `-` | `587` | Port for connecting to the SMTP server. | +| `email_encryption` | `TLS`, `SSL`, etc. | `-` | `TLS` | Encryption method for email communication. | +| `email_username` | Any non-empty string | `-` | `user@example.com` | Username for SMTP authentication. | +| `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. | --- diff --git a/docker/compose.yaml b/docker/compose.yaml index d8f2eac9..445c3102 100644 --- a/docker/compose.yaml +++ b/docker/compose.yaml @@ -13,6 +13,7 @@ services: # 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. diff --git a/docker/entrypoint.sh b/docker/entrypoint.sh index 8b34ad81..fe2d2dff 100644 --- a/docker/entrypoint.sh +++ b/docker/entrypoint.sh @@ -85,8 +85,6 @@ 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 @@ -102,23 +100,51 @@ ensure_installation() { 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 # Setup WireGuard if needed @@ -142,14 +168,25 @@ set_envvars() { # 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}" @@ -183,6 +220,24 @@ set_envvars() { 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 @@ -207,6 +262,9 @@ set_envvars() { 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 diff --git a/docker/wg-dashboard-oidc-providers.json.template b/docker/wg-dashboard-oidc-providers.json.template new file mode 100644 index 00000000..3764d0d2 --- /dev/null +++ b/docker/wg-dashboard-oidc-providers.json.template @@ -0,0 +1,16 @@ +{ + "Admin": { + "Provider": { + "client_id": "", + "client_secret": "", + "issuer": "" + } + }, + "Client": { + "Provider": { + "client_id": "", + "client_secret": "", + "issuer": "" + } + } +} diff --git a/src/client.py b/src/client.py index cc7ac218..7d089c29 100644 --- a/src/client.py +++ b/src/client.py @@ -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/') + @client.get(f'{prefix}/img/') + 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') diff --git a/src/dashboard.py b/src/dashboard.py index ff718a15..a61be851 100644 --- a/src/dashboard.py +++ b/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,11 +95,13 @@ def peerInformationBackgroundThread(): c.getPeersTransfer() c.getPeersEndpoint() c.getPeers() - if delay == 6: - if c.configurationInfo.PeerTrafficTracking: - c.logPeersTraffic() - if c.configurationInfo.PeerHistoricalEndpointTracking: - 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) @@ -161,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.") @@ -264,7 +269,8 @@ def auth_req(): ("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}/static') + and not request.path.startswith(f'{appPrefix}/img') + and not request.path.startswith(f'{appPrefix}/assets') and request.path not in whiteList ): response = Flask.make_response(app, { @@ -422,11 +428,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') @@ -523,7 +529,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) @@ -562,8 +568,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(): @@ -706,15 +712,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, @@ -864,6 +885,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: @@ -919,7 +941,7 @@ def API_addPeers(configName): "endpoint_allowed_ip": endpoint_allowed_ip, "mtu": mtu, "keepalive": keep_alive, - "advanced_security": "off" + "notes": "" }) if addedCount == bulkAddAmount: break @@ -962,8 +984,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 @@ -981,14 +1006,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") @@ -1125,13 +1149,24 @@ def API_GetPeerTraffics(): @app.get(f'{APP_PREFIX}/api/getPeerTrackingTableCounts') def API_GetPeerTrackingTableCounts(): configurationName = request.args.get("configurationName") - if configurationName not in WireguardConfigurations.keys(): + if configurationName and configurationName not in WireguardConfigurations.keys(): return ResponseObject(False, "Configuration does not exist") - c = WireguardConfigurations.get(configurationName) - return ResponseObject(data={ - "TrafficTrackingTableSize": c.getTransferTableSize(), - "HistoricalTrackingTableSize": c.getHistoricalEndpointTableSize() - }) + + 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(): @@ -1325,12 +1360,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") @@ -1700,9 +1740,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) \ No newline at end of file + app.run(host=app_ip, debug=False, port=app_port) diff --git a/src/gunicorn.conf.py b/src/gunicorn.conf.py index cba84b21..f046bb86 100644 --- a/src/gunicorn.conf.py +++ b/src/gunicorn.conf.py @@ -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,7 +17,7 @@ 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" diff --git a/src/modules/AmneziaWireguardConfiguration.py b/src/modules/AmneziaConfiguration.py similarity index 63% rename from src/modules/AmneziaWireguardConfiguration.py rename to src/modules/AmneziaConfiguration.py index 6ada7d5f..6ddba994 100644 --- a/src/modules/AmneziaWireguardConfiguration.py +++ b/src/modules/AmneziaConfiguration.py @@ -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,15 +144,20 @@ 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 ) self.metadata.create_all(self.engine) def getPeers(self): - self.Peers.clear() + self.Peers.clear() if self.configurationFileChanged(): with open(self.configPath, 'r') as configFile: p = [] @@ -211,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, @@ -229,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 "" } @@ -243,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 = { @@ -258,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 = { @@ -278,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) @@ -293,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']) @@ -311,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): @@ -319,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)) \ No newline at end of file + self.RestrictedPeers.append(AmneziaPeer(i, self)) diff --git a/src/modules/AmneziaPeer.py b/src/modules/AmneziaPeer.py new file mode 100644 index 00000000..509f4305 --- /dev/null +++ b/src/modules/AmneziaPeer.py @@ -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" diff --git a/src/modules/AmneziaWGPeer.py b/src/modules/AmneziaWGPeer.py deleted file mode 100644 index 17101b6b..00000000 --- a/src/modules/AmneziaWGPeer.py +++ /dev/null @@ -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() \ No newline at end of file diff --git a/src/modules/DashboardClients.py b/src/modules/DashboardClients.py index 231141b1..54ba4802 100644 --- a/src/modules/DashboardClients.py +++ b/src/modules/DashboardClients.py @@ -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 diff --git a/src/modules/DashboardClientsPeerAssignment.py b/src/modules/DashboardClientsPeerAssignment.py index 80722d06..c507e5aa 100644 --- a/src/modules/DashboardClientsPeerAssignment.py +++ b/src/modules/DashboardClientsPeerAssignment.py @@ -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 diff --git a/src/modules/DashboardClientsTOTP.py b/src/modules/DashboardClientsTOTP.py index e3830fb5..e3e1f5f6 100644 --- a/src/modules/DashboardClientsTOTP.py +++ b/src/modules/DashboardClientsTOTP.py @@ -3,7 +3,7 @@ import hashlib import uuid import sqlalchemy as db -from .ConnectionString import ConnectionString +from .DatabaseConnection import ConnectionString class DashboardClientsTOTP: diff --git a/src/modules/DashboardConfig.py b/src/modules/DashboardConfig.py index 5fe28626..3ae7ab3f 100644 --- a/src/modules/DashboardConfig.py +++ b/src/modules/DashboardConfig.py @@ -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.2' + 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, diff --git a/src/modules/DashboardLogger.py b/src/modules/DashboardLogger.py index 9b4e1f24..5d2b8ced 100644 --- a/src/modules/DashboardLogger.py +++ b/src/modules/DashboardLogger.py @@ -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: diff --git a/src/modules/DashboardWebHooks.py b/src/modules/DashboardWebHooks.py index a598444b..ebaf43cd 100644 --- a/src/modules/DashboardWebHooks.py +++ b/src/modules/DashboardWebHooks.py @@ -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'] diff --git a/src/modules/ConnectionString.py b/src/modules/DatabaseConnection.py similarity index 88% rename from src/modules/ConnectionString.py rename to src/modules/DatabaseConnection.py index 77f69644..e3d7d1a0 100644 --- a/src/modules/ConnectionString.py +++ b/src/modules/DatabaseConnection.py @@ -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 \ No newline at end of file diff --git a/src/modules/Email.py b/src/modules/Email.py index 145a4d72..d607973c 100644 --- a/src/modules/Email.py +++ b/src/modules/Email.py @@ -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" \ No newline at end of file + 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 diff --git a/src/modules/NewConfigurationTemplates.py b/src/modules/NewConfigurationTemplates.py index 9c4511a4..05acceaa 100644 --- a/src/modules/NewConfigurationTemplates.py +++ b/src/modules/NewConfigurationTemplates.py @@ -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): diff --git a/src/modules/Peer.py b/src/modules/Peer.py index 9201a9f0..970e747f 100644 --- a/src/modules/Peer.py +++ b/src/modules/Peer.py @@ -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] + # 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 allowed_ip in existingAllowedIps: + 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 ValidateIPAddressesWithRange(endpoint_allowed_ip): - return False, f"Endpoint Allowed IPs format is incorrect" + if not ValidateDNSAddress(dns_addresses): + return False, f"DNS IP-Address or FQDN 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: + 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,12 +163,14 @@ 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): @@ -163,10 +196,17 @@ 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 = { @@ -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}" \ No newline at end of file + return f"{int(hours):02}:{int(minutes):02}:{int(seconds):02}" diff --git a/src/modules/PeerJobLogger.py b/src/modules/PeerJobLogger.py index 9f121971..4e74b822 100644 --- a/src/modules/PeerJobLogger.py +++ b/src/modules/PeerJobLogger.py @@ -8,7 +8,7 @@ import sqlalchemy as db from flask import current_app from sqlalchemy import RowMapping -from .ConnectionString import ConnectionString +from .DatabaseConnection import ConnectionString from .Log import Log class PeerJobLogger: diff --git a/src/modules/PeerJobs.py b/src/modules/PeerJobs.py index 274a4263..c24e47fc 100644 --- a/src/modules/PeerJobs.py +++ b/src/modules/PeerJobs.py @@ -3,7 +3,7 @@ Peer Jobs """ import sqlalchemy -from .ConnectionString import ConnectionString +from .DatabaseConnection import ConnectionString from .PeerJob import PeerJob from .PeerJobLogger import PeerJobLogger import sqlalchemy as db diff --git a/src/modules/PeerShareLinks.py b/src/modules/PeerShareLinks.py index 206e2fd0..eec6e898 100644 --- a/src/modules/PeerShareLinks.py +++ b/src/modules/PeerShareLinks.py @@ -1,4 +1,4 @@ -from .ConnectionString import ConnectionString +from .DatabaseConnection import ConnectionString from .PeerShareLink import PeerShareLink import sqlalchemy as db from datetime import datetime diff --git a/src/modules/Utilities.py b/src/modules/Utilities.py index 0ba24066..661d3500 100644 --- a/src/modules/Utilities.py +++ b/src/modules/Utilities.py @@ -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 \ No newline at end of file + return True, None diff --git a/src/modules/WireguardConfiguration.py b/src/modules/WireguardConfiguration.py index f1fdfe16..548dfcbe 100644 --- a/src/modules/WireguardConfiguration.py +++ b/src/modules/WireguardConfiguration.py @@ -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), @@ -404,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]") @@ -439,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, @@ -454,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 "" } @@ -526,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 = { @@ -546,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"] } @@ -560,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']) @@ -577,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): @@ -615,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 @@ -636,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], @@ -665,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)" @@ -719,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() @@ -768,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] @@ -783,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: @@ -826,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: @@ -847,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")) @@ -921,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, @@ -995,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]") @@ -1033,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, @@ -1065,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): @@ -1226,7 +1241,6 @@ class WireguardConfiguration: 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: @@ -1291,4 +1305,4 @@ class WireguardConfiguration: conn.execute(sqlalchemy.text('VACUUM;')) except Exception as e: return False - return True \ No newline at end of file + return True diff --git a/src/static/app/index.html b/src/static/app/index.html index 0c731866..afa86e31 100644 --- a/src/static/app/index.html +++ b/src/static/app/index.html @@ -6,14 +6,21 @@ - - + + WGDashboard + +
- diff --git a/src/static/app/package-lock.json b/src/static/app/package-lock.json index d328fe46..94f3f647 100644 --- a/src/static/app/package-lock.json +++ b/src/static/app/package-lock.json @@ -1,22 +1,22 @@ { "name": "app", - "version": "4.3.1", + "version": "4.3.3", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "app", - "version": "4.3.1", + "version": "4.3.3", "dependencies": { "@volar/language-server": "2.4.28", "@vue/language-server": "3.2.4", "@vuepic/vue-datepicker": "^12.1.0", - "@vueuse/core": "^14.2.0", "@vueuse/shared": "^14.2.1", + "@vueuse/core": "^14.2.1", "animate.css": "^4.1.1", "bootstrap": "^5.3.2", "bootstrap-icons": "^1.11.3", - "cidr-tools": "^11.0.8", + "cidr-tools": "^11.3.2", "css-color-converter": "^2.0.0", "dayjs": "^1.11.19", "electron-builder": "^26.7.0", @@ -143,9 +143,9 @@ } }, "node_modules/@electron/asar/node_modules/minimatch": { - "version": "3.1.2", - "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.2.tgz", - "integrity": "sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw==", + "version": "3.1.5", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.5.tgz", + "integrity": "sha512-VgjWUsnnT6n+NUk6eZq77zeFdpW2LWDzP6zFGrCbHXiYNul5Dzqk2HHQ5uFH2DNW5Xbp8+jVzaeNt94ssEEl4w==", "license": "ISC", "dependencies": { "brace-expansion": "^1.1.7" @@ -353,9 +353,9 @@ } }, "node_modules/@electron/universal/node_modules/brace-expansion": { - "version": "2.0.2", - "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-2.0.2.tgz", - "integrity": "sha512-Jt0vHyM+jmUBqojB7E1NIYadt0vI0Qxjxd2TErW94wDz+E2LAm5vKMXXwg6ZZBTHPuUlDgQHKXvjGBdfcF1ZDQ==", + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-2.0.3.tgz", + "integrity": "sha512-MCV/fYJEbqx68aE58kv2cA/kiky1G8vux3OR6/jbS+jIMe/6fJWa0DTzJU7dqijOWYwHi1t29FlfYI9uytqlpA==", "license": "MIT", "dependencies": { "balanced-match": "^1.0.0" @@ -376,12 +376,12 @@ } }, "node_modules/@electron/universal/node_modules/minimatch": { - "version": "9.0.5", - "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-9.0.5.tgz", - "integrity": "sha512-G6T0ZX48xgozx7587koeX9Ys2NYy6Gmv//P89sEte9V9whIapMNF4idKxnW2QtCcLiTWlb/wfCabAtAFWhhBow==", + "version": "9.0.9", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-9.0.9.tgz", + "integrity": "sha512-OBwBN9AL4dqmETlpS2zasx+vTeWclWzkblfZk7KTA5j3jeOONz/tRCnZomUyvNg83wL5Zv9Ss6HMJXAgL8R2Yg==", "license": "ISC", "dependencies": { - "brace-expansion": "^2.0.1" + "brace-expansion": "^2.0.2" }, "engines": { "node": ">=16 || 14 >=14.17" @@ -986,27 +986,6 @@ } } }, - "node_modules/@isaacs/balanced-match": { - "version": "4.0.1", - "resolved": "https://registry.npmjs.org/@isaacs/balanced-match/-/balanced-match-4.0.1.tgz", - "integrity": "sha512-yzMTt9lEb8Gv7zRioUilSglI0c0smZ9k5D65677DLWLtWJaXIS3CqcGyUFByYKlnUj6TkjLVs54fBl6+TiGQDQ==", - "license": "MIT", - "engines": { - "node": "20 || >=22" - } - }, - "node_modules/@isaacs/brace-expansion": { - "version": "5.0.0", - "resolved": "https://registry.npmjs.org/@isaacs/brace-expansion/-/brace-expansion-5.0.0.tgz", - "integrity": "sha512-ZT55BDLV0yv0RBm2czMiZ+SqCGO7AvmOM3G/w2xhVPH+te0aKgFjmBvGlL1dH+ql2tgGO3MVrbb3jCKyvpgnxA==", - "license": "MIT", - "dependencies": { - "@isaacs/balanced-match": "^4.0.1" - }, - "engines": { - "node": "20 || >=22" - } - }, "node_modules/@isaacs/cliui": { "version": "8.0.2", "resolved": "https://registry.npmjs.org/@isaacs/cliui/-/cliui-8.0.2.tgz", @@ -1294,9 +1273,9 @@ "license": "MIT" }, "node_modules/@rollup/rollup-android-arm-eabi": { - "version": "4.50.2", - "resolved": "https://registry.npmmirror.com/@rollup/rollup-android-arm-eabi/-/rollup-android-arm-eabi-4.50.2.tgz", - "integrity": "sha512-uLN8NAiFVIRKX9ZQha8wy6UUs06UNSZ32xj6giK/rmMXAgKahwExvK6SsmgU5/brh4w/nSgj8e0k3c1HBQpa0A==", + "version": "4.60.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm-eabi/-/rollup-android-arm-eabi-4.60.1.tgz", + "integrity": "sha512-d6FinEBLdIiK+1uACUttJKfgZREXrF0Qc2SmLII7W2AD8FfiZ9Wjd+rD/iRuf5s5dWrr1GgwXCvPqOuDquOowA==", "cpu": [ "arm" ], @@ -1308,9 +1287,9 @@ ] }, "node_modules/@rollup/rollup-android-arm64": { - "version": "4.50.2", - "resolved": "https://registry.npmmirror.com/@rollup/rollup-android-arm64/-/rollup-android-arm64-4.50.2.tgz", - "integrity": "sha512-oEouqQk2/zxxj22PNcGSskya+3kV0ZKH+nQxuCCOGJ4oTXBdNTbv+f/E3c74cNLeMO1S5wVWacSws10TTSB77g==", + "version": "4.60.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm64/-/rollup-android-arm64-4.60.1.tgz", + "integrity": "sha512-YjG/EwIDvvYI1YvYbHvDz/BYHtkY4ygUIXHnTdLhG+hKIQFBiosfWiACWortsKPKU/+dUwQQCKQM3qrDe8c9BA==", "cpu": [ "arm64" ], @@ -1322,9 +1301,9 @@ ] }, "node_modules/@rollup/rollup-darwin-arm64": { - "version": "4.50.2", - "resolved": "https://registry.npmmirror.com/@rollup/rollup-darwin-arm64/-/rollup-darwin-arm64-4.50.2.tgz", - "integrity": "sha512-OZuTVTpj3CDSIxmPgGH8en/XtirV5nfljHZ3wrNwvgkT5DQLhIKAeuFSiwtbMto6oVexV0k1F1zqURPKf5rI1Q==", + "version": "4.60.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-arm64/-/rollup-darwin-arm64-4.60.1.tgz", + "integrity": "sha512-mjCpF7GmkRtSJwon+Rq1N8+pI+8l7w5g9Z3vWj4T7abguC4Czwi3Yu/pFaLvA3TTeMVjnu3ctigusqWUfjZzvw==", "cpu": [ "arm64" ], @@ -1336,9 +1315,9 @@ ] }, "node_modules/@rollup/rollup-darwin-x64": { - "version": "4.50.2", - "resolved": "https://registry.npmmirror.com/@rollup/rollup-darwin-x64/-/rollup-darwin-x64-4.50.2.tgz", - "integrity": "sha512-Wa/Wn8RFkIkr1vy1k1PB//VYhLnlnn5eaJkfTQKivirOvzu5uVd2It01ukeQstMursuz7S1bU+8WW+1UPXpa8A==", + "version": "4.60.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-x64/-/rollup-darwin-x64-4.60.1.tgz", + "integrity": "sha512-haZ7hJ1JT4e9hqkoT9R/19XW2QKqjfJVv+i5AGg57S+nLk9lQnJ1F/eZloRO3o9Scy9CM3wQ9l+dkXtcBgN5Ew==", "cpu": [ "x64" ], @@ -1350,9 +1329,9 @@ ] }, "node_modules/@rollup/rollup-freebsd-arm64": { - "version": "4.50.2", - "resolved": "https://registry.npmmirror.com/@rollup/rollup-freebsd-arm64/-/rollup-freebsd-arm64-4.50.2.tgz", - "integrity": "sha512-QkzxvH3kYN9J1w7D1A+yIMdI1pPekD+pWx7G5rXgnIlQ1TVYVC6hLl7SOV9pi5q9uIDF9AuIGkuzcbF7+fAhow==", + "version": "4.60.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-freebsd-arm64/-/rollup-freebsd-arm64-4.60.1.tgz", + "integrity": "sha512-czw90wpQq3ZsAVBlinZjAYTKduOjTywlG7fEeWKUA7oCmpA8xdTkxZZlwNJKWqILlq0wehoZcJYfBvOyhPTQ6w==", "cpu": [ "arm64" ], @@ -1364,9 +1343,9 @@ ] }, "node_modules/@rollup/rollup-freebsd-x64": { - "version": "4.50.2", - "resolved": "https://registry.npmmirror.com/@rollup/rollup-freebsd-x64/-/rollup-freebsd-x64-4.50.2.tgz", - "integrity": "sha512-dkYXB0c2XAS3a3jmyDkX4Jk0m7gWLFzq1C3qUnJJ38AyxIF5G/dyS4N9B30nvFseCfgtCEdbYFhk0ChoCGxPog==", + "version": "4.60.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-freebsd-x64/-/rollup-freebsd-x64-4.60.1.tgz", + "integrity": "sha512-KVB2rqsxTHuBtfOeySEyzEOB7ltlB/ux38iu2rBQzkjbwRVlkhAGIEDiiYnO2kFOkJp+Z7pUXKyrRRFuFUKt+g==", "cpu": [ "x64" ], @@ -1378,9 +1357,9 @@ ] }, "node_modules/@rollup/rollup-linux-arm-gnueabihf": { - "version": "4.50.2", - "resolved": "https://registry.npmmirror.com/@rollup/rollup-linux-arm-gnueabihf/-/rollup-linux-arm-gnueabihf-4.50.2.tgz", - "integrity": "sha512-9VlPY/BN3AgbukfVHAB8zNFWB/lKEuvzRo1NKev0Po8sYFKx0i+AQlCYftgEjcL43F2h9Ui1ZSdVBc4En/sP2w==", + "version": "4.60.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-gnueabihf/-/rollup-linux-arm-gnueabihf-4.60.1.tgz", + "integrity": "sha512-L+34Qqil+v5uC0zEubW7uByo78WOCIrBvci69E7sFASRl0X7b/MB6Cqd1lky/CtcSVTydWa2WZwFuWexjS5o6g==", "cpu": [ "arm" ], @@ -1392,9 +1371,9 @@ ] }, "node_modules/@rollup/rollup-linux-arm-musleabihf": { - "version": "4.50.2", - "resolved": "https://registry.npmmirror.com/@rollup/rollup-linux-arm-musleabihf/-/rollup-linux-arm-musleabihf-4.50.2.tgz", - "integrity": "sha512-+GdKWOvsifaYNlIVf07QYan1J5F141+vGm5/Y8b9uCZnG/nxoGqgCmR24mv0koIWWuqvFYnbURRqw1lv7IBINw==", + "version": "4.60.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-musleabihf/-/rollup-linux-arm-musleabihf-4.60.1.tgz", + "integrity": "sha512-n83O8rt4v34hgFzlkb1ycniJh7IR5RCIqt6mz1VRJD6pmhRi0CXdmfnLu9dIUS6buzh60IvACM842Ffb3xd6Gg==", "cpu": [ "arm" ], @@ -1406,9 +1385,9 @@ ] }, "node_modules/@rollup/rollup-linux-arm64-gnu": { - "version": "4.50.2", - "resolved": "https://registry.npmmirror.com/@rollup/rollup-linux-arm64-gnu/-/rollup-linux-arm64-gnu-4.50.2.tgz", - "integrity": "sha512-df0Eou14ojtUdLQdPFnymEQteENwSJAdLf5KCDrmZNsy1c3YaCNaJvYsEUHnrg+/DLBH612/R0xd3dD03uz2dg==", + "version": "4.60.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-gnu/-/rollup-linux-arm64-gnu-4.60.1.tgz", + "integrity": "sha512-Nql7sTeAzhTAja3QXeAI48+/+GjBJ+QmAH13snn0AJSNL50JsDqotyudHyMbO2RbJkskbMbFJfIJKWA6R1LCJQ==", "cpu": [ "arm64" ], @@ -1420,9 +1399,9 @@ ] }, "node_modules/@rollup/rollup-linux-arm64-musl": { - "version": "4.50.2", - "resolved": "https://registry.npmmirror.com/@rollup/rollup-linux-arm64-musl/-/rollup-linux-arm64-musl-4.50.2.tgz", - "integrity": "sha512-iPeouV0UIDtz8j1YFR4OJ/zf7evjauqv7jQ/EFs0ClIyL+by++hiaDAfFipjOgyz6y6xbDvJuiU4HwpVMpRFDQ==", + "version": "4.60.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-musl/-/rollup-linux-arm64-musl-4.60.1.tgz", + "integrity": "sha512-+pUymDhd0ys9GcKZPPWlFiZ67sTWV5UU6zOJat02M1+PiuSGDziyRuI/pPue3hoUwm2uGfxdL+trT6Z9rxnlMA==", "cpu": [ "arm64" ], @@ -1434,9 +1413,23 @@ ] }, "node_modules/@rollup/rollup-linux-loong64-gnu": { - "version": "4.50.2", - "resolved": "https://registry.npmmirror.com/@rollup/rollup-linux-loong64-gnu/-/rollup-linux-loong64-gnu-4.50.2.tgz", - "integrity": "sha512-OL6KaNvBopLlj5fTa5D5bau4W82f+1TyTZRr2BdnfsrnQnmdxh4okMxR2DcDkJuh4KeoQZVuvHvzuD/lyLn2Kw==", + "version": "4.60.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-loong64-gnu/-/rollup-linux-loong64-gnu-4.60.1.tgz", + "integrity": "sha512-VSvgvQeIcsEvY4bKDHEDWcpW4Yw7BtlKG1GUT4FzBUlEKQK0rWHYBqQt6Fm2taXS+1bXvJT6kICu5ZwqKCnvlQ==", + "cpu": [ + "loong64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-loong64-musl": { + "version": "4.60.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-loong64-musl/-/rollup-linux-loong64-musl-4.60.1.tgz", + "integrity": "sha512-4LqhUomJqwe641gsPp6xLfhqWMbQV04KtPp7/dIp0nzPxAkNY1AbwL5W0MQpcalLYk07vaW9Kp1PBhdpZYYcEw==", "cpu": [ "loong64" ], @@ -1448,9 +1441,23 @@ ] }, "node_modules/@rollup/rollup-linux-ppc64-gnu": { - "version": "4.50.2", - "resolved": "https://registry.npmmirror.com/@rollup/rollup-linux-ppc64-gnu/-/rollup-linux-ppc64-gnu-4.50.2.tgz", - "integrity": "sha512-I21VJl1w6z/K5OTRl6aS9DDsqezEZ/yKpbqlvfHbW0CEF5IL8ATBMuUx6/mp683rKTK8thjs/0BaNrZLXetLag==", + "version": "4.60.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-ppc64-gnu/-/rollup-linux-ppc64-gnu-4.60.1.tgz", + "integrity": "sha512-tLQQ9aPvkBxOc/EUT6j3pyeMD6Hb8QF2BTBnCQWP/uu1lhc9AIrIjKnLYMEroIz/JvtGYgI9dF3AxHZNaEH0rw==", + "cpu": [ + "ppc64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-ppc64-musl": { + "version": "4.60.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-ppc64-musl/-/rollup-linux-ppc64-musl-4.60.1.tgz", + "integrity": "sha512-RMxFhJwc9fSXP6PqmAz4cbv3kAyvD1etJFjTx4ONqFP9DkTkXsAMU4v3Vyc5BgzC+anz7nS/9tp4obsKfqkDHg==", "cpu": [ "ppc64" ], @@ -1462,9 +1469,9 @@ ] }, "node_modules/@rollup/rollup-linux-riscv64-gnu": { - "version": "4.50.2", - "resolved": "https://registry.npmmirror.com/@rollup/rollup-linux-riscv64-gnu/-/rollup-linux-riscv64-gnu-4.50.2.tgz", - "integrity": "sha512-Hq6aQJT/qFFHrYMjS20nV+9SKrXL2lvFBENZoKfoTH2kKDOJqff5OSJr4x72ZaG/uUn+XmBnGhfr4lwMRrmqCQ==", + "version": "4.60.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-riscv64-gnu/-/rollup-linux-riscv64-gnu-4.60.1.tgz", + "integrity": "sha512-QKgFl+Yc1eEk6MmOBfRHYF6lTxiiiV3/z/BRrbSiW2I7AFTXoBFvdMEyglohPj//2mZS4hDOqeB0H1ACh3sBbg==", "cpu": [ "riscv64" ], @@ -1476,9 +1483,9 @@ ] }, "node_modules/@rollup/rollup-linux-riscv64-musl": { - "version": "4.50.2", - "resolved": "https://registry.npmmirror.com/@rollup/rollup-linux-riscv64-musl/-/rollup-linux-riscv64-musl-4.50.2.tgz", - "integrity": "sha512-82rBSEXRv5qtKyr0xZ/YMF531oj2AIpLZkeNYxmKNN6I2sVE9PGegN99tYDLK2fYHJITL1P2Lgb4ZXnv0PjQvw==", + "version": "4.60.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-riscv64-musl/-/rollup-linux-riscv64-musl-4.60.1.tgz", + "integrity": "sha512-RAjXjP/8c6ZtzatZcA1RaQr6O1TRhzC+adn8YZDnChliZHviqIjmvFwHcxi4JKPSDAt6Uhf/7vqcBzQJy0PDJg==", "cpu": [ "riscv64" ], @@ -1490,9 +1497,9 @@ ] }, "node_modules/@rollup/rollup-linux-s390x-gnu": { - "version": "4.50.2", - "resolved": "https://registry.npmmirror.com/@rollup/rollup-linux-s390x-gnu/-/rollup-linux-s390x-gnu-4.50.2.tgz", - "integrity": "sha512-4Q3S3Hy7pC6uaRo9gtXUTJ+EKo9AKs3BXKc2jYypEcMQ49gDPFU2P1ariX9SEtBzE5egIX6fSUmbmGazwBVF9w==", + "version": "4.60.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-s390x-gnu/-/rollup-linux-s390x-gnu-4.60.1.tgz", + "integrity": "sha512-wcuocpaOlaL1COBYiA89O6yfjlp3RwKDeTIA0hM7OpmhR1Bjo9j31G1uQVpDlTvwxGn2nQs65fBFL5UFd76FcQ==", "cpu": [ "s390x" ], @@ -1504,9 +1511,9 @@ ] }, "node_modules/@rollup/rollup-linux-x64-gnu": { - "version": "4.50.2", - "resolved": "https://registry.npmmirror.com/@rollup/rollup-linux-x64-gnu/-/rollup-linux-x64-gnu-4.50.2.tgz", - "integrity": "sha512-9Jie/At6qk70dNIcopcL4p+1UirusEtznpNtcq/u/C5cC4HBX7qSGsYIcG6bdxj15EYWhHiu02YvmdPzylIZlA==", + "version": "4.60.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-gnu/-/rollup-linux-x64-gnu-4.60.1.tgz", + "integrity": "sha512-77PpsFQUCOiZR9+LQEFg9GClyfkNXj1MP6wRnzYs0EeWbPcHs02AXu4xuUbM1zhwn3wqaizle3AEYg5aeoohhg==", "cpu": [ "x64" ], @@ -1518,9 +1525,9 @@ ] }, "node_modules/@rollup/rollup-linux-x64-musl": { - "version": "4.50.2", - "resolved": "https://registry.npmmirror.com/@rollup/rollup-linux-x64-musl/-/rollup-linux-x64-musl-4.50.2.tgz", - "integrity": "sha512-HPNJwxPL3EmhzeAnsWQCM3DcoqOz3/IC6de9rWfGR8ZCuEHETi9km66bH/wG3YH0V3nyzyFEGUZeL5PKyy4xvw==", + "version": "4.60.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-musl/-/rollup-linux-x64-musl-4.60.1.tgz", + "integrity": "sha512-5cIATbk5vynAjqqmyBjlciMJl1+R/CwX9oLk/EyiFXDWd95KpHdrOJT//rnUl4cUcskrd0jCCw3wpZnhIHdD9w==", "cpu": [ "x64" ], @@ -1531,10 +1538,24 @@ "linux" ] }, + "node_modules/@rollup/rollup-openbsd-x64": { + "version": "4.60.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-openbsd-x64/-/rollup-openbsd-x64-4.60.1.tgz", + "integrity": "sha512-cl0w09WsCi17mcmWqqglez9Gk8isgeWvoUZ3WiJFYSR3zjBQc2J5/ihSjpl+VLjPqjQ/1hJRcqBfLjssREQILw==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "openbsd" + ] + }, "node_modules/@rollup/rollup-openharmony-arm64": { - "version": "4.50.2", - "resolved": "https://registry.npmmirror.com/@rollup/rollup-openharmony-arm64/-/rollup-openharmony-arm64-4.50.2.tgz", - "integrity": "sha512-nMKvq6FRHSzYfKLHZ+cChowlEkR2lj/V0jYj9JnGUVPL2/mIeFGmVM2mLaFeNa5Jev7W7TovXqXIG2d39y1KYA==", + "version": "4.60.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-openharmony-arm64/-/rollup-openharmony-arm64-4.60.1.tgz", + "integrity": "sha512-4Cv23ZrONRbNtbZa37mLSueXUCtN7MXccChtKpUnQNgF010rjrjfHx3QxkS2PI7LqGT5xXyYs1a7LbzAwT0iCA==", "cpu": [ "arm64" ], @@ -1546,9 +1567,9 @@ ] }, "node_modules/@rollup/rollup-win32-arm64-msvc": { - "version": "4.50.2", - "resolved": "https://registry.npmmirror.com/@rollup/rollup-win32-arm64-msvc/-/rollup-win32-arm64-msvc-4.50.2.tgz", - "integrity": "sha512-eFUvvnTYEKeTyHEijQKz81bLrUQOXKZqECeiWH6tb8eXXbZk+CXSG2aFrig2BQ/pjiVRj36zysjgILkqarS2YA==", + "version": "4.60.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-arm64-msvc/-/rollup-win32-arm64-msvc-4.60.1.tgz", + "integrity": "sha512-i1okWYkA4FJICtr7KpYzFpRTHgy5jdDbZiWfvny21iIKky5YExiDXP+zbXzm3dUcFpkEeYNHgQ5fuG236JPq0g==", "cpu": [ "arm64" ], @@ -1560,9 +1581,9 @@ ] }, "node_modules/@rollup/rollup-win32-ia32-msvc": { - "version": "4.50.2", - "resolved": "https://registry.npmmirror.com/@rollup/rollup-win32-ia32-msvc/-/rollup-win32-ia32-msvc-4.50.2.tgz", - "integrity": "sha512-cBaWmXqyfRhH8zmUxK3d3sAhEWLrtMjWBRwdMMHJIXSjvjLKvv49adxiEz+FJ8AP90apSDDBx2Tyd/WylV6ikA==", + "version": "4.60.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-ia32-msvc/-/rollup-win32-ia32-msvc-4.60.1.tgz", + "integrity": "sha512-u09m3CuwLzShA0EYKMNiFgcjjzwqtUMLmuCJLeZWjjOYA3IT2Di09KaxGBTP9xVztWyIWjVdsB2E9goMjZvTQg==", "cpu": [ "ia32" ], @@ -1573,10 +1594,24 @@ "win32" ] }, + "node_modules/@rollup/rollup-win32-x64-gnu": { + "version": "4.60.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-x64-gnu/-/rollup-win32-x64-gnu-4.60.1.tgz", + "integrity": "sha512-k+600V9Zl1CM7eZxJgMyTUzmrmhB/0XZnF4pRypKAlAgxmedUA+1v9R+XOFv56W4SlHEzfeMtzujLJD22Uz5zg==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ] + }, "node_modules/@rollup/rollup-win32-x64-msvc": { - "version": "4.50.2", - "resolved": "https://registry.npmmirror.com/@rollup/rollup-win32-x64-msvc/-/rollup-win32-x64-msvc-4.50.2.tgz", - "integrity": "sha512-APwKy6YUhvZaEoHyM+9xqmTpviEI+9eL7LoCH+aLcvWYHJ663qG5zx7WzWZY+a9qkg5JtzcMyJ9z0WtQBMDmgA==", + "version": "4.60.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-x64-msvc/-/rollup-win32-x64-msvc-4.60.1.tgz", + "integrity": "sha512-lWMnixq/QzxyhTV6NjQJ4SFo1J6PvOX8vUx5Wb4bBPsEb+8xZ89Bz6kOXpfXj9ak9AHTQVQzlgzBEc1SyM27xQ==", "cpu": [ "x64" ], @@ -2110,14 +2145,14 @@ } }, "node_modules/@vueuse/core": { - "version": "14.2.0", - "resolved": "https://registry.npmjs.org/@vueuse/core/-/core-14.2.0.tgz", - "integrity": "sha512-tpjzVl7KCQNVd/qcaCE9XbejL38V6KJAEq/tVXj7mDPtl6JtzmUdnXelSS+ULRkkrDgzYVK7EerQJvd2jR794Q==", + "version": "14.2.1", + "resolved": "https://registry.npmjs.org/@vueuse/core/-/core-14.2.1.tgz", + "integrity": "sha512-3vwDzV+GDUNpdegRY6kzpLm4Igptq+GA0QkJ3W61Iv27YWwW/ufSlOfgQIpN6FZRMG0mkaz4gglJRtq5SeJyIQ==", "license": "MIT", "dependencies": { "@types/web-bluetooth": "^0.0.21", - "@vueuse/metadata": "14.2.0", - "@vueuse/shared": "14.2.0" + "@vueuse/metadata": "14.2.1", + "@vueuse/shared": "14.2.1" }, "funding": { "url": "https://github.com/sponsors/antfu" @@ -2139,9 +2174,9 @@ } }, "node_modules/@vueuse/metadata": { - "version": "14.2.0", - "resolved": "https://registry.npmjs.org/@vueuse/metadata/-/metadata-14.2.0.tgz", - "integrity": "sha512-i3axTGjU8b13FtyR4Keeama+43iD+BwX9C2TmzBVKqjSHArF03hjkp2SBZ1m72Jk2UtrX0aYCugBq2R1fhkuAQ==", + "version": "14.2.1", + "resolved": "https://registry.npmjs.org/@vueuse/metadata/-/metadata-14.2.1.tgz", + "integrity": "sha512-1ButlVtj5Sb/HDtIy1HFr1VqCP4G6Ypqt5MAo0lCgjokrk2mvQKsK2uuy0vqu/Ks+sHfuHo0B9Y9jn9xKdjZsw==", "license": "MIT", "funding": { "url": "https://github.com/sponsors/antfu" @@ -2215,9 +2250,9 @@ } }, "node_modules/ajv": { - "version": "6.12.6", - "resolved": "https://registry.npmjs.org/ajv/-/ajv-6.12.6.tgz", - "integrity": "sha512-j3fVLgvTo527anyYyJOGTYJbG+vnnQYvE0m5mmkc1TK+nxAppkCLMIL0aZ4dblVCNoGShhm+kzE4ZUykBoMg4g==", + "version": "6.14.0", + "resolved": "https://registry.npmjs.org/ajv/-/ajv-6.14.0.tgz", + "integrity": "sha512-IWrosm/yrn43eiKqkfkHis7QioDleaXQHdDVPKg0FSwwd/DuvyX79TZnFOnYpB7dcsFAMmtFztZuXPDvSePkFw==", "license": "MIT", "dependencies": { "fast-deep-equal": "^3.1.1", @@ -2537,9 +2572,9 @@ "license": "MIT" }, "node_modules/brace-expansion": { - "version": "1.1.12", - "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.12.tgz", - "integrity": "sha512-9T9UjW3r0UW5c1Q7GTwllptXwhvYmEzFhzMfZ9H7FQWt+uZePjZPjBP/W1ZEyZ1twGWom5/56TF4lPcqjnDHcg==", + "version": "1.1.13", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.13.tgz", + "integrity": "sha512-9ZLprWS6EENmhEOpjCYW2c8VkmOvckIJZfkr7rBW6dObmfgJ/L1GpSYW5Hpo9lDz4D1+n0Ckz8rU7FwHDQiG/w==", "license": "MIT", "dependencies": { "balanced-match": "^1.0.0", @@ -2637,9 +2672,9 @@ } }, "node_modules/cacache/node_modules/brace-expansion": { - "version": "2.0.2", - "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-2.0.2.tgz", - "integrity": "sha512-Jt0vHyM+jmUBqojB7E1NIYadt0vI0Qxjxd2TErW94wDz+E2LAm5vKMXXwg6ZZBTHPuUlDgQHKXvjGBdfcF1ZDQ==", + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-2.0.3.tgz", + "integrity": "sha512-MCV/fYJEbqx68aE58kv2cA/kiky1G8vux3OR6/jbS+jIMe/6fJWa0DTzJU7dqijOWYwHi1t29FlfYI9uytqlpA==", "license": "MIT", "dependencies": { "balanced-match": "^1.0.0" @@ -2672,12 +2707,12 @@ "license": "ISC" }, "node_modules/cacache/node_modules/minimatch": { - "version": "9.0.5", - "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-9.0.5.tgz", - "integrity": "sha512-G6T0ZX48xgozx7587koeX9Ys2NYy6Gmv//P89sEte9V9whIapMNF4idKxnW2QtCcLiTWlb/wfCabAtAFWhhBow==", + "version": "9.0.9", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-9.0.9.tgz", + "integrity": "sha512-OBwBN9AL4dqmETlpS2zasx+vTeWclWzkblfZk7KTA5j3jeOONz/tRCnZomUyvNg83wL5Zv9Ss6HMJXAgL8R2Yg==", "license": "ISC", "dependencies": { - "brace-expansion": "^2.0.1" + "brace-expansion": "^2.0.2" }, "engines": { "node": ">=16 || 14 >=14.17" @@ -2847,12 +2882,12 @@ } }, "node_modules/cidr-tools": { - "version": "11.0.8", - "resolved": "https://registry.npmjs.org/cidr-tools/-/cidr-tools-11.0.8.tgz", - "integrity": "sha512-z8TVpm6JAemTAuYsza3GL4KJH5VxSFwWge6YhA+9tFYoUICx8fraMdd6bCBb6Y/15o94POW32XghQt9LH4CHXQ==", + "version": "11.3.2", + "resolved": "https://registry.npmjs.org/cidr-tools/-/cidr-tools-11.3.2.tgz", + "integrity": "sha512-yCtb8tB3CngbbzGUsEz8koyxHDxEA3VUOOldXFeLqQREWl1HK/FZxLzxxWQgPDPMwuvMF7q9kyIsFLmmBPaN6A==", "license": "BSD-2-Clause", "dependencies": { - "ip-bigint": "^8.2.4" + "ip-bigint": "^8.3.2" }, "engines": { "node": ">=18" @@ -3259,9 +3294,9 @@ } }, "node_modules/dir-compare/node_modules/minimatch": { - "version": "3.1.2", - "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.2.tgz", - "integrity": "sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw==", + "version": "3.1.5", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.5.tgz", + "integrity": "sha512-VgjWUsnnT6n+NUk6eZq77zeFdpW2LWDzP6zFGrCbHXiYNul5Dzqk2HHQ5uFH2DNW5Xbp8+jVzaeNt94ssEEl4w==", "license": "ISC", "dependencies": { "brace-expansion": "^1.1.7" @@ -3746,18 +3781,18 @@ } }, "node_modules/filelist/node_modules/brace-expansion": { - "version": "2.0.2", - "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-2.0.2.tgz", - "integrity": "sha512-Jt0vHyM+jmUBqojB7E1NIYadt0vI0Qxjxd2TErW94wDz+E2LAm5vKMXXwg6ZZBTHPuUlDgQHKXvjGBdfcF1ZDQ==", + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-2.0.3.tgz", + "integrity": "sha512-MCV/fYJEbqx68aE58kv2cA/kiky1G8vux3OR6/jbS+jIMe/6fJWa0DTzJU7dqijOWYwHi1t29FlfYI9uytqlpA==", "license": "MIT", "dependencies": { "balanced-match": "^1.0.0" } }, "node_modules/filelist/node_modules/minimatch": { - "version": "5.1.6", - "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-5.1.6.tgz", - "integrity": "sha512-lKwV/1brpG6mBUFHtb7NUmtABCb2WZZmm2wNiOA5hAb8VdCS4B3dtMWyvcoViccwAW/COERjXLt0zP1zXUN26g==", + "version": "5.1.9", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-5.1.9.tgz", + "integrity": "sha512-7o1wEA2RyMP7Iu7GNba9vc0RWWGACJOCZBJX2GJWip0ikV+wcOsgVuY9uE8CPiyQhkGFSlhuSkZPavN7u1c2Fw==", "license": "ISC", "dependencies": { "brace-expansion": "^2.0.1" @@ -4002,9 +4037,9 @@ } }, "node_modules/glob/node_modules/minimatch": { - "version": "3.1.2", - "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.2.tgz", - "integrity": "sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw==", + "version": "3.1.5", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.5.tgz", + "integrity": "sha512-VgjWUsnnT6n+NUk6eZq77zeFdpW2LWDzP6zFGrCbHXiYNul5Dzqk2HHQ5uFH2DNW5Xbp8+jVzaeNt94ssEEl4w==", "license": "ISC", "dependencies": { "brace-expansion": "^1.1.7" @@ -4317,9 +4352,9 @@ } }, "node_modules/ip-bigint": { - "version": "8.2.4", - "resolved": "https://registry.npmjs.org/ip-bigint/-/ip-bigint-8.2.4.tgz", - "integrity": "sha512-uLnCfRdjiqRSX36+sKW3PBsotx58qEXSfbRWqdy1N5w6LtlIDCQnxuVce5OIBD50WA9VX2DWixtiBVtalWb1fA==", + "version": "8.3.3", + "resolved": "https://registry.npmjs.org/ip-bigint/-/ip-bigint-8.3.3.tgz", + "integrity": "sha512-r1bCRPxLv4PcyIB95FbsxaVvymNhFfbsIxrFdHqgZhWOBvfEPnzIHDGLAP+85cBozVf+XGaM00em4ZoJHm9EHw==", "license": "BSD-2-Clause", "engines": { "node": ">=18" @@ -4767,20 +4802,41 @@ } }, "node_modules/minimatch": { - "version": "10.1.1", - "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-10.1.1.tgz", - "integrity": "sha512-enIvLvRAFZYXJzkCYG5RKmPfrFArdLv+R+lbQ53BmIMLIry74bjKzX6iHAm8WYamJkhSSEabrWN5D97XnKObjQ==", + "version": "10.2.5", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-10.2.5.tgz", + "integrity": "sha512-MULkVLfKGYDFYejP07QOurDLLQpcjk7Fw+7jXS2R2czRQzR56yHRveU5NDJEOviH+hETZKSkIk5c+T23GjFUMg==", "license": "BlueOak-1.0.0", "dependencies": { - "@isaacs/brace-expansion": "^5.0.0" + "brace-expansion": "^5.0.5" }, "engines": { - "node": "20 || >=22" + "node": "18 || 20 || >=22" }, "funding": { "url": "https://github.com/sponsors/isaacs" } }, + "node_modules/minimatch/node_modules/balanced-match": { + "version": "4.0.4", + "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-4.0.4.tgz", + "integrity": "sha512-BLrgEcRTwX2o6gGxGOCNyMvGSp35YofuYzw9h1IMTRmKqttAZZVU67bdb9Pr2vUHA8+j3i2tJfjO6C6+4myGTA==", + "license": "MIT", + "engines": { + "node": "18 || 20 || >=22" + } + }, + "node_modules/minimatch/node_modules/brace-expansion": { + "version": "5.0.5", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-5.0.5.tgz", + "integrity": "sha512-VZznLgtwhn+Mact9tfiwx64fA9erHH/MCXEUfB/0bX/6Fz6ny5EGTXYltMocqg4xFAQZtnO3DHWWXi8RiuN7cQ==", + "license": "MIT", + "dependencies": { + "balanced-match": "^4.0.2" + }, + "engines": { + "node": "18 || 20 || >=22" + } + }, "node_modules/minimist": { "version": "1.2.8", "resolved": "https://registry.npmjs.org/minimist/-/minimist-1.2.8.tgz", @@ -5091,9 +5147,9 @@ } }, "node_modules/npm": { - "version": "11.8.0", - "resolved": "https://registry.npmjs.org/npm/-/npm-11.8.0.tgz", - "integrity": "sha512-n19sJeW+RGKdkHo8SCc5xhSwkKhQUFfZaFzSc+EsYXLjSqIV0tl72aDYQVuzVvfrbysGwdaQsNLNy58J10EBSQ==", + "version": "11.12.1", + "resolved": "https://registry.npmjs.org/npm/-/npm-11.12.1.tgz", + "integrity": "sha512-zcoUuF1kezGSAo0CqtvoLXX3mkRqzuqYdL6Y5tdo8g69NVV3CkjQ6ZBhBgB4d7vGkPcV6TcvLi3GRKPDFX+xTA==", "bundleDependencies": [ "@isaacs/string-locale-compare", "@npmcli/arborist", @@ -5111,7 +5167,6 @@ "cacache", "chalk", "ci-info", - "cli-columns", "fastest-levenshtein", "fs-minipass", "glob", @@ -5172,47 +5227,46 @@ ], "dependencies": { "@isaacs/string-locale-compare": "^1.1.0", - "@npmcli/arborist": "^9.1.10", - "@npmcli/config": "^10.5.0", + "@npmcli/arborist": "^9.4.2", + "@npmcli/config": "^10.8.1", "@npmcli/fs": "^5.0.0", "@npmcli/map-workspaces": "^5.0.3", "@npmcli/metavuln-calculator": "^9.0.3", - "@npmcli/package-json": "^7.0.4", + "@npmcli/package-json": "^7.0.5", "@npmcli/promise-spawn": "^9.0.1", "@npmcli/redact": "^4.0.0", - "@npmcli/run-script": "^10.0.3", - "@sigstore/tuf": "^4.0.1", + "@npmcli/run-script": "^10.0.4", + "@sigstore/tuf": "^4.0.2", "abbrev": "^4.0.0", "archy": "~1.0.0", - "cacache": "^20.0.3", + "cacache": "^20.0.4", "chalk": "^5.6.2", - "ci-info": "^4.3.1", - "cli-columns": "^4.0.0", + "ci-info": "^4.4.0", "fastest-levenshtein": "^1.0.16", "fs-minipass": "^3.0.3", - "glob": "^13.0.0", + "glob": "^13.0.6", "graceful-fs": "^4.2.11", "hosted-git-info": "^9.0.2", "ini": "^6.0.0", - "init-package-json": "^8.2.4", - "is-cidr": "^6.0.1", + "init-package-json": "^8.2.5", + "is-cidr": "^6.0.3", "json-parse-even-better-errors": "^5.0.0", "libnpmaccess": "^10.0.3", - "libnpmdiff": "^8.0.13", - "libnpmexec": "^10.1.12", - "libnpmfund": "^7.0.13", + "libnpmdiff": "^8.1.5", + "libnpmexec": "^10.2.5", + "libnpmfund": "^7.0.19", "libnpmorg": "^8.0.1", - "libnpmpack": "^9.0.13", + "libnpmpack": "^9.1.5", "libnpmpublish": "^11.1.3", "libnpmsearch": "^9.0.1", "libnpmteam": "^8.0.2", "libnpmversion": "^8.0.3", - "make-fetch-happen": "^15.0.3", - "minimatch": "^10.1.1", - "minipass": "^7.1.1", + "make-fetch-happen": "^15.0.5", + "minimatch": "^10.2.4", + "minipass": "^7.1.3", "minipass-pipeline": "^1.2.4", "ms": "^2.1.2", - "node-gyp": "^12.1.0", + "node-gyp": "^12.2.0", "nopt": "^9.0.0", "npm-audit-report": "^7.0.0", "npm-install-checks": "^8.0.0", @@ -5222,21 +5276,21 @@ "npm-registry-fetch": "^19.1.1", "npm-user-validate": "^4.0.0", "p-map": "^7.0.4", - "pacote": "^21.0.4", + "pacote": "^21.5.0", "parse-conflict-json": "^5.0.1", "proc-log": "^6.1.0", "qrcode-terminal": "^0.12.0", "read": "^5.0.1", - "semver": "^7.7.3", + "semver": "^7.7.4", "spdx-expression-parse": "^4.0.0", - "ssri": "^13.0.0", + "ssri": "^13.0.1", "supports-color": "^10.2.2", - "tar": "^7.5.4", + "tar": "^7.5.11", "text-table": "~0.2.0", "tiny-relative-date": "^2.0.2", "treeverse": "^3.0.0", "validate-npm-package-name": "^7.0.2", - "which": "^6.0.0" + "which": "^6.0.1" }, "bin": { "npm": "bin/npm-cli.js", @@ -5246,23 +5300,12 @@ "node": "^20.17.0 || >=22.9.0" } }, - "node_modules/npm/node_modules/@isaacs/balanced-match": { - "version": "4.0.1", + "node_modules/npm/node_modules/@gar/promise-retry": { + "version": "1.0.3", "inBundle": true, "license": "MIT", "engines": { - "node": "20 || >=22" - } - }, - "node_modules/npm/node_modules/@isaacs/brace-expansion": { - "version": "5.0.0", - "inBundle": true, - "license": "MIT", - "dependencies": { - "@isaacs/balanced-match": "^4.0.1" - }, - "engines": { - "node": "20 || >=22" + "node": "^20.17.0 || >=22.9.0" } }, "node_modules/npm/node_modules/@isaacs/fs-minipass": { @@ -5297,10 +5340,11 @@ } }, "node_modules/npm/node_modules/@npmcli/arborist": { - "version": "9.1.10", + "version": "9.4.2", "inBundle": true, "license": "ISC", "dependencies": { + "@gar/promise-retry": "^1.0.0", "@isaacs/string-locale-compare": "^1.1.0", "@npmcli/fs": "^5.0.0", "@npmcli/installed-package-contents": "^4.0.0", @@ -5343,7 +5387,7 @@ } }, "node_modules/npm/node_modules/@npmcli/config": { - "version": "10.5.0", + "version": "10.8.1", "inBundle": true, "license": "ISC", "dependencies": { @@ -5372,16 +5416,16 @@ } }, "node_modules/npm/node_modules/@npmcli/git": { - "version": "7.0.1", + "version": "7.0.2", "inBundle": true, "license": "ISC", "dependencies": { + "@gar/promise-retry": "^1.0.0", "@npmcli/promise-spawn": "^9.0.0", "ini": "^6.0.0", "lru-cache": "^11.2.1", "npm-pick-manifest": "^11.0.1", "proc-log": "^6.0.0", - "promise-retry": "^2.0.1", "semver": "^7.3.5", "which": "^6.0.0" }, @@ -5450,7 +5494,7 @@ } }, "node_modules/npm/node_modules/@npmcli/package-json": { - "version": "7.0.4", + "version": "7.0.5", "inBundle": true, "license": "ISC", "dependencies": { @@ -5460,7 +5504,7 @@ "json-parse-even-better-errors": "^5.0.0", "proc-log": "^6.0.0", "semver": "^7.5.3", - "validate-npm-package-license": "^3.0.4" + "spdx-expression-parse": "^4.0.0" }, "engines": { "node": "^20.17.0 || >=22.9.0" @@ -5497,7 +5541,7 @@ } }, "node_modules/npm/node_modules/@npmcli/run-script": { - "version": "10.0.3", + "version": "10.0.4", "inBundle": true, "license": "ISC", "dependencies": { @@ -5505,8 +5549,7 @@ "@npmcli/package-json": "^7.0.0", "@npmcli/promise-spawn": "^9.0.0", "node-gyp": "^12.1.0", - "proc-log": "^6.0.0", - "which": "^6.0.0" + "proc-log": "^6.0.0" }, "engines": { "node": "^20.17.0 || >=22.9.0" @@ -5524,7 +5567,7 @@ } }, "node_modules/npm/node_modules/@sigstore/core": { - "version": "3.1.0", + "version": "3.2.0", "inBundle": true, "license": "Apache-2.0", "engines": { @@ -5540,23 +5583,23 @@ } }, "node_modules/npm/node_modules/@sigstore/sign": { - "version": "4.1.0", + "version": "4.1.1", "inBundle": true, "license": "Apache-2.0", "dependencies": { + "@gar/promise-retry": "^1.0.2", "@sigstore/bundle": "^4.0.0", - "@sigstore/core": "^3.1.0", + "@sigstore/core": "^3.2.0", "@sigstore/protobuf-specs": "^0.5.0", - "make-fetch-happen": "^15.0.3", - "proc-log": "^6.1.0", - "promise-retry": "^2.0.1" + "make-fetch-happen": "^15.0.4", + "proc-log": "^6.1.0" }, "engines": { "node": "^20.17.0 || >=22.9.0" } }, "node_modules/npm/node_modules/@sigstore/tuf": { - "version": "4.0.1", + "version": "4.0.2", "inBundle": true, "license": "Apache-2.0", "dependencies": { @@ -5616,14 +5659,6 @@ "node": ">= 14" } }, - "node_modules/npm/node_modules/ansi-regex": { - "version": "5.0.1", - "inBundle": true, - "license": "MIT", - "engines": { - "node": ">=8" - } - }, "node_modules/npm/node_modules/aproba": { "version": "2.1.0", "inBundle": true, @@ -5634,6 +5669,14 @@ "inBundle": true, "license": "MIT" }, + "node_modules/npm/node_modules/balanced-match": { + "version": "4.0.4", + "inBundle": true, + "license": "MIT", + "engines": { + "node": "18 || 20 || >=22" + } + }, "node_modules/npm/node_modules/bin-links": { "version": "6.0.0", "inBundle": true, @@ -5660,8 +5703,19 @@ "url": "https://github.com/sponsors/sindresorhus" } }, + "node_modules/npm/node_modules/brace-expansion": { + "version": "5.0.4", + "inBundle": true, + "license": "MIT", + "dependencies": { + "balanced-match": "^4.0.2" + }, + "engines": { + "node": "18 || 20 || >=22" + } + }, "node_modules/npm/node_modules/cacache": { - "version": "20.0.3", + "version": "20.0.4", "inBundle": true, "license": "ISC", "dependencies": { @@ -5674,8 +5728,7 @@ "minipass-flush": "^1.0.5", "minipass-pipeline": "^1.2.4", "p-map": "^7.0.2", - "ssri": "^13.0.0", - "unique-filename": "^5.0.0" + "ssri": "^13.0.0" }, "engines": { "node": "^20.17.0 || >=22.9.0" @@ -5701,7 +5754,7 @@ } }, "node_modules/npm/node_modules/ci-info": { - "version": "4.3.1", + "version": "4.4.0", "funding": [ { "type": "github", @@ -5715,28 +5768,13 @@ } }, "node_modules/npm/node_modules/cidr-regex": { - "version": "5.0.1", + "version": "5.0.3", "inBundle": true, "license": "BSD-2-Clause", - "dependencies": { - "ip-regex": "5.0.0" - }, "engines": { "node": ">=20" } }, - "node_modules/npm/node_modules/cli-columns": { - "version": "4.0.0", - "inBundle": true, - "license": "MIT", - "dependencies": { - "string-width": "^4.2.3", - "strip-ansi": "^6.0.1" - }, - "engines": { - "node": ">= 10" - } - }, "node_modules/npm/node_modules/cmd-shim": { "version": "8.0.0", "inBundle": true, @@ -5788,20 +5826,6 @@ "node": ">=0.3.1" } }, - "node_modules/npm/node_modules/emoji-regex": { - "version": "8.0.0", - "inBundle": true, - "license": "MIT" - }, - "node_modules/npm/node_modules/encoding": { - "version": "0.1.13", - "inBundle": true, - "license": "MIT", - "optional": true, - "dependencies": { - "iconv-lite": "^0.6.2" - } - }, "node_modules/npm/node_modules/env-paths": { "version": "2.2.1", "inBundle": true, @@ -5810,11 +5834,6 @@ "node": ">=6" } }, - "node_modules/npm/node_modules/err-code": { - "version": "2.0.3", - "inBundle": true, - "license": "MIT" - }, "node_modules/npm/node_modules/exponential-backoff": { "version": "3.1.3", "inBundle": true, @@ -5840,16 +5859,16 @@ } }, "node_modules/npm/node_modules/glob": { - "version": "13.0.0", + "version": "13.0.6", "inBundle": true, "license": "BlueOak-1.0.0", "dependencies": { - "minimatch": "^10.1.1", - "minipass": "^7.1.2", - "path-scurry": "^2.0.0" + "minimatch": "^10.2.2", + "minipass": "^7.1.3", + "path-scurry": "^2.0.2" }, "engines": { - "node": "20 || >=22" + "node": "18 || 20 || >=22" }, "funding": { "url": "https://github.com/sponsors/isaacs" @@ -5901,7 +5920,7 @@ } }, "node_modules/npm/node_modules/iconv-lite": { - "version": "0.6.3", + "version": "0.7.2", "inBundle": true, "license": "MIT", "optional": true, @@ -5910,6 +5929,10 @@ }, "engines": { "node": ">=0.10.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/express" } }, "node_modules/npm/node_modules/ignore-walk": { @@ -5923,14 +5946,6 @@ "node": "^20.17.0 || >=22.9.0" } }, - "node_modules/npm/node_modules/imurmurhash": { - "version": "0.1.4", - "inBundle": true, - "license": "MIT", - "engines": { - "node": ">=0.8.19" - } - }, "node_modules/npm/node_modules/ini": { "version": "6.0.0", "inBundle": true, @@ -5940,7 +5955,7 @@ } }, "node_modules/npm/node_modules/init-package-json": { - "version": "8.2.4", + "version": "8.2.5", "inBundle": true, "license": "ISC", "dependencies": { @@ -5949,7 +5964,6 @@ "promzard": "^3.0.1", "read": "^5.0.1", "semver": "^7.7.2", - "validate-npm-package-license": "^3.0.4", "validate-npm-package-name": "^7.0.0" }, "engines": { @@ -5964,42 +5978,23 @@ "node": ">= 12" } }, - "node_modules/npm/node_modules/ip-regex": { - "version": "5.0.0", - "inBundle": true, - "license": "MIT", - "engines": { - "node": "^12.20.0 || ^14.13.1 || >=16.0.0" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, "node_modules/npm/node_modules/is-cidr": { - "version": "6.0.1", + "version": "6.0.3", "inBundle": true, "license": "BSD-2-Clause", "dependencies": { - "cidr-regex": "5.0.1" + "cidr-regex": "^5.0.1" }, "engines": { "node": ">=20" } }, - "node_modules/npm/node_modules/is-fullwidth-code-point": { - "version": "3.0.0", - "inBundle": true, - "license": "MIT", - "engines": { - "node": ">=8" - } - }, "node_modules/npm/node_modules/isexe": { - "version": "3.1.1", + "version": "4.0.0", "inBundle": true, - "license": "ISC", + "license": "BlueOak-1.0.0", "engines": { - "node": ">=16" + "node": ">=20" } }, "node_modules/npm/node_modules/json-parse-even-better-errors": { @@ -6049,11 +6044,11 @@ } }, "node_modules/npm/node_modules/libnpmdiff": { - "version": "8.0.13", + "version": "8.1.5", "inBundle": true, "license": "ISC", "dependencies": { - "@npmcli/arborist": "^9.1.10", + "@npmcli/arborist": "^9.4.2", "@npmcli/installed-package-contents": "^4.0.0", "binary-extensions": "^3.0.0", "diff": "^8.0.2", @@ -6067,18 +6062,18 @@ } }, "node_modules/npm/node_modules/libnpmexec": { - "version": "10.1.12", + "version": "10.2.5", "inBundle": true, "license": "ISC", "dependencies": { - "@npmcli/arborist": "^9.1.10", + "@gar/promise-retry": "^1.0.0", + "@npmcli/arborist": "^9.4.2", "@npmcli/package-json": "^7.0.0", "@npmcli/run-script": "^10.0.0", "ci-info": "^4.0.0", "npm-package-arg": "^13.0.0", "pacote": "^21.0.2", "proc-log": "^6.0.0", - "promise-retry": "^2.0.1", "read": "^5.0.1", "semver": "^7.3.7", "signal-exit": "^4.1.0", @@ -6089,11 +6084,11 @@ } }, "node_modules/npm/node_modules/libnpmfund": { - "version": "7.0.13", + "version": "7.0.19", "inBundle": true, "license": "ISC", "dependencies": { - "@npmcli/arborist": "^9.1.10" + "@npmcli/arborist": "^9.4.2" }, "engines": { "node": "^20.17.0 || >=22.9.0" @@ -6112,11 +6107,11 @@ } }, "node_modules/npm/node_modules/libnpmpack": { - "version": "9.0.13", + "version": "9.1.5", "inBundle": true, "license": "ISC", "dependencies": { - "@npmcli/arborist": "^9.1.10", + "@npmcli/arborist": "^9.4.2", "@npmcli/run-script": "^10.0.0", "npm-package-arg": "^13.0.0", "pacote": "^21.0.2" @@ -6182,7 +6177,7 @@ } }, "node_modules/npm/node_modules/lru-cache": { - "version": "11.2.4", + "version": "11.2.7", "inBundle": true, "license": "BlueOak-1.0.0", "engines": { @@ -6190,11 +6185,13 @@ } }, "node_modules/npm/node_modules/make-fetch-happen": { - "version": "15.0.3", + "version": "15.0.5", "inBundle": true, "license": "ISC", "dependencies": { + "@gar/promise-retry": "^1.0.0", "@npmcli/agent": "^4.0.0", + "@npmcli/redact": "^4.0.0", "cacache": "^20.0.1", "http-cache-semantics": "^4.1.1", "minipass": "^7.0.2", @@ -6203,7 +6200,6 @@ "minipass-pipeline": "^1.2.4", "negotiator": "^1.0.0", "proc-log": "^6.0.0", - "promise-retry": "^2.0.1", "ssri": "^13.0.0" }, "engines": { @@ -6211,23 +6207,23 @@ } }, "node_modules/npm/node_modules/minimatch": { - "version": "10.1.1", + "version": "10.2.4", "inBundle": true, "license": "BlueOak-1.0.0", "dependencies": { - "@isaacs/brace-expansion": "^5.0.0" + "brace-expansion": "^5.0.2" }, "engines": { - "node": "20 || >=22" + "node": "18 || 20 || >=22" }, "funding": { "url": "https://github.com/sponsors/isaacs" } }, "node_modules/npm/node_modules/minipass": { - "version": "7.1.2", + "version": "7.1.3", "inBundle": true, - "license": "ISC", + "license": "BlueOak-1.0.0", "engines": { "node": ">=16 || 14 >=14.17" } @@ -6244,19 +6240,19 @@ } }, "node_modules/npm/node_modules/minipass-fetch": { - "version": "5.0.0", + "version": "5.0.2", "inBundle": true, "license": "MIT", "dependencies": { "minipass": "^7.0.3", - "minipass-sized": "^1.0.3", + "minipass-sized": "^2.0.0", "minizlib": "^3.0.1" }, "engines": { "node": "^20.17.0 || >=22.9.0" }, "optionalDependencies": { - "encoding": "^0.1.13" + "iconv-lite": "^0.7.2" } }, "node_modules/npm/node_modules/minipass-flush": { @@ -6281,6 +6277,11 @@ "node": ">=8" } }, + "node_modules/npm/node_modules/minipass-flush/node_modules/yallist": { + "version": "4.0.0", + "inBundle": true, + "license": "ISC" + }, "node_modules/npm/node_modules/minipass-pipeline": { "version": "1.2.4", "inBundle": true, @@ -6303,23 +6304,17 @@ "node": ">=8" } }, - "node_modules/npm/node_modules/minipass-sized": { - "version": "1.0.3", + "node_modules/npm/node_modules/minipass-pipeline/node_modules/yallist": { + "version": "4.0.0", "inBundle": true, - "license": "ISC", - "dependencies": { - "minipass": "^3.0.0" - }, - "engines": { - "node": ">=8" - } + "license": "ISC" }, - "node_modules/npm/node_modules/minipass-sized/node_modules/minipass": { - "version": "3.3.6", + "node_modules/npm/node_modules/minipass-sized": { + "version": "2.0.0", "inBundle": true, "license": "ISC", "dependencies": { - "yallist": "^4.0.0" + "minipass": "^7.1.2" }, "engines": { "node": ">=8" @@ -6358,7 +6353,7 @@ } }, "node_modules/npm/node_modules/node-gyp": { - "version": "12.1.0", + "version": "12.2.0", "inBundle": true, "license": "MIT", "dependencies": { @@ -6369,7 +6364,7 @@ "nopt": "^9.0.0", "proc-log": "^6.0.0", "semver": "^7.3.5", - "tar": "^7.5.2", + "tar": "^7.5.4", "tinyglobby": "^0.2.12", "which": "^6.0.0" }, @@ -6447,7 +6442,7 @@ } }, "node_modules/npm/node_modules/npm-packlist": { - "version": "10.0.3", + "version": "10.0.4", "inBundle": true, "license": "ISC", "dependencies": { @@ -6522,10 +6517,11 @@ } }, "node_modules/npm/node_modules/pacote": { - "version": "21.0.4", + "version": "21.5.0", "inBundle": true, "license": "ISC", "dependencies": { + "@gar/promise-retry": "^1.0.0", "@npmcli/git": "^7.0.0", "@npmcli/installed-package-contents": "^4.0.0", "@npmcli/package-json": "^7.0.0", @@ -6539,7 +6535,6 @@ "npm-pick-manifest": "^11.0.1", "npm-registry-fetch": "^19.0.0", "proc-log": "^6.0.0", - "promise-retry": "^2.0.1", "sigstore": "^4.0.0", "ssri": "^13.0.0", "tar": "^7.4.3" @@ -6565,7 +6560,7 @@ } }, "node_modules/npm/node_modules/path-scurry": { - "version": "2.0.1", + "version": "2.0.2", "inBundle": true, "license": "BlueOak-1.0.0", "dependencies": { @@ -6573,7 +6568,7 @@ "minipass": "^7.1.2" }, "engines": { - "node": "20 || >=22" + "node": "18 || 20 || >=22" }, "funding": { "url": "https://github.com/sponsors/isaacs" @@ -6623,18 +6618,6 @@ "url": "https://github.com/sponsors/isaacs" } }, - "node_modules/npm/node_modules/promise-retry": { - "version": "2.0.1", - "inBundle": true, - "license": "MIT", - "dependencies": { - "err-code": "^2.0.2", - "retry": "^0.12.0" - }, - "engines": { - "node": ">=10" - } - }, "node_modules/npm/node_modules/promzard": { "version": "3.0.1", "inBundle": true, @@ -6672,14 +6655,6 @@ "node": "^20.17.0 || >=22.9.0" } }, - "node_modules/npm/node_modules/retry": { - "version": "0.12.0", - "inBundle": true, - "license": "MIT", - "engines": { - "node": ">= 4" - } - }, "node_modules/npm/node_modules/safer-buffer": { "version": "2.1.2", "inBundle": true, @@ -6687,7 +6662,7 @@ "optional": true }, "node_modules/npm/node_modules/semver": { - "version": "7.7.3", + "version": "7.7.4", "inBundle": true, "license": "ISC", "bin": { @@ -6759,24 +6734,6 @@ "node": ">= 14" } }, - "node_modules/npm/node_modules/spdx-correct": { - "version": "3.2.0", - "inBundle": true, - "license": "Apache-2.0", - "dependencies": { - "spdx-expression-parse": "^3.0.0", - "spdx-license-ids": "^3.0.0" - } - }, - "node_modules/npm/node_modules/spdx-correct/node_modules/spdx-expression-parse": { - "version": "3.0.1", - "inBundle": true, - "license": "MIT", - "dependencies": { - "spdx-exceptions": "^2.1.0", - "spdx-license-ids": "^3.0.0" - } - }, "node_modules/npm/node_modules/spdx-exceptions": { "version": "2.5.0", "inBundle": true, @@ -6792,12 +6749,12 @@ } }, "node_modules/npm/node_modules/spdx-license-ids": { - "version": "3.0.22", + "version": "3.0.23", "inBundle": true, "license": "CC0-1.0" }, "node_modules/npm/node_modules/ssri": { - "version": "13.0.0", + "version": "13.0.1", "inBundle": true, "license": "ISC", "dependencies": { @@ -6807,30 +6764,6 @@ "node": "^20.17.0 || >=22.9.0" } }, - "node_modules/npm/node_modules/string-width": { - "version": "4.2.3", - "inBundle": true, - "license": "MIT", - "dependencies": { - "emoji-regex": "^8.0.0", - "is-fullwidth-code-point": "^3.0.0", - "strip-ansi": "^6.0.1" - }, - "engines": { - "node": ">=8" - } - }, - "node_modules/npm/node_modules/strip-ansi": { - "version": "6.0.1", - "inBundle": true, - "license": "MIT", - "dependencies": { - "ansi-regex": "^5.0.1" - }, - "engines": { - "node": ">=8" - } - }, "node_modules/npm/node_modules/supports-color": { "version": "10.2.2", "inBundle": true, @@ -6843,7 +6776,7 @@ } }, "node_modules/npm/node_modules/tar": { - "version": "7.5.4", + "version": "7.5.11", "inBundle": true, "license": "BlueOak-1.0.0", "dependencies": { @@ -6857,14 +6790,6 @@ "node": ">=18" } }, - "node_modules/npm/node_modules/tar/node_modules/yallist": { - "version": "5.0.0", - "inBundle": true, - "license": "BlueOak-1.0.0", - "engines": { - "node": ">=18" - } - }, "node_modules/npm/node_modules/text-table": { "version": "0.2.0", "inBundle": true, @@ -6938,51 +6863,11 @@ "node": "^20.17.0 || >=22.9.0" } }, - "node_modules/npm/node_modules/unique-filename": { - "version": "5.0.0", - "inBundle": true, - "license": "ISC", - "dependencies": { - "unique-slug": "^6.0.0" - }, - "engines": { - "node": "^20.17.0 || >=22.9.0" - } - }, - "node_modules/npm/node_modules/unique-slug": { - "version": "6.0.0", - "inBundle": true, - "license": "ISC", - "dependencies": { - "imurmurhash": "^0.1.4" - }, - "engines": { - "node": "^20.17.0 || >=22.9.0" - } - }, "node_modules/npm/node_modules/util-deprecate": { "version": "1.0.2", "inBundle": true, "license": "MIT" }, - "node_modules/npm/node_modules/validate-npm-package-license": { - "version": "3.0.4", - "inBundle": true, - "license": "Apache-2.0", - "dependencies": { - "spdx-correct": "^3.0.0", - "spdx-expression-parse": "^3.0.0" - } - }, - "node_modules/npm/node_modules/validate-npm-package-license/node_modules/spdx-expression-parse": { - "version": "3.0.1", - "inBundle": true, - "license": "MIT", - "dependencies": { - "spdx-exceptions": "^2.1.0", - "spdx-license-ids": "^3.0.0" - } - }, "node_modules/npm/node_modules/validate-npm-package-name": { "version": "7.0.2", "inBundle": true, @@ -7000,11 +6885,11 @@ } }, "node_modules/npm/node_modules/which": { - "version": "6.0.0", + "version": "6.0.1", "inBundle": true, "license": "ISC", "dependencies": { - "isexe": "^3.1.1" + "isexe": "^4.0.0" }, "bin": { "node-which": "bin/which.js" @@ -7014,11 +6899,10 @@ } }, "node_modules/npm/node_modules/write-file-atomic": { - "version": "7.0.0", + "version": "7.0.1", "inBundle": true, "license": "ISC", "dependencies": { - "imurmurhash": "^0.1.4", "signal-exit": "^4.0.1" }, "engines": { @@ -7026,9 +6910,21 @@ } }, "node_modules/npm/node_modules/yallist": { - "version": "4.0.0", + "version": "5.0.0", "inBundle": true, - "license": "ISC" + "license": "BlueOak-1.0.0", + "engines": { + "node": ">=18" + } + }, + "node_modules/numcodecs": { + "version": "0.3.2", + "resolved": "https://registry.npmjs.org/numcodecs/-/numcodecs-0.3.2.tgz", + "integrity": "sha512-6YSPnmZgg0P87jnNhi3s+FVLOcIn3y+1CTIgUulA3IdASzK9fJM87sUFkpyA+be9GibGRaST2wCgkD+6U+fWKw==", + "license": "MIT", + "dependencies": { + "fflate": "^0.8.0" + } }, "node_modules/numcodecs": { "version": "0.3.2", @@ -7313,9 +7209,9 @@ "license": "ISC" }, "node_modules/picomatch": { - "version": "4.0.3", - "resolved": "https://registry.npmmirror.com/picomatch/-/picomatch-4.0.3.tgz", - "integrity": "sha512-5gTmgEY/sqK6gFXLIsQNH19lWb4ebPDLA4SdLP7dsWkIXHWlG66oPuVvXSGFPppYZz8ZDZq0dYYrbHfBCVUb1Q==", + "version": "4.0.4", + "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-4.0.4.tgz", + "integrity": "sha512-QP88BAKvMam/3NxH6vj2o21R6MjxZUAd6nlwAS/pnGvN9IVLocLHxGYIzFhg6fUQ+5th6P4dv4eW9jX3DSIj7A==", "license": "MIT", "engines": { "node": ">=12" @@ -7856,9 +7752,9 @@ } }, "node_modules/rollup": { - "version": "4.50.2", - "resolved": "https://registry.npmmirror.com/rollup/-/rollup-4.50.2.tgz", - "integrity": "sha512-BgLRGy7tNS9H66aIMASq1qSYbAAJV6Z6WR4QYTvj5FgF15rZ/ympT1uixHXwzbZUBDbkvqUI1KR0fH1FhMaQ9w==", + "version": "4.60.1", + "resolved": "https://registry.npmjs.org/rollup/-/rollup-4.60.1.tgz", + "integrity": "sha512-VmtB2rFU/GroZ4oL8+ZqXgSA38O6GR8KSIvWmEFv63pQ0G6KaBH9s07PO8XTXP4vI+3UJUEypOfjkGfmSBBR0w==", "dev": true, "license": "MIT", "dependencies": { @@ -7872,27 +7768,31 @@ "npm": ">=8.0.0" }, "optionalDependencies": { - "@rollup/rollup-android-arm-eabi": "4.50.2", - "@rollup/rollup-android-arm64": "4.50.2", - "@rollup/rollup-darwin-arm64": "4.50.2", - "@rollup/rollup-darwin-x64": "4.50.2", - "@rollup/rollup-freebsd-arm64": "4.50.2", - "@rollup/rollup-freebsd-x64": "4.50.2", - "@rollup/rollup-linux-arm-gnueabihf": "4.50.2", - "@rollup/rollup-linux-arm-musleabihf": "4.50.2", - "@rollup/rollup-linux-arm64-gnu": "4.50.2", - "@rollup/rollup-linux-arm64-musl": "4.50.2", - "@rollup/rollup-linux-loong64-gnu": "4.50.2", - "@rollup/rollup-linux-ppc64-gnu": "4.50.2", - "@rollup/rollup-linux-riscv64-gnu": "4.50.2", - "@rollup/rollup-linux-riscv64-musl": "4.50.2", - "@rollup/rollup-linux-s390x-gnu": "4.50.2", - "@rollup/rollup-linux-x64-gnu": "4.50.2", - "@rollup/rollup-linux-x64-musl": "4.50.2", - "@rollup/rollup-openharmony-arm64": "4.50.2", - "@rollup/rollup-win32-arm64-msvc": "4.50.2", - "@rollup/rollup-win32-ia32-msvc": "4.50.2", - "@rollup/rollup-win32-x64-msvc": "4.50.2", + "@rollup/rollup-android-arm-eabi": "4.60.1", + "@rollup/rollup-android-arm64": "4.60.1", + "@rollup/rollup-darwin-arm64": "4.60.1", + "@rollup/rollup-darwin-x64": "4.60.1", + "@rollup/rollup-freebsd-arm64": "4.60.1", + "@rollup/rollup-freebsd-x64": "4.60.1", + "@rollup/rollup-linux-arm-gnueabihf": "4.60.1", + "@rollup/rollup-linux-arm-musleabihf": "4.60.1", + "@rollup/rollup-linux-arm64-gnu": "4.60.1", + "@rollup/rollup-linux-arm64-musl": "4.60.1", + "@rollup/rollup-linux-loong64-gnu": "4.60.1", + "@rollup/rollup-linux-loong64-musl": "4.60.1", + "@rollup/rollup-linux-ppc64-gnu": "4.60.1", + "@rollup/rollup-linux-ppc64-musl": "4.60.1", + "@rollup/rollup-linux-riscv64-gnu": "4.60.1", + "@rollup/rollup-linux-riscv64-musl": "4.60.1", + "@rollup/rollup-linux-s390x-gnu": "4.60.1", + "@rollup/rollup-linux-x64-gnu": "4.60.1", + "@rollup/rollup-linux-x64-musl": "4.60.1", + "@rollup/rollup-openbsd-x64": "4.60.1", + "@rollup/rollup-openharmony-arm64": "4.60.1", + "@rollup/rollup-win32-arm64-msvc": "4.60.1", + "@rollup/rollup-win32-ia32-msvc": "4.60.1", + "@rollup/rollup-win32-x64-gnu": "4.60.1", + "@rollup/rollup-win32-x64-msvc": "4.60.1", "fsevents": "~2.3.2" } }, @@ -8253,9 +8153,9 @@ } }, "node_modules/tar": { - "version": "7.5.7", - "resolved": "https://registry.npmjs.org/tar/-/tar-7.5.7.tgz", - "integrity": "sha512-fov56fJiRuThVFXD6o6/Q354S7pnWMJIVlDBYijsTNx6jKSE4pvrDTs6lUnmGvNyfJwFQQwWy3owKz1ucIhveQ==", + "version": "7.5.13", + "resolved": "https://registry.npmjs.org/tar/-/tar-7.5.13.tgz", + "integrity": "sha512-tOG/7GyXpFevhXVh8jOPJrmtRpOTsYqUIkVdVooZYJS/z8WhfQUX8RJILmeuJNinGAMSu1veBr4asSHFt5/hng==", "license": "BlueOak-1.0.0", "dependencies": { "@isaacs/fs-minipass": "^4.0.0", @@ -9097,9 +8997,9 @@ "license": "ISC" }, "node_modules/yaml": { - "version": "2.8.2", - "resolved": "https://registry.npmjs.org/yaml/-/yaml-2.8.2.tgz", - "integrity": "sha512-mplynKqc1C2hTVYxd0PU2xQAc22TI1vShAYGksCCfxbn/dFwnHTNi1bvYsBTkhdUNtGIf5xNOg938rrSSYvS9A==", + "version": "2.8.3", + "resolved": "https://registry.npmjs.org/yaml/-/yaml-2.8.3.tgz", + "integrity": "sha512-AvbaCLOO2Otw/lW5bmh9d/WEdcDFdQp2Z2ZUH3pX9U2ihyUY0nvLv7J6TrWowklRGPYbB/IuIMfYgxaCPg5Bpg==", "license": "ISC", "bin": { "yaml": "bin.mjs" diff --git a/src/static/app/package.json b/src/static/app/package.json index a12f1fa4..4c73028e 100644 --- a/src/static/app/package.json +++ b/src/static/app/package.json @@ -1,6 +1,6 @@ { "name": "app", - "version": "4.3.2", + "version": "4.3.3", "private": true, "type": "module", "module": "es2022", @@ -15,12 +15,12 @@ "@volar/language-server": "2.4.28", "@vue/language-server": "3.2.4", "@vuepic/vue-datepicker": "^12.1.0", - "@vueuse/core": "^14.2.0", "@vueuse/shared": "^14.2.1", + "@vueuse/core": "^14.2.1", "animate.css": "^4.1.1", "bootstrap": "^5.3.2", "bootstrap-icons": "^1.11.3", - "cidr-tools": "^11.0.8", + "cidr-tools": "^11.3.2", "css-color-converter": "^2.0.0", "dayjs": "^1.11.19", "electron-builder": "^26.7.0", diff --git a/src/static/app/src/components/clientComponents/clientSettings.vue b/src/static/app/src/components/clientComponents/clientSettings.vue index ae554db4..d872a347 100644 --- a/src/static/app/src/components/clientComponents/clientSettings.vue +++ b/src/static/app/src/components/clientComponents/clientSettings.vue @@ -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 } @@ -37,16 +41,43 @@ const toggleClientSideApp = async () => {
- +
+
+
+
+ +
+
+ + +
+
+ + + +
+
+ + + + +
+ diff --git a/src/static/app/src/components/configurationComponents/editConfiguration.vue b/src/static/app/src/components/configurationComponents/editConfiguration.vue index 164af07e..f5d67dc4 100644 --- a/src/static/app/src/components/configurationComponents/editConfiguration.vue +++ b/src/static/app/src/components/configurationComponents/editConfiguration.vue @@ -197,14 +197,14 @@ const deleteConfigurationModal = ref(false) v-model="data[key]" :id="'configuration_' + key"> -
- diff --git a/src/static/app/src/components/configurationComponents/newPeersComponents/notesInput.vue b/src/static/app/src/components/configurationComponents/newPeersComponents/notesInput.vue new file mode 100644 index 00000000..6d12adee --- /dev/null +++ b/src/static/app/src/components/configurationComponents/newPeersComponents/notesInput.vue @@ -0,0 +1,31 @@ + + + + + \ No newline at end of file diff --git a/src/static/app/src/components/configurationComponents/peer.vue b/src/static/app/src/components/configurationComponents/peer.vue index b514463b..4f98f696 100644 --- a/src/static/app/src/components/configurationComponents/peer.vue +++ b/src/static/app/src/components/configurationComponents/peer.vue @@ -134,12 +134,17 @@ export default {
- diff --git a/src/static/app/src/components/configurationComponents/peerAddModal.vue b/src/static/app/src/components/configurationComponents/peerAddModal.vue index 5212464c..e0ccded2 100644 --- a/src/static/app/src/components/configurationComponents/peerAddModal.vue +++ b/src/static/app/src/components/configurationComponents/peerAddModal.vue @@ -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(() => { @@ -118,7 +120,7 @@ watch(() => { -