captive.whump.shanti-portal/tools/manage_client.py

272 lines
7.7 KiB
Python
Raw Normal View History

#!/usr/bin/env python
2017-11-20 21:46:15 +00:00
# Python helper tool to add portal clients into ipset and DB via CLI.
#
# by Stefan Midjich
from os import getuid
2017-03-06 15:03:57 +00:00
from sys import exit
2017-09-29 16:40:57 +00:00
from argparse import ArgumentParser, FileType, ArgumentTypeError
from pprint import pprint as pp
2017-03-03 00:04:12 +00:00
from configparser import RawConfigParser
2017-09-29 15:56:16 +00:00
from datetime import datetime, timedelta
from io import BytesIO
from portalclientlib.client import Client
from portalclientlib.storage import StoragePostgres
from portalclientlib.helpers import run_ipset
2017-09-29 15:44:40 +00:00
DATETIME_FORMAT = '%Y-%m-%d.%H:%M'
2017-09-29 15:44:40 +00:00
# Custom defined argparse types for dates
def valid_date_type(arg_date_str):
"""custom argparse *date* type for user dates values given from the command line"""
try:
2017-09-29 16:40:57 +00:00
return datetime.strptime(arg_date_str, "%Y-%m-%d")
2017-09-29 15:44:40 +00:00
except ValueError:
msg = "Given Date ({0}) not valid! Expected format, YYYY-MM-DD!".format(arg_date_str)
2017-09-29 16:40:57 +00:00
raise ArgumentTypeError(msg)
2017-09-29 15:44:40 +00:00
def valid_datetime_type(arg_datetime_str):
"""custom argparse type for user datetime values given from the command line"""
try:
return datetime.strptime(arg_datetime_str, DATETIME_FORMAT)
2017-09-29 15:44:40 +00:00
except ValueError:
msg = (
'Given Datetime ({arg_dt}) not valid! '
'Expected format, "{dt_format}"!'
).format(
dt_format=DATETIME_FORMAT,
arg_dt=arg_datetime_str
)
2017-11-18 20:33:37 +00:00
raise ArgumentTypeError(msg)
2017-09-29 15:44:40 +00:00
parser = ArgumentParser(
'''
Handle clients in the captive portal. Default mode of operation is to
create new clients and enable them.
'''
)
parser.add_argument(
'-v', '--verbose',
action='count',
default=False,
dest='verbose',
help='Verbose output, use more v\'s to increase verbosity'
)
2017-09-29 15:44:40 +00:00
parser.add_argument(
'--expires',
type=valid_datetime_type,
default=datetime.now() + timedelta(days=1),
help='Expiry date in format "{dt_format}"'.format(
dt_format=DATETIME_FORMAT
)
2017-09-29 15:44:40 +00:00
)
parser.add_argument(
'--disable',
default=False,
2017-03-07 16:21:52 +00:00
action='store_true',
help='Disable the client in the DB and delete from firewall'
)
parser.add_argument(
'--delete',
default=False,
2017-03-07 16:21:52 +00:00
action='store_true',
2017-11-20 21:46:15 +00:00
help='Delete the client from DB and firewall. Normally this is excessive.'
)
2017-03-03 00:04:12 +00:00
parser.add_argument(
'--refresh',
action='store_true',
help='Refresh client ipset data first'
2017-03-03 00:04:12 +00:00
)
parser.add_argument(
'--config',
type=FileType('r'),
required=True,
help='Configuration file'
)
parser.add_argument(
'src_ip',
nargs='*',
2017-03-03 00:04:12 +00:00
help='Client source IP to add'
)
args = parser.parse_args()
2017-03-03 00:04:12 +00:00
config = RawConfigParser()
config.readfp(args.config)
2017-03-06 15:03:57 +00:00
sr = StoragePostgres(config=config)
2017-11-18 23:45:24 +00:00
if getuid() == 0:
use_sudo = False
else:
use_sudo = True
2017-11-20 21:46:15 +00:00
# Sync clients and packet counters from ipset into storage.
# This is a long process that ensures ipset and the DB have the same clients.
# Also that old clients are disabled and that active clients have fresh data
# in DB showing they're active.
2017-11-18 23:45:24 +00:00
if args.refresh:
proc = run_ipset(
'list',
config.get('portalclient', 'ipset_name'),
'-output',
'save',
use_sudo=use_sudo,
timeout=600
)
2017-11-18 20:33:37 +00:00
current_time = datetime.now()
for _line in proc.splitlines():
# Convert from bytestring first
line = _line.decode('utf-8')
2017-11-18 20:33:37 +00:00
expired = False
if not line.startswith('add'):
continue
(
cmd,
set_name,
client_ip,
packets_str,
packets_val,
bytes_str,
bytes_val
) = line.split()
2017-11-20 21:46:15 +00:00
# Init a client instance, this will fetch an existing client matching
# the IP from DB and load its data. Or create a new with default
# values.
try:
client = Client(
storage=sr,
ip_address=client_ip,
ipset_name=config.get('portalclient', 'ipset_name'),
2017-11-16 22:56:15 +00:00
use_sudo=use_sudo
)
except Exception as e:
if args.verbose:
print('Failed to init client:{ip}: {error}'.format(
ip=client_ip,
error=str(e)
))
2017-11-18 20:33:37 +00:00
if args.verbose > 2:
raise
continue
2017-11-20 21:46:15 +00:00
# Set the expired variable for future processing if the client has
# has expired.
2017-11-18 20:33:37 +00:00
if current_time > client.expires:
expired = True
2017-11-20 21:46:15 +00:00
# The _new attribute is only set on brand new clients not previously
# in DB. This will get less and less common as DB builds up.
2017-11-18 20:33:37 +00:00
if client._new:
2017-11-20 22:06:52 +00:00
if args.verbose > 1:
print('Client:{ip} created'.format(
ip=client.ip_address
2017-11-16 20:53:22 +00:00
))
2017-11-16 21:26:57 +00:00
client.enabled = True
2017-11-18 20:33:37 +00:00
2017-11-20 21:46:15 +00:00
# In this case the client exists in ipset but is disabled in DB so
# we enable it and set a new expired date for it. As if it's a new
# (reset) client.
if not client.enabled:
if args.verbose:
print('Client:{ip} enabled'.format(
ip=client.ip_address
))
2017-11-20 21:46:15 +00:00
client.enabled = True
client.expires = current_time
2017-11-20 21:46:15 +00:00
# If we have more packets in ipset output now than we had in DB we
# consider that an active client.
2017-11-18 20:33:37 +00:00
if int(packets_val) > client.last_packets:
if args.verbose > 1:
2017-11-18 20:33:37 +00:00
print('Client:{ip} updated'.format(
ip=client.ip_address
2017-11-16 20:53:22 +00:00
))
2017-11-18 20:33:37 +00:00
client.last_packets = packets_val
client.last_activity = current_time
2017-11-20 21:46:15 +00:00
elif int(packets_val) < client.last_packets:
# Otherwise this client could have been reset and in that case
# we reset its values. This should only happen once in every
# unique clients life-time.
if args.verbose > 1:
print('Client:{ip} reset'.format(
ip=client.ip_address
2017-11-20 21:46:15 +00:00
))
client.last_packets = packets_val
client.last_activity = current_time
client.expires = datetime.now() + timedelta(days=1)
# Here we handle fringe cases where clients have had no activity
# logged and are expired.
if not client.last_activity and expired:
if args.verbose:
print('Client:{ip} disabled, no activity logged'.format(
ip=client.ip_address
2017-11-20 21:46:15 +00:00
))
client.enabled = False
2017-11-16 20:46:05 +00:00
2017-11-20 21:46:15 +00:00
# If the client has had activity logged but is expired we need to
# ensure that we don't disable active clients by only disabling
# the ones with more than 1 day since their last activity.
2017-11-18 20:33:37 +00:00
if client.last_activity and expired:
active_diff = current_time - client.last_activity
2017-11-20 21:46:15 +00:00
if active_diff.days >= 1:
2017-11-16 20:53:22 +00:00
if args.verbose:
2017-11-18 20:33:37 +00:00
print('Client:{ip} disabled, no activity since "{last_activity}"'.format(
last_activity=client.last_activity,
ip=client.ip_address
2017-11-16 20:53:22 +00:00
))
2017-11-18 20:33:37 +00:00
client.enabled = False
client.commit()
for src_ip in args.src_ip:
# Get client by IP or create a new one.
2017-03-06 15:03:57 +00:00
client = Client(
storage=sr,
ip_address=src_ip,
ipset_name=config.get('portalclient', 'ipset_name'),
2017-11-18 23:45:24 +00:00
use_sudo=use_sudo
2017-03-06 15:03:57 +00:00
)
if args.delete:
# This both deletes the ipset entry AND the client entry from DB. Normally
# excessive and disable is better.
client.delete()
exit(0)
if args.disable:
client.enabled = False
else:
client.enabled = True
2017-11-20 21:46:15 +00:00
if args.expires:
client.expires = args.expires
else:
client.expires = datetime.now + timedelta(days=1)
client.commit()