Source code for tornadio2.router

# -*- coding: utf-8 -*-
#
# Copyright: (c) 2011 by the Serge S. Koval, see AUTHORS for more details.
#
# Licensed under the Apache License, Version 2.0 (the "License"); you may
# not use this file except in compliance with the License. You may obtain
# a copy of the License at
#
#     http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
# License for the specific language governing permissions and limitations
# under the License.

"""
    tornadio2.router
    ~~~~~~~~~~~~~~~~

    Transport protocol router and main entry point for all socket.io clients.
"""

from tornado import ioloop, version_info
from tornado.web import HTTPError

from tornadio2 import persistent, polling, sessioncontainer, session, proto, preflight, stats

PROTOCOLS = {
    'websocket': persistent.TornadioWebSocketHandler,
    'flashsocket': persistent.TornadioFlashSocketHandler,
    'xhr-polling': polling.TornadioXHRPollingHandler,
    'htmlfile': polling.TornadioHtmlFileHandler,
    'jsonp-polling': polling.TornadioJSONPHandler,
    }

DEFAULT_SETTINGS = {
    # Sessions check interval in seconds
    'session_check_interval': 15,
    # Session expiration in seconds
    'session_expiry': 30,
    # Heartbeat time in seconds. Do not change this value unless
    # you absolutely sure that new value will work.
    'heartbeat_interval': 12,
    # Enabled protocols
    'enabled_protocols': ['websocket', 'flashsocket', 'xhr-polling',
                          'jsonp-polling', 'htmlfile'],
    # XHR-Polling request timeout, in seconds
    'xhr_polling_timeout': 20,
    # Some antivirus software messed up with HTTP traffic and, as a result, websockets
    # to port 80 stop to work. If you enable this setting, TornadIO will try to send
    # ping packet and wait for response. If nothing will happen during 5 seconds,
    # TornadIO considers connection not working.
    'websocket_check': False,
    # Starting from socket.io 0.9.2, client started verifying heartbeats for all transports.
    # Disable this if you're on 0.9.1 or lower, as this settings will significantly increase
    # your server load for clients with polling transports.
    'global_heartbeats': True,
    # Client timeout adjustment in seconds. If you see your clients disconnect without a
    # reason, increase this value.
    'client_timeout': 5,
    # Verify remote IP. May want to disable this for some setups. Some networks send traffic
    # from same client, different IP each time. If you set this to False, TornadIO will not
    # check the session ID against IP address. This has consequences for spoofing sessions and
    # so on, so use with extreme caution.
    'verify_remote_ip': True,
    }


class HandshakeHandler(preflight.PreflightHandler):
    """socket.io handshake handler"""

    def initialize(self, server):
        self.server = server

    def get(self, version, *args, **kwargs):
        try:
            self.server.stats.connection_opened()

            # Only version 1 is supported now
            if version != '1':
                raise HTTPError(503, "Invalid socket.io protocol version")

            sess = self.server.create_session(self.request)

            settings = self.server.settings

            # TODO: Fix heartbeat timeout. For now, it is adding 5 seconds to the client timeout.
            data = '%s:%d:%d:%s' % (
                sess.session_id,
                # TODO: Fix me somehow a well. 0.9.2 will drop connection is no
                # heartbeat was sent over
                settings['heartbeat_interval'] + settings['client_timeout'],
                # TODO: Fix me somehow.
                settings['xhr_polling_timeout'] + settings['client_timeout'],
                ','.join(t for t in self.server.settings.get('enabled_protocols'))
                )

            if self.server.settings['global_heartbeats']:
                sess.reset_heartbeat()

            jsonp = self.get_argument('jsonp', None)
            if jsonp is not None:
                self.set_header('Content-Type', 'application/javascript; charset=UTF-8')

                data = 'io.j[%s](%s);' % (jsonp, proto.json_dumps(data))
            else:
                self.set_header('Content-Type', 'text/plain; charset=UTF-8')

            self.preflight()

            self.write(data)
            self.finish()
        finally:
            self.server.stats.connection_closed()


[docs]class TornadioRouter(object): """TornadIO2 router implementation"""
[docs] def __init__(self, connection, user_settings=dict(), namespace='socket.io', io_loop=None): """Constructor. `connection` SocketConnection class instance `user_settings` Settings `namespace` Router namespace, defaulted to 'socket.io' `io_loop` IOLoop instance, optional. """ # TODO: Version check if version_info[0] < 2: raise Exception('TornadIO2 requires Tornado 2.0 or higher.') # Store connection class self._connection = connection # Initialize io_loop self.io_loop = io_loop or ioloop.IOLoop.instance() # Settings self.settings = DEFAULT_SETTINGS.copy() if user_settings: self.settings.update(user_settings) # Sessions self._sessions = sessioncontainer.SessionContainer() check_interval = self.settings['session_check_interval'] * 1000 self._sessions_cleanup = ioloop.PeriodicCallback(self._sessions.expire, check_interval, self.io_loop) self._sessions_cleanup.start() # Stats self.stats = stats.StatsCollector() self.stats.start(self.io_loop) # Initialize URLs self._transport_urls = [ (r'/%s/(?P<version>\d+)/$' % namespace, HandshakeHandler, dict(server=self)) ] for t in self.settings.get('enabled_protocols', dict()): proto = PROTOCOLS.get(t) if not proto: # TODO: Error logging continue # Only version 1 is supported self._transport_urls.append( (r'/%s/1/%s/(?P<session_id>[^/]+)/?' % (namespace, t), proto, dict(server=self)) )
@property def urls(self): """List of the URLs to be added to the Tornado application""" return self._transport_urls
[docs] def apply_routes(self, routes): """Feed list of the URLs to the routes list. Returns list""" routes.extend(self._transport_urls) return routes
[docs] def create_session(self, request): """Creates new session object and returns it. `request` Request that created the session. Will be used to get query string parameters and cookies. """ # TODO: Possible optimization here for settings.get s = session.Session(self._connection, self, request, self.settings.get('session_expiry') ) self._sessions.add(s) return s
[docs] def get_session(self, session_id): """Get session by session id """ return self._sessions.get(session_id)