Вы не можете выбрать более 25 тем Темы должны начинаться с буквы или цифры, могут содержать дефисы(-) и должны содержать не более 35 символов.

464 строки
20KB

  1. import asyncio
  2. import six
  3. from six.moves import urllib
  4. from . import exceptions
  5. from . import packet
  6. from . import server
  7. from . import asyncio_socket
  8. class AsyncServer(server.Server):
  9. """An Engine.IO server for asyncio.
  10. This class implements a fully compliant Engine.IO web server with support
  11. for websocket and long-polling transports, compatible with the asyncio
  12. framework on Python 3.5 or newer.
  13. :param async_mode: The asynchronous model to use. See the Deployment
  14. section in the documentation for a description of the
  15. available options. Valid async modes are "aiohttp",
  16. "sanic", "tornado" and "asgi". If this argument is not
  17. given, "aiohttp" is tried first, followed by "sanic",
  18. "tornado", and finally "asgi". The first async mode that
  19. has all its dependencies installed is the one that is
  20. chosen.
  21. :param ping_timeout: The time in seconds that the client waits for the
  22. server to respond before disconnecting.
  23. :param ping_interval: The interval in seconds at which the client pings
  24. the server.
  25. :param max_http_buffer_size: The maximum size of a message when using the
  26. polling transport.
  27. :param allow_upgrades: Whether to allow transport upgrades or not.
  28. :param http_compression: Whether to compress packages when using the
  29. polling transport.
  30. :param compression_threshold: Only compress messages when their byte size
  31. is greater than this value.
  32. :param cookie: Name of the HTTP cookie that contains the client session
  33. id. If set to ``None``, a cookie is not sent to the client.
  34. :param cors_allowed_origins: Origin or list of origins that are allowed to
  35. connect to this server. Only the same origin
  36. is allowed by default. Set this argument to
  37. ``'*'`` to allow all origins, or to ``[]`` to
  38. disable CORS handling.
  39. :param cors_credentials: Whether credentials (cookies, authentication) are
  40. allowed in requests to this server.
  41. :param logger: To enable logging set to ``True`` or pass a logger object to
  42. use. To disable logging set to ``False``.
  43. :param json: An alternative json module to use for encoding and decoding
  44. packets. Custom json modules must have ``dumps`` and ``loads``
  45. functions that are compatible with the standard library
  46. versions.
  47. :param async_handlers: If set to ``True``, run message event handlers in
  48. non-blocking threads. To run handlers synchronously,
  49. set to ``False``. The default is ``True``.
  50. :param kwargs: Reserved for future extensions, any additional parameters
  51. given as keyword arguments will be silently ignored.
  52. """
  53. def is_asyncio_based(self):
  54. return True
  55. def async_modes(self):
  56. return ['aiohttp', 'sanic', 'tornado', 'asgi']
  57. def attach(self, app, engineio_path='engine.io'):
  58. """Attach the Engine.IO server to an application."""
  59. engineio_path = engineio_path.strip('/')
  60. self._async['create_route'](app, self, '/{}/'.format(engineio_path))
  61. async def send(self, sid, data, binary=None):
  62. """Send a message to a client.
  63. :param sid: The session id of the recipient client.
  64. :param data: The data to send to the client. Data can be of type
  65. ``str``, ``bytes``, ``list`` or ``dict``. If a ``list``
  66. or ``dict``, the data will be serialized as JSON.
  67. :param binary: ``True`` to send packet as binary, ``False`` to send
  68. as text. If not given, unicode (Python 2) and str
  69. (Python 3) are sent as text, and str (Python 2) and
  70. bytes (Python 3) are sent as binary.
  71. Note: this method is a coroutine.
  72. """
  73. try:
  74. socket = self._get_socket(sid)
  75. except KeyError:
  76. # the socket is not available
  77. self.logger.warning('Cannot send to sid %s', sid)
  78. return
  79. await socket.send(packet.Packet(packet.MESSAGE, data=data,
  80. binary=binary))
  81. async def get_session(self, sid):
  82. """Return the user session for a client.
  83. :param sid: The session id of the client.
  84. The return value is a dictionary. Modifications made to this
  85. dictionary are not guaranteed to be preserved. If you want to modify
  86. the user session, use the ``session`` context manager instead.
  87. """
  88. socket = self._get_socket(sid)
  89. return socket.session
  90. async def save_session(self, sid, session):
  91. """Store the user session for a client.
  92. :param sid: The session id of the client.
  93. :param session: The session dictionary.
  94. """
  95. socket = self._get_socket(sid)
  96. socket.session = session
  97. def session(self, sid):
  98. """Return the user session for a client with context manager syntax.
  99. :param sid: The session id of the client.
  100. This is a context manager that returns the user session dictionary for
  101. the client. Any changes that are made to this dictionary inside the
  102. context manager block are saved back to the session. Example usage::
  103. @eio.on('connect')
  104. def on_connect(sid, environ):
  105. username = authenticate_user(environ)
  106. if not username:
  107. return False
  108. with eio.session(sid) as session:
  109. session['username'] = username
  110. @eio.on('message')
  111. def on_message(sid, msg):
  112. async with eio.session(sid) as session:
  113. print('received message from ', session['username'])
  114. """
  115. class _session_context_manager(object):
  116. def __init__(self, server, sid):
  117. self.server = server
  118. self.sid = sid
  119. self.session = None
  120. async def __aenter__(self):
  121. self.session = await self.server.get_session(sid)
  122. return self.session
  123. async def __aexit__(self, *args):
  124. await self.server.save_session(sid, self.session)
  125. return _session_context_manager(self, sid)
  126. async def disconnect(self, sid=None):
  127. """Disconnect a client.
  128. :param sid: The session id of the client to close. If this parameter
  129. is not given, then all clients are closed.
  130. Note: this method is a coroutine.
  131. """
  132. if sid is not None:
  133. try:
  134. socket = self._get_socket(sid)
  135. except KeyError: # pragma: no cover
  136. # the socket was already closed or gone
  137. pass
  138. else:
  139. await socket.close()
  140. del self.sockets[sid]
  141. else:
  142. await asyncio.wait([client.close()
  143. for client in six.itervalues(self.sockets)])
  144. self.sockets = {}
  145. async def handle_request(self, *args, **kwargs):
  146. """Handle an HTTP request from the client.
  147. This is the entry point of the Engine.IO application. This function
  148. returns the HTTP response to deliver to the client.
  149. Note: this method is a coroutine.
  150. """
  151. translate_request = self._async['translate_request']
  152. if asyncio.iscoroutinefunction(translate_request):
  153. environ = await translate_request(*args, **kwargs)
  154. else:
  155. environ = translate_request(*args, **kwargs)
  156. if self.cors_allowed_origins != []:
  157. # Validate the origin header if present
  158. # This is important for WebSocket more than for HTTP, since
  159. # browsers only apply CORS controls to HTTP.
  160. origin = environ.get('HTTP_ORIGIN')
  161. if origin:
  162. allowed_origins = self._cors_allowed_origins(environ)
  163. if allowed_origins is not None and origin not in \
  164. allowed_origins:
  165. self.logger.info(origin + ' is not an accepted origin.')
  166. r = self._bad_request()
  167. make_response = self._async['make_response']
  168. response = make_response(r['status'], r['headers'],
  169. r['response'], environ)
  170. return response
  171. method = environ['REQUEST_METHOD']
  172. query = urllib.parse.parse_qs(environ.get('QUERY_STRING', ''))
  173. sid = query['sid'][0] if 'sid' in query else None
  174. b64 = False
  175. jsonp = False
  176. jsonp_index = None
  177. if 'b64' in query:
  178. if query['b64'][0] == "1" or query['b64'][0].lower() == "true":
  179. b64 = True
  180. if 'j' in query:
  181. jsonp = True
  182. try:
  183. jsonp_index = int(query['j'][0])
  184. except (ValueError, KeyError, IndexError):
  185. # Invalid JSONP index number
  186. pass
  187. if jsonp and jsonp_index is None:
  188. self.logger.warning('Invalid JSONP index number')
  189. r = self._bad_request()
  190. elif method == 'GET':
  191. if sid is None:
  192. transport = query.get('transport', ['polling'])[0]
  193. if transport != 'polling' and transport != 'websocket':
  194. self.logger.warning('Invalid transport %s', transport)
  195. r = self._bad_request()
  196. else:
  197. r = await self._handle_connect(environ, transport,
  198. b64, jsonp_index)
  199. else:
  200. if sid not in self.sockets:
  201. self.logger.warning('Invalid session %s', sid)
  202. r = self._bad_request()
  203. else:
  204. socket = self._get_socket(sid)
  205. try:
  206. packets = await socket.handle_get_request(environ)
  207. if isinstance(packets, list):
  208. r = self._ok(packets, b64=b64,
  209. jsonp_index=jsonp_index)
  210. else:
  211. r = packets
  212. except exceptions.EngineIOError:
  213. if sid in self.sockets: # pragma: no cover
  214. await self.disconnect(sid)
  215. r = self._bad_request()
  216. if sid in self.sockets and self.sockets[sid].closed:
  217. del self.sockets[sid]
  218. elif method == 'POST':
  219. if sid is None or sid not in self.sockets:
  220. self.logger.warning('Invalid session %s', sid)
  221. r = self._bad_request()
  222. else:
  223. socket = self._get_socket(sid)
  224. try:
  225. await socket.handle_post_request(environ)
  226. r = self._ok(jsonp_index=jsonp_index)
  227. except exceptions.EngineIOError:
  228. if sid in self.sockets: # pragma: no cover
  229. await self.disconnect(sid)
  230. r = self._bad_request()
  231. except: # pragma: no cover
  232. # for any other unexpected errors, we log the error
  233. # and keep going
  234. self.logger.exception('post request handler error')
  235. r = self._ok(jsonp_index=jsonp_index)
  236. elif method == 'OPTIONS':
  237. r = self._ok()
  238. else:
  239. self.logger.warning('Method %s not supported', method)
  240. r = self._method_not_found()
  241. if not isinstance(r, dict):
  242. return r
  243. if self.http_compression and \
  244. len(r['response']) >= self.compression_threshold:
  245. encodings = [e.split(';')[0].strip() for e in
  246. environ.get('HTTP_ACCEPT_ENCODING', '').split(',')]
  247. for encoding in encodings:
  248. if encoding in self.compression_methods:
  249. r['response'] = \
  250. getattr(self, '_' + encoding)(r['response'])
  251. r['headers'] += [('Content-Encoding', encoding)]
  252. break
  253. cors_headers = self._cors_headers(environ)
  254. make_response = self._async['make_response']
  255. if asyncio.iscoroutinefunction(make_response):
  256. response = await make_response(r['status'],
  257. r['headers'] + cors_headers,
  258. r['response'], environ)
  259. else:
  260. response = make_response(r['status'], r['headers'] + cors_headers,
  261. r['response'], environ)
  262. return response
  263. def start_background_task(self, target, *args, **kwargs):
  264. """Start a background task using the appropriate async model.
  265. This is a utility function that applications can use to start a
  266. background task using the method that is compatible with the
  267. selected async mode.
  268. :param target: the target function to execute.
  269. :param args: arguments to pass to the function.
  270. :param kwargs: keyword arguments to pass to the function.
  271. The return value is a ``asyncio.Task`` object.
  272. """
  273. return asyncio.ensure_future(target(*args, **kwargs))
  274. async def sleep(self, seconds=0):
  275. """Sleep for the requested amount of time using the appropriate async
  276. model.
  277. This is a utility function that applications can use to put a task to
  278. sleep without having to worry about using the correct call for the
  279. selected async mode.
  280. Note: this method is a coroutine.
  281. """
  282. return await asyncio.sleep(seconds)
  283. def create_queue(self, *args, **kwargs):
  284. """Create a queue object using the appropriate async model.
  285. This is a utility function that applications can use to create a queue
  286. without having to worry about using the correct call for the selected
  287. async mode. For asyncio based async modes, this returns an instance of
  288. ``asyncio.Queue``.
  289. """
  290. return asyncio.Queue(*args, **kwargs)
  291. def get_queue_empty_exception(self):
  292. """Return the queue empty exception for the appropriate async model.
  293. This is a utility function that applications can use to work with a
  294. queue without having to worry about using the correct call for the
  295. selected async mode. For asyncio based async modes, this returns an
  296. instance of ``asyncio.QueueEmpty``.
  297. """
  298. return asyncio.QueueEmpty
  299. def create_event(self, *args, **kwargs):
  300. """Create an event object using the appropriate async model.
  301. This is a utility function that applications can use to create an
  302. event without having to worry about using the correct call for the
  303. selected async mode. For asyncio based async modes, this returns
  304. an instance of ``asyncio.Event``.
  305. """
  306. return asyncio.Event(*args, **kwargs)
  307. async def _handle_connect(self, environ, transport, b64=False,
  308. jsonp_index=None):
  309. """Handle a client connection request."""
  310. if self.start_service_task:
  311. # start the service task to monitor connected clients
  312. self.start_service_task = False
  313. self.start_background_task(self._service_task)
  314. sid = self._generate_id()
  315. s = asyncio_socket.AsyncSocket(self, sid)
  316. self.sockets[sid] = s
  317. pkt = packet.Packet(
  318. packet.OPEN, {'sid': sid,
  319. 'upgrades': self._upgrades(sid, transport),
  320. 'pingTimeout': int(self.ping_timeout * 1000),
  321. 'pingInterval': int(self.ping_interval * 1000)})
  322. await s.send(pkt)
  323. ret = await self._trigger_event('connect', sid, environ,
  324. run_async=False)
  325. if ret is False:
  326. del self.sockets[sid]
  327. self.logger.warning('Application rejected connection')
  328. return self._unauthorized()
  329. if transport == 'websocket':
  330. ret = await s.handle_get_request(environ)
  331. if s.closed:
  332. # websocket connection ended, so we are done
  333. del self.sockets[sid]
  334. return ret
  335. else:
  336. s.connected = True
  337. headers = None
  338. if self.cookie:
  339. headers = [('Set-Cookie', self.cookie + '=' + sid)]
  340. try:
  341. return self._ok(await s.poll(), headers=headers, b64=b64,
  342. jsonp_index=jsonp_index)
  343. except exceptions.QueueEmpty:
  344. return self._bad_request()
  345. async def _trigger_event(self, event, *args, **kwargs):
  346. """Invoke an event handler."""
  347. run_async = kwargs.pop('run_async', False)
  348. ret = None
  349. if event in self.handlers:
  350. if asyncio.iscoroutinefunction(self.handlers[event]) is True:
  351. if run_async:
  352. return self.start_background_task(self.handlers[event],
  353. *args)
  354. else:
  355. try:
  356. ret = await self.handlers[event](*args)
  357. except asyncio.CancelledError: # pragma: no cover
  358. pass
  359. except:
  360. self.logger.exception(event + ' async handler error')
  361. if event == 'connect':
  362. # if connect handler raised error we reject the
  363. # connection
  364. return False
  365. else:
  366. if run_async:
  367. async def async_handler():
  368. return self.handlers[event](*args)
  369. return self.start_background_task(async_handler)
  370. else:
  371. try:
  372. ret = self.handlers[event](*args)
  373. except:
  374. self.logger.exception(event + ' handler error')
  375. if event == 'connect':
  376. # if connect handler raised error we reject the
  377. # connection
  378. return False
  379. return ret
  380. async def _service_task(self): # pragma: no cover
  381. """Monitor connected clients and clean up those that time out."""
  382. while True:
  383. if len(self.sockets) == 0:
  384. # nothing to do
  385. await self.sleep(self.ping_timeout)
  386. continue
  387. # go through the entire client list in a ping interval cycle
  388. sleep_interval = self.ping_timeout / len(self.sockets)
  389. try:
  390. # iterate over the current clients
  391. for socket in self.sockets.copy().values():
  392. if not socket.closing and not socket.closed:
  393. await socket.check_ping_timeout()
  394. await self.sleep(sleep_interval)
  395. except (SystemExit, KeyboardInterrupt, asyncio.CancelledError):
  396. self.logger.info('service task canceled')
  397. break
  398. except:
  399. if asyncio.get_event_loop().is_closed():
  400. self.logger.info('event loop is closed, exiting service '
  401. 'task')
  402. break
  403. # an unexpected exception has occurred, log it and continue
  404. self.logger.exception('service task exception')