diff --git a/sylk/configuration/__init__.py b/sylk/configuration/__init__.py index ca07868..7d252fc 100644 --- a/sylk/configuration/__init__.py +++ b/sylk/configuration/__init__.py @@ -1,78 +1,88 @@ # Copyright (C) 2010-2011 AG Projects. See LICENSE for details. # from application.configuration import ConfigSection, ConfigSetting from application.configuration.datatypes import NetworkRangeList, StringList from application.system import host from sipsimple.configuration.datatypes import NonNegativeInteger, SampleRate from sylk import configuration_filename from sylk.configuration.datatypes import AudioCodecs, IPAddress, Path, Port, PortRange, SIPProxyAddress, SRTPEncryption from sylk.resources import Resources, VarResources from sylk.tls import Certificate, PrivateKey class ServerConfig(ConfigSection): __cfgfile__ = configuration_filename __section__ = 'Server' ca_file = ConfigSetting(type=Path, value=Path(Resources.get('tls/ca.crt'))) certificate = ConfigSetting(type=Path, value=Path(Resources.get('tls/default.crt'))) verify_server = False enable_bonjour = False default_application = 'conference' application_map = ConfigSetting(type=StringList, value=['echo:echo']) disabled_applications = ConfigSetting(type=StringList, value='') extra_applications_dir = ConfigSetting(type=Path, value=None) trace_dir = ConfigSetting(type=Path, value=Path(VarResources.get('log/sylkserver'))) trace_core = False trace_sip = False trace_msrp = False trace_notifications = False spool_dir = ConfigSetting(type=Path, value=Path(VarResources.get('spool/sylkserver'))) class SIPConfig(ConfigSection): __cfgfile__ = configuration_filename __section__ = 'SIP' local_ip = ConfigSetting(type=IPAddress, value=IPAddress(host.default_ip)) local_udp_port = ConfigSetting(type=Port, value=5060) local_tcp_port = ConfigSetting(type=Port, value=5060) local_tls_port = ConfigSetting(type=Port, value=5061) advertised_ip = ConfigSetting(type=IPAddress, value=None) outbound_proxy = ConfigSetting(type=SIPProxyAddress, value=None) trusted_peers = ConfigSetting(type=NetworkRangeList, value=NetworkRangeList('any')) enable_ice = False class MSRPConfig(ConfigSection): __cfgfile__ = configuration_filename __section__ = 'MSRP' use_tls = True class RTPConfig(ConfigSection): __cfgfile__ = configuration_filename __section__ = 'RTP' audio_codecs = ConfigSetting(type=AudioCodecs, value=['opus', 'speex', 'G722', 'PCMA', 'PCMU']) port_range = ConfigSetting(type=PortRange, value=PortRange('50000:50500')) srtp_encryption = ConfigSetting(type=SRTPEncryption, value='opportunistic') timeout = ConfigSetting(type=NonNegativeInteger, value=30) sample_rate = ConfigSetting(type=SampleRate, value=32000) +class WebServerConfig(ConfigSection): + __cfgfile__ = configuration_filename + __section__ = 'WebServer' + + local_ip = ConfigSetting(type=IPAddress, value=IPAddress(host.default_ip)) + local_port = ConfigSetting(type=Port, value=8088) + hostname = '' + certificate = ConfigSetting(type=Path, value=Path(Resources.get('tls/default.crt'))) + + class ThorNodeConfig(ConfigSection): __cfgfile__ = configuration_filename __section__ = 'ThorNetwork' enabled = False domain = "sipthor.net" multiply = 1000 certificate = ConfigSetting(type=Certificate, value=None) private_key = ConfigSetting(type=PrivateKey, value=None) ca = ConfigSetting(type=Certificate, value=None) diff --git a/sylk/server.py b/sylk/server.py index 83244e9..17b1856 100644 --- a/sylk/server.py +++ b/sylk/server.py @@ -1,245 +1,248 @@ # Copyright (C) 2010-2011 AG Projects. See LICENSE for details. # import os import sys from threading import Event from uuid import uuid4 from application import log from application.notification import NotificationCenter from application.python import Null from application.system import makedirs from eventlib import proc from sipsimple.account import Account, BonjourAccount, AccountManager from sipsimple.application import SIPApplication from sipsimple.audio import AudioDevice, RootAudioBridge from sipsimple.configuration.settings import SIPSimpleSettings from sipsimple.core import AudioMixer from sipsimple.lookup import DNSManager from sipsimple.storage import MemoryStorage from sipsimple.threading import ThreadManager from sipsimple.threading.green import run_in_green_thread from sipsimple.video import VideoDevice from twisted.internet import reactor # Load stream extensions needed for integration with SIP SIMPLE SDK import sylk.streams del sylk.streams from sylk.accounts import DefaultAccount from sylk.applications import IncomingRequestHandler from sylk.configuration import ServerConfig, SIPConfig, ThorNodeConfig from sylk.configuration.settings import AccountExtension, BonjourAccountExtension, SylkServerSettingsExtension from sylk.log import Logger from sylk.session import SessionManager +from sylk.web import WebServer class SylkServer(SIPApplication): - def __init__(self): self.request_handler = Null self.thor_interface = Null + self.web_server = Null self.logger = Logger() self.stopping_event = Event() self.stop_event = Event() def start(self, options): self.options = options if self.options.enable_bonjour: ServerConfig.enable_bonjour = True notification_center = NotificationCenter() notification_center.add_observer(self, sender=self) notification_center.add_observer(self, name='ThorNetworkGotFatalError') Account.register_extension(AccountExtension) BonjourAccount.register_extension(BonjourAccountExtension) SIPSimpleSettings.register_extension(SylkServerSettingsExtension) try: super(SylkServer, self).start(MemoryStorage()) except Exception, e: log.fatal("Error starting SIP Application: %s" % e) sys.exit(1) def _initialize_core(self): # SylkServer needs to listen for extra events and request types notification_center = NotificationCenter() settings = SIPSimpleSettings() # initialize core options = dict(# general ip_address=SIPConfig.local_ip, user_agent=settings.user_agent, # SIP detect_sip_loops=False, udp_port=settings.sip.udp_port if 'udp' in settings.sip.transport_list else None, tcp_port=settings.sip.tcp_port if 'tcp' in settings.sip.transport_list else None, tls_port=None, # TLS tls_verify_server=False, tls_ca_file=None, tls_cert_file=None, tls_privkey_file=None, # rtp rtp_port_range=(settings.rtp.port_range.start, settings.rtp.port_range.end), # audio codecs=list(settings.rtp.audio_codec_list), # video video_codecs=list(settings.rtp.video_codec_list), enable_colorbar_device=True, # logging log_level=settings.logs.pjsip_level if settings.logs.trace_pjsip else 0, trace_sip=settings.logs.trace_sip, # events and requests to handle events={'conference': ['application/conference-info+xml'], 'presence': ['application/pidf+xml'], 'refer': ['message/sipfrag;version=2.0']}, incoming_events=set(['conference', 'presence']), incoming_requests=set(['MESSAGE'])) notification_center.add_observer(self, sender=self.engine) self.engine.start(**options) @run_in_green_thread def _initialize_subsystems(self): account_manager = AccountManager() dns_manager = DNSManager() notification_center = NotificationCenter() session_manager = SessionManager() settings = SIPSimpleSettings() notification_center.post_notification('SIPApplicationWillStart', sender=self) if self.state == 'stopping': reactor.stop() return # Initialize default account default_account = DefaultAccount() account_manager.default_account = default_account # initialize TLS self._initialize_tls() # initialize PJSIP internal resolver self.engine.set_nameservers(dns_manager.nameservers) # initialize audio objects voice_mixer = AudioMixer(None, None, settings.audio.sample_rate, 0, 9999) self.voice_audio_device = AudioDevice(voice_mixer) self.voice_audio_bridge = RootAudioBridge(voice_mixer) self.voice_audio_bridge.add(self.voice_audio_device) # initialize video objects self.video_device = VideoDevice(u'Colorbar generator', settings.video.resolution, settings.video.framerate) # initialize instance id settings.instance_id = uuid4().urn settings.save() # initialize ZRTP cache makedirs(ServerConfig.spool_dir.normalized) self.engine.zrtp_cache = os.path.join(ServerConfig.spool_dir.normalized, 'zrtp.db') # initialize middleware components dns_manager.start() account_manager.start() session_manager.start() notification_center.add_observer(self, name='CFGSettingsObjectDidChange') self.state = 'started' notification_center.post_notification('SIPApplicationDidStart', sender=self) # start SylkServer components if ThorNodeConfig.enabled: from sylk.interfaces.sipthor import ConferenceNode self.thor_interface = ConferenceNode() + self.web_server = WebServer() + self.web_server.start() self.request_handler = IncomingRequestHandler() self.request_handler.start() @run_in_green_thread def _shutdown_subsystems(self): dns_manager = DNSManager() account_manager = AccountManager() session_manager = SessionManager() # terminate all sessions p = proc.spawn(session_manager.stop) p.wait() # shutdown SylkServer components - procs = [proc.spawn(self.request_handler.stop), proc.spawn(self.thor_interface.stop)] + procs = [proc.spawn(self.web_server.stop), proc.spawn(self.request_handler.stop), proc.spawn(self.thor_interface.stop)] proc.waitall(procs) # shutdown other middleware components procs = [proc.spawn(dns_manager.stop), proc.spawn(account_manager.stop)] proc.waitall(procs) # shutdown engine self.engine.stop() self.engine.join() # stop threads thread_manager = ThreadManager() thread_manager.stop() # stop the reactor reactor.stop() def _NH_AudioDevicesDidChange(self, notification): pass def _NH_DefaultAudioDeviceDidChange(self, notification): pass def _NH_SIPApplicationFailedToStartTLS(self, notification): log.fatal("Couldn't set TLS options: %s" % notification.data.error) def _NH_SIPApplicationWillStart(self, notification): self.logger.start() settings = SIPSimpleSettings() if settings.logs.trace_sip and self.logger._siptrace_filename is not None: log.msg('Logging SIP trace to file "%s"' % self.logger._siptrace_filename) if settings.logs.trace_msrp and self.logger._msrptrace_filename is not None: log.msg('Logging MSRP trace to file "%s"' % self.logger._msrptrace_filename) if settings.logs.trace_pjsip and self.logger._pjsiptrace_filename is not None: log.msg('Logging PJSIP trace to file "%s"' % self.logger._pjsiptrace_filename) if settings.logs.trace_notifications and self.logger._notifications_filename is not None: log.msg('Logging notifications trace to file "%s"' % self.logger._notifications_filename) def _NH_SIPApplicationDidStart(self, notification): settings = SIPSimpleSettings() local_ip = SIPConfig.local_ip log.msg("SylkServer started, listening on:") for transport in settings.sip.transport_list: try: log.msg("%s:%d (%s)" % (local_ip, getattr(self.engine, '%s_port' % transport), transport.upper())) except TypeError: pass def _NH_SIPApplicationWillEnd(self, notification): self.stopping_event.set() def _NH_SIPApplicationDidEnd(self, notification): log.msg('SIP application ended') self.logger.stop() if not self.stopping_event.is_set(): log.warning('SIP application ended without shutting down all subsystems') self.stopping_event.set() self.stop_event.set() def _NH_SIPEngineGotException(self, notification): log.error('An exception occured within the SIP core:\n%s\n' % notification.data.traceback) def _NH_SIPEngineDidFail(self, notification): log.error('SIP engine failed') super(SylkServer, self)._NH_SIPEngineDidFail(notification) def _NH_ThorNetworkGotFatalError(self, notification): log.error("All Thor Event Servers have unrecoverable errors.") diff --git a/sylk/web/__init__.py b/sylk/web/__init__.py new file mode 100644 index 0000000..3afe665 --- /dev/null +++ b/sylk/web/__init__.py @@ -0,0 +1,70 @@ +# Copyright (C) 2015 AG Projects. See LICENSE for details. +# + +__all__ = ['Klein', 'WebServer', 'server'] + +import os + +from application import log +from application.python.types import Singleton +from gnutls.interfaces.twisted import X509Credentials +from twisted.internet import reactor +from twisted.web.resource import Resource +from twisted.web.server import Site + +from sylk import __version__ +from sylk.configuration import WebServerConfig +from sylk.tls import Certificate, PrivateKey +from sylk.web.klein import Klein + +# Set the 'Server' header string which Twisted Web will use +import twisted.web.server +twisted.web.server.version = b'SylkServer/%s' % __version__ + + +class RootResource(Resource): + isLeaf = True + + def render_GET(self, request): + request.setHeader('Content-Type', 'text/plain') + return 'Welcome to SylkServer!' + + +class WebServer(object): + __metaclass__ = Singleton + + def __init__(self): + self.base = Resource() + self.base.putChild('', RootResource()) + self.site = Site(self.base, logPath=os.devnull) + self.listener = None + + @property + def url(self): + return self.__dict__.get('url', '') + + def register_resource(self, path, resource): + self.base.putChild(path, resource) + + def start(self): + interface = WebServerConfig.local_ip + port = WebServerConfig.local_port + if os.path.isfile(WebServerConfig.certificate): + cert = Certificate(WebServerConfig.certificate.normalized) + key = PrivateKey(WebServerConfig.certificate.normalized) + credentials = X509Credentials(cert, key) + self.listener = reactor.listenTLS(port, self.site, credentials, interface=interface) + scheme = 'https' + else: + self.listener = reactor.listenTCP(port, self.site, interface=interface) + scheme = 'http' + port = self.listener.getHost().port + self.__dict__['url'] = '%s://%s:%d' % (scheme, WebServerConfig.hostname or interface.normalized, port) + log.msg('Web server listening for requests on: %s' % self.url) + + def stop(self): + if self.listener is not None: + self.listener.stopListening() + +server = WebServer() + diff --git a/sylk/web/klein/__init__.py b/sylk/web/klein/__init__.py new file mode 100644 index 0000000..78fbc82 --- /dev/null +++ b/sylk/web/klein/__init__.py @@ -0,0 +1,12 @@ + +# Vendored Klein from: https://github.com/twisted/klein +# Changes from original version: +# - Removed global Klein app +# - Removed Klein.run method +# - Fixed imports to work inside SylkServer + + +from sylk.web.klein.app import Klein + +__all__ = ['Klein'] + diff --git a/sylk/web/klein/app.py b/sylk/web/klein/app.py new file mode 100644 index 0000000..4f69392 --- /dev/null +++ b/sylk/web/klein/app.py @@ -0,0 +1,254 @@ +""" +Applications are great. Lets have more of them. +""" +import weakref + +from functools import wraps +from twisted.python.components import registerAdapter +from twisted.web.server import Request +from werkzeug.routing import Map, Rule +from zope.interface import implements + +from sylk.web.klein.resource import KleinResource +from sylk.web.klein.interfaces import IKleinRequest + +__all__ = ['Klein'] + + +def _call(instance, f, *args, **kwargs): + if instance is None: + return f(*args, **kwargs) + + return f(instance, *args, **kwargs) + + +class KleinRequest(object): + implements(IKleinRequest) + + def __init__(self, request): + self.branch_segments = [''] + self.mapper = None + + def url_for(self, *args, **kwargs): + return self.mapper.build(*args, **kwargs) + + +registerAdapter(KleinRequest, Request, IKleinRequest) + + +class Klein(object): + """ + L{Klein} is an object which is responsible for maintaining the routing + configuration of our application. + + @ivar _url_map: A C{werkzeug.routing.Map} object which will be used for + routing resolution. + @ivar _endpoints: A C{dict} mapping endpoint names to handler functions. + """ + + _bound_klein_instances = weakref.WeakKeyDictionary() + + def __init__(self): + self._url_map = Map() + self._endpoints = {} + self._error_handlers = [] + self._instance = None + + + def __eq__(self, other): + if isinstance(other, Klein): + return vars(self) == vars(other) + return NotImplemented + + + def __ne__(self, other): + result = self.__eq__(other) + if result is NotImplemented: + return result + return not result + + + @property + def url_map(self): + """ + Read only property exposing L{Klein._url_map}. + """ + return self._url_map + + + @property + def endpoints(self): + """ + Read only property exposing L{Klein._endpoints}. + """ + return self._endpoints + + + def execute_endpoint(self, endpoint, *args, **kwargs): + """ + Execute the named endpoint with all arguments and possibly a bound + instance. + """ + endpoint_f = self._endpoints[endpoint] + return endpoint_f(self._instance, *args, **kwargs) + + + def execute_error_handler(self, handler, request, failure): + """ + Execute the passed error handler, possibly with a bound instance. + """ + return handler(self._instance, request, failure) + + + def resource(self): + """ + Return an L{IResource} which suitably wraps this app. + + @returns: An L{IResource} + """ + + return KleinResource(self) + + + def __get__(self, instance, owner): + """ + Get an instance of L{Klein} bound to C{instance}. + """ + if instance is None: + return self + + k = self._bound_klein_instances.get(instance) + + if k is None: + k = self.__class__() + k._url_map = self._url_map + k._endpoints = self._endpoints + k._error_handlers = self._error_handlers + k._instance = instance + self._bound_klein_instances[instance] = k + + return k + + + def route(self, url, *args, **kwargs): + """ + Add a new handler for C{url} passing C{args} and C{kwargs} directly to + C{werkzeug.routing.Rule}. The handler function will be passed at least + one argument an L{twisted.web.server.Request} and any keyword arguments + taken from the C{url} pattern. + + :: + @app.route("/") + def index(request): + return "Hello" + + @param url: A werkzeug URL pattern given to C{werkzeug.routing.Rule}. + @type url: str + + @param branch: A bool indiciated if a branch endpoint should + be added that allows all child path segments that don't + match some other route to be consumed. Default C{False}. + @type branch: bool + + + @returns: decorated handler function. + """ + segment_count = url.count('/') + if url.endswith('/'): + segment_count -= 1 + + def deco(f): + kwargs.setdefault('endpoint', f.__name__) + if kwargs.pop('branch', False): + branchKwargs = kwargs.copy() + branchKwargs['endpoint'] = branchKwargs['endpoint'] + '_branch' + + @wraps(f) + def branch_f(instance, request, *a, **kw): + IKleinRequest(request).branch_segments = kw.pop('__rest__', '').split('/') + return _call(instance, f, request, *a, **kw) + + branch_f.segment_count = segment_count + + self._endpoints[branchKwargs['endpoint']] = branch_f + self._url_map.add(Rule(url.rstrip('/') + '/' + '', *args, **branchKwargs)) + + @wraps(f) + def _f(instance, request, *a, **kw): + return _call(instance, f, request, *a, **kw) + + _f.segment_count = segment_count + + self._endpoints[kwargs['endpoint']] = _f + self._url_map.add(Rule(url, *args, **kwargs)) + return f + + return deco + + + def handle_errors(self, f_or_exception, *additional_exceptions): + """ + Register an error handler. This decorator supports two syntaxes. The + simpler of these can be used to register a handler for all C{Exception} + types:: + + @app.handle_errors + def error_handler(request, failure): + request.setResponseCode(500) + return 'Uh oh' + + Alternately, a handler can be registered for one or more specific + C{Exception} tyes:: + + @app.handle_errors(EncodingError, ValidationError): + def error_handler(request, failure) + request.setResponseCode(400) + return failure.getTraceback() + + The handler will be passed a L{twisted.web.server.Request} as well as a + L{twisted.python.failure.Failure} instance. Error handlers may return a + deferred, a failure or a response body. + + If more than one error handler is registered, the handlers will be + executed in the order in which they are defined, until a handler is + encountered which completes successfully. If no handler completes + successfully, L{twisted.web.server.Request}'s processingFailed() method + will be called. + + In addition to handling errors that occur within a route handler, error + handlers also handle any C{werkzeug.exceptions.HTTPException} which is + raised during routing. In particular, C{werkzeug.exceptions.NotFound} + will be raised if no matching route is found, so to return a custom 404 + users can do the following:: + + @app.handle_errors(NotFound) + def error_handler(request, failure): + request.setResponseCode(404) + return 'Not found' + + @param f_or_exception: An error handler function, or an C{Exception} + subclass to scope the decorated handler to. + @type f_or_exception: C{function} or C{Exception} + + @param additional_exceptions Additional C{Exception} subclasses to + scope the decorated function to. + @type additional_exceptions C{list} of C{Exception}s + + @returns: decorated error handler function. + """ + # Try to detect calls using the "simple" @app.handle_error syntax by + # introspecting the first argument - if it isn't a type which + # subclasses Exception we assume the simple syntax was used. + if not isinstance(f_or_exception, type) or not issubclass(f_or_exception, Exception): + return self.handle_errors(Exception)(f_or_exception) + + def deco(f): + @wraps(f) + def _f(instance, request, failure): + return _call(instance, f, request, failure) + + self._error_handlers.append(([f_or_exception] + list(additional_exceptions), _f)) + return _f + + return deco + diff --git a/sylk/web/klein/interfaces.py b/sylk/web/klein/interfaces.py new file mode 100644 index 0000000..f8fc92c --- /dev/null +++ b/sylk/web/klein/interfaces.py @@ -0,0 +1,12 @@ + +from zope.interface import Interface, Attribute + + +class IKleinRequest(Interface): + branch_segments = Attribute("Segments consumed by a branch route.") + mapper = Attribute("L{werkzeug.routing.MapAdapter}") + + def url_for(self, endpoint, values=None, method=None, force_external=False, append_unknown=True): + """ + L{werkzeug.routing.MapAdapter.build} + """ diff --git a/sylk/web/klein/resource.py b/sylk/web/klein/resource.py new file mode 100644 index 0000000..52fc230 --- /dev/null +++ b/sylk/web/klein/resource.py @@ -0,0 +1,262 @@ +from twisted.internet import defer +from twisted.python import log, failure +from twisted.web import server +from twisted.web.iweb import IRenderable +from twisted.web.resource import Resource, IResource, getChildForRequest +from twisted.web.template import flattenString +from werkzeug.exceptions import HTTPException + +from sylk.web.klein.interfaces import IKleinRequest + + + +__all__ = ["KleinResource", "ensure_utf8_bytes"] + + + +def ensure_utf8_bytes(v): + """ + Coerces a value which is either a C{unicode} or C{str} to a C{str}. + If ``v`` is a C{unicode} object it is encoded as utf-8. + """ + if isinstance(v, unicode): + v = v.encode("utf-8") + return v + + + +class _StandInResource(object): + """ + A standin for a Resource. + + This is a sentinel value for L{KleinResource}, to say that we are rendering + a L{Resource}, which may close the connection itself later. + """ + + + +class _URLDecodeError(Exception): + """ + Raised if one or more string parts of the URL could not be decoded. + """ + __slots__ = ["errors"] + + def __init__(self, errors): + """ + @param errors: List of decoding errors. + @type errors: L{list} of L{tuple} of L{str}, + L{twisted.python.failure.Failure} + """ + self.errors = errors + + def __repr__(self): + return "".format(self.errors) + + + +def _extractURLparts(request): + """ + Extracts and decodes URI parts from C{request}. + + All strings must be UTF8-decodable. + + @param request: A Twisted Web request. + @type request: L{twisted.web.iweb.IRequest} + + @raise URLDecodeError: If one of the parts could not be decoded as UTF-8. + + @return: L{tuple} of the URL scheme, the server name, the server port, the + path info and the script name. + @rtype: L{tuple} of L{unicode}, L{unicode}, L{int}, L{unicode}, L{unicode} + """ + server_name = request.getRequestHostname() + server_port = request.getHost().port + if (bool(request.isSecure()), server_port) not in [ + (True, 443), (False, 80)]: + server_name = '%s:%d' % (server_name, server_port) + script_name = '' + if request.prepath: + script_name = '/'.join(request.prepath) + + if not script_name.startswith('/'): + script_name = '/' + script_name + + path_info = '' + if request.postpath: + path_info = '/'.join(request.postpath) + + if not path_info.startswith('/'): + path_info = '/' + path_info + + url_scheme = u'https' if request.isSecure() else u'http' + + utf8Failures = [] + try: + server_name = server_name.decode("utf-8") + except UnicodeDecodeError: + utf8Failures.append(("SERVER_NAME", failure.Failure())) + try: + path_info = path_info.decode("utf-8") + except UnicodeDecodeError: + utf8Failures.append(("PATH_INFO", failure.Failure())) + try: + script_name = script_name.decode("utf-8") + except UnicodeDecodeError: + utf8Failures.append(("SCRIPT_NAME", failure.Failure())) + + if utf8Failures: + raise _URLDecodeError(utf8Failures) + + return url_scheme, server_name, server_port, path_info, script_name + + + +class KleinResource(Resource): + """ + A ``Resource`` that can do URL routing. + """ + isLeaf = True + + + def __init__(self, app): + Resource.__init__(self) + self._app = app + + + def __eq__(self, other): + if isinstance(other, KleinResource): + return vars(self) == vars(other) + return NotImplemented + + + def __ne__(self, other): + result = self.__eq__(other) + if result is NotImplemented: + return result + return not result + + + def render(self, request): + # Stuff we need to know for the mapper. + try: + url_scheme, server_name, server_port, path_info, script_name = \ + _extractURLparts(request) + except _URLDecodeError as e: + for what, fail in e.errors: + log.err(fail, "Invalid encoding in {what}.".format(what=what)) + request.setResponseCode(400) + return b"Non-UTF-8 encoding in URL." + + # Bind our mapper. + mapper = self._app.url_map.bind( + server_name, + script_name, + path_info=path_info, + default_method=request.method, + url_scheme=url_scheme, + ) + # Make the mapper available to the view. + kleinRequest = IKleinRequest(request) + kleinRequest.mapper = mapper + + # Make sure we'll notice when the connection goes away unambiguously. + request_finished = [False] + + def _finish(result): + request_finished[0] = True + + def _execute(): + # Actually doing the match right here. This can cause an exception + # to percolate up. If that happens it will be handled below in + # processing_failed, either by a user-registered error handler or + # one of our defaults. + (rule, kwargs) = mapper.match(return_rule=True) + endpoint = rule.endpoint + + # Try pretty hard to fix up prepath and postpath. + segment_count = self._app.endpoints[endpoint].segment_count + request.prepath.extend(request.postpath[:segment_count]) + request.postpath = request.postpath[segment_count:] + + request.notifyFinish().addBoth(_finish) + + # Standard Twisted Web stuff. Defer the method action, giving us + # something renderable or printable. Return NOT_DONE_YET and set up + # the incremental renderer. + d = defer.maybeDeferred(self._app.execute_endpoint, + endpoint, + request, + **kwargs) + + request.notifyFinish().addErrback(lambda _: d.cancel()) + + return d + + d = defer.maybeDeferred(_execute) + + def write_response(r): + if r is not _StandInResource: + if isinstance(r, unicode): + r = r.encode('utf-8') + + if r is not None: + request.write(r) + + if not request_finished[0]: + request.finish() + + def process(r): + if IResource.providedBy(r): + request.render(getChildForRequest(r, request)) + return _StandInResource + + if IRenderable.providedBy(r): + return flattenString(request, r).addCallback(process) + + return r + + d.addCallback(process) + + def processing_failed(failure, error_handlers): + # The failure processor writes to the request. If the + # request is already finished we should suppress failure + # processing. We don't return failure here because there + # is no way to surface this failure to the user if the + # request is finished. + if request_finished[0]: + if not failure.check(defer.CancelledError): + log.err(failure, "Unhandled Error Processing Request.") + return + + # If there are no more registered handlers, apply some defaults + if len(error_handlers) == 0: + if failure.check(HTTPException): + he = failure.value + request.setResponseCode(he.code) + resp = he.get_response({}) + + for header, value in resp.headers: + request.setHeader(ensure_utf8_bytes(header), ensure_utf8_bytes(value)) + + return ensure_utf8_bytes(he.get_body({})) + else: + request.processingFailed(failure) + return + + error_handler = error_handlers[0] + + # Each error handler is a tuple of (list_of_exception_types, handler_fn) + if failure.check(*error_handler[0]): + d = defer.maybeDeferred(self._app.execute_error_handler, + error_handler[1], + request, + failure) + + return d.addErrback(processing_failed, error_handlers[1:]) + + return processing_failed(failure, error_handlers[1:]) + + + d.addErrback(processing_failed, self._app._error_handlers) + d.addCallback(write_response).addErrback(log.err, _why="Unhandled Error writing response") + return server.NOT_DONE_YET