From 581ce7ef960b85ec97739b9e499b1a0b05e647ff Mon Sep 17 00:00:00 2001 From: Uchechukwu Orji Date: Sat, 31 Aug 2024 22:11:30 +0100 Subject: [PATCH 1/5] add script to download protonvpn wireguard configuration files. --- .../Dockerfile | 15 ++ .../README.md | 23 ++ .../pyproject.toml | 237 ++++++++++++++++++ .../__about__.py | 1 + .../__init__.py | 11 + .../entrypoint.py | 54 ++++ .../protonvpn.py | 91 +++++++ .../settings.py | 35 +++ .../tasks.py | 110 ++++++++ .../tests/test_basic.py | 0 10 files changed, 577 insertions(+) create mode 100644 protonvpn-wireguard-config-downloader/Dockerfile create mode 100644 protonvpn-wireguard-config-downloader/README.md create mode 100644 protonvpn-wireguard-config-downloader/pyproject.toml create mode 100644 protonvpn-wireguard-config-downloader/src/protonvpn_wireguard_config_downloader/__about__.py create mode 100644 protonvpn-wireguard-config-downloader/src/protonvpn_wireguard_config_downloader/__init__.py create mode 100644 protonvpn-wireguard-config-downloader/src/protonvpn_wireguard_config_downloader/entrypoint.py create mode 100644 protonvpn-wireguard-config-downloader/src/protonvpn_wireguard_config_downloader/protonvpn.py create mode 100644 protonvpn-wireguard-config-downloader/src/protonvpn_wireguard_config_downloader/settings.py create mode 100644 protonvpn-wireguard-config-downloader/tasks.py create mode 100644 protonvpn-wireguard-config-downloader/tests/test_basic.py diff --git a/protonvpn-wireguard-config-downloader/Dockerfile b/protonvpn-wireguard-config-downloader/Dockerfile new file mode 100644 index 0000000..22636c7 --- /dev/null +++ b/protonvpn-wireguard-config-downloader/Dockerfile @@ -0,0 +1,15 @@ +FROM python:3.11-slim-bookworm +LABEL org.opencontainers.image.source=https://github.com/kiwix/mirrors-qa + +RUN apt-get update && apt-get install -y gnupg2 + +COPY src /src/src + +COPY pyproject.toml README.md /src/ + +RUN pip install --no-cache-dir /src \ + && rm -rf /src + +RUN mkdir /data + +CMD ["protonvpn-wireguard-configs", "--help"] diff --git a/protonvpn-wireguard-config-downloader/README.md b/protonvpn-wireguard-config-downloader/README.md new file mode 100644 index 0000000..718b9e4 --- /dev/null +++ b/protonvpn-wireguard-config-downloader/README.md @@ -0,0 +1,23 @@ +# ProtonVPN Wireguard Configuration Downloader + +A tool to automatically download wireguard configuration files for all available VPN servers from ProtonVPN. + +**NOTE** +This script is intended to be used in Linux environments only. + +## Environment Variables + +- `USERNAME`: username for connnecting to ProtonVPN account +- `PASSWORD` +- `WORKDIR`: location to store configuration files. (default: /data) +- `WIREGUARD_PORT`: Port of the wireguard configuration files (default: 51820) + +## Usage +- Build the image + ```sh + docker build -t protonvpn-wireguard-config-downloader . + ``` +- Download the configuration files + ```sh + docker run --rm -e USERNAME=abcd@efg -e PASSWORD=pa55word -v ./:/data protonvpn-wireguard-config-downloader protonvpn-wireguard-configs + ``` diff --git a/protonvpn-wireguard-config-downloader/pyproject.toml b/protonvpn-wireguard-config-downloader/pyproject.toml new file mode 100644 index 0000000..2b62d11 --- /dev/null +++ b/protonvpn-wireguard-config-downloader/pyproject.toml @@ -0,0 +1,237 @@ +[build-system] +requires = ["hatchling", "hatch-openzim"] +build-backend = "hatchling.build" + +[project] +name = "protonvpn_wireguard_config_downloader" +requires-python = ">=3.11,<3.13" +description = "ProtonVPN Wireguard Configuration Files Downloader" +readme = "README.md" +authors = [ + { name = "Kiwix", email = "dev@kiwix.org" }, +] +keywords = ["protonvpn", "wireguard"] +dependencies = [ + "proton-core @ https://github.com/ProtonVPN/python-proton-core/archive/refs/tags/v0.2.0.zip", + "proton-vpn-logger @ https://github.com/ProtonVPN/python-proton-vpn-logger/archive/refs/tags/v0.2.1.zip", + "proton-vpn-api-core @ https://github.com/ProtonVPN/python-proton-vpn-api-core/archive/refs/tags/v0.32.2.zip", + "distro==1.9.0" +] +license = {text = "GPL-3.0-or-later"} +classifiers = [ + "Programming Language :: Python :: 3", + "Programming Language :: Python :: 3.11", + "License :: OSI Approved :: GNU General Public License v3 or later (GPLv3+)", +] + +dynamic = ["version"] + +[project.urls] +Homepage = "https://github.com/kiwix/mirrors-qa/protonvpn-wireguard-config-downloader" + +[project.optional-dependencies] +scripts = [ + "invoke==2.2.0", +] +lint = [ + "black==24.1.1", + "ruff==0.2.0", +] +check = [ + "pyright==1.1.349", +] +test = [ + "pytest==8.0.0", + "coverage==7.4.1", +] +dev = [ + "pre-commit==3.6.0", + "debugpy==1.8.0", + "protonvpn_wireguard_config_downloader[scripts]", + "protonvpn_wireguard_config_downloader[lint]", + "protonvpn_wireguard_config_downloader[test]", + "protonvpn_wireguard_config_downloader[check]", +] + +[project.scripts] +protonvpn-wireguard-configs = "protonvpn_wireguard_config_downloader.entrypoint:main" + +[tool.hatch.version] +path = "src/protonvpn_wireguard_config_downloader/__about__.py" + +[tool.hatch.build] +exclude = [ + "/.github", +] + +[tool.hatch.metadata] +allow-direct-references = true + +[tool.hatch.build.targets.wheel] +packages = ["src/protonvpn_wireguard_config_downloader"] + +[tool.hatch.envs.default] +features = ["dev"] + +[tool.hatch.envs.test] +features = ["scripts", "test"] + + +[tool.hatch.envs.test.scripts] +run = "inv test --args '{args}'" +run-cov = "inv test-cov --args '{args}'" +report-cov = "inv report-cov" +coverage = "inv coverage --args '{args}'" +html = "inv coverage --html --args '{args}'" + +[tool.hatch.envs.lint] +template = "lint" +skip-install = false +features = ["scripts", "lint"] + +[tool.hatch.envs.lint.scripts] +black = "inv lint-black --args '{args}'" +ruff = "inv lint-ruff --args '{args}'" +all = "inv lintall --args '{args}'" +fix-black = "inv fix-black --args '{args}'" +fix-ruff = "inv fix-ruff --args '{args}'" +fixall = "inv fixall --args '{args}'" + +[tool.hatch.envs.check] +features = ["scripts", "check"] + +[tool.hatch.envs.check.scripts] +pyright = "inv check-pyright --args '{args}'" +all = "inv checkall --args '{args}'" + +[tool.black] +line-length = 88 +target-version = ['py310'] + +[tool.ruff] +target-version = "py311" +line-length = 88 +src = ["src"] + +[tool.ruff.lint] +select = [ + "A", # flake8-builtins + # "ANN", # flake8-annotations + "ARG", # flake8-unused-arguments + # "ASYNC", # flake8-async + "B", # flake8-bugbear + # "BLE", # flake8-blind-except + "C4", # flake8-comprehensions + "C90", # mccabe + # "COM", # flake8-commas + # "D", # pydocstyle + # "DJ", # flake8-django + "DTZ", # flake8-datetimez + "E", # pycodestyle (default) + "EM", # flake8-errmsg + # "ERA", # eradicate + # "EXE", # flake8-executable + "F", # Pyflakes (default) + # "FA", # flake8-future-annotations + "FBT", # flake8-boolean-trap + # "FLY", # flynt + # "G", # flake8-logging-format + "I", # isort + "ICN", # flake8-import-conventions + # "INP", # flake8-no-pep420 + # "INT", # flake8-gettext + "ISC", # flake8-implicit-str-concat + "N", # pep8-naming + # "NPY", # NumPy-specific rules + # "PD", # pandas-vet + # "PGH", # pygrep-hooks + # "PIE", # flake8-pie + # "PL", # Pylint + "PLC", # Pylint: Convention + "PLE", # Pylint: Error + "PLR", # Pylint: Refactor + "PLW", # Pylint: Warning + # "PT", # flake8-pytest-style + # "PTH", # flake8-use-pathlib + # "PYI", # flake8-pyi + "Q", # flake8-quotes + # "RET", # flake8-return + # "RSE", # flake8-raise + "RUF", # Ruff-specific rules + "S", # flake8-bandit + # "SIM", # flake8-simplify + # "SLF", # flake8-self + "T10", # flake8-debugger + "T20", # flake8-print + # "TCH", # flake8-type-checking + # "TD", # flake8-todos + "TID", # flake8-tidy-imports + # "TRY", # tryceratops + "UP", # pyupgrade + "W", # pycodestyle + "YTT", # flake8-2020 +] +ignore = [ + # Allow non-abstract empty methods in abstract base classes + "B027", + # Remove flake8-errmsg since we consider they bloat the code and provide limited value + "EM", + # Allow boolean positional values in function calls, like `dict.get(... True)` + "FBT003", + # Ignore checks for possible passwords + "S105", "S106", "S107", + # Ignore warnings on subprocess.run / popen + "S603", + # Ignore complexity + "C901", "PLR0911", "PLR0912", "PLR0913", "PLR0915", +] +unfixable = [ + # Don't touch unused imports + "F401", +] + +[tool.ruff.lint.isort] +known-first-party = ["protonvpn_wireguard_config_downloader"] + +[tool.ruff.lint.flake8-bugbear] +# add exceptions to B008 for fastapi. +extend-immutable-calls = ["fastapi.Depends", "fastapi.Query"] + +[tool.ruff.lint.flake8-tidy-imports] +ban-relative-imports = "all" + +[tool.ruff.lint.per-file-ignores] +# Tests can use magic values, assertions, and relative imports +"tests/**/*" = ["PLR2004", "S101", "TID252"] + +[tool.pytest.ini_options] +minversion = "7.3" +testpaths = ["tests"] +pythonpath = [".", "src"] + +[tool.coverage.paths] +protonvpn_wireguard_config_downloader = ["src/protonvpn_wireguard_config_downloader"] +tests = ["tests"] + +[tool.coverage.run] +source_pkgs = ["protonvpn_wireguard_config_downloader"] +branch = true +parallel = true +omit = [ + "src/protonvpn_wireguard_config_downloader/__about__.py", +] + +[tool.coverage.report] +exclude_lines = [ + "no cov", + "if __name__ == .__main__.:", + "if TYPE_CHECKING:", +] + +[tool.pyright] +include = ["src", "tests", "tasks.py"] +exclude = [".env/**", ".venv/**"] +extraPaths = ["src"] +pythonVersion = "3.11" +typeCheckingMode="strict" +disableBytesTypePromotions = true diff --git a/protonvpn-wireguard-config-downloader/src/protonvpn_wireguard_config_downloader/__about__.py b/protonvpn-wireguard-config-downloader/src/protonvpn_wireguard_config_downloader/__about__.py new file mode 100644 index 0000000..a237a3e --- /dev/null +++ b/protonvpn-wireguard-config-downloader/src/protonvpn_wireguard_config_downloader/__about__.py @@ -0,0 +1 @@ +__version__ = "0.0.1-dev0" diff --git a/protonvpn-wireguard-config-downloader/src/protonvpn_wireguard_config_downloader/__init__.py b/protonvpn-wireguard-config-downloader/src/protonvpn_wireguard_config_downloader/__init__.py new file mode 100644 index 0000000..a3bebc9 --- /dev/null +++ b/protonvpn-wireguard-config-downloader/src/protonvpn_wireguard_config_downloader/__init__.py @@ -0,0 +1,11 @@ +import logging + +from protonvpn_wireguard_config_downloader.settings import Settings + +logger = logging.getLogger("task") + +if not logger.hasHandlers(): + logger.setLevel(logging.DEBUG if Settings.DEBUG else logging.INFO) + handler = logging.StreamHandler() + handler.setFormatter(logging.Formatter("[%(asctime)s: %(levelname)s] %(message)s")) + logger.addHandler(handler) diff --git a/protonvpn-wireguard-config-downloader/src/protonvpn_wireguard_config_downloader/entrypoint.py b/protonvpn-wireguard-config-downloader/src/protonvpn_wireguard_config_downloader/entrypoint.py new file mode 100644 index 0000000..4fca789 --- /dev/null +++ b/protonvpn-wireguard-config-downloader/src/protonvpn_wireguard_config_downloader/entrypoint.py @@ -0,0 +1,54 @@ +import argparse +import asyncio +import logging +from pathlib import Path + +from protonvpn_wireguard_config_downloader import logger +from protonvpn_wireguard_config_downloader.__about__ import __version__ +from protonvpn_wireguard_config_downloader.protonvpn import ( + login, + logout, + save_vpn_server_wireguard_config, + vpn_servers, +) +from protonvpn_wireguard_config_downloader.settings import Settings + + +async def download_vpn_wireguard_configs( + username: str, password: str, wireguard_port: int, work_dir: Path +) -> None: + """Download Wireguard configuration files for all VPN servers.""" + session = await login(username, password) + try: + logger.debug("Fetching available VPN servers for client...") + for vpn_server in vpn_servers(session, wireguard_port): + save_vpn_server_wireguard_config(session, vpn_server, work_dir) + finally: + logger.debug("Logging out...") + await logout(session) + logger.info("Successfully logged out client.") + + +def main(): + parser = argparse.ArgumentParser() + parser.add_argument( + "-v", "--verbose", help="Show verbose output", action="store_true" + ) + parser.add_argument( + "--version", + help="Show version and exit.", + action="version", + version="%(prog)s " + __version__, + ) + args = parser.parse_args() + if args.verbose: + logger.setLevel(logging.DEBUG) + + asyncio.run( + download_vpn_wireguard_configs( + Settings.USERNAME, + Settings.PASSWORD, + Settings.WIREGUARD_PORT, + Settings.WORKDIR, + ) + ) diff --git a/protonvpn-wireguard-config-downloader/src/protonvpn_wireguard_config_downloader/protonvpn.py b/protonvpn-wireguard-config-downloader/src/protonvpn_wireguard_config_downloader/protonvpn.py new file mode 100644 index 0000000..ecca919 --- /dev/null +++ b/protonvpn-wireguard-config-downloader/src/protonvpn_wireguard_config_downloader/protonvpn.py @@ -0,0 +1,91 @@ +# pyright: strict, reportMissingTypeStubs=false, reportUnknownMemberType=false, reportOptionalSubscript=false, reportUnknownVariableType=false, reportUnknownArgumentType=false +from collections.abc import Generator +from pathlib import Path +from typing import cast + +from proton.sso import ProtonSSO +from proton.vpn.connection.vpnconfiguration import WireguardConfig +from proton.vpn.core.connection import VPNServer +from proton.vpn.session import VPNSession + +from protonvpn_wireguard_config_downloader import logger +from protonvpn_wireguard_config_downloader.settings import Settings + + +async def login(username: str, password: str) -> VPNSession: + """Log in to Proton VPN account.""" + logger.info("Logging in to ProtonVPN...") + sso = ProtonSSO( + user_agent=Settings.USER_AGENT, appversion=Settings.PROTONVPN_APP_VERSION + ) + session = cast(VPNSession, sso.get_session(username, override_class=VPNSession)) + logger.debug("Authenticating credentials with ProtonVPN.") + await session.async_authenticate(username, password) + logger.debug("Fetching client session data.") + await session.fetch_session_data() + logger.info("Logged in to ProtonVPN.") + return session + + +async def logout(session: VPNSession) -> None: + """Log out from the Proton VPN account.""" + if session.authenticated: + await session.async_logout() + + +def wireguard_port_is_available(session: VPNSession, port: int) -> bool: + """Check that the wireguard port is available in the client config.""" + return port in session.client_config.wireguard_ports.udp + + +def vpn_servers( + session: VPNSession, + wireguard_port: int, +) -> Generator[VPNServer, None, None]: + """Generate the available VPN servers for this account. + + Raises: + ValueError: Specified wireguard port is not available for this client. + """ + client_config = session.client_config + if wireguard_port not in client_config.wireguard_ports.udp: + raise ValueError(f"Port {wireguard_port} is not available in client config.") + + logical_servers = ( + server + for server in session.server_list.logicals + if server.enabled and server.tier <= session.server_list.user_tier + ) + return ( + VPNServer( + server_ip=physical_server.entry_ip, + domain=physical_server.domain, + x25519pk=physical_server.x25519_pk, + openvpn_ports=client_config.openvpn_ports, + wireguard_ports=[ + wireguard_port + ], # pyright: ignore[reportGeneralTypeIssues, reportArgumentType] + server_id=logical_server.id, + server_name=f"{logical_server.exit_country.lower()}-{logical_server.name}", + label=physical_server.label, + ) + for logical_server in logical_servers + for physical_server in logical_server.physical_servers + ) + + +def save_vpn_server_wireguard_config( + session: VPNSession, vpn_server: VPNServer, dest_dir: Path +) -> Path: + """Save the Wireguard config for the VPN server.""" + logger.debug(f"Saving configuration file for VPN server: {vpn_server.server_name}") + config = WireguardConfig( + vpn_server, session.vpn_account.vpn_credentials, None, use_certificate=True + ) + dest_fpath = dest_dir / f"{vpn_server.server_name}.conf" + dest_fpath.write_text(config.generate(), encoding="utf-8") + logger.info( + f"Saved configuration file for VPN server: {vpn_server.server_name}, " + f"name: {dest_fpath.name}" + ) + return dest_fpath diff --git a/protonvpn-wireguard-config-downloader/src/protonvpn_wireguard_config_downloader/settings.py b/protonvpn-wireguard-config-downloader/src/protonvpn_wireguard_config_downloader/settings.py new file mode 100644 index 0000000..caa6a8c --- /dev/null +++ b/protonvpn-wireguard-config-downloader/src/protonvpn_wireguard_config_downloader/settings.py @@ -0,0 +1,35 @@ +import os +from pathlib import Path +from typing import Any + +import distro + + +def getenv(key: str, *, mandatory: bool = False, default: Any = None) -> Any: + value = os.getenv(key, default=default) + + if mandatory and not value: + raise OSError(f"Please set the {key} environment variable") + + return value + + +class Settings: + """Task worker configuration""" + + USERNAME = getenv("USERNAME", mandatory=True) + PASSWORD = getenv("PASSWORD", mandatory=True) + DEBUG = bool(getenv("DEBUG", default=False)) + WORKDIR = Path(getenv("WORKDIR", default="/data")).resolve() + PROTONVPN_VERSION = getenv("PROTONVPN_APP_VERSION", default="4.4.4") + PROTONVPN_APP_VERSION = getenv( + "PROTONVPN_APP_VERSION", default=f"LinuxVPN_{PROTONVPN_VERSION}" + ) + USER_AGENT = getenv( + "USER_AGENT", + default=( + f"ProtonVPN/{PROTONVPN_VERSION} " + f"(Linux; {distro.name()}/{distro.version()})" + ), + ) + WIREGUARD_PORT = int(getenv("WIREGUARD_PORT", default=51820)) diff --git a/protonvpn-wireguard-config-downloader/tasks.py b/protonvpn-wireguard-config-downloader/tasks.py new file mode 100644 index 0000000..87cd552 --- /dev/null +++ b/protonvpn-wireguard-config-downloader/tasks.py @@ -0,0 +1,110 @@ +# pyright: strict, reportUntypedFunctionDecorator=false +import os + +from invoke.context import Context +from invoke.tasks import task # pyright: ignore [reportUnknownVariableType] + +use_pty = not os.getenv("CI", "") + + +@task(optional=["args"], help={"args": "pytest additional arguments"}) +def test(ctx: Context, args: str = ""): + """run tests (without coverage)""" + ctx.run(f"pytest {args}", pty=use_pty) + + +@task(optional=["args"], help={"args": "pytest additional arguments"}) +def test_cov(ctx: Context, args: str = ""): + """run test vith coverage""" + ctx.run(f"coverage run -m pytest {args}", pty=use_pty) + + +@task(optional=["html"], help={"html": "flag to export html report"}) +def report_cov(ctx: Context, *, html: bool = False): + """report coverage""" + ctx.run("coverage combine", warn=True, pty=use_pty) + ctx.run("coverage report --show-missing", pty=use_pty) + ctx.run("coverage xml", pty=use_pty) + if html: + ctx.run("coverage html", pty=use_pty) + + +@task( + optional=["args", "html"], + help={ + "args": "pytest additional arguments", + "html": "flag to export html report", + }, +) +def coverage(ctx: Context, args: str = "", *, html: bool = False): + """run tests and report coverage""" + test_cov(ctx, args=args) + report_cov(ctx, html=html) + + +@task(optional=["args"], help={"args": "black additional arguments"}) +def lint_black(ctx: Context, args: str = "."): + args = args or "." # needed for hatch script + ctx.run("black --version", pty=use_pty) + ctx.run(f"black --check --diff {args}", pty=use_pty) + + +@task(optional=["args"], help={"args": "ruff additional arguments"}) +def lint_ruff(ctx: Context, args: str = "."): + args = args or "." # needed for hatch script + ctx.run("ruff --version", pty=use_pty) + ctx.run(f"ruff check {args}", pty=use_pty) + + +@task( + optional=["args"], + help={ + "args": "linting tools (black, ruff) additional arguments, typically a path", + }, +) +def lintall(ctx: Context, args: str = "."): + """Check linting""" + args = args or "." # needed for hatch script + lint_black(ctx, args) + lint_ruff(ctx, args) + + +@task(optional=["args"], help={"args": "check tools (pyright) additional arguments"}) +def check_pyright(ctx: Context, args: str = ""): + """check static types with pyright""" + ctx.run("pyright --version") + ctx.run(f"pyright {args}", pty=use_pty) + + +@task(optional=["args"], help={"args": "check tools (pyright) additional arguments"}) +def checkall(ctx: Context, args: str = ""): + """check static types""" + check_pyright(ctx, args) + + +@task(optional=["args"], help={"args": "black additional arguments"}) +def fix_black(ctx: Context, args: str = "."): + """fix black formatting""" + args = args or "." # needed for hatch script + ctx.run(f"black {args}", pty=use_pty) + + +@task(optional=["args"], help={"args": "ruff additional arguments"}) +def fix_ruff(ctx: Context, args: str = "."): + """fix all ruff rules""" + args = args or "." # needed for hatch script + ctx.run(f"ruff check --fix {args}", pty=use_pty) + + +@task( + optional=["args"], + help={ + "args": "linting tools (black, ruff) additional arguments, typically a path", + }, +) +def fixall(ctx: Context, args: str = "."): + """Fix everything automatically""" + args = args or "." # needed for hatch script + fix_black(ctx, args) + fix_ruff(ctx, args) + lintall(ctx, args) diff --git a/protonvpn-wireguard-config-downloader/tests/test_basic.py b/protonvpn-wireguard-config-downloader/tests/test_basic.py new file mode 100644 index 0000000..e69de29 From 5120e9ad93991600f66054c2d034726b0a62e34b Mon Sep 17 00:00:00 2001 From: Uchechukwu Orji Date: Sat, 31 Aug 2024 22:15:01 +0100 Subject: [PATCH 2/5] add QA CI for protonvpn-wireguard-config-downloader --- ...tonvpn-wireguard-config-downloader-QA.yaml | 40 +++++++++++++++++++ 1 file changed, 40 insertions(+) create mode 100644 .github/workflows/protonvpn-wireguard-config-downloader-QA.yaml diff --git a/.github/workflows/protonvpn-wireguard-config-downloader-QA.yaml b/.github/workflows/protonvpn-wireguard-config-downloader-QA.yaml new file mode 100644 index 0000000..10d858a --- /dev/null +++ b/.github/workflows/protonvpn-wireguard-config-downloader-QA.yaml @@ -0,0 +1,40 @@ +name: Worker Task QA + +on: + pull_request: + push: + paths: + - 'protonvpn-wireguard-config-downloader/**' + branches: + - main + +jobs: + + check-qa: + runs-on: ubuntu-24.04 + steps: + - uses: actions/checkout@v3 + + - name: Set up Python + uses: actions/setup-python@v4 + with: + python-version-file: protonvpn-wireguard-config-downloader/pyproject.toml + architecture: x64 + + - name: Install dependencies (and project) + working-directory: worker/task + run: | + pip install -U pip + pip install -e .[lint,scripts,test,check] + + - name: Check black formatting + working-directory: protonvpn-wireguard-config-downloader + run: inv lint-black + + - name: Check ruff + working-directory: protonvpn-wireguard-config-downloader + run: inv lint-ruff + + - name: Check pyright + working-directory: protonvpn-wireguard-config-downloader + run: inv check-pyright From 3f48af76435957ab3013fcd7e420a7a4ca2f2ce7 Mon Sep 17 00:00:00 2001 From: Uchechukwu Orji Date: Sat, 31 Aug 2024 22:22:52 +0100 Subject: [PATCH 3/5] give QA task appropriate name --- .github/workflows/protonvpn-wireguard-config-downloader-QA.yaml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/protonvpn-wireguard-config-downloader-QA.yaml b/.github/workflows/protonvpn-wireguard-config-downloader-QA.yaml index 10d858a..db349b6 100644 --- a/.github/workflows/protonvpn-wireguard-config-downloader-QA.yaml +++ b/.github/workflows/protonvpn-wireguard-config-downloader-QA.yaml @@ -1,4 +1,4 @@ -name: Worker Task QA +name: ProtonVPN Wireguard Config Downloader QA on: pull_request: From dce883baf1eacd5c28966a1e79622d70558f4d02 Mon Sep 17 00:00:00 2001 From: Uchechukwu Orji Date: Sat, 31 Aug 2024 22:29:29 +0100 Subject: [PATCH 4/5] update working directory for QA --- .github/workflows/protonvpn-wireguard-config-downloader-QA.yaml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/protonvpn-wireguard-config-downloader-QA.yaml b/.github/workflows/protonvpn-wireguard-config-downloader-QA.yaml index db349b6..f8c4329 100644 --- a/.github/workflows/protonvpn-wireguard-config-downloader-QA.yaml +++ b/.github/workflows/protonvpn-wireguard-config-downloader-QA.yaml @@ -22,7 +22,7 @@ jobs: architecture: x64 - name: Install dependencies (and project) - working-directory: worker/task + working-directory: protonvpn-wireguard-config-downloader run: | pip install -U pip pip install -e .[lint,scripts,test,check] From 45b4e65a12a0454ed23ffa67474c0434dc501b1b Mon Sep 17 00:00:00 2001 From: Uchechukwu Orji Date: Mon, 2 Sep 2024 16:23:25 +0100 Subject: [PATCH 5/5] improve documentation and comments --- protonvpn-wireguard-config-downloader/Dockerfile | 1 + protonvpn-wireguard-config-downloader/README.md | 4 ++-- .../pyproject.toml | 15 ++++++++------- .../__about__.py | 2 +- .../protonvpn.py | 7 ++----- .../settings.py | 12 +++--------- 6 files changed, 17 insertions(+), 24 deletions(-) diff --git a/protonvpn-wireguard-config-downloader/Dockerfile b/protonvpn-wireguard-config-downloader/Dockerfile index 22636c7..dabd290 100644 --- a/protonvpn-wireguard-config-downloader/Dockerfile +++ b/protonvpn-wireguard-config-downloader/Dockerfile @@ -1,6 +1,7 @@ FROM python:3.11-slim-bookworm LABEL org.opencontainers.image.source=https://github.com/kiwix/mirrors-qa +# We need gnupg2 for python-gnupg used in Proton libraries to work properly. RUN apt-get update && apt-get install -y gnupg2 COPY src /src/src diff --git a/protonvpn-wireguard-config-downloader/README.md b/protonvpn-wireguard-config-downloader/README.md index 718b9e4..7d8bee4 100644 --- a/protonvpn-wireguard-config-downloader/README.md +++ b/protonvpn-wireguard-config-downloader/README.md @@ -10,7 +10,7 @@ This script is intended to be used in Linux environments only. - `USERNAME`: username for connnecting to ProtonVPN account - `PASSWORD` - `WORKDIR`: location to store configuration files. (default: /data) -- `WIREGUARD_PORT`: Port of the wireguard configuration files (default: 51820) +- `WIREGUARD_PORT`: Port of the wireguard configuration files (default: 51820).This allows to choose the wireguard port for the configuration files rather than leaving it to the ProtonVPN library which often defaults to the first available port in the session object. ## Usage - Build the image @@ -19,5 +19,5 @@ This script is intended to be used in Linux environments only. ``` - Download the configuration files ```sh - docker run --rm -e USERNAME=abcd@efg -e PASSWORD=pa55word -v ./:/data protonvpn-wireguard-config-downloader protonvpn-wireguard-configs + docker run --rm -e USERNAME=abcd@efg -e PASSWORD=pa55word -v ./proton:/data protonvpn-wireguard-config-downloader protonvpn-wireguard-configs ``` diff --git a/protonvpn-wireguard-config-downloader/pyproject.toml b/protonvpn-wireguard-config-downloader/pyproject.toml index 2b62d11..3cd18fe 100644 --- a/protonvpn-wireguard-config-downloader/pyproject.toml +++ b/protonvpn-wireguard-config-downloader/pyproject.toml @@ -21,6 +21,7 @@ license = {text = "GPL-3.0-or-later"} classifiers = [ "Programming Language :: Python :: 3", "Programming Language :: Python :: 3.11", + "Programming Language :: Python :: 3.12", "License :: OSI Approved :: GNU General Public License v3 or later (GPLv3+)", ] @@ -34,19 +35,19 @@ scripts = [ "invoke==2.2.0", ] lint = [ - "black==24.1.1", - "ruff==0.2.0", + "black==24.4.2", + "ruff==0.5.1", ] check = [ - "pyright==1.1.349", + "pyright==1.1.370", ] test = [ - "pytest==8.0.0", - "coverage==7.4.1", + "pytest==8.2.2", + "coverage==7.5.4", ] dev = [ - "pre-commit==3.6.0", - "debugpy==1.8.0", + "pre-commit==3.7.1", + "debugpy==1.8.2", "protonvpn_wireguard_config_downloader[scripts]", "protonvpn_wireguard_config_downloader[lint]", "protonvpn_wireguard_config_downloader[test]", diff --git a/protonvpn-wireguard-config-downloader/src/protonvpn_wireguard_config_downloader/__about__.py b/protonvpn-wireguard-config-downloader/src/protonvpn_wireguard_config_downloader/__about__.py index a237a3e..5becc17 100644 --- a/protonvpn-wireguard-config-downloader/src/protonvpn_wireguard_config_downloader/__about__.py +++ b/protonvpn-wireguard-config-downloader/src/protonvpn_wireguard_config_downloader/__about__.py @@ -1 +1 @@ -__version__ = "0.0.1-dev0" +__version__ = "1.0.0" diff --git a/protonvpn-wireguard-config-downloader/src/protonvpn_wireguard_config_downloader/protonvpn.py b/protonvpn-wireguard-config-downloader/src/protonvpn_wireguard_config_downloader/protonvpn.py index ecca919..9cf0d7d 100644 --- a/protonvpn-wireguard-config-downloader/src/protonvpn_wireguard_config_downloader/protonvpn.py +++ b/protonvpn-wireguard-config-downloader/src/protonvpn_wireguard_config_downloader/protonvpn.py @@ -33,11 +33,6 @@ async def logout(session: VPNSession) -> None: await session.async_logout() -def wireguard_port_is_available(session: VPNSession, port: int) -> bool: - """Check that the wireguard port is available in the client config.""" - return port in session.client_config.wireguard_ports.udp - - def vpn_servers( session: VPNSession, wireguard_port: int, @@ -51,6 +46,8 @@ def vpn_servers( if wireguard_port not in client_config.wireguard_ports.udp: raise ValueError(f"Port {wireguard_port} is not available in client config.") + # Build up the list of servers, filtering out disabled servers and + # servers that are above the client's tier. logical_servers = ( server for server in session.server_list.logicals diff --git a/protonvpn-wireguard-config-downloader/src/protonvpn_wireguard_config_downloader/settings.py b/protonvpn-wireguard-config-downloader/src/protonvpn_wireguard_config_downloader/settings.py index caa6a8c..26ceb5f 100644 --- a/protonvpn-wireguard-config-downloader/src/protonvpn_wireguard_config_downloader/settings.py +++ b/protonvpn-wireguard-config-downloader/src/protonvpn_wireguard_config_downloader/settings.py @@ -22,14 +22,8 @@ class Settings: DEBUG = bool(getenv("DEBUG", default=False)) WORKDIR = Path(getenv("WORKDIR", default="/data")).resolve() PROTONVPN_VERSION = getenv("PROTONVPN_APP_VERSION", default="4.4.4") - PROTONVPN_APP_VERSION = getenv( - "PROTONVPN_APP_VERSION", default=f"LinuxVPN_{PROTONVPN_VERSION}" - ) - USER_AGENT = getenv( - "USER_AGENT", - default=( - f"ProtonVPN/{PROTONVPN_VERSION} " - f"(Linux; {distro.name()}/{distro.version()})" - ), + PROTONVPN_APP_VERSION = f"LinuxVPN_{PROTONVPN_VERSION}" + USER_AGENT = ( + f"ProtonVPN/{PROTONVPN_VERSION} (Linux; {distro.name()}/{distro.version()})" ) WIREGUARD_PORT = int(getenv("WIREGUARD_PORT", default=51820))