Browse Source

Very basic web interface

master
JustAnotherArchivist 3 years ago
parent
commit
ef07cd4af3
1 changed files with 51 additions and 47 deletions
  1. +51
    -47
      irclog.py

+ 51
- 47
irclog.py View File

@@ -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 ""}<a href="/{path}/today">{channel}</a>')
return aiohttp.web.Response(text = f'<html><body>{"<br />".join(lines)}</body></html>', 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'<html><body><a href="/{request.match_info["path"]}/{(date - datetime.timedelta(days = 1)).strftime("%Y-%m-%d")}">Previous day</a> <a href="/{request.match_info["path"]}/{(date + datetime.timedelta(days = 1)).strftime("%Y-%m-%d")}">Next day</a><br /><br />' + '<br />'.join(' '.join(l) for l in lines if dateStart <= float(l[0]) < dateEnd) + '</body></html>', 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 = '<html><body><form><input name="q" /><input type="submit" value="Search!" /></form></body></html>', 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 = '<html><body>' + '<br />'.join(stdout.decode('utf-8').splitlines()) + '</body></html>', content_type = 'text/html')


def configure_logging(config):


Loading…
Cancel
Save