PageRenderTime 57ms CodeModel.GetById 33ms RepoModel.GetById 0ms app.codeStats 0ms

/app.py

https://gitlab.com/johannes.valbjorn/CallRoulette
Python | 214 lines | 198 code | 16 blank | 0 comment | 13 complexity | 78894424ba35c814883bb642cdec2d02 MD5 | raw file
  1. import asyncio
  2. import json
  3. import logging
  4. import mimetypes
  5. import os
  6. import signal
  7. import sys
  8. from aiohttp import web
  9. logging.basicConfig(level=logging.DEBUG)
  10. log = logging.getLogger('CallRoulette')
  11. BASE_DIR = os.path.dirname(__file__)
  12. STATIC_FILES = os.path.join(BASE_DIR, 'static')
  13. INDEX_FILE = os.path.join(BASE_DIR, 'index.html')
  14. READ_TIMEOUT = 5.0
  15. class LazyFileHandler:
  16. def __init__(self, filename, content_type):
  17. self.filename = filename
  18. self.content_type = content_type
  19. self.data = None
  20. @asyncio.coroutine
  21. def __call__(self, request):
  22. if self.data is None:
  23. try:
  24. with open(self.filename, 'rb') as f:
  25. self.data = f.read()
  26. except IOError:
  27. log.warning('Could not load %s file' % self.filename)
  28. raise web.HTTPNotFound()
  29. return web.Response(body=self.data, content_type=self.content_type)
  30. class StaticFilesHandler:
  31. def __init__(self, base_path):
  32. self.base_path = base_path
  33. self.cache = {}
  34. @asyncio.coroutine
  35. def __call__(self, request):
  36. path = request.match_info['path']
  37. try:
  38. data, content_type = self.cache[path]
  39. except KeyError:
  40. full_path = os.path.join(self.base_path, path)
  41. try:
  42. with open(full_path, 'rb') as f:
  43. content_type, encoding = mimetypes.guess_type(full_path, strict=False)
  44. data = f.read()
  45. except IOError:
  46. log.warning('Could not open %s file' % path)
  47. raise web.HTTPNotFound()
  48. self.cache[path] = data, content_type
  49. log.debug('Loaded file %s (%s)' % (path, content_type))
  50. return web.Response(body=data, content_type=content_type)
  51. class Connection:
  52. def __init__(self, ws):
  53. self.ws = ws
  54. self._closed = False
  55. @property
  56. def closed(self):
  57. return self._closed or self.ws.closing
  58. @asyncio.coroutine
  59. def read(self, timeout=None):
  60. try:
  61. data = yield from asyncio.wait_for(self.ws.receive_str(), timeout)
  62. return data
  63. except asyncio.TimeoutError:
  64. log.warning('Timeout reading from socket')
  65. self.close()
  66. except web.WSClientDisconnectedError as e:
  67. log.info('WS client disconnected: %d:%s' % (e.code, e.message))
  68. self.close()
  69. return ''
  70. def write(self, data):
  71. self.ws.send_str(data)
  72. def close(self):
  73. if self._closed:
  74. return
  75. if not self.ws.closing:
  76. self.ws.close()
  77. self._closed = True
  78. @asyncio.coroutine
  79. def wait_closed(self):
  80. try:
  81. yield from self.ws.wait_closed()
  82. except web.WSClientDisconnectedError:
  83. pass
  84. class WebSocketHandler:
  85. def __init__(self):
  86. self.waiter = None
  87. @asyncio.coroutine
  88. def __call__(self, request):
  89. ws = web.WebSocketResponse(protocols=('callroulette',))
  90. ws.start(request)
  91. conn = Connection(ws)
  92. if self.waiter is None:
  93. self.waiter = asyncio.Future()
  94. fs = [conn.read(), self.waiter]
  95. done, pending = yield from asyncio.wait(fs, return_when=asyncio.FIRST_COMPLETED)
  96. if self.waiter not in done:
  97. # the connection was most likely closed
  98. self.waiter = None
  99. return ws
  100. other = self.waiter.result()
  101. self.waiter = None
  102. reading_task = pending.pop()
  103. asyncio.async(self.run_roulette(conn, other, reading_task))
  104. else:
  105. self.waiter.set_result(conn)
  106. yield from conn.wait_closed()
  107. return ws
  108. @asyncio.coroutine
  109. def run_roulette(self, peerA, peerB, initial_reading_task):
  110. log.info('Running roulette: %s, %s' % (peerA, peerB))
  111. def _close_connections():
  112. peerA.close()
  113. peerB.close()
  114. # request offer
  115. data = dict(type='offer_request');
  116. peerA.write(json.dumps(data))
  117. # get offer
  118. # I cannot seem to cancel the reading task that was started before, which is the
  119. # only way one can know if the connection was closed, so use if for the initial
  120. # reading
  121. try:
  122. data = yield from asyncio.wait_for(initial_reading_task, READ_TIMEOUT)
  123. except asyncio.TimeoutError:
  124. data = ''
  125. if not data:
  126. return _close_connections()
  127. data = json.loads(data)
  128. if data.get('type') != 'offer' or not data.get('sdp'):
  129. log.warning('Invalid offer received')
  130. return _close_connections()
  131. # send offer
  132. data = dict(type='offer', sdp=data['sdp']);
  133. peerB.write(json.dumps(data))
  134. # wait for answer
  135. data = yield from peerB.read(timeout=READ_TIMEOUT)
  136. if not data:
  137. return _close_connections()
  138. data = json.loads(data)
  139. if data.get('type') != 'answer' or not data.get('sdp'):
  140. log.warning('Invalid answer received')
  141. return _close_connections()
  142. # dispatch answer
  143. data = dict(type='answer', sdp=data['sdp']);
  144. peerA.write(json.dumps(data))
  145. # wait for end
  146. fs = [peerA.read(), peerB.read()]
  147. yield from asyncio.wait(fs, return_when=asyncio.FIRST_COMPLETED)
  148. # close connections
  149. return _close_connections()
  150. @asyncio.coroutine
  151. def init(loop):
  152. app = web.Application(loop=loop)
  153. app.router.add_route('GET', '/', LazyFileHandler(INDEX_FILE, 'text/html'))
  154. app.router.add_route('GET', '/ws', WebSocketHandler())
  155. app.router.add_route('GET', '/static/{path:.*}', StaticFilesHandler(STATIC_FILES))
  156. handler = app.make_handler()
  157. server = yield from loop.create_server(handler, '0.0.0.0', 8080)
  158. print("Server started at 0.0.0.0:8080")
  159. return server, handler
  160. loop = asyncio.new_event_loop()
  161. asyncio.set_event_loop(loop)
  162. server, handler = loop.run_until_complete(init(loop))
  163. loop.add_signal_handler(signal.SIGINT, loop.stop)
  164. loop.run_forever()
  165. server.close()
  166. tasks = [server.wait_closed(), handler.finish_connections()]
  167. loop.run_until_complete(asyncio.wait(tasks, loop=loop))
  168. del tasks
  169. loop.close()
  170. sys.exit(0)