diff --git a/.github/workflows/codeql-analysis.yml b/.github/workflows/codeql-analysis.yml index df3823f..2dce297 100644 --- a/.github/workflows/codeql-analysis.yml +++ b/.github/workflows/codeql-analysis.yml @@ -1,51 +1,24 @@ -name: "Code scanning - action" +name: "Code Scanning - Action" on: push: + branches: [main] pull_request: - schedule: - - cron: '0 15 * * 2' + branches: [main] jobs: CodeQL-Build: - runs-on: ubuntu-latest - steps: - - name: Checkout repository - uses: actions/checkout@v2 - with: - # We must fetch at least the immediate parents so that if this is - # a pull request then we can checkout the head. - fetch-depth: 2 - - # If this run was triggered by a pull request event, then checkout - # the head of the pull request instead of the merge commit. - - run: git checkout HEAD^2 - if: ${{ github.event_name == 'pull_request' }} - - # Initializes the CodeQL tools for scanning. - - name: Initialize CodeQL - uses: github/codeql-action/init@v1 - # Override language selection by uncommenting this and choosing your languages - # with: - # languages: go, javascript, csharp, python, cpp, java - - # Autobuild attempts to build any compiled languages (C/C++, C#, or Java). - # If this step fails, then you should remove it and run the build manually (see below) - - name: Autobuild - uses: github/codeql-action/autobuild@v1 - - # ℹī¸ Command-line programs to run using the OS shell. - # 📚 https://git.io/JvXDl - - # ✏ī¸ If the Autobuild fails above, remove it and uncomment the following three lines - # and modify them (or add more) to build your code if your project - # uses a compiled language - - #- run: | - # make bootstrap - # make release - - - name: Perform CodeQL Analysis - uses: github/codeql-action/analyze@v1 + - name: Checkout repository + uses: actions/checkout@v4 + with: + fetch-depth: 2 + - run: git checkout HEAD^2 + if: ${{ github.event_name == 'pull_request' }} + - name: Initialize CodeQL + uses: github/codeql-action/init@v3 + - name: Autobuild + uses: github/codeql-action/autobuild@v3 + - name: Perform CodeQL Analysis + uses: github/codeql-action/analyze@v3 diff --git a/.github/workflows/create-release-and-upload-assets.yml b/.github/workflows/create-release-and-upload-assets.yml index 9e3955a..d443d8a 100644 --- a/.github/workflows/create-release-and-upload-assets.yml +++ b/.github/workflows/create-release-and-upload-assets.yml @@ -8,7 +8,7 @@ jobs: check-version: runs-on: ubuntu-latest steps: - - uses: actions/checkout@v2 + - uses: actions/checkout@v4 - name: check-version run: | version=$(echo "${{ github.ref }}" | cut -d/ -f3) @@ -17,7 +17,7 @@ jobs: needs: check-version runs-on: ubuntu-latest steps: - - uses: actions/checkout@v2 + - uses: actions/checkout@v4 - name: Create release id: create_release uses: actions/create-release@v1 @@ -32,7 +32,7 @@ jobs: run: | echo "${{ steps.create_release.outputs.upload_url }}" > upload_url.txt - name: Upload upload_url artifact - uses: actions/upload-artifact@v2 + uses: actions/upload-artifact@v4 with: name: upload_url.txt path: upload_url.txt @@ -40,18 +40,16 @@ jobs: needs: create-release runs-on: ubuntu-latest container: - image: debian:buster + image: debian:bookworm steps: - name: Install build dependencies run: | apt update export DEBIAN_FRONTEND=noninteractive apt -y install python3-stdeb dh-python - # https://bugs.launchpad.net/bugs/1916551 - sed -i -e "s/python-all/python3-all/g" /usr/lib/python3/dist-packages/stdeb/util.py - - uses: actions/checkout@v2 + - uses: actions/checkout@v4 - name: Download upload_url artifact - uses: actions/download-artifact@v2 + uses: actions/download-artifact@v4 with: name: upload_url.txt - name: Get upload_url @@ -82,9 +80,9 @@ jobs: - name: Install build dependencies run: | dnf -y install rpm-build python3 python3-setuptools git - - uses: actions/checkout@v2 + - uses: actions/checkout@v4 - name: Download upload_url artifact - uses: actions/download-artifact@v2 + uses: actions/download-artifact@v4 with: name: upload_url.txt - name: Get upload_url @@ -110,9 +108,9 @@ jobs: needs: create-release runs-on: ubuntu-latest steps: - - uses: actions/checkout@v2 + - uses: actions/checkout@v4 - name: Set up python - uses: actions/setup-python@v2 + uses: actions/setup-python@v5 with: python-version: '3.x' - name: Install dependencies diff --git a/.github/workflows/lint.yml b/.github/workflows/lint.yml index 809cf79..40095bd 100644 --- a/.github/workflows/lint.yml +++ b/.github/workflows/lint.yml @@ -8,9 +8,9 @@ jobs: strategy: max-parallel: 4 matrix: - python-version: ['3.9', '3.10', '3.11', '3.12'] + python-version: ['3.x'] steps: - - uses: actions/checkout@v1 + - uses: actions/checkout@v4 - name: Set up Python ${{ matrix.python-version }} uses: actions/setup-python@v1 with: diff --git a/MANIFEST.in b/MANIFEST.in index a7e62f2..2055f9e 100644 --- a/MANIFEST.in +++ b/MANIFEST.in @@ -1,9 +1,9 @@ include README.md -include openvpn-monitor.py +recursive-include openvpn_monitor *.py include openvpn-monitor.conf.example include AUTHORS include COPYING include MANIFEST.in include VERSION.txt include requirements.txt -recursive-include images * +recursive-include openvpn_monitor/static/images * diff --git a/README.md b/README.md index 17f8109..401f09c 100644 --- a/README.md +++ b/README.md @@ -10,26 +10,20 @@ manage remote servers. [![](https://raw.githubusercontent.com/furlongm/openvpn-monitor/gh-pages/screenshots/openvpn-monitor.png)](https://raw.githubusercontent.com/furlongm/openvpn-monitor/gh-pages/screenshots/openvpn-monitor.png) - ## Supported Operating Systems + - Ubuntu 24.04 LTS (noble) - Debian 11 (bullseye) - Rocky/Alma/RHEL 9 -## Source - -The current source code is available on github: - -https://github.com/furlongm/openvpn-monitor - - -## Install Options +## Installation Options + - [source](#source) - [deb/rpm](#deb--rpm) - - [virtualenv + pip + gunicorn](#virtualenv--pip--gunicorn) - [apache](#apache) - [docker](#docker) + - [virtualenv + pip + gunicorn](#virtualenv--pip--gunicorn) - [nginx + uwsgi](#nginx--uwsgi) N.B. all Rocky/Alma/RHEL instructions assume the EPEL repository has been installed: @@ -47,34 +41,80 @@ semanage port -a -t openvpn_port_t -p tcp 5555 setsebool -P httpd_can_network_connect 1 ``` +### Source -### virtualenv + pip + gunicorn +Checkout the code: ```shell -# apt -y install python3-venv # (debian/ubuntu) -# dnf -y install python3 geolite2-city # (rocky/alma/rhel) -mkdir /srv/openvpn-monitor -cd /srv/openvpn-monitor +cd /var/www/html +git clone https://github.com/furlongm/openvpn-monitor +cd openvpn-monitor +yarnpkg --prod --modules-folder openvpn_monitor/static/dist install python3 -m venv .venv . venv/bin/activate -pip install openvpn-monitor gunicorn -gunicorn openvpn_monitor.app -b 0.0.0.0:80 +pip install -r requirements.txt ``` -See [configuration](#configuration) for details on configuring openvpn-monitor. +Run the development server in debug mode: + +```shell +flask --app openvpn_monitor/app run --debug +``` + +### deb/rpm + +### Ubuntu 24.04 (noble) + +```shell +curl -sS https://repo.openbytes.ie/openbytes.gpg > /usr/share/keyrings/openbytes.gpg +echo "deb [signed-by=/usr/share/keyrings/openbytes.gpg] https://repo.openbytes.ie/openvpn-monitor/ubuntu noble main" > /etc/apt/sources.list.d/openvpn-monitor.list +apt update +apt -y install python3-openvpn-monitor +``` + +### Debian 12 (bookworm) + +```shell +curl -sS https://repo.openbytes.ie/openbytes.gpg > /usr/share/keyrings/openbytes.gpg +echo "deb [signed-by=/usr/share/keyrings/openbytes.gpg] https://repo.openbytes.ie/openvpn-monitor/debian bookworm main" > /etc/apt/sources.list.d/openvpn-monitor.list +apt update +apt -y install python3-openvpn-monitor +``` + +### CentOS 9 + +This also applies to Rocky/Alma/RHEL + +```shell +curl -sS https://repo.openbytes.ie/openbytes.gpg > /etc/pki/rpm-gpg/RPM-GPG-KEY-openbytes +cat <> /etc/yum.repos.d/openvpn-monitor.repo +[openbytes] +name=openbytes +baseurl=https://repo.openbytes.ie/openvpn-monitor/el9 +enabled=1 +gpgcheck=1 +gpgkey=file:///etc/pki/rpm-gpg/RPM-GPG-KEY-openbytes +EOF +update-crypto-policies --set DEFAULT:SHA1 +dnf -y install epel-release +dnf makecache +dnf -y install python3-openvpn-monitor +systemctl restart httpd +``` ### apache #### Install dependencies and configure apache +These instructions assume a source checkout to /var/www/html/openvpn-monitor + ##### Debian / Ubuntu ```shell apt -y install git apache2 libapache2-mod-wsgi-py3 python3-geoip2 python3-humanize python3-flask python3-semver yarnpkg a2enmod rewrite wsgi -echo "RewriteRule ^/openvpn-monitor$ /openvpn-monitor/ [R,L]" > /etc/apache2/conf-available/openvpn-monitor.conf -echo "WSGIScriptAlias /openvpn-monitor /var/www/html/openvpn-monitor/openvpn_monitor/app.py" >> /etc/apache2/conf-available/openvpn-monitor.conf +echo "WSGIScriptAlias /openvpn-monitor /var/www/html/openvpn-monitor/openvpn_monitor/app.py" > /etc/apache2/conf-available/openvpn-monitor.conf a2enconf openvpn-monitor service apache2 restart ``` @@ -83,20 +123,10 @@ service apache2 restart ```shell dnf -y install git httpd mod_wsgi python3-geoip2 python3-humanize python3-flask python3-semver geolite2-city yarnpkg -echo "RewriteRule ^/openvpn-monitor$ /openvpn-monitor/ [R,L]" > /etc/httpd/conf.d/openvpn-monitor.conf -echo "WSGIScriptAlias /openvpn-monitor /var/www/html/openvpn-monitor/openvpn_monitor/app.py" >> /etc/httpd/conf.d/openvpn-monitor.conf +echo "WSGIScriptAlias /openvpn-monitor /var/www/html/openvpn-monitor/openvpn_monitor/app.py" > /etc/httpd/conf.d/openvpn-monitor.conf systemctl restart httpd ``` -#### Checkout openvpn-monitor - -```shell -cd /var/www/html -git clone https://github.com/furlongm/openvpn-monitor -cd openvpn-monitor -yarnpkg --prod --modules-folder openvpn_monitor/static/dist install -``` - See [configuration](#configuration) for details on configuring openvpn-monitor. @@ -111,18 +141,29 @@ for details on how to generate a dynamic configuration using only environment variables. -### nginx + uwsgi - -#### Install dependencies +### virtualenv + pip + gunicorn ```shell -apt -y install git gcc nginx uwsgi uwsgi-plugin-python3 python3-dev python3-venv libgeoip-dev yarnpkg # (debian/ubuntu) -dnf -y install git gcc nginx uwsgi uwsgi-plugin-python3 python3-devel geolite2-city yarnpkg # (centos/rhel) +apt -y install python3-venv # (debian/ubuntu) +dnf -y install python3 geolite2-city # (rocky/alma/rhel) +mkdir /srv/openvpn-monitor +cd /srv/openvpn-monitor +python3 -m venv .venv +. venv/bin/activate +pip install openvpn-monitor gunicorn +gunicorn openvpn_monitor.app -b 0.0.0.0:80 ``` -#### Checkout openvpn-monitor +See [configuration](#configuration) for details on configuring openvpn-monitor. + + +### nginx + uwsgi + +#### Install openvpn-monitor ```shell +apt -y install git gcc nginx uwsgi uwsgi-plugin-python3 python3-dev python3-venv libgeoip-dev yarnpkg # (debian/ubuntu) +dnf -y install git gcc nginx uwsgi uwsgi-plugin-python3 python3-devel geolite2-city yarnpkg # (centos/rhel) cd /srv git clone https://github.com/furlongm/openvpn-monitor cd openvpn-monitor @@ -201,11 +242,15 @@ access to the management interface. ### Configure openvpn-monitor Copy the example configuration file `openvpn-monitor.conf.example` to the same -directory as app.py. +directory as app.py or to /etc/openvpn-monitor/openvpn-monitor.conf ```shell cp openvpn-monitor.conf.example openvpn_monitor/openvpn-monitor.conf - +``` +or +```shell +mkdir -p /etc/openvpn-monitor +cp openvpn-monitor.conf.example /etc/openvpn_monitor/openvpn-monitor.conf ``` In this file you can set site name, add a logo, set the default map location @@ -214,16 +259,6 @@ In this file you can set site name, add a logo, set the default map location Once configured, navigate to `http://myipaddress/openvpn-monitor/` -### Development / Debugging - -openvpn-monitor can be run from the command line for development / debugging -purposes: - -```shell -cd /var/www/html/openvpn-monitor -flask --app openvpn_monitor/app run --debug -``` - ## License openvpn-monitor is licensed under the GPLv3, a copy of which can be found in diff --git a/openvpn_monitor/app.py b/openvpn_monitor/app.py index 9f60599..212b347 100644 --- a/openvpn_monitor/app.py +++ b/openvpn_monitor/app.py @@ -18,29 +18,22 @@ import logging import os +import secrets import sys from datetime import datetime -from flask import Flask, request, redirect, make_response, send_file, render_template -from flask_login import ( - LoginManager, - UserMixin, - current_user, - login_required, - login_user, - logout_user, -) +from flask import Flask, request, render_template from flask_wtf import CSRFProtect from humanize import naturalsize -from pprint import pformat, pprint +from pprint import pformat cwd = os.path.dirname(__file__) os.chdir(cwd) sys.path.append(cwd) -from config.loader import ConfigLoader -from vpns.openvpn.data_collector import VPNDataCollector -from vpns.openvpn.disconnector import VPNDisconnector -from location_data.maxmind.geoip import GeoipDBLoader -from util import is_truthy +from config.loader import ConfigLoader # noqa +from vpns.openvpn.data_collector import VPNDataCollector # noqa +from vpns.openvpn.disconnector import VPNDisconnector # noqa +from location_data.maxmind.geoip import GeoipDBLoader # noqa +from util import is_truthy # noqa logging.basicConfig(stream=sys.stderr, format='[%(asctime)s] [%(process)d] [%(levelname)s] %(message)s') logging.getLogger().setLevel(logging.INFO) @@ -52,15 +45,16 @@ def openvpn_monitor_wsgi(): app.url_map.strict_slashes = False csrf = CSRFProtect(app) csrf.init_app(app) - app.secret_key = b'_53oi3uriq9pifpff;aplasd' + secret_key = secrets.token_hex(16) + app.secret_key = secret_key if app.debug: logging.getLogger().setLevel(logging.DEBUG) - config = './openvpn-monitor.conf' - cfg = ConfigLoader(config) - settings = cfg.settings - loaded_vpns = cfg.vpns + config_file = os.getenv('OPENVPNMONITOR_CONFIG_FILE', '/etc/openvpn-monitor/openvpn-monitor.conf') + config = ConfigLoader(config_file) + settings = config.settings + loaded_vpns = config.vpns geoip_db = GeoipDBLoader(settings) @app.template_filter() diff --git a/openvpn_monitor/config/__init__.py b/openvpn_monitor/config/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/openvpn_monitor/config/loader.py b/openvpn_monitor/config/loader.py index 7f0fec1..6695ca9 100644 --- a/openvpn_monitor/config/loader.py +++ b/openvpn_monitor/config/loader.py @@ -18,7 +18,7 @@ import configparser import logging -import sys +import os from collections import OrderedDict from pprint import pformat from util import is_truthy, multiline_info_log @@ -29,33 +29,35 @@ class ConfigLoader(object): def __init__(self, config_file): self.settings = {} self.vpns = OrderedDict() + self.load_config_file(config_file) + logging.debug(f'=== begin section\n{self.settings}\n=== end section') + logging.debug(f'=== begin section\n{self.vpns}\n=== end section') + + def load_config_file(self, config_file): + if os.path.isfile(config_file) and os.access(config_file, os.R_OK): + logging.info(f'Using config file: {config_file}') + else: + logging.error(f'Config file does not exist or is unreadable: {config_file}') + config_file = os.path.join(os.path.abspath(os.getcwd()), 'openvpn-monitor.conf') + logging.warning(f'Trying {config_file} as config file') config = configparser.RawConfigParser() contents = config.read(config_file) - - if not contents and config_file == './openvpn-monitor.conf': - logging.warning(f'Config file does not exist or is unreadable: {config_file}') - if sys.prefix == '/usr': - conf_path = '/etc/' - else: - conf_path = sys.prefix + '/etc/' - config_file = conf_path + 'openvpn-monitor.conf' - contents = config.read(config_file) - if contents: - logging.info(f'Using config file: {config_file}') + self.parse_config(config) else: - logging.warning(f'Config file does not exist or is unreadable: {config_file}') + logging.error(f'No settings found in config file: {config_file}') + logging.warning(f'Falling back to default settings') self.load_default_settings() + def parse_config(self, config): for section in config.sections(): if section.lower() == 'openvpn-monitor': self.parse_global_section(config) else: self.parse_vpn_section(config, section) - multiline_info_log(f'Parsed config:\n{pformat(config._sections)}') + multiline_info_log(f'Using loaded config:\n{pformat(config._sections)}') def load_default_settings(self): - logging.info('Using default settings => localhost:5555') self.settings = {'site': 'Default Site', 'enable_maps': False, 'geoip_data': '/usr/share/GeoIP/GeoLite2-City.mmdb', @@ -65,7 +67,9 @@ def load_default_settings(self): 'port': '5555', 'password': '', 'show_disconnect': False} - logging.debug(f'=== begin section\n{self.settings}\n=== end section') + logging.info('Using default settings:') + multiline_info_log(pformat(self.settings)) + multiline_info_log(pformat(self.vpns)) def parse_global_section(self, config): global_vars = [ @@ -83,7 +87,6 @@ def parse_global_section(self, config): self.settings[var] = config.get('openvpn-monitor', var) except configparser.NoOptionError: pass - logging.debug(f'=== begin section\n{self.settings}\n=== end section') def parse_vpn_section(self, config, section): self.vpns[section] = {} @@ -98,4 +101,3 @@ def parse_vpn_section(self, config, section): logging.warning(f'config: {e} on option {option}: ') vpn[option] = None vpn['show_disconnect'] = is_truthy(vpn.get('show_disconnect', False)) - logging.debug(f'=== begin section\n{vpn}\n=== end section') diff --git a/openvpn_monitor/location_data/__init__.py b/openvpn_monitor/location_data/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/openvpn_monitor/location_data/maxmind/__init__.py b/openvpn_monitor/location_data/maxmind/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/openvpn_monitor/vpns/__init__.py b/openvpn_monitor/vpns/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/openvpn_monitor/vpns/openvpn/__init__.py b/openvpn_monitor/vpns/openvpn/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/setup.py b/setup.py index 6fd7b13..db55645 100755 --- a/setup.py +++ b/setup.py @@ -1,24 +1,40 @@ #!/usr/bin/env python3 +# -*- coding: utf-8 -*- +# +# Copyright 2011 VPAC +# Copyright 2012-2024 Marcus Furlong +# +# This program is free software: you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation, version 3 only. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program. If not, see import os import sys -from setuptools import setup +from setuptools import setup, find_packages -with open('VERSION.txt', 'r') as v: +with open('VERSION.txt', 'r', encoding='utf_8') as v: version = v.readline().strip() -with open('README.md', 'r') as r: +with open('README.md', 'r', encoding='utf_8') as r: long_description = r.read() -for dirpath, dirnames, filenames in os.walk('images/flags'): +with open('requirements.txt', 'r', encoding='utf_8') as rt: + install_requires = rt.read().splitlines() + +data_files = [] + +for dirpath, dirnames, filenames in os.walk('openvpn_monitor/static/images/flags'): data_files = [('share/openvpn-monitor/images/flags', [os.path.join(dirpath, f) for f in filenames])] -with open('requirements.txt') as rt: - install_requires = [] - for line in rt.read().splitlines(): - install_requires.append(line) - if sys.prefix == '/usr': conf_path = '/etc' else: @@ -34,7 +50,7 @@ license='GPLv3', keywords='web openvpn monitor', url='http://openvpn-monitor.openbytes.ie', - py_modules=['openvpn-monitor', ], + packages=find_packages(), install_requires=install_requires, long_description=long_description, long_description_content_type='text/markdown',