diff --git a/sylk/applications/conference/__init__.py b/sylk/applications/conference/__init__.py index f9abba2..326d655 100644 --- a/sylk/applications/conference/__init__.py +++ b/sylk/applications/conference/__init__.py @@ -1,415 +1,415 @@ import os import re import shutil from application.notification import IObserver, NotificationCenter from application.python import Null from sipsimple.account.bonjour import BonjourPresenceState from sipsimple.configuration.settings import SIPSimpleSettings from sipsimple.core import SIPURI, SIPCoreError from sipsimple.core import Header, FromHeader, ToHeader, SubjectHeader from sipsimple.lookup import DNSLookup from sipsimple.streams import MediaStreamRegistry from sipsimple.threading.green import run_in_green_thread from twisted.internet import reactor from zope.interface import implementer from sylk.accounts import DefaultAccount from sylk.applications import SylkApplication from sylk.applications.conference.configuration import get_room_config, ConferenceConfig from sylk.applications.conference.logger import log from sylk.applications.conference.room import Room from sylk.applications.conference.web import ConferenceWeb from sylk.bonjour import BonjourService from sylk.configuration import ServerConfig, ThorNodeConfig from sylk.session import Session, IllegalStateError from sylk.web import server as web_server class ACLValidationError(Exception): pass class RoomNotFoundError(Exception): pass @implementer(IObserver) class ConferenceApplication(SylkApplication): def __init__(self): self._rooms = {} self.invited_participants_map = {} self.bonjour_focus_service = Null self.bonjour_room_service = Null self.web = Null def start(self): self.web = ConferenceWeb(self) - web_server.register_resource('conference', self.web.resource) + web_server.register_resource(b'conference', self.web.resource) # cleanup old files for path in (ConferenceConfig.file_transfer_dir, ConferenceConfig.screensharing_images_dir): try: shutil.rmtree(path) except EnvironmentError: pass if ServerConfig.enable_bonjour and ServerConfig.default_application == 'conference': self.bonjour_focus_service = BonjourService(service='sipfocus') self.bonjour_focus_service.start() log.info("Bonjour publication started for service 'sipfocus'") self.bonjour_room_service = BonjourService(service='sipuri', name='Conference Room', uri_user='conference') self.bonjour_room_service.start() self.bonjour_room_service.presence_state = BonjourPresenceState('available', 'No participants') log.info("Bonjour publication started for service 'sipuri'") def stop(self): self.bonjour_focus_service.stop() self.bonjour_room_service.stop() def get_room(self, uri, create=False): room_uri = '%s@%s' % (uri.user, uri.host) try: room = self._rooms[room_uri] except KeyError: if create: room = Room(room_uri) self._rooms[room_uri] = room return room else: raise RoomNotFoundError else: return room def remove_room(self, uri): room_uri = '%s@%s' % (uri.user, uri.host) self._rooms.pop(room_uri, None) 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): peer = '%s:%s' % (session.transport, session.peer_address) log.info('Session %s from %s: %s -> %s' % (session.call_id, peer, session.remote_identity.uri, session.local_identity.uri)) settings = SIPSimpleSettings() 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'] transfer_streams = [stream for stream in session.proposed_streams if stream.type=='file-transfer'] if not audio_streams and not chat_streams and not transfer_streams: log.info(u'Session rejected: invalid media') session.reject(488) return audio_stream = audio_streams[0] if audio_streams else None chat_stream = chat_streams[0] if chat_streams else None transfer_stream = transfer_streams[0] if transfer_streams else None try: self.validate_acl(session.request_uri, session.remote_identity.uri) except ACLValidationError: log.info('Session rejected: unauthorized by access list') session.reject(403) return if transfer_stream is not None: try: room = self.get_room(session.request_uri) except RoomNotFoundError: log.info('Session rejected: room not found') session.reject(404) return if transfer_stream.direction == 'sendonly': # file transfer 'pull' try: file = next(file for file in room.files if file.hash == transfer_stream.file_selector.hash) except StopIteration: log.info('Session rejected: requested file not found') session.reject(404) return try: transfer_stream.file_selector = file.file_selector except EnvironmentError as e: log.info('Session rejected: error opening requested file: %s' % e) session.reject(404) return else: transfer_stream.handler.save_directory = os.path.join(settings.file_transfer.directory.normalized, room.uri) NotificationCenter().add_observer(self, sender=session) if audio_stream: session.send_ring_indication() streams = [stream for stream in (audio_stream, chat_stream, transfer_stream) if stream] reactor.callLater(4 if audio_stream is not None 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 Null in (from_header, to_header): subscribe_request.reject(400) return if subscribe_request.event != b'conference': log.info('Subscription for event %s rejected: only conference event is supported' % subscribe_request.event) subscribe_request.reject(489) 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: # Check if we need to skip the ACL because this was an invited participant if not (str(from_header.uri) in self.invited_participants_map.get('%s@%s' % (data.request_uri.user, data.request_uri.host), {}) or str(from_header.uri) in self.invited_participants_map.get('%s@%s' % (to_header.uri.user, to_header.uri.host), {})): log.info('Subscription rejected: unauthorized by access list') subscribe_request.reject(403) return try: room = self.get_room(data.request_uri) except RoomNotFoundError: try: room = self.get_room(to_header.uri) except RoomNotFoundError: log.info('Subscription rejected: room not yet created') subscribe_request.reject(480) return if not room.started: log.info('Subscription rejected: room not started yet') subscribe_request.reject(480) else: 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 log.info('Room %s - join request from %s to %s' % ('%s@%s' % (to_header.uri.user, to_header.uri.host), from_header.uri, refer_to_header.uri)) try: self.validate_acl(data.request_uri, from_header.uri) except ACLValidationError: log.info('Room %s - invite participant request rejected: unauthorized by access list' % data.request_uri) refer_request.reject(403) return referral_handler = IncomingReferralHandler(refer_request, data) referral_handler.start() def incoming_message(self, message_request, data): log.info('SIP MESSAGE is not supported, use MSRP media instead') message_request.answer(405) def accept_session(self, session, streams): if session.state == 'incoming': try: session.accept(streams, is_focus=True) except IllegalStateError: pass def add_participant(self, session, room_uri): # Keep track of the invited participants, we must skip ACL policy # for SUBSCRIBE requests room_uri_str = '%s@%s' % (room_uri.user, room_uri.host) log.info('Room %s - outgoing session to %s started' % (room_uri_str, session.remote_identity.uri)) d = self.invited_participants_map.setdefault(room_uri_str, {}) d.setdefault(str(session.remote_identity.uri), 0) d[str(session.remote_identity.uri)] += 1 NotificationCenter().add_observer(self, sender=session) room = self.get_room(room_uri, True) room.start() room.add_session(session) def remove_participant(self, participant_uri, room_uri): try: room = self.get_room(room_uri) except RoomNotFoundError: pass else: log.info('Room %s - %s removed from conference' % (room_uri, participant_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 room = self.get_room(session.request_uri, True) room.start() room.add_session(session) @run_in_green_thread def _NH_SIPSessionDidEnd(self, notification): session = notification.sender notification.center.remove_observer(self, sender=session) if session.direction == 'incoming': room_uri = session.request_uri else: # Clear invited participants mapping room_uri_str = '%s@%s' % (session.local_identity.uri.user, session.local_identity.uri.host) d = self.invited_participants_map[room_uri_str] d[str(session.remote_identity.uri)] -= 1 if d[str(session.remote_identity.uri)] == 0: del d[str(session.remote_identity.uri)] room_uri = session.local_identity.uri # We could get this notifiction even if we didn't get SIPSessionDidStart try: room = self.get_room(room_uri) except RoomNotFoundError: return if session in room.sessions: room.remove_session(session) if not room.stopping and room.empty: self.remove_room(room_uri) room.stop() def _NH_SIPSessionDidFail(self, notification): session = notification.sender notification.center.remove_observer(self, sender=session) log.info('Session from %s failed: %s (%s)' % (session.remote_identity.uri, notification.data.reason, notification.data.failure_reason)) @implementer(IObserver) class IncomingReferralHandler(object): def __init__(self, refer_request, data): self._refer_request = refer_request self._refer_headers = data.headers self.room_uri = data.request_uri self.room_uri_str = '%s@%s' % (self.room_uri.user, self.room_uri.host) self.refer_to_uri = re.sub('<|>', '', data.headers.get('Refer-To').uri) self.method = data.headers.get('Refer-To').parameters.get('method', 'INVITE').upper() self.session = None self.streams = [] def start(self): if not self.refer_to_uri.startswith(('sip:', 'sips:')): self.refer_to_uri = 'sip:%s' % self.refer_to_uri try: self.refer_to_uri = SIPURI.parse(self.refer_to_uri) except SIPCoreError: log.info('Room %s - failed to add %s' % (self.room_uri_str, self.refer_to_uri)) self._refer_request.reject(488) return notification_center = NotificationCenter() notification_center.add_observer(self, sender=self._refer_request) if self.method == 'INVITE': self._refer_request.accept() settings = SIPSimpleSettings() account = DefaultAccount() 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.info('Room %s - %s removed %s from the room' % (self.room_uri_str, self._refer_headers.get('From').uri, self.refer_to_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 = DefaultAccount() conference_application = ConferenceApplication() try: room = conference_application.get_room(self.room_uri) except RoomNotFoundError: log.info('Room %s - failed to add %s' % (self.room_uri_str, self.refer_to_uri)) self._refer_request.end(500) return active_media = set(room.active_media).intersection(('audio', 'chat')) if not active_media: log.info('Room %s - failed to add %s' % (self.room_uri_str, self.refer_to_uri)) self._refer_request.end(500) return for stream_type in active_media: self.streams.append(MediaStreamRegistry.get(stream_type)()) self.session = Session(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), 'Conference Call') to_header = ToHeader(self.refer_to_uri) extra_headers = [] if ThorNodeConfig.enabled: extra_headers.append(Header('Thor-Scope', 'conference-invitation')) # TODO: if this header is longer than 15 characters somehow the SIP packet gets corrupted -adi extra_headers.append(Header('X-Orig-From', str(original_from_header.uri))) extra_headers.append(SubjectHeader('Join conference request from %s' % original_identity)) 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))) route = notification.data.result[0] self.session.connect(from_header, to_header, route=route, streams=self.streams, is_focus=True, extra_headers=extra_headers) def _NH_DNSLookupDidFail(self, notification): notification.center.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, notification.data.reason) def _NH_SIPSessionDidStart(self, notification): notification.center.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) log.info('Room %s - %s added %s' % (self.room_uri_str, self._refer_headers.get('From').uri, self.refer_to_uri)) self.session = None self.streams = [] def _NH_SIPSessionDidFail(self, notification): log.info('Room %s - failed to add %s: %s' % (self.room_uri_str, self.refer_to_uri, notification.data.reason)) notification.center.remove_observer(self, sender=notification.sender) if self._refer_request is not None: self._refer_request.end(notification.data.code or 500, notification.data.reason or notification.data.code) 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 log.info('Room %s - failed to add %s' % (self.room_uri_str, self.refer_to_uri)) notification.center.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): notification.center.remove_observer(self, sender=notification.sender) self._refer_request = None diff --git a/sylk/applications/conference/web.py b/sylk/applications/conference/web.py index 678887a..3c31643 100644 --- a/sylk/applications/conference/web.py +++ b/sylk/applications/conference/web.py @@ -1,83 +1,83 @@ import os import urllib.request, urllib.parse, urllib.error from application.python.types import Singleton from twisted.web.resource import ErrorPage, NoResource from twisted.web.static import File from sylk.applications.conference.configuration import ConferenceConfig from sylk.web import Klein class ConferenceWeb(object, metaclass=Singleton): app = Klein() screensharing_template = """ SylkServer Screen Sharing
""" def __init__(self, conference): self._resource = self.app.resource() self.conference = conference @property def resource(self): return self._resource @app.route('/') def home(self, request): return NoResource('Nothing to see here, move along.') @app.route('/') def room(self, request, room_uri): return NoResource('Nothing to see here, move along.') @app.route('//screensharing') def screensharing(self, request, room_uri): try: room = self.conference._rooms[room_uri] except KeyError: return NoResource('Room not found') request.setHeader('Content-Type', 'text/html; charset=utf-8') - if 'image' not in request.args or not request.args.get('image', [''])[0].endswith('jpg'): + if b'image' not in request.args or not request.args.get(b'image', [''])[0].endswith(b'jpg'): return ErrorPage(400, 'Bad Request', '\"image\" not provided') images_path = os.path.join(ConferenceConfig.screensharing_images_dir, room.uri) - image_path = os.path.basename(urllib.parse.unquote(request.args['image'][0])) + image_path = os.path.basename(urllib.parse.unquote(request.args[b'image'][0])) if not os.path.isfile(os.path.join(images_path, image_path)): return NoResource('Image not found') image = os.path.join('screensharing_img', image_path) width = 'width: 100%' if 'fit' in request.args else '' return self.screensharing_template % dict(image=image, width=width) @app.route('//screensharing_img/') def screensharing_image(self, request, room_uri, filename): try: room = self.conference._rooms[room_uri] except KeyError: return NoResource('Room not found') images_path = os.path.join(ConferenceConfig.screensharing_images_dir, room.uri) return File(os.path.join(images_path, os.path.basename(filename)))