diff --git a/README.md b/README.md index 72fa675..17f8109 100644 --- a/README.md +++ b/README.md @@ -3,16 +3,16 @@ ## Summary -openvpn-monitor is a simple python program to generate html that displays the -status of an OpenVPN server, including all current connections. It uses the -OpenVPN management console. It typically runs on the same host as the OpenVPN -server, however it does not necessarily need to. +openvpn-monitor is a flask app that displays the status of OpenVPN servers, +including all current connections. It uses the OpenVPN management console. +It typically runs on the same host as the OpenVPN server, but it can also +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 22.04 LTS (jammy) + - Ubuntu 24.04 LTS (noble) - Debian 11 (bullseye) - Rocky/Alma/RHEL 9 @@ -26,11 +26,11 @@ https://github.com/furlongm/openvpn-monitor ## Install Options + - [deb/rpm](#deb--rpm) - [virtualenv + pip + gunicorn](#virtualenv--pip--gunicorn) - [apache](#apache) - [docker](#docker) - [nginx + uwsgi](#nginx--uwsgi) - - [deb/rpm](#deb--rpm) N.B. all Rocky/Alma/RHEL instructions assume the EPEL repository has been installed: @@ -55,10 +55,10 @@ setsebool -P httpd_can_network_connect 1 # dnf -y install python3 geolite2-city # (rocky/alma/rhel) mkdir /srv/openvpn-monitor cd /srv/openvpn-monitor -python3 -m venv . -. bin/activate +python3 -m venv .venv +. venv/bin/activate pip install openvpn-monitor gunicorn -gunicorn openvpn-monitor -b 0.0.0.0:80 +gunicorn openvpn_monitor.app -b 0.0.0.0:80 ``` See [configuration](#configuration) for details on configuring openvpn-monitor. @@ -71,20 +71,20 @@ See [configuration](#configuration) for details on configuring openvpn-monitor. ##### Debian / Ubuntu ```shell -apt -y install git apache2 libapache2-mod-wsgi-py3 python3-geoip2 python3-humanize python3-flask python3-semver +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.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 -systemctl restart apache2 +service apache2 restart ``` ##### CentOS / RHEL ```shell -dnf -y install git httpd mod_wsgi python3-geoip2 python3-humanize python3-flask python3-semver geolite2-city +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.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 ``` @@ -93,6 +93,8 @@ systemctl restart httpd ```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. @@ -114,8 +116,8 @@ variables. #### Install dependencies ```shell -# apt -y install git gcc nginx uwsgi uwsgi-plugin-python3 python3-dev python3-venv libgeoip-dev # (debian/ubuntu) -# dnf -y install git gcc nginx uwsgi uwsgi-plugin-python3 python3-devel geolite2-city # (centos/rhel) +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) ``` #### Checkout openvpn-monitor @@ -127,6 +129,7 @@ cd openvpn-monitor python3 -m venv .venv . .venv/bin/activate pip install -r requirements.txt +yarnpkg --prod --modules-folder openvpn_monitor/static/dist install ``` #### uWSGI app config @@ -143,7 +146,7 @@ chdir = %(base)/%(project) virtualenv = %(chdir)/.venv module = openvpn-monitor:application manage-script-name = true -mount=/openvpn-monitor=openvpn-monitor.py +mount=/openvpn-monitor=openvpn_monitor/app.py ``` #### Nginx site config @@ -198,10 +201,10 @@ access to the management interface. ### Configure openvpn-monitor Copy the example configuration file `openvpn-monitor.conf.example` to the same -directory as openvpn-monitor.py. +directory as app.py. ```shell -cp openvpn-monitor.conf.example openvpn-monitor.conf +cp openvpn-monitor.conf.example openvpn_monitor/openvpn-monitor.conf ``` @@ -211,24 +214,16 @@ In this file you can set site name, add a logo, set the default map location Once configured, navigate to `http://myipaddress/openvpn-monitor/` -### Debugging - -openvpn-monitor can be run from the command line in order to test if the html -generates correctly: +### Development / Debugging -```shell -cd /var/www/html/openvpn-monitor -python3 openvpn-monitor.py -``` - -Further debugging can be enabled by specifying the `--debug` flag: +openvpn-monitor can be run from the command line for development / debugging +purposes: ```shell cd /var/www/html/openvpn-monitor -python3 openvpn-monitor.py -d +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/VERSION.txt b/VERSION.txt index 781dcb0..227cea2 100644 --- a/VERSION.txt +++ b/VERSION.txt @@ -1 +1 @@ -1.1.3 +2.0.0 diff --git a/openvpn-monitor.py b/openvpn-monitor.py deleted file mode 100755 index 2c23806..0000000 --- a/openvpn-monitor.py +++ /dev/null @@ -1,883 +0,0 @@ -#!/usr/bin/env python3 -# -*- coding: utf-8 -*- - -# Copyright 2011 VPAC -# Copyright 2012-2023 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 argparse -import configparser -import logging -import os -import re -import semver -import socket -import string -import sys -from collections import OrderedDict, deque -from datetime import datetime -from humanize import naturalsize -from ipaddress import ip_address -from geoip2 import database -from geoip2.errors import AddressNotFoundError -from pprint import pformat - -logging.basicConfig(stream=sys.stderr, format='[%(asctime)s] [%(process)d] [%(levelname)s] %(message)s') -logging.getLogger().setLevel(logging.INFO) - - -def output(s): - global wsgi, wsgi_output - if not wsgi: - print(s) - else: - wsgi_output += s - - -def get_date(date_string, uts=False): - if not uts: - return datetime.strptime(date_string, '%a %b %d %H:%M:%S %Y') - else: - return datetime.fromtimestamp(float(date_string)) - - -def is_truthy(s): - return s in ['True', 'true', 'Yes', 'yes', True] - - -class ConfigLoader(object): - - def __init__(self, config_file): - self.settings = {} - self.vpns = OrderedDict() - 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}') - else: - logging.warning(f'Config file does not exist or is unreadable: {config_file}') - self.load_default_settings() - - for section in config.sections(): - if section.lower() == 'openvpn-monitor': - self.parse_global_section(config) - else: - self.parse_vpn_section(config, section) - - def load_default_settings(self): - logging.info('Using default settings => localhost:5555') - self.settings = {'site': 'Default Site', - 'maps': 'True', - 'geoip_data': '/usr/share/GeoIP/GeoLite2-City.mmdb', - 'datetime_format': '%d/%m/%Y %H:%M:%S'} - self.vpns['Default VPN'] = {'name': 'default', - 'host': 'localhost', - 'port': '5555', - 'password': '', - 'show_disconnect': False} - - def parse_global_section(self, config): - global_vars = ['site', 'logo', 'latitude', 'longitude', 'maps', 'maps_height', 'geoip_data', 'datetime_format'] - for var in global_vars: - try: - self.settings[var] = config.get('openvpn-monitor', var) - except configparser.NoSectionError: - # backwards compat - try: - self.settings[var] = config.get('OpenVPN-Monitor', var) - except configparser.NoOptionError: - pass - 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] = {} - vpn = self.vpns[section] - options = config.options(section) - for option in options: - try: - vpn[option] = config.get(section, option) - if vpn[option] == -1: - logging.warning(f'CONFIG: skipping {option}') - except configparser.Error as e: - 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') - - -class OpenvpnMgmtInterface(object): - - def __init__(self, cfg, **kwargs): - self.vpns = cfg.vpns - - if kwargs.get('vpn_id'): - vpn = self.vpns[kwargs['vpn_id']] - disconnection_allowed = vpn['show_disconnect'] - if disconnection_allowed: - self._socket_connect(vpn) - if vpn['socket_connected']: - full_version = self.send_command('version\n') - release = self.parse_version(full_version) - version = semver.parse_version_info(release.split(' ')[1]) - command = False - client_id = None - if kwargs.get('client_id'): - client_id = int(kwargs.get('client_id')) - if version.major == 2 and version.minor >= 4 and client_id: - command = f'client-kill {client_id}\n' - else: - ip = ip_address(kwargs['ip']) - port = int(kwargs['port']) - if ip and port: - command = f'kill {ip}:{port}\n' - if command: - self.send_command(command) - self._socket_disconnect() - - geoip_data = cfg.settings.get('geoip_data') - maps = is_truthy(cfg.settings.get('maps', False)) - self.gi = None - if maps and geoip_data: - self.gi = database.Reader(geoip_data) - - for _, vpn in list(self.vpns.items()): - self._socket_connect(vpn) - if vpn['socket_connected']: - self.collect_data(vpn) - self._socket_disconnect() - - def collect_data(self, vpn): - full_version = self.send_command('version\n') - vpn['release'] = self.parse_version(full_version) - vpn['version'] = semver.parse_version_info(vpn['release'].split(' ')[1]) - state = self.send_command('state\n') - vpn['state'] = self.parse_state(state) - stats = self.send_command('load-stats\n') - vpn['stats'] = self.parse_stats(stats) - status = self.send_command('status 3\n') - vpn['sessions'] = self.parse_status(status, vpn['version']) - - def _socket_send(self, command): - if sys.version_info[0] == 2: - self.s.send(command) - else: - self.s.send(bytes(command, 'utf-8')) - - def _socket_recv(self, length): - if sys.version_info[0] == 2: - return self.s.recv(length) - else: - return self.s.recv(length).decode('utf-8') - - def _socket_connect(self, vpn): - timeout = 3 - self.s = False - try: - if vpn.get('socket'): - self.s = socket.socket(socket.AF_UNIX, socket.SOCK_STREAM) - self.s.connect(vpn['socket']) - else: - host = vpn['host'] - port = int(vpn['port']) - self.s = socket.create_connection((host, port), timeout) - if self.s: - password = vpn.get('password') - if password: - self.wait_for_data(password=password) - vpn['socket_connected'] = True - except socket.timeout as e: - vpn['error'] = e - logging.warning(f'socket timeout: {e}') - vpn['socket_connected'] = False - if self.s: - self.s.shutdown(socket.SHUT_RDWR) - self.s.close() - except socket.error as e: - vpn['error'] = e.strerror - logging.warning(f'socket error: {e}') - vpn['socket_connected'] = False - except Exception as e: - vpn['error'] = e - logging.warning(f'unexpected error: {e}') - vpn['socket_connected'] = False - - def _socket_disconnect(self): - self._socket_send('quit\n') - self.s.shutdown(socket.SHUT_RDWR) - self.s.close() - - def send_command(self, command): - logging.info(f'Sending openvpn management command: `{command.rstrip()}`') - self._socket_send(command) - if command.startswith('kill') or command.startswith('client-kill'): - return - return self.wait_for_data(command=command) - - def wait_for_data(self, password=None, command=None): - data = '' - while 1: - socket_data = self._socket_recv(1024) - socket_data = re.sub('>INFO(.)*\r\n', '', socket_data) - data += socket_data - if data.endswith('ENTER PASSWORD:'): - if password: - self._socket_send(f'{password}\n') - else: - logging.warning('password requested but no password supplied by configuration') - if data.endswith('SUCCESS: password is correct\r\n'): - break - if command == 'load-stats\n' and data != '': - break - elif data.endswith('\nEND\r\n'): - break - logging.debug(f'=== begin raw data\n{data}\n=== end raw data') - return data - - @staticmethod - def parse_state(data): - state = {} - for line in data.splitlines(): - parts = line.split(',') - logging.debug(f'=== begin split line\n{parts}\n=== end split line') - if parts[0].startswith('>INFO') or \ - parts[0].startswith('END') or \ - parts[0].startswith('>CLIENT'): - continue - else: - state['up_since'] = get_date(date_string=parts[0], uts=True) - state['connected'] = parts[1] - state['success'] = parts[2] - if parts[3]: - state['local_ip'] = ip_address(parts[3]) - else: - state['local_ip'] = '' - if parts[4]: - state['remote_ip'] = ip_address(parts[4]) - state['mode'] = 'Client' - else: - state['remote_ip'] = '' - state['mode'] = 'Server' - return state - - @staticmethod - def parse_stats(data): - stats = {} - line = re.sub('SUCCESS: ', '', data) - parts = line.split(',') - logging.debug(f'=== begin split line\n{parts}\n=== end split line') - stats['nclients'] = int(re.sub('nclients=', '', parts[0])) - stats['bytesin'] = int(re.sub('bytesin=', '', parts[1])) - stats['bytesout'] = int(re.sub('bytesout=', '', parts[2]).replace('\r\n', '')) - return stats - - def parse_status(self, data, version): - gi = self.gi - client_section = False - routes_section = False - sessions = {} - client_session = {} - - for line in data.splitlines(): - parts = deque(line.split('\t')) - logging.debug(f'=== begin split line\n{parts}\n=== end split line') - - if parts[0].startswith('END'): - break - if parts[0].startswith('TITLE') or \ - parts[0].startswith('GLOBAL') or \ - parts[0].startswith('TIME'): - continue - if parts[0] == 'HEADER': - if parts[1] == 'CLIENT_LIST': - client_section = True - routes_section = False - if parts[1] == 'ROUTING_TABLE': - client_section = False - routes_section = True - continue - - if parts[0].startswith('TUN') or \ - parts[0].startswith('TCP') or \ - parts[0].startswith('Auth'): - parts = parts[0].split(',') - if parts[0] == 'TUN/TAP read bytes': - client_session['tuntap_read'] = int(parts[1]) - continue - if parts[0] == 'TUN/TAP write bytes': - client_session['tuntap_write'] = int(parts[1]) - continue - if parts[0] == 'TCP/UDP read bytes': - client_session['tcpudp_read'] = int(parts[1]) - continue - if parts[0] == 'TCP/UDP write bytes': - client_session['tcpudp_write'] = int(parts[1]) - continue - if parts[0] == 'Auth read bytes': - client_session['auth_read'] = int(parts[1]) - sessions['Client'] = client_session - continue - - if client_section: - session = {} - parts.popleft() - common_name = parts.popleft() - remote_str = parts.popleft() - if remote_str.count(':') == 1: - remote, port = remote_str.split(':') - elif '(' in remote_str: - remote, port = remote_str.split('(') - port = port[:-1] - else: - remote = remote_str - port = None - remote_ip = ip_address(remote) - session['remote_ip'] = remote_ip - if port: - session['port'] = int(port) - else: - session['port'] = '' - if session['remote_ip'].is_private: - session['location'] = 'RFC1918' - elif session['remote_ip'].is_loopback: - session['location'] = 'loopback' - else: - try: - if gi: - gir = gi.city(str(session['remote_ip'])) - session['location'] = gir.country.iso_code - session['region'] = gir.subdivisions.most_specific.iso_code - session['city'] = gir.city.name - session['country'] = gir.country.name - session['longitude'] = gir.location.longitude - session['latitude'] = gir.location.latitude - except AddressNotFoundError as e: - logging.warning(e) - except SystemError: - pass - local_ipv4 = parts.popleft() - if local_ipv4: - session['local_ip'] = ip_address(local_ipv4) - else: - session['local_ip'] = '' - if version.major >= 2 and version.minor >= 4: - local_ipv6 = parts.popleft() - if local_ipv6: - session['local_ip'] = ip_address(local_ipv6) - session['bytes_recv'] = int(parts.popleft()) - session['bytes_sent'] = int(parts.popleft()) - parts.popleft() - session['connected_since'] = get_date(parts.popleft(), uts=True) - username = parts.popleft() - if username != 'UNDEF': - session['username'] = username - else: - session['username'] = common_name - if version.major == 2 and version.minor >= 4: - session['client_id'] = parts.popleft() - session['peer_id'] = parts.popleft() - sessions[str(session['local_ip'])] = session - - if routes_section: - local_ip = parts[1] - remote_ip = parts[3] - last_seen = get_date(parts[5], uts=True) - if sessions.get(local_ip): - sessions[local_ip]['last_seen'] = last_seen - elif self.is_mac_address(local_ip): - matching_local_ips = [sessions[s]['local_ip'] - for s in sessions if remote_ip == - self.get_remote_address(sessions[s]['remote_ip'], sessions[s]['port'])] - if len(matching_local_ips) == 1: - local_ip = f'{matching_local_ips[0]}' - if sessions[local_ip].get('last_seen'): - prev_last_seen = sessions[local_ip]['last_seen'] - if prev_last_seen < last_seen: - sessions[local_ip]['last_seen'] = last_seen - else: - sessions[local_ip]['last_seen'] = last_seen - - if sessions: - pretty_sessions = pformat(sessions) - logging.debug(f'=== begin sessions\n{pretty_sessions}\n=== end sessions') - else: - logging.debug('no sessions') - - return sessions - - @staticmethod - def parse_version(data): - for line in data.splitlines(): - if line.startswith('OpenVPN'): - return line.replace('OpenVPN Version: ', '') - - @staticmethod - def is_mac_address(s): - return len(s) == 17 and \ - len(s.split(':')) == 6 and \ - all(c in string.hexdigits for c in s.replace(':', '')) - - @staticmethod - def get_remote_address(ip, port): - if port: - return f'{ip}:{port}' - else: - return f'{ip}' - - -class OpenvpnHtmlPrinter(object): - - def __init__(self, cfg, monitor): - self.init_vars(cfg.settings, monitor) - self.print_html_header() - for key, vpn in self.vpns: - if vpn['socket_connected']: - self.print_vpn(key, vpn) - else: - self.print_unavailable_vpn(vpn) - if self.maps: - self.print_maps_html() - self.print_html_footer() - - def init_vars(self, settings, monitor): - self.vpns = list(monitor.vpns.items()) - self.site = settings.get('site', 'Example') - self.logo = settings.get('logo') - self.maps = is_truthy(settings.get('maps', False)) - if self.maps: - self.maps_height = settings.get('maps_height', 500) - self.latitude = settings.get('latitude', 40.72) - self.longitude = settings.get('longitude', -74) - self.datetime_format = settings.get('datetime_format') - - def print_html_header(self): - - global wsgi - if not wsgi: - output('Content-Type: text/html\n') - output('') - output('') - output('') - output('') - output('') - output(f'{self.site} OpenVPN Status Monitor') - output('') - - # css - output('') # noqa - output('') # noqa - output('') # noqa - if self.maps: - output('') # noqa - output('') # noqa - output('') - - # js - output('') # noqa - output('') # noqa - output('') # noqa - output('') # noqa - output('') # noqa - output('') # noqa - output('') - if self.maps: - output('') # noqa - output('') # noqa - output('') # noqa - - output('') - - output('') - output('
') - - @staticmethod - def print_session_table_headers(vpn_mode, show_disconnect): - server_headers = ['Username / Hostname', 'VPN IP', - 'Remote IP', 'Location', 'Bytes In', - 'Bytes Out', 'Connected Since', 'Last Ping', 'Time Online'] - if show_disconnect: - server_headers.append('Action') - - client_headers = ['Tun-Tap-Read', 'Tun-Tap-Write', 'TCP-UDP-Read', - 'TCP-UDP-Write', 'Auth-Read'] - - if vpn_mode == 'Client': - headers = client_headers - elif vpn_mode == 'Server': - headers = server_headers - - output('
') - output('') - output('') - for header in headers: - if header == 'Time Online': - output(f'') - else: - output(f'') - output('') - - @staticmethod - def print_session_table_footer(): - output('
{header}{header}
') - - @staticmethod - def print_unavailable_vpn(vpn): - anchor = vpn['name'].lower().replace(' ', '_') - output(f'
') - output('
') - output(f"

{vpn['name']}

") - output('
') - output('Could not connect to ') - if vpn.get('host') and vpn.get('port'): - output(f"{vpn['host']}:{vpn['port']} ({vpn['error']})
") - elif vpn.get('socket'): - output(f"{vpn['socket']} ({vpn['error']})
") - else: - logging.warning(f'failed to get socket or network info: {vpn}') - output('network or unix socket') - - def print_vpn(self, vpn_id, vpn): - - if vpn['state']['success'] == 'SUCCESS': - pingable = 'Yes' - else: - pingable = 'No' - - connection = vpn['state']['connected'] - nclients = vpn['stats']['nclients'] - bytesin = vpn['stats']['bytesin'] - bytesout = vpn['stats']['bytesout'] - vpn_mode = vpn['state']['mode'] - vpn_sessions = vpn['sessions'] - local_ip = vpn['state']['local_ip'] - remote_ip = vpn['state']['remote_ip'] - up_since = vpn['state']['up_since'] - show_disconnect = vpn['show_disconnect'] - - anchor = vpn['name'].lower().replace(' ', '_') - output(f'
') - output(f"

{vpn['name']}

") - output('
') - output('
') - output('') - output('') - output('') - output('') - if vpn_mode == 'Client': - output('') - output('') - output(f'') - output(f'') - output(f'') - output(f'') - output(f'') - output(f'') - output(f'') - output(f'') - if vpn_mode == 'Client': - output(f'') - output('
VPN ModeStatusPingableClientsTotal Bytes InTotal Bytes OutUp SinceLocal IP AddressRemote IP Address
{vpn_mode}{connection}{pingable}{nclients}{bytesin} ({naturalsize(bytesin, binary=True)}){bytesout} ({naturalsize(bytesout, binary=True)}){up_since.strftime(self.datetime_format)}{local_ip}{remote_ip}
') - - if vpn_mode == 'Client' or nclients > 0: - self.print_session_table_headers(vpn_mode, show_disconnect) - self.print_session_table(vpn_id, vpn_mode, vpn_sessions, show_disconnect) - self.print_session_table_footer() - - output('
') - output('') - output('
') - - @staticmethod - def print_client_session(session): - tuntap_r = session['tuntap_read'] - tuntap_w = session['tuntap_write'] - tcpudp_r = session['tcpudp_read'] - tcpudp_w = session['tcpudp_write'] - auth_r = session['auth_read'] - output(f'{tuntap_r} ({naturalsize(tuntap_r, binary=True)})') - output(f'{tuntap_w} ({naturalsize(tuntap_w, binary=True)})') - output(f'{tcpudp_r} ({naturalsize(tcpudp_w, binary=True)})') - output(f'{tcpudp_w} ({naturalsize(tcpudp_w, binary=True)})') - output(f'{auth_r} ({naturalsize(auth_r, binary=True)})') - - def print_server_session(self, vpn_id, session, show_disconnect): - total_time = str(datetime.now() - session['connected_since'])[:-7] - bytes_recv = session['bytes_recv'] - bytes_sent = session['bytes_sent'] - output(f"{session['username']}") - output(f"{session['local_ip']}") - output(f"{session['remote_ip']}") - - if session.get('location'): - flag = f'images/flags/{session["location"].lower()}.png' - if session.get('country'): - country = session['country'] - full_location = country - if session.get('region'): - region = session['region'] - full_location = f'{region}, {full_location}' - if session.get('city'): - city = session['city'] - full_location = f'{city}, {full_location}' - if session['location'] in ['RFC1918', 'loopback']: - if session['location'] == 'RFC1918': - city = 'RFC1918' - elif session['location'] == 'loopback': - city = 'loopback' - country = 'Internet' - full_location = f'{city}, {country}' - flag = 'images/flags/rfc.png' - output(f'{full_location} ') - output(f'{full_location}') - else: - output('Unknown') - - output(f'{bytes_recv} ({naturalsize(bytes_recv, binary=True)})') - output(f'{bytes_sent} ({naturalsize(bytes_sent, binary=True)})') - output(f"{session['connected_since'].strftime(self.datetime_format)}") - if session.get('last_seen'): - output(f"{session['last_seen'].strftime(self.datetime_format)}") - else: - output('Unknown') - output(f'{total_time}') - if show_disconnect: - output('
') - output(f'') - if session.get('port'): - output(f"") - output(f"") - if session.get('client_id'): - output(f"") - output('
') - - def print_session_table(self, vpn_id, vpn_mode, sessions, show_disconnect): - for _, session in list(sessions.items()): - if vpn_mode == 'Client': - output('') - self.print_client_session(session) - output('') - elif vpn_mode == 'Server' and session['local_ip']: - output('') - self.print_server_session(vpn_id, session, show_disconnect) - output('') - - def print_maps_html(self): - output('
') - output('

Map View

') - output(f'
') - output('') - output('
') - - def print_html_footer(self): - output('
') - output('Page automatically reloads every 5 minutes. ') - output(f'Last update: {datetime.now().strftime(self.datetime_format)}
') - output('') - - -def main(**kwargs): - cfg = ConfigLoader(args.config) - monitor = OpenvpnMgmtInterface(cfg, **kwargs) - OpenvpnHtmlPrinter(cfg, monitor) - pretty_vpns = pformat((dict(monitor.vpns))) - logging.debug(f'=== begin vpns\n{pretty_vpns}\n=== end vpns') - - -def get_args(): - parser = argparse.ArgumentParser( - description='Display a html page with openvpn status and connections') - parser.add_argument('-d', '--debug', action='store_true', - required=False, default=False, - help='Run in debug mode') - parser.add_argument('-c', '--config', type=str, - required=False, default='./openvpn-monitor.conf', - help='Path to config file openvpn-monitor.conf') - return parser.parse_args() - - -def monitor_wsgi(): - app = Flask(__name__) - app.url_map.strict_slashes = False - if app.debug: - args.debug = True - if args.debug: - logging.getLogger().setLevel(logging.DEBUG) - - def render(**kwargs): - global wsgi_output - wsgi_output = '' - main(**kwargs) - return make_response(wsgi_output) - - @app.before_request - def strip_slash(): - logging.debug(pformat(request.environ)) - rp = request.path - if rp != '/' and rp.endswith('/'): - return redirect(rp.rstrip('/')) - - @app.route('/', methods=['GET', 'POST']) - def handle_root(): - logging.debug(pformat(request.environ)) - if request.method == 'GET': - return render() - elif request.method == 'POST': - vpn_id = request.form.get('vpn_id') - ip = request.form.get('ip') - port = request.form.get('port') - client_id = request.form.get('client_id') - return render(vpn_id=vpn_id, ip=ip, port=port, client_id=client_id) - - @app.route('/images/', methods=['GET']) - def get_flag(filename): - logging.debug(pformat(request.environ)) - return send_file(f'images/{filename}') - - @app.route('/images/flags/', methods=['GET']) - def get_image(filename): - logging.debug(pformat(request.environ)) - return send_file(f'images/flags/{filename}') - - return app - - -if __name__ == '__main__': - args = get_args() - if args.debug: - logging.getLogger().setLevel(logging.DEBUG) - wsgi = False - main() -elif __name__.startswith('_mod_wsgi_') or \ - __name__ == 'openvpn-monitor' or \ - __name__ == 'uwsgi_file_openvpn-monitor': - if __file__ != 'openvpn-monitor.py': - os.chdir(os.path.dirname(__file__)) - sys.path.append(os.path.dirname(__file__)) - from flask import Flask, request, redirect, make_response, send_file - - class args(object): - debug = False - config = './openvpn-monitor.conf' - - wsgi = True - wsgi_output = '' - application = monitor_wsgi() diff --git a/openvpn_monitor/__init__.py b/openvpn_monitor/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/openvpn_monitor/app.py b/openvpn_monitor/app.py new file mode 100644 index 0000000..b1b3af8 --- /dev/null +++ b/openvpn_monitor/app.py @@ -0,0 +1,200 @@ +#!/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 logging +import os +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_wtf import CSRFProtect +from humanize import naturalsize +from pprint import pformat, pprint + +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 + +logging.basicConfig(stream=sys.stderr, format='[%(asctime)s] [%(process)d] [%(levelname)s] %(message)s') +logging.getLogger().setLevel(logging.INFO) + + +def openvpn_monitor_wsgi(): + + app = Flask(__name__) + app.url_map.strict_slashes = False + csrf = CSRFProtect(app) + csrf.init_app(app) + app.secret_key = b'_53oi3uriq9pifpff;aplasd' + + if app.debug: + logging.getLogger().setLevel(logging.DEBUG) + + config = './openvpn-monitor.conf' + cfg = ConfigLoader(config) + pprint(cfg.__dict__) + geoip_db = GeoipDB(cfg) + + @app.template_filter() + def get_formatted_time_now(datetime_format): + return datetime.now().strftime(datetime_format) + + @app.template_filter() + def get_vpn_anchor(vpn): + if vpn.get('name'): + return vpn.get('name').lower().replace(' ', '_') + + @app.template_filter() + def get_naturalsize(nbytes): + if nbytes: + return naturalsize(nbytes, binary=True) + + @app.template_filter() + def get_full_location(session): + full_location = '' + location = session.get('location') + if location: + if location in ['RFC1918', 'loopback']: + city = location + country = 'Internet' + full_location = f'{city}, {country}' + else: + if session.get('country'): + country = session.get('country') + full_location = country + if session.get('region'): + region = session.get('region') + full_location = f'{region}, {full_location}' + if session.get('city'): + city = session.get('city') + full_location = f'{city}, {full_location}' + return full_location + + @app.template_filter() + def get_flag(session): + flag = '' + location = session.get('location') + if location: + if location in ['RFC1918', 'loopback']: + flag = 'images/flags/rfc.png' + else: + flag = f'images/flags/{location.lower()}.png' + return flag + + @app.template_filter() + def get_vpn_error(vpn): + name = vpn.get('name') + host = vpn.get('host') + port = vpn.get('port') + socket = vpn.get('socket') + error = vpn.get('error') + if host and port: + return f'{host}:{port} ({error})' + elif socket: + return f'{socket} ({error})' + else: + logging.error(f'unknown error with vpn {name} - {error}') + return f'network or unix socket ({error})' + + @app.template_filter() + def get_session_headers(vpn_mode): + server_headers = [ + 'Username / Hostname', + 'VPN IP', + 'Remote IP', + 'Location', + 'Bytes In', + 'Bytes Out', + 'Connected Since', + 'Last Ping', + 'Time Online' + ] + client_headers = [ + 'Tun-Tap-Read', + 'Tun-Tap-Write', + 'TCP-UDP-Read', + 'TCP-UDP-Write', + 'Auth-Read' + ] + if vpn_mode == 'Client': + headers = client_headers + elif vpn_mode == 'Server': + headers = server_headers + return headers + + @app.template_filter() + def get_total_connected_time(connected_since): + return str(datetime.now() - connected_since)[:-7] + + @app.context_processor + def inject_settings(): + site = settings.get('site', 'Example') + logo = settings.get('logo') + enable_maps = is_truthy(settings.get('enable_maps', False)) + maps_height = settings.get('maps_height', 500) + latitude = settings.get('latitude', 40.72) + longitude = settings.get('longitude', -74) + datetime_format = settings.get('datetime_format', '%d/%m/%Y %H:%M:%S') + return dict( + site=site, + logo=logo, + enable_maps=enable_maps, + maps_height=maps_height, + latitude=latitude, + longitude=longitude, + datetime_format=datetime_format, + ) + + @app.route('/', methods=['GET', 'POST']) + def handle_root(): + vpn_data = VPNDataCollector(loaded_vpns, geoip_db.gi) + vpns = vpn_data.vpns.items() + pretty_vpns = pformat((dict(vpns))) + logging.debug(f'=== begin vpns\n{pretty_vpns}\n=== end vpns') + if request.method == 'GET': + return render_template('base.html', vpns=vpns) + elif request.method == 'POST': + vpn_id = request.form.get('vpn_id') + ip = request.form.get('ip') + port = request.form.get('port') + client_id = request.form.get('client_id') + VPNDisconnector( + vpns=vpns, + vpn_id=vpn_id, + ip=ip, + port=port, + client_id=client_id, + ) + return render_template('base.html', vpns=vpns) + + return app + + +application = openvpn_monitor_wsgi() diff --git a/openvpn_monitor/config/loader.py b/openvpn_monitor/config/loader.py new file mode 100644 index 0000000..7f0fec1 --- /dev/null +++ b/openvpn_monitor/config/loader.py @@ -0,0 +1,101 @@ +#!/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 configparser +import logging +import sys +from collections import OrderedDict +from pprint import pformat +from util import is_truthy, multiline_info_log + + +class ConfigLoader(object): + + def __init__(self, config_file): + self.settings = {} + self.vpns = OrderedDict() + 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}') + else: + logging.warning(f'Config file does not exist or is unreadable: {config_file}') + self.load_default_settings() + + 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)}') + + 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', + 'datetime_format': '%d/%m/%Y %H:%M:%S'} + self.vpns['Default VPN'] = {'name': 'default', + 'host': 'localhost', + 'port': '5555', + 'password': '', + 'show_disconnect': False} + logging.debug(f'=== begin section\n{self.settings}\n=== end section') + + def parse_global_section(self, config): + global_vars = [ + 'site', + 'logo', + 'latitude', + 'longitude', + 'enable_maps', + 'maps_height', + 'geoip_data', + 'datetime_format' + ] + for var in global_vars: + try: + 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] = {} + vpn = self.vpns[section] + options = config.options(section) + for option in options: + try: + vpn[option] = config.get(section, option) + if vpn[option] == -1: + logging.warning(f'config: skipping {option}') + except configparser.Error as e: + 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/maxmind/geoip.py b/openvpn_monitor/location_data/maxmind/geoip.py new file mode 100644 index 0000000..a2843df --- /dev/null +++ b/openvpn_monitor/location_data/maxmind/geoip.py @@ -0,0 +1,30 @@ +#!/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 + +from geoip2 import database +from util import is_truthy + + +class GeoipDBLoader(object): + + def __init__(self, settings): + geoip_data = settings.get('geoip_data') + enable_maps = is_truthy(settings.get('enable_maps', False)) + self.gi = False + if enable_maps and geoip_data: + self.gi = database.Reader(geoip_data) diff --git a/images/flags/ad.png b/openvpn_monitor/static/images/flags/ad.png similarity index 100% rename from images/flags/ad.png rename to openvpn_monitor/static/images/flags/ad.png diff --git a/images/flags/ad.woa.png b/openvpn_monitor/static/images/flags/ad.woa.png similarity index 100% rename from images/flags/ad.woa.png rename to openvpn_monitor/static/images/flags/ad.woa.png diff --git a/images/flags/ae.png b/openvpn_monitor/static/images/flags/ae.png similarity index 100% rename from images/flags/ae.png rename to openvpn_monitor/static/images/flags/ae.png diff --git a/images/flags/af.png b/openvpn_monitor/static/images/flags/af.png similarity index 100% rename from images/flags/af.png rename to openvpn_monitor/static/images/flags/af.png diff --git a/images/flags/ag.png b/openvpn_monitor/static/images/flags/ag.png similarity index 100% rename from images/flags/ag.png rename to openvpn_monitor/static/images/flags/ag.png diff --git a/images/flags/ai.png b/openvpn_monitor/static/images/flags/ai.png similarity index 100% rename from images/flags/ai.png rename to openvpn_monitor/static/images/flags/ai.png diff --git a/images/flags/al.png b/openvpn_monitor/static/images/flags/al.png similarity index 100% rename from images/flags/al.png rename to openvpn_monitor/static/images/flags/al.png diff --git a/images/flags/am.png b/openvpn_monitor/static/images/flags/am.png similarity index 100% rename from images/flags/am.png rename to openvpn_monitor/static/images/flags/am.png diff --git a/images/flags/an.png b/openvpn_monitor/static/images/flags/an.png similarity index 100% rename from images/flags/an.png rename to openvpn_monitor/static/images/flags/an.png diff --git a/images/flags/ao.png b/openvpn_monitor/static/images/flags/ao.png similarity index 100% rename from images/flags/ao.png rename to openvpn_monitor/static/images/flags/ao.png diff --git a/images/flags/aq.png b/openvpn_monitor/static/images/flags/aq.png similarity index 100% rename from images/flags/aq.png rename to openvpn_monitor/static/images/flags/aq.png diff --git a/images/flags/ar.png b/openvpn_monitor/static/images/flags/ar.png similarity index 100% rename from images/flags/ar.png rename to openvpn_monitor/static/images/flags/ar.png diff --git a/images/flags/ar.woa.png b/openvpn_monitor/static/images/flags/ar.woa.png similarity index 100% rename from images/flags/ar.woa.png rename to openvpn_monitor/static/images/flags/ar.woa.png diff --git a/images/flags/as.png b/openvpn_monitor/static/images/flags/as.png similarity index 100% rename from images/flags/as.png rename to openvpn_monitor/static/images/flags/as.png diff --git a/images/flags/at.png b/openvpn_monitor/static/images/flags/at.png similarity index 100% rename from images/flags/at.png rename to openvpn_monitor/static/images/flags/at.png diff --git a/images/flags/au.png b/openvpn_monitor/static/images/flags/au.png similarity index 100% rename from images/flags/au.png rename to openvpn_monitor/static/images/flags/au.png diff --git a/images/flags/aw.png b/openvpn_monitor/static/images/flags/aw.png similarity index 100% rename from images/flags/aw.png rename to openvpn_monitor/static/images/flags/aw.png diff --git a/images/flags/ax.png b/openvpn_monitor/static/images/flags/ax.png similarity index 100% rename from images/flags/ax.png rename to openvpn_monitor/static/images/flags/ax.png diff --git a/images/flags/az.png b/openvpn_monitor/static/images/flags/az.png similarity index 100% rename from images/flags/az.png rename to openvpn_monitor/static/images/flags/az.png diff --git a/images/flags/ba.png b/openvpn_monitor/static/images/flags/ba.png similarity index 100% rename from images/flags/ba.png rename to openvpn_monitor/static/images/flags/ba.png diff --git a/images/flags/bb.png b/openvpn_monitor/static/images/flags/bb.png similarity index 100% rename from images/flags/bb.png rename to openvpn_monitor/static/images/flags/bb.png diff --git a/images/flags/bd.png b/openvpn_monitor/static/images/flags/bd.png similarity index 100% rename from images/flags/bd.png rename to openvpn_monitor/static/images/flags/bd.png diff --git a/images/flags/be.png b/openvpn_monitor/static/images/flags/be.png similarity index 100% rename from images/flags/be.png rename to openvpn_monitor/static/images/flags/be.png diff --git a/images/flags/bf.png b/openvpn_monitor/static/images/flags/bf.png similarity index 100% rename from images/flags/bf.png rename to openvpn_monitor/static/images/flags/bf.png diff --git a/images/flags/bg.png b/openvpn_monitor/static/images/flags/bg.png similarity index 100% rename from images/flags/bg.png rename to openvpn_monitor/static/images/flags/bg.png diff --git a/images/flags/bh.png b/openvpn_monitor/static/images/flags/bh.png similarity index 100% rename from images/flags/bh.png rename to openvpn_monitor/static/images/flags/bh.png diff --git a/images/flags/bi.png b/openvpn_monitor/static/images/flags/bi.png similarity index 100% rename from images/flags/bi.png rename to openvpn_monitor/static/images/flags/bi.png diff --git a/images/flags/bj.png b/openvpn_monitor/static/images/flags/bj.png similarity index 100% rename from images/flags/bj.png rename to openvpn_monitor/static/images/flags/bj.png diff --git a/images/flags/bl.loc.png b/openvpn_monitor/static/images/flags/bl.loc.png similarity index 100% rename from images/flags/bl.loc.png rename to openvpn_monitor/static/images/flags/bl.loc.png diff --git a/images/flags/bl.png b/openvpn_monitor/static/images/flags/bl.png similarity index 100% rename from images/flags/bl.png rename to openvpn_monitor/static/images/flags/bl.png diff --git a/images/flags/bm.png b/openvpn_monitor/static/images/flags/bm.png similarity index 100% rename from images/flags/bm.png rename to openvpn_monitor/static/images/flags/bm.png diff --git a/images/flags/bn.png b/openvpn_monitor/static/images/flags/bn.png similarity index 100% rename from images/flags/bn.png rename to openvpn_monitor/static/images/flags/bn.png diff --git a/images/flags/bo.png b/openvpn_monitor/static/images/flags/bo.png similarity index 100% rename from images/flags/bo.png rename to openvpn_monitor/static/images/flags/bo.png diff --git a/images/flags/br.png b/openvpn_monitor/static/images/flags/br.png similarity index 100% rename from images/flags/br.png rename to openvpn_monitor/static/images/flags/br.png diff --git a/images/flags/bs.png b/openvpn_monitor/static/images/flags/bs.png similarity index 100% rename from images/flags/bs.png rename to openvpn_monitor/static/images/flags/bs.png diff --git a/images/flags/bt.png b/openvpn_monitor/static/images/flags/bt.png similarity index 100% rename from images/flags/bt.png rename to openvpn_monitor/static/images/flags/bt.png diff --git a/images/flags/bv.png b/openvpn_monitor/static/images/flags/bv.png similarity index 100% rename from images/flags/bv.png rename to openvpn_monitor/static/images/flags/bv.png diff --git a/images/flags/bw.png b/openvpn_monitor/static/images/flags/bw.png similarity index 100% rename from images/flags/bw.png rename to openvpn_monitor/static/images/flags/bw.png diff --git a/images/flags/by.png b/openvpn_monitor/static/images/flags/by.png similarity index 100% rename from images/flags/by.png rename to openvpn_monitor/static/images/flags/by.png diff --git a/images/flags/bz.png b/openvpn_monitor/static/images/flags/bz.png similarity index 100% rename from images/flags/bz.png rename to openvpn_monitor/static/images/flags/bz.png diff --git a/images/flags/ca.png b/openvpn_monitor/static/images/flags/ca.png similarity index 100% rename from images/flags/ca.png rename to openvpn_monitor/static/images/flags/ca.png diff --git a/images/flags/cc.png b/openvpn_monitor/static/images/flags/cc.png similarity index 100% rename from images/flags/cc.png rename to openvpn_monitor/static/images/flags/cc.png diff --git a/images/flags/cd.png b/openvpn_monitor/static/images/flags/cd.png similarity index 100% rename from images/flags/cd.png rename to openvpn_monitor/static/images/flags/cd.png diff --git a/images/flags/cf.png b/openvpn_monitor/static/images/flags/cf.png similarity index 100% rename from images/flags/cf.png rename to openvpn_monitor/static/images/flags/cf.png diff --git a/images/flags/cg.png b/openvpn_monitor/static/images/flags/cg.png similarity index 100% rename from images/flags/cg.png rename to openvpn_monitor/static/images/flags/cg.png diff --git a/images/flags/ch.png b/openvpn_monitor/static/images/flags/ch.png similarity index 100% rename from images/flags/ch.png rename to openvpn_monitor/static/images/flags/ch.png diff --git a/images/flags/ci.png b/openvpn_monitor/static/images/flags/ci.png similarity index 100% rename from images/flags/ci.png rename to openvpn_monitor/static/images/flags/ci.png diff --git a/images/flags/ck.png b/openvpn_monitor/static/images/flags/ck.png similarity index 100% rename from images/flags/ck.png rename to openvpn_monitor/static/images/flags/ck.png diff --git a/images/flags/cl.png b/openvpn_monitor/static/images/flags/cl.png similarity index 100% rename from images/flags/cl.png rename to openvpn_monitor/static/images/flags/cl.png diff --git a/images/flags/cm.png b/openvpn_monitor/static/images/flags/cm.png similarity index 100% rename from images/flags/cm.png rename to openvpn_monitor/static/images/flags/cm.png diff --git a/images/flags/cn.png b/openvpn_monitor/static/images/flags/cn.png similarity index 100% rename from images/flags/cn.png rename to openvpn_monitor/static/images/flags/cn.png diff --git a/images/flags/co.png b/openvpn_monitor/static/images/flags/co.png similarity index 100% rename from images/flags/co.png rename to openvpn_monitor/static/images/flags/co.png diff --git a/images/flags/cr.png b/openvpn_monitor/static/images/flags/cr.png similarity index 100% rename from images/flags/cr.png rename to openvpn_monitor/static/images/flags/cr.png diff --git a/images/flags/cr.woa.png b/openvpn_monitor/static/images/flags/cr.woa.png similarity index 100% rename from images/flags/cr.woa.png rename to openvpn_monitor/static/images/flags/cr.woa.png diff --git a/images/flags/cs.png b/openvpn_monitor/static/images/flags/cs.png similarity index 100% rename from images/flags/cs.png rename to openvpn_monitor/static/images/flags/cs.png diff --git a/images/flags/cu.png b/openvpn_monitor/static/images/flags/cu.png similarity index 100% rename from images/flags/cu.png rename to openvpn_monitor/static/images/flags/cu.png diff --git a/images/flags/cv.png b/openvpn_monitor/static/images/flags/cv.png similarity index 100% rename from images/flags/cv.png rename to openvpn_monitor/static/images/flags/cv.png diff --git a/images/flags/cx.png b/openvpn_monitor/static/images/flags/cx.png similarity index 100% rename from images/flags/cx.png rename to openvpn_monitor/static/images/flags/cx.png diff --git a/images/flags/cy.png b/openvpn_monitor/static/images/flags/cy.png similarity index 100% rename from images/flags/cy.png rename to openvpn_monitor/static/images/flags/cy.png diff --git a/images/flags/cz.png b/openvpn_monitor/static/images/flags/cz.png similarity index 100% rename from images/flags/cz.png rename to openvpn_monitor/static/images/flags/cz.png diff --git a/images/flags/de.png b/openvpn_monitor/static/images/flags/de.png similarity index 100% rename from images/flags/de.png rename to openvpn_monitor/static/images/flags/de.png diff --git a/images/flags/dj.png b/openvpn_monitor/static/images/flags/dj.png similarity index 100% rename from images/flags/dj.png rename to openvpn_monitor/static/images/flags/dj.png diff --git a/images/flags/dk.png b/openvpn_monitor/static/images/flags/dk.png similarity index 100% rename from images/flags/dk.png rename to openvpn_monitor/static/images/flags/dk.png diff --git a/images/flags/dm.png b/openvpn_monitor/static/images/flags/dm.png similarity index 100% rename from images/flags/dm.png rename to openvpn_monitor/static/images/flags/dm.png diff --git a/images/flags/do.png b/openvpn_monitor/static/images/flags/do.png similarity index 100% rename from images/flags/do.png rename to openvpn_monitor/static/images/flags/do.png diff --git a/images/flags/dz.png b/openvpn_monitor/static/images/flags/dz.png similarity index 100% rename from images/flags/dz.png rename to openvpn_monitor/static/images/flags/dz.png diff --git a/images/flags/ec.png b/openvpn_monitor/static/images/flags/ec.png similarity index 100% rename from images/flags/ec.png rename to openvpn_monitor/static/images/flags/ec.png diff --git a/images/flags/ee.png b/openvpn_monitor/static/images/flags/ee.png similarity index 100% rename from images/flags/ee.png rename to openvpn_monitor/static/images/flags/ee.png diff --git a/images/flags/eg.png b/openvpn_monitor/static/images/flags/eg.png similarity index 100% rename from images/flags/eg.png rename to openvpn_monitor/static/images/flags/eg.png diff --git a/images/flags/eh.png b/openvpn_monitor/static/images/flags/eh.png similarity index 100% rename from images/flags/eh.png rename to openvpn_monitor/static/images/flags/eh.png diff --git a/images/flags/er.png b/openvpn_monitor/static/images/flags/er.png similarity index 100% rename from images/flags/er.png rename to openvpn_monitor/static/images/flags/er.png diff --git a/images/flags/es.png b/openvpn_monitor/static/images/flags/es.png similarity index 100% rename from images/flags/es.png rename to openvpn_monitor/static/images/flags/es.png diff --git a/images/flags/et.png b/openvpn_monitor/static/images/flags/et.png similarity index 100% rename from images/flags/et.png rename to openvpn_monitor/static/images/flags/et.png diff --git a/images/flags/fi.png b/openvpn_monitor/static/images/flags/fi.png similarity index 100% rename from images/flags/fi.png rename to openvpn_monitor/static/images/flags/fi.png diff --git a/images/flags/fj.png b/openvpn_monitor/static/images/flags/fj.png similarity index 100% rename from images/flags/fj.png rename to openvpn_monitor/static/images/flags/fj.png diff --git a/images/flags/fk.png b/openvpn_monitor/static/images/flags/fk.png similarity index 100% rename from images/flags/fk.png rename to openvpn_monitor/static/images/flags/fk.png diff --git a/images/flags/fm.png b/openvpn_monitor/static/images/flags/fm.png similarity index 100% rename from images/flags/fm.png rename to openvpn_monitor/static/images/flags/fm.png diff --git a/images/flags/fo.png b/openvpn_monitor/static/images/flags/fo.png similarity index 100% rename from images/flags/fo.png rename to openvpn_monitor/static/images/flags/fo.png diff --git a/images/flags/fr.png b/openvpn_monitor/static/images/flags/fr.png similarity index 100% rename from images/flags/fr.png rename to openvpn_monitor/static/images/flags/fr.png diff --git a/images/flags/fx.png b/openvpn_monitor/static/images/flags/fx.png similarity index 100% rename from images/flags/fx.png rename to openvpn_monitor/static/images/flags/fx.png diff --git a/images/flags/ga.png b/openvpn_monitor/static/images/flags/ga.png similarity index 100% rename from images/flags/ga.png rename to openvpn_monitor/static/images/flags/ga.png diff --git a/images/flags/gb.png b/openvpn_monitor/static/images/flags/gb.png similarity index 100% rename from images/flags/gb.png rename to openvpn_monitor/static/images/flags/gb.png diff --git a/images/flags/gd.png b/openvpn_monitor/static/images/flags/gd.png similarity index 100% rename from images/flags/gd.png rename to openvpn_monitor/static/images/flags/gd.png diff --git a/images/flags/ge.png b/openvpn_monitor/static/images/flags/ge.png similarity index 100% rename from images/flags/ge.png rename to openvpn_monitor/static/images/flags/ge.png diff --git a/images/flags/gf.png b/openvpn_monitor/static/images/flags/gf.png similarity index 100% rename from images/flags/gf.png rename to openvpn_monitor/static/images/flags/gf.png diff --git a/images/flags/gg.png b/openvpn_monitor/static/images/flags/gg.png similarity index 100% rename from images/flags/gg.png rename to openvpn_monitor/static/images/flags/gg.png diff --git a/images/flags/gh.png b/openvpn_monitor/static/images/flags/gh.png similarity index 100% rename from images/flags/gh.png rename to openvpn_monitor/static/images/flags/gh.png diff --git a/images/flags/gi.png b/openvpn_monitor/static/images/flags/gi.png similarity index 100% rename from images/flags/gi.png rename to openvpn_monitor/static/images/flags/gi.png diff --git a/images/flags/gl.png b/openvpn_monitor/static/images/flags/gl.png similarity index 100% rename from images/flags/gl.png rename to openvpn_monitor/static/images/flags/gl.png diff --git a/images/flags/gm.png b/openvpn_monitor/static/images/flags/gm.png similarity index 100% rename from images/flags/gm.png rename to openvpn_monitor/static/images/flags/gm.png diff --git a/images/flags/gn.png b/openvpn_monitor/static/images/flags/gn.png similarity index 100% rename from images/flags/gn.png rename to openvpn_monitor/static/images/flags/gn.png diff --git a/images/flags/gp.loc1.png b/openvpn_monitor/static/images/flags/gp.loc1.png similarity index 100% rename from images/flags/gp.loc1.png rename to openvpn_monitor/static/images/flags/gp.loc1.png diff --git a/images/flags/gp.loc2.png b/openvpn_monitor/static/images/flags/gp.loc2.png similarity index 100% rename from images/flags/gp.loc2.png rename to openvpn_monitor/static/images/flags/gp.loc2.png diff --git a/images/flags/gp.png b/openvpn_monitor/static/images/flags/gp.png similarity index 100% rename from images/flags/gp.png rename to openvpn_monitor/static/images/flags/gp.png diff --git a/images/flags/gq.png b/openvpn_monitor/static/images/flags/gq.png similarity index 100% rename from images/flags/gq.png rename to openvpn_monitor/static/images/flags/gq.png diff --git a/images/flags/gr.png b/openvpn_monitor/static/images/flags/gr.png similarity index 100% rename from images/flags/gr.png rename to openvpn_monitor/static/images/flags/gr.png diff --git a/images/flags/gs.png b/openvpn_monitor/static/images/flags/gs.png similarity index 100% rename from images/flags/gs.png rename to openvpn_monitor/static/images/flags/gs.png diff --git a/images/flags/gt.png b/openvpn_monitor/static/images/flags/gt.png similarity index 100% rename from images/flags/gt.png rename to openvpn_monitor/static/images/flags/gt.png diff --git a/images/flags/gu.png b/openvpn_monitor/static/images/flags/gu.png similarity index 100% rename from images/flags/gu.png rename to openvpn_monitor/static/images/flags/gu.png diff --git a/images/flags/gw.png b/openvpn_monitor/static/images/flags/gw.png similarity index 100% rename from images/flags/gw.png rename to openvpn_monitor/static/images/flags/gw.png diff --git a/images/flags/gy.png b/openvpn_monitor/static/images/flags/gy.png similarity index 100% rename from images/flags/gy.png rename to openvpn_monitor/static/images/flags/gy.png diff --git a/images/flags/hk.png b/openvpn_monitor/static/images/flags/hk.png similarity index 100% rename from images/flags/hk.png rename to openvpn_monitor/static/images/flags/hk.png diff --git a/images/flags/hm.png b/openvpn_monitor/static/images/flags/hm.png similarity index 100% rename from images/flags/hm.png rename to openvpn_monitor/static/images/flags/hm.png diff --git a/images/flags/hn.png b/openvpn_monitor/static/images/flags/hn.png similarity index 100% rename from images/flags/hn.png rename to openvpn_monitor/static/images/flags/hn.png diff --git a/images/flags/hr.png b/openvpn_monitor/static/images/flags/hr.png similarity index 100% rename from images/flags/hr.png rename to openvpn_monitor/static/images/flags/hr.png diff --git a/images/flags/ht.png b/openvpn_monitor/static/images/flags/ht.png similarity index 100% rename from images/flags/ht.png rename to openvpn_monitor/static/images/flags/ht.png diff --git a/images/flags/ht.woa.png b/openvpn_monitor/static/images/flags/ht.woa.png similarity index 100% rename from images/flags/ht.woa.png rename to openvpn_monitor/static/images/flags/ht.woa.png diff --git a/images/flags/hu.png b/openvpn_monitor/static/images/flags/hu.png similarity index 100% rename from images/flags/hu.png rename to openvpn_monitor/static/images/flags/hu.png diff --git a/images/flags/id.png b/openvpn_monitor/static/images/flags/id.png similarity index 100% rename from images/flags/id.png rename to openvpn_monitor/static/images/flags/id.png diff --git a/images/flags/ie.png b/openvpn_monitor/static/images/flags/ie.png similarity index 100% rename from images/flags/ie.png rename to openvpn_monitor/static/images/flags/ie.png diff --git a/images/flags/il.png b/openvpn_monitor/static/images/flags/il.png similarity index 100% rename from images/flags/il.png rename to openvpn_monitor/static/images/flags/il.png diff --git a/images/flags/in.png b/openvpn_monitor/static/images/flags/in.png similarity index 100% rename from images/flags/in.png rename to openvpn_monitor/static/images/flags/in.png diff --git a/images/flags/io.png b/openvpn_monitor/static/images/flags/io.png similarity index 100% rename from images/flags/io.png rename to openvpn_monitor/static/images/flags/io.png diff --git a/images/flags/iq.png b/openvpn_monitor/static/images/flags/iq.png similarity index 100% rename from images/flags/iq.png rename to openvpn_monitor/static/images/flags/iq.png diff --git a/images/flags/ir.png b/openvpn_monitor/static/images/flags/ir.png similarity index 100% rename from images/flags/ir.png rename to openvpn_monitor/static/images/flags/ir.png diff --git a/images/flags/is.png b/openvpn_monitor/static/images/flags/is.png similarity index 100% rename from images/flags/is.png rename to openvpn_monitor/static/images/flags/is.png diff --git a/images/flags/it.png b/openvpn_monitor/static/images/flags/it.png similarity index 100% rename from images/flags/it.png rename to openvpn_monitor/static/images/flags/it.png diff --git a/images/flags/je.png b/openvpn_monitor/static/images/flags/je.png similarity index 100% rename from images/flags/je.png rename to openvpn_monitor/static/images/flags/je.png diff --git a/images/flags/jm.png b/openvpn_monitor/static/images/flags/jm.png similarity index 100% rename from images/flags/jm.png rename to openvpn_monitor/static/images/flags/jm.png diff --git a/images/flags/jo.png b/openvpn_monitor/static/images/flags/jo.png similarity index 100% rename from images/flags/jo.png rename to openvpn_monitor/static/images/flags/jo.png diff --git a/images/flags/jp.png b/openvpn_monitor/static/images/flags/jp.png similarity index 100% rename from images/flags/jp.png rename to openvpn_monitor/static/images/flags/jp.png diff --git a/images/flags/ke.png b/openvpn_monitor/static/images/flags/ke.png similarity index 100% rename from images/flags/ke.png rename to openvpn_monitor/static/images/flags/ke.png diff --git a/images/flags/kg.png b/openvpn_monitor/static/images/flags/kg.png similarity index 100% rename from images/flags/kg.png rename to openvpn_monitor/static/images/flags/kg.png diff --git a/images/flags/kh.png b/openvpn_monitor/static/images/flags/kh.png similarity index 100% rename from images/flags/kh.png rename to openvpn_monitor/static/images/flags/kh.png diff --git a/images/flags/ki.png b/openvpn_monitor/static/images/flags/ki.png similarity index 100% rename from images/flags/ki.png rename to openvpn_monitor/static/images/flags/ki.png diff --git a/images/flags/km.png b/openvpn_monitor/static/images/flags/km.png similarity index 100% rename from images/flags/km.png rename to openvpn_monitor/static/images/flags/km.png diff --git a/images/flags/kn.png b/openvpn_monitor/static/images/flags/kn.png similarity index 100% rename from images/flags/kn.png rename to openvpn_monitor/static/images/flags/kn.png diff --git a/images/flags/kp.png b/openvpn_monitor/static/images/flags/kp.png similarity index 100% rename from images/flags/kp.png rename to openvpn_monitor/static/images/flags/kp.png diff --git a/images/flags/kr.png b/openvpn_monitor/static/images/flags/kr.png similarity index 100% rename from images/flags/kr.png rename to openvpn_monitor/static/images/flags/kr.png diff --git a/images/flags/kw.png b/openvpn_monitor/static/images/flags/kw.png similarity index 100% rename from images/flags/kw.png rename to openvpn_monitor/static/images/flags/kw.png diff --git a/images/flags/ky.png b/openvpn_monitor/static/images/flags/ky.png similarity index 100% rename from images/flags/ky.png rename to openvpn_monitor/static/images/flags/ky.png diff --git a/images/flags/kz.png b/openvpn_monitor/static/images/flags/kz.png similarity index 100% rename from images/flags/kz.png rename to openvpn_monitor/static/images/flags/kz.png diff --git a/images/flags/la.png b/openvpn_monitor/static/images/flags/la.png similarity index 100% rename from images/flags/la.png rename to openvpn_monitor/static/images/flags/la.png diff --git a/images/flags/lb.png b/openvpn_monitor/static/images/flags/lb.png similarity index 100% rename from images/flags/lb.png rename to openvpn_monitor/static/images/flags/lb.png diff --git a/images/flags/lc.png b/openvpn_monitor/static/images/flags/lc.png similarity index 100% rename from images/flags/lc.png rename to openvpn_monitor/static/images/flags/lc.png diff --git a/images/flags/li.png b/openvpn_monitor/static/images/flags/li.png similarity index 100% rename from images/flags/li.png rename to openvpn_monitor/static/images/flags/li.png diff --git a/images/flags/li.woa.png b/openvpn_monitor/static/images/flags/li.woa.png similarity index 100% rename from images/flags/li.woa.png rename to openvpn_monitor/static/images/flags/li.woa.png diff --git a/images/flags/lk.png b/openvpn_monitor/static/images/flags/lk.png similarity index 100% rename from images/flags/lk.png rename to openvpn_monitor/static/images/flags/lk.png diff --git a/images/flags/lr.png b/openvpn_monitor/static/images/flags/lr.png similarity index 100% rename from images/flags/lr.png rename to openvpn_monitor/static/images/flags/lr.png diff --git a/images/flags/ls.png b/openvpn_monitor/static/images/flags/ls.png similarity index 100% rename from images/flags/ls.png rename to openvpn_monitor/static/images/flags/ls.png diff --git a/images/flags/lt.png b/openvpn_monitor/static/images/flags/lt.png similarity index 100% rename from images/flags/lt.png rename to openvpn_monitor/static/images/flags/lt.png diff --git a/images/flags/lu.png b/openvpn_monitor/static/images/flags/lu.png similarity index 100% rename from images/flags/lu.png rename to openvpn_monitor/static/images/flags/lu.png diff --git a/images/flags/lv.png b/openvpn_monitor/static/images/flags/lv.png similarity index 100% rename from images/flags/lv.png rename to openvpn_monitor/static/images/flags/lv.png diff --git a/images/flags/ly.png b/openvpn_monitor/static/images/flags/ly.png similarity index 100% rename from images/flags/ly.png rename to openvpn_monitor/static/images/flags/ly.png diff --git a/images/flags/ma.png b/openvpn_monitor/static/images/flags/ma.png similarity index 100% rename from images/flags/ma.png rename to openvpn_monitor/static/images/flags/ma.png diff --git a/images/flags/mc.png b/openvpn_monitor/static/images/flags/mc.png similarity index 100% rename from images/flags/mc.png rename to openvpn_monitor/static/images/flags/mc.png diff --git a/images/flags/md.png b/openvpn_monitor/static/images/flags/md.png similarity index 100% rename from images/flags/md.png rename to openvpn_monitor/static/images/flags/md.png diff --git a/images/flags/me.png b/openvpn_monitor/static/images/flags/me.png similarity index 100% rename from images/flags/me.png rename to openvpn_monitor/static/images/flags/me.png diff --git a/images/flags/mf.loc1.png b/openvpn_monitor/static/images/flags/mf.loc1.png similarity index 100% rename from images/flags/mf.loc1.png rename to openvpn_monitor/static/images/flags/mf.loc1.png diff --git a/images/flags/mf.loc2.png b/openvpn_monitor/static/images/flags/mf.loc2.png similarity index 100% rename from images/flags/mf.loc2.png rename to openvpn_monitor/static/images/flags/mf.loc2.png diff --git a/images/flags/mf.png b/openvpn_monitor/static/images/flags/mf.png similarity index 100% rename from images/flags/mf.png rename to openvpn_monitor/static/images/flags/mf.png diff --git a/images/flags/mg.png b/openvpn_monitor/static/images/flags/mg.png similarity index 100% rename from images/flags/mg.png rename to openvpn_monitor/static/images/flags/mg.png diff --git a/images/flags/mh.png b/openvpn_monitor/static/images/flags/mh.png similarity index 100% rename from images/flags/mh.png rename to openvpn_monitor/static/images/flags/mh.png diff --git a/images/flags/mk.png b/openvpn_monitor/static/images/flags/mk.png similarity index 100% rename from images/flags/mk.png rename to openvpn_monitor/static/images/flags/mk.png diff --git a/images/flags/ml.png b/openvpn_monitor/static/images/flags/ml.png similarity index 100% rename from images/flags/ml.png rename to openvpn_monitor/static/images/flags/ml.png diff --git a/images/flags/mm.png b/openvpn_monitor/static/images/flags/mm.png similarity index 100% rename from images/flags/mm.png rename to openvpn_monitor/static/images/flags/mm.png diff --git a/images/flags/mn.png b/openvpn_monitor/static/images/flags/mn.png similarity index 100% rename from images/flags/mn.png rename to openvpn_monitor/static/images/flags/mn.png diff --git a/images/flags/mo.png b/openvpn_monitor/static/images/flags/mo.png similarity index 100% rename from images/flags/mo.png rename to openvpn_monitor/static/images/flags/mo.png diff --git a/images/flags/mp.png b/openvpn_monitor/static/images/flags/mp.png similarity index 100% rename from images/flags/mp.png rename to openvpn_monitor/static/images/flags/mp.png diff --git a/images/flags/mq.png b/openvpn_monitor/static/images/flags/mq.png similarity index 100% rename from images/flags/mq.png rename to openvpn_monitor/static/images/flags/mq.png diff --git a/images/flags/mq.snake1.png b/openvpn_monitor/static/images/flags/mq.snake1.png similarity index 100% rename from images/flags/mq.snake1.png rename to openvpn_monitor/static/images/flags/mq.snake1.png diff --git a/images/flags/mq.snake2.png b/openvpn_monitor/static/images/flags/mq.snake2.png similarity index 100% rename from images/flags/mq.snake2.png rename to openvpn_monitor/static/images/flags/mq.snake2.png diff --git a/images/flags/mr.png b/openvpn_monitor/static/images/flags/mr.png similarity index 100% rename from images/flags/mr.png rename to openvpn_monitor/static/images/flags/mr.png diff --git a/images/flags/ms.png b/openvpn_monitor/static/images/flags/ms.png similarity index 100% rename from images/flags/ms.png rename to openvpn_monitor/static/images/flags/ms.png diff --git a/images/flags/mt.png b/openvpn_monitor/static/images/flags/mt.png similarity index 100% rename from images/flags/mt.png rename to openvpn_monitor/static/images/flags/mt.png diff --git a/images/flags/mu.png b/openvpn_monitor/static/images/flags/mu.png similarity index 100% rename from images/flags/mu.png rename to openvpn_monitor/static/images/flags/mu.png diff --git a/images/flags/mv.png b/openvpn_monitor/static/images/flags/mv.png similarity index 100% rename from images/flags/mv.png rename to openvpn_monitor/static/images/flags/mv.png diff --git a/images/flags/mw.png b/openvpn_monitor/static/images/flags/mw.png similarity index 100% rename from images/flags/mw.png rename to openvpn_monitor/static/images/flags/mw.png diff --git a/images/flags/mx.png b/openvpn_monitor/static/images/flags/mx.png similarity index 100% rename from images/flags/mx.png rename to openvpn_monitor/static/images/flags/mx.png diff --git a/images/flags/my.png b/openvpn_monitor/static/images/flags/my.png similarity index 100% rename from images/flags/my.png rename to openvpn_monitor/static/images/flags/my.png diff --git a/images/flags/mz.png b/openvpn_monitor/static/images/flags/mz.png similarity index 100% rename from images/flags/mz.png rename to openvpn_monitor/static/images/flags/mz.png diff --git a/images/flags/na.png b/openvpn_monitor/static/images/flags/na.png similarity index 100% rename from images/flags/na.png rename to openvpn_monitor/static/images/flags/na.png diff --git a/images/flags/nc.png b/openvpn_monitor/static/images/flags/nc.png similarity index 100% rename from images/flags/nc.png rename to openvpn_monitor/static/images/flags/nc.png diff --git a/images/flags/ne.png b/openvpn_monitor/static/images/flags/ne.png similarity index 100% rename from images/flags/ne.png rename to openvpn_monitor/static/images/flags/ne.png diff --git a/images/flags/nf.png b/openvpn_monitor/static/images/flags/nf.png similarity index 100% rename from images/flags/nf.png rename to openvpn_monitor/static/images/flags/nf.png diff --git a/images/flags/ng.png b/openvpn_monitor/static/images/flags/ng.png similarity index 100% rename from images/flags/ng.png rename to openvpn_monitor/static/images/flags/ng.png diff --git a/images/flags/ni.png b/openvpn_monitor/static/images/flags/ni.png similarity index 100% rename from images/flags/ni.png rename to openvpn_monitor/static/images/flags/ni.png diff --git a/images/flags/ni.woa.png b/openvpn_monitor/static/images/flags/ni.woa.png similarity index 100% rename from images/flags/ni.woa.png rename to openvpn_monitor/static/images/flags/ni.woa.png diff --git a/images/flags/nl.png b/openvpn_monitor/static/images/flags/nl.png similarity index 100% rename from images/flags/nl.png rename to openvpn_monitor/static/images/flags/nl.png diff --git a/images/flags/no.png b/openvpn_monitor/static/images/flags/no.png similarity index 100% rename from images/flags/no.png rename to openvpn_monitor/static/images/flags/no.png diff --git a/images/flags/np.png b/openvpn_monitor/static/images/flags/np.png similarity index 100% rename from images/flags/np.png rename to openvpn_monitor/static/images/flags/np.png diff --git a/images/flags/nr.png b/openvpn_monitor/static/images/flags/nr.png similarity index 100% rename from images/flags/nr.png rename to openvpn_monitor/static/images/flags/nr.png diff --git a/images/flags/nu.png b/openvpn_monitor/static/images/flags/nu.png similarity index 100% rename from images/flags/nu.png rename to openvpn_monitor/static/images/flags/nu.png diff --git a/images/flags/nz.png b/openvpn_monitor/static/images/flags/nz.png similarity index 100% rename from images/flags/nz.png rename to openvpn_monitor/static/images/flags/nz.png diff --git a/images/flags/om.png b/openvpn_monitor/static/images/flags/om.png similarity index 100% rename from images/flags/om.png rename to openvpn_monitor/static/images/flags/om.png diff --git a/images/flags/pa.png b/openvpn_monitor/static/images/flags/pa.png similarity index 100% rename from images/flags/pa.png rename to openvpn_monitor/static/images/flags/pa.png diff --git a/images/flags/pe.png b/openvpn_monitor/static/images/flags/pe.png similarity index 100% rename from images/flags/pe.png rename to openvpn_monitor/static/images/flags/pe.png diff --git a/images/flags/pe.woa.png b/openvpn_monitor/static/images/flags/pe.woa.png similarity index 100% rename from images/flags/pe.woa.png rename to openvpn_monitor/static/images/flags/pe.woa.png diff --git a/images/flags/pf.png b/openvpn_monitor/static/images/flags/pf.png similarity index 100% rename from images/flags/pf.png rename to openvpn_monitor/static/images/flags/pf.png diff --git a/images/flags/pg.png b/openvpn_monitor/static/images/flags/pg.png similarity index 100% rename from images/flags/pg.png rename to openvpn_monitor/static/images/flags/pg.png diff --git a/images/flags/ph.png b/openvpn_monitor/static/images/flags/ph.png similarity index 100% rename from images/flags/ph.png rename to openvpn_monitor/static/images/flags/ph.png diff --git a/images/flags/pk.png b/openvpn_monitor/static/images/flags/pk.png similarity index 100% rename from images/flags/pk.png rename to openvpn_monitor/static/images/flags/pk.png diff --git a/images/flags/pl.png b/openvpn_monitor/static/images/flags/pl.png similarity index 100% rename from images/flags/pl.png rename to openvpn_monitor/static/images/flags/pl.png diff --git a/images/flags/pm.loc.png b/openvpn_monitor/static/images/flags/pm.loc.png similarity index 100% rename from images/flags/pm.loc.png rename to openvpn_monitor/static/images/flags/pm.loc.png diff --git a/images/flags/pm.png b/openvpn_monitor/static/images/flags/pm.png similarity index 100% rename from images/flags/pm.png rename to openvpn_monitor/static/images/flags/pm.png diff --git a/images/flags/pn.png b/openvpn_monitor/static/images/flags/pn.png similarity index 100% rename from images/flags/pn.png rename to openvpn_monitor/static/images/flags/pn.png diff --git a/images/flags/pr.png b/openvpn_monitor/static/images/flags/pr.png similarity index 100% rename from images/flags/pr.png rename to openvpn_monitor/static/images/flags/pr.png diff --git a/images/flags/ps.png b/openvpn_monitor/static/images/flags/ps.png similarity index 100% rename from images/flags/ps.png rename to openvpn_monitor/static/images/flags/ps.png diff --git a/images/flags/pt.png b/openvpn_monitor/static/images/flags/pt.png similarity index 100% rename from images/flags/pt.png rename to openvpn_monitor/static/images/flags/pt.png diff --git a/images/flags/pw.png b/openvpn_monitor/static/images/flags/pw.png similarity index 100% rename from images/flags/pw.png rename to openvpn_monitor/static/images/flags/pw.png diff --git a/images/flags/py.png b/openvpn_monitor/static/images/flags/py.png similarity index 100% rename from images/flags/py.png rename to openvpn_monitor/static/images/flags/py.png diff --git a/images/flags/qa.png b/openvpn_monitor/static/images/flags/qa.png similarity index 100% rename from images/flags/qa.png rename to openvpn_monitor/static/images/flags/qa.png diff --git a/images/flags/re.png b/openvpn_monitor/static/images/flags/re.png similarity index 100% rename from images/flags/re.png rename to openvpn_monitor/static/images/flags/re.png diff --git a/images/flags/rfc.png b/openvpn_monitor/static/images/flags/rfc.png similarity index 100% rename from images/flags/rfc.png rename to openvpn_monitor/static/images/flags/rfc.png diff --git a/images/flags/ro.png b/openvpn_monitor/static/images/flags/ro.png similarity index 100% rename from images/flags/ro.png rename to openvpn_monitor/static/images/flags/ro.png diff --git a/images/flags/rs.png b/openvpn_monitor/static/images/flags/rs.png similarity index 100% rename from images/flags/rs.png rename to openvpn_monitor/static/images/flags/rs.png diff --git a/images/flags/rs.woa.png b/openvpn_monitor/static/images/flags/rs.woa.png similarity index 100% rename from images/flags/rs.woa.png rename to openvpn_monitor/static/images/flags/rs.woa.png diff --git a/images/flags/ru.png b/openvpn_monitor/static/images/flags/ru.png similarity index 100% rename from images/flags/ru.png rename to openvpn_monitor/static/images/flags/ru.png diff --git a/images/flags/rw.png b/openvpn_monitor/static/images/flags/rw.png similarity index 100% rename from images/flags/rw.png rename to openvpn_monitor/static/images/flags/rw.png diff --git a/images/flags/sa.png b/openvpn_monitor/static/images/flags/sa.png similarity index 100% rename from images/flags/sa.png rename to openvpn_monitor/static/images/flags/sa.png diff --git a/images/flags/sb.png b/openvpn_monitor/static/images/flags/sb.png similarity index 100% rename from images/flags/sb.png rename to openvpn_monitor/static/images/flags/sb.png diff --git a/images/flags/sc.png b/openvpn_monitor/static/images/flags/sc.png similarity index 100% rename from images/flags/sc.png rename to openvpn_monitor/static/images/flags/sc.png diff --git a/images/flags/sd.png b/openvpn_monitor/static/images/flags/sd.png similarity index 100% rename from images/flags/sd.png rename to openvpn_monitor/static/images/flags/sd.png diff --git a/images/flags/se.png b/openvpn_monitor/static/images/flags/se.png similarity index 100% rename from images/flags/se.png rename to openvpn_monitor/static/images/flags/se.png diff --git a/images/flags/sg.png b/openvpn_monitor/static/images/flags/sg.png similarity index 100% rename from images/flags/sg.png rename to openvpn_monitor/static/images/flags/sg.png diff --git a/images/flags/sh.png b/openvpn_monitor/static/images/flags/sh.png similarity index 100% rename from images/flags/sh.png rename to openvpn_monitor/static/images/flags/sh.png diff --git a/images/flags/si.png b/openvpn_monitor/static/images/flags/si.png similarity index 100% rename from images/flags/si.png rename to openvpn_monitor/static/images/flags/si.png diff --git a/images/flags/sj.png b/openvpn_monitor/static/images/flags/sj.png similarity index 100% rename from images/flags/sj.png rename to openvpn_monitor/static/images/flags/sj.png diff --git a/images/flags/sk.png b/openvpn_monitor/static/images/flags/sk.png similarity index 100% rename from images/flags/sk.png rename to openvpn_monitor/static/images/flags/sk.png diff --git a/images/flags/sl.png b/openvpn_monitor/static/images/flags/sl.png similarity index 100% rename from images/flags/sl.png rename to openvpn_monitor/static/images/flags/sl.png diff --git a/images/flags/sm.png b/openvpn_monitor/static/images/flags/sm.png similarity index 100% rename from images/flags/sm.png rename to openvpn_monitor/static/images/flags/sm.png diff --git a/images/flags/sn.png b/openvpn_monitor/static/images/flags/sn.png similarity index 100% rename from images/flags/sn.png rename to openvpn_monitor/static/images/flags/sn.png diff --git a/images/flags/so.png b/openvpn_monitor/static/images/flags/so.png similarity index 100% rename from images/flags/so.png rename to openvpn_monitor/static/images/flags/so.png diff --git a/images/flags/sr.png b/openvpn_monitor/static/images/flags/sr.png similarity index 100% rename from images/flags/sr.png rename to openvpn_monitor/static/images/flags/sr.png diff --git a/images/flags/st.png b/openvpn_monitor/static/images/flags/st.png similarity index 100% rename from images/flags/st.png rename to openvpn_monitor/static/images/flags/st.png diff --git a/images/flags/sv.png b/openvpn_monitor/static/images/flags/sv.png similarity index 100% rename from images/flags/sv.png rename to openvpn_monitor/static/images/flags/sv.png diff --git a/images/flags/sv.woa.png b/openvpn_monitor/static/images/flags/sv.woa.png similarity index 100% rename from images/flags/sv.woa.png rename to openvpn_monitor/static/images/flags/sv.woa.png diff --git a/images/flags/sy.png b/openvpn_monitor/static/images/flags/sy.png similarity index 100% rename from images/flags/sy.png rename to openvpn_monitor/static/images/flags/sy.png diff --git a/images/flags/sz.png b/openvpn_monitor/static/images/flags/sz.png similarity index 100% rename from images/flags/sz.png rename to openvpn_monitor/static/images/flags/sz.png diff --git a/images/flags/tc.png b/openvpn_monitor/static/images/flags/tc.png similarity index 100% rename from images/flags/tc.png rename to openvpn_monitor/static/images/flags/tc.png diff --git a/images/flags/td.png b/openvpn_monitor/static/images/flags/td.png similarity index 100% rename from images/flags/td.png rename to openvpn_monitor/static/images/flags/td.png diff --git a/images/flags/tf.png b/openvpn_monitor/static/images/flags/tf.png similarity index 100% rename from images/flags/tf.png rename to openvpn_monitor/static/images/flags/tf.png diff --git a/images/flags/tg.png b/openvpn_monitor/static/images/flags/tg.png similarity index 100% rename from images/flags/tg.png rename to openvpn_monitor/static/images/flags/tg.png diff --git a/images/flags/th.png b/openvpn_monitor/static/images/flags/th.png similarity index 100% rename from images/flags/th.png rename to openvpn_monitor/static/images/flags/th.png diff --git a/images/flags/tj.png b/openvpn_monitor/static/images/flags/tj.png similarity index 100% rename from images/flags/tj.png rename to openvpn_monitor/static/images/flags/tj.png diff --git a/images/flags/tk.png b/openvpn_monitor/static/images/flags/tk.png similarity index 100% rename from images/flags/tk.png rename to openvpn_monitor/static/images/flags/tk.png diff --git a/images/flags/tl.png b/openvpn_monitor/static/images/flags/tl.png similarity index 100% rename from images/flags/tl.png rename to openvpn_monitor/static/images/flags/tl.png diff --git a/images/flags/tm.png b/openvpn_monitor/static/images/flags/tm.png similarity index 100% rename from images/flags/tm.png rename to openvpn_monitor/static/images/flags/tm.png diff --git a/images/flags/tn.png b/openvpn_monitor/static/images/flags/tn.png similarity index 100% rename from images/flags/tn.png rename to openvpn_monitor/static/images/flags/tn.png diff --git a/images/flags/to.png b/openvpn_monitor/static/images/flags/to.png similarity index 100% rename from images/flags/to.png rename to openvpn_monitor/static/images/flags/to.png diff --git a/images/flags/tp.png b/openvpn_monitor/static/images/flags/tp.png similarity index 100% rename from images/flags/tp.png rename to openvpn_monitor/static/images/flags/tp.png diff --git a/images/flags/tr.png b/openvpn_monitor/static/images/flags/tr.png similarity index 100% rename from images/flags/tr.png rename to openvpn_monitor/static/images/flags/tr.png diff --git a/images/flags/tt.png b/openvpn_monitor/static/images/flags/tt.png similarity index 100% rename from images/flags/tt.png rename to openvpn_monitor/static/images/flags/tt.png diff --git a/images/flags/tv.png b/openvpn_monitor/static/images/flags/tv.png similarity index 100% rename from images/flags/tv.png rename to openvpn_monitor/static/images/flags/tv.png diff --git a/images/flags/tw.png b/openvpn_monitor/static/images/flags/tw.png similarity index 100% rename from images/flags/tw.png rename to openvpn_monitor/static/images/flags/tw.png diff --git a/images/flags/tz.png b/openvpn_monitor/static/images/flags/tz.png similarity index 100% rename from images/flags/tz.png rename to openvpn_monitor/static/images/flags/tz.png diff --git a/images/flags/ua.png b/openvpn_monitor/static/images/flags/ua.png similarity index 100% rename from images/flags/ua.png rename to openvpn_monitor/static/images/flags/ua.png diff --git a/images/flags/ug.png b/openvpn_monitor/static/images/flags/ug.png similarity index 100% rename from images/flags/ug.png rename to openvpn_monitor/static/images/flags/ug.png diff --git a/images/flags/um.png b/openvpn_monitor/static/images/flags/um.png similarity index 100% rename from images/flags/um.png rename to openvpn_monitor/static/images/flags/um.png diff --git a/images/flags/us.png b/openvpn_monitor/static/images/flags/us.png similarity index 100% rename from images/flags/us.png rename to openvpn_monitor/static/images/flags/us.png diff --git a/images/flags/uy.png b/openvpn_monitor/static/images/flags/uy.png similarity index 100% rename from images/flags/uy.png rename to openvpn_monitor/static/images/flags/uy.png diff --git a/images/flags/uz.png b/openvpn_monitor/static/images/flags/uz.png similarity index 100% rename from images/flags/uz.png rename to openvpn_monitor/static/images/flags/uz.png diff --git a/images/flags/va.png b/openvpn_monitor/static/images/flags/va.png similarity index 100% rename from images/flags/va.png rename to openvpn_monitor/static/images/flags/va.png diff --git a/images/flags/vc.png b/openvpn_monitor/static/images/flags/vc.png similarity index 100% rename from images/flags/vc.png rename to openvpn_monitor/static/images/flags/vc.png diff --git a/images/flags/ve.png b/openvpn_monitor/static/images/flags/ve.png similarity index 100% rename from images/flags/ve.png rename to openvpn_monitor/static/images/flags/ve.png diff --git a/images/flags/vg.png b/openvpn_monitor/static/images/flags/vg.png similarity index 100% rename from images/flags/vg.png rename to openvpn_monitor/static/images/flags/vg.png diff --git a/images/flags/vi.png b/openvpn_monitor/static/images/flags/vi.png similarity index 100% rename from images/flags/vi.png rename to openvpn_monitor/static/images/flags/vi.png diff --git a/images/flags/vn.png b/openvpn_monitor/static/images/flags/vn.png similarity index 100% rename from images/flags/vn.png rename to openvpn_monitor/static/images/flags/vn.png diff --git a/images/flags/vu.png b/openvpn_monitor/static/images/flags/vu.png similarity index 100% rename from images/flags/vu.png rename to openvpn_monitor/static/images/flags/vu.png diff --git a/images/flags/wf.png b/openvpn_monitor/static/images/flags/wf.png similarity index 100% rename from images/flags/wf.png rename to openvpn_monitor/static/images/flags/wf.png diff --git a/images/flags/ws.png b/openvpn_monitor/static/images/flags/ws.png similarity index 100% rename from images/flags/ws.png rename to openvpn_monitor/static/images/flags/ws.png diff --git a/images/flags/xt.png b/openvpn_monitor/static/images/flags/xt.png similarity index 100% rename from images/flags/xt.png rename to openvpn_monitor/static/images/flags/xt.png diff --git a/images/flags/ye.png b/openvpn_monitor/static/images/flags/ye.png similarity index 100% rename from images/flags/ye.png rename to openvpn_monitor/static/images/flags/ye.png diff --git a/images/flags/yt.loc.png b/openvpn_monitor/static/images/flags/yt.loc.png similarity index 100% rename from images/flags/yt.loc.png rename to openvpn_monitor/static/images/flags/yt.loc.png diff --git a/images/flags/yt.png b/openvpn_monitor/static/images/flags/yt.png similarity index 100% rename from images/flags/yt.png rename to openvpn_monitor/static/images/flags/yt.png diff --git a/images/flags/yu.png b/openvpn_monitor/static/images/flags/yu.png similarity index 100% rename from images/flags/yu.png rename to openvpn_monitor/static/images/flags/yu.png diff --git a/images/flags/za.png b/openvpn_monitor/static/images/flags/za.png similarity index 100% rename from images/flags/za.png rename to openvpn_monitor/static/images/flags/za.png diff --git a/images/flags/zm.png b/openvpn_monitor/static/images/flags/zm.png similarity index 100% rename from images/flags/zm.png rename to openvpn_monitor/static/images/flags/zm.png diff --git a/images/flags/zw.png b/openvpn_monitor/static/images/flags/zw.png similarity index 100% rename from images/flags/zw.png rename to openvpn_monitor/static/images/flags/zw.png diff --git a/openvpn_monitor/templates/base.html b/openvpn_monitor/templates/base.html new file mode 100644 index 0000000..34e0362 --- /dev/null +++ b/openvpn_monitor/templates/base.html @@ -0,0 +1,28 @@ + + + + + + + {{ site }} OpenVPN Status Monitor + {% include 'css.html' %} + {% include 'js.html' %} + + + {% include 'navbar.html' %} +
+ {% for vpn_id, vpn in vpns %} + {% if vpn.get('management_connection_successful') %} + {% include 'vpn_info.html' %} + {% else %} + {% include 'unavailable_vpn.html' %} + {% endif %} + {% endfor %} + {% block content %}{% endblock %} + {% include 'maps.html' %} +
+ Page automatically reloads every 5 minutes. Last update: {{ datetime_format | get_formatted_time_now }} +
+
+ + diff --git a/openvpn_monitor/templates/client_session.html b/openvpn_monitor/templates/client_session.html new file mode 100644 index 0000000..0d35381 --- /dev/null +++ b/openvpn_monitor/templates/client_session.html @@ -0,0 +1,10 @@ +{% set tuntap_r = session.get('tuntap_read') %} +{% set tuntap_w = session.get('tuntap_write') %} +{% set tcpudp_r = session.get('tcpudp_read') %} +{% set tcpudp_w = session.get('tcpudp_write') %} +{% set auth_r = session.get('auth_read') %} +{{ tuntap_r }} ({{ tuntap_r | get_naturalsize }}) +{{ tuntap_w }} ({{ tuntap_w | get_naturalsize }}) +{{ tcpudp_r }} ({{ tcpudp_r | get_naturalsize }}) +{{ tcpudp_w }} ({{ tcpudp_w | get_naturalsize }}) +{{ auth_r }} ({{ auth_r | get_naturalsize }}) diff --git a/openvpn_monitor/templates/content.html b/openvpn_monitor/templates/content.html new file mode 100644 index 0000000..f1bc573 --- /dev/null +++ b/openvpn_monitor/templates/content.html @@ -0,0 +1,4 @@ +{% extends "base.html" %} +{% block content %} + +{% endblock %} diff --git a/openvpn_monitor/templates/css.html b/openvpn_monitor/templates/css.html new file mode 100644 index 0000000..cf57bf4 --- /dev/null +++ b/openvpn_monitor/templates/css.html @@ -0,0 +1,20 @@ +{% block css %} + + + +{% if enable_maps %} + + +{% endif %} + +{% endblock %} diff --git a/openvpn_monitor/templates/js.html b/openvpn_monitor/templates/js.html new file mode 100644 index 0000000..4bc1a09 --- /dev/null +++ b/openvpn_monitor/templates/js.html @@ -0,0 +1,19 @@ +{% block js %} + + + + + + + +{% if enable_maps %} + + + +{% endif %} +{% endblock %} diff --git a/openvpn_monitor/templates/maps.html b/openvpn_monitor/templates/maps.html new file mode 100644 index 0000000..970a4ee --- /dev/null +++ b/openvpn_monitor/templates/maps.html @@ -0,0 +1,11 @@ +{% if enable_maps %} +
+
+

Map View

+
+
+
+ +
+
+{% endif %} diff --git a/openvpn_monitor/templates/maps.js b/openvpn_monitor/templates/maps.js new file mode 100644 index 0000000..b137bdc --- /dev/null +++ b/openvpn_monitor/templates/maps.js @@ -0,0 +1,34 @@ +var map = L.map("map_canvas", { fullscreenControl: true, fullscreenControlOptions: { position: "topleft" } }); +var centre = L.latLng({{ latitude }}, {{ longitude }}); +map.setView(centre, 8); +url = "https://{s}.tile.openstreetmap.org/{z}/{x}/{y}.png"; +var layer = new L.TileLayer(url, {}); +map.addLayer(layer); +var bounds = L.latLngBounds(centre); +var oms = new OverlappingMarkerSpiderfier (map,{keepSpiderfied:true}); +var popup = new L.Popup({closeButton:false, offset:new L.Point(0.5,-24)}); +oms.addListener("click", function(marker) { + popup.setContent(marker.alt); + popup.setLatLng(marker.getLatLng()); + map.openPopup(popup); +}); +oms.addListener("spiderfy", function(markers) { + map.closePopup(); +}); +{% for _, vpn in vpns -%} + {% if vpn.get('sessions') -%} + bounds.extend(centre); + {% for _, session in vpn.get('sessions').items() if session.get('local_ip') -%} + {% if session.get('latitude') and session.get('longitude') -%} + var latlng = new L.latLng({{ session.get('latitude') }}, {{ session.get('longitude') }}); +bounds.extend(latlng); +var client_marker = L.marker(latlng).addTo(map); +oms.addMarker(client_marker); +var client_popup = L.popup().setLatLng(latlng); +client_popup.setContent("{{ session.get('username') }} - {{ session.get('remote_ip') }}"); +client_marker.bindPopup(client_popup); +map.fitBounds(bounds) + {% endif -%} + {% endfor -%} + {% endif -%} +{% endfor -%} diff --git a/openvpn_monitor/templates/navbar.html b/openvpn_monitor/templates/navbar.html new file mode 100644 index 0000000..4749ba3 --- /dev/null +++ b/openvpn_monitor/templates/navbar.html @@ -0,0 +1,33 @@ + diff --git a/openvpn_monitor/templates/server_session.html b/openvpn_monitor/templates/server_session.html new file mode 100644 index 0000000..09cb743 --- /dev/null +++ b/openvpn_monitor/templates/server_session.html @@ -0,0 +1,49 @@ +{% set connected_since = session.get('connected_since').strftime(datetime_format) %} +{% set last_seen = session.get('last_seen').strftime(datetime_format) %} +{% set total_connected_time = session.get('connected_since') | get_total_connected_time %} +{% set bytes_recv = session.get('bytes_recv') %} +{% set bytes_sent = session.get('bytes_sent') %} +{% set username = session.get('username') %} +{% set local_ip = session.get('local_ip') %} +{% set remote_ip = session.get('remote_ip') %} +{% set port = session.get('port') %} +{% set client_id = session.get('client_id') %} +{% set location = session.get('location') %} +{% set full_location = session | get_full_location %} +{% set flag = session | get_flag %} +{{ username }} +{{ local_ip }} +{{ remote_ip }} +{% if location %} +{{ full_location }} {{ full_location }} +{% else %} +Unknown +{% endif %} +{{ bytes_recv }} ({{ bytes_recv | get_naturalsize }}) +{{ bytes_sent }} ({{ bytes_sent | get_naturalsize }}) +{{ connected_since }} +{% if session.get('last_seen') %} +{{ last_seen }} +{% else %} +Unknown +{% endif %} +{{ total_connected_time }} +{% if show_disconnect %} + +
+ + + {% if remote_ip is defined and port is defined %} + + + {% endif %} + {% if client_id is defined %} + + {% endif %} + +
+ +{% endif %} diff --git a/openvpn_monitor/templates/sessions.html b/openvpn_monitor/templates/sessions.html new file mode 100644 index 0000000..9d48ee5 --- /dev/null +++ b/openvpn_monitor/templates/sessions.html @@ -0,0 +1,26 @@ +
+ + + + {% set headers = vpn_mode | get_session_headers %} + {% if vpn_mode == 'Server' and show_disconnect %} + {% set headers = headers + ['Action'] %} + {% endif %} + {% for header in headers %} + {% if header == 'Time Online' %} + {% endfor %} + + + + {% for _, session in sessions %} + + {% if vpn_mode == 'Client' %} + {% include 'client_session.html' %} + {% elif vpn_mode == 'Server' and session.get('local_ip') %} + {% include 'server_session.html' %} + {% endif %} + + {% endfor %} + +
{% else %}{% endif %}{{ header }}
+
diff --git a/openvpn_monitor/templates/unavailable_vpn.html b/openvpn_monitor/templates/unavailable_vpn.html new file mode 100644 index 0000000..d455c0f --- /dev/null +++ b/openvpn_monitor/templates/unavailable_vpn.html @@ -0,0 +1,8 @@ +
+
+

{{ vpn['name'] }}

+
+
+ Could not connect to {{ vpn | get_vpn_error }} +
+
diff --git a/openvpn_monitor/templates/vpn_info.html b/openvpn_monitor/templates/vpn_info.html new file mode 100644 index 0000000..3e8cd62 --- /dev/null +++ b/openvpn_monitor/templates/vpn_info.html @@ -0,0 +1,62 @@ +{% if vpn.get('state').get('success') == 'SUCCESS' %} + {% set pingable = 'Yes' %} +{% else %} + {% set pingable = 'No' %} +{% endif %} +{% set connection = vpn.get('state').get('connected') %} +{% set nclients = vpn.get('stats').get('nclients')|int %} +{% set bytesin = vpn.get('stats').get('bytesin')|int %} +{% set bytesout = vpn.get('stats').get('bytesout')|int %} +{% set vpn_mode = vpn.get('state').get('mode') %} +{% set sessions = vpn.get('sessions', {}).items() %} +{% set local_ip = vpn.get('state').get('local_ip') %} +{% set remote_ip = vpn.get('state').get('remote_ip') %} +{% set up_since = vpn.get('state').get('up_since') %} +{% set show_disconnect = vpn.get('show_disconnect') %} +{% set vpn_version = vpn.get('release') %} + +
+
+

{{ vpn.get('name') }}

+
+
+
+ + + + + + + + + + + + {% if vpn_mode == 'Client' %} + + {% endif %} + + + + + + + + + + + + + {% if vpn_mode == 'Client' %} + + {% endif %} + + +
VPN ModeStatusPingableClientsTotal Bytes InTotal Bytes OutUp SinceLocal IP AddressRemote IP Address
{{ vpn_mode }}{{ connection }}{{ pingable }}{{ nclients }}{{ bytesin }} ({{ bytesin | get_naturalsize }}){{ bytesout }} ({{ bytesin | get_naturalsize }}){{ up_since.strftime(datetime_format) }}{{ local_ip }}{{ remote_ip }}
+
+ {% if vpn_mode == 'Client' or nclients > 0 %} + {% include 'sessions.html' %} + {% endif %} +
+ +
diff --git a/openvpn_monitor/util/__init__.py b/openvpn_monitor/util/__init__.py new file mode 100644 index 0000000..cee4a60 --- /dev/null +++ b/openvpn_monitor/util/__init__.py @@ -0,0 +1,36 @@ +#!/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 logging +from datetime import datetime + + +def get_date(date_string, uts=False): + if not uts: + return datetime.strptime(date_string, '%a %b %d %H:%M:%S %Y') + else: + return datetime.fromtimestamp(float(date_string)) + + +def is_truthy(s): + return s in ['True', 'true', 'Yes', 'yes', True] + + +def multiline_info_log(s): + for line in s.splitlines(): + logging.info(line) diff --git a/openvpn_monitor/vpns/openvpn/data_collector.py b/openvpn_monitor/vpns/openvpn/data_collector.py new file mode 100755 index 0000000..2c9baba --- /dev/null +++ b/openvpn_monitor/vpns/openvpn/data_collector.py @@ -0,0 +1,242 @@ +#!/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 logging +import re +import semver +import string +from collections import deque +from ipaddress import ip_address +from geoip2.errors import AddressNotFoundError +from pprint import pformat +from util import get_date +from vpns.openvpn.management_connection import ManagementConnection + + +class VPNDataCollector(object): + + def __init__(self, vpns, gi, **kwargs): + self.vpns = vpns + for _, vpn in list(self.vpns.items()): + self.collect_data(vpn, gi) + + def collect_data(self, vpn, gi): + connection = ManagementConnection(vpn) + connection.connect() + if connection.is_connected(): + full_version = connection.send_command('version') + release = self.parse_version(full_version) + version = semver.parse_version_info(release.split(' ')[1]) + vpn['release'] = release + vpn['version'] = version + state = connection.send_command('state') + vpn['state'] = self.parse_state(state) + stats = connection.send_command('load-stats') + vpn['stats'] = self.parse_stats(stats) + status = connection.send_command('status 3') + vpn['sessions'] = self.parse_status(status, version, gi) + connection.disconnect() + + @staticmethod + def parse_state(data): + state = {} + for line in data.splitlines(): + parts = line.split(',') + logging.debug(f'=== begin split line\n{parts}\n=== end split line') + if parts[0].startswith('>INFO') or \ + parts[0].startswith('END') or \ + parts[0].startswith('>CLIENT'): + continue + else: + state['up_since'] = get_date(date_string=parts[0], uts=True) + state['connected'] = parts[1] + state['success'] = parts[2] + if parts[3]: + state['local_ip'] = ip_address(parts[3]) + else: + state['local_ip'] = '' + if parts[4]: + state['remote_ip'] = ip_address(parts[4]) + state['mode'] = 'Client' + else: + state['remote_ip'] = '' + state['mode'] = 'Server' + return state + + @staticmethod + def parse_stats(data): + stats = {} + line = re.sub('SUCCESS: ', '', data) + parts = line.split(',') + logging.debug(f'=== begin split line\n{parts}\n=== end split line') + stats['nclients'] = int(re.sub('nclients=', '', parts[0])) + stats['bytesin'] = int(re.sub('bytesin=', '', parts[1])) + stats['bytesout'] = int(re.sub('bytesout=', '', parts[2]).replace('\r\n', '')) + return stats + + def parse_status(self, data, version, gi): + client_section = False + routes_section = False + sessions = {} + client_session = {} + + for line in data.splitlines(): + parts = deque(line.split('\t')) + logging.debug(f'=== begin split line\n{parts}\n=== end split line') + + if parts[0].startswith('END'): + break + if parts[0].startswith('TITLE') or \ + parts[0].startswith('GLOBAL') or \ + parts[0].startswith('TIME'): + continue + if parts[0] == 'HEADER': + if parts[1] == 'CLIENT_LIST': + client_section = True + routes_section = False + if parts[1] == 'ROUTING_TABLE': + client_section = False + routes_section = True + continue + + if parts[0].startswith('TUN') or \ + parts[0].startswith('TCP') or \ + parts[0].startswith('Auth'): + parts = parts[0].split(',') + if parts[0] == 'TUN/TAP read bytes': + client_session['tuntap_read'] = int(parts[1]) + continue + if parts[0] == 'TUN/TAP write bytes': + client_session['tuntap_write'] = int(parts[1]) + continue + if parts[0] == 'TCP/UDP read bytes': + client_session['tcpudp_read'] = int(parts[1]) + continue + if parts[0] == 'TCP/UDP write bytes': + client_session['tcpudp_write'] = int(parts[1]) + continue + if parts[0] == 'Auth read bytes': + client_session['auth_read'] = int(parts[1]) + sessions['Client'] = client_session + continue + + if client_section: + session = {} + parts.popleft() + common_name = parts.popleft() + remote_str = parts.popleft() + if remote_str.count(':') == 1: + remote, port = remote_str.split(':') + elif '(' in remote_str: + remote, port = remote_str.split('(') + port = port[:-1] + else: + remote = remote_str + port = None + remote_ip = ip_address(remote) + session['remote_ip'] = remote_ip + if port: + session['port'] = int(port) + else: + session['port'] = '' + if session['remote_ip'].is_private: + session['location'] = 'RFC1918' + elif session['remote_ip'].is_loopback: + session['location'] = 'loopback' + else: + try: + if gi: + gir = gi.city(str(session.get('remote_ip'))) + session['location'] = gir.country.iso_code + session['region'] = gir.subdivisions.most_specific.iso_code + session['city'] = gir.city.name + session['country'] = gir.country.name + session['longitude'] = gir.location.longitude + session['latitude'] = gir.location.latitude + except AddressNotFoundError as e: + logging.warning(e) + except SystemError: + pass + local_ipv4 = parts.popleft() + if local_ipv4: + session['local_ip'] = ip_address(local_ipv4) + else: + session['local_ip'] = '' + if version.major >= 2 and version.minor >= 4: + local_ipv6 = parts.popleft() + if local_ipv6: + session['local_ip'] = ip_address(local_ipv6) + session['bytes_recv'] = int(parts.popleft()) + session['bytes_sent'] = int(parts.popleft()) + parts.popleft() + session['connected_since'] = get_date(parts.popleft(), uts=True) + username = parts.popleft() + if username != 'UNDEF': + session['username'] = username + else: + session['username'] = common_name + if version.major == 2 and version.minor >= 4: + session['client_id'] = parts.popleft() + session['peer_id'] = parts.popleft() + sessions[str(session['local_ip'])] = session + + if routes_section: + local_ip = parts[1] + remote_ip = parts[3] + last_seen = get_date(parts[5], uts=True) + if sessions.get(local_ip): + sessions[local_ip]['last_seen'] = last_seen + elif self.is_mac_address(local_ip): + matching_local_ips = [sessions[s]['local_ip'] + for s in sessions if remote_ip == + self.get_remote_address(sessions[s]['remote_ip'], sessions[s]['port'])] + if len(matching_local_ips) == 1: + local_ip = f'{matching_local_ips[0]}' + if sessions[local_ip].get('last_seen'): + prev_last_seen = sessions[local_ip].get('last_seen') + if prev_last_seen < last_seen: + sessions[local_ip]['last_seen'] = last_seen + else: + sessions[local_ip]['last_seen'] = last_seen + + if sessions: + pretty_sessions = pformat(sessions) + logging.debug(f'=== begin sessions\n{pretty_sessions}\n=== end sessions') + else: + logging.debug('no sessions found') + + return sessions + + @staticmethod + def parse_version(data): + for line in data.splitlines(): + if line.startswith('OpenVPN'): + return line.replace('OpenVPN Version: ', '') + + @staticmethod + def is_mac_address(s): + return len(s) == 17 and \ + len(s.split(':')) == 6 and \ + all(c in string.hexdigits for c in s.replace(':', '')) + + @staticmethod + def get_remote_address(ip, port): + if port: + return f'{ip}:{port}' + else: + return f'{ip}' diff --git a/openvpn_monitor/vpns/openvpn/disconnector.py b/openvpn_monitor/vpns/openvpn/disconnector.py new file mode 100644 index 0000000..a897098 --- /dev/null +++ b/openvpn_monitor/vpns/openvpn/disconnector.py @@ -0,0 +1,59 @@ +#!/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 logging +from ipaddress import ip_address +from vpns.openvpn.management_connection import ManagementConnection + + +class VPNDisconnector(object): + + def __init__(self, vpns, **kwargs): + self.vpns = dict(vpns) + self.check_disconnects(**kwargs) + + def check_disconnects(self, **kwargs): + vpn_id = kwargs.get('vpn_id') + if vpn_id: + vpn = self.vpns[vpn_id] + disconnection_allowed = vpn.get('show_disconnect') + if disconnection_allowed: + self.disconnect_client(vpn, **kwargs) + + def disconnect_client(self, vpn, **kwargs): + connection = ManagementConnection(vpn) + connection.connect() + if connection.is_connected(): + name = vpn.get('name') + version = vpn.get('version') + command = False + client_id = None + if kwargs.get('client_id'): + client_id = kwargs.get('client_id') + if client_id and version.major == 2 and version.minor >= 4: + logging.info(f'[{name}] Disconnecting client id `{client_id}`') + command = f'client-kill {client_id}' + else: + ip = ip_address(kwargs.get('ip')) + port = kwargs.get('port') + if ip and port: + logging.info(f'[{name}] Disconnecting client `{ip}:{port}`') + command = f'kill {ip}:{port}' + if command: + connection.send_command(command) + connection.disconnect() diff --git a/openvpn_monitor/vpns/openvpn/management_connection.py b/openvpn_monitor/vpns/openvpn/management_connection.py new file mode 100755 index 0000000..4b405a8 --- /dev/null +++ b/openvpn_monitor/vpns/openvpn/management_connection.py @@ -0,0 +1,156 @@ +#!/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 logging +import re +import socket +import ssl + + +class ManagementConnection(object): + + def __init__(self, vpn_config): + self.__vpn_config = vpn_config + self.__name = self.__vpn_config.get('name') + self.__timeout = 3 + self.__error = False + self.__socket = False + self.__vpn_config['management_connection_successful'] = False + + def is_connected(self): + return self.__socket + + def connect(self): + try: + self.__connect() + self.__authenticate() + self.__vpn_config['management_connection_successful'] = True + except socket.timeout as e: + self.__handle_connect_error(e, 'socket timeout') + except socket.error as e: + self.__handle_connect_error(e, 'socket error') + except ssl.SSLError as e: + self.__handle_connect_error(e, 'ssl error') + except Exception as e: + self.__handle_connect_error(e, 'unexpected error') + + def __handle_connect_error(self, error, msg): + self.__error = error + self.__vpn_config['error'] = self.__error + self.__close() + logging.warning(f'{msg}: {error}') + + def send_command(self, command): + logging.info(f'[{self.__name}] Sending openvpn management command: `{command.rstrip()}`') + self.__send(f'{command}\n') + if command.startswith('kill') or command.startswith('client-kill'): + return + return self.__wait_for_data(command=command) + + def __wait_for_data(self, password=None, command=None): + data = '' + while 1: + try: + socket_data = self.__recv(1024) + except TimeoutError: + logging.error(f'[{self.__name}] Timeout receiving data') + break + socket_data = re.sub('>INFO(.)*\r\n', '', socket_data) + data += socket_data + if data.endswith('ENTER PASSWORD:'): + if password: + self.__send(f'{password}\n') + else: + logging.warning(f'[{self.__name}] Password requested but no password supplied by configuration') + if data.endswith('SUCCESS: password is correct\r\n'): + break + if command == 'load-stats' and data != '': + break + if command == 'quit': + break + elif data.endswith("\nEND\r\n"): + break + logging.debug(f'[{self.__name}] === begin raw data\n{data}\n=== end raw data') + return data + + def __send(self, data): + self.__socket.send(bytes(data, 'utf-8')) + + def __recv(self, length): + return self.__socket.recv(length).decode('utf-8') + + def disconnect(self): + if self.__socket: + self.send_command('quit') + self.__close() + + def __close(self): + if self.__socket: + self.__socket.shutdown(socket.SHUT_RDWR) + self.__socket.close() + self.__socket = False + + def __authenticate(self): + if (self.__vpn_config.get('password')): + self.__wait_for_data(password=self.__vpn_config.get('password')) + + def __connect(self): + if self.__is_tls_socket(): + self.__connect_tls() + elif self.__is_tcp_socket(): + self.__connect_tcp() + elif self.__is_unix_socket(): + self.__connect_unix() + else: + raise Exception('Unknown socket type') + + def __is_tls_socket(self): + return self.__vpn_config.get('host') and self.__vpn_config.get('ssl') + + def __connect_tls(self): + logging.info(f'[{self.__name}] Initiating TLS socket connection') + context = self.__create_tls_context() + self.__connect_tcp() + self.__socket = context.wrap_socket(self.__socket) + + def __create_tls_context(self): + context = ssl.SSLContext(ssl.PROTOCOL_TLS_CLIENT) + context.options |= ssl.OP_NO_TLSv1 + context.options |= ssl.OP_NO_TLSv1_1 + if (self.__vpn_config.get('ssl') == 'any-cert'): + context.check_hostname = False + context.verify_mode = ssl.CERT_NONE + return context + + def __is_tcp_socket(self): + return self.__vpn_config.get('host') and not self.__vpn_config.get('ssl') + + def __connect_tcp(self): + host = self.__vpn_config['host'] + port = int(self.__vpn_config['port']) + logging.info(f'[{self.__name}] Initiating TCP socket connection to {host}:{port}') + self.__socket = socket.create_connection((host, port), self.__timeout) + + def __is_unix_socket(self): + return bool(self.__vpn_config.get('socket')) + + def __connect_unix(self): + self.__socket = socket.socket(socket.AF_UNIX, socket.SOCK_STREAM) + unix_socket = self.__vpn_config['socket'] + logging.info(f'[{self.__name}] Initiating UNIX socket connection to {unix_socket}') + self.__socket.connect(unix_socket) diff --git a/package.json b/package.json new file mode 100644 index 0000000..fcd3845 --- /dev/null +++ b/package.json @@ -0,0 +1,15 @@ +{ + "name": "openvpn-monitor", + "version": "2.0.0", + "description": "OpenVPN Monitor", + "repository": "https//github.com/furlongm/openvpn-monitor", + "author": "Marcus Furlong ", + "license": "GPL-3.0", + "dependencies": { + "bootstrap3": "^3.3.5", + "leaflet": "^1.9.4", + "leaflet.fullscreen": "^3.0.2", + "overlapping-marker-spiderfier-leaflet": "^0.2.7", + "tablesorter": "^2.32.0" + } +}