diff --git a/config.ini.sample b/config.ini.sample index 21d3d1c..326aeb5 100644 --- a/config.ini.sample +++ b/config.ini.sample @@ -1,93 +1,96 @@ ; SylkServer configuration file [Server] ; The following settings are the default used by the software, uncomment ; them only if you want to make changes ; default_application = conference ; Statically map a Request URI to a specific application. In the example ; below, 123 is matched 1st against the domain part, than the username part ; of the Request URI This static mapping can be overwritten by adding ; X-Sylk-App header set to the value of a valid SylkServer application name ; application_map = echo:echo,123:conference,test:ircconference,gmail.com:xmppgateway ; Disable the specified applications ; disabled_applications = +; Directory where extra applications are stored +; extra_applications_dir = + ; trace_dir = /var/log/sylkserver ; trace_core = False ; trace_sip = False ; trace_msrp = False ; trace_notifications = False ; TLS can be used for encryption of SIP signaling and MSRP media. TLS is ; disabled by default. To enable TLS, you must have a valid X.509 ; certificate and configure it below, then set the local_tls_port in the SIP ; section and use_tls in MSRP section ; The X.509 Certificate Authorities file ; ca_file = /etc/sylkserver/tls/ca.crt ; The file containing X.509 certificate and private key in unencrypted format ; certificate = /etc/sylkserver/tls/default.crt ; verify_server = False ; Enable Bonjour capabilities for applications ; enable_bonjour = False [SIP] ; SIP transport settings ; IP address used for SIP signaling; empty string or any means listen on interface used ; by the default route ; local_ip = ; Ports used for SIP transports, if not set to any value the transport will be disabled ; local_udp_port = 5060 ; local_tcp_port = 5060 ; local_tls_port = 5061 ; If set, all outbound SIP requests will be sent through this SIP proxy ; The proxy address format is: proxy.example.com:5061;transport=tls ; Transport can be udp, tcp or tls, if skipped it is considered udp ; If only the hostname is set, RFC3263 lookups are performed to lookup ; the outbound proxy server address ; outbound_proxy = ; A comma-separated list of hosts or networks to trust. ; The elements can be an IP address in CIDR format, a ; hostname or an IP address (in the latter 2 a mask of 32 ; is assumed), or the special keywords 'any' and 'none' ; (being equivalent to 0.0.0.0/0 and 0.0.0.0/32 ; respectively). It defaults to 'any'. ; trusted_peers = [MSRP] ; MSRP transport settings ; A valid X.509 certificate is required for MSRP to work over TLS. ; TLS is enabled by default, a default TLS certificate is provided with SylkServer. ; use_tls = True [RTP] ; RTP transport settings ; Allowed codec list, valid values: G722, speex, PCMU, PCMA, iLBC, GSM ; audio_codecs = G722,speex,PCMU,PCMA ; Port range used for RTP ; port_range = 50000:50500 ; SRTP valid values: disabled, mandatory, optional ; srtp_encryption = optional ; RTP stream timeout, session will be disconnected after this value ; timeout = 30 diff --git a/sylk/applications/__init__.py b/sylk/applications/__init__.py index 8b4affc..3ea8dd5 100644 --- a/sylk/applications/__init__.py +++ b/sylk/applications/__init__.py @@ -1,293 +1,306 @@ # Copyright (C) 2010-2011 AG Projects. See LICENSE for details # __all__ = ['ISylkApplication', 'ApplicationRegistry', 'SylkApplication', 'IncomingRequestHandler', 'ApplicationLogger'] import abc import os import socket import struct +import sys from application import log from application.configuration.datatypes import NetworkRange from application.notification import IObserver, NotificationCenter from application.python import Null from application.python.types import Singleton from itertools import chain from sipsimple.threading import run_in_twisted_thread from zope.interface import implements from sylk.configuration import ServerConfig, SIPConfig, ThorNodeConfig SYLK_APP_HEADER = 'X-Sylk-App' class ApplicationRegistry(object): __metaclass__ = Singleton def __init__(self): self.applications = [] def __iter__(self): return iter(self.applications) def add(self, app): if app not in self.applications: self.applications.append(app) class ApplicationName(object): def __get__(self, obj, objtype): name = objtype.__name__ return name[:-11].lower() if name.endswith('Application') else name.lower() class SylkApplicationMeta(abc.ABCMeta, Singleton): """Metaclass for defining SylkServer applications: a Singleton that also adds them to the application registry""" def __init__(cls, name, bases, dic): super(SylkApplicationMeta, cls).__init__(name, bases, dic) if name != 'SylkApplication': ApplicationRegistry().add(cls) class SylkApplication(object): """Base class for all SylkServer applications""" __metaclass__ = SylkApplicationMeta __appname__ = ApplicationName() @abc.abstractmethod def start(self): pass @abc.abstractmethod def stop(self): pass @abc.abstractmethod def incoming_session(self, session): pass @abc.abstractmethod def incoming_subscription(self, subscribe_request, data): pass @abc.abstractmethod def incoming_referral(self, refer_request, data): pass @abc.abstractmethod def incoming_sip_message(self, message_request, data): pass -def load_applications(): +def load_builtin_applications(): toplevel = os.path.dirname(__file__) app_list = [item for item in os.listdir(toplevel) if os.path.isdir(os.path.join(toplevel, item)) and '__init__.py' in os.listdir(os.path.join(toplevel, item))] for module in ['sylk.applications.%s' % item for item in set(app_list).difference(ServerConfig.disabled_applications)]: __import__(module) - map(__import__, []) + +def load_extra_applications(): + if ServerConfig.extra_applications_dir: + toplevel = os.path.realpath(os.path.abspath(ServerConfig.extra_applications_dir.normalized)) + if os.path.isdir(toplevel): + app_list = [item for item in os.listdir(toplevel) if os.path.isdir(os.path.join(toplevel, item)) and '__init__.py' in os.listdir(os.path.join(toplevel, item))] + sys.path.append(toplevel) + for module in (item for item in set(app_list).difference(ServerConfig.disabled_applications)): + __import__(module) + +def load_applications(): + load_builtin_applications() + load_extra_applications() [app() for app in ApplicationRegistry()] class ApplicationNotLoadedError(Exception): pass class IncomingRequestHandler(object): """ Handle incoming requests and match them to applications. """ __metaclass__ = Singleton implements(IObserver) def __init__(self): load_applications() log.msg('Loaded applications: %s' % ', '.join([app.__appname__ for app in ApplicationRegistry()])) self.application_map = dict((item.split(':')) for item in ServerConfig.application_map) self.authorization_handler = AuthorizationHandler() def start(self): [app().start() for app in ApplicationRegistry()] self.authorization_handler.start() notification_center = NotificationCenter() notification_center.add_observer(self, name='SIPSessionNewIncoming') notification_center.add_observer(self, name='SIPIncomingSubscriptionGotSubscribe') notification_center.add_observer(self, name='SIPIncomingReferralGotRefer') notification_center.add_observer(self, name='SIPIncomingRequestGotRequest') def stop(self): self.authorization_handler.stop() notification_center = NotificationCenter() notification_center.remove_observer(self, name='SIPSessionNewIncoming') notification_center.remove_observer(self, name='SIPIncomingSubscriptionGotSubscribe') notification_center.remove_observer(self, name='SIPIncomingReferralGotRefer') notification_center.remove_observer(self, name='SIPIncomingRequestGotRequest') [app().stop() for app in ApplicationRegistry()] def get_application(self, ruri, headers): if SYLK_APP_HEADER in headers: application = headers[SYLK_APP_HEADER].body.strip() else: application = ServerConfig.default_application if self.application_map: prefixes = ("%s@%s" % (ruri.user, ruri.host), ruri.host, ruri.user) for prefix in prefixes: if prefix in self.application_map: application = self.application_map[prefix] break try: app = (app for app in ApplicationRegistry() if app.__appname__ == application).next() except StopIteration: log.error('Application %s is not loaded' % application) raise ApplicationNotLoadedError else: return app() @run_in_twisted_thread def handle_notification(self, notification): handler = getattr(self, '_NH_%s' % notification.name, Null) handler(notification) def _NH_SIPSessionNewIncoming(self, notification): session = notification.sender try: self.authorization_handler.authorize_source(session.peer_address.ip) except UnauthorizedRequest: session.reject(403) return try: app = self.get_application(session._invitation.request_uri, notification.data.headers) except ApplicationNotLoadedError: session.reject(404) else: app.incoming_session(session) def _NH_SIPIncomingSubscriptionGotSubscribe(self, notification): subscribe_request = notification.sender try: self.authorization_handler.authorize_source(subscribe_request.peer_address.ip) except UnauthorizedRequest: subscribe_request.reject(403) return try: app = self.get_application(notification.data.request_uri, notification.data.headers) except ApplicationNotLoadedError: subscribe_request.reject(404) else: app.incoming_subscription(subscribe_request, notification.data) def _NH_SIPIncomingReferralGotRefer(self, notification): refer_request = notification.sender try: self.authorization_handler.authorize_source(refer_request.peer_address.ip) except UnauthorizedRequest: refer_request.reject(403) return try: app = self.get_application(notification.data.request_uri, notification.data.headers) except ApplicationNotLoadedError: refer_request.reject(404) else: app.incoming_referral(refer_request, notification.data) def _NH_SIPIncomingRequestGotRequest(self, notification): request = notification.sender if notification.data.method != 'MESSAGE': request.answer(405) return try: self.authorization_handler.authorize_source(request.peer_address.ip) except UnauthorizedRequest: request.answer(403) return try: app = self.get_application(notification.data.request_uri, notification.data.headers) except ApplicationNotLoadedError: request.answer(404) else: app.incoming_sip_message(request, notification.data) class UnauthorizedRequest(Exception): pass class AuthorizationHandler(object): implements(IObserver) def __init__(self): self.state = None self.trusted_peers = SIPConfig.trusted_peers self.thor_nodes = [] @property def trusted_parties(self): if ThorNodeConfig.enabled: return self.thor_nodes return self.trusted_peers def start(self): NotificationCenter().add_observer(self, name='ThorNetworkGotUpdate') self.state = 'started' def stop(self): self.state = 'stopped' NotificationCenter().remove_observer(self, name='ThorNetworkGotUpdate') def authorize_source(self, ip_address): if self.state != 'started': raise UnauthorizedRequest for range in self.trusted_parties: if struct.unpack('!L', socket.inet_aton(ip_address))[0] & range[1] == range[0]: return True raise UnauthorizedRequest @run_in_twisted_thread def handle_notification(self, notification): handler = getattr(self, '_NH_%s' % notification.name, Null) handler(notification) def _NH_ThorNetworkGotUpdate(self, notification): thor_nodes = [] for node in chain(*(n.nodes for n in notification.data.networks.values())): thor_nodes.append(NetworkRange(node)) self.thor_nodes = thor_nodes class ApplicationLogger(object): __metaclass__ = Singleton @classmethod def for_package(cls, package): return cls(package.split('.')[-1]) def __init__(self, prefix): self.prefix = '[%s] ' % prefix def info(self, message, **context): log.info(self.prefix+message, **context) def warning(self, message, **context): log.warning(self.prefix+message, **context) def debug(self, message, **context): log.debug(self.prefix+message, **context) def error(self, message, **context): log.error(self.prefix+message, **context) def critical(self, message, **context): log.critical(self.prefix+message, **context) def exception(self, message=None, **context): if message is not None: message = self.prefix+message log.exception(message, **context) # Some aliases that are commonly used msg = info warn = warning fatal = critical err = exception diff --git a/sylk/configuration/__init__.py b/sylk/configuration/__init__.py index bec6e40..32c4c95 100644 --- a/sylk/configuration/__init__.py +++ b/sylk/configuration/__init__.py @@ -1,73 +1,74 @@ # 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, SRTPEncryption from sylk import configuration_filename from sylk.configuration.datatypes import AudioCodecs, IPAddress, NillablePath, Path, Port, PortRange, SIPProxyAddress from sylk.tls import Certificate, PrivateKey class ServerConfig(ConfigSection): __cfgfile__ = configuration_filename __section__ = 'Server' ca_file = ConfigSetting(type=NillablePath, value=NillablePath('tls/ca.crt')) certificate = ConfigSetting(type=NillablePath, value=NillablePath('tls/default.crt')) verify_server = False enable_bonjour = False default_application = 'conference' application_map = ConfigSetting(type=StringList, value='') disabled_applications = ConfigSetting(type=StringList, value='') + extra_applications_dir = ConfigSetting(type=NillablePath, value=None) resources_dir = ConfigSetting(type=Path, value=None) trace_dir = ConfigSetting(type=Path, value=Path('var/log/sylkserver')) trace_core = False trace_sip = False trace_msrp = False trace_notifications = False 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) outbound_proxy = ConfigSetting(type=SIPProxyAddress, value=None) trusted_peers = ConfigSetting(type=NetworkRangeList, value=NetworkRangeList('any')) 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=None) port_range = ConfigSetting(type=PortRange, value=PortRange('50000:50500')) srtp_encryption = ConfigSetting(type=SRTPEncryption, value='optional') timeout = ConfigSetting(type=NonNegativeInteger, value=30) 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)