Skip to content

Commit

Permalink
Merge pull request #292 from furlongm/flask
Browse files Browse the repository at this point in the history
switch from bottle to flask
  • Loading branch information
furlongm authored Dec 13, 2024
2 parents fa4b2b7 + b3049c2 commit 2e9222c
Show file tree
Hide file tree
Showing 3 changed files with 116 additions and 122 deletions.
161 changes: 76 additions & 85 deletions openvpn-monitor.py
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@

import argparse
import configparser
import logging
import os
import re
import semver
Expand All @@ -32,6 +33,9 @@
from geoip2.errors import AddressNotFoundError
from pprint import pformat

logging.basicConfig(stream=sys.stderr, format='%(asctime)s %(levelname)s %(message)s')
logging.getLogger().setLevel(logging.INFO)


def output(s):
global wsgi, wsgi_output
Expand All @@ -41,29 +45,13 @@ def output(s):
wsgi_output += s


def info(*objs):
print('INFO:', *objs, file=sys.stderr)


def warning(*objs):
print('WARNING:', *objs, file=sys.stderr)


def debug(*objs):
print('DEBUG\n', *objs, file=sys.stderr)


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 get_str(s):
return s


def is_truthy(s):
return s in ['True', 'true', 'Yes', 'yes', True]

Expand All @@ -77,7 +65,7 @@ def __init__(self, config_file):
contents = config.read(config_file)

if not contents and config_file == './openvpn-monitor.conf':
warning(f'Config file does not exist or is unreadable: {config_file}')
logging.warning(f'Config file does not exist or is unreadable: {config_file}')
if sys.prefix == '/usr':
conf_path = '/etc/'
else:
Expand All @@ -86,9 +74,9 @@ def __init__(self, config_file):
contents = config.read(config_file)

if contents:
info(f'Using config file: {config_file}')
logging.info(f'Using config file: {config_file}')
else:
warning(f'Config file does not exist or is unreadable: {config_file}')
logging.warning(f'Config file does not exist or is unreadable: {config_file}')
self.load_default_settings()

for section in config.sections():
Expand All @@ -98,7 +86,7 @@ def __init__(self, config_file):
self.parse_vpn_section(config, section)

def load_default_settings(self):
info('Using default settings => localhost:5555')
logging.info('Using default settings => localhost:5555')
self.settings = {'site': 'Default Site',
'maps': 'True',
'geoip_data': '/usr/share/GeoIP/GeoLite2-City.mmdb',
Expand All @@ -122,8 +110,7 @@ def parse_global_section(self, config):
pass
except configparser.NoOptionError:
pass
if args.debug:
debug(f'=== begin section\n{self.settings}\n=== end section')
logging.debug(f'=== begin section\n{self.settings}\n=== end section')

def parse_vpn_section(self, config, section):
self.vpns[section] = {}
Expand All @@ -133,13 +120,12 @@ def parse_vpn_section(self, config, section):
try:
vpn[option] = config.get(section, option)
if vpn[option] == -1:
warning(f'CONFIG: skipping {option}')
logging.warning(f'CONFIG: skipping {option}')
except configparser.Error as e:
warning(f'CONFIG: {e} on option {option}: ')
logging.warning(f'CONFIG: {e} on option {option}: ')
vpn[option] = None
vpn['show_disconnect'] = is_truthy(vpn.get('show_disconnect', False))
if args.debug:
debug(f'=== begin section\n{vpn}\n=== end section')
logging.debug(f'=== begin section\n{vpn}\n=== end section')


class OpenvpnMgmtInterface(object):
Expand Down Expand Up @@ -169,8 +155,11 @@ def __init__(self, cfg, **kwargs):
self.send_command(command)
self._socket_disconnect()

geoip_data = cfg.settings['geoip_data']
self.gi = database.Reader(geoip_data)
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)
Expand Down Expand Up @@ -219,18 +208,18 @@ def _socket_connect(self, vpn):
vpn['socket_connected'] = True
except socket.timeout as e:
vpn['error'] = e
warning(f'socket timeout: {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
warning(f'socket error: {e}')
logging.warning(f'socket error: {e}')
vpn['socket_connected'] = False
except Exception as e:
vpn['error'] = e
warning(f'unexpected error: {e}')
logging.warning(f'unexpected error: {e}')
vpn['socket_connected'] = False

def _socket_disconnect(self):
Expand All @@ -239,7 +228,7 @@ def _socket_disconnect(self):
self.s.close()

def send_command(self, command):
info(f'Sending command: {command}')
logging.info(f'Sending command: {command}')
self._socket_send(command)
if command.startswith('kill') or command.startswith('client-kill'):
return
Expand All @@ -255,24 +244,22 @@ def wait_for_data(self, password=None, command=None):
if password:
self._socket_send(f'{password}\n')
else:
warning('password requested but no password supplied by configuration')
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
if args.debug:
debug(f'=== begin raw data\n{data}\n=== end raw data')
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(',')
if args.debug:
debug(f'=== begin split line\n{parts}\n=== end split line')
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'):
Expand All @@ -298,8 +285,7 @@ def parse_stats(data):
stats = {}
line = re.sub('SUCCESS: ', '', data)
parts = line.split(',')
if args.debug:
debug(f'=== begin split line\n{parts}\n=== end split line')
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', ''))
Expand All @@ -314,8 +300,7 @@ def parse_status(self, data, version):

for line in data.splitlines():
parts = deque(line.split('\t'))
if args.debug:
debug(f'=== begin split line\n{parts}\n=== end split line')
logging.debug(f'=== begin split line\n{parts}\n=== end split line')

if parts[0].startswith('END'):
break
Expand Down Expand Up @@ -378,15 +363,16 @@ def parse_status(self, data, version):
session['location'] = 'loopback'
else:
try:
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:
pass
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()
Expand Down Expand Up @@ -431,12 +417,11 @@ def parse_status(self, data, version):
else:
sessions[local_ip]['last_seen'] = last_seen

if args.debug:
if sessions:
pretty_sessions = pformat(sessions)
debug(f'=== begin sessions\n{pretty_sessions}\n=== end sessions')
else:
debug('no sessions')
if sessions:
pretty_sessions = pformat(sessions)
logging.debug(f'=== begin sessions\n{pretty_sessions}\n=== end sessions')
else:
logging.debug('no sessions')

return sessions

Expand Down Expand Up @@ -527,7 +512,7 @@ def print_html_header(self):
output('<script>$(document).ready(function(){')
output('$("table.tablesorter").tablesorter({')
output('sortList: [[0,0]], theme:"bootstrap", headerTemplate:"{content} {icon}", widgets:["uitheme"],')
output('durationLabels : "(?:years|year|y),(?:days|day|d),(?:hours|hour|h),(?:minutes|minute|min|m),(?:seconds|second|sec|s)"')
output('durationLabels : "(?:years|year|y),(?:days|day|d),(?:hours|hour|h),(?:minutes|minute|min|m),(?:seconds|second|sec|s)"') # noqa
output('});')
output('});</script>')
if self.maps:
Expand Down Expand Up @@ -623,7 +608,7 @@ def print_unavailable_vpn(vpn):
elif vpn.get('socket'):
output(f"{vpn['socket']} ({vpn['error']})</div></div>")
else:
warning(f'failed to get socket or network info: {vpn}')
logging.warning(f'failed to get socket or network info: {vpn}')
output('network or unix socket</div></div>')

def print_vpn(self, vpn_id, vpn):
Expand Down Expand Up @@ -811,9 +796,8 @@ def main(**kwargs):
cfg = ConfigLoader(args.config)
monitor = OpenvpnMgmtInterface(cfg, **kwargs)
OpenvpnHtmlPrinter(cfg, monitor)
if args.debug:
pretty_vpns = pformat((dict(monitor.vpns)))
debug(f'=== begin vpns\n{pretty_vpns}\n=== end vpns')
pretty_vpns = pformat((dict(monitor.vpns)))
logging.debug(f'=== begin vpns\n{pretty_vpns}\n=== end vpns')


def get_args():
Expand All @@ -830,49 +814,56 @@ def get_args():

if __name__ == '__main__':
args = get_args()
if args.debug:
logging.getLogger().setLevel(logging.DEBUG)
wsgi = False
main()


def monitor_wsgi():

owd = os.getcwd()
if owd.endswith('site-packages') and sys.prefix != '/usr':
# virtualenv
image_dir = owd + '/../../../share/openvpn-monitor/'
else:
image_dir = ''

app = Bottle()
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)
response.content_type = 'text/html;'
return wsgi_output
return make_response(wsgi_output)

@app.hook('before_request')
@app.before_request
def strip_slash():
request.environ['PATH_INFO'] = request.environ.get('PATH_INFO', '/').rstrip('/')
if args.debug:
debug(pformat(request.environ))

@app.route('/', method='GET')
def get_slash():
return render()

@app.route('/', method='POST')
def post_slash():
vpn_id = request.forms.get('vpn_id')
ip = request.forms.get('ip')
port = request.forms.get('port')
client_id = request.forms.get('client_id')
return render(vpn_id=vpn_id, ip=ip, port=port, client_id=client_id)

@app.route('/<filename:re:.*\.(jpg|png)>', method='GET')
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.forms.get('vpn_id')
ip = request.forms.get('ip')
port = request.forms.get('port')
client_id = request.forms.get('client_id')
return render(vpn_id=vpn_id, ip=ip, port=port, client_id=client_id)

@app.route('/images/flags/<filename>', methods=['GET'])
def get_images(filename):
return static_file(filename, image_dir)
logging.debug(pformat(request.environ))
return send_from_directory(image_dir + 'images/flags', filename)

return app

Expand All @@ -883,7 +874,7 @@ def get_images(filename):
if __file__ != 'openvpn-monitor.py':
os.chdir(os.path.dirname(__file__))
sys.path.append(os.path.dirname(__file__))
from bottle import Bottle, response, request, static_file
from flask import Flask, request, redirect, make_response, send_from_directory

class args(object):
debug = False
Expand Down
2 changes: 1 addition & 1 deletion requirements.txt
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
geoip2==4.7.0
humanize==4.8.0
bottle==0.12.25
Flask==3.0.0
semver==2.13.0
Loading

0 comments on commit 2e9222c

Please sign in to comment.