codekingpro/portable-devtools
114k
1import logging2 3import engineio4 5from . import base_server6from . import exceptions7from . import packet8 9default_logger = logging.getLogger('socketio.server')10 11 12class Server(base_server.BaseServer):13 """A Socket.IO server.14 15 This class implements a fully compliant Socket.IO web server with support16 for websocket and long-polling transports.17 18 :param client_manager: The client manager instance that will manage the19 client list. When this is omitted, the client list20 is stored in an in-memory structure, so the use of21 multiple connected servers is not possible.22 :param logger: To enable logging set to ``True`` or pass a logger object to23 use. To disable logging set to ``False``. The default is24 ``False``. Note that fatal errors are logged even when25 ``logger`` is ``False``.26 :param serializer: The serialization method to use when transmitting27 packets. Valid values are ``'default'``, ``'pickle'``,28 ``'msgpack'`` and ``'cbor'``. Alternatively, a subclass29 of the :class:`Packet` class with custom implementations30 of the ``encode()`` and ``decode()`` methods can be31 provided. Client and server must use compatible32 serializers.33 :param json: An alternative json module to use for encoding and decoding34 packets. Custom json modules must have ``dumps`` and ``loads``35 functions that are compatible with the standard library36 versions.37 :param async_handlers: If set to ``True``, event handlers for a client are38 executed in separate threads. To run handlers for a39 client synchronously, set to ``False``. The default40 is ``True``.41 :param always_connect: When set to ``False``, new connections are42 provisory until the connect handler returns43 something other than ``False``, at which point they44 are accepted. When set to ``True``, connections are45 immediately accepted, and then if the connect46 handler returns ``False`` a disconnect is issued.47 Set to ``True`` if you need to emit events from the48 connect handler and your client is confused when it49 receives events before the connection acceptance.50 In any other case use the default of ``False``.51 :param namespaces: a list of namespaces that are accepted, in addition to52 any namespaces for which handlers have been defined. The53 default is `['/']`, which always accepts connections to54 the default namespace. Set to `'*'` to accept all55 namespaces.56 :param kwargs: Connection parameters for the underlying Engine.IO server.57 58 The Engine.IO configuration supports the following settings:59 60 :param async_mode: The asynchronous model to use. See the Deployment61 section in the documentation for a description of the62 available options. Valid async modes are63 ``'threading'``, ``'eventlet'``, ``'gevent'`` and64 ``'gevent_uwsgi'``. If this argument is not given,65 ``'eventlet'`` is tried first, then ``'gevent_uwsgi'``,66 then ``'gevent'``, and finally ``'threading'``.67 The first async mode that has all its dependencies68 installed is then one that is chosen.69 :param ping_interval: The interval in seconds at which the server pings70 the client. The default is 25 seconds. For advanced71 control, a two element tuple can be given, where72 the first number is the ping interval and the second73 is a grace period added by the server.74 :param ping_timeout: The time in seconds that the client waits for the75 server to respond before disconnecting. The default76 is 20 seconds.77 :param max_http_buffer_size: The maximum size that is accepted for incoming78 messages. The default is 1,000,000 bytes. In79 spite of its name, the value set in this80 argument is enforced for HTTP long-polling and81 WebSocket connections.82 :param allow_upgrades: Whether to allow transport upgrades or not. The83 default is ``True``.84 :param http_compression: Whether to compress packages when using the85 polling transport. The default is ``True``.86 :param compression_threshold: Only compress messages when their byte size87 is greater than this value. The default is88 1024 bytes.89 :param cookie: If set to a string, it is the name of the HTTP cookie the90 server sends back tot he client containing the client91 session id. If set to a dictionary, the ``'name'`` key92 contains the cookie name and other keys define cookie93 attributes, where the value of each attribute can be a94 string, a callable with no arguments, or a boolean. If set95 to ``None`` (the default), a cookie is not sent to the96 client.97 :param cors_allowed_origins: Origin or list of origins that are allowed to98 connect to this server. Only the same origin99 is allowed by default. Set this argument to100 ``'*'`` to allow all origins, or to ``[]`` to101 disable CORS handling.102 :param cors_credentials: Whether credentials (cookies, authentication) are103 allowed in requests to this server. The default is104 ``True``.105 :param monitor_clients: If set to ``True``, a background task will ensure106 inactive clients are closed. Set to ``False`` to107 disable the monitoring task (not recommended). The108 default is ``True``.109 :param engineio_logger: To enable Engine.IO logging set to ``True`` or pass110 a logger object to use. To disable logging set to111 ``False``. The default is ``False``. Note that112 fatal errors are logged even when113 ``engineio_logger`` is ``False``.114 """115 def emit(self, event, data=None, to=None, room=None, skip_sid=None,116 namespace=None, callback=None, ignore_queue=False):117 """Emit a custom event to one or more connected clients.118 119 :param event: The event name. It can be any string. The event names120 ``'connect'``, ``'message'`` and ``'disconnect'`` are121 reserved and should not be used.122 :param data: The data to send to the client or clients. Data can be of123 type ``str``, ``bytes``, ``list`` or ``dict``. To send124 multiple arguments, use a tuple where each element is of125 one of the types indicated above.126 :param to: The recipient of the message. This can be set to the127 session ID of a client to address only that client, to any128 custom room created by the application to address all129 the clients in that room, or to a list of custom room130 names. If this argument is omitted the event is broadcasted131 to all connected clients.132 :param room: Alias for the ``to`` parameter.133 :param skip_sid: The session ID of a client to skip when broadcasting134 to a room or to all clients. This can be used to135 prevent a message from being sent to the sender. To136 skip multiple sids, pass a list.137 :param namespace: The Socket.IO namespace for the event. If this138 argument is omitted the event is emitted to the139 default namespace.140 :param callback: If given, this function will be called to acknowledge141 the client has received the message. The arguments142 that will be passed to the function are those provided143 by the client. Callback functions can only be used144 when addressing an individual client.145 :param ignore_queue: Only used when a message queue is configured. If146 set to ``True``, the event is emitted to the147 clients directly, without going through the queue.148 This is more efficient, but only works when a149 single server process is used. It is recommended150 to always leave this parameter with its default151 value of ``False``.152 153 Note: this method is not thread safe. If multiple threads are emitting154 at the same time to the same client, then messages composed of155 multiple packets may end up being sent in an incorrect sequence. Use156 standard concurrency solutions (such as a Lock object) to prevent this157 situation.158 """159 namespace = namespace or '/'160 room = to or room161 self.logger.info('emitting event "%s" to %s [%s]', event,162 room or 'all', namespace)163 self.manager.emit(event, data, namespace, room=room,164 skip_sid=skip_sid, callback=callback,165 ignore_queue=ignore_queue)166 167 def send(self, data, to=None, room=None, skip_sid=None, namespace=None,168 callback=None, ignore_queue=False):169 """Send a message to one or more connected clients.170 171 This function emits an event with the name ``'message'``. Use172 :func:`emit` to issue custom event names.173 174 :param data: The data to send to the client or clients. Data can be of175 type ``str``, ``bytes``, ``list`` or ``dict``. To send176 multiple arguments, use a tuple where each element is of177 one of the types indicated above.178 :param to: The recipient of the message. This can be set to the179 session ID of a client to address only that client, to any180 any custom room created by the application to address all181 the clients in that room, or to a list of custom room182 names. If this argument is omitted the event is broadcasted183 to all connected clients.184 :param room: Alias for the ``to`` parameter.185 :param skip_sid: The session ID of a client to skip when broadcasting186 to a room or to all clients. This can be used to187 prevent a message from being sent to the sender. To188 skip multiple sids, pass a list.189 :param namespace: The Socket.IO namespace for the event. If this190 argument is omitted the event is emitted to the191 default namespace.192 :param callback: If given, this function will be called to acknowledge193 the client has received the message. The arguments194 that will be passed to the function are those provided195 by the client. Callback functions can only be used196 when addressing an individual client.197 :param ignore_queue: Only used when a message queue is configured. If198 set to ``True``, the event is emitted to the199 clients directly, without going through the queue.200 This is more efficient, but only works when a201 single server process is used. It is recommended202 to always leave this parameter with its default203 value of ``False``.204 """205 self.emit('message', data=data, to=to, room=room, skip_sid=skip_sid,206 namespace=namespace, callback=callback,207 ignore_queue=ignore_queue)208 209 def call(self, event, data=None, to=None, sid=None, namespace=None,210 timeout=60, ignore_queue=False):211 """Emit a custom event to a client and wait for the response.212 213 This method issues an emit with a callback and waits for the callback214 to be invoked before returning. If the callback isn't invoked before215 the timeout, then a ``TimeoutError`` exception is raised. If the216 Socket.IO connection drops during the wait, this method still waits217 until the specified timeout.218 219 :param event: The event name. It can be any string. The event names220 ``'connect'``, ``'message'`` and ``'disconnect'`` are221 reserved and should not be used.222 :param data: The data to send to the client or clients. Data can be of223 type ``str``, ``bytes``, ``list`` or ``dict``. To send224 multiple arguments, use a tuple where each element is of225 one of the types indicated above.226 :param to: The session ID of the recipient client.227 :param sid: Alias for the ``to`` parameter.228 :param namespace: The Socket.IO namespace for the event. If this229 argument is omitted the event is emitted to the230 default namespace.231 :param timeout: The waiting timeout. If the timeout is reached before232 the client acknowledges the event, then a233 ``TimeoutError`` exception is raised.234 :param ignore_queue: Only used when a message queue is configured. If235 set to ``True``, the event is emitted to the236 client directly, without going through the queue.237 This is more efficient, but only works when a238 single server process is used. It is recommended239 to always leave this parameter with its default240 value of ``False``.241 242 Note: this method is not thread safe. If multiple threads are emitting243 at the same time to the same client, then messages composed of244 multiple packets may end up being sent in an incorrect sequence. Use245 standard concurrency solutions (such as a Lock object) to prevent this246 situation.247 """248 if to is None and sid is None:249 raise ValueError('Cannot use call() to broadcast.')250 if not self.async_handlers:251 raise RuntimeError(252 'Cannot use call() when async_handlers is False.')253 callback_event = self.eio.create_event()254 callback_args = []255 256 def event_callback(*args):257 callback_args.append(args)258 callback_event.set()259 260 self.emit(event, data=data, room=to or sid, namespace=namespace,261 callback=event_callback, ignore_queue=ignore_queue)262 if not callback_event.wait(timeout=timeout):263 raise exceptions.TimeoutError()264 return callback_args[0] if len(callback_args[0]) > 1 \265 else callback_args[0][0] if len(callback_args[0]) == 1 \266 else None267 268 def enter_room(self, sid, room, namespace=None):269 """Enter a room.270 271 This function adds the client to a room. The :func:`emit` and272 :func:`send` functions can optionally broadcast events to all the273 clients in a room.274 275 :param sid: Session ID of the client.276 :param room: Room name. If the room does not exist it is created.277 :param namespace: The Socket.IO namespace for the event. If this278 argument is omitted the default namespace is used.279 """280 namespace = namespace or '/'281 self.logger.info('%s is entering room %s [%s]', sid, room, namespace)282 self.manager.enter_room(sid, namespace, room)283 284 def leave_room(self, sid, room, namespace=None):285 """Leave a room.286 287 This function removes the client from a room.288 289 :param sid: Session ID of the client.290 :param room: Room name.291 :param namespace: The Socket.IO namespace for the event. If this292 argument is omitted the default namespace is used.293 """294 namespace = namespace or '/'295 self.logger.info('%s is leaving room %s [%s]', sid, room, namespace)296 self.manager.leave_room(sid, namespace, room)297 298 def close_room(self, room, namespace=None):299 """Close a room.300 301 This function removes all the clients from the given room.302 303 :param room: Room name.304 :param namespace: The Socket.IO namespace for the event. If this305 argument is omitted the default namespace is used.306 """307 namespace = namespace or '/'308 self.logger.info('room %s is closing [%s]', room, namespace)309 self.manager.close_room(room, namespace)310 311 def get_session(self, sid, namespace=None):312 """Return the user session for a client.313 314 :param sid: The session id of the client.315 :param namespace: The Socket.IO namespace. If this argument is omitted316 the default namespace is used.317 318 The return value is a dictionary. Modifications made to this319 dictionary are not guaranteed to be preserved unless320 ``save_session()`` is called, or when the ``session`` context manager321 is used.322 """323 namespace = namespace or '/'324 eio_sid = self.manager.eio_sid_from_sid(sid, namespace)325 eio_session = self.eio.get_session(eio_sid)326 return eio_session.setdefault(namespace, {})327 328 def save_session(self, sid, session, namespace=None):329 """Store the user session for a client.330 331 :param sid: The session id of the client.332 :param session: The session dictionary.333 :param namespace: The Socket.IO namespace. If this argument is omitted334 the default namespace is used.335 """336 namespace = namespace or '/'337 eio_sid = self.manager.eio_sid_from_sid(sid, namespace)338 eio_session = self.eio.get_session(eio_sid)339 eio_session[namespace] = session340 341 def session(self, sid, namespace=None):342 """Return the user session for a client with context manager syntax.343 344 :param sid: The session id of the client.345 346 This is a context manager that returns the user session dictionary for347 the client. Any changes that are made to this dictionary inside the348 context manager block are saved back to the session. Example usage::349 350 @sio.on('connect')351 def on_connect(sid, environ):352 username = authenticate_user(environ)353 if not username:354 return False355 with sio.session(sid) as session:356 session['username'] = username357 358 @sio.on('message')359 def on_message(sid, msg):360 with sio.session(sid) as session:361 print('received message from ', session['username'])362 """363 class _session_context_manager(object):364 def __init__(self, server, sid, namespace):365 self.server = server366 self.sid = sid367 self.namespace = namespace368 self.session = None369 370 def __enter__(self):371 self.session = self.server.get_session(sid,372 namespace=namespace)373 return self.session374 375 def __exit__(self, *args):376 self.server.save_session(sid, self.session,377 namespace=namespace)378 379 return _session_context_manager(self, sid, namespace)380 381 def disconnect(self, sid, namespace=None, ignore_queue=False):382 """Disconnect a client.383 384 :param sid: Session ID of the client.385 :param namespace: The Socket.IO namespace to disconnect. If this386 argument is omitted the default namespace is used.387 :param ignore_queue: Only used when a message queue is configured. If388 set to ``True``, the disconnect is processed389 locally, without broadcasting on the queue. It is390 recommended to always leave this parameter with391 its default value of ``False``.392 """393 namespace = namespace or '/'394 if ignore_queue:395 delete_it = self.manager.is_connected(sid, namespace)396 else:397 delete_it = self.manager.can_disconnect(sid, namespace)398 if delete_it:399 self.logger.info('Disconnecting %s [%s]', sid, namespace)400 eio_sid = self.manager.pre_disconnect(sid, namespace=namespace)401 self._send_packet(eio_sid, self.packet_class(402 packet.DISCONNECT, namespace=namespace))403 self._trigger_event('disconnect', namespace, sid)404 self.manager.disconnect(sid, namespace=namespace,405 ignore_queue=True)406 407 def shutdown(self):408 """Stop Socket.IO background tasks.409 410 This method stops all background activity initiated by the Socket.IO411 server. It must be called before shutting down the web server.412 """413 self.logger.info('Socket.IO is shutting down')414 self.eio.shutdown()415 416 def handle_request(self, environ, start_response):417 """Handle an HTTP request from the client.418 419 This is the entry point of the Socket.IO application, using the same420 interface as a WSGI application. For the typical usage, this function421 is invoked by the :class:`Middleware` instance, but it can be invoked422 directly when the middleware is not used.423 424 :param environ: The WSGI environment.425 :param start_response: The WSGI ``start_response`` function.426 427 This function returns the HTTP response body to deliver to the client428 as a byte sequence.429 """430 return self.eio.handle_request(environ, start_response)431 432 def start_background_task(self, target, *args, **kwargs):433 """Start a background task using the appropriate async model.434 435 This is a utility function that applications can use to start a436 background task using the method that is compatible with the437 selected async mode.438 439 :param target: the target function to execute.440 :param args: arguments to pass to the function.441 :param kwargs: keyword arguments to pass to the function.442 443 This function returns an object that represents the background task,444 on which the ``join()`` methond can be invoked to wait for the task to445 complete.446 """447 return self.eio.start_background_task(target, *args, **kwargs)448 449 def sleep(self, seconds=0):450 """Sleep for the requested amount of time using the appropriate async451 model.452 453 This is a utility function that applications can use to put a task to454 sleep without having to worry about using the correct call for the455 selected async mode.456 """457 return self.eio.sleep(seconds)458 459 def instrument(self, auth=None, mode='development', read_only=False,460 server_id=None, namespace='/admin',461 server_stats_interval=2):462 """Instrument the Socket.IO server for monitoring with the `Socket.IO463 Admin UI <https://socket.io/docs/v4/admin-ui/>`_.464 465 :param auth: Authentication credentials for Admin UI access. Set to a466 dictionary with the expected login (usually ``username``467 and ``password``) or a list of dictionaries if more than468 one set of credentials need to be available. For more469 complex authentication methods, set to a callable that470 receives the authentication dictionary as an argument and471 returns ``True`` if the user is allowed or ``False``472 otherwise. To disable authentication, set this argument to473 ``False`` (not recommended, never do this on a production474 server).475 :param mode: The reporting mode. The default is ``'development'``,476 which is best used while debugging, as it may have a477 significant performance effect. Set to ``'production'`` to478 reduce the amount of information that is reported to the479 admin UI.480 :param read_only: If set to ``True``, the admin interface will be481 read-only, with no option to modify room assignments482 or disconnect clients. The default is ``False``.483 :param server_id: The server name to use for this server. If this484 argument is omitted, the server generates its own485 name.486 :param namespace: The Socket.IO namespace to use for the admin487 interface. The default is ``/admin``.488 :param server_stats_interval: The interval in seconds at which the489 server emits a summary of it stats to all490 connected admins.491 """492 from .admin import InstrumentedServer493 return InstrumentedServer(494 self, auth=auth, mode=mode, read_only=read_only,495 server_id=server_id, namespace=namespace,496 server_stats_interval=server_stats_interval)497 498 def _send_packet(self, eio_sid, pkt):499 """Send a Socket.IO packet to a client."""500 encoded_packet = pkt.encode()501 if isinstance(encoded_packet, list):502 for ep in encoded_packet:503 self.eio.send(eio_sid, ep)504 else:505 self.eio.send(eio_sid, encoded_packet)506 507 def _send_eio_packet(self, eio_sid, eio_pkt):508 """Send a raw Engine.IO packet to a client."""509 self.eio.send_packet(eio_sid, eio_pkt)510 511 def _handle_connect(self, eio_sid, namespace, data):512 """Handle a client connection request."""513 namespace = namespace or '/'514 sid = None515 if namespace in self.handlers or namespace in self.namespace_handlers \516 or self.namespaces == '*' or namespace in self.namespaces:517 sid = self.manager.connect(eio_sid, namespace)518 if sid is None:519 self._send_packet(eio_sid, self.packet_class(520 packet.CONNECT_ERROR, data='Unable to connect',521 namespace=namespace))522 return523 524 if self.always_connect:525 self._send_packet(eio_sid, self.packet_class(526 packet.CONNECT, {'sid': sid}, namespace=namespace))527 fail_reason = exceptions.ConnectionRefusedError().error_args528 try:529 if data:530 success = self._trigger_event(531 'connect', namespace, sid, self.environ[eio_sid], data)532 else:533 try:534 success = self._trigger_event(535 'connect', namespace, sid, self.environ[eio_sid])536 except TypeError:537 success = self._trigger_event(538 'connect', namespace, sid, self.environ[eio_sid], None)539 except exceptions.ConnectionRefusedError as exc:540 fail_reason = exc.error_args541 success = False542 543 if success is False:544 if self.always_connect:545 self.manager.pre_disconnect(sid, namespace)546 self._send_packet(eio_sid, self.packet_class(547 packet.DISCONNECT, data=fail_reason, namespace=namespace))548 else:549 self._send_packet(eio_sid, self.packet_class(550 packet.CONNECT_ERROR, data=fail_reason,551 namespace=namespace))552 self.manager.disconnect(sid, namespace, ignore_queue=True)553 elif not self.always_connect:554 self._send_packet(eio_sid, self.packet_class(555 packet.CONNECT, {'sid': sid}, namespace=namespace))556 557 def _handle_disconnect(self, eio_sid, namespace):558 """Handle a client disconnect."""559 namespace = namespace or '/'560 sid = self.manager.sid_from_eio_sid(eio_sid, namespace)561 if not self.manager.is_connected(sid, namespace): # pragma: no cover562 return563 self.manager.pre_disconnect(sid, namespace=namespace)564 self._trigger_event('disconnect', namespace, sid)565 self.manager.disconnect(sid, namespace, ignore_queue=True)566 567 def _handle_event(self, eio_sid, namespace, id, data):568 """Handle an incoming client event."""569 namespace = namespace or '/'570 sid = self.manager.sid_from_eio_sid(eio_sid, namespace)571 self.logger.info('received event "%s" from %s [%s]', data[0], sid,572 namespace)573 if not self.manager.is_connected(sid, namespace):574 self.logger.warning('%s is not connected to namespace %s',575 sid, namespace)576 return577 if self.async_handlers:578 self.start_background_task(self._handle_event_internal, self, sid,579 eio_sid, data, namespace, id)580 else:581 self._handle_event_internal(self, sid, eio_sid, data, namespace,582 id)583 584 def _handle_event_internal(self, server, sid, eio_sid, data, namespace,585 id):586 r = server._trigger_event(data[0], namespace, sid, *data[1:])587 if r != self.not_handled and id is not None:588 # send ACK packet with the response returned by the handler589 # tuples are expanded as multiple arguments590 if r is None:591 data = []592 elif isinstance(r, tuple):593 data = list(r)594 else:595 data = [r]596 server._send_packet(eio_sid, self.packet_class(597 packet.ACK, namespace=namespace, id=id, data=data))598 599 def _handle_ack(self, eio_sid, namespace, id, data):600 """Handle ACK packets from the client."""601 namespace = namespace or '/'602 sid = self.manager.sid_from_eio_sid(eio_sid, namespace)603 self.logger.info('received ack from %s [%s]', sid, namespace)604 self.manager.trigger_callback(sid, id, data)605 606 def _trigger_event(self, event, namespace, *args):607 """Invoke an application event handler."""608 # first see if we have an explicit handler for the event609 handler, args = self._get_event_handler(event, namespace, args)610 if handler:611 return handler(*args)612 # or else, forward the event to a namespace handler if one exists613 handler, args = self._get_namespace_handler(namespace, args)614 if handler:615 return handler.trigger_event(event, *args)616 else:617 return self.not_handled618 619 def _handle_eio_connect(self, eio_sid, environ):620 """Handle the Engine.IO connection event."""621 if not self.manager_initialized:622 self.manager_initialized = True623 self.manager.initialize()624 self.environ[eio_sid] = environ625 626 def _handle_eio_message(self, eio_sid, data):627 """Dispatch Engine.IO messages."""628 if eio_sid in self._binary_packet:629 pkt = self._binary_packet[eio_sid]630 if pkt.add_attachment(data):631 del self._binary_packet[eio_sid]632 if pkt.packet_type == packet.BINARY_EVENT:633 self._handle_event(eio_sid, pkt.namespace, pkt.id,634 pkt.data)635 else:636 self._handle_ack(eio_sid, pkt.namespace, pkt.id, pkt.data)637 else:638 pkt = self.packet_class(encoded_packet=data)639 if pkt.packet_type == packet.CONNECT:640 self._handle_connect(eio_sid, pkt.namespace, pkt.data)641 elif pkt.packet_type == packet.DISCONNECT:642 self._handle_disconnect(eio_sid, pkt.namespace)643 elif pkt.packet_type == packet.EVENT:644 self._handle_event(eio_sid, pkt.namespace, pkt.id, pkt.data)645 elif pkt.packet_type == packet.ACK:646 self._handle_ack(eio_sid, pkt.namespace, pkt.id, pkt.data)647 elif pkt.packet_type == packet.BINARY_EVENT or \648 pkt.packet_type == packet.BINARY_ACK:649 self._binary_packet[eio_sid] = pkt650 elif pkt.packet_type == packet.CONNECT_ERROR:651 raise ValueError('Unexpected CONNECT_ERROR packet.')652 else:653 raise ValueError('Unknown packet type.')654 655 def _handle_eio_disconnect(self, eio_sid):656 """Handle Engine.IO disconnect event."""657 for n in list(self.manager.get_namespaces()).copy():658 self._handle_disconnect(eio_sid, n)659 if eio_sid in self.environ:660 del self.environ[eio_sid]661 662 def _engineio_server_class(self):663 return engineio.Server664 