From 78845fc694da670cfe1a430ff95a838a9d0c11c1 Mon Sep 17 00:00:00 2001 From: Ad Schellevis Date: Wed, 22 May 2024 17:31:43 +0200 Subject: [PATCH] Services: Captive Portal - code cleanup in session handling and presentation. --- .../CaptivePortal/Api/AccessController.php | 3 +- .../CaptivePortal/Api/SessionController.php | 56 +++--- .../views/OPNsense/CaptivePortal/clients.volt | 166 +++++++----------- .../scripts/OPNsense/CaptivePortal/allow.py | 60 +++---- .../OPNsense/CaptivePortal/disconnect.py | 44 ++--- .../scripts/OPNsense/CaptivePortal/lib/db.py | 12 +- .../OPNsense/CaptivePortal/listClients.py | 50 +----- .../CaptivePortal/set_session_restrictions.py | 33 ++-- .../conf/actions.d/actions_captiveportal.conf | 8 +- 9 files changed, 162 insertions(+), 270 deletions(-) diff --git a/src/opnsense/mvc/app/controllers/OPNsense/CaptivePortal/Api/AccessController.php b/src/opnsense/mvc/app/controllers/OPNsense/CaptivePortal/Api/AccessController.php index fc84fdd1079..4c7652d92ad 100644 --- a/src/opnsense/mvc/app/controllers/OPNsense/CaptivePortal/Api/AccessController.php +++ b/src/opnsense/mvc/app/controllers/OPNsense/CaptivePortal/Api/AccessController.php @@ -190,8 +190,7 @@ public function logonAction($zoneid = 0) (string)$cpZone->zoneid, $userName, $clientIp, - $authServerName, - 'json' + $authServerName ] ); $CPsession = json_decode($CPsession, true); diff --git a/src/opnsense/mvc/app/controllers/OPNsense/CaptivePortal/Api/SessionController.php b/src/opnsense/mvc/app/controllers/OPNsense/CaptivePortal/Api/SessionController.php index 5681b3db3fc..94d85716cee 100644 --- a/src/opnsense/mvc/app/controllers/OPNsense/CaptivePortal/Api/SessionController.php +++ b/src/opnsense/mvc/app/controllers/OPNsense/CaptivePortal/Api/SessionController.php @@ -1,7 +1,7 @@ getByZoneID($zoneid); if ($cpZone != null) { - $backend = new Backend(); - $allClientsRaw = $backend->configdpRun( - "captiveportal list_clients", - array($cpZone->zoneid, 'json') - ); - $allClients = json_decode($allClientsRaw ?? '', true); - - return $allClients; + $allClientsRaw = (new Backend())->configdpRun("captiveportal list_clients", [$cpZone->zoneid]); + return json_decode($allClientsRaw ?? '', true); } else { // illegal zone, return empty response - return array(); + return []; } } + /** + * search through connected clients + */ + public function searchAction() + { + $this->sessionClose(); + $selected_zones = $this->request->get('selected_zones'); + $records = json_decode((new Backend())->configdRun("captiveportal list_clients") ?? '', true); + + $response = $this->searchRecordsetBase($records, null, 'userName', function ($key) use ($selected_zones) { + return empty($selected_zones) || in_array($key['zoneid'], $selected_zones); + }); + + return $response; + } + /** * return list of available zones * @return array available zones */ public function zonesAction() { - $response = array(); + $response = []; $mdlCP = new CaptivePortal(); foreach ($mdlCP->zones->zone->iterateItems() as $zone) { $response[(string)$zone->zoneid] = (string)$zone->description; @@ -81,25 +91,24 @@ public function zonesAction() /** * disconnect a client - * @param string|int $zoneid zoneid + * @param string|int $zoneid zoneid (deprecated) * @return array|mixed */ - public function disconnectAction($zoneid = 0) + public function disconnectAction($zoneid = '') { if ($this->request->isPost() && $this->request->hasPost('sessionId')) { - $backend = new Backend(); - $statusRAW = $backend->configdpRun( + $statusRAW = (new Backend())->configdpRun( "captiveportal disconnect", - array($zoneid, $this->request->getPost('sessionId'), 'json') + [$this->request->getPost('sessionId')] ); - $status = json_decode($statusRAW, true); + $status = json_decode($statusRAW ?? '', true); if ($status != null) { return $status; } else { - return array("status" => "Illegal response"); + return ["status" => "Illegal response"]; } } - return array(); + return []; } /** @@ -109,7 +118,7 @@ public function disconnectAction($zoneid = 0) */ public function connectAction($zoneid = 0) { - $response = array(); + $response = []; if ($this->request->isPost()) { // Get details from POST request @@ -136,13 +145,12 @@ public function connectAction($zoneid = 0) $backend = new Backend(); $CPsession = $backend->configdpRun( "captiveportal allow", - array( + [ (string)$cpZone->zoneid, $userName, $clientIp, - 'API', - 'json' - ) + 'API' + ] ); // Only return session if configd returned a valid json response diff --git a/src/opnsense/mvc/app/views/OPNsense/CaptivePortal/clients.volt b/src/opnsense/mvc/app/views/OPNsense/CaptivePortal/clients.volt index 8970b591212..7b911ccbbe7 100644 --- a/src/opnsense/mvc/app/views/OPNsense/CaptivePortal/clients.volt +++ b/src/opnsense/mvc/app/views/OPNsense/CaptivePortal/clients.volt @@ -1,6 +1,6 @@ {# -OPNsense® is Copyright © 2014 – 2015 by Deciso B.V. +OPNsense® is Copyright © 2014 – 2024 by Deciso B.V. All rights reserved. Redistribution and use in source and binary forms, with or without modification, @@ -30,115 +30,75 @@ POSSIBILITY OF SUCH DAMAGE. -
-
-
-
-
- -
-
-
-
- - - - - - - - - - - - - - - -
{{ lang._('Session') }}{{ lang._('Username') }}{{ lang._('MAC address') }}{{ lang._('IP address') }}{{ lang._('Connected since') }}{{ lang._('Commands') }}
-
-
+ +
+
+
+ + + + + + + + + + + + + +
{{ lang._('Session') }}{{ lang._('Username') }}{{ lang._('MAC address') }}{{ lang._('IP address') }}{{ lang._('Connected since') }}{{ lang._('Commands') }}
diff --git a/src/opnsense/scripts/OPNsense/CaptivePortal/allow.py b/src/opnsense/scripts/OPNsense/CaptivePortal/allow.py index d090e01b433..937bb604752 100755 --- a/src/opnsense/scripts/OPNsense/CaptivePortal/allow.py +++ b/src/opnsense/scripts/OPNsense/CaptivePortal/allow.py @@ -1,7 +1,7 @@ #!/usr/local/bin/python3 """ - Copyright (c) 2015-2019 Ad Schellevis + Copyright (c) 2015-2024 Ad Schellevis All rights reserved. Redistribution and use in source and binary forms, with or without @@ -28,48 +28,28 @@ -------------------------------------------------------------------------------------- allow user/host to captive portal """ +import argparse import sys import ujson from lib.db import DB from lib.arp import ARP from lib.ipfw import IPFW -# parse input parameters -parameters = {'username': '', 'ip_address': None, 'zoneid': None, 'authenticated_via': None, 'output_type': 'plain'} -current_param = None -for param in sys.argv[1:]: - if len(param) > 1 and param[0] == '/': - current_param = param[1:].lower() - elif current_param is not None: - if current_param in parameters: - parameters[current_param] = param.strip() - current_param = None - -# create new session -if parameters['ip_address'] is not None and parameters['zoneid'] is not None: - cpDB = DB() - cpIPFW = IPFW() - arp_entry = ARP().get_by_ipaddress(parameters['ip_address']) - if arp_entry is not None: - mac_address = arp_entry['mac'] - else: - mac_address = None - - response = cpDB.add_client(zoneid=parameters['zoneid'], - authenticated_via=parameters['authenticated_via'], - username=parameters['username'], - ip_address=parameters['ip_address'], - mac_address=mac_address - ) - cpIPFW.add_to_table(table_number=parameters['zoneid'], address=parameters['ip_address']) - response['clientState'] = 'AUTHORIZED' -else: - response = {'clientState': 'UNKNOWN'} - - -# output result as plain text or json -if parameters['output_type'] != 'json': - for item in response: - print ('%20s %s' % (item, response[item])) -else: - print(ujson.dumps(response)) +parser = argparse.ArgumentParser() +parser.add_argument('-username', help='username', type=str, required=True) +parser.add_argument('-zoneid', help='zone number to allow this user in', type=str, required=True) +parser.add_argument('-authenticated_via', help='authentication source', type=str) +parser.add_argument('-ip_address', help='source ip address', type=str) +args = parser.parse_args() + +arp_entry = ARP().get_by_ipaddress(args.ip_address) +response = DB().add_client( + zoneid=args.zoneid, + authenticated_via=args.authenticated_via, + username=args.username, + ip_address=args.ip_address, + mac_address=arp_entry['mac'] if arp_entry is not None else None +) +IPFW().add_to_table(table_number=args.zoneid, address=args.ip_address) +response['clientState'] = 'AUTHORIZED' +print(ujson.dumps(response)) diff --git a/src/opnsense/scripts/OPNsense/CaptivePortal/disconnect.py b/src/opnsense/scripts/OPNsense/CaptivePortal/disconnect.py index d103a402a4c..6721d5cf85f 100755 --- a/src/opnsense/scripts/OPNsense/CaptivePortal/disconnect.py +++ b/src/opnsense/scripts/OPNsense/CaptivePortal/disconnect.py @@ -1,7 +1,7 @@ #!/usr/local/bin/python3 """ - Copyright (c) 2015-2019 Ad Schellevis + Copyright (c) 2015-2024 Ad Schellevis All rights reserved. Redistribution and use in source and binary forms, with or without @@ -28,35 +28,23 @@ -------------------------------------------------------------------------------------- disconnect client """ -import sys +import argparse import ujson from lib.db import DB from lib.ipfw import IPFW -# parse input parameters -parameters = {'sessionid': None, 'zoneid': None, 'output_type': 'plain'} -current_param = None -for param in sys.argv[1:]: - if len(param) > 1 and param[0] == '/' and param[1:] in parameters: - current_param = param[1:].lower() - elif current_param is not None: - parameters[current_param] = param.strip() - current_param = None - -# disconnect client + +parser = argparse.ArgumentParser() +parser.add_argument('session', help='session id to delete', type=str) +parser.add_argument('-z', help='optional zoneid to filter on', type=str) +args = parser.parse_args() + response = {'terminateCause': 'UNKNOWN'} -if parameters['sessionid'] is not None and parameters['zoneid'] is not None: - # remove client - client_session_info = DB().del_client(parameters['zoneid'], parameters['sessionid']) - if client_session_info is not None: - if client_session_info['ip_address']: - IPFW().delete(parameters['zoneid'], client_session_info['ip_address']) - client_session_info['terminateCause'] = 'User-Request' - response = client_session_info - -# output result as plain text or json -if parameters['output_type'] != 'json': - for item in response: - print ('%20s %s' % (item, response[item])) -else: - print(ujson.dumps(response)) +client_session_info = DB().del_client(int(args.z) if str(args.z).isdigit() else None, args.session) +if client_session_info is not None: + if client_session_info['ip_address']: + IPFW().delete(client_session_info['zoneid'], client_session_info['ip_address']) + client_session_info['terminateCause'] = 'User-Request' + response = client_session_info + +print(ujson.dumps(response)) diff --git a/src/opnsense/scripts/OPNsense/CaptivePortal/lib/db.py b/src/opnsense/scripts/OPNsense/CaptivePortal/lib/db.py index d309ef7e668..e9a415d530f 100755 --- a/src/opnsense/scripts/OPNsense/CaptivePortal/lib/db.py +++ b/src/opnsense/scripts/OPNsense/CaptivePortal/lib/db.py @@ -167,7 +167,7 @@ def del_client(self, zoneid, sessionid): cur.execute(""" select * from cp_clients where sessionid = :sessionid - and zoneid = :zoneid + and (zoneid = :zoneid or :zoneid is null) and deleted = 0 """, {'zoneid': zoneid, 'sessionid': sessionid}) data = cur.fetchall() @@ -176,15 +176,17 @@ def del_client(self, zoneid, sessionid): for fields in cur.description: session_info[fields[0]] = data[0][len(session_info)] # remove client - cur.execute("update cp_clients set deleted = 1 where sessionid = :sessionid and zoneid = :zoneid", - {'zoneid': zoneid, 'sessionid': sessionid}) + cur.execute( + "update cp_clients set deleted = 1 where sessionid = :sessionid", + {'zoneid': zoneid, 'sessionid': sessionid} + ) self._connection.commit() return session_info else: return None - def list_clients(self, zoneid): + def list_clients(self, zoneid=None): """ return list of (administrative) connected clients and usage statistics :param zoneid: zone id :return: list of clients @@ -212,7 +214,7 @@ def list_clients(self, zoneid): from cp_clients cc left join session_info si on si.zoneid = cc.zoneid and si.sessionid = cc.sessionid left join session_restrictions sr on sr.zoneid = cc.zoneid and sr.sessionid = cc.sessionid - where cc.zoneid = :zoneid + where (cc.zoneid = :zoneid or :zoneid is null) and cc.deleted = 0 order by case when cc.username is not null then cc.username else cc.ip_address end , cc.created desc diff --git a/src/opnsense/scripts/OPNsense/CaptivePortal/listClients.py b/src/opnsense/scripts/OPNsense/CaptivePortal/listClients.py index 58e2c5c0c72..2c8c7336f72 100755 --- a/src/opnsense/scripts/OPNsense/CaptivePortal/listClients.py +++ b/src/opnsense/scripts/OPNsense/CaptivePortal/listClients.py @@ -1,7 +1,7 @@ #!/usr/local/bin/python3 """ - Copyright (c) 2015-2019 Ad Schellevis + Copyright (c) 2015-2024 Ad Schellevis All rights reserved. Redistribution and use in source and binary forms, with or without @@ -26,50 +26,16 @@ POSSIBILITY OF SUCH DAMAGE. -------------------------------------------------------------------------------------- - list connected clients for a captive portal zone + list connected clients for a (or all) captive portal zone(s) """ -import sys -import time +import argparse import ujson from lib.db import DB # parse input parameters -parameters = {'zoneid': None, 'output_type': 'plain'} -current_param = None -for param in sys.argv[1:]: - if len(param) > 1 and param[0] == '/': - current_param = param[1:].lower() - elif current_param is not None: - if current_param in parameters: - parameters[current_param] = param.strip() - current_param = None +parser = argparse.ArgumentParser() +parser.add_argument('-z', help='optional zoneid to filter on', type=str) +args = parser.parse_args() -if parameters['zoneid'] is not None: - response = DB().list_clients(parameters['zoneid']) -else: - response = [] - -# output result as plain text or json -if parameters['output_type'] != 'json': - heading = { - 'sessionId': 'sessionid', - 'userName': 'username', - 'ipAddress': 'ip_address', - 'macAddress': 'mac_address', - 'total_bytes': 'total_bytes', - 'idletime': 'idletime', - 'totaltime': 'totaltime', - 'acc_timeout': 'acc_session_timeout' - } - heading_format = '%(sessionId)-30s %(userName)-25s %(ipAddress)-20s %(macAddress)-20s '\ - + '%(total_bytes)-15s %(idletime)-10s %(totaltime)-10s %(acc_timeout)-10s' - print (heading_format % heading) - for item in response: - item['total_bytes'] = (item['bytes_out'] + item['bytes_in']) - item['idletime'] = time.time() - item['last_accessed'] - item['totaltime'] = time.time() - item['startTime'] - frmt = '%(sessionId)-30s %(userName)-25s %(ipAddress)-20s %(macAddress)-20s '\ - + '%(total_bytes)-15s %(idletime)-10d %(totaltime)-10d %(acc_session_timeout)-10s' - print (frmt % item) -else: - print(ujson.dumps(response)) +response = DB().list_clients(int(args.z) if str(args.z).isdigit() else None) +print(ujson.dumps(response)) diff --git a/src/opnsense/scripts/OPNsense/CaptivePortal/set_session_restrictions.py b/src/opnsense/scripts/OPNsense/CaptivePortal/set_session_restrictions.py index b98f365598b..631685fc545 100755 --- a/src/opnsense/scripts/OPNsense/CaptivePortal/set_session_restrictions.py +++ b/src/opnsense/scripts/OPNsense/CaptivePortal/set_session_restrictions.py @@ -1,6 +1,6 @@ #!/usr/local/bin/python3 """ - Copyright (c) 2015-2019 Ad Schellevis + Copyright (c) 2015-2024 Ad Schellevis All rights reserved. Redistribution and use in source and binary forms, with or without @@ -26,29 +26,18 @@ -------------------------------------------------------------------------------------- update (or add) client/session restrictions """ -import sys +import argparse import ujson from lib.db import DB -parameters = {'zoneid': '', 'sessionid': None, 'session_timeout': None, 'output_type': 'plain'} -current_param = None -for param in sys.argv[1:]: - if len(param) > 1 and param[0] == '/' and param[1:] in parameters: - current_param = param[1:].lower() - elif current_param is not None: - parameters[current_param] = param.strip() - current_param = None +parser = argparse.ArgumentParser() +parser.add_argument('-zoneid', help='zone number to allow this user in', type=str, required=True) +parser.add_argument('-sessionid', help='session id', type=str, required=True) +parser.add_argument('-session_timeout', help='authentication source', type=str) +args = parser.parse_args() -response = dict() -if parameters['zoneid'] is not None and parameters['sessionid'] is not None: - db = DB() - response['response'] = db.update_session_restrictions(parameters['zoneid'], - parameters['sessionid'], - parameters['session_timeout']) -# output result as plain text or json -if parameters['output_type'] != 'json': - for item in response: - print ('%20s %s' % (item, response[item])) -else: - print(ujson.dumps(response)) +response = { + 'response': DB().update_session_restrictions(args.zoneid, args.sessionid, args.session_timeout) +} +print(ujson.dumps(response)) diff --git a/src/opnsense/service/conf/actions.d/actions_captiveportal.conf b/src/opnsense/service/conf/actions.d/actions_captiveportal.conf index e6282dcf583..79a2f76c9c2 100644 --- a/src/opnsense/service/conf/actions.d/actions_captiveportal.conf +++ b/src/opnsense/service/conf/actions.d/actions_captiveportal.conf @@ -1,24 +1,24 @@ [list_clients] command:/usr/local/opnsense/scripts/OPNsense/CaptivePortal/listClients.py -parameters:/zoneid %s /output_type %s +parameters:-z %s type:script_output message:list registered clients [allow] command:/usr/local/opnsense/scripts/OPNsense/CaptivePortal/allow.py -parameters:/zoneid %s /username %s /ip_address %s /authenticated_via %s /output_type %s +parameters:-zoneid %s -username %s -ip_address %s -authenticated_via %s type:script_output message:allow client access to captive portal [disconnect] command:/usr/local/opnsense/scripts/OPNsense/CaptivePortal/disconnect.py -parameters:/zoneid %s /sessionid %s /output_type %s +parameters:%s type:script_output message:disconnect client [set.session_restrictions] command:/usr/local/opnsense/scripts/OPNsense/CaptivePortal/set_session_restrictions.py -parameters:/zoneid %s /sessionid %s /session_timeout %s /output_type %s +parameters:-zoneid %s -sessionid %s -session_timeout %s type:script_output message:set extra restrictions for session (%s) %s