mirror of
https://codeberg.org/prof_x_pvt_ltd/captive.whump.shanti-portal
synced 2024-08-14 22:46:42 +00:00
271 lines
7.7 KiB
Python
271 lines
7.7 KiB
Python
#!/usr/bin/env python
|
|
# Python helper tool to add portal clients into ipset and DB via CLI.
|
|
#
|
|
# by Stefan Midjich
|
|
|
|
from os import getuid
|
|
from sys import exit
|
|
from argparse import ArgumentParser, FileType, ArgumentTypeError
|
|
from pprint import pprint as pp
|
|
from configparser import RawConfigParser
|
|
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
|
|
|
|
|
|
DATETIME_FORMAT = '%Y-%m-%d.%H:%M'
|
|
|
|
# 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:
|
|
return datetime.strptime(arg_date_str, "%Y-%m-%d")
|
|
except ValueError:
|
|
msg = "Given Date ({0}) not valid! Expected format, YYYY-MM-DD!".format(arg_date_str)
|
|
raise ArgumentTypeError(msg)
|
|
|
|
|
|
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)
|
|
except ValueError:
|
|
msg = (
|
|
'Given Datetime ({arg_dt}) not valid! '
|
|
'Expected format, "{dt_format}"!'
|
|
).format(
|
|
dt_format=DATETIME_FORMAT,
|
|
arg_dt=arg_datetime_str
|
|
)
|
|
raise ArgumentTypeError(msg)
|
|
|
|
|
|
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'
|
|
)
|
|
|
|
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
|
|
)
|
|
)
|
|
|
|
parser.add_argument(
|
|
'--disable',
|
|
default=False,
|
|
action='store_true',
|
|
help='Disable the client in the DB and delete from firewall'
|
|
)
|
|
|
|
parser.add_argument(
|
|
'--delete',
|
|
default=False,
|
|
action='store_true',
|
|
help='Delete the client from DB and firewall. Normally this is excessive.'
|
|
)
|
|
|
|
parser.add_argument(
|
|
'--refresh',
|
|
action='store_true',
|
|
help='Refresh client ipset data first'
|
|
)
|
|
|
|
parser.add_argument(
|
|
'--config',
|
|
type=FileType('r'),
|
|
required=True,
|
|
help='Configuration file'
|
|
)
|
|
|
|
parser.add_argument(
|
|
'src_ip',
|
|
nargs='*',
|
|
help='Client source IP to add'
|
|
)
|
|
|
|
args = parser.parse_args()
|
|
|
|
config = RawConfigParser()
|
|
config.readfp(args.config)
|
|
|
|
sr = StoragePostgres(config=config)
|
|
|
|
if getuid() == 0:
|
|
use_sudo = False
|
|
else:
|
|
use_sudo = True
|
|
|
|
# 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.
|
|
if args.refresh:
|
|
proc = run_ipset(
|
|
'list',
|
|
config.get('portalclient', 'ipset_name'),
|
|
'-output',
|
|
'save',
|
|
use_sudo=use_sudo,
|
|
timeout=600
|
|
)
|
|
|
|
current_time = datetime.now()
|
|
|
|
for _line in proc.splitlines():
|
|
# Convert from bytestring first
|
|
line = _line.decode('utf-8')
|
|
expired = False
|
|
|
|
if not line.startswith('add'):
|
|
continue
|
|
|
|
(
|
|
cmd,
|
|
set_name,
|
|
client_ip,
|
|
packets_str,
|
|
packets_val,
|
|
bytes_str,
|
|
bytes_val
|
|
) = line.split()
|
|
|
|
# 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'),
|
|
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)
|
|
))
|
|
|
|
if args.verbose > 2:
|
|
raise
|
|
|
|
continue
|
|
|
|
# Set the expired variable for future processing if the client has
|
|
# has expired.
|
|
if current_time > client.expires:
|
|
expired = True
|
|
|
|
# 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.
|
|
if client._new:
|
|
if args.verbose > 1:
|
|
print('Client:{ip} created'.format(
|
|
ip=client.ip_address
|
|
))
|
|
client.enabled = True
|
|
|
|
# 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
|
|
))
|
|
client.enabled = True
|
|
client.expires = current_time
|
|
|
|
# If we have more packets in ipset output now than we had in DB we
|
|
# consider that an active client.
|
|
if int(packets_val) > client.last_packets:
|
|
if args.verbose > 1:
|
|
print('Client:{ip} updated'.format(
|
|
ip=client.ip_address
|
|
))
|
|
|
|
client.last_packets = packets_val
|
|
client.last_activity = current_time
|
|
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
|
|
))
|
|
|
|
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
|
|
))
|
|
|
|
client.enabled = False
|
|
|
|
# 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.
|
|
if client.last_activity and expired:
|
|
active_diff = current_time - client.last_activity
|
|
|
|
if active_diff.days >= 1:
|
|
if args.verbose:
|
|
print('Client:{ip} disabled, no activity since "{last_activity}"'.format(
|
|
last_activity=client.last_activity,
|
|
ip=client.ip_address
|
|
))
|
|
|
|
client.enabled = False
|
|
|
|
client.commit()
|
|
|
|
for src_ip in args.src_ip:
|
|
# Get client by IP or create a new one.
|
|
client = Client(
|
|
storage=sr,
|
|
ip_address=src_ip,
|
|
ipset_name=config.get('portalclient', 'ipset_name'),
|
|
use_sudo=use_sudo
|
|
)
|
|
|
|
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
|
|
|
|
if args.expires:
|
|
client.expires = args.expires
|
|
else:
|
|
client.expires = datetime.now + timedelta(days=1)
|
|
|
|
client.commit()
|