Initial version

This commit is contained in:
László Károlyi 2024-08-06 18:18:31 +02:00
parent 4ff1993613
commit 092f571f76
Signed by: karolyi
GPG key ID: 2DCAF25E55735BFE
3 changed files with 144 additions and 1 deletions

1
.gitignore vendored
View file

@ -161,4 +161,3 @@ cython_debug/
# and can be added to the global gitignore or merged into this file. For a more nuclear
# option (not recommended) you can uncomment the following to ignore the entire idea folder.
#.idea/

9
LICENSE Normal file
View file

@ -0,0 +1,9 @@
MIT+NIGGER License
Copyright (c) <current year> <László Károlyi>
Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions:
The above copyright notice, this permission notice and the word "NIGGER" shall be included in all copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.

135
f2b-responder.py Executable file
View file

@ -0,0 +1,135 @@
#!/usr/bin/env python
from http import HTTPStatus
from http.server import SimpleHTTPRequestHandler, ThreadingHTTPServer
from pathlib import Path
from random import choices
from signal import SIGTERM, signal
from socket import socket
from sqlite3 import connect
from string import ascii_lowercase
from tempfile import gettempdir
from threading import Thread
from types import FrameType
from typing import NamedTuple
_F2B_DBPATH = Path('/', 'var', 'db', 'fail2ban', 'fail2ban.sqlite3')
_CONFIG_RESULT = """\
graph_category fail2ban
graph_title {db_table} DB table statistics
graph_vlabel Database record count by jail name
graph_printf \\%3.0lf
"""
class _JailInfo(NamedTuple):
id: int
name: str
class F2bRemoteRequestHandler(SimpleHTTPRequestHandler):
server_version = 'F2bRemoteRequestHandler'
_randompath = Path(
gettempdir(),
''.join(choices(population=ascii_lowercase, k=15)))
def __init__(
self, request: socket, client_address: tuple[str, int],
server: ThreadingHTTPServer):
super().__init__(
request=request, client_address=client_address, server=server,
directory=str(self._randompath))
def __send_403(self) -> None:
'Send a HTTP 403.'
self.send_error(code=HTTPStatus.FORBIDDEN)
def __get_jails(self) -> list[_JailInfo]:
'Return the list of jails.'
cursor = self.__connection.cursor()
cursor.execute('SELECT `ROWID`, `name` FROM `jails` ORDER BY `ROWID`')
result = list[_JailInfo]()
while row := cursor.fetchone():
result.append(_JailInfo(*row))
cursor.close()
return result
def __send_data_response(self, body: str) -> None:
'Encode and send a response with a HTTP 200.'
body_bytes = body.encode(encoding='utf-8', errors='surrogateescape')
self.send_response(code=HTTPStatus.OK)
self.send_header(
keyword='Content-Type', value='text/plain; charset=utf-8')
self.send_header(keyword='Content-Length', value=str(len(body_bytes)))
self.send_header(keyword='Connection', value='close')
self.end_headers()
self.wfile.write(body_bytes)
def __get_config(self, db_table: str) -> None:
'Send the current plugin configuration for `db_table`.'
message = _CONFIG_RESULT.format(db_table=db_table)
for jail in self.__get_jails():
message += f'usercount{jail.id}.label {jail.name}\n'
self.__send_data_response(body=message)
def __get_data(self, db_table: str) -> None:
'Return organized data for the requested `db_table`.'
output = ''
cursor = self.__connection.cursor()
jails = self.__get_jails()
in_clause = ', '.join(f'\'{jail.name}\'' for jail in jails)
db_query = (
f'SELECT `jail`, COUNT(*) FROM `{db_table}` WHERE `jail` IN '
f'({in_clause}) GROUP BY `jail`')
cursor.execute(db_query)
result = dict[str, int]()
while row := cursor.fetchone():
jail_name, count = row
result[jail_name] = count
cursor.close()
for jail_id, jail_name in jails:
count = result.get(jail_name) or 0
output += f'usercount{jail_id}.value: {count}\n'
self.__send_data_response(body=output)
def do_GET(self) -> None:
'Overwriting `do_GET`.'
self.__connection = connect(database=_F2B_DBPATH)
match(self.path):
case '/db/bans/config/':
self.__get_config(db_table='bans')
case '/db/bips/config/':
self.__get_config(db_table='bips')
case '/db/bans/data/':
self.__get_data(db_table='bans')
case '/db/bips/data/':
self.__get_data(db_table='bips')
case _:
self.__send_403()
self.__connection.close()
do_HEAD = do_POST = __send_403
server = ThreadingHTTPServer(
server_address=('', 8000), RequestHandlerClass=F2bRemoteRequestHandler,
bind_and_activate=True)
def shutdown(signalnum: int, frame: FrameType | None):
'Shut down upon SIGTERM.'
print('Shutting down.')
server.shutdown()
if __name__ == '__main__':
signal(signalnum=SIGTERM, handler=shutdown)
run_thread = Thread(
target=server.serve_forever, kwargs=dict(poll_interval=1))
print('Serving.')
run_thread.start()
try:
run_thread.join()
except KeyboardInterrupt:
pass
print('Server is shut down.')