Update listing logic + logging changes

This commit is contained in:
László Károlyi 2022-03-31 19:01:52 +02:00
parent f9265d677a
commit 22e385f7ac
Signed by: karolyi
GPG Key ID: 2DCAF25E55735BFE
5 changed files with 175 additions and 67 deletions

View File

@ -3,3 +3,4 @@ path_datadir: /etc/icygov/data
allowed_server_nets: []
geoip:
asn_db_path: /var/lib/GeoIP/GeoLite2-ASN.mmdb
city_db_path: /var/lib/GeoIP/GeoLite2-City.mmdb

View File

@ -1,3 +1,3 @@
Flask~=2.0.1
Flask~=2.0.3
PyYAML~=6.0
geoip2~=4.5.0

View File

@ -54,36 +54,36 @@ def redownload_ranges():
load()
def match_wl_v4(address: IPv4Address) -> Optional[Tuple[ListingType, str]]:
def match_wl_v4(address: IPv4Address) -> Optional[str]:
'Match whitelisting, return `None` when unlisted.'
for func in _MATCH_FUNCS_WL_V4:
if result := func(address=address):
# Listener IP was whitelisted
return ListingType.WHITELISTED, result
return result
def match_wl_v6(address: IPv6Address) -> Optional[Tuple[ListingType, str]]:
def match_wl_v6(address: IPv6Address) -> Optional[str]:
'Match whitelisting, return `None` when unlisted.'
for func in _MATCH_FUNCS_WL_V6:
if result := func(address=address):
# Listener IP was whitelisted
return ListingType.WHITELISTED, result
return result
def match_bl_v4(address: IPv4Address) -> Optional[Tuple[ListingType, str]]:
def match_bl_v4(address: IPv4Address) -> Optional[str]:
'Match blacklisting, return `None` when unlisted.'
for func in _MATCH_FUNCS_BL_V4:
if result := func(address=address):
# Listener IP was blacklisted
return ListingType.BLACKLISTED, result
return result
def match_bl_v6(address: IPv6Address) -> Optional[Tuple[ListingType, str]]:
def match_bl_v6(address: IPv6Address) -> Optional[str]:
'Match blacklisting, return `None` when unlisted.'
for func in _MATCH_FUNCS_BL_V6:
if result := func(address=address):
# Listener IP was blacklisted
return ListingType.BLACKLISTED, result
return result
load()

View File

@ -8,7 +8,7 @@ from werkzeug.middleware.proxy_fix import ProxyFix
from .ip_collector.reader import redownload_ranges
from .middleware import RequesterFilterMiddleWare
from .utils import IcyGovListener
from .utils import ListenerAddEvaluator, ListenerRemoveEvaluator
flask_app = Flask(import_name='icy_governor')
flask_app.wsgi_app = RequesterFilterMiddleWare( # type: ignore
@ -28,25 +28,13 @@ def reload_ranges() -> Response:
@flask_app.route(rule=f'{BASE_PATH}listener-add/', methods=['POST'])
def listener_add() -> Response:
flask_app.logger.debug('Add data received: %s', request.form)
response = make_response('alrighty', 200)
ip = request.form.get('ip')
if not ip:
response.headers['Icecast-Auth-Message'] = 'IP not specified'
return response
icygov_addr = IcyGovListener(
address=ip, user_agent=request.form.get('agent', ''))
flask_app.logger.debug(icygov_addr)
if icygov_addr.can_listen:
response.headers['Icecast-Auth-User'] = 1
else:
response.headers['Icecast-Auth-Message'] = 'Stream not found'
# response.headers['Icecast-Auth-Timelimit'] = 10
return response
evaluator = ListenerAddEvaluator(
form=request.form, logger=flask_app.logger)
return evaluator.response
@flask_app.route(rule=f'{BASE_PATH}listener-remove/', methods=['POST'])
def listener_remove() -> Response:
flask_app.logger.debug('Remove data received: %s', request.form)
response = make_response('alrighty', 200)
return response
evaluator = ListenerRemoveEvaluator(
form=request.form, logger=flask_app.logger)
return evaluator.response

View File

@ -1,10 +1,16 @@
from datetime import timedelta
from functools import cached_property
from ipaddress import IPv4Address, IPv6Address, ip_address
from logging import Logger
from typing import Optional
from flask.helpers import make_response
from flask.wrappers import Response
from geoip2.database import Reader
from geoip2.errors import AddressNotFoundError
from geoip2.models import ASN
from geoip2.types import IPAddress
from werkzeug.datastructures import ImmutableMultiDict
from .config import CONFIG
from .ip_collector.asn_checker import is_asn_blacklisted, is_asn_whitelisted
@ -13,29 +19,77 @@ from .ip_collector.reader import (
from .ip_collector.useragent import useragent_match_bl, useragent_match_wl
class IcyGovListener(object):
'Depicting IP addresses internally.'
class ListenerEvaluatorBase(object):
'Base for evaluation classes.'
status: ListingType = ListingType.UNLISTED
_asn_description = 'ASN unknown'
_result_description = 'not listed anywhere'
_status: ListingType = ListingType.UNLISTED
_asn_description = '-'
_result_description = '-'
def __init__(self, address: str, user_agent: str):
self.address = ip_address(address=address)
self.user_agent = user_agent
def __init__(self, form: ImmutableMultiDict, logger: Logger):
self._logger = logger
self._form = form
self.process()
@property
def can_listen(self) -> bool:
'Return `True` if this listener can listen, `False` otherwise.'
return self.status != ListingType.BLACKLISTED
@cached_property
def response(self) -> Response:
response = make_response('alrighty', 200)
return response
def set_result(self, status: ListingType, description: str):
'Set internal result status.'
self._status = status
self._result_description = description
@cached_property
def _address(self) -> Optional[IPAddress]:
'Evaluate IP address.'
ip = self._form.get('ip')
if not ip:
self.response.headers['Icecast-Auth-Message'] = \
'IP address not specified'
self._status = ListingType.BLACKLISTED
self.set_result(
status=ListingType.BLACKLISTED,
description=f'IP not passed: {self._form}')
return
return ip_address(address=ip)
@cached_property
def _client_id(self) -> str:
return self._form.get('client', '-')
@cached_property
def _mount_user_pass(self) -> str:
mount = self._form.get('mount')
user = self._form.get('user')
user = repr(user) if user else '-'
passwd = self._form.get('pass')
passwd = repr(passwd) if passwd else '-'
return f'{mount!r}|{user}|{passwd}'
@cached_property
def _city(self) -> str:
if not self._address:
return '-'
try:
with Reader(fileish=CONFIG['geoip']['city_db_path']) as reader:
res = reader.city(ip_address=self._address)
# from IPython import embed; embed()
county = res.subdivisions.most_specific.name or '-'
city = res.city.name or '-'
return f'{res.country.name}|{county}|{city}'
except AddressNotFoundError:
return '-'
@cached_property
def asn(self) -> Optional[ASN]:
'Do the DB lookup, return results.'
if not self._address:
return
try:
with Reader(fileish=CONFIG['geoip']['asn_db_path']) as reader:
asn = reader.asn(ip_address=self.address)
asn = reader.asn(ip_address=self._address)
self._asn_description = (
f'AS{asn.autonomous_system_number} ({asn.network}): '
f'{asn.autonomous_system_organization}')
@ -43,16 +97,35 @@ class IcyGovListener(object):
except AddressNotFoundError:
pass
def __str__(self) -> str:
return (
f'<{self.address}>: {self.status}, <{self._asn_description}>, '
f'<{self._result_description}>')
@cached_property
def _user_agent(self) -> Optional[str]:
'Evaluate useragent.'
return self._form.get('agent', '-')
def set_result(self, status: ListingType, description: str = None):
'Set internal result status.'
self.status = status
if description:
self._result_description = description
@cached_property
def _server_port(self) -> str:
server = self._form.get('server') or '-'
port = self._form.get('port') or '-'
return f'{server}:{port}'
def process(self):
'Placeholder method.'
class ListenerAddEvaluator(ListenerEvaluatorBase):
'Evaluating listener_add requests.'
@cached_property
def _referer(self) -> str:
if referer := self._form.get('ClientHeader.referer'):
return repr(referer)
return '-'
@cached_property
def _metadata(self) -> str:
if metadata := self._form.get('ClientHeader.icy-metadata'):
return f'Metadata: True({metadata})'
return 'Metadata: False'
def _match_wl(self) -> bool:
'Return `True` when whitelisted, `False` otherwise.'
@ -64,17 +137,19 @@ class IcyGovListener(object):
status=ListingType.WHITELISTED, description=result)
return True
# Useragent whitelists
if result := useragent_match_wl(user_agent=self.user_agent):
self.set_result(status=ListingType.WHITELISTED, description=result)
return True
if self._user_agent:
if result := useragent_match_wl(user_agent=self._user_agent):
self.set_result(
status=ListingType.WHITELISTED, description=result)
return True
# Network whitelists
result = None
if type(self.address) is IPv4Address:
result = match_wl_v4(address=self.address)
elif type(self.address) is IPv6Address:
result = match_wl_v6(address=self.address)
if type(self._address) is IPv4Address:
result = match_wl_v4(address=self._address)
elif type(self._address) is IPv6Address:
result = match_wl_v6(address=self._address)
if result:
self.set_result(status=result[0], description=result[1])
self.set_result(status=ListingType.WHITELISTED, description=result)
return True
return False
@ -88,19 +163,63 @@ class IcyGovListener(object):
status=ListingType.BLACKLISTED, description=result)
return True
# Useragent blacklists
if result := useragent_match_bl(user_agent=self.user_agent):
self.set_result(status=ListingType.BLACKLISTED, description=result)
return True
if self._user_agent:
if result := useragent_match_bl(user_agent=self._user_agent):
self.set_result(
status=ListingType.BLACKLISTED, description=result)
return True
# Network blacklists
result = None
if type(self.address) is IPv4Address:
result = match_bl_v4(address=self.address)
elif type(self.address) is IPv6Address:
result = match_bl_v6(address=self.address)
if type(self._address) is IPv4Address:
result = match_bl_v4(address=self._address)
elif type(self._address) is IPv6Address:
result = match_bl_v6(address=self._address)
if result:
self.set_result(status=result[0], description=result[1])
self.set_result(status=ListingType.BLACKLISTED, description=result)
return True
return False
def __str__(self) -> str:
return (
f'ADD: <{self._client_id}|{self._address}> <{self._status}: '
f'{self._result_description}> <{self._city}> '
f'<{self._asn_description}> <{self._user_agent}> '
f'<{self._mount_user_pass}> <{self._metadata}> '
f'<{self._server_port}> <{self._referer}>')
def process(self):
self._match_wl() or self._match_bl()
'Start processing.'
super().process()
if not self._address:
self._logger.debug(msg=self)
return
self._match_wl() or self._match_bl() # type: ignore
if self._status == ListingType.BLACKLISTED:
self.response.headers['Icecast-Auth-Message'] = 'Stream not found'
else:
self.response.headers['Icecast-Auth-User'] = 1
self._logger.debug(msg=self)
class ListenerRemoveEvaluator(ListenerEvaluatorBase):
'Evaluating disconnecting listeners.'
@cached_property
def _duration(self) -> str:
seconds = int(self._form.get('duration') or 0)
return str(timedelta(seconds=seconds))
def __str__(self) -> str:
return (
f'REMOVE: <{self._client_id}|{self._address}> <{self._duration}> '
f'<{self._city}> <{self._asn_description}> <{self._user_agent}> '
f'<{self._mount_user_pass}> <{self._server_port}>')
def process(self):
'Start processing.'
super().process()
if not self._address:
self._logger.debug(msg=self)
return
self.asn # Do the ASN variables setting for logging
self._logger.debug(msg=self)