security/q-feeds-connector: add optional locked mode in qfeedsctl.py for cron runners and wait for configfile changes when HTTP 401 is thrown. closes https://github.com/opnsense/plugins/issues/5416

This should prevent firewalls from spamming Q-Feeds infrastructure when either an empty or invalid token is specified.
This commit is contained in:
Ad Schellevis 2026-05-04 17:25:33 +02:00
parent 11ac729b07
commit 7b3b5a4e3c
3 changed files with 38 additions and 4 deletions

View file

@ -29,16 +29,24 @@ from configparser import ConfigParser
class QFeedsConfig:
config_filename = '/usr/local/etc/qfeeds.conf'
api_key = None
conf_timestamp = None
def __init__(self):
config_filename = '/usr/local/etc/qfeeds.conf'
if os.path.isfile(config_filename):
if os.path.isfile(self.config_filename):
cnf = ConfigParser()
cnf.read(config_filename)
cnf.read(self.config_filename)
if cnf.has_section('api') and cnf.has_option('api', 'key'):
self.api_key = cnf.get('api', 'key').strip()
if self.conf_timestamp is None:
QFeedsConfig.conf_timestamp = os.stat(self.config_filename).st_mtime
@classmethod
def has_changed(cls):
return os.path.isfile(cls.config_filename) and cls.conf_timestamp != os.stat(cls.config_filename).st_mtime
class Api:
def __init__(self):

View file

@ -27,10 +27,13 @@
"""
import argparse
import fcntl
import time
import sys
import ujson
from requests.exceptions import HTTPError, Timeout
from lib import QFeedsActions
from lib.api import QFeedsConfig
if __name__ == '__main__':
@ -38,8 +41,20 @@ if __name__ == '__main__':
parser.add_argument('--target_dir', default='/var/db/qfeeds-tables')
parser.add_argument('-f', help='forced (auto index)' , default=False, action='store_true')
parser.add_argument('-v', help='verbose output' , default=False, action='store_true')
parser.add_argument('-l', help='lock operation' , default=False, action='store_true')
parser.add_argument("action", choices=QFeedsActions.list_actions(), nargs='*')
args = parser.parse_args()
fhandle = None
if args.l:
lck_filename = '/tmp/qfeeds_prc.LCK'
fhandle = open(lck_filename, 'a+')
try:
fcntl.flock(fhandle, fcntl.LOCK_EX | fcntl.LOCK_NB)
except IOError:
print('already busy, exit')
sys.exit(0)
if args.v:
# verbose mode
import http.client as http_client
@ -51,6 +66,14 @@ if __name__ == '__main__':
print(msg)
except HTTPError as exc:
print('exit with HTTPError %d (%s)' % (exc.response.status_code, exc.response.text))
if exc.response.status_code == 401 and 'update' in args.action:
print('batch mode - wait for configuration update or timeout')
t_start = time.time()
while not QFeedsConfig.has_changed():
if time.time() - t_start > 3600:
print('timeout waiting for config change')
break
time.sleep(5)
sys.exit(-1)
except Timeout as exc:
print('timeout reaching api endpoint')
@ -61,3 +84,6 @@ if __name__ == '__main__':
except ujson.JSONDecodeError:
print("JSON decode error")
sys.exit(-1)
finally:
if fhandle:
fcntl.flock(fhandle, fcntl.LOCK_UN)

View file

@ -6,7 +6,7 @@ message:reconfigure QFeeds
errors:no
[update]
command:/usr/local/opnsense/scripts/qfeeds/qfeedsctl.py update
command:/usr/local/opnsense/scripts/qfeeds/qfeedsctl.py -l update
parameters:
type:script_output
message:update QFeeds