diff --git a/conference.ini.sample b/conference.ini.sample index f01ab88..50e8c79 100644 --- a/conference.ini.sample +++ b/conference.ini.sample @@ -1,18 +1,32 @@ ; SylkServer Conference application configuration file [Conference] ; The following settings are the default used by the software, uncomment them ; only if you want to make changes ; db_uri = sqlite:///var/lib/sylkserver/conference.sqlite ; Table for storing messages history ; history_table = message_history ; Playback the last messages after join a room ; replay_history = 20 ; Use MESSAGE for participants that joined a conference room with audio but without MSRP chat ; enable_sip_message = False +; Apache style ACLs, applied to SIP domains or SIP URIs +; https://httpd.apache.org/docs/2.2/mod/mod_authz_host.html#order + +; Default policy +; access_policy = allow, deny +; allow = all +; deny = none + +; ACLs may also be applied per room (overrides default policy): +; [test@domain.com] +; access_policy = allow, deny +; allow = sip2sip.info, test@domain.com +; deny = all + diff --git a/sylk/applications/conference/__init__.py b/sylk/applications/conference/__init__.py index 4ea9e4c..ccd101e 100644 --- a/sylk/applications/conference/__init__.py +++ b/sylk/applications/conference/__init__.py @@ -1,261 +1,304 @@ # Copyright (C) 2010-2011 AG Projects. See LICENSE for details # import re from application import log from application.notification import IObserver, NotificationCenter from application.python.util import Null, Singleton from sipsimple.account import AccountManager from sipsimple.configuration.settings import SIPSimpleSettings from sipsimple.core import SIPURI, SIPCoreError, Header, ContactHeader, FromHeader, ToHeader from sipsimple.lookup import DNSLookup from sipsimple.streams import AudioStream from twisted.internet import reactor from zope.interface import implements from sylk.applications import ISylkApplication, sylk_application -from sylk.applications.conference.configuration import ConferenceConfig +from sylk.applications.conference.configuration import ConferenceConfig, get_room_config from sylk.applications.conference.room import Room from sylk.configuration import SIPConfig, ThorNodeConfig from sylk.extensions import ChatStream from sylk.session import ServerSession # Initialize database from sylk.applications.conference import database +class ACLValidationError(Exception): pass + @sylk_application class ConferenceApplication(object): __metaclass__ = Singleton implements(ISylkApplication, IObserver) __appname__ = 'conference' def __init__(self): self.rooms = set() self.pending_sessions = [] + def validate_acl(self, room_uri, from_uri): + room_uri = '%s@%s' % (room_uri.user, room_uri.host) + cfg = get_room_config(room_uri) + if cfg.access_policy == 'allow,deny': + if cfg.allow.match(from_uri) and not cfg.deny.match(from_uri): + return + raise ACLValidationError + else: + if cfg.deny.match(from_uri) and not cfg.allow.match(from_uri): + raise ACLValidationError + def incoming_session(self, session): log.msg('New incoming session from %s' % session.remote_identity.uri) audio_streams = [stream for stream in session.proposed_streams if stream.type=='audio'] chat_streams = [stream for stream in session.proposed_streams if stream.type=='chat'] if not audio_streams and not chat_streams: session.reject(488) return + try: + self.validate_acl(session._invitation.request_uri, session.remote_identity.uri) + except ACLValidationError: + session.reject(403) + return self.pending_sessions.append(session) notification_center = NotificationCenter() notification_center.add_observer(self, sender=session) if audio_streams: session.send_ring_indication() streams = [streams[0] for streams in (audio_streams, chat_streams) if streams] reactor.callLater(4 if audio_streams else 0, self.accept_session, session, streams) def incoming_subscription(self, subscribe_request, data): + from_header = data.headers.get('From', Null) to_header = data.headers.get('To', Null) - if to_header is Null: + if Null in (from_header, to_header): subscribe_request.reject(400) return + try: + self.validate_acl(data.request_uri, from_header.uri) + except ACLValidationError: + try: + self.validate_acl(to_header.uri, from_header.uri) + except ACLValidationError: + subscribe_request.reject(403) + return room = Room.get_room(data.request_uri) if not room.started: room = Room.get_room(to_header.uri) if not room.started: subscribe_request.reject(480) return room.handle_incoming_subscription(subscribe_request, data) def incoming_referral(self, refer_request, data): from_header = data.headers.get('From', Null) to_header = data.headers.get('To', Null) refer_to_header = data.headers.get('Refer-To', Null) if Null in (from_header, to_header, refer_to_header): refer_request.reject(400) return + try: + self.validate_acl(data.request_uri, from_header.uri) + except ACLValidationError: + refer_request.reject(403) + return referral_handler = IncomingReferralHandler(refer_request, data) referral_handler.start() def incoming_sip_message(self, message_request, data): + from_header = data.headers.get('From', Null) + to_header = data.headers.get('To', Null) + if Null in (from_header, to_header): + message_request.answer(400) + return if not ConferenceConfig.enable_sip_message: message_request.answer(405) return + try: + self.validate_acl(data.request_uri, from_header.uri) + except ACLValidationError: + message_request.answer(403) + return room = Room.get_room(data.request_uri) if not room.started: message_request.answer(480) return room.handle_incoming_sip_message(message_request, data) def accept_session(self, session, streams): if session in self.pending_sessions: session.accept(streams, is_focus=True) def add_participant(self, session, room_uri): log.msg('Outgoing session to %s started' % session.remote_identity.uri) notification_center = NotificationCenter() notification_center.add_observer(self, sender=session) room = Room.get_room(room_uri) room.start() room.add_session(session) self.rooms.add(room) def remove_participant(self, participant_uri, room_uri): room = Room.get_room(room_uri) room.terminate_sessions(participant_uri) def handle_notification(self, notification): handler = getattr(self, '_NH_%s' % notification.name, Null) handler(notification) def _NH_SIPSessionDidStart(self, notification): session = notification.sender self.pending_sessions.remove(session) room = Room.get_room(session._invitation.request_uri) # FIXME room.start() room.add_session(session) self.rooms.add(room) def _NH_SIPSessionDidEnd(self, notification): session = notification.sender log.msg('Session from %s ended' % session.remote_identity.uri) notification_center = NotificationCenter() notification_center.remove_observer(self, sender=session) if session.direction == 'incoming': room = Room.get_room(session._invitation.request_uri) # FIXME else: room = Room.get_room(session.local_identity.uri) if session in room.sessions: # We could get this notifiction even if we didn't get SIPSessionDidStart room.remove_session(session) if room.empty: room.stop() try: self.rooms.remove(room) except KeyError: pass def _NH_SIPSessionDidFail(self, notification): session = notification.sender self.pending_sessions.remove(session) log.msg('Session from %s failed' % session.remote_identity.uri) class IncomingReferralHandler(object): implements(IObserver) def __init__(self, refer_request, data): self._refer_request = refer_request self._refer_headers = data.headers self.room_uri = data.headers.get('To').uri self.refer_to_uri = data.headers.get('Refer-To').uri self.method = data.headers.get('Refer-To').parameters.get('method', 'invite').lower() self.session = None self.streams = [] def start(self): if not re.match('^(sip:|sips:).*', self.refer_to_uri): self.refer_to_uri = 'sip:%s' % self.refer_to_uri try: self.refer_to_uri = SIPURI.parse(self.refer_to_uri) except SIPCoreError: self._refer_request.reject(488) return notification_center = NotificationCenter() notification_center.add_observer(self, sender=self._refer_request) if self.method == 'invite': log.msg('%s added %s to %s' % (self._refer_headers.get('From').uri, self.refer_to_uri, self.room_uri)) self._refer_request.accept() settings = SIPSimpleSettings() account = AccountManager().default_account if account.sip.outbound_proxy is not None: uri = SIPURI(host=account.sip.outbound_proxy.host, port=account.sip.outbound_proxy.port, parameters={'transport': account.sip.outbound_proxy.transport}) else: uri = self.refer_to_uri lookup = DNSLookup() notification_center.add_observer(self, sender=lookup) lookup.lookup_sip_proxy(uri, settings.sip.transport_list) elif self.method == 'bye': log.msg('%s removed %s from %s' % (self._refer_headers.get('From').uri, self.refer_to_uri, self.room_uri)) self._refer_request.accept() conference_application = ConferenceApplication() conference_application.remove_participant(self.refer_to_uri, self.room_uri) self._refer_request.end(200) else: self._refer_request.reject(488) def handle_notification(self, notification): handler = getattr(self, '_NH_%s' % notification.name, Null) handler(notification) def _NH_DNSLookupDidSucceed(self, notification): notification_center = NotificationCenter() notification_center.remove_observer(self, sender=notification.sender) account = AccountManager().default_account active_media = Room.get_room(self.room_uri).active_media if not active_media: return if 'audio' in active_media: self.streams.append(AudioStream(account)) if 'chat' in active_media: self.streams.append(ChatStream(account)) self.session = ServerSession(account) notification_center.add_observer(self, sender=self.session) original_from_header = self._refer_headers.get('From') if original_from_header.display_name: original_identity = "%s <%s@%s>" % (original_from_header.display_name, original_from_header.uri.user, original_from_header.uri.host) else: original_identity = "%s@%s" % (original_from_header.uri.user, original_from_header.uri.host) from_header = FromHeader(SIPURI.new(self.room_uri), u'Conference Call') to_header = ToHeader(self.refer_to_uri) transport = notification.data.result[0].transport parameters = {} if transport=='udp' else {'transport': transport} contact_header = ContactHeader(SIPURI(user=self.room_uri.user, host=SIPConfig.local_ip, port=getattr(SIPConfig, 'local_%s_port' % transport), parameters=parameters)) extra_headers = [] if self._refer_headers.get('Referred-By', None) is not None: extra_headers.append(Header.new(self._refer_headers.get('Referred-By'))) else: extra_headers.append(Header('Referred-By', str(original_from_header.uri))) if ThorNodeConfig.enabled: extra_headers.append(Header('Thor-Scope', 'conference-invitation')) extra_headers.append(Header('X-Referrer-From', str(original_from_header.uri))) subject = u'Join conference request from %s' % original_identity self.session.connect(from_header, to_header, contact_header, routes=notification.data.result, streams=self.streams, is_focus=True, subject=subject, extra_headers=extra_headers) def _NH_DNSLookupDidFail(self, notification): NotificationCenter().remove_observer(self, sender=notification.sender) def _NH_SIPSessionGotRingIndication(self, notification): if self._refer_request is not None: self._refer_request.send_notify(180) def _NH_SIPSessionGotProvisionalResponse(self, notification): if self._refer_request is not None: self._refer_request.send_notify(notification.data.code) def _NH_SIPSessionDidStart(self, notification): NotificationCenter().remove_observer(self, sender=notification.sender) if self._refer_request is not None: self._refer_request.end(200) conference_application = ConferenceApplication() conference_application.add_participant(self.session, self.room_uri) self.session = None self.streams = [] def _NH_SIPSessionDidFail(self, notification): NotificationCenter().remove_observer(self, sender=notification.sender) if self._refer_request is not None: self._refer_request.end(notification.data.code or 500) self.session = None self.streams = [] def _NH_SIPSessionDidEnd(self, notification): # If any stream fails to start we won't get SIPSessionDidFail, we'll get here instead NotificationCenter().remove_observer(self, sender=notification.sender) if self._refer_request is not None: self._refer_request.end(200) self.session = None self.streams = [] def _NH_SIPIncomingReferralDidEnd(self, notification): NotificationCenter().remove_observer(self, sender=notification.sender) + diff --git a/sylk/applications/conference/configuration.py b/sylk/applications/conference/configuration.py index bfb5cb9..6bac2bf 100644 --- a/sylk/applications/conference/configuration.py +++ b/sylk/applications/conference/configuration.py @@ -1,16 +1,113 @@ # Copyright (C) 2010-2011 AG Projects. See LICENSE for details. # -from application.configuration import ConfigSection, ConfigSetting +__all__ = ['ConferenceConfig', 'get_room_config'] +import re + +from application.configuration import ConfigFile, ConfigSection, ConfigSetting + + +# Datatypes + +class AccessPolicyValue(str): + allowed_values = ('allow,deny', 'deny,allow') + + def __new__(cls, value): + value = re.sub('\s', '', value) + if value not in cls.allowed_values: + raise ValueError('invalid value, allowed values are: %s' % ' | '.join(cls.allowed_values)) + return str.__new__(cls, value) + + +class Domain(str): + domain_re = re.compile(r"^[a-zA-Z0-9\-_]+(\.[a-zA-Z0-9\-_]+)*$") + + def __new__(cls, value): + value = str(value) + if not cls.domain_re.match(value): + raise ValueError("illegal domain: %s" % value) + return str.__new__(cls, value) + + +class SIPAddress(str): + def __new__(cls, address): + address = str(address) + address = address.replace('@', '%40', address.count('@')-1) + try: + username, domain = address.split('@') + Domain(domain) + except ValueError: + raise ValueError("illegal SIP address: %s, must be in user@domain format" % address) + return str.__new__(cls, address) + + +class PolicySettingValue(list): + def __init__(self, value): + if isinstance(value, (tuple, list)): + l = [str(x) for x in value] + elif isinstance(value, basestring): + if value.lower() in ('none', ''): + return list.__init__(self, []) + elif value.lower() in ('any', 'all', '*'): + return list.__init__(self, ['*']) + else: + l = re.split(r'\s*,\s*', value) + else: + raise TypeError("value must be a string, list or tuple") + values = [] + for item in l: + if '@' in item: + values.append(SIPAddress(item)) + else: + values.append(Domain(item)) + return list.__init__(self, values) + + def match(self, uri): + if self == ['*']: + return True + domain = uri.host + uri = re.sub('^(sip:|sips:)', '', str(uri)) + return uri in self or domain in self + + +# Configuration objects class ConferenceConfig(ConfigSection): __cfgfile__ = 'conference.ini' __section__ = 'Conference' db_uri = ConfigSetting(type=str, value='sqlite:///var/lib/sylkserver/conference.sqlite') history_table = ConfigSetting(type=str, value='message_history') enable_sip_message = False replay_history = 20 + access_policy = ConfigSetting(type=AccessPolicyValue, value=AccessPolicyValue('allow, deny')) + allow = ConfigSetting(type=PolicySettingValue, value=PolicySettingValue('all')) + deny = ConfigSetting(type=PolicySettingValue, value=PolicySettingValue('none')) + + +class RoomConfig(ConfigSection): + __cfgfile__ = 'conference.ini' + + access_policy = ConfigSetting(type=AccessPolicyValue, value=AccessPolicyValue('allow, deny')) + allow = ConfigSetting(type=PolicySettingValue, value=PolicySettingValue('all')) + deny = ConfigSetting(type=PolicySettingValue, value=PolicySettingValue('none')) + + +class Configuration(object): + def __init__(self, data): + self.__dict__.update(data) + +def get_room_config(room): + config_file = ConfigFile(RoomConfig.__cfgfile__) + section = config_file.get_section(room) + if section is not None: + RoomConfig.read(section=room) + config = Configuration(dict(RoomConfig)) + RoomConfig.reset() + else: + # Apply general policy + config = Configuration(dict((attr, getattr(ConferenceConfig, attr)) for attr in ('access_policy', 'allow', 'deny'))) + return config