From e491f54b241fda2ba1795cfb69befd7c7fd1ac92 Mon Sep 17 00:00:00 2001 From: n9k Date: Sun, 12 Jun 2022 22:23:17 +0000 Subject: [PATCH] Control socket (WIP) --- anonstream/__init__.py | 7 ++ anonstream/config.py | 1 + anonstream/control/__init__.py | 0 anonstream/control/commands.py | 0 anonstream/control/server.py | 125 +++++++++++++++++++++++++++++++++ config.toml | 3 + 6 files changed, 136 insertions(+) create mode 100644 anonstream/control/__init__.py create mode 100644 anonstream/control/commands.py create mode 100644 anonstream/control/server.py diff --git a/anonstream/__init__.py b/anonstream/__init__.py index 8579eac..6d59fb3 100644 --- a/anonstream/__init__.py +++ b/anonstream/__init__.py @@ -59,6 +59,13 @@ def create_app(config_file): @app.before_serving async def startup(): + # Start control server + from anonstream.control.server import start_control_server_at + async def start_control_server(): + return await start_control_server_at(app.config['CONTROL_ADDRESS']) + app.add_background_task(start_control_server) + + # Create routes and background tasks import anonstream.routes import anonstream.tasks diff --git a/anonstream/config.py b/anonstream/config.py index 0a489c2..204e306 100644 --- a/anonstream/config.py +++ b/anonstream/config.py @@ -13,6 +13,7 @@ def update_flask_from_toml(toml_config, flask_config): flask_config.update({ 'SECRET_KEY_STRING': toml_config['secret_key'], 'SECRET_KEY': toml_config['secret_key'].encode(), + 'CONTROL_ADDRESS': toml_config['control']['address'], 'AUTH_USERNAME': toml_config['auth']['username'], 'AUTH_PWHASH': auth_pwhash, 'AUTH_TOKEN': generate_token(), diff --git a/anonstream/control/__init__.py b/anonstream/control/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/anonstream/control/commands.py b/anonstream/control/commands.py new file mode 100644 index 0000000..e69de29 diff --git a/anonstream/control/server.py b/anonstream/control/server.py new file mode 100644 index 0000000..20b9082 --- /dev/null +++ b/anonstream/control/server.py @@ -0,0 +1,125 @@ +import asyncio +import json + +from anonstream.stream import get_stream_title + +def start_control_server_at(address): + return asyncio.start_unix_server( + handle_control_client, + address, + ) + +async def handle_control_client(reader, writer): + while line := await reader.readline(): + try: + request = line.decode('utf-8') + except UnicodeDecodeError as e: + response = f'error: {e}' + else: + method, args, normal, response = await parse(request) + if method is None: + pass + elif normal is not None: + writer.write(f'{normal}\n'.encode()) + elif response is not None: + writer.write(f'error: '.encode()) + if response is not None: + writer.write(response.encode()) + await writer.drain() + else: + writer.close() + break + +async def parse(request): + try: + method, *args = request.split() + except ValueError: + method, args, normal, response = None, [], None, '' + else: + match method: + case 'help': + normal_args, response = await parse_help(args) + case 'exit': + normal_args, response = await parse_exit(args) + case 'title': + normal_args, response = await parse_title(args) + case _: + normal_args = None + response = f"method {method!r} is unknown, try 'help'\n" + if normal_args is None: + normal = None + if response is None: + response = f"command {args[0]!r} is unknown, try {f'{method} help'!r}\n" + elif len(normal_args) == 0: + normal = method + else: + normal = f'{method} {" ".join(normal_args)}' or method + return method, args, normal, response + +async def parse_help(args): + match args: + case []: + normal_args = [] + response = ( + 'Usage: METHOD {COMMAND | help}\n' + 'Examples:\n' + ' help.......................show this help message\n' + ' exit.......................close the connection\n' + ' title [show [CODEC]].......show the stream title\n' + ' title set TITLE............set the stream title\n' + ' user [show]................show a list of users\n' + ' user set USER ATTR VALUE...set an attribute of a user\n' + ) + case ['help']: + normal_args = ['help'] + response = ( + 'Usage: help\n' + 'show usage syntax and examples\n' + ) + case _: + normal_args = None + response = None + return normal_args, response + +async def parse_exit(args): + match args: + case []: + normal_args = [] + response = None + case ['help']: + normal_args = ['help'] + response = ( + 'Usage: {exit | quit}\n' + 'close the connection\n' + ) + case _: + normal_args = None + response = None + return normal_args, response + +async def parse_title(args): + match args: + case [] | ['show'] | ['show', 'json']: + normal_args = ['show', 'json'] + response = json.dumps(await get_stream_title()) + '\n' + case ['show', 'utf-8']: + normal_args = ['show'] + response = await get_stream_title() + '\n' + case ['show', arg, *_]: + normal_args = None + response = f"option {arg!r} is unknown, try 'title help'\n" + case ['help']: + normal_args = ['help'] + response = ( + 'Usage: title {show [CODEC] | set TITLE}\n' + 'Commands:\n' + ' title show [CODEC].....show the stream title\n' + ' title set TITLE........set the stream title to TITLE collapsing whitespace\n' + 'Arguments:\n' + ' CODEC..................=[utf-8 | json]\n' + ' TITLE..................a UTF-8-encoded string\n' + ) + case _: + normal_args = None + response = None + return normal_args, response diff --git a/config.toml b/config.toml index 672a57e..717c64e 100644 --- a/config.toml +++ b/config.toml @@ -1,5 +1,8 @@ secret_key = "place secret key here" +[control] +address = "control.sock" + [auth] username = "broadcaster"