|
| 1 | +import logging |
| 2 | +logging.basicConfig(level=logging.INFO) |
1 | 3 | import os
|
| 4 | +import argparse |
2 | 5 | import asyncio
|
3 | 6 | import json
|
4 | 7 | import simpleobsws
|
5 | 8 | import aiohttp
|
6 | 9 | from aiohttp import web
|
7 | 10 | from configparser import ConfigParser
|
8 | 11 |
|
9 |
| -config = ConfigParser() |
10 |
| -config.read('config.ini') |
11 |
| -httpAddress = config.get('http', 'bind_to_address') |
12 |
| -httpPort = config.getint('http', 'bind_to_port') |
13 |
| -httpAuthKey = config.get('http', 'authentication_key') |
14 |
| -if httpAuthKey: |
15 |
| - print('Starting HTTP server with AuthKey set to "{}"'.format(httpAuthKey)) |
16 |
| -else: |
17 |
| - print('Starting HTTP server without authentication.') |
18 |
| - httpAuthKey = None |
19 |
| -wsAddress = config.get('obsws', 'ws_address') |
20 |
| -wsPort = config.getint('obsws', 'ws_port') |
21 |
| -wsPassword = config.get('obsws', 'ws_password') |
22 | 12 | loop = asyncio.get_event_loop()
|
23 |
| -ws = simpleobsws.WebSocketClient(url='ws://{}:{}'.format(wsAddress, wsPort), password=wsPassword) |
| 13 | +app = web.Application() |
| 14 | +ws = None |
| 15 | + |
| 16 | +# Make aiohttp shut up |
| 17 | +aiohttpLogger = logging.getLogger('aiohttp') |
| 18 | +aiohttpLogger.setLevel(logging.WARNING) |
| 19 | + |
| 20 | +def fail_response(comment): |
| 21 | + return web.json_response({'result': False, 'comment': comment}) |
24 | 22 |
|
25 |
| -def statusmessage(message): |
26 |
| - print(str(message) + '... ', end='', flush=True) |
| 23 | +def validate_request(request): |
| 24 | + if not httpAuthKey: |
| 25 | + return True, None |
| 26 | + if 'Authorization' not in request.headers: |
| 27 | + return False, 'You are missing the `Authorization` header.' |
| 28 | + if request.headers['Authorization'] != httpAuthKey: |
| 29 | + return False, 'Invalid authorization key.' |
| 30 | + return True, None |
| 31 | + |
| 32 | +async def get_json(request): |
| 33 | + try: |
| 34 | + return await request.json() |
| 35 | + except json.decoder.JSONDecodeError: |
| 36 | + return None |
27 | 37 |
|
28 | 38 | def response_to_object(response: simpleobsws.RequestResponse):
|
29 | 39 | ret = {}
|
30 | 40 | ret['requestType'] = response.requestType
|
31 | 41 | ret['requestStatus'] = {'result': response.requestStatus.result, 'code': response.requestStatus.code}
|
32 | 42 | if response.requestStatus.comment:
|
33 | 43 | ret['requestStatus']['comment'] = response.requestStatus.comment
|
34 |
| - if ret.has_data(): |
| 44 | + if response.responseData: |
35 | 45 | ret['responseData'] = response.responseData
|
36 | 46 | return ret
|
37 | 47 |
|
38 |
| -async def handle_emit_request(request): |
39 |
| - """Handler function for all emit-based HTTP requests. Assumes that you know what you are doing because it will never return an error.""" |
40 |
| - if ('AuthKey' not in request.headers) and httpAuthKey != None: |
41 |
| - return web.json_response({'status': False, 'comment': 'AuthKey header is required.'}) |
42 |
| - if httpAuthKey == None or (request.headers['AuthKey'] == httpAuthKey): |
43 |
| - requesttype = request.match_info['type'] |
44 |
| - try: |
45 |
| - requestdata = await request.json() |
46 |
| - except json.decoder.JSONDecodeError: |
47 |
| - requestdata = None |
48 |
| - req = simpleobsws.Request(requesttype, requestdata) |
| 48 | +async def request_callback(request, emit): |
| 49 | + if not ws or not ws.is_identified(): |
| 50 | + return fail_response('obs-websocket is not connected.') |
| 51 | + authOk, comment = validate_request(request) |
| 52 | + if not authOk: |
| 53 | + return fail_response(comment) |
| 54 | + |
| 55 | + requestType = request.match_info.get('requestType') |
| 56 | + if not requestType: |
| 57 | + return fail_response('Your path is missing a request type.') |
| 58 | + requestData = await get_json(request) |
| 59 | + req = simpleobsws.Request(requestType, requestData) |
| 60 | + |
| 61 | + logging.info('Performing request for request type `{}` | Emit: {} | Client IP: {}'.format(requestType, emit, request.remote)) |
| 62 | + logging.debug('Request data:\n{}'.format(requestData)) |
| 63 | + |
| 64 | + if emit: |
49 | 65 | await ws.emit(req)
|
50 |
| - return web.json_response({'status': True}) |
51 |
| - else: |
52 |
| - return web.json_response({'status': False, 'comment': 'Bad AuthKey'}) |
53 |
| - |
54 |
| -async def handle_call_request(request): |
55 |
| - """Handler function for all call-based HTTP requests.""" |
56 |
| - if ('AuthKey' not in request.headers) and httpAuthKey != None: |
57 |
| - return web.json_response({'status': False, 'comment': 'AuthKey header is required.'}) |
58 |
| - if httpAuthKey == None or (request.headers['AuthKey'] == httpAuthKey): |
59 |
| - requesttype = request.match_info['type'] |
60 |
| - try: |
61 |
| - requestdata = await request.json() |
62 |
| - except json.decoder.JSONDecodeError: |
63 |
| - if (await request.text()) == '': |
64 |
| - requestdata = None |
65 |
| - try: |
66 |
| - req = simpleobsws.Request(requesttype, requestdata) |
67 |
| - ret = await ws.call(req) |
68 |
| - responsedata = {'status': True, 'response': response_to_object(ret)} |
69 |
| - except simpleobsws.MessageTimeout: |
70 |
| - responsedata = {'status': False, 'comment': 'The obs-websocket request timed out.'} |
71 |
| - return web.json_response(responsedata) |
72 |
| - else: |
73 |
| - return web.json_response({'status': False, 'comment': 'Bad AuthKey'}) |
| 66 | + return web.json_response({'result': True}) |
| 67 | + |
| 68 | + try: |
| 69 | + ret = await ws.call(req) |
| 70 | + except simpleobsws.MessageTimeout: |
| 71 | + return fail_response('The obs-websocket request timed out.') |
| 72 | + responseData = {'result': True, 'requestResult': response_to_object(ret)} |
| 73 | + return web.json_response(responseData) |
| 74 | + |
| 75 | +async def call_request_callback(request): |
| 76 | + return await request_callback(request, False) |
| 77 | + |
| 78 | +async def emit_request_callback(request): |
| 79 | + return await request_callback(request, True) |
74 | 80 |
|
75 |
| -async def init_ws(): |
| 81 | +async def init(): |
| 82 | + logging.info('Connecting to obs-websocket...') |
76 | 83 | await ws.connect()
|
77 | 84 | if not await ws.wait_until_identified():
|
78 |
| - print('Identification with obs-websocket timed out. Could it be using 4.x?') |
| 85 | + logging.error('Identification with obs-websocket timed out. Could it be using 4.x?') |
79 | 86 | return False
|
| 87 | + logging.info('Connected to obs-websocket.') |
80 | 88 | return True
|
81 | 89 |
|
82 |
| -app = web.Application() |
83 |
| -app.add_routes([web.post('/emit/{type}', handle_emit_request), web.post('/call/{type}', handle_call_request)]) |
84 |
| -statusmessage('Connecting to obs-websocket') |
85 |
| -if not loop.run_until_complete(init_ws()): |
86 |
| - os._exit(1) |
87 |
| -print('[Connected.]') |
88 |
| -try: |
89 |
| - web.run_app(app, host=httpAddress, port=httpPort) |
90 |
| -except KeyboardInterrupt: |
91 |
| - print('Shutting down...') |
| 90 | +async def shutdown(app): |
| 91 | + logging.info('Shutting down...') |
| 92 | + if ws.is_identified(): |
| 93 | + logging.info('Disconnecting from obs-websocket...') |
| 94 | + await ws.disconnect() |
| 95 | + logging.info('Disconnected from obs-websocket.') |
| 96 | + else: |
| 97 | + logging.info('Not connected to obs-websocket, not disconnecting.') |
| 98 | + |
| 99 | +if __name__ == '__main__': |
| 100 | + config = ConfigParser() |
| 101 | + config.read('config.ini') |
| 102 | + |
| 103 | + # Command line args take priority, with fallback to config.ini, and further fallback to defaults. |
| 104 | + parser = argparse.ArgumentParser(description='A Python-based program that provides HTTP endpoints for obs-websocket') |
| 105 | + parser.add_argument('--http_bind_addres', dest='http_bind_addres', default=config.get('http', 'bind_to_address', fallback='0.0.0.0')) |
| 106 | + parser.add_argument('--http_bind_port', dest='http_bind_port', type=int, default=config.getint('http', 'bind_to_port', fallback=4445)) |
| 107 | + parser.add_argument('--http_auth_key', dest='http_auth_key', default=config.get('http', 'authentication_key', fallback='')) |
| 108 | + parser.add_argument('--ws_url', dest='ws_url', default=config.get('obsws', 'ws_url', fallback='ws://127.0.0.1:4444')) |
| 109 | + parser.add_argument('--ws_password', dest='ws_password', default=config.get('obsws', 'ws_password', fallback='')) |
| 110 | + args = parser.parse_args() |
| 111 | + |
| 112 | + httpAddress = args.http_bind_addres |
| 113 | + httpPort = args.http_bind_port |
| 114 | + httpAuthKey = args.http_auth_key |
| 115 | + wsUrl = args.ws_url |
| 116 | + wsPassword = args.ws_password |
| 117 | + |
| 118 | + if httpAuthKey: |
| 119 | + logging.info('HTTP server will start with AuthKey set to `{}`'.format(httpAuthKey)) |
| 120 | + else: |
| 121 | + logging.info('HTTP server will start without authentication.') |
| 122 | + httpAuthKey = None |
| 123 | + |
| 124 | + ws = simpleobsws.WebSocketClient(url=wsUrl, password=wsPassword) |
| 125 | + |
| 126 | + if not loop.run_until_complete(init()): |
| 127 | + os._exit(1) |
| 128 | + |
| 129 | + app.add_routes([web.post('/call/{requestType}', call_request_callback), web.post('/emit/{requestType}', emit_request_callback)]) |
| 130 | + app.on_cleanup.append(shutdown) |
| 131 | + |
| 132 | + web.run_app(app, host=httpAddress, port=httpPort, loop=loop) |
0 commit comments