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: [] allowed_server_nets: []
geoip: geoip:
asn_db_path: /var/lib/GeoIP/GeoLite2-ASN.mmdb 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 PyYAML~=6.0
geoip2~=4.5.0 geoip2~=4.5.0

View File

@ -54,36 +54,36 @@ def redownload_ranges():
load() 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.' 'Match whitelisting, return `None` when unlisted.'
for func in _MATCH_FUNCS_WL_V4: for func in _MATCH_FUNCS_WL_V4:
if result := func(address=address): if result := func(address=address):
# Listener IP was whitelisted # 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.' 'Match whitelisting, return `None` when unlisted.'
for func in _MATCH_FUNCS_WL_V6: for func in _MATCH_FUNCS_WL_V6:
if result := func(address=address): if result := func(address=address):
# Listener IP was whitelisted # 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.' 'Match blacklisting, return `None` when unlisted.'
for func in _MATCH_FUNCS_BL_V4: for func in _MATCH_FUNCS_BL_V4:
if result := func(address=address): if result := func(address=address):
# Listener IP was blacklisted # 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.' 'Match blacklisting, return `None` when unlisted.'
for func in _MATCH_FUNCS_BL_V6: for func in _MATCH_FUNCS_BL_V6:
if result := func(address=address): if result := func(address=address):
# Listener IP was blacklisted # Listener IP was blacklisted
return ListingType.BLACKLISTED, result return result
load() load()

View File

@ -8,7 +8,7 @@ from werkzeug.middleware.proxy_fix import ProxyFix
from .ip_collector.reader import redownload_ranges from .ip_collector.reader import redownload_ranges
from .middleware import RequesterFilterMiddleWare from .middleware import RequesterFilterMiddleWare
from .utils import IcyGovListener from .utils import ListenerAddEvaluator, ListenerRemoveEvaluator
flask_app = Flask(import_name='icy_governor') flask_app = Flask(import_name='icy_governor')
flask_app.wsgi_app = RequesterFilterMiddleWare( # type: ignore 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']) @flask_app.route(rule=f'{BASE_PATH}listener-add/', methods=['POST'])
def listener_add() -> Response: def listener_add() -> Response:
flask_app.logger.debug('Add data received: %s', request.form) evaluator = ListenerAddEvaluator(
response = make_response('alrighty', 200) form=request.form, logger=flask_app.logger)
ip = request.form.get('ip') return evaluator.response
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
@flask_app.route(rule=f'{BASE_PATH}listener-remove/', methods=['POST']) @flask_app.route(rule=f'{BASE_PATH}listener-remove/', methods=['POST'])
def listener_remove() -> Response: def listener_remove() -> Response:
flask_app.logger.debug('Remove data received: %s', request.form) evaluator = ListenerRemoveEvaluator(
response = make_response('alrighty', 200) form=request.form, logger=flask_app.logger)
return response return evaluator.response

View File

@ -1,10 +1,16 @@
from datetime import timedelta
from functools import cached_property from functools import cached_property
from ipaddress import IPv4Address, IPv6Address, ip_address from ipaddress import IPv4Address, IPv6Address, ip_address
from logging import Logger
from typing import Optional from typing import Optional
from flask.helpers import make_response
from flask.wrappers import Response
from geoip2.database import Reader from geoip2.database import Reader
from geoip2.errors import AddressNotFoundError from geoip2.errors import AddressNotFoundError
from geoip2.models import ASN from geoip2.models import ASN
from geoip2.types import IPAddress
from werkzeug.datastructures import ImmutableMultiDict
from .config import CONFIG from .config import CONFIG
from .ip_collector.asn_checker import is_asn_blacklisted, is_asn_whitelisted 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 from .ip_collector.useragent import useragent_match_bl, useragent_match_wl
class IcyGovListener(object): class ListenerEvaluatorBase(object):
'Depicting IP addresses internally.' 'Base for evaluation classes.'
status: ListingType = ListingType.UNLISTED _status: ListingType = ListingType.UNLISTED
_asn_description = 'ASN unknown' _asn_description = '-'
_result_description = 'not listed anywhere' _result_description = '-'
def __init__(self, address: str, user_agent: str): def __init__(self, form: ImmutableMultiDict, logger: Logger):
self.address = ip_address(address=address) self._logger = logger
self.user_agent = user_agent self._form = form
self.process() self.process()
@property @cached_property
def can_listen(self) -> bool: def response(self) -> Response:
'Return `True` if this listener can listen, `False` otherwise.' response = make_response('alrighty', 200)
return self.status != ListingType.BLACKLISTED 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 @cached_property
def asn(self) -> Optional[ASN]: def asn(self) -> Optional[ASN]:
'Do the DB lookup, return results.' 'Do the DB lookup, return results.'
if not self._address:
return
try: try:
with Reader(fileish=CONFIG['geoip']['asn_db_path']) as reader: 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 = ( self._asn_description = (
f'AS{asn.autonomous_system_number} ({asn.network}): ' f'AS{asn.autonomous_system_number} ({asn.network}): '
f'{asn.autonomous_system_organization}') f'{asn.autonomous_system_organization}')
@ -43,16 +97,35 @@ class IcyGovListener(object):
except AddressNotFoundError: except AddressNotFoundError:
pass pass
def __str__(self) -> str: @cached_property
return ( def _user_agent(self) -> Optional[str]:
f'<{self.address}>: {self.status}, <{self._asn_description}>, ' 'Evaluate useragent.'
f'<{self._result_description}>') return self._form.get('agent', '-')
def set_result(self, status: ListingType, description: str = None): @cached_property
'Set internal result status.' def _server_port(self) -> str:
self.status = status server = self._form.get('server') or '-'
if description: port = self._form.get('port') or '-'
self._result_description = description 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: def _match_wl(self) -> bool:
'Return `True` when whitelisted, `False` otherwise.' 'Return `True` when whitelisted, `False` otherwise.'
@ -64,17 +137,19 @@ class IcyGovListener(object):
status=ListingType.WHITELISTED, description=result) status=ListingType.WHITELISTED, description=result)
return True return True
# Useragent whitelists # Useragent whitelists
if result := useragent_match_wl(user_agent=self.user_agent): if self._user_agent:
self.set_result(status=ListingType.WHITELISTED, description=result) if result := useragent_match_wl(user_agent=self._user_agent):
return True self.set_result(
status=ListingType.WHITELISTED, description=result)
return True
# Network whitelists # Network whitelists
result = None result = None
if type(self.address) is IPv4Address: if type(self._address) is IPv4Address:
result = match_wl_v4(address=self.address) result = match_wl_v4(address=self._address)
elif type(self.address) is IPv6Address: elif type(self._address) is IPv6Address:
result = match_wl_v6(address=self.address) result = match_wl_v6(address=self._address)
if result: if result:
self.set_result(status=result[0], description=result[1]) self.set_result(status=ListingType.WHITELISTED, description=result)
return True return True
return False return False
@ -88,19 +163,63 @@ class IcyGovListener(object):
status=ListingType.BLACKLISTED, description=result) status=ListingType.BLACKLISTED, description=result)
return True return True
# Useragent blacklists # Useragent blacklists
if result := useragent_match_bl(user_agent=self.user_agent): if self._user_agent:
self.set_result(status=ListingType.BLACKLISTED, description=result) if result := useragent_match_bl(user_agent=self._user_agent):
return True self.set_result(
status=ListingType.BLACKLISTED, description=result)
return True
# Network blacklists # Network blacklists
result = None result = None
if type(self.address) is IPv4Address: if type(self._address) is IPv4Address:
result = match_bl_v4(address=self.address) result = match_bl_v4(address=self._address)
elif type(self.address) is IPv6Address: elif type(self._address) is IPv6Address:
result = match_bl_v6(address=self.address) result = match_bl_v6(address=self._address)
if result: if result:
self.set_result(status=result[0], description=result[1]) self.set_result(status=ListingType.BLACKLISTED, description=result)
return True return True
return False 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): 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)