From ef07cd4af3ce549da36a7d41cf2cf0bdb8719063 Mon Sep 17 00:00:00 2001 From: JustAnotherArchivist Date: Sat, 10 Oct 2020 00:01:46 +0000 Subject: [PATCH] Very basic web interface --- irclog.py | 98 +++++++++++++++++++++++++++++-------------------------- 1 file changed, 51 insertions(+), 47 deletions(-) diff --git a/irclog.py b/irclog.py index 702a05b..7d3851a 100644 --- a/irclog.py +++ b/irclog.py @@ -4,6 +4,7 @@ import asyncio import base64 import collections import datetime +import functools import importlib.util import inspect import ircstates @@ -683,16 +684,21 @@ class WebServer: def __init__(self, config): self.config = config - self._paths = {} # '/path' => ('#channel', auth, module, moduleargs) where auth is either False (no authentication) or the HTTP header value for basic auth + self._paths = {} # '/path' => ('#channel', auth) where auth is either False (no authentication) or the HTTP header value for basic auth self._app = aiohttp.web.Application() - self._app.add_routes([aiohttp.web.post('/{path:.+}', self.post)]) + self._app.add_routes([ + aiohttp.web.get('/', self.get_homepage), + aiohttp.web.get(r'/{path:[^/]+}/{date:\d{4}-\d{2}-\d{2}}', functools.partial(self._channel_handler, handler = self.get_log)), + aiohttp.web.get(r'/{path:[^/]+}/{date:today}', functools.partial(self._channel_handler, handler = self.get_log)), + aiohttp.web.get('/{path:[^/]+}/search', functools.partial(self._channel_handler, handler = self.search)), + ]) self.update_config(config) self._configChanged = asyncio.Event() def update_config(self, config): -# self._paths = {channel['webpath']: (channel['ircchannel'], f'Basic {base64.b64encode(channel["auth"].encode("utf-8")).decode("utf-8")}' if channel['auth'] else False) for channel in config['channels'].values()} + self._paths = {channel['path']: (channel['ircchannel'], f'Basic {base64.b64encode(channel["auth"].encode("utf-8")).decode("utf-8")}' if channel['auth'] else False) for channel in config['channels'].values()} needRebind = self.config['web'] != config['web'] #TODO only if there are changes to web.host or web.port; everything else can be updated without rebinding self.config = config if needRebind: @@ -710,57 +716,55 @@ class WebServer: break self._configChanged.clear() -# https://docs.python.org/3/library/asyncio-subprocess.html#asyncio.asyncio.subprocess.Process -# https://stackoverflow.com/questions/1180606/using-subprocess-popen-for-process-with-large-output -# -> https://stackoverflow.com/questions/57730010/python-asyncio-subprocess-write-stdin-and-read-stdout-stderr-continuously - - async def post(self, request): - self.logger.info(f'Received request {id(request)} from {request.remote!r} for {request.path!r} with body {(await request.read())!r}') - try: - channel, auth, module, moduleargs, overlongmode = self._paths[request.path] - except KeyError: + async def _check_valid_channel(self, request): + if request.match_info['path'] not in self._paths: self.logger.info(f'Bad request {id(request)}: no path {request.path!r}') raise aiohttp.web.HTTPNotFound() + + async def _check_auth(self, request): + auth = self._paths[request.match_info['path']][1] if auth: authHeader = request.headers.get('Authorization') if not authHeader or authHeader != auth: self.logger.info(f'Bad request {id(request)}: authentication failed: {authHeader!r} != {auth}') - raise aiohttp.web.HTTPForbidden() - if module is not None: - self.logger.debug(f'Processing request {id(request)} using {module!r}') - try: - message = await module.process(request, *moduleargs) - except aiohttp.web.HTTPException as e: - raise e - except Exception as e: - self.logger.error(f'Bad request {id(request)}: exception in module process function: {type(e).__module__}.{type(e).__name__}: {e!s}') - raise aiohttp.web.HTTPBadRequest() - if '\r' in message or '\n' in message: - self.logger.error(f'Bad request {id(request)}: module process function returned message with linebreaks: {message!r}') - raise aiohttp.web.HTTPBadRequest() + raise aiohttp.web.HTTPUnauthorized() + + async def _channel_handler(self, request, handler): + await self._check_valid_channel(request) + await self._check_auth(request) + return (await handler(request)) + + async def get_homepage(self, request): + self.logger.info(f'Received request {id(request)} from {request.remote!r} for {request.path!r}') + lines = [] + for path, (channel, auth) in self._paths.items(): + lines.append(f'{"(PW) " if auth else ""}{channel}') + return aiohttp.web.Response(text = f'{"
".join(lines)}', content_type = 'text/html') + + async def get_log(self, request): + self.logger.info(f'Received request {id(request)} from {request.remote!r} for {request.path!r}') + if request.match_info['date'] == 'today': + date = datetime.datetime.now(tz = datetime.timezone.utc).replace(hour = 0, minute = 0, second = 0, microsecond = 0) else: - self.logger.debug(f'Processing request {id(request)} using default processor') - message = await self._default_process(request) - self.logger.info(f'Accepted request {id(request)}, putting message {message!r} for {channel} into message queue') - self.messageQueue.put_nowait((channel, message, overlongmode)) - raise aiohttp.web.HTTPOk() - - async def _default_process(self, request): - try: - message = await request.text() - except Exception as e: - self.logger.info(f'Bad request {id(request)}: exception while reading request data: {e!s}') - raise aiohttp.web.HTTPBadRequest() # Yes, it's always the client's fault. :-) - self.logger.debug(f'Request {id(request)} payload: {message!r}') - # Strip optional [CR] LF at the end of the payload - if message.endswith('\r\n'): - message = message[:-2] - elif message.endswith('\n'): - message = message[:-1] - if '\r' in message or '\n' in message: - self.logger.info(f'Bad request {id(request)}: linebreaks in message') - raise aiohttp.web.HTTPBadRequest() - return message + date = datetime.datetime.strptime(request.match_info['date'], '%Y-%m-%d').replace(tzinfo = datetime.timezone.utc) + #TODO Implement this in a better way... + fn = date.strftime('%Y-%m.log') + with open(os.path.join(self.config['storage']['path'], request.match_info["path"], fn), 'r') as fp: + lines = [l.strip().split(' ', 2) for l in fp] + dateStart = date.timestamp() + dateEnd = (date + datetime.timedelta(days = 1)).timestamp() + return aiohttp.web.Response(text = f'Previous day Next day

' + '
'.join(' '.join(l) for l in lines if dateStart <= float(l[0]) < dateEnd) + '', content_type = 'text/html') + + async def search(self, request): + self.logger.info(f'Received request {id(request)} from {request.remote!r} for {request.path!r}') + + if 'q' not in request.query: + return aiohttp.web.Response(text = '
', content_type = 'text/html') + + proc = await asyncio.create_subprocess_exec('grep', '--fixed-strings', '--recursive', request.query['q'], os.path.join(self.config['storage']['path'], request.match_info["path"], ''), stdout = asyncio.subprocess.PIPE) + #TODO Limit size and runtime + stdout, _ = await proc.communicate() + return aiohttp.web.Response(text = '' + '
'.join(stdout.decode('utf-8').splitlines()) + '', content_type = 'text/html') def configure_logging(config):