diff --git a/setup_pjsip.py b/setup_pjsip.py index e948e672..010eb446 100644 --- a/setup_pjsip.py +++ b/setup_pjsip.py @@ -1,220 +1,220 @@ import errno import itertools import os import platform import re import shutil import subprocess import sys if sys.platform.startswith('linux'): sys_platform = 'linux' elif sys.platform.startswith('freebsd'): sys_platform = 'freebsd' else: sys_platform = sys.platform # Hack to set environment variables before importing distutils # modules that will fetch them and set the compiler and linker # to be used. -Saul if sys_platform == "darwin": min_osx_version = "10.13" try: osx_sdk_path = subprocess.check_output(["xcodebuild", "-version", "-sdk", "macosx", "Path"]).strip() except subprocess.CalledProcessError as e: raise RuntimeError("Could not locate SDK path: %s" % str(e)) # OpenSSL (must be installed with Homebrew) ossl_cflags = "-I/usr/local/opt/openssl/include" ossl_ldflags = "-L/usr/local/opt/openssl/lib" # SQLite (must be installed with Homebrew) sqlite_cflags = "-I/usr/local/opt/sqlite/include" sqlite_ldflags = "-L/usr/local/opt/sqlite/lib" # Prepare final flags arch_flags = "-arch x86_64" local_cflags = " %s %s %s -mmacosx-version-min=%s -isysroot %s" % (arch_flags, ossl_cflags, sqlite_cflags, min_osx_version, osx_sdk_path) local_ldflags = " %s %s %s -isysroot %s" % (arch_flags, ossl_ldflags, sqlite_ldflags, osx_sdk_path) os.environ['CFLAGS'] = os.environ.get('CFLAGS', '') + local_cflags os.environ['LDFLAGS'] = os.environ.get('LDFLAGS', '') + local_ldflags os.environ['ARCHFLAGS'] = arch_flags os.environ['MACOSX_DEPLOYMENT_TARGET'] = min_osx_version from distutils import log from distutils.dir_util import copy_tree from distutils.errors import DistutilsError from Cython.Distutils import build_ext class PJSIP_build_ext(build_ext): config_site = ["#define PJ_SCANNER_USE_BITWISE 0", "#define PJSIP_SAFE_MODULE 0", "#define PJSIP_MAX_PKT_LEN 262144", "#define PJSIP_UNESCAPE_IN_PLACE 1", "#define PJMEDIA_AUDIO_DEV_HAS_COREAUDIO %d" % (1 if sys_platform=="darwin" else 0), "#define PJMEDIA_AUDIO_DEV_HAS_ALSA %d" % (1 if sys_platform=="linux" else 0), "#define PJMEDIA_AUDIO_DEV_HAS_WMME %d" % (1 if sys_platform=="win32" else 0), "#define PJMEDIA_HAS_SPEEX_AEC 0", "#define PJMEDIA_HAS_WEBRTC_AEC %d" % (1 if re.match('i\d86|x86|x86_64', platform.machine()) else 0), "#define PJMEDIA_RTP_PT_TELEPHONE_EVENTS 101", "#define PJMEDIA_RTP_PT_TELEPHONE_EVENTS_STR \"101\"", "#define PJMEDIA_STREAM_ENABLE_KA PJMEDIA_STREAM_KA_EMPTY_RTP", "#define PJMEDIA_STREAM_VAD_SUSPEND_MSEC 0", "#define PJMEDIA_CODEC_MAX_SILENCE_PERIOD -1", "#define PJ_ICE_MAX_CHECKS 256", "#define PJ_LOG_MAX_LEVEL 6", "#define PJ_IOQUEUE_MAX_HANDLES 1024", "#define PJ_DNS_RESOLVER_MAX_TTL 0", "#define PJ_DNS_RESOLVER_INVALID_TTL 0", "#define PJSIP_TRANSPORT_IDLE_TIME 7200", "#define PJ_ENABLE_EXTRA_CHECK 1", "#define PJSIP_DONT_SWITCH_TO_TCP 1", "#define PJMEDIA_VIDEO_DEV_HAS_SDL 0", "#define PJMEDIA_VIDEO_DEV_HAS_AVI 0", "#define PJMEDIA_VIDEO_DEV_HAS_FB 1", "#define PJMEDIA_VIDEO_DEV_HAS_V4L2 %d" % (1 if sys_platform=="linux" else 0), "#define PJMEDIA_VIDEO_DEV_HAS_AVF %d" % (1 if sys_platform=="darwin" else 0), "#define PJMEDIA_VIDEO_DEV_HAS_DSHOW %d" % (1 if sys_platform=="win32" else 0), "#define PJMEDIA_VIDEO_DEV_HAS_CBAR_SRC 1", "#define PJMEDIA_VIDEO_DEV_HAS_NULL 1"] user_options = build_ext.user_options user_options.extend([ ("pjsip-clean-compile", None, "Clean PJSIP tree before compilation"), ("pjsip-disable-assertions", None, "Disable assertion checks within PJSIP"), ("pjsip-verbose-build", None, "Print output of PJSIP compilation process") ]) boolean_options = build_ext.boolean_options boolean_options.extend(["pjsip-clean-compile", "pjsip-disable-assertions", "pjsip-verbose-build"]) @staticmethod def distutils_exec_process(cmdline, silent=True, input=None, **kwargs): """Execute a subprocess and returns the returncode, stdout buffer and stderr buffer. Optionally prints stdout and stderr while running.""" try: sub = subprocess.Popen(cmdline, stdin=subprocess.PIPE, stdout=subprocess.PIPE, stderr=subprocess.PIPE, **kwargs) stdout, stderr = sub.communicate(input=input) returncode = sub.returncode if not silent: sys.stdout.write(stdout) sys.stderr.write(stderr) - except OSError, e: + except OSError as e: if e.errno == errno.ENOENT: raise RuntimeError('"%s" is not present on this system' % cmdline[0]) else: raise if returncode != 0: raise RuntimeError('Got return value %d while executing "%s", stderr output was:\n%s' % (returncode, " ".join(cmdline), stderr.rstrip("\n"))) return stdout @staticmethod def get_make_cmd(): if sys_platform == "freebsd": return "gmake" else: return "make" @staticmethod def get_opts_from_string(line, prefix): """Returns all options that have a particular prefix on a commandline""" chunks = [chunk.strip() for chunk in line.split()] return [chunk[len(prefix):] for chunk in chunks if chunk.startswith(prefix)] @classmethod def get_makefile_variables(cls, makefile): """Returns all variables in a makefile as a dict""" stdout = cls.distutils_exec_process([cls.get_make_cmd(), "-f", makefile, "-pR", makefile], silent=True) return dict(tup for tup in re.findall("(^[a-zA-Z]\w+)\s*:?=\s*(.*)$", stdout, re.MULTILINE)) @classmethod def makedirs(cls, path): try: os.makedirs(path) - except OSError, e: + except OSError as e: if e.errno==errno.EEXIST and os.path.isdir(path) and os.access(path, os.R_OK | os.W_OK | os.X_OK): return raise def initialize_options(self): build_ext.initialize_options(self) self.pjsip_clean_compile = 0 self.pjsip_verbose_build = 0 self.pjsip_dir = os.path.join(os.path.dirname(__file__), "deps", "pjsip") def configure_pjsip(self): log.info("Configuring PJSIP") with open(os.path.join(self.build_dir, "pjlib", "include", "pj", "config_site.h"), "wb") as f: f.write("\n".join(self.config_site+[""])) cflags = "-DNDEBUG -g -fPIC -fno-omit-frame-pointer -fno-strict-aliasing -Wno-unused-label" if self.debug or hasattr(sys, 'gettotalrefcount'): log.info("PJSIP will be built without optimizations") cflags += " -O0" else: cflags += " -O2" env = os.environ.copy() env['CFLAGS'] = ' '.join(x for x in (cflags, env.get('CFLAGS', None)) if x) if sys_platform == "win32": cmd = ["bash", "configure"] else: cmd = ["./configure"] cmd.extend(["--disable-g7221-codec"]) ffmpeg_path = env.get("SIPSIMPLE_FFMPEG_PATH", None) if ffmpeg_path is not None: cmd.append("--with-ffmpeg=%s" % os.path.abspath(os.path.expanduser(ffmpeg_path))) libvpx_path = env.get("SIPSIMPLE_LIBVPX_PATH", None) if libvpx_path is not None: cmd.append("--with-vpx=%s" % os.path.abspath(os.path.expanduser(libvpx_path))) self.distutils_exec_process(cmd, silent=not self.pjsip_verbose_build, cwd=self.build_dir, env=env) if "#define PJ_HAS_SSL_SOCK 1\n" not in open(os.path.join(self.build_dir, "pjlib", "include", "pj", "compat", "os_auto.h")).readlines(): os.remove(os.path.join(self.build_dir, "build.mak")) raise DistutilsError("PJSIP TLS support was disabled, OpenSSL development files probably not present on this system") def compile_pjsip(self): log.info("Compiling PJSIP") self.distutils_exec_process([self.get_make_cmd()], silent=not self.pjsip_verbose_build, cwd=self.build_dir) def clean_pjsip(self): log.info("Cleaning PJSIP") try: shutil.rmtree(self.build_dir) - except OSError, e: + except OSError as e: if e.errno == errno.ENOENT: return raise def update_extension(self, extension): build_mak_vars = self.get_makefile_variables(os.path.join(self.build_dir, "build.mak")) extension.include_dirs = self.get_opts_from_string(build_mak_vars["PJ_CFLAGS"], "-I") extension.library_dirs = self.get_opts_from_string(build_mak_vars["PJ_LDFLAGS"], "-L") extension.libraries = self.get_opts_from_string(build_mak_vars["PJ_LDLIBS"], "-l") extension.define_macros = [tuple(define.split("=", 1)) for define in self.get_opts_from_string(build_mak_vars["PJ_CFLAGS"], "-D")] extension.define_macros.append(("PJ_SVN_REVISION", open(os.path.join(self.build_dir, "base_rev"), "r").read().strip())) extension.define_macros.append(("__PYX_FORCE_INIT_THREADS", 1)) extension.extra_compile_args.append("-Wno-unused-function") # silence warning if sys_platform == "darwin": extension.define_macros.append(("MACOSX_DEPLOYMENT_TARGET", min_osx_version)) frameworks = re.findall("-framework (\S+)(?:\s|$)", build_mak_vars["PJ_LDLIBS"]) extension.extra_link_args = list(itertools.chain(*(("-framework", val) for val in frameworks))) extension.extra_link_args.append("-mmacosx-version-min=%s" % min_osx_version) extension.extra_compile_args.append("-mmacosx-version-min=%s" % min_osx_version) extension.library_dirs.append("%s/usr/lib" % osx_sdk_path) extension.include_dirs.append("%s/usr/include" % osx_sdk_path) extension.depends = build_mak_vars["PJ_LIB_FILES"].split() self.libraries = extension.depends[:] def cython_sources(self, sources, extension): log.info("Compiling Cython extension %s" % extension.name) if extension.name == "sipsimple.core._core": self.build_dir = os.path.join(self.build_temp, "pjsip") if self.pjsip_clean_compile: self.clean_pjsip() copy_tree(self.pjsip_dir, self.build_dir, verbose=0) if not os.path.exists(os.path.join(self.build_dir, "build.mak")): self.configure_pjsip() self.update_extension(extension) self.compile_pjsip() return build_ext.cython_sources(self, sources, extension) diff --git a/sipsimple/account/__init__.py b/sipsimple/account/__init__.py index 5f6ab05b..cf70b834 100644 --- a/sipsimple/account/__init__.py +++ b/sipsimple/account/__init__.py @@ -1,848 +1,846 @@ """ Implements a SIP Account management system that allows the definition of multiple SIP accounts and their properties. """ __all__ = ['Account', 'BonjourAccount', 'AccountManager'] from itertools import chain from threading import Lock from application.notification import IObserver, NotificationCenter, NotificationData from application.python import Null from application.python.decorator import execute_once from application.python.descriptor import classproperty from application.python.types import Singleton from application.system import host as Host from eventlib import coros, proc from gnutls.crypto import X509Certificate, X509PrivateKey from gnutls.interfaces.twisted import X509Credentials from zope.interface import implements from sipsimple.account.bonjour import BonjourServices, _bonjour from sipsimple.account.publication import PresencePublisher, DialogPublisher from sipsimple.account.registration import Registrar from sipsimple.account.subscription import MWISubscriber, PresenceWinfoSubscriber, DialogWinfoSubscriber, PresenceSubscriber, SelfPresenceSubscriber, DialogSubscriber from sipsimple.account.xcap import XCAPManager from sipsimple.core import Credentials, SIPURI, ContactURIFactory from sipsimple.configuration import ConfigurationManager, Setting, SettingsGroup, SettingsObject, SettingsObjectID from sipsimple.configuration.datatypes import AudioCodecList, MSRPConnectionModel, MSRPRelayAddress, MSRPTransport, NonNegativeInteger, Path, SIPAddress, SIPProxyAddress, SRTPKeyNegotiation, STUNServerAddressList, VideoCodecList, XCAPRoot from sipsimple.configuration.settings import SIPSimpleSettings from sipsimple.payloads import ParserError from sipsimple.payloads.messagesummary import MessageSummary from sipsimple.payloads.pidf import PIDFDocument from sipsimple.payloads.rlsnotify import RLSNotify from sipsimple.payloads.watcherinfo import WatcherInfoDocument from sipsimple.threading import call_in_thread from sipsimple.threading.green import call_in_green_thread, run_in_green_thread from sipsimple.util import user_info class AuthSettings(SettingsGroup): username = Setting(type=str, default=None, nillable=True) password = Setting(type=str, default='') class SIPSettings(SettingsGroup): always_use_my_proxy = Setting(type=bool, default=False) outbound_proxy = Setting(type=SIPProxyAddress, default=None, nillable=True) register = Setting(type=bool, default=True) register_interval = Setting(type=NonNegativeInteger, default=3600) subscribe_interval = Setting(type=NonNegativeInteger, default=3600) publish_interval = Setting(type=NonNegativeInteger, default=3600) class SRTPEncryptionSettings(SettingsGroup): enabled = Setting(type=bool, default=True) key_negotiation = Setting(type=SRTPKeyNegotiation, default='opportunistic') class RTPSettings(SettingsGroup): audio_codec_list = Setting(type=AudioCodecList, default=None, nillable=True) video_codec_list = Setting(type=VideoCodecList, default=None, nillable=True) encryption = SRTPEncryptionSettings class NATTraversalSettings(SettingsGroup): use_ice = Setting(type=bool, default=False) stun_server_list = Setting(type=STUNServerAddressList, default=None, nillable=True) msrp_relay = Setting(type=MSRPRelayAddress, default=None, nillable=True) use_msrp_relay_for_outbound = Setting(type=bool, default=False) class MessageSummarySettings(SettingsGroup): enabled = Setting(type=bool, default=False) voicemail_uri = Setting(type=SIPAddress, default=None, nillable=True) class XCAPSettings(SettingsGroup): enabled = Setting(type=bool, default=False) discovered = Setting(type=bool, default=False) xcap_root = Setting(type=XCAPRoot, default=None, nillable=True) class PresenceSettings(SettingsGroup): enabled = Setting(type=bool, default=False) class TLSSettings(SettingsGroup): certificate = Setting(type=Path, default=None, nillable=True) verify_server = Setting(type=bool, default=False) class MSRPSettings(SettingsGroup): transport = Setting(type=MSRPTransport, default='tls') connection_model = Setting(type=MSRPConnectionModel, default='relay') class Account(SettingsObject): """ Object representing a SIP account. Contains configuration settings and attributes for accessing SIP related objects. When the account is active, it will register, publish its presence and subscribe to watcher-info events depending on its settings. If the object is un-pickled and its enabled flag was set, it will automatically activate. When the save method is called, depending on the value of the enabled flag, the account will activate/deactivate. Notifications sent by instances of Account: * CFGSettingsObjectWasCreated * CFGSettingsObjectWasActivated * CFGSettingsObjectWasDeleted * CFGSettingsObjectDidChange * SIPAccountWillActivate * SIPAccountDidActivate * SIPAccountWillDeactivate * SIPAccountDidDeactivate """ implements(IObserver) __group__ = 'Accounts' __id__ = SettingsObjectID(type=SIPAddress) id = __id__ enabled = Setting(type=bool, default=False) - display_name = Setting(type=unicode, default=None, nillable=True) + display_name = Setting(type=str, default=None, nillable=True) auth = AuthSettings sip = SIPSettings rtp = RTPSettings nat_traversal = NATTraversalSettings message_summary = MessageSummarySettings msrp = MSRPSettings presence = PresenceSettings xcap = XCAPSettings tls = TLSSettings def __new__(cls, id): with AccountManager.load.lock: if not AccountManager.load.called: raise RuntimeError("cannot instantiate %s before calling AccountManager.load" % cls.__name__) return SettingsObject.__new__(cls, id) def __init__(self, id): self.contact = ContactURIFactory() self.xcap_manager = XCAPManager(self) self._started = False self._deleted = False self._active = False self._activation_lock = coros.Semaphore(1) self._registrar = Registrar(self) self._mwi_subscriber = MWISubscriber(self) self._pwi_subscriber = PresenceWinfoSubscriber(self) self._dwi_subscriber = DialogWinfoSubscriber(self) self._presence_subscriber = PresenceSubscriber(self) self._self_presence_subscriber = SelfPresenceSubscriber(self) self._dialog_subscriber = DialogSubscriber(self) self._presence_publisher = PresencePublisher(self) self._dialog_publisher = DialogPublisher(self) self._mwi_voicemail_uri = None self._pwi_version = None self._dwi_version = None self._presence_version = None self._dialog_version = None def start(self): if self._started or self._deleted: return self._started = True notification_center = NotificationCenter() notification_center.add_observer(self, name='CFGSettingsObjectDidChange', sender=self) notification_center.add_observer(self, name='CFGSettingsObjectDidChange', sender=SIPSimpleSettings()) notification_center.add_observer(self, name='XCAPManagerDidDiscoverServerCapabilities', sender=self.xcap_manager) notification_center.add_observer(self, sender=self._mwi_subscriber) notification_center.add_observer(self, sender=self._pwi_subscriber) notification_center.add_observer(self, sender=self._dwi_subscriber) notification_center.add_observer(self, sender=self._presence_subscriber) notification_center.add_observer(self, sender=self._self_presence_subscriber) notification_center.add_observer(self, sender=self._dialog_subscriber) self.xcap_manager.init() if self.enabled: self._activate() def stop(self): if not self._started: return self._started = False self._deactivate() notification_center = NotificationCenter() notification_center.remove_observer(self, name='CFGSettingsObjectDidChange', sender=self) notification_center.remove_observer(self, name='CFGSettingsObjectDidChange', sender=SIPSimpleSettings()) notification_center.remove_observer(self, name='XCAPManagerDidDiscoverServerCapabilities', sender=self.xcap_manager) notification_center.remove_observer(self, sender=self._mwi_subscriber) notification_center.remove_observer(self, sender=self._pwi_subscriber) notification_center.remove_observer(self, sender=self._dwi_subscriber) notification_center.remove_observer(self, sender=self._presence_subscriber) notification_center.remove_observer(self, sender=self._self_presence_subscriber) notification_center.remove_observer(self, sender=self._dialog_subscriber) def delete(self): if self._deleted: return self._deleted = True self.stop() self._registrar = None self._mwi_subscriber = None self._pwi_subscriber = None self._dwi_subscriber = None self._presence_subscriber = None self._self_presence_subscriber = None self._dialog_subscriber = None self._presence_publisher = None self._dialog_publisher = None self.xcap_manager = None SettingsObject.delete(self) @run_in_green_thread def reregister(self): if self._started: self._registrar.reregister() @run_in_green_thread def resubscribe(self): if self._started: self._mwi_subscriber.resubscribe() self._pwi_subscriber.resubscribe() self._dwi_subscriber.resubscribe() self._presence_subscriber.resubscribe() self._self_presence_subscriber.resubscribe() self._dialog_subscriber.resubscribe() @property def credentials(self): return Credentials(self.auth.username or self.id.username, self.auth.password) @property def registered(self): try: return self._registrar.registered except AttributeError: return False @property def mwi_active(self): try: return self._mwi_subscriber.subscribed except AttributeError: return False @property def tls_credentials(self): # This property can be optimized to cache the credentials it loads from disk, # however this is not a time consuming operation (~ 3000 req/sec). -Luci settings = SIPSimpleSettings() if self.tls.certificate is not None: certificate_data = open(self.tls.certificate.normalized).read() certificate = X509Certificate(certificate_data) private_key = X509PrivateKey(certificate_data) else: certificate = None private_key = None if settings.tls.ca_list is not None: # we should read all certificates in the file, rather than just the first -Luci trusted = [X509Certificate(open(settings.tls.ca_list.normalized).read())] else: trusted = [] credentials = X509Credentials(certificate, private_key, trusted) credentials.verify_peer = self.tls.verify_server return credentials @property def uri(self): return SIPURI(user=self.id.username, host=self.id.domain) @property def voicemail_uri(self): return self._mwi_voicemail_uri or self.message_summary.voicemail_uri @property def presence_state(self): try: return self._presence_publisher.state except AttributeError: return None @presence_state.setter def presence_state(self, state): try: self._presence_publisher.state = state except AttributeError: pass @property def dialog_state(self): try: return self._dialog_publisher.state except AttributeError: return None @dialog_state.setter def dialog_state(self, state): try: self._dialog_publisher.state = state except AttributeError: pass def handle_notification(self, notification): handler = getattr(self, '_NH_%s' % notification.name, Null) handler(notification) @run_in_green_thread def _NH_CFGSettingsObjectDidChange(self, notification): if self._started and 'enabled' in notification.data.modified: if self.enabled: self._activate() else: self._deactivate() def _NH_XCAPManagerDidDiscoverServerCapabilities(self, notification): if self._started and self.xcap.discovered is False: self.xcap.discovered = True self.save() notification.center.post_notification('SIPAccountDidDiscoverXCAPSupport', sender=self) def _NH_MWISubscriberDidDeactivate(self, notification): self._mwi_voicemail_uri = None def _NH_MWISubscriptionGotNotify(self, notification): if notification.data.body and notification.data.content_type == MessageSummary.content_type: try: message_summary = MessageSummary.parse(notification.data.body) except ParserError: pass else: self._mwi_voicemail_uri = message_summary.message_account and SIPAddress(message_summary.message_account.replace('sip:', '', 1)) or None notification.center.post_notification('SIPAccountGotMessageSummary', sender=self, data=NotificationData(message_summary=message_summary)) def _NH_PresenceWinfoSubscriptionGotNotify(self, notification): if notification.data.body and notification.data.content_type == WatcherInfoDocument.content_type: try: watcher_info = WatcherInfoDocument.parse(notification.data.body) watcher_list = watcher_info['sip:' + self.id] except (ParserError, KeyError): pass else: if watcher_list.package != 'presence': return if self._pwi_version is None: if watcher_info.state == 'partial': self._pwi_subscriber.resubscribe() elif watcher_info.version <= self._pwi_version: return elif watcher_info.state == 'partial' and watcher_info.version > self._pwi_version + 1: self._pwi_subscriber.resubscribe() self._pwi_version = watcher_info.version data = NotificationData(version=watcher_info.version, state=watcher_info.state, watcher_list=watcher_list) notification.center.post_notification('SIPAccountGotPresenceWinfo', sender=self, data=data) def _NH_PresenceWinfoSubscriptionDidEnd(self, notification): self._pwi_version = None def _NH_PresenceWinfoSubscriptionDidFail(self, notification): self._pwi_version = None def _NH_DialogWinfoSubscriptionGotNotify(self, notification): if notification.data.body and notification.data.content_type == WatcherInfoDocument.content_type: try: watcher_info = WatcherInfoDocument.parse(notification.data.body) watcher_list = watcher_info['sip:' + self.id] except (ParserError, KeyError): pass else: if watcher_list.package != 'dialog': return if self._dwi_version is None: if watcher_info.state == 'partial': self._dwi_subscriber.resubscribe() elif watcher_info.version <= self._dwi_version: return elif watcher_info.state == 'partial' and watcher_info.version > self._dwi_version + 1: self._dwi_subscriber.resubscribe() self._dwi_version = watcher_info.version data = NotificationData(version=watcher_info.version, state=watcher_info.state, watcher_list=watcher_list) notification.center.post_notification('SIPAccountGotDialogWinfo', sender=self, data=data) def _NH_DialogWinfoSubscriptionDidEnd(self, notification): self._dwi_version = None def _NH_DialogWinfoSubscriptionDidFail(self, notification): self._dwi_version = None def _NH_PresenceSubscriptionGotNotify(self, notification): if notification.data.body and notification.data.content_type == RLSNotify.content_type: try: rls_notify = RLSNotify.parse('{content_type}\r\n\r\n{body}'.format(content_type=notification.data.headers['Content-Type'], body=notification.data.body)) except ParserError: pass else: if rls_notify.uri != self.xcap_manager.rls_presence_uri: return if self._presence_version is None: if not rls_notify.full_state: self._presence_subscriber.resubscribe() elif rls_notify.version <= self._presence_version: return elif not rls_notify.full_state and rls_notify.version > self._presence_version + 1: self._presence_subscriber.resubscribe() self._presence_version = rls_notify.version data = NotificationData(version=rls_notify.version, full_state=rls_notify.full_state, resource_map=dict((resource.uri, resource) for resource in rls_notify)) notification.center.post_notification('SIPAccountGotPresenceState', sender=self, data=data) def _NH_PresenceSubscriptionDidEnd(self, notification): self._presence_version = None def _NH_PresenceSubscriptionDidFail(self, notification): self._presence_version = None def _NH_SelfPresenceSubscriptionGotNotify(self, notification): if notification.data.body and notification.data.content_type == PIDFDocument.content_type: try: pidf_doc = PIDFDocument.parse(notification.data.body) except ParserError: pass else: if pidf_doc.entity.partition('sip:')[2] != self.id: return notification.center.post_notification('SIPAccountGotSelfPresenceState', sender=self, data=NotificationData(pidf=pidf_doc)) def _NH_DialogSubscriptionGotNotify(self, notification): if notification.data.body and notification.data.content_type == RLSNotify.content_type: try: rls_notify = RLSNotify.parse('{content_type}\r\n\r\n{body}'.format(content_type=notification.data.headers['Content-Type'], body=notification.data.body)) except ParserError: pass else: if rls_notify.uri != self.xcap_manager.rls_dialog_uri: return if self._dialog_version is None: if not rls_notify.full_state: self._dialog_subscriber.resubscribe() elif rls_notify.version <= self._dialog_version: return elif not rls_notify.full_state and rls_notify.version > self._dialog_version + 1: self._dialog_subscriber.resubscribe() self._dialog_version = rls_notify.version data = NotificationData(version=rls_notify.version, full_state=rls_notify.full_state, resource_map=dict((resource.uri, resource) for resource in rls_notify)) notification.center.post_notification('SIPAccountGotDialogState', sender=self, data=data) def _NH_DialogSubscriptionDidEnd(self, notification): self._dialog_version = None def _NH_DialogSubscriptionDidFail(self, notification): self._dialog_version = None def _activate(self): with self._activation_lock: if self._active: return notification_center = NotificationCenter() notification_center.post_notification('SIPAccountWillActivate', sender=self) self._active = True self._registrar.start() self._mwi_subscriber.start() self._pwi_subscriber.start() self._dwi_subscriber.start() self._presence_subscriber.start() self._self_presence_subscriber.start() self._dialog_subscriber.start() self._presence_publisher.start() self._dialog_publisher.start() if self.xcap.enabled: self.xcap_manager.start() notification_center.post_notification('SIPAccountDidActivate', sender=self) def _deactivate(self): with self._activation_lock: if not self._active: return notification_center = NotificationCenter() notification_center.post_notification('SIPAccountWillDeactivate', sender=self) self._active = False handlers = [self._registrar, self._mwi_subscriber, self._pwi_subscriber, self._dwi_subscriber, self._presence_subscriber, self._self_presence_subscriber, self._dialog_subscriber, self._presence_publisher, self._dialog_publisher, self.xcap_manager] proc.waitall([proc.spawn(handler.stop) for handler in handlers]) notification_center.post_notification('SIPAccountDidDeactivate', sender=self) def __repr__(self): return '%s(%r)' % (self.__class__.__name__, self.id) def __setstate__(self, data): # This restores the password from its previous location as a top level setting # after it was moved under the auth group. SettingsObject.__setstate__(self, data) if not data.get('auth', {}).get('password') and data.get('password'): self.auth.password = data.pop('password') self.save() class BonjourMSRPSettings(SettingsGroup): transport = Setting(type=MSRPTransport, default='tcp') class BonjourAccountEnabledSetting(Setting): def __get__(self, obj, objtype): if obj is None: return self return _bonjour.available and self.values.get(obj, self.default) def __set__(self, obj, value): if not _bonjour.available: raise RuntimeError('mdns support is not available') Setting.__set__(self, obj, value) class BonjourAccount(SettingsObject): """ Object representing a bonjour account. Contains configuration settings and attributes for accessing bonjour related options. When the account is active, it will send broadcast its contact address on the LAN. If the object is un-pickled and its enabled flag was set, it will automatically activate. When the save method is called, depending on the value of the enabled flag, the account will activate/deactivate. Notifications sent by instances of Account: * CFGSettingsObjectWasCreated * CFGSettingsObjectWasActivated * CFGSettingsObjectWasDeleted * CFGSettingsObjectDidChange * SIPAccountWillActivate * SIPAccountDidActivate * SIPAccountWillDeactivate * SIPAccountDidDeactivate """ implements(IObserver) __group__ = 'Accounts' __id__ = SIPAddress('bonjour@local') id = property(lambda self: self.__id__) enabled = BonjourAccountEnabledSetting(type=bool, default=True) - display_name = Setting(type=unicode, default=user_info.fullname, nillable=False) + display_name = Setting(type=str, default=user_info.fullname, nillable=False) msrp = BonjourMSRPSettings presence = PresenceSettings rtp = RTPSettings tls = TLSSettings def __new__(cls): with AccountManager.load.lock: if not AccountManager.load.called: raise RuntimeError("cannot instantiate %s before calling AccountManager.load" % cls.__name__) return SettingsObject.__new__(cls) def __init__(self): self.contact = ContactURIFactory() self.credentials = None self._started = False self._active = False self._activation_lock = coros.Semaphore(1) self._bonjour_services = BonjourServices(self) # initialize fake settings (these are here to make the bonjour account quack like a duck) self.nat_traversal = NATTraversalSettings() self.nat_traversal.use_ice = False self.nat_traversal.msrp_relay = None self.nat_traversal.use_msrp_relay_for_outbound = False self.xcap = XCAPSettings() self.xcap.enabled = False self.xcap.discovered = False self.xcap.xcap_root = None def __repr__(self): return '%s()' % self.__class__.__name__ def start(self): if self._started: return self._started = True notification_center = NotificationCenter() notification_center.add_observer(self, name='CFGSettingsObjectDidChange', sender=self) notification_center.add_observer(self, name='CFGSettingsObjectDidChange', sender=SIPSimpleSettings()) self._bonjour_services.start() if self.enabled: self._activate() def stop(self): if not self._started: return self._started = False self._deactivate() self._bonjour_services.stop() notification_center = NotificationCenter() notification_center.remove_observer(self, name='CFGSettingsObjectDidChange', sender=self) notification_center.remove_observer(self, name='CFGSettingsObjectDidChange', sender=SIPSimpleSettings()) @classproperty def mdns_available(cls): return _bonjour.available @property def registered(self): return False @property def tls_credentials(self): # This property can be optimized to cache the credentials it loads from disk, # however this is not a time consuming operation (~ 3000 req/sec). -Luci settings = SIPSimpleSettings() if self.tls.certificate is not None: certificate_data = open(self.tls.certificate.normalized).read() certificate = X509Certificate(certificate_data) private_key = X509PrivateKey(certificate_data) else: certificate = None private_key = None if settings.tls.ca_list is not None: # we should read all certificates in the file, rather than just the first -Luci trusted = [X509Certificate(open(settings.tls.ca_list.normalized).read())] else: trusted = [] credentials = X509Credentials(certificate, private_key, trusted) credentials.verify_peer = self.tls.verify_server return credentials @property def uri(self): return SIPURI(user=self.contact.username, host=Host.default_ip or '127.0.0.1') @property def presence_state(self): return self._bonjour_services.presence_state @presence_state.setter def presence_state(self, state): self._bonjour_services.presence_state = state def handle_notification(self, notification): handler = getattr(self, '_NH_%s' % notification.name, Null) handler(notification) @run_in_green_thread def _NH_CFGSettingsObjectDidChange(self, notification): if self._started: if 'enabled' in notification.data.modified: if self.enabled: self._activate() else: self._deactivate() elif self.enabled: if 'display_name' in notification.data.modified: self._bonjour_services.update_registrations() if {'sip.transport_list', 'tls.certificate'}.intersection(notification.data.modified): self._bonjour_services.update_registrations() self._bonjour_services.restart_discovery() def _activate(self): with self._activation_lock: if self._active: return notification_center = NotificationCenter() notification_center.post_notification('SIPAccountWillActivate', sender=self) self._active = True self._bonjour_services.activate() notification_center.post_notification('SIPAccountDidActivate', sender=self) def _deactivate(self): with self._activation_lock: if not self._active: return notification_center = NotificationCenter() notification_center.post_notification('SIPAccountWillDeactivate', sender=self) self._active = False self._bonjour_services.deactivate() notification_center.post_notification('SIPAccountDidDeactivate', sender=self) -class AccountManager(object): +class AccountManager(object, metaclass=Singleton): """ This is a singleton object which manages all the SIP accounts. It is also used to manage the default account (the one used for outbound sessions) using the default_account attribute: manager = AccountManager() manager.default_account = manager.get_account('alice@example.net') The following notifications are sent: * SIPAccountManagerDidRemoveAccount * SIPAccountManagerDidAddAccount * SIPAccountManagerDidChangeDefaultAccount """ - __metaclass__ = Singleton - implements(IObserver) def __init__(self): self._lock = Lock() self.accounts = {} notification_center = NotificationCenter() notification_center.add_observer(self, name='CFGSettingsObjectWasActivated') notification_center.add_observer(self, name='CFGSettingsObjectWasCreated') @execute_once def load(self): """ Load all accounts from the configuration. The accounts will not be started until the start method is called. """ configuration = ConfigurationManager() bonjour_account = BonjourAccount() names = configuration.get_names([Account.__group__]) [Account(id) for id in names if id != bonjour_account.id] default_account = self.default_account if default_account is None or not default_account.enabled: try: - self.default_account = (account for account in self.accounts.itervalues() if account.enabled).next() + self.default_account = next((account for account in list(self.accounts.values()) if account.enabled)) except StopIteration: self.default_account = None def start(self): """ Start the accounts, which will determine the ones with the enabled flag set to activate. """ notification_center = NotificationCenter() notification_center.post_notification('SIPAccountManagerWillStart', sender=self) - proc.waitall([proc.spawn(account.start) for account in self.accounts.itervalues()]) + proc.waitall([proc.spawn(account.start) for account in list(self.accounts.values())]) notification_center.post_notification('SIPAccountManagerDidStart', sender=self) def stop(self): """ Stop the accounts, which will determine the ones that were enabled to deactivate. This method returns only once the accounts were stopped successfully or they timed out trying. """ notification_center = NotificationCenter() notification_center.post_notification('SIPAccountManagerWillEnd', sender=self) - proc.waitall([proc.spawn(account.stop) for account in self.accounts.itervalues()]) + proc.waitall([proc.spawn(account.stop) for account in list(self.accounts.values())]) notification_center.post_notification('SIPAccountManagerDidEnd', sender=self) def has_account(self, id): return id in self.accounts def get_account(self, id): return self.accounts[id] def get_accounts(self): - return self.accounts.values() + return list(self.accounts.values()) def iter_accounts(self): - return self.accounts.itervalues() + return iter(list(self.accounts.values())) def find_account(self, contact_uri): # compare contact_address with account contact - exact_matches = (account for account in self.accounts.itervalues() if account.enabled and account.contact.username==contact_uri.user) + exact_matches = (account for account in list(self.accounts.values()) if account.enabled and account.contact.username==contact_uri.user) # compare username in contact URI with account username - loose_matches = (account for account in self.accounts.itervalues() if account.enabled and account.id.username==contact_uri.user) - return chain(exact_matches, loose_matches, [None]).next() + loose_matches = (account for account in list(self.accounts.values()) if account.enabled and account.id.username==contact_uri.user) + return next(chain(exact_matches, loose_matches, [None])) def handle_notification(self, notification): handler = getattr(self, '_NH_%s' % notification.name, Null) handler(notification) def _NH_CFGSettingsObjectWasActivated(self, notification): if isinstance(notification.sender, Account) or (isinstance(notification.sender, BonjourAccount) and _bonjour.available): account = notification.sender self.accounts[account.id] = account notification.center.add_observer(self, sender=account, name='CFGSettingsObjectDidChange') notification.center.add_observer(self, sender=account, name='CFGSettingsObjectWasDeleted') notification.center.post_notification('SIPAccountManagerDidAddAccount', sender=self, data=NotificationData(account=account)) from sipsimple.application import SIPApplication if SIPApplication.running: call_in_green_thread(account.start) def _NH_CFGSettingsObjectWasCreated(self, notification): if isinstance(notification.sender, Account): account = notification.sender if account.enabled and self.default_account is None: self.default_account = account def _NH_CFGSettingsObjectWasDeleted(self, notification): account = notification.sender del self.accounts[account.id] notification.center.remove_observer(self, sender=account, name='CFGSettingsObjectDidChange') notification.center.remove_observer(self, sender=account, name='CFGSettingsObjectWasDeleted') notification.center.post_notification('SIPAccountManagerDidRemoveAccount', sender=self, data=NotificationData(account=account)) def _NH_CFGSettingsObjectDidChange(self, notification): account = notification.sender if '__id__' in notification.data.modified: modified_id = notification.data.modified['__id__'] self.accounts[modified_id.new] = self.accounts.pop(modified_id.old) if 'enabled' in notification.data.modified: if account.enabled and self.default_account is None: self.default_account = account elif not account.enabled and self.default_account is account: try: - self.default_account = (account for account in self.accounts.itervalues() if account.enabled).next() + self.default_account = next((account for account in list(self.accounts.values()) if account.enabled)) except StopIteration: self.default_account = None @property def default_account(self): settings = SIPSimpleSettings() return self.accounts.get(settings.default_account, None) @default_account.setter def default_account(self, account): if account is not None and not account.enabled: raise ValueError("account %s is not enabled" % account.id) notification_center = NotificationCenter() settings = SIPSimpleSettings() with self._lock: old_account = self.accounts.get(settings.default_account, None) if account is old_account: return if account is None: settings.default_account = None else: settings.default_account = account.id settings.save() # we need to post the notification in the file-io thread in order to have it serialized after the # SIPAccountManagerDidAddAccount notification that is triggered when the account is saved the first # time, because save is executed in the file-io thread while this runs in the current thread. -Dan call_in_thread('file-io', notification_center.post_notification, 'SIPAccountManagerDidChangeDefaultAccount', sender=self, data=NotificationData(old_account=old_account, account=account)) diff --git a/sipsimple/account/bonjour/__init__.py b/sipsimple/account/bonjour/__init__.py index 18aead87..c5d09465 100644 --- a/sipsimple/account/bonjour/__init__.py +++ b/sipsimple/account/bonjour/__init__.py @@ -1,488 +1,488 @@ """Implements Bonjour service handlers""" __all__ = ['BonjourServices'] import re import uuid from threading import Lock from weakref import WeakKeyDictionary from application.notification import IObserver, NotificationCenter, NotificationData from application.python import Null from eventlib import api, coros, proc from eventlib.green import select from twisted.internet import reactor from zope.interface import implements from sipsimple import log from sipsimple.account.bonjour import _bonjour from sipsimple.core import FrozenSIPURI, SIPCoreError, NoGRUU from sipsimple.configuration.settings import SIPSimpleSettings from sipsimple.threading import call_in_twisted_thread, run_in_twisted_thread from sipsimple.threading.green import Command class RestartSelect(Exception): pass class BonjourFile(object): __instances__ = WeakKeyDictionary() def __new__(cls, file): if cls is BonjourFile: raise TypeError("BonjourFile cannot be instantiated directly") instance = cls.__instances__.get(file) if instance is None: instance = object.__new__(cls) instance.file = file instance.active = False cls.__instances__[file] = instance return instance def fileno(self): return self.file.fileno() if not self.closed else -1 def close(self): self.file.close() self.file = None @property def closed(self): return self.file is None @classmethod def find_by_file(cls, file): """Return the instance matching the given DNSServiceRef file""" try: return cls.__instances__[file] except KeyError: raise KeyError("cannot find a %s matching the given DNSServiceRef file" % cls.__name__) class BonjourDiscoveryFile(BonjourFile): def __new__(cls, file, transport): instance = BonjourFile.__new__(cls, file) instance.transport = transport return instance class BonjourRegistrationFile(BonjourFile): def __new__(cls, file, transport): instance = BonjourFile.__new__(cls, file) instance.transport = transport return instance class BonjourResolutionFile(BonjourFile): def __new__(cls, file, discovery_file, service_description): instance = BonjourFile.__new__(cls, file) instance.discovery_file = discovery_file instance.service_description = service_description return instance @property def transport(self): return self.discovery_file.transport class BonjourServiceDescription(object): def __init__(self, name, type, domain): self.name = name self.type = type self.domain = domain def __repr__(self): return "%s(%r, %r, %r)" % (self.__class__.__name__, self.name, self.type, self.domain) def __hash__(self): return hash(self.name) def __eq__(self, other): if isinstance(other, BonjourServiceDescription): return self.name==other.name and self.type==other.type and self.domain==other.domain return NotImplemented def __ne__(self, other): equal = self.__eq__(other) return NotImplemented if equal is NotImplemented else not equal class BonjourNeighbourPresence(object): def __init__(self, state, note): self.state = state self.note = note class BonjourNeighbourRecord(object): def __init__(self, service_description, host, txtrecord): self.id = txtrecord.get('instance_id', None) self.name = txtrecord.get('name', '').decode('utf-8') or None self.host = re.match(r'^(?P.*?)(\.local)?\.?$', host).group('host') self.uri = FrozenSIPURI.parse(txtrecord.get('contact', service_description.name)) self.presence = BonjourNeighbourPresence(txtrecord.get('state', txtrecord.get('status', None)), txtrecord.get('note', '').decode('utf-8') or None) # status is read for legacy (remove later) -Dan class BonjourServices(object): implements(IObserver) def __init__(self, account): self.account = account self._started = False self._files = [] self._neighbours = {} self._command_channel = coros.queue() self._select_proc = None self._discover_timer = None self._register_timer = None self._update_timer = None self._lock = Lock() self.__dict__['presence_state'] = None def start(self): notification_center = NotificationCenter() notification_center.add_observer(self, name='NetworkConditionsDidChange') self._select_proc = proc.spawn(self._process_files) proc.spawn(self._handle_commands) def stop(self): notification_center = NotificationCenter() notification_center.remove_observer(self, name='NetworkConditionsDidChange') self._select_proc.kill() self._command_channel.send_exception(api.GreenletExit) def activate(self): self._started = True self._command_channel.send(Command('register')) self._command_channel.send(Command('discover')) def deactivate(self): command = Command('stop') self._command_channel.send(command) command.wait() self._started = False def restart_discovery(self): self._command_channel.send(Command('discover')) def restart_registration(self): self._command_channel.send(Command('unregister')) self._command_channel.send(Command('register')) def update_registrations(self): self._command_channel.send(Command('update_registrations')) @property def presence_state(self): return self.__dict__['presence_state'] @presence_state.setter def presence_state(self, state): if state is not None and not isinstance(state, BonjourPresenceState): raise ValueError("state must be a BonjourPresenceState instance or None") with self._lock: old_state = self.__dict__['presence_state'] self.__dict__['presence_state'] = state if state != old_state: call_in_twisted_thread(self.update_registrations) def _register_cb(self, file, flags, error_code, name, regtype, domain): notification_center = NotificationCenter() file = BonjourRegistrationFile.find_by_file(file) if error_code == _bonjour.kDNSServiceErr_NoError: notification_center.post_notification('BonjourAccountRegistrationDidSucceed', sender=self.account, data=NotificationData(name=name, transport=file.transport)) else: error = _bonjour.BonjourError(error_code) notification_center.post_notification('BonjourAccountRegistrationDidFail', sender=self.account, data=NotificationData(reason=str(error), transport=file.transport)) self._files.remove(file) self._select_proc.kill(RestartSelect) file.close() if self._register_timer is None: self._register_timer = reactor.callLater(1, self._command_channel.send, Command('register')) def _browse_cb(self, file, flags, interface_index, error_code, service_name, regtype, reply_domain): notification_center = NotificationCenter() file = BonjourDiscoveryFile.find_by_file(file) service_description = BonjourServiceDescription(service_name, regtype, reply_domain) if error_code != _bonjour.kDNSServiceErr_NoError: error = _bonjour.BonjourError(error_code) notification_center.post_notification('BonjourAccountDiscoveryDidFail', sender=self.account, data=NotificationData(reason=str(error), transport=file.transport)) removed_files = [file] + [f for f in self._files if isinstance(f, BonjourResolutionFile) and f.discovery_file==file] for f in removed_files: self._files.remove(f) self._select_proc.kill(RestartSelect) for f in removed_files: f.close() if self._discover_timer is None: self._discover_timer = reactor.callLater(1, self._command_channel.send, Command('discover')) return if reply_domain != 'local.': return if flags & _bonjour.kDNSServiceFlagsAdd: try: - resolution_file = (f for f in self._files if isinstance(f, BonjourResolutionFile) and f.discovery_file==file and f.service_description==service_description).next() + resolution_file = next((f for f in self._files if isinstance(f, BonjourResolutionFile) and f.discovery_file==file and f.service_description==service_description)) except StopIteration: try: resolution_file = _bonjour.DNSServiceResolve(0, interface_index, service_name, regtype, reply_domain, self._resolve_cb) - except _bonjour.BonjourError, e: + except _bonjour.BonjourError as e: notification_center.post_notification('BonjourAccountDiscoveryFailure', sender=self.account, data=NotificationData(error=str(e), transport=file.transport)) else: resolution_file = BonjourResolutionFile(resolution_file, discovery_file=file, service_description=service_description) self._files.append(resolution_file) self._select_proc.kill(RestartSelect) else: try: - resolution_file = (f for f in self._files if isinstance(f, BonjourResolutionFile) and f.discovery_file==file and f.service_description==service_description).next() + resolution_file = next((f for f in self._files if isinstance(f, BonjourResolutionFile) and f.discovery_file==file and f.service_description==service_description)) except StopIteration: pass else: self._files.remove(resolution_file) self._select_proc.kill(RestartSelect) resolution_file.close() service_description = resolution_file.service_description if service_description in self._neighbours: record = self._neighbours.pop(service_description) notification_center.post_notification('BonjourAccountDidRemoveNeighbour', sender=self.account, data=NotificationData(neighbour=service_description, record=record)) def _resolve_cb(self, file, flags, interface_index, error_code, fullname, host_target, port, txtrecord): notification_center = NotificationCenter() settings = SIPSimpleSettings() file = BonjourResolutionFile.find_by_file(file) if error_code == _bonjour.kDNSServiceErr_NoError: service_description = file.service_description try: record = BonjourNeighbourRecord(service_description, host_target, _bonjour.TXTRecord.parse(txtrecord)) except SIPCoreError: pass else: transport = record.uri.transport supported_transport = transport in settings.sip.transport_list and (transport!='tls' or self.account.tls.certificate is not None) if not supported_transport and service_description in self._neighbours: record = self._neighbours.pop(service_description) notification_center.post_notification('BonjourAccountDidRemoveNeighbour', sender=self.account, data=NotificationData(neighbour=service_description, record=record)) elif supported_transport: try: our_contact_uri = self.account.contact[NoGRUU, transport] except KeyError: return if record.uri != our_contact_uri: had_neighbour = service_description in self._neighbours self._neighbours[service_description] = record notification_name = 'BonjourAccountDidUpdateNeighbour' if had_neighbour else 'BonjourAccountDidAddNeighbour' notification_data = NotificationData(neighbour=service_description, record=record) notification_center.post_notification(notification_name, sender=self.account, data=notification_data) else: self._files.remove(file) self._select_proc.kill(RestartSelect) file.close() error = _bonjour.BonjourError(error_code) notification_center.post_notification('BonjourAccountDiscoveryFailure', sender=self.account, data=NotificationData(error=str(error), transport=file.transport)) # start a new resolve process here? -Dan def _process_files(self): while True: try: ready = select.select([f for f in self._files if not f.active and not f.closed], [], [])[0] except RestartSelect: continue else: for file in ready: file.active = True self._command_channel.send(Command('process_results', files=[f for f in ready if not f.closed])) def _handle_commands(self): while True: command = self._command_channel.wait() if self._started: handler = getattr(self, '_CH_%s' % command.name) handler(command) def _CH_unregister(self, command): if self._register_timer is not None and self._register_timer.active(): self._register_timer.cancel() self._register_timer = None if self._update_timer is not None and self._update_timer.active(): self._update_timer.cancel() self._update_timer = None old_files = [] for file in (f for f in self._files[:] if isinstance(f, BonjourRegistrationFile)): old_files.append(file) self._files.remove(file) self._select_proc.kill(RestartSelect) for file in old_files: file.close() notification_center = NotificationCenter() for transport in set(file.transport for file in self._files): notification_center.post_notification('BonjourAccountRegistrationDidEnd', sender=self.account, data=NotificationData(transport=transport)) command.signal() def _CH_register(self, command): notification_center = NotificationCenter() settings = SIPSimpleSettings() if self._register_timer is not None and self._register_timer.active(): self._register_timer.cancel() self._register_timer = None supported_transports = set(transport for transport in settings.sip.transport_list if transport!='tls' or self.account.tls.certificate is not None) registered_transports = set(file.transport for file in self._files if isinstance(file, BonjourRegistrationFile)) missing_transports = supported_transports - registered_transports added_transports = set() for transport in missing_transports: notification_center.post_notification('BonjourAccountWillRegister', sender=self.account, data=NotificationData(transport=transport)) try: contact = self.account.contact[NoGRUU, transport] instance_id = str(uuid.UUID(settings.instance_id)) txtdata = dict(txtvers=1, name=self.account.display_name.encode('utf-8'), contact="<%s>" % str(contact), instance_id=instance_id) state = self.account.presence_state if self.account.presence.enabled and state is not None: txtdata['state'] = state.state txtdata['note'] = state.note.encode('utf-8') file = _bonjour.DNSServiceRegister(name=str(contact), regtype="_sipuri._%s" % (transport if transport == 'udp' else 'tcp'), port=contact.port, callBack=self._register_cb, txtRecord=_bonjour.TXTRecord(items=txtdata)) - except (_bonjour.BonjourError, KeyError), e: + except (_bonjour.BonjourError, KeyError) as e: notification_center.post_notification('BonjourAccountRegistrationDidFail', sender=self.account, data=NotificationData(reason=str(e), transport=transport)) else: self._files.append(BonjourRegistrationFile(file, transport)) added_transports.add(transport) if added_transports: self._select_proc.kill(RestartSelect) if added_transports != missing_transports: self._register_timer = reactor.callLater(1, self._command_channel.send, Command('register', command.event)) else: command.signal() def _CH_update_registrations(self, command): notification_center = NotificationCenter() settings = SIPSimpleSettings() if self._update_timer is not None and self._update_timer.active(): self._update_timer.cancel() self._update_timer = None available_transports = settings.sip.transport_list old_files = [] for file in (f for f in self._files[:] if isinstance(f, BonjourRegistrationFile) and f.transport not in available_transports): old_files.append(file) self._files.remove(file) self._select_proc.kill(RestartSelect) for file in old_files: file.close() update_failure = False for file in (f for f in self._files if isinstance(f, BonjourRegistrationFile)): try: contact = self.account.contact[NoGRUU, file.transport] instance_id = str(uuid.UUID(settings.instance_id)) txtdata = dict(txtvers=1, name=self.account.display_name.encode('utf-8'), contact="<%s>" % str(contact), instance_id=instance_id) state = self.account.presence_state if self.account.presence.enabled and state is not None: txtdata['state'] = state.state txtdata['note'] = state.note.encode('utf-8') _bonjour.DNSServiceUpdateRecord(file.file, None, flags=0, rdata=_bonjour.TXTRecord(items=txtdata), ttl=0) - except (_bonjour.BonjourError, KeyError), e: + except (_bonjour.BonjourError, KeyError) as e: notification_center.post_notification('BonjourAccountRegistrationUpdateDidFail', sender=self.account, data=NotificationData(reason=str(e), transport=file.transport)) update_failure = True self._command_channel.send(Command('register')) if update_failure: self._update_timer = reactor.callLater(1, self._command_channel.send, Command('update_registrations', command.event)) else: command.signal() def _CH_discover(self, command): notification_center = NotificationCenter() settings = SIPSimpleSettings() if self._discover_timer is not None and self._discover_timer.active(): self._discover_timer.cancel() self._discover_timer = None supported_transports = set(transport for transport in settings.sip.transport_list if transport!='tls' or self.account.tls.certificate is not None) discoverable_transports = set('tcp' if transport=='tls' else transport for transport in supported_transports) old_files = [] for file in (f for f in self._files[:] if isinstance(f, (BonjourDiscoveryFile, BonjourResolutionFile)) and f.transport not in discoverable_transports): old_files.append(file) self._files.remove(file) self._select_proc.kill(RestartSelect) for file in old_files: file.close() - for service_description in [service for service, record in self._neighbours.iteritems() if record.uri.transport not in supported_transports]: + for service_description in [service for service, record in list(self._neighbours.items()) if record.uri.transport not in supported_transports]: record = self._neighbours.pop(service_description) notification_center.post_notification('BonjourAccountDidRemoveNeighbour', sender=self.account, data=NotificationData(neighbour=service_description, record=record)) discovered_transports = set(file.transport for file in self._files if isinstance(file, BonjourDiscoveryFile)) missing_transports = discoverable_transports - discovered_transports added_transports = set() for transport in missing_transports: notification_center.post_notification('BonjourAccountWillInitiateDiscovery', sender=self.account, data=NotificationData(transport=transport)) try: file = _bonjour.DNSServiceBrowse(regtype="_sipuri._%s" % transport, callBack=self._browse_cb) - except _bonjour.BonjourError, e: + except _bonjour.BonjourError as e: notification_center.post_notification('BonjourAccountDiscoveryDidFail', sender=self.account, data=NotificationData(reason=str(e), transport=transport)) else: self._files.append(BonjourDiscoveryFile(file, transport)) added_transports.add(transport) if added_transports: self._select_proc.kill(RestartSelect) if added_transports != missing_transports: self._discover_timer = reactor.callLater(1, self._command_channel.send, Command('discover', command.event)) else: command.signal() def _CH_process_results(self, command): for file in (f for f in command.files if not f.closed): try: _bonjour.DNSServiceProcessResult(file.file) except: # Should we close the file? The documentation doesn't say anything about this. -Luci log.exception() for file in command.files: file.active = False self._files = [f for f in self._files if not f.closed] self._select_proc.kill(RestartSelect) def _CH_stop(self, command): if self._discover_timer is not None and self._discover_timer.active(): self._discover_timer.cancel() self._discover_timer = None if self._register_timer is not None and self._register_timer.active(): self._register_timer.cancel() self._register_timer = None if self._update_timer is not None and self._update_timer.active(): self._update_timer.cancel() self._update_timer = None files = self._files neighbours = self._neighbours self._files = [] self._select_proc.kill(RestartSelect) self._neighbours = {} for file in files: file.close() notification_center = NotificationCenter() - for neighbour, record in neighbours.iteritems(): + for neighbour, record in list(neighbours.items()): notification_center.post_notification('BonjourAccountDidRemoveNeighbour', sender=self.account, data=NotificationData(neighbour=neighbour, record=record)) for transport in set(file.transport for file in files): notification_center.post_notification('BonjourAccountRegistrationDidEnd', sender=self.account, data=NotificationData(transport=transport)) command.signal() @run_in_twisted_thread def handle_notification(self, notification): handler = getattr(self, '_NH_%s' % notification.name, Null) handler(notification) def _NH_NetworkConditionsDidChange(self, notification): if self._files: self.restart_discovery() self.restart_registration() class BonjourPresenceState(object): def __init__(self, state, note=None): self.state = state - self.note = note or u'' + self.note = note or '' def __eq__(self, other): if isinstance(other, BonjourPresenceState): return self.state == other.state and self.note == other.note return NotImplemented def __ne__(self, other): equal = self.__eq__(other) return NotImplemented if equal is NotImplemented else not equal diff --git a/sipsimple/account/bonjour/_bonjour.py b/sipsimple/account/bonjour/_bonjour.py index 7ca40734..0582fc01 100644 --- a/sipsimple/account/bonjour/_bonjour.py +++ b/sipsimple/account/bonjour/_bonjour.py @@ -1,2109 +1,2109 @@ ################################################################################ # # Copyright (c) 2007-2008 Christopher J. Stawarz # # Permission is hereby granted, free of charge, to any person # obtaining a copy of this software and associated documentation files # (the "Software"), to deal in the Software without restriction, # including without limitation the rights to use, copy, modify, merge, # publish, distribute, sublicense, and/or sell copies of the Software, # and to permit persons to whom the Software is furnished to do so, # subject to the following conditions: # # The above copyright notice and this permission notice shall be # included in all copies or substantial portions of the Software. # # THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, # EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF # MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND # NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS # BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN # ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN # CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE # SOFTWARE. # ################################################################################ """ Pure-Python interface to Apple Bonjour and compatible DNS-SD libraries pybonjour provides a pure-Python interface (via ctypes) to Apple Bonjour and compatible DNS-SD libraries (such as Avahi). It allows Python scripts to take advantage of Zero Configuration Networking (Zeroconf) to register, discover, and resolve services on both local and wide-area networks. Since pybonjour is implemented in pure Python, scripts that use it can easily be ported to Mac OS X, Windows, Linux, and other systems that run Bonjour. Note on strings: Internally, all strings used in DNS-SD are UTF-8 strings. String arguments passed to the DNS-SD functions provided by pybonjour must be either unicode instances or str instances that can be converted to unicode using the default encoding. (Passing a non-convertible str will result in an exception.) Strings returned from pybonjour (either directly from API functions or passed to application callbacks) are always unicode instances. """ __author__ = 'Christopher Stawarz ' __version__ = '1.1.1' __revision__ = int('$Revision: 6125 $'.split()[1]) import ctypes import os import re import socket import sys from application.python import Null ################################################################################ # # Global setup # ################################################################################ class _DummyLock(object): @staticmethod def acquire(): pass @staticmethod def release(): pass _global_lock = _DummyLock() if sys.platform == 'win32': # Need to use the stdcall variants try: _libdnssd = ctypes.windll.dnssd except: _libdnssd = _CFunc = Null available = False else: _CFunc = ctypes.WINFUNCTYPE available = True else: if sys.platform == 'darwin': _libdnssd = 'libSystem.B.dylib' else: _libdnssd = 'libdns_sd.so.1' # If libdns_sd is actually Avahi's Bonjour compatibility # layer, silence its annoying warning messages, and use a real # RLock as the global lock, since the compatibility layer # isn't thread safe. try: ctypes.cdll.LoadLibrary('libavahi-client.so.3') except OSError: pass else: os.environ['AVAHI_COMPAT_NOWARN'] = '1' from eventlib.green.threading import RLock _global_lock = RLock() try: _libdnssd = ctypes.cdll.LoadLibrary(_libdnssd) except: _libdnssd = _CFunc = Null available = False else: _CFunc = ctypes.CFUNCTYPE available = True ################################################################################ # # Constants # ################################################################################ # # General flags # kDNSServiceFlagsMoreComing = 0x1 kDNSServiceFlagsAdd = 0x2 kDNSServiceFlagsDefault = 0x4 kDNSServiceFlagsNoAutoRename = 0x8 kDNSServiceFlagsShared = 0x10 kDNSServiceFlagsUnique = 0x20 kDNSServiceFlagsBrowseDomains = 0x40 kDNSServiceFlagsRegistrationDomains = 0x80 kDNSServiceFlagsLongLivedQuery = 0x100 kDNSServiceFlagsAllowRemoteQuery = 0x200 kDNSServiceFlagsForceMulticast = 0x400 kDNSServiceFlagsReturnCNAME = 0x800 # # Service classes # kDNSServiceClass_IN = 1 # # Service types # kDNSServiceType_A = 1 kDNSServiceType_NS = 2 kDNSServiceType_MD = 3 kDNSServiceType_MF = 4 kDNSServiceType_CNAME = 5 kDNSServiceType_SOA = 6 kDNSServiceType_MB = 7 kDNSServiceType_MG = 8 kDNSServiceType_MR = 9 kDNSServiceType_NULL = 10 kDNSServiceType_WKS = 11 kDNSServiceType_PTR = 12 kDNSServiceType_HINFO = 13 kDNSServiceType_MINFO = 14 kDNSServiceType_MX = 15 kDNSServiceType_TXT = 16 kDNSServiceType_RP = 17 kDNSServiceType_AFSDB = 18 kDNSServiceType_X25 = 19 kDNSServiceType_ISDN = 20 kDNSServiceType_RT = 21 kDNSServiceType_NSAP = 22 kDNSServiceType_NSAP_PTR = 23 kDNSServiceType_SIG = 24 kDNSServiceType_KEY = 25 kDNSServiceType_PX = 26 kDNSServiceType_GPOS = 27 kDNSServiceType_AAAA = 28 kDNSServiceType_LOC = 29 kDNSServiceType_NXT = 30 kDNSServiceType_EID = 31 kDNSServiceType_NIMLOC = 32 kDNSServiceType_SRV = 33 kDNSServiceType_ATMA = 34 kDNSServiceType_NAPTR = 35 kDNSServiceType_KX = 36 kDNSServiceType_CERT = 37 kDNSServiceType_A6 = 38 kDNSServiceType_DNAME = 39 kDNSServiceType_SINK = 40 kDNSServiceType_OPT = 41 kDNSServiceType_TKEY = 249 kDNSServiceType_TSIG = 250 kDNSServiceType_IXFR = 251 kDNSServiceType_AXFR = 252 kDNSServiceType_MAILB = 253 kDNSServiceType_MAILA = 254 kDNSServiceType_ANY = 255 # # Error codes # kDNSServiceErr_NoError = 0 kDNSServiceErr_Unknown = -65537 kDNSServiceErr_NoSuchName = -65538 kDNSServiceErr_NoMemory = -65539 kDNSServiceErr_BadParam = -65540 kDNSServiceErr_BadReference = -65541 kDNSServiceErr_BadState = -65542 kDNSServiceErr_BadFlags = -65543 kDNSServiceErr_Unsupported = -65544 kDNSServiceErr_NotInitialized = -65545 kDNSServiceErr_AlreadyRegistered = -65547 kDNSServiceErr_NameConflict = -65548 kDNSServiceErr_Invalid = -65549 kDNSServiceErr_Firewall = -65550 kDNSServiceErr_Incompatible = -65551 kDNSServiceErr_BadInterfaceIndex = -65552 kDNSServiceErr_Refused = -65553 kDNSServiceErr_NoSuchRecord = -65554 kDNSServiceErr_NoAuth = -65555 kDNSServiceErr_NoSuchKey = -65556 kDNSServiceErr_NATTraversal = -65557 kDNSServiceErr_DoubleNAT = -65558 kDNSServiceErr_BadTime = -65559 # # Other constants # kDNSServiceMaxServiceName = 64 kDNSServiceMaxDomainName = 1005 kDNSServiceInterfaceIndexAny = 0 kDNSServiceInterfaceIndexLocalOnly = -1 ################################################################################ # # Error handling # ################################################################################ class BonjourError(Exception): """ Exception representing an error returned by the DNS-SD library. The errorCode attribute contains the actual integer error code returned. """ _errmsg = { kDNSServiceErr_NoSuchName: 'no such name', kDNSServiceErr_NoMemory: 'no memory', kDNSServiceErr_BadParam: 'bad param', kDNSServiceErr_BadReference: 'bad reference', kDNSServiceErr_BadState: 'bad state', kDNSServiceErr_BadFlags: 'bad flags', kDNSServiceErr_Unsupported: 'unsupported', kDNSServiceErr_NotInitialized: 'not initialized', kDNSServiceErr_AlreadyRegistered: 'already registered', kDNSServiceErr_NameConflict: 'name conflict', kDNSServiceErr_Invalid: 'invalid', kDNSServiceErr_Firewall: 'firewall', kDNSServiceErr_Incompatible: 'incompatible', kDNSServiceErr_BadInterfaceIndex: 'bad interface index', kDNSServiceErr_Refused: 'refused', kDNSServiceErr_NoSuchRecord: 'no such record', kDNSServiceErr_NoAuth: 'no auth', kDNSServiceErr_NoSuchKey: 'no such key', kDNSServiceErr_NATTraversal: 'NAT traversal', kDNSServiceErr_DoubleNAT: 'double NAT', kDNSServiceErr_BadTime: 'bad time', } @classmethod def _errcheck(cls, result, func, args): if result != kDNSServiceErr_NoError: raise cls(result) return args def __init__(self, errorCode): self.errorCode = errorCode Exception.__init__(self, (errorCode, self._errmsg.get(errorCode, 'unknown'))) ################################################################################ # # Data types # ################################################################################ class _utf8_char_p(ctypes.c_char_p): @classmethod def from_param(cls, obj): if (obj is not None) and (not isinstance(obj, cls)): - if not isinstance(obj, basestring): + if not isinstance(obj, str): raise TypeError('parameter must be a string type instance') - if not isinstance(obj, unicode): - obj = unicode(obj) + if not isinstance(obj, str): + obj = str(obj) obj = obj.encode('utf-8') return ctypes.c_char_p.from_param(obj) def decode(self): if self.value is None: return None return self.value.decode('utf-8') class _utf8_char_p_non_null(_utf8_char_p): @classmethod def from_param(cls, obj): if obj is None: raise ValueError('parameter cannot be None') return _utf8_char_p.from_param(obj) _DNSServiceFlags = ctypes.c_uint32 _DNSServiceErrorType = ctypes.c_int32 class DNSRecordRef(ctypes.c_void_p): """ A DNSRecordRef pointer. DO NOT CREATE INSTANCES OF THIS CLASS! Only instances returned by the DNS-SD library are valid. Using others will likely cause the Python interpreter to crash. Application code should not use any of the methods of this class. The only valid use of a DNSRecordRef instance is as an argument to a DNS-SD function. To compare two DNSRecordRef instances for equality, use '==' rather than 'is'. """ @classmethod def from_param(cls, obj): if type(obj) is not cls: raise TypeError("expected '%s', got '%s'" % (cls.__name__, type(obj).__name__)) if obj.value is None: raise ValueError('invalid %s instance' % cls.__name__) return obj def __eq__(self, other): return ((type(other) is type(self)) and (other.value == self.value)) def __ne__(self, other): return not (other == self) def __hash__(self): return hash(self.value) def _invalidate(self): self.value = None def _valid(self): return (self.value is not None) class _DNSRecordRef_or_null(DNSRecordRef): @classmethod def from_param(cls, obj): if obj is None: return obj return DNSRecordRef.from_param(obj) class DNSServiceRef(DNSRecordRef): """ A DNSServiceRef pointer. DO NOT CREATE INSTANCES OF THIS CLASS! Only instances returned by the DNS-SD library are valid. Using others will likely cause the Python interpreter to crash. An instance of this class represents an active connection to the mDNS daemon. The connection remains open until the close() method is called (which also terminates the associated browse, resolve, etc.). Note that this method is *not* called automatically when the instance is deallocated; therefore, application code must be certain to call close() when the connection is no longer needed. The primary use of a DNSServiceRef instance is in conjunction with select() or poll() to determine when a response from the daemon is available. When the file descriptor returned by fileno() is ready for reading, a reply from the daemon is available and should be processed by passing the DNSServiceRef instance to DNSServiceProcessResult(), which will invoke the appropriate application callback function. (Note that the file descriptor should never be read from or written to directly.) The DNSServiceRef class supports the context management protocol introduced in Python 2.5, meaning applications can use the 'with' statement to ensure that DNSServiceRef instances are closed regardless of whether an exception occurs, e.g. sdRef = DNSServiceBrowse(...) with sdRef: # sdRef will be closed regardless of how this block is # exited ... To compare two DNSServiceRef instances for equality, use '==' rather than 'is'. """ def __init__(self, *args, **kwargs): DNSRecordRef.__init__(self, *args, **kwargs) # Since callback functions are called asynchronously, we need # to hold onto references to them for as long as they're in # use. Otherwise, Python could deallocate them before we call # DNSServiceProcessResult(), meaning the Bonjour library would # dereference freed memory when it tried to invoke the # callback. self._callbacks = [] # A DNSRecordRef is invalidated if DNSServiceRefDeallocate() # is called on the corresponding DNSServiceRef, so we need to # keep track of all our record refs and invalidate them when # we're closed. self._record_refs = [] def __enter__(self): return self def __exit__(self, type, value, traceback): self.close() def _add_callback(self, cb): self._callbacks.append(cb) def _add_record_ref(self, ref): self._record_refs.append(ref) def close(self): """ Close the connection to the mDNS daemon and terminate any associated browse, resolve, etc. operations. """ if self._valid(): for ref in self._record_refs: ref._invalidate() del self._record_refs _global_lock.acquire() try: _DNSServiceRefDeallocate(self) finally: _global_lock.release() self._invalidate() del self._callbacks def fileno(self): """ Return the file descriptor associated with this connection. This descriptor should never be read from or written to directly. It should only be passed to select() or poll() to determine when a response from the mDNS daemon is available. """ _global_lock.acquire() try: fd = _DNSServiceRefSockFD(self) finally: _global_lock.release() return fd _DNSServiceDomainEnumReply = _CFunc( None, DNSServiceRef, # sdRef _DNSServiceFlags, # flags ctypes.c_uint32, # interfaceIndex _DNSServiceErrorType, # errorCode _utf8_char_p, # replyDomain ctypes.c_void_p, # context ) _DNSServiceRegisterReply = _CFunc( None, DNSServiceRef, # sdRef _DNSServiceFlags, # flags _DNSServiceErrorType, # errorCode _utf8_char_p, # name _utf8_char_p, # regtype _utf8_char_p, # domain ctypes.c_void_p, # context ) _DNSServiceBrowseReply = _CFunc( None, DNSServiceRef, # sdRef _DNSServiceFlags, # flags ctypes.c_uint32, # interfaceIndex _DNSServiceErrorType, # errorCode _utf8_char_p, # serviceName _utf8_char_p, # regtype _utf8_char_p, # replyDomain ctypes.c_void_p, # context ) _DNSServiceResolveReply = _CFunc( None, DNSServiceRef, # sdRef _DNSServiceFlags, # flags ctypes.c_uint32, # interfaceIndex _DNSServiceErrorType, # errorCode _utf8_char_p, # fullname _utf8_char_p, # hosttarget ctypes.c_uint16, # port ctypes.c_uint16, # txtLen ctypes.c_void_p, # txtRecord (not null-terminated, so c_void_p) ctypes.c_void_p, # context ) _DNSServiceRegisterRecordReply = _CFunc( None, DNSServiceRef, # sdRef DNSRecordRef, # RecordRef _DNSServiceFlags, # flags _DNSServiceErrorType, # errorCode ctypes.c_void_p, # context ) _DNSServiceQueryRecordReply = _CFunc( None, DNSServiceRef, # sdRef _DNSServiceFlags, # flags ctypes.c_uint32, # interfaceIndex _DNSServiceErrorType, # errorCode _utf8_char_p, # fullname ctypes.c_uint16, # rrtype ctypes.c_uint16, # rrclass ctypes.c_uint16, # rdlen ctypes.c_void_p, # rdata ctypes.c_uint32, # ttl ctypes.c_void_p, # context ) ################################################################################ # # Low-level function bindings # ################################################################################ def _create_function_bindings(): ERRCHECK = True NO_ERRCHECK = False OUTPARAM = (lambda index: index) NO_OUTPARAM = None specs = { #'funcname': #( # return_type, # errcheck, # outparam, # ( # param_1_type, # param_2_type, # ... # param_n_type, # )), 'DNSServiceRefSockFD': ( ctypes.c_int, NO_ERRCHECK, NO_OUTPARAM, ( DNSServiceRef, # sdRef )), 'DNSServiceProcessResult': ( _DNSServiceErrorType, ERRCHECK, NO_OUTPARAM, ( DNSServiceRef, # sdRef )), 'DNSServiceRefDeallocate': ( None, NO_ERRCHECK, NO_OUTPARAM, ( DNSServiceRef, # sdRef )), 'DNSServiceEnumerateDomains': ( _DNSServiceErrorType, ERRCHECK, OUTPARAM(0), ( ctypes.POINTER(DNSServiceRef), # sdRef _DNSServiceFlags, # flags ctypes.c_uint32, # interfaceIndex _DNSServiceDomainEnumReply, # callBack ctypes.c_void_p, # context )), 'DNSServiceRegister': ( _DNSServiceErrorType, ERRCHECK, OUTPARAM(0), ( ctypes.POINTER(DNSServiceRef), # sdRef _DNSServiceFlags, # flags ctypes.c_uint32, # interfaceIndex _utf8_char_p, # name _utf8_char_p_non_null, # regtype _utf8_char_p, # domain _utf8_char_p, # host ctypes.c_uint16, # port ctypes.c_uint16, # txtLen ctypes.c_void_p, # txtRecord _DNSServiceRegisterReply, # callBack ctypes.c_void_p, # context )), 'DNSServiceAddRecord': ( _DNSServiceErrorType, ERRCHECK, OUTPARAM(1), ( DNSServiceRef, # sdRef ctypes.POINTER(DNSRecordRef), # RecordRef _DNSServiceFlags, # flags ctypes.c_uint16, # rrtype ctypes.c_uint16, # rdlen ctypes.c_void_p, # rdata ctypes.c_uint32, # ttl )), 'DNSServiceUpdateRecord': ( _DNSServiceErrorType, ERRCHECK, NO_OUTPARAM, ( DNSServiceRef, # sdRef _DNSRecordRef_or_null, # RecordRef _DNSServiceFlags, # flags ctypes.c_uint16, # rdlen ctypes.c_void_p, # rdata ctypes.c_uint32, # ttl )), 'DNSServiceRemoveRecord': ( _DNSServiceErrorType, ERRCHECK, NO_OUTPARAM, ( DNSServiceRef, # sdRef DNSRecordRef, # RecordRef _DNSServiceFlags, # flags )), 'DNSServiceBrowse': ( _DNSServiceErrorType, ERRCHECK, OUTPARAM(0), ( ctypes.POINTER(DNSServiceRef), # sdRef _DNSServiceFlags, # flags ctypes.c_uint32, # interfaceIndex _utf8_char_p_non_null, # regtype _utf8_char_p, # domain _DNSServiceBrowseReply, # callBack ctypes.c_void_p, # context )), 'DNSServiceResolve': ( _DNSServiceErrorType, ERRCHECK, OUTPARAM(0), ( ctypes.POINTER(DNSServiceRef), # sdRef _DNSServiceFlags, # flags ctypes.c_uint32, # interfaceIndex _utf8_char_p_non_null, # name _utf8_char_p_non_null, # regtype _utf8_char_p_non_null, # domain _DNSServiceResolveReply, # callBack ctypes.c_void_p, # context )), 'DNSServiceCreateConnection': ( _DNSServiceErrorType, ERRCHECK, OUTPARAM(0), ( ctypes.POINTER(DNSServiceRef), # sdRef )), 'DNSServiceRegisterRecord': ( _DNSServiceErrorType, ERRCHECK, OUTPARAM(1), ( DNSServiceRef, # sdRef ctypes.POINTER(DNSRecordRef), # RecordRef _DNSServiceFlags, # flags ctypes.c_uint32, # interfaceIndex _utf8_char_p_non_null, # fullname ctypes.c_uint16, # rrtype ctypes.c_uint16, # rrclass ctypes.c_uint16, # rdlen ctypes.c_void_p, # rdata ctypes.c_uint32, # ttl _DNSServiceRegisterRecordReply, # callBack ctypes.c_void_p, # context )), 'DNSServiceQueryRecord': ( _DNSServiceErrorType, ERRCHECK, OUTPARAM(0), ( ctypes.POINTER(DNSServiceRef), # sdRef _DNSServiceFlags, # flags ctypes.c_uint32, # interfaceIndex _utf8_char_p_non_null, # fullname ctypes.c_uint16, # rrtype ctypes.c_uint16, # rrclass _DNSServiceQueryRecordReply, # callBack ctypes.c_void_p, # context )), 'DNSServiceReconfirmRecord': ( None, # _DNSServiceErrorType in more recent versions NO_ERRCHECK, NO_OUTPARAM, ( _DNSServiceFlags, # flags ctypes.c_uint32, # interfaceIndex _utf8_char_p_non_null, # fullname ctypes.c_uint16, # rrtype ctypes.c_uint16, # rrclass ctypes.c_uint16, # rdlen ctypes.c_void_p, # rdata )), 'DNSServiceConstructFullName': ( ctypes.c_int, ERRCHECK, OUTPARAM(0), ( ctypes.c_char * kDNSServiceMaxDomainName, # fullName _utf8_char_p, # service _utf8_char_p_non_null, # regtype _utf8_char_p_non_null, # domain )), } - for name, (restype, errcheck, outparam, argtypes) in specs.iteritems(): + for name, (restype, errcheck, outparam, argtypes) in list(specs.items()): prototype = _CFunc(restype, *argtypes) paramflags = [1] * len(argtypes) if outparam is not None: paramflags[outparam] = 2 paramflags = tuple((val,) for val in paramflags) func = prototype((name, _libdnssd), paramflags) if errcheck: func.errcheck = BonjourError._errcheck globals()['_' + name] = func # preset these so that pyflakes doesn't complain _DNSServiceRefDeallocate = None _DNSServiceRefSockFD = None _DNSServiceProcessResult = None _DNSServiceEnumerateDomains = None _DNSServiceRegister = None _DNSServiceAddRecord = None _DNSServiceUpdateRecord = None _DNSServiceRemoveRecord = None _DNSServiceBrowse = None _DNSServiceResolve = None _DNSServiceCreateConnection = None _DNSServiceRegisterRecord = None _DNSServiceQueryRecord = None _DNSServiceReconfirmRecord = None _DNSServiceConstructFullName = None # Only need to do this once _create_function_bindings() del _create_function_bindings ################################################################################ # # Internal utility types and functions # ################################################################################ class _NoDefault(object): def __repr__(self): return '' def check(self, obj): if obj is self: raise ValueError('required parameter value missing') _NO_DEFAULT = _NoDefault() def _string_to_length_and_void_p(string): if isinstance(string, TXTRecord): string = str(string) void_p = ctypes.cast(ctypes.c_char_p(string), ctypes.c_void_p) return len(string), void_p def _length_and_void_p_to_string(length, void_p): char_p = ctypes.cast(void_p, ctypes.POINTER(ctypes.c_char)) - return ''.join(char_p[i] for i in xrange(length)) + return ''.join(char_p[i] for i in range(length)) ################################################################################ # # High-level functions # ################################################################################ def DNSServiceProcessResult( sdRef, ): """ Read a reply from the daemon, calling the appropriate application callback. This call will block until the daemon's response is received. Use sdRef in conjunction with select() or poll() to determine the presence of a response from the server before calling this function to process the reply without blocking. Call this function at any point if it is acceptable to block until the daemon's response arrives. Note that the client is responsible for ensuring that DNSServiceProcessResult() is called whenever there is a reply from the daemon; the daemon may terminate its connection with a client that does not process the daemon's responses. sdRef: A DNSServiceRef returned by any of the DNSService calls that take a callback parameter. """ _global_lock.acquire() try: _DNSServiceProcessResult(sdRef) finally: _global_lock.release() def DNSServiceEnumerateDomains( flags, interfaceIndex = kDNSServiceInterfaceIndexAny, callBack = None, ): """ Asynchronously enumerate domains available for browsing and registration. The enumeration MUST be cancelled by closing the returned DNSServiceRef when no more domains are to be found. flags: Possible values are: kDNSServiceFlagsBrowseDomains to enumerate domains recommended for browsing. kDNSServiceFlagsRegistrationDomains to enumerate domains recommended for registration. interfaceIndex: If non-zero, specifies the interface on which to look for domains. Most applications will pass kDNSServiceInterfaceIndexAny (0) to enumerate domains on all interfaces. callBack: The function to be called when a domain is found or the call asynchronously fails. Its signature should be callBack(sdRef, flags, interfaceIndex, errorCode, replyDomain). return value: A DNSServiceRef instance. Callback Parameters: sdRef: The DNSServiceRef returned by DNSServiceEnumerateDomains(). flags: Possible values are: kDNSServiceFlagsMoreComing kDNSServiceFlagsAdd kDNSServiceFlagsDefault interfaceIndex: Specifies the interface on which the domain exists. errorCode: Will be kDNSServiceErr_NoError (0) on success, otherwise indicates the failure that occurred (in which case other parameters are undefined). replyDomain: The name of the domain. """ @_DNSServiceDomainEnumReply def _callback(sdRef, flags, interfaceIndex, errorCode, replyDomain, context): if callBack is not None: callBack(sdRef, flags, interfaceIndex, errorCode, replyDomain.decode()) _global_lock.acquire() try: sdRef = _DNSServiceEnumerateDomains(flags, interfaceIndex, _callback, None) finally: _global_lock.release() sdRef._add_callback(_callback) return sdRef def DNSServiceRegister( flags = 0, interfaceIndex = kDNSServiceInterfaceIndexAny, name = None, regtype = _NO_DEFAULT, domain = None, host = None, port = _NO_DEFAULT, txtRecord = '', callBack = None, ): """ Register a service that is discovered via DNSServiceBrowse() and DNSServiceResolve() calls. flags: Indicates the renaming behavior on name conflict. Most applications will pass 0. interfaceIndex: If non-zero, specifies the interface on which to register the service. Most applications will pass kDNSServiceInterfaceIndexAny (0) to register on all available interfaces. name: If not None, specifies the service name to be registered. Most applications will not specify a name, in which case the computer name is used. (This name is communicated to the client via the callback.) If a name is specified, it must be 1-63 bytes of UTF-8 text. If the name is longer than 63 bytes, it will be automatically truncated to a legal length, unless the flag kDNSServiceFlagsNoAutoRename is set, in which case a BonjourError exception will be thrown. regtype: The service type followed by the protocol, separated by a dot (e.g. "_ftp._tcp"). The service type must be an underscore, followed by 1-14 characters, which may be letters, digits, or hyphens. The transport protocol must be "_tcp" or "_udp". New service types should be registered at . domain: If not None, specifies the domain on which to advertise the service. Most applications will not specify a domain, instead automatically registering in the default domain(s). host: If not None, specifies the SRV target host name. Most applications will not specify a host, instead automatically using the machine's default host name(s). Note that specifying a host name does NOT create an address record for that host; the application is responsible for ensuring that the appropriate address record exists, or creating it via DNSServiceRegisterRecord(). port: The port, in host (not network) byte order, on which the service accepts connections. Pass 0 for a "placeholder" service (i.e. a service that will not be discovered by browsing, but will cause a name conflict if another client tries to register that same name). Most clients will not use placeholder services. txtRecord: The TXT record rdata. If not None, txtRecord must be either a TXTRecord instance or a string containing a properly formatted DNS TXT record, i.e. ... callBack: The function to be called when the registration completes or asynchronously fails. Its signature should be callBack(sdRef, flags, errorCode, name, regtype, domain). The client MAY pass None for the callback, in which case the client will NOT be notified of the default values picked on its behalf, and the client will NOT be notified of any asynchronous errors (e.g. out of memory errors, etc.) that may prevent the registration of the service. The client may NOT pass the flag kDNSServiceFlagsNoAutoRename if the callback is None. The client may still deregister the service at any time by closing the returned DNSServiceRef. return value: A DNSServiceRef instance. The registration will remain active indefinitely until the client terminates it by closing the DNSServiceRef. Callback Parameters: sdRef: The DNSServiceRef returned by DNSServiceRegister(). flags: Currently unused, reserved for future use. errorCode: Will be kDNSServiceErr_NoError on success, otherwise will indicate the failure that occurred (including name conflicts, if the kDNSServiceFlagsNoAutoRename flag was used when registering). Other parameters are undefined if an error occurred. name: The service name registered. (If the application did not specify a name in DNSServiceRegister(), this indicates what name was automatically chosen.) regtype: The type of service registered, as it was passed to the callout. domain: The domain on which the service was registered. (If the application did not specify a domain in DNSServiceRegister(), this indicates the default domain on which the service was registered.) """ _NO_DEFAULT.check(regtype) _NO_DEFAULT.check(port) port = socket.htons(port) if not txtRecord: txtLen, txtRecord = 1, '\0' else: txtLen, txtRecord = _string_to_length_and_void_p(txtRecord) @_DNSServiceRegisterReply def _callback(sdRef, flags, errorCode, name, regtype, domain, context): if callBack is not None: callBack(sdRef, flags, errorCode, name.decode(), regtype.decode(), domain.decode()) _global_lock.acquire() try: sdRef = _DNSServiceRegister(flags, interfaceIndex, name, regtype, domain, host, port, txtLen, txtRecord, _callback, None) finally: _global_lock.release() sdRef._add_callback(_callback) return sdRef def DNSServiceAddRecord( sdRef, flags = 0, rrtype = _NO_DEFAULT, rdata = _NO_DEFAULT, ttl = 0, ): """ Add a record to a registered service. The name of the record will be the same as the registered service's name. The record can later be updated or deregistered by passing the DNSRecordRef returned by this function to DNSServiceUpdateRecord() or DNSServiceRemoveRecord(). Note that DNSServiceAddRecord/UpdateRecord/RemoveRecord are NOT thread-safe with respect to a single DNSServiceRef. If you plan to have multiple threads in your program simultaneously add, update, or remove records from the same DNSServiceRef, then it's the caller's responsibility to use a lock or take similar appropriate precautions to serialize those calls. sdRef: A DNSServiceRef returned by DNSServiceRegister(). flags: Currently ignored, reserved for future use. rrtype: The type of the record (e.g. kDNSServiceType_TXT, kDNSServiceType_SRV, etc.). rdata: A string containing the raw rdata to be contained in the added resource record. ttl: The time to live of the resource record, in seconds. Pass 0 to use a default value. return value: A DNSRecordRef instance, which may be passed to DNSServiceUpdateRecord() or DNSServiceRemoveRecord(). If sdRef is closed, the DNSRecordRef is also invalidated and may not be used further. """ _NO_DEFAULT.check(rrtype) _NO_DEFAULT.check(rdata) rdlen, rdata = _string_to_length_and_void_p(rdata) _global_lock.acquire() try: RecordRef = _DNSServiceAddRecord(sdRef, flags, rrtype, rdlen, rdata, ttl) finally: _global_lock.release() sdRef._add_record_ref(RecordRef) return RecordRef def DNSServiceUpdateRecord( sdRef, RecordRef = None, flags = 0, rdata = _NO_DEFAULT, ttl = 0, ): """ Update a registered resource record. The record must either be: - The primary txt record of a service registered via DNSServiceRegister(), or - A record added to a registered service via DNSServiceAddRecord(), or - An individual record registered by DNSServiceRegisterRecord() sdRef: A DNSServiceRef returned by DNSServiceRegister() or DNSServiceCreateConnection(). RecordRef: A DNSRecordRef returned by DNSServiceAddRecord(), or None to update the service's primary txt record. flags: Currently ignored, reserved for future use. rdata: A string containing the new rdata to be contained in the updated resource record. ttl: The time to live of the updated resource record, in seconds. """ _NO_DEFAULT.check(rdata) rdlen, rdata = _string_to_length_and_void_p(rdata) _global_lock.acquire() try: _DNSServiceUpdateRecord(sdRef, RecordRef, flags, rdlen, rdata, ttl) finally: _global_lock.release() def DNSServiceRemoveRecord( sdRef, RecordRef, flags = 0, ): """ Remove a record previously added to a service record set via DNSServiceAddRecord(), or deregister a record registered individually via DNSServiceRegisterRecord(). sdRef: A DNSServiceRef returned by DNSServiceRegister() (if the record being removed was registered via DNSServiceAddRecord()) or by DNSServiceCreateConnection() (if the record being removed was registered via DNSServiceRegisterRecord()). recordRef: A DNSRecordRef returned by DNSServiceAddRecord() or DNSServiceRegisterRecord(). flags: Currently ignored, reserved for future use. """ _global_lock.acquire() try: _DNSServiceRemoveRecord(sdRef, RecordRef, flags) finally: _global_lock.release() RecordRef._invalidate() def DNSServiceBrowse( flags = 0, interfaceIndex = kDNSServiceInterfaceIndexAny, regtype = _NO_DEFAULT, domain = None, callBack = None, ): """ Browse for instances of a service. flags: Currently ignored, reserved for future use. interfaceIndex: If non-zero, specifies the interface on which to browse for services. Most applications will pass kDNSServiceInterfaceIndexAny (0) to browse on all available interfaces. regtype: The service type being browsed for followed by the protocol, separated by a dot (e.g. "_ftp._tcp"). The transport protocol must be "_tcp" or "_udp". domain: If not None, specifies the domain on which to browse for services. Most applications will not specify a domain, instead browsing on the default domain(s). callBack: The function to be called when an instance of the service being browsed for is found, or if the call asynchronously fails. Its signature should be callBack(sdRef, flags, interfaceIndex, errorCode, serviceName, regtype, replyDomain). return value: A DNSServiceRef instance. The browse operation will run indefinitely until the client terminates it by closing the DNSServiceRef. Callback Parameters: sdRef: The DNSServiceRef returned by DNSServiceBrowse(). flags: Possible values are kDNSServiceFlagsMoreComing and kDNSServiceFlagsAdd. interfaceIndex: The interface on which the service is advertised. This index should be passed to DNSServiceResolve() when resolving the service. errorCode: Will be kDNSServiceErr_NoError (0) on success, otherwise will indicate the failure that occurred. Other parameters are undefined if an error occurred. serviceName: The discovered service name. This name should be displayed to the user and stored for subsequent use in the DNSServiceResolve() call. regtype: The service type, which is usually (but not always) the same as was passed to DNSServiceBrowse(). One case where the discovered service type may not be the same as the requested service type is when using subtypes: The client may want to browse for only those ftp servers that allow anonymous connections. The client will pass the string "_ftp._tcp,_anon" to DNSServiceBrowse(), but the type of the service that's discovered is simply "_ftp._tcp". The regtype for each discovered service instance should be stored along with the name, so that it can be passed to DNSServiceResolve() when the service is later resolved. replyDomain: The domain of the discovered service instance. This may or may not be the same as the domain that was passed to DNSServiceBrowse(). The domain for each discovered service instance should be stored along with the name, so that it can be passed to DNSServiceResolve() when the service is later resolved. """ _NO_DEFAULT.check(regtype) @_DNSServiceBrowseReply def _callback(sdRef, flags, interfaceIndex, errorCode, serviceName, regtype, replyDomain, context): if callBack is not None: callBack(sdRef, flags, interfaceIndex, errorCode, serviceName.decode(), regtype.decode(), replyDomain.decode()) _global_lock.acquire() try: sdRef = _DNSServiceBrowse(flags, interfaceIndex, regtype, domain, _callback, None) finally: _global_lock.release() sdRef._add_callback(_callback) return sdRef def DNSServiceResolve( flags = 0, interfaceIndex = _NO_DEFAULT, name = _NO_DEFAULT, regtype = _NO_DEFAULT, domain = _NO_DEFAULT, callBack = None, ): """ Resolve a service name discovered via DNSServiceBrowse() to a target host name, port number, and txt record. Note: Applications should NOT use DNSServiceResolve() solely for txt record monitoring; use DNSServiceQueryRecord() instead, as it is more efficient for this task. Note: When the desired results have been returned, the client MUST terminate the resolve by closing the returned DNSServiceRef. Note: DNSServiceResolve() behaves correctly for typical services that have a single SRV record and a single TXT record. To resolve non-standard services with multiple SRV or TXT records, DNSServiceQueryRecord() should be used. flags: Currently ignored, reserved for future use. interfaceIndex: The interface on which to resolve the service. If this resolve call is as a result of a currently active DNSServiceBrowse() operation, then the interfaceIndex should be the index reported in the browse callback. If this resolve call is using information previously saved (e.g. in a preference file) for later use, then use kDNSServiceInterfaceIndexAny (0), because the desired service may now be reachable via a different physical interface. name: The name of the service instance to be resolved, as reported to the DNSServiceBrowse() callback. regtype: The type of the service instance to be resolved, as reported to the DNSServiceBrowse() callback. domain: The domain of the service instance to be resolved, as reported to the DNSServiceBrowse() callback. callBack: The function to be called when a result is found, or if the call asynchronously fails. Its signature should be callBack(sdRef, flags, interfaceIndex, errorCode, fullname, hosttarget, port, txtRecord). return value: A DNSServiceRef instance. The resolve operation will run indefinitely until the client terminates it by closing the DNSServiceRef. Callback Parameters: sdRef: The DNSServiceRef returned by DNSServiceResolve(). flags: Currently unused, reserved for future use. interfaceIndex: The interface on which the service was resolved. errorCode: Will be kDNSServiceErr_NoError (0) on success, otherwise will indicate the failure that occurred. Other parameters are undefined if an error occurred. fullname: The full service domain name, in the form ... hosttarget: The target hostname of the machine providing the service. port: The port, in host (not network) byte order, on which connections are accepted for this service. txtRecord: A string containing the service's primary txt record, in standard txt record format. """ _NO_DEFAULT.check(interfaceIndex) _NO_DEFAULT.check(name) _NO_DEFAULT.check(regtype) _NO_DEFAULT.check(domain) @_DNSServiceResolveReply def _callback(sdRef, flags, interfaceIndex, errorCode, fullname, hosttarget, port, txtLen, txtRecord, context): if callBack is not None: port = socket.ntohs(port) txtRecord = _length_and_void_p_to_string(txtLen, txtRecord) callBack(sdRef, flags, interfaceIndex, errorCode, fullname.decode(), hosttarget.decode(), port, txtRecord) _global_lock.acquire() try: sdRef = _DNSServiceResolve(flags, interfaceIndex, name, regtype, domain, _callback, None) finally: _global_lock.release() sdRef._add_callback(_callback) return sdRef def DNSServiceCreateConnection(): """ Create a connection to the daemon allowing efficient registration of multiple individual records. return value: A DNSServiceRef instance. Closing it severs the connection and deregisters all records registered on this connection. """ _global_lock.acquire() try: sdRef = _DNSServiceCreateConnection() finally: _global_lock.release() return sdRef def DNSServiceRegisterRecord( sdRef, flags, interfaceIndex = kDNSServiceInterfaceIndexAny, fullname = _NO_DEFAULT, rrtype = _NO_DEFAULT, rrclass = kDNSServiceClass_IN, rdata = _NO_DEFAULT, ttl = 0, callBack = None, ): """ Register an individual resource record on a connected DNSServiceRef. Note that name conflicts occurring for records registered via this call must be handled by the client in the callback. sdRef: A DNSServiceRef returned by DNSServiceCreateConnection(). flags: Possible values are kDNSServiceFlagsShared or kDNSServiceFlagsUnique. interfaceIndex: If non-zero, specifies the interface on which to register the record. Passing kDNSServiceInterfaceIndexAny (0) causes the record to be registered on all interfaces. fullname: The full domain name of the resource record. rrtype: The numerical type of the resource record (e.g. kDNSServiceType_PTR, kDNSServiceType_SRV, etc.). rrclass: The class of the resource record (usually kDNSServiceClass_IN). rdata: A string containing the raw rdata, as it is to appear in the DNS record. ttl: The time to live of the resource record, in seconds. Pass 0 to use a default value. callBack: The function to be called when a result is found, or if the call asynchronously fails (e.g. because of a name conflict). Its signature should be callBack(sdRef, RecordRef, flags, errorCode). return value: A DNSRecordRef instance, which may be passed to DNSServiceUpdateRecord() or DNSServiceRemoveRecord(). (To deregister ALL records registered on a single connected DNSServiceRef and deallocate each of their corresponding DNSRecordRefs, close the DNSServiceRef.) Callback Parameters: sdRef: The connected DNSServiceRef returned by DNSServiceCreateConnection(). RecordRef: The DNSRecordRef returned by DNSServiceRegisterRecord(). flags: Currently unused, reserved for future use. errorCode: Will be kDNSServiceErr_NoError on success, otherwise will indicate the failure that occurred (including name conflicts). Other parameters are undefined if an error occurred. """ _NO_DEFAULT.check(fullname) _NO_DEFAULT.check(rrtype) _NO_DEFAULT.check(rdata) rdlen, rdata = _string_to_length_and_void_p(rdata) @_DNSServiceRegisterRecordReply def _callback(sdRef, RecordRef, flags, errorCode, context): if callBack is not None: callBack(sdRef, RecordRef, flags, errorCode) _global_lock.acquire() try: RecordRef = _DNSServiceRegisterRecord(sdRef, flags, interfaceIndex, fullname, rrtype, rrclass, rdlen, rdata, ttl, _callback, None) finally: _global_lock.release() sdRef._add_callback(_callback) sdRef._add_record_ref(RecordRef) return RecordRef def DNSServiceQueryRecord( flags = 0, interfaceIndex = kDNSServiceInterfaceIndexAny, fullname = _NO_DEFAULT, rrtype = _NO_DEFAULT, rrclass = kDNSServiceClass_IN, callBack = None, ): """ Query for an arbitrary DNS record. flags: Pass kDNSServiceFlagsLongLivedQuery to create a "long-lived" unicast query in a non-local domain. Without setting this flag, unicast queries will be one-shot; that is, only answers available at the time of the call will be returned. By setting this flag, answers (including Add and Remove events) that become available after the initial call is made will generate callbacks. This flag has no effect on link-local multicast queries. interfaceIndex: If non-zero, specifies the interface on which to issue the query. Passing kDNSServiceInterfaceIndexAny (0) causes the name to be queried for on all interfaces. fullname: The full domain name of the resource record to be queried for. rrtype: The numerical type of the resource record to be queried for (e.g. kDNSServiceType_PTR, kDNSServiceType_SRV, etc.). rrclass: The class of the resource record (usually kDNSServiceClass_IN). callBack: The function to be called when a result is found, or if the call asynchronously fails. Its signature should be callBack(sdRef, flags, interfaceIndex, errorCode, fullname, rrtype, rrclass, rdata, ttl). return value: A DNSServiceRef instance. The query operation will run indefinitely until the client terminates it by closing the DNSServiceRef. Callback Parameters: sdRef: The DNSServiceRef returned by DNSServiceQueryRecord(). flags: Possible values are kDNSServiceFlagsMoreComing and kDNSServiceFlagsAdd. The Add flag is NOT set for PTR records with a ttl of 0, i.e. "Remove" events. interfaceIndex: The interface on which the query was resolved. errorCode: Will be kDNSServiceErr_NoError on success, otherwise will indicate the failure that occurred. Other parameters are undefined if an error occurred. fullname: The resource record's full domain name. rrtype: The resource record's type (e.g. kDNSServiceType_PTR, kDNSServiceType_SRV, etc.). rrclass: The class of the resource record (usually kDNSServiceClass_IN). rdata: A string containing the raw rdata of the resource record. ttl: The resource record's time to live, in seconds. """ _NO_DEFAULT.check(fullname) _NO_DEFAULT.check(rrtype) @_DNSServiceQueryRecordReply def _callback(sdRef, flags, interfaceIndex, errorCode, fullname, rrtype, rrclass, rdlen, rdata, ttl, context): if callBack is not None: rdata = _length_and_void_p_to_string(rdlen, rdata) callBack(sdRef, flags, interfaceIndex, errorCode, fullname.decode(), rrtype, rrclass, rdata, ttl) _global_lock.acquire() try: sdRef = _DNSServiceQueryRecord(flags, interfaceIndex, fullname, rrtype, rrclass, _callback, None) finally: _global_lock.release() sdRef._add_callback(_callback) return sdRef def DNSServiceReconfirmRecord( flags = 0, interfaceIndex = kDNSServiceInterfaceIndexAny, fullname = _NO_DEFAULT, rrtype = _NO_DEFAULT, rrclass = kDNSServiceClass_IN, rdata = _NO_DEFAULT, ): """ Instruct the daemon to verify the validity of a resource record that appears to be out of date (e.g. because tcp connection to a service's target failed). Causes the record to be flushed from the daemon's cache (as well as all other daemons' caches on the network) if the record is determined to be invalid. flags: Currently unused, reserved for future use. interfaceIndex: If non-zero, specifies the interface of the record in question. Passing kDNSServiceInterfaceIndexAny (0) causes all instances of this record to be reconfirmed. fullname: The resource record's full domain name. rrtype: The resource record's type (e.g. kDNSServiceType_PTR, kDNSServiceType_SRV, etc.). rrclass: The class of the resource record (usually kDNSServiceClass_IN). rdata: A string containing the raw rdata of the resource record. """ _NO_DEFAULT.check(fullname) _NO_DEFAULT.check(rrtype) _NO_DEFAULT.check(rdata) rdlen, rdata = _string_to_length_and_void_p(rdata) _global_lock.acquire() try: _DNSServiceReconfirmRecord(flags, interfaceIndex, fullname, rrtype, rrclass, rdlen, rdata) finally: _global_lock.release() def DNSServiceConstructFullName( service = None, regtype = _NO_DEFAULT, domain = _NO_DEFAULT, ): """ Concatenate a three-part domain name (as returned by a callback function) into a properly-escaped full domain name. Note that callback functions already escape strings where necessary. service: The service name; any dots or backslashes must NOT be escaped. May be None (to construct a PTR record name, e.g. "_ftp._tcp.apple.com."). regtype: The service type followed by the protocol, separated by a dot (e.g. "_ftp._tcp"). domain: The domain name, e.g. "apple.com.". Literal dots or backslashes, if any, must be escaped, e.g. "1st\. Floor.apple.com." return value: The resulting full domain name. """ _NO_DEFAULT.check(regtype) _NO_DEFAULT.check(domain) _global_lock.acquire() try: fullName = _DNSServiceConstructFullName(service, regtype, domain) finally: _global_lock.release() return fullName.value.decode('utf-8') ################################################################################ # # TXTRecord class # ################################################################################ class TXTRecord(object): """ A mapping representing a DNS TXT record. The TXT record's name=value entries are stored as key/value pairs in the mapping. Although keys can be accessed in a case-insensitive fashion (meaning txt['foo'] and txt['FoO'] refer to the same value), key case is preserved in the wire representation of the record (so txt['FoO'] = 'bar' will generate a FoO=bar entry in the TXT record). Key order is also preserved, so keys appear in the wire format in the order in which they were created. Note that in addition to being valid as a txtRecord parameter to DNSServiceRegister(), a TXTRecord instance can be used in place of a resource record data string (i.e. rdata parameter) with any function that accepts one. """ def __init__(self, items={}, strict=True): """ Create a new TXTRecord instance, initializing it with the contents of items. If strict is true, then strict conformance to the DNS TXT record format will be enforced, and attempts to add a name containing invalid characters or a name/value pair whose wire representation is longer than 255 bytes will raise a ValueError exception. """ self.strict = strict self._names = [] self._items = {} - for name, value in items.iteritems(): + for name, value in list(items.items()): self[name] = value def __contains__(self, name): 'Return True if name is a key in the record, False otherwise' return (name.lower() in self._items) def __iter__(self): 'Return an iterator over name/value pairs' for name in self._names: yield self._items[name] def __len__(self): 'Return the number of name/value pairs' return len(self._names) - def __nonzero__(self): + def __bool__(self): 'Return False if the record is empty, True otherwise' return bool(self._items) def __str__(self): """ Return the wire representation of the TXT record as a string. If self.strict is false, any name/value pair whose wire length if greater than 255 bytes will be truncated to 255 bytes. If the record is empty, '\\0' is returned. """ if not self: return '\0' parts = [] for name, value in self: if value is None: item = name else: item = '%s=%s' % (name, value) if (not self.strict) and (len(item) > 255): item = item[:255] parts.append(chr(len(item))) parts.append(item) return ''.join(parts) def __getitem__(self, name): """ Return the value associated with name. The value is either None (meaning name has no associated value) or an str instance (which may be of length 0). Raises KeyError if name is not a key. """ return self._items[name.lower()][1] # Require one or more printable ASCII characters (0x20-0x7E), # excluding '=' (0x3D) _valid_name_re = re.compile(r'^[ -<>-~]+$') def __setitem__(self, name, value): """ Add a name/value pair to the record. If value is None, then name will have no associated value. If value is a unicode instance, it will be encoded as a UTF-8 string. Otherwise, value will be converted to an str instance. """ stored_name = name name = name.lower() length = len(name) if value is not None: - if isinstance(value, unicode): + if isinstance(value, str): value = value.encode('utf-8') else: value = str(value) length += 1 + len(value) if self.strict and (length > 255): raise ValueError('name=value string must be 255 bytes or less') if name not in self._items: if self.strict and (self._valid_name_re.match(stored_name) is None): raise ValueError("invalid name: '%s'" % stored_name) self._names.append(name) self._items[name] = (stored_name, value) def __delitem__(self, name): """ Remove name and its corresponding value from the record. Raises KeyError if name is not a key. """ name = name.lower() del self._items[name] self._names.remove(name) def get(self, name, default=None): return self._items.get(name.lower(), (name, default))[1] @classmethod def parse(cls, data, strict=False): """ Given a string data containing the wire representation of a DNS TXT record, parse it and return a TXTRecord instance. The strict parameter is passed to the TXTRecord constructor. """ txt = cls(strict=strict) while data: length = ord(data[0]) item = data[1:length+1].split('=', 1) # Add the item only if the name is non-empty and there are # no existing items with the same name if item[0] and (item[0] not in txt): if len(item) == 1: txt[item[0]] = None else: txt[item[0]] = item[1] data = data[length+1:] return txt diff --git a/sipsimple/account/publication.py b/sipsimple/account/publication.py index 16b02cb1..1d84d26f 100644 --- a/sipsimple/account/publication.py +++ b/sipsimple/account/publication.py @@ -1,391 +1,390 @@ """Implements the publisher handlers""" __all__ = ['Publisher', 'PresencePublisher', 'DialogPublisher'] import random from abc import ABCMeta, abstractproperty from threading import Lock from time import time from application.notification import IObserver, NotificationCenter, NotificationData from application.python import Null, limit from application.python.types import MarkerType from eventlib import coros, proc from twisted.internet import reactor from zope.interface import implements from sipsimple.core import FromHeader, Publication, PublicationETagError, RouteHeader, SIPURI, SIPCoreError from sipsimple.configuration.settings import SIPSimpleSettings from sipsimple.lookup import DNSLookup, DNSLookupError from sipsimple.payloads.dialoginfo import DialogInfoDocument from sipsimple.payloads.pidf import PIDFDocument from sipsimple.threading import run_in_twisted_thread from sipsimple.threading.green import Command, run_in_green_thread Command.register_defaults('publish', refresh_interval=None) -class SameState: __metaclass__ = MarkerType +class SameState(metaclass=MarkerType): pass class SIPPublicationDidFail(Exception): def __init__(self, data): self.data = data class SIPPublicationDidNotEnd(Exception): def __init__(self, data): self.data = data class PublicationError(Exception): def __init__(self, error, retry_after, refresh_interval=None): self.error = error self.retry_after = retry_after self.refresh_interval = refresh_interval class PublisherNickname(dict): def __missing__(self, name): return self.setdefault(name, name[:-9] if name.endswith('Publisher') else name) def __get__(self, obj, objtype): return self[objtype.__name__] def __set__(self, obj, value): raise AttributeError('cannot set attribute') def __delete__(self, obj): raise AttributeError('cannot delete attribute') -class Publisher(object): - __metaclass__ = ABCMeta +class Publisher(object, metaclass=ABCMeta): __nickname__ = PublisherNickname() __transports__ = frozenset(['tls', 'tcp', 'udp']) implements(IObserver) def __init__(self, account): self.account = account self.started = False self.active = False self.publishing = False self._lock = Lock() self._command_proc = None self._command_channel = coros.queue() self._data_channel = coros.queue() self._publication = None self._dns_wait = 1 self._publish_wait = 1 self._publication_timer = None self.__dict__['state'] = None @abstractproperty def event(self): return None @abstractproperty def payload_type(self): return None @property def extra_headers(self): return [] @property def state(self): return self.__dict__['state'] @state.setter def state(self, state): if state is not None and not isinstance(state, self.payload_type.root_element): raise ValueError("state must be a %s document or None" % self.payload_type.root_element.__name__) with self._lock: old_state = self.__dict__['state'] self.__dict__['state'] = state if state == old_state: return self._publish(state) def start(self): if self.started: return self.started = True notification_center = NotificationCenter() notification_center.add_observer(self, sender=self) notification_center.post_notification(self.__class__.__name__ + 'WillStart', sender=self) notification_center.add_observer(self, name='CFGSettingsObjectDidChange', sender=self.account) notification_center.add_observer(self, name='CFGSettingsObjectDidChange', sender=SIPSimpleSettings()) notification_center.add_observer(self, name='NetworkConditionsDidChange') self._command_proc = proc.spawn(self._run) notification_center.post_notification(self.__class__.__name__ + 'DidStart', sender=self) notification_center.remove_observer(self, sender=self) def stop(self): if not self.started: return self.started = False self.active = False notification_center = NotificationCenter() notification_center.add_observer(self, sender=self) notification_center.post_notification(self.__class__.__name__ + 'WillEnd', sender=self) notification_center.remove_observer(self, name='CFGSettingsObjectDidChange', sender=self.account) notification_center.remove_observer(self, name='CFGSettingsObjectDidChange', sender=SIPSimpleSettings()) notification_center.remove_observer(self, name='NetworkConditionsDidChange') command = Command('terminate') self._command_channel.send(command) command.wait() self._command_proc = None notification_center.post_notification(self.__class__.__name__ + 'DidDeactivate', sender=self) notification_center.post_notification(self.__class__.__name__ + 'DidEnd', sender=self) notification_center.remove_observer(self, sender=self) def activate(self): if not self.started: raise RuntimeError("not started") self.active = True self._command_channel.send(Command('publish', state=self.state)) notification_center = NotificationCenter() notification_center.post_notification(self.__class__.__name__ + 'DidActivate', sender=self) def deactivate(self): if not self.started: raise RuntimeError("not started") self.active = False self._command_channel.send(Command('unpublish')) notification_center = NotificationCenter() notification_center.post_notification(self.__class__.__name__ + 'DidDeactivate', sender=self) @run_in_twisted_thread def _publish(self, state): if not self.active: return if state is None: self._command_channel.send(Command('unpublish')) else: self._command_channel.send(Command('publish', state=state)) def _run(self): while True: command = self._command_channel.wait() handler = getattr(self, '_CH_%s' % command.name) handler(command) def _CH_publish(self, command): if command.state is None or self._publication is None and command.state is SameState: command.signal() return notification_center = NotificationCenter() settings = SIPSimpleSettings() if self._publication_timer is not None and self._publication_timer.active(): self._publication_timer.cancel() self._publication_timer = None if self._publication is None: duration = command.refresh_interval or self.account.sip.publish_interval from_header = FromHeader(self.account.uri, self.account.display_name) self._publication = Publication(from_header, self.event, self.payload_type.content_type, credentials=self.account.credentials, duration=duration, extra_headers=self.extra_headers) notification_center.add_observer(self, sender=self._publication) notification_center.post_notification(self.__class__.__name__ + 'WillPublish', sender=self, data=NotificationData(state=command.state, duration=duration)) else: notification_center.post_notification(self.__class__.__name__ + 'WillRefresh', sender=self, data=NotificationData(state=command.state)) try: # Lookup routes valid_transports = self.__transports__.intersection(settings.sip.transport_list) if self.account.sip.outbound_proxy is not None and self.account.sip.outbound_proxy.transport in valid_transports: uri = SIPURI(host=self.account.sip.outbound_proxy.host, port=self.account.sip.outbound_proxy.port, parameters={'transport': self.account.sip.outbound_proxy.transport}) else: uri = SIPURI(host=self.account.id.domain) lookup = DNSLookup() try: routes = lookup.lookup_sip_proxy(uri, valid_transports).wait() - except DNSLookupError, e: + except DNSLookupError as e: retry_after = random.uniform(self._dns_wait, 2*self._dns_wait) self._dns_wait = limit(2*self._dns_wait, max=30) raise PublicationError('DNS lookup failed: %s' % e, retry_after=retry_after) else: self._dns_wait = 1 body = None if command.state is SameState else command.state.toxml() # Publish by trying each route in turn publish_timeout = time() + 30 for route in routes: remaining_time = publish_timeout-time() if remaining_time > 0: try: try: self._publication.publish(body, RouteHeader(route.uri), timeout=limit(remaining_time, min=1, max=10)) except ValueError as e: # this happens for an initial PUBLISH with body=None raise PublicationError(str(e), retry_after=0) except PublicationETagError: state = self.state # access self.state only once to avoid race conditions if state is not None: self._publication.publish(state.toxml(), RouteHeader(route.uri), timeout=limit(remaining_time, min=1, max=10)) else: command.signal() return except SIPCoreError: raise PublicationError('Internal error', retry_after=5) try: while True: notification = self._data_channel.wait() if notification.name == 'SIPPublicationDidSucceed': break if notification.name == 'SIPPublicationDidEnd': raise PublicationError('Publication expired', retry_after=0) # publication expired while we were trying to re-publish - except SIPPublicationDidFail, e: + except SIPPublicationDidFail as e: if e.data.code == 407: # Authentication failed, so retry the publication in some time raise PublicationError('Authentication failed', retry_after=random.uniform(60, 120)) elif e.data.code == 412: raise PublicationError('Conditional request failed', retry_after=0) elif e.data.code == 423: # Get the value of the Min-Expires header if e.data.min_expires is not None and e.data.min_expires > self.account.sip.publish_interval: refresh_interval = e.data.min_expires else: refresh_interval = None raise PublicationError('Interval too short', retry_after=random.uniform(60, 120), refresh_interval=refresh_interval) elif e.data.code in (405, 406, 489): raise PublicationError('Method or event not supported', retry_after=3600) else: # Otherwise just try the next route continue else: self.publishing = True self._publish_wait = 1 command.signal() break else: # There are no more routes to try, reschedule the publication retry_after = random.uniform(self._publish_wait, 2*self._publish_wait) self._publish_wait = limit(self._publish_wait*2, max=30) raise PublicationError('No more routes to try', retry_after=retry_after) - except PublicationError, e: + except PublicationError as e: self.publishing = False notification_center.remove_observer(self, sender=self._publication) def publish(): if self.active: self._command_channel.send(Command('publish', event=command.event, state=self.state, refresh_interval=e.refresh_interval)) else: command.signal() self._publication_timer = None self._publication_timer = reactor.callLater(e.retry_after, publish) self._publication = None notification_center.post_notification(self.__nickname__ + 'PublicationDidFail', sender=self, data=NotificationData(reason=e.error)) else: notification_center.post_notification(self.__nickname__ + 'PublicationDidSucceed', sender=self) def _CH_unpublish(self, command): # Cancel any timer which would restart the publication process if self._publication_timer is not None and self._publication_timer.active(): self._publication_timer.cancel() self._publication_timer = None publishing = self.publishing self.publishing = False if self._publication is not None: notification_center = NotificationCenter() if publishing: self._publication.end(timeout=2) try: while True: notification = self._data_channel.wait() if notification.name == 'SIPPublicationDidEnd': break except (SIPPublicationDidFail, SIPPublicationDidNotEnd): notification_center.post_notification(self.__nickname__ + 'PublicationDidNotEnd', sender=self) else: notification_center.post_notification(self.__nickname__ + 'PublicationDidEnd', sender=self) notification_center.remove_observer(self, sender=self._publication) self._publication = None command.signal() def _CH_terminate(self, command): self._CH_unpublish(command) raise proc.ProcExit @run_in_twisted_thread def handle_notification(self, notification): handler = getattr(self, '_NH_%s' % notification.name, Null) handler(notification) def _NH_SIPPublicationDidSucceed(self, notification): if notification.sender is self._publication: self._data_channel.send(notification) def _NH_SIPPublicationDidFail(self, notification): if notification.sender is self._publication: self._data_channel.send_exception(SIPPublicationDidFail(notification.data)) def _NH_SIPPublicationDidEnd(self, notification): if notification.sender is self._publication: self._data_channel.send(notification) def _NH_SIPPublicationDidNotEnd(self, notification): if notification.sender is self._publication: self._data_channel.send_exception(SIPPublicationDidNotEnd(notification.data)) def _NH_SIPPublicationWillExpire(self, notification): if notification.sender is self._publication: self._publish(SameState) @run_in_green_thread def _NH_CFGSettingsObjectDidChange(self, notification): if not self.started: return if 'enabled' in notification.data.modified: return # global account activation is handled separately by the account itself elif 'presence.enabled' in notification.data.modified: if self.account.presence.enabled: self.activate() else: self.deactivate() elif self.active and {'__id__', 'auth.password', 'auth.username', 'sip.outbound_proxy', 'sip.transport_list', 'sip.publish_interval'}.intersection(notification.data.modified): self._command_channel.send(Command('unpublish')) self._command_channel.send(Command('publish', state=self.state)) def _NH_NetworkConditionsDidChange(self, notification): if self.active: self._command_channel.send(Command('unpublish')) self._command_channel.send(Command('publish', state=self.state)) class PresencePublisher(Publisher): """A publisher for presence state""" @property def event(self): return 'presence' @property def payload_type(self): return PIDFDocument def _NH_PresencePublisherDidStart(self, notification): if self.account.presence.enabled: self.activate() class DialogPublisher(Publisher): """A publisher for dialog info state""" @property def event(self): return 'dialog' @property def payload_type(self): return DialogInfoDocument def _NH_DialogPublisherDidStart(self, notification): if self.account.presence.enabled: self.activate() diff --git a/sipsimple/account/registration.py b/sipsimple/account/registration.py index ec884aa3..6b9ca5d6 100644 --- a/sipsimple/account/registration.py +++ b/sipsimple/account/registration.py @@ -1,302 +1,302 @@ """Implements the registration handler""" __all__ = ['Registrar'] import random from time import time from application.notification import IObserver, NotificationCenter, NotificationData from application.python import Null, limit from eventlib import coros, proc from twisted.internet import reactor from zope.interface import implements from sipsimple.core import ContactHeader, FromHeader, Header, Registration, RouteHeader, SIPURI, SIPCoreError, NoGRUU from sipsimple.configuration.settings import SIPSimpleSettings from sipsimple.lookup import DNSLookup, DNSLookupError from sipsimple.threading import run_in_twisted_thread from sipsimple.threading.green import Command, run_in_green_thread Command.register_defaults('register', refresh_interval=None) class SIPRegistrationDidFail(Exception): def __init__(self, data): self.data = data class SIPRegistrationDidNotEnd(Exception): def __init__(self, data): self.data = data class RegistrationError(Exception): def __init__(self, error, retry_after, refresh_interval=None): self.error = error self.retry_after = retry_after self.refresh_interval = refresh_interval class Registrar(object): implements(IObserver) def __init__(self, account): self.account = account self.started = False self.active = False self.registered = False self._command_proc = None self._command_channel = coros.queue() self._data_channel = coros.queue() self._registration = None self._dns_wait = 1 self._register_wait = 1 self._registration_timer = None def start(self): if self.started: return self.started = True notification_center = NotificationCenter() notification_center.add_observer(self, name='CFGSettingsObjectDidChange', sender=self.account) notification_center.add_observer(self, name='CFGSettingsObjectDidChange', sender=SIPSimpleSettings()) notification_center.add_observer(self, name='NetworkConditionsDidChange') self._command_proc = proc.spawn(self._run) if self.account.sip.register: self.activate() def stop(self): if not self.started: return self.started = False self.active = False notification_center = NotificationCenter() notification_center.remove_observer(self, name='CFGSettingsObjectDidChange', sender=self.account) notification_center.remove_observer(self, name='CFGSettingsObjectDidChange', sender=SIPSimpleSettings()) notification_center.remove_observer(self, name='NetworkConditionsDidChange') command = Command('terminate') self._command_channel.send(command) command.wait() self._command_proc = None def activate(self): if not self.started: raise RuntimeError("not started") self.active = True self._command_channel.send(Command('register')) def deactivate(self): if not self.started: raise RuntimeError("not started") self.active = False self._command_channel.send(Command('unregister')) def reregister(self): if self.active: self._command_channel.send(Command('unregister')) self._command_channel.send(Command('register')) def _run(self): while True: command = self._command_channel.wait() handler = getattr(self, '_CH_%s' % command.name) handler(command) def _CH_register(self, command): notification_center = NotificationCenter() settings = SIPSimpleSettings() if self._registration_timer is not None and self._registration_timer.active(): self._registration_timer.cancel() self._registration_timer = None # Initialize the registration if self._registration is None: duration = command.refresh_interval or self.account.sip.register_interval self._registration = Registration(FromHeader(self.account.uri, self.account.display_name), credentials=self.account.credentials, duration=duration, extra_headers=[Header('Supported', 'gruu')]) notification_center.add_observer(self, sender=self._registration) notification_center.post_notification('SIPAccountWillRegister', sender=self.account) else: notification_center.post_notification('SIPAccountRegistrationWillRefresh', sender=self.account) try: # Lookup routes if self.account.sip.outbound_proxy is not None and self.account.sip.outbound_proxy.transport in settings.sip.transport_list: uri = SIPURI(host=self.account.sip.outbound_proxy.host, port=self.account.sip.outbound_proxy.port, parameters={'transport': self.account.sip.outbound_proxy.transport}) else: uri = SIPURI(host=self.account.id.domain) lookup = DNSLookup() try: routes = lookup.lookup_sip_proxy(uri, settings.sip.transport_list).wait() - except DNSLookupError, e: + except DNSLookupError as e: retry_after = random.uniform(self._dns_wait, 2*self._dns_wait) self._dns_wait = limit(2*self._dns_wait, max=30) raise RegistrationError('DNS lookup failed: %s' % e, retry_after=retry_after) else: self._dns_wait = 1 # Register by trying each route in turn register_timeout = time() + 30 for route in routes: remaining_time = register_timeout-time() if remaining_time > 0: try: contact_uri = self.account.contact[NoGRUU, route] except KeyError: continue contact_header = ContactHeader(contact_uri) contact_header.parameters['+sip.instance'] = '"<%s>"' % settings.instance_id if self.account.nat_traversal.use_ice: contact_header.parameters['+sip.ice'] = None route_header = RouteHeader(route.uri) try: self._registration.register(contact_header, route_header, timeout=limit(remaining_time, min=1, max=10)) except SIPCoreError: raise RegistrationError('Internal error', retry_after=5) try: while True: notification = self._data_channel.wait() if notification.name == 'SIPRegistrationDidSucceed': break if notification.name == 'SIPRegistrationDidEnd': raise RegistrationError('Registration expired', retry_after=0) # registration expired while we were trying to re-register - except SIPRegistrationDidFail, e: + except SIPRegistrationDidFail as e: notification_data = NotificationData(code=e.data.code, reason=e.data.reason, registration=self._registration, registrar=route) notification_center.post_notification('SIPAccountRegistrationGotAnswer', sender=self.account, data=notification_data) if e.data.code == 401: # Authentication failed, so retry the registration in some time raise RegistrationError('Authentication failed', retry_after=random.uniform(60, 120)) elif e.data.code == 423: # Get the value of the Min-Expires header if e.data.min_expires is not None and e.data.min_expires > self.account.sip.register_interval: refresh_interval = e.data.min_expires else: refresh_interval = None raise RegistrationError('Interval too short', retry_after=random.uniform(60, 120), refresh_interval=refresh_interval) else: # Otherwise just try the next route continue else: notification_data = NotificationData(code=notification.data.code, reason=notification.data.reason, registration=self._registration, registrar=route) notification_center.post_notification('SIPAccountRegistrationGotAnswer', sender=self.account, data=notification_data) self.registered = True # Save GRUU try: header = next(header for header in notification.data.contact_header_list if header.parameters.get('+sip.instance', '').strip('"<>') == settings.instance_id) except StopIteration: self.account.contact.public_gruu = None self.account.contact.temporary_gruu = None else: public_gruu = header.parameters.get('pub-gruu', None) temporary_gruu = header.parameters.get('temp-gruu', None) try: self.account.contact.public_gruu = SIPURI.parse(public_gruu.strip('"')) except (AttributeError, SIPCoreError): self.account.contact.public_gruu = None try: self.account.contact.temporary_gruu = SIPURI.parse(temporary_gruu.strip('"')) except (AttributeError, SIPCoreError): self.account.contact.temporary_gruu = None notification_data = NotificationData(contact_header=notification.data.contact_header, contact_header_list=notification.data.contact_header_list, expires=notification.data.expires_in, registrar=route) notification_center.post_notification('SIPAccountRegistrationDidSucceed', sender=self.account, data=notification_data) self._register_wait = 1 command.signal() break else: # There are no more routes to try, reschedule the registration retry_after = random.uniform(self._register_wait, 2*self._register_wait) self._register_wait = limit(self._register_wait*2, max=30) raise RegistrationError('No more routes to try', retry_after=retry_after) - except RegistrationError, e: + except RegistrationError as e: self.registered = False notification_center.remove_observer(self, sender=self._registration) notification_center.post_notification('SIPAccountRegistrationDidFail', sender=self.account, data=NotificationData(error=e.error, retry_after=e.retry_after)) def register(): if self.active: self._command_channel.send(Command('register', command.event, refresh_interval=e.refresh_interval)) self._registration_timer = None self._registration_timer = reactor.callLater(e.retry_after, register) self._registration = None self.account.contact.public_gruu = None self.account.contact.temporary_gruu = None def _CH_unregister(self, command): # Cancel any timer which would restart the registration process if self._registration_timer is not None and self._registration_timer.active(): self._registration_timer.cancel() self._registration_timer = None registered = self.registered self.registered = False if self._registration is not None: notification_center = NotificationCenter() if registered: self._registration.end(timeout=2) try: while True: notification = self._data_channel.wait() if notification.name == 'SIPRegistrationDidEnd': break - except (SIPRegistrationDidFail, SIPRegistrationDidNotEnd), e: + except (SIPRegistrationDidFail, SIPRegistrationDidNotEnd) as e: notification_center.post_notification('SIPAccountRegistrationDidNotEnd', sender=self.account, data=NotificationData(code=e.data.code, reason=e.data.reason, registration=self._registration)) else: notification_center.post_notification('SIPAccountRegistrationDidEnd', sender=self.account, data=NotificationData(registration=self._registration)) notification_center.remove_observer(self, sender=self._registration) self._registration = None self.account.contact.public_gruu = None self.account.contact.temporary_gruu = None command.signal() def _CH_terminate(self, command): self._CH_unregister(command) raise proc.ProcExit @run_in_twisted_thread def handle_notification(self, notification): handler = getattr(self, '_NH_%s' % notification.name, Null) handler(notification) def _NH_SIPRegistrationDidSucceed(self, notification): if notification.sender is self._registration: self._data_channel.send(notification) def _NH_SIPRegistrationDidFail(self, notification): if notification.sender is self._registration: self._data_channel.send_exception(SIPRegistrationDidFail(notification.data)) def _NH_SIPRegistrationDidEnd(self, notification): if notification.sender is self._registration: self._data_channel.send(notification) def _NH_SIPRegistrationDidNotEnd(self, notification): if notification.sender is self._registration: self._data_channel.send_exception(SIPRegistrationDidNotEnd(notification.data)) def _NH_SIPRegistrationWillExpire(self, notification): if self.active: self._command_channel.send(Command('register')) @run_in_green_thread def _NH_CFGSettingsObjectDidChange(self, notification): if not self.started: return if 'enabled' in notification.data.modified: return # global account activation is handled separately by the account itself elif 'sip.register' in notification.data.modified: if self.account.sip.register: self.activate() else: self.deactivate() elif self.active and {'__id__', 'auth.password', 'auth.username', 'nat_traversal.use_ice', 'sip.outbound_proxy', 'sip.transport_list', 'sip.register_interval'}.intersection(notification.data.modified): self._command_channel.send(Command('unregister')) self._command_channel.send(Command('register')) def _NH_NetworkConditionsDidChange(self, notification): if self.active: self._command_channel.send(Command('unregister')) self._command_channel.send(Command('register')) diff --git a/sipsimple/account/subscription.py b/sipsimple/account/subscription.py index 3ae37834..c363933a 100644 --- a/sipsimple/account/subscription.py +++ b/sipsimple/account/subscription.py @@ -1,495 +1,494 @@ """Implements the subscription handlers""" __all__ = ['Subscriber', 'MWISubscriber', 'PresenceWinfoSubscriber', 'DialogWinfoSubscriber', 'PresenceSubscriber', 'SelfPresenceSubscriber', 'DialogSubscriber'] import random from abc import ABCMeta, abstractproperty from time import time from application.notification import IObserver, NotificationCenter, NotificationData from application.python import Null, limit from eventlib import coros, proc from twisted.internet import reactor from zope.interface import implements from sipsimple.core import ContactHeader, FromHeader, Header, RouteHeader, SIPURI, Subscription, ToHeader, SIPCoreError, NoGRUU from sipsimple.configuration.settings import SIPSimpleSettings from sipsimple.lookup import DNSLookup, DNSLookupError from sipsimple.threading import run_in_twisted_thread from sipsimple.threading.green import Command, run_in_green_thread Command.register_defaults('subscribe', refresh_interval=None) class SIPSubscriptionDidFail(Exception): def __init__(self, data): self.data = data class SubscriptionError(Exception): def __init__(self, error, retry_after, refresh_interval=None): self.error = error self.retry_after = retry_after self.refresh_interval = refresh_interval class InterruptSubscription(Exception): pass class TerminateSubscription(Exception): pass class Content(object): def __init__(self, body, type): self.body = body self.type = type class SubscriberNickname(dict): def __missing__(self, name): return self.setdefault(name, name[:-10] if name.endswith('Subscriber') else name) def __get__(self, obj, objtype): return self[objtype.__name__] def __set__(self, obj, value): raise AttributeError('cannot set attribute') def __delete__(self, obj): raise AttributeError('cannot delete attribute') -class Subscriber(object): - __metaclass__ = ABCMeta +class Subscriber(object, metaclass=ABCMeta): __nickname__ = SubscriberNickname() __transports__ = frozenset(['tls', 'tcp', 'udp']) implements(IObserver) def __init__(self, account): self.account = account self.started = False self.active = False self.subscribed = False self._command_proc = None self._command_channel = coros.queue() self._data_channel = coros.queue() self._subscription = None self._subscription_proc = None self._subscription_timer = None @abstractproperty def event(self): return None @property def subscription_uri(self): return self.account.id @property def content(self): return Content(None, None) @property def extra_headers(self): return [] def start(self): if self.started: return self.started = True notification_center = NotificationCenter() notification_center.add_observer(self, sender=self) notification_center.post_notification(self.__class__.__name__ + 'WillStart', sender=self) notification_center.add_observer(self, name='NetworkConditionsDidChange') self._command_proc = proc.spawn(self._run) notification_center.post_notification(self.__class__.__name__ + 'DidStart', sender=self) notification_center.remove_observer(self, sender=self) def stop(self): if not self.started: return self.started = False self.active = False notification_center = NotificationCenter() notification_center.add_observer(self, sender=self) notification_center.post_notification(self.__class__.__name__ + 'WillEnd', sender=self) notification_center.remove_observer(self, name='NetworkConditionsDidChange') command = Command('terminate') self._command_channel.send(command) command.wait() self._command_proc = None notification_center.post_notification(self.__class__.__name__ + 'DidDeactivate', sender=self) notification_center.post_notification(self.__class__.__name__ + 'DidEnd', sender=self) notification_center.remove_observer(self, sender=self) def activate(self): if not self.started: raise RuntimeError("not started") self.active = True self._command_channel.send(Command('subscribe')) notification_center = NotificationCenter() notification_center.post_notification(self.__class__.__name__ + 'DidActivate', sender=self) def deactivate(self): if not self.started: raise RuntimeError("not started") self.active = False self._command_channel.send(Command('unsubscribe')) notification_center = NotificationCenter() notification_center.post_notification(self.__class__.__name__ + 'DidDeactivate', sender=self) def resubscribe(self): if self.active: self._command_channel.send(Command('subscribe')) def _run(self): while True: command = self._command_channel.wait() handler = getattr(self, '_CH_%s' % command.name) handler(command) def _CH_subscribe(self, command): if self._subscription_timer is not None and self._subscription_timer.active(): self._subscription_timer.cancel() self._subscription_timer = None if self._subscription_proc is not None: subscription_proc = self._subscription_proc subscription_proc.kill(InterruptSubscription) subscription_proc.wait() self._subscription_proc = proc.spawn(self._subscription_handler, command) def _CH_unsubscribe(self, command): # Cancel any timer which would restart the subscription process if self._subscription_timer is not None and self._subscription_timer.active(): self._subscription_timer.cancel() self._subscription_timer = None if self._subscription_proc is not None: subscription_proc = self._subscription_proc subscription_proc.kill(TerminateSubscription) subscription_proc.wait() self._subscription_proc = None command.signal() def _CH_terminate(self, command): self._CH_unsubscribe(command) raise proc.ProcExit def _subscription_handler(self, command): notification_center = NotificationCenter() settings = SIPSimpleSettings() subscription_uri = self.subscription_uri refresh_interval = command.refresh_interval or self.account.sip.subscribe_interval valid_transports = self.__transports__.intersection(settings.sip.transport_list) try: # Lookup routes if self.account.sip.outbound_proxy is not None and self.account.sip.outbound_proxy.transport in valid_transports: uri = SIPURI(host=self.account.sip.outbound_proxy.host, port=self.account.sip.outbound_proxy.port, parameters={'transport': self.account.sip.outbound_proxy.transport}) elif self.account.sip.always_use_my_proxy: uri = SIPURI(host=self.account.id.domain) else: uri = SIPURI(host=subscription_uri.domain) lookup = DNSLookup() try: routes = lookup.lookup_sip_proxy(uri, valid_transports).wait() - except DNSLookupError, e: + except DNSLookupError as e: raise SubscriptionError('DNS lookup failed: %s' % e, retry_after=random.uniform(15, 30)) subscription_uri = SIPURI(user=subscription_uri.username, host=subscription_uri.domain) content = self.content timeout = time() + 30 for route in routes: remaining_time = timeout - time() if remaining_time > 0: try: contact_uri = self.account.contact[NoGRUU, route] except KeyError: continue subscription = Subscription(subscription_uri, FromHeader(self.account.uri, self.account.display_name), ToHeader(subscription_uri), ContactHeader(contact_uri), self.event, RouteHeader(route.uri), credentials=self.account.credentials, refresh=refresh_interval) notification_center.add_observer(self, sender=subscription) try: subscription.subscribe(body=content.body, content_type=content.type, extra_headers=self.extra_headers, timeout=limit(remaining_time, min=1, max=5)) except SIPCoreError: notification_center.remove_observer(self, sender=subscription) raise SubscriptionError('Internal error', retry_after=5) self._subscription = subscription try: while True: notification = self._data_channel.wait() if notification.name == 'SIPSubscriptionDidStart': break - except SIPSubscriptionDidFail, e: + except SIPSubscriptionDidFail as e: notification_center.remove_observer(self, sender=subscription) self._subscription = None if e.data.code == 407: # Authentication failed, so retry the subscription in some time raise SubscriptionError('Authentication failed', retry_after=random.uniform(60, 120)) elif e.data.code == 423: # Get the value of the Min-Expires header if e.data.min_expires is not None and e.data.min_expires > self.account.sip.subscribe_interval: refresh_interval = e.data.min_expires else: refresh_interval = None raise SubscriptionError('Interval too short', retry_after=random.uniform(60, 120), refresh_interval=refresh_interval) elif e.data.code in (405, 406, 489): raise SubscriptionError('Method or event not supported', retry_after=3600) elif e.data.code == 1400: raise SubscriptionError(e.data.reason, retry_after=3600) else: # Otherwise just try the next route continue else: self.subscribed = True command.signal() break else: # There are no more routes to try, reschedule the subscription raise SubscriptionError('No more routes to try', retry_after=random.uniform(60, 180)) # At this point it is subscribed. Handle notifications and ending/failures. notification_center.post_notification(self.__nickname__ + 'SubscriptionDidStart', sender=self) try: while True: notification = self._data_channel.wait() if notification.name == 'SIPSubscriptionGotNotify': notification_center.post_notification(self.__nickname__ + 'SubscriptionGotNotify', sender=self, data=notification.data) elif notification.name == 'SIPSubscriptionDidEnd': notification_center.post_notification(self.__nickname__ + 'SubscriptionDidEnd', sender=self, data=NotificationData(originator='remote')) if self.active: self._command_channel.send(Command('subscribe')) break except SIPSubscriptionDidFail: notification_center.post_notification(self.__nickname__ + 'SubscriptionDidFail', sender=self) if self.active: self._command_channel.send(Command('subscribe')) notification_center.remove_observer(self, sender=self._subscription) - except InterruptSubscription, e: + except InterruptSubscription as e: if not self.subscribed: command.signal(e) if self._subscription is not None: notification_center.remove_observer(self, sender=self._subscription) try: self._subscription.end(timeout=2) except SIPCoreError: pass finally: notification_center.post_notification(self.__nickname__ + 'SubscriptionDidEnd', sender=self, data=NotificationData(originator='local')) - except TerminateSubscription, e: + except TerminateSubscription as e: if not self.subscribed: command.signal(e) if self._subscription is not None: try: self._subscription.end(timeout=2) except SIPCoreError: pass else: try: while True: notification = self._data_channel.wait() if notification.name == 'SIPSubscriptionDidEnd': break except SIPSubscriptionDidFail: pass finally: notification_center.remove_observer(self, sender=self._subscription) notification_center.post_notification(self.__nickname__ + 'SubscriptionDidEnd', sender=self, data=NotificationData(originator='local')) - except SubscriptionError, e: + except SubscriptionError as e: def subscribe(): if self.active: self._command_channel.send(Command('subscribe', command.event, refresh_interval=e.refresh_interval)) self._subscription_timer = None self._subscription_timer = reactor.callLater(e.retry_after, subscribe) notification_center.post_notification(self.__nickname__ + 'SubscriptionDidFail', sender=self) finally: self.subscribed = False self._subscription = None self._subscription_proc = None @run_in_twisted_thread def handle_notification(self, notification): handler = getattr(self, '_NH_%s' % notification.name, Null) handler(notification) def _NH_SIPSubscriptionDidStart(self, notification): if notification.sender is self._subscription: self._data_channel.send(notification) def _NH_SIPSubscriptionDidEnd(self, notification): if notification.sender is self._subscription: self._data_channel.send(notification) def _NH_SIPSubscriptionDidFail(self, notification): if notification.sender is self._subscription: self._data_channel.send_exception(SIPSubscriptionDidFail(notification.data)) def _NH_SIPSubscriptionGotNotify(self, notification): if notification.sender is self._subscription: self._data_channel.send(notification) def _NH_NetworkConditionsDidChange(self, notification): if self.active: self._command_channel.send(Command('subscribe')) class MWISubscriber(Subscriber): """Message Waiting Indicator subscriber""" @property def event(self): return 'message-summary' @property def subscription_uri(self): return self.account.message_summary.voicemail_uri or self.account.id def _NH_MWISubscriberWillStart(self, notification): notification.center.add_observer(self, name='CFGSettingsObjectDidChange', sender=self.account) notification.center.add_observer(self, name='CFGSettingsObjectDidChange', sender=SIPSimpleSettings()) def _NH_MWISubscriberWillEnd(self, notification): notification.center.remove_observer(self, name='CFGSettingsObjectDidChange', sender=self.account) notification.center.remove_observer(self, name='CFGSettingsObjectDidChange', sender=SIPSimpleSettings()) def _NH_MWISubscriberDidStart(self, notification): if self.account.message_summary.enabled: self.activate() @run_in_green_thread def _NH_CFGSettingsObjectDidChange(self, notification): if not self.started: return if 'enabled' in notification.data.modified: return # global account activation is handled separately by the account itself elif 'message_summary.enabled' in notification.data.modified: if self.account.message_summary.enabled: self.activate() else: self.deactivate() elif self.active and {'__id__', 'auth.password', 'auth.username', 'message_summary.voicemail_uri', 'sip.always_use_my_proxy', 'sip.outbound_proxy', 'sip.subscribe_interval', 'sip.transport_list'}.intersection(notification.data.modified): self._command_channel.send(Command('subscribe')) class AbstractPresenceSubscriber(Subscriber): """Abstract class defining behavior for all presence subscribers""" __transports__ = frozenset(['tls', 'tcp']) def _NH_AbstractPresenceSubscriberWillStart(self, notification): notification.center.add_observer(self, name='SIPAccountDidDiscoverXCAPSupport', sender=self.account) notification.center.add_observer(self, name='CFGSettingsObjectDidChange', sender=self.account) notification.center.add_observer(self, name='CFGSettingsObjectDidChange', sender=SIPSimpleSettings()) def _NH_AbstractPresenceSubscriberWillEnd(self, notification): notification.center.remove_observer(self, name='SIPAccountDidDiscoverXCAPSupport', sender=self.account) notification.center.remove_observer(self, name='CFGSettingsObjectDidChange', sender=self.account) notification.center.remove_observer(self, name='CFGSettingsObjectDidChange', sender=SIPSimpleSettings()) def _NH_AbstractPresenceSubscriberDidStart(self, notification): if self.account.presence.enabled and self.account.xcap.discovered: self.activate() def _NH_SIPAccountDidDiscoverXCAPSupport(self, notification): if self.account.presence.enabled and not self.active: self.activate() @run_in_green_thread def _NH_CFGSettingsObjectDidChange(self, notification): if not self.started or not self.account.xcap.discovered: return if 'enabled' in notification.data.modified: return # global account activation is handled separately by the account itself elif 'presence.enabled' in notification.data.modified: if self.account.presence.enabled: self.activate() else: self.deactivate() elif self.active and {'__id__', 'auth.password', 'auth.username', 'sip.always_use_my_proxy', 'sip.outbound_proxy', 'sip.subscribe_interval', 'sip.transport_list'}.intersection(notification.data.modified): self._command_channel.send(Command('subscribe')) class PresenceWinfoSubscriber(AbstractPresenceSubscriber): """Presence Watcher Info subscriber""" _NH_PresenceWinfoSubscriberWillStart = AbstractPresenceSubscriber._NH_AbstractPresenceSubscriberWillStart _NH_PresenceWinfoSubscriberWillEnd = AbstractPresenceSubscriber._NH_AbstractPresenceSubscriberWillEnd _NH_PresenceWinfoSubscriberDidStart = AbstractPresenceSubscriber._NH_AbstractPresenceSubscriberDidStart @property def event(self): return 'presence.winfo' class DialogWinfoSubscriber(AbstractPresenceSubscriber): """Dialog Watcher Info subscriber""" _NH_DialogWinfoSubscriberWillStart = AbstractPresenceSubscriber._NH_AbstractPresenceSubscriberWillStart _NH_DialogWinfoSubscriberWillEnd = AbstractPresenceSubscriber._NH_AbstractPresenceSubscriberWillEnd _NH_DialogWinfoSubscriberDidStart = AbstractPresenceSubscriber._NH_AbstractPresenceSubscriberDidStart @property def event(self): return 'dialog.winfo' class PresenceSubscriber(AbstractPresenceSubscriber): """Presence subscriber""" _NH_PresenceSubscriberWillStart = AbstractPresenceSubscriber._NH_AbstractPresenceSubscriberWillStart _NH_PresenceSubscriberWillEnd = AbstractPresenceSubscriber._NH_AbstractPresenceSubscriberWillEnd _NH_PresenceSubscriberDidStart = AbstractPresenceSubscriber._NH_AbstractPresenceSubscriberDidStart @property def event(self): return 'presence' @property def subscription_uri(self): return self.account.xcap_manager.rls_presence_uri @property def extra_headers(self): return [Header('Supported', 'eventlist')] class SelfPresenceSubscriber(AbstractPresenceSubscriber): """Self presence subscriber""" _NH_SelfPresenceSubscriberWillStart = AbstractPresenceSubscriber._NH_AbstractPresenceSubscriberWillStart _NH_SelfPresenceSubscriberWillEnd = AbstractPresenceSubscriber._NH_AbstractPresenceSubscriberWillEnd _NH_SelfPresenceSubscriberDidStart = AbstractPresenceSubscriber._NH_AbstractPresenceSubscriberDidStart @property def event(self): return 'presence' @property def subscription_uri(self): return self.account.id class DialogSubscriber(AbstractPresenceSubscriber): """Dialog subscriber""" _NH_DialogSubscriberWillStart = AbstractPresenceSubscriber._NH_AbstractPresenceSubscriberWillStart _NH_DialogSubscriberWillEnd = AbstractPresenceSubscriber._NH_AbstractPresenceSubscriberWillEnd _NH_DialogSubscriberDidStart = AbstractPresenceSubscriber._NH_AbstractPresenceSubscriberDidStart @property def event(self): return 'dialog' @property def subscription_uri(self): return self.account.xcap_manager.rls_dialog_uri @property def extra_headers(self): return [Header('Supported', 'eventlist')] diff --git a/sipsimple/account/xcap/__init__.py b/sipsimple/account/xcap/__init__.py index a45e11dd..d82cd2d2 100644 --- a/sipsimple/account/xcap/__init__.py +++ b/sipsimple/account/xcap/__init__.py @@ -1,1765 +1,1765 @@ """High-level management of XCAP documents based on OMA specifications""" __all__ = ['Group', 'Contact', 'ContactURI', 'EventHandling', 'Policy', 'Icon', 'OfflineStatus', 'XCAPManager', 'XCAPTransaction'] import base64 -import cPickle +import pickle import os import random import socket import weakref -from cStringIO import StringIO +from io import StringIO from collections import OrderedDict from datetime import datetime from itertools import chain from operator import attrgetter -from urllib2 import URLError +from urllib.error import URLError from application.notification import IObserver, NotificationCenter, NotificationData from application.python import Null from application.python.decorator import execute_once from eventlib import api, coros, proc from eventlib.green.httplib import BadStatusLine from twisted.internet.error import ConnectionLost from xcaplib.green import XCAPClient from xcaplib.error import HTTPError from zope.interface import implements from sipsimple import log from sipsimple.account.subscription import Subscriber, Content from sipsimple.account.xcap.storage import IXCAPStorage, XCAPStorageError from sipsimple.configuration.datatypes import SIPAddress from sipsimple.configuration.settings import SIPSimpleSettings from sipsimple.lookup import DNSLookup, DNSLookupError from sipsimple.payloads import ParserError, IterateTypes, IterateIDs, IterateItems, All from sipsimple.payloads import addressbook, commonpolicy, dialogrules, omapolicy, pidf, prescontent, presrules, resourcelists, rlsservices, xcapcaps, xcapdiff from sipsimple.payloads import rpid; del rpid # needs to be imported to register its namespace from sipsimple.threading import run_in_twisted_thread from sipsimple.threading.green import Command, Worker, run_in_green_thread class XCAPError(Exception): pass class FetchRequiredError(XCAPError): pass class Document(object): name = None application = None payload_type = None default_namespace = None global_tree = None filename = None cached = True def __init__(self, manager): self.manager = weakref.proxy(manager) self.content = None self.etag = None self.fetch_time = datetime.fromtimestamp(0) self.update_time = datetime.fromtimestamp(0) self.dirty = False self.supported = False - def __nonzero__(self): + def __bool__(self): return self.content is not None @property def dirty(self): return self.__dict__['dirty'] or (self.content is not None and self.content.__dirty__) @dirty.setter def dirty(self, dirty): if self.content is not None and not dirty: self.content.__dirty__ = dirty self.__dict__['dirty'] = dirty @property def relative_url(self): return self.url[len(self.manager.xcap_root):].lstrip('/') @property def url(self): return self.manager.client.get_url(self.application, None, globaltree=self.global_tree, filename=self.filename) def load_from_cache(self): if not self.cached: return try: document = StringIO(self.manager.storage.load(self.name)) self.etag = document.readline().strip() or None self.content = self.payload_type.parse(document) self.__dict__['dirty'] = False except (XCAPStorageError, ParserError): self.etag = None self.content = None self.dirty = False self.fetch_time = datetime.utcnow() def initialize(self, server_caps): self.supported = self.application in server_caps.auids if not self.supported: self.reset() def reset(self): if self.cached and self.content is not None: try: self.manager.storage.delete(self.name) except XCAPStorageError: pass self.content = None self.etag = None self.dirty = False def fetch(self): try: document = self.manager.client.get(self.application, etagnot=self.etag, globaltree=self.global_tree, headers={'Accept': self.payload_type.content_type}, filename=self.filename) self.content = self.payload_type.parse(document) self.etag = document.etag self.__dict__['dirty'] = False - except (BadStatusLine, ConnectionLost, URLError, socket.error), e: + except (BadStatusLine, ConnectionLost, URLError, socket.error) as e: raise XCAPError("failed to fetch %s document: %s" % (self.name, e)) - except HTTPError, e: + except HTTPError as e: if e.status == 404: # Not Found if self.content is not None: self.reset() self.fetch_time = datetime.utcnow() elif e.status != 304: # Other than Not Modified: raise XCAPError("failed to fetch %s document: %s" % (self.name, e)) - except ParserError, e: + except ParserError as e: raise XCAPError("failed to parse %s document: %s" % (self.name, e)) else: self.fetch_time = datetime.utcnow() if self.cached: try: self.manager.storage.save(self.name, self.etag + os.linesep + document) except XCAPStorageError: pass def update(self): if not self.dirty: return data = self.content.toxml() if self.content is not None else None try: kw = dict(etag=self.etag) if self.etag is not None else dict(etagnot='*') if data is not None: response = self.manager.client.put(self.application, data, globaltree=self.global_tree, filename=self.filename, headers={'Content-Type': self.payload_type.content_type}, **kw) else: response = self.manager.client.delete(self.application, data, globaltree=self.global_tree, filename=self.filename, **kw) - except (BadStatusLine, ConnectionLost, URLError), e: + except (BadStatusLine, ConnectionLost, URLError) as e: raise XCAPError("failed to update %s document: %s" % (self.name, e)) - except HTTPError, e: + except HTTPError as e: if e.status == 412: # Precondition Failed raise FetchRequiredError("document %s was modified externally" % self.name) elif e.status == 404 and data is None: # attempted to delete a document that did't exist in the first place pass else: raise XCAPError("failed to update %s document: %s" % (self.name, e)) self.etag = response.etag if data is not None else None self.dirty = False self.update_time = datetime.utcnow() if self.cached: try: if data is not None: self.manager.storage.save(self.name, self.etag + os.linesep + data) else: self.manager.storage.delete(self.name) except XCAPStorageError: pass class DialogRulesDocument(Document): name = 'dialog-rules' application = 'org.openxcap.dialog-rules' payload_type = dialogrules.DialogRulesDocument default_namespace = dialogrules.namespace global_tree = False filename = 'index' class PresRulesDocument(Document): name = 'pres-rules' application = 'org.openmobilealliance.pres-rules' payload_type = presrules.PresRulesDocument default_namespace = presrules.namespace global_tree = False filename = 'index' class ResourceListsDocument(Document): name = 'resource-lists' application = 'resource-lists' payload_type = resourcelists.ResourceListsDocument default_namespace = resourcelists.namespace global_tree = False filename = 'index' def update(self): if self.content is not None: sipsimple_addressbook = self.content['sipsimple_addressbook'] groups = ItemCollection(sipsimple_addressbook[addressbook.Group, IterateItems]) contacts = ItemCollection(sipsimple_addressbook[addressbook.Contact, IterateItems]) policies = ItemCollection(sipsimple_addressbook[addressbook.Policy, IterateItems]) for group, missing_id in ((group, missing_id) for group in groups for missing_id in [id for id in group.contacts if id not in contacts]): group.contacts.remove(missing_id) if any(item.__dirty__ for item in chain(contacts, policies)): oma_grantedcontacts = self.content['oma_grantedcontacts'] oma_blockedcontacts = self.content['oma_blockedcontacts'] dialog_grantedcontacts = self.content['dialog_grantedcontacts'] dialog_blockedcontacts = self.content['dialog_blockedcontacts'] sipsimple_presence_rls = self.content['sipsimple_presence_rls'] sipsimple_dialog_rls = self.content['sipsimple_dialog_rls'] all_contact_uris = set(uri.uri for contact in contacts for uri in contact.uris) contact_allow_presence_uris = set(uri.uri for contact in contacts for uri in contact.uris if contact.presence.policy=='allow') contact_block_presence_uris = set(uri.uri for contact in contacts for uri in contact.uris if contact.presence.policy=='block') contact_allow_dialog_uris = set(uri.uri for contact in contacts for uri in contact.uris if contact.dialog.policy=='allow') contact_block_dialog_uris = set(uri.uri for contact in contacts for uri in contact.uris if contact.dialog.policy=='block') contact_subscribe_presence_uris = set(uri.uri for contact in contacts for uri in contact.uris if contact.presence.subscribe==True) contact_subscribe_dialog_uris = set(uri.uri for contact in contacts for uri in contact.uris if contact.dialog.subscribe==True) policy_allow_presence_uris = set(policy.uri for policy in policies if policy.presence.policy=='allow') policy_block_presence_uris = set(policy.uri for policy in policies if policy.presence.policy=='block') policy_allow_dialog_uris = set(policy.uri for policy in policies if policy.dialog.policy=='allow') policy_block_dialog_uris = set(policy.uri for policy in policies if policy.dialog.policy=='block') policy_subscribe_presence_uris = set(policy.uri for policy in policies if policy.presence.subscribe==True) policy_subscribe_dialog_uris = set(policy.uri for policy in policies if policy.dialog.subscribe==True) allowed_presence_uris = contact_allow_presence_uris - contact_block_presence_uris | policy_allow_presence_uris - policy_block_presence_uris - all_contact_uris blocked_presence_uris = contact_block_presence_uris | policy_block_presence_uris - all_contact_uris allowed_dialog_uris = contact_allow_dialog_uris - contact_block_dialog_uris | policy_allow_dialog_uris - policy_block_dialog_uris - all_contact_uris blocked_dialog_uris = contact_block_dialog_uris | policy_block_dialog_uris - all_contact_uris subscribe_presence_uris = contact_subscribe_presence_uris | policy_subscribe_presence_uris - all_contact_uris subscribe_dialog_uris = contact_subscribe_dialog_uris | policy_subscribe_dialog_uris - all_contact_uris if allowed_presence_uris != set(entry.uri for entry in oma_grantedcontacts): oma_grantedcontacts.clear() oma_grantedcontacts.update(resourcelists.Entry(uri) for uri in allowed_presence_uris) if blocked_presence_uris != set(entry.uri for entry in oma_blockedcontacts): oma_blockedcontacts.clear() oma_blockedcontacts.update(resourcelists.Entry(uri) for uri in blocked_presence_uris) if allowed_dialog_uris != set(entry.uri for entry in dialog_grantedcontacts): dialog_grantedcontacts.clear() dialog_grantedcontacts.update(resourcelists.Entry(uri) for uri in allowed_dialog_uris) if blocked_dialog_uris != set(entry.uri for entry in dialog_blockedcontacts): dialog_blockedcontacts.clear() dialog_blockedcontacts.update(resourcelists.Entry(uri) for uri in blocked_dialog_uris) if subscribe_presence_uris != set(entry.uri for entry in sipsimple_presence_rls): sipsimple_presence_rls.clear() sipsimple_presence_rls.update(resourcelists.Entry(uri) for uri in subscribe_presence_uris) if subscribe_dialog_uris != set(entry.uri for entry in sipsimple_dialog_rls): sipsimple_dialog_rls.clear() sipsimple_dialog_rls.update(resourcelists.Entry(uri) for uri in subscribe_dialog_uris) super(ResourceListsDocument, self).update() class RLSServicesDocument(Document): name = 'rls-services' application = 'rls-services' payload_type = rlsservices.RLSServicesDocument default_namespace = rlsservices.namespace global_tree = False filename = 'index' class XCAPCapsDocument(Document): name = 'xcap-caps' application = 'xcap-caps' payload_type = xcapcaps.XCAPCapabilitiesDocument default_namespace = xcapcaps.namespace global_tree = True filename = 'index' cached = False def initialize(self): self.supported = True class StatusIconDocument(Document): name = 'status-icon' application = 'org.openmobilealliance.pres-content' payload_type = prescontent.PresenceContentDocument default_namespace = prescontent.namespace global_tree = False filename = 'oma_status-icon/index' class PIDFManipulationDocument(Document): name = 'pidf-manipulation' application = 'pidf-manipulation' payload_type = pidf.PIDFDocument default_namespace = pidf.pidf_namespace global_tree = False filename = 'index' class ItemCollection(object): def __init__(self, items): self.items = OrderedDict((item.id, item) for item in items) def __getitem__(self, key): return self.items[key] def __contains__(self, key): return key in self.items def __iter__(self): - return self.items.itervalues() + return iter(list(self.items.values())) def __reversed__(self): return (self[id] for id in reversed(self.items)) def __len__(self): return len(self.items) def __eq__(self, other): if isinstance(other, ItemCollection): return self.items == other.items return NotImplemented def __ne__(self, other): equal = self.__eq__(other) return NotImplemented if equal is NotImplemented else not equal def __repr__(self): - return "%s(%r)" % (self.__class__.__name__, self.items.values()) + return "%s(%r)" % (self.__class__.__name__, list(self.items.values())) def ids(self): - return self.items.keys() + return list(self.items.keys()) def iterids(self): - return self.items.iterkeys() + return iter(list(self.items.keys())) def get(self, key, default=None): return self.items.get(key, default) def add(self, item): self.items[item.id] = item def remove(self, item): del self.items[item.id] class ContactList(ItemCollection): pass class ContactURIList(ItemCollection): def __init__(self, items, default=None): super(ContactURIList, self).__init__(items) self.default = default def __eq__(self, other): if isinstance(other, ContactURIList): return self.items == other.items and self.default == other.default return NotImplemented def __repr__(self): - return "%s(%r, default=%r)" % (self.__class__.__name__, self.items.values(), self.default) + return "%s(%r, default=%r)" % (self.__class__.__name__, list(self.items.values()), self.default) class Group(object): def __init__(self, id, name, contacts, **attributes): self.id = id self.name = name self.contacts = contacts self.attributes = attributes def __eq__(self, other): if isinstance(other, Group): return self is other or (self.id == other.id and self.name == other.name and self.contacts.ids() == other.contacts.ids() and self.attributes == other.attributes) return NotImplemented def __ne__(self, other): equal = self.__eq__(other) return NotImplemented if equal is NotImplemented else not equal def __setattr__(self, name, value): if name == 'contacts' and not isinstance(value, ContactList): value = ContactList(value) object.__setattr__(self, name, value) class ContactURI(object): def __init__(self, id, uri, type, **attributes): self.id = id self.uri = uri self.type = type self.attributes = attributes def __eq__(self, other): if isinstance(other, ContactURI): return self is other or (self.id == other.id and self.uri == other.uri and self.type == other.type and self.attributes == other.attributes) return NotImplemented def __ne__(self, other): equal = self.__eq__(other) return NotImplemented if equal is NotImplemented else not equal class EventHandling(object): def __init__(self, policy, subscribe): self.policy = policy self.subscribe = subscribe def __eq__(self, other): if isinstance(other, EventHandling): return self is other or (self.policy == other.policy and self.subscribe == other.subscribe) return NotImplemented def __ne__(self, other): equal = self.__eq__(other) return NotImplemented if equal is NotImplemented else not equal def __repr__(self): return '%s(%r, %r)' % (self.__class__.__name__, self.policy, self.subscribe) class Contact(object): def __init__(self, id, name, uris, presence_handling=None, dialog_handling=None, **attributes): self.id = id self.name = name self.uris = uris self.dialog = dialog_handling or EventHandling(policy='default', subscribe=False) self.presence = presence_handling or EventHandling(policy='default', subscribe=False) self.attributes = attributes def __eq__(self, other): if isinstance(other, Contact): return self is other or (self.id == other.id and self.name == other.name and self.uris == other.uris and self.dialog == other.dialog and self.presence == other.presence and self.attributes == other.attributes) return NotImplemented def __ne__(self, other): equal = self.__eq__(other) return NotImplemented if equal is NotImplemented else not equal def __setattr__(self, name, value): if name == 'uris' and not isinstance(value, ContactURIList): value = ContactURIList(value) object.__setattr__(self, name, value) class Policy(object): def __init__(self, id, uri, name, presence_handling=None, dialog_handling=None, **attributes): self.id = id self.uri = uri self.name = name self.dialog = dialog_handling or EventHandling(policy='default', subscribe=False) self.presence = presence_handling or EventHandling(policy='default', subscribe=False) self.attributes = attributes def __eq__(self, other): if isinstance(other, Policy): return self is other or (self.id == other.id and self.uri == other.uri and self.name == other.name and self.dialog == other.dialog and self.presence == other.presence and self.attributes == other.attributes) return NotImplemented def __ne__(self, other): equal = self.__eq__(other) return NotImplemented if equal is NotImplemented else not equal class Addressbook(object): def __init__(self, contacts, groups, policies): self.contacts = contacts self.groups = groups self.policies = policies def __eq__(self, other): if isinstance(other, Addressbook): return self is other or (self.contacts == other.contacts and self.groups == other.groups and self.policies == other.policies) return NotImplemented def __ne__(self, other): equal = self.__eq__(other) return NotImplemented if equal is NotImplemented else not equal @classmethod def from_payload(cls, payload): def payload_to_contact(payload): uris = ContactURIList((ContactURI(uri.id, uri.uri, uri.type, **(uri.attributes or {})) for uri in payload.uris), default=payload.uris.default) presence_handling = EventHandling(payload.presence.policy.value, payload.presence.subscribe.value) dialog_handling = EventHandling(payload.dialog.policy.value, payload.dialog.subscribe.value) return Contact(payload.id, payload.name.value, uris, presence_handling, dialog_handling, **(payload.attributes or {})) def payload_to_group(payload): return Group(payload.id, payload.name.value, [contacts[contact_id] for contact_id in payload.contacts], **(payload.attributes or {})) def payload_to_policy(payload): presence_handling = EventHandling(payload.presence.policy.value, payload.presence.subscribe.value) dialog_handling = EventHandling(payload.dialog.policy.value, payload.dialog.subscribe.value) return Policy(payload.id, payload.uri, payload.name.value, presence_handling, dialog_handling, **(payload.attributes or {})) contacts = ItemCollection(payload_to_contact(item) for item in payload[addressbook.Contact, IterateItems]) groups = ItemCollection(payload_to_group(item) for item in payload[addressbook.Group, IterateItems]) policies = ItemCollection(payload_to_policy(item) for item in payload[addressbook.Policy, IterateItems]) return cls(contacts, groups, policies) class PresenceRules(object): def __init__(self, default_policy): self.default_policy = default_policy def __eq__(self, other): if isinstance(other, PresenceRules): return self is other or (self.default_policy == other.default_policy) return NotImplemented def __ne__(self, other): equal = self.__eq__(other) return NotImplemented if equal is NotImplemented else not equal @classmethod def from_payload(cls, default_rule): default_policy = next(item for item in default_rule.actions if isinstance(item, presrules.SubHandling)).value return cls(default_policy) class DialogRules(object): def __init__(self, default_policy): self.default_policy = default_policy def __eq__(self, other): if isinstance(other, DialogRules): return self is other or (self.default_policy == other.default_policy) return NotImplemented def __ne__(self, other): equal = self.__eq__(other) return NotImplemented if equal is NotImplemented else not equal @classmethod def from_payload(cls, default_rule): if default_rule is not None: default_policy = next(item for item in default_rule.actions if isinstance(item, dialogrules.SubHandling)).value else: default_policy = None return cls(default_policy) class Icon(object): __mimetypes__ = ('image/jpeg', 'image/png', 'image/gif') def __init__(self, data, mime_type, description=None): self.data = data self.mime_type = mime_type self.description = description self.url = None self.etag = None def __eq__(self, other): if isinstance(other, Icon): return self is other or (self.data == other.data and self.mime_type == other.mime_type and self.description == other.description) return NotImplemented def __ne__(self, other): equal = self.__eq__(other) return NotImplemented if equal is NotImplemented else not equal def __setattr__(self, name, value): if name == 'mime_type' and value not in self.__mimetypes__: raise ValueError("invalid mime type: '%s'. Should be one of: %s" % (value, ', '.join(self.__mimetypes__))) object.__setattr__(self, name, value) @classmethod def from_payload(cls, payload): try: data = base64.decodestring(payload.data.value) except Exception: return None else: description = payload.description.value if payload.description else None return cls(data, payload.mime_type.value, description) class OfflineStatus(object): __slots__ = ('pidf',) def __init__(self, pidf): self.pidf = pidf def __setattr__(self, name, value): if name == 'pidf' and not isinstance(value, pidf.PIDF): raise ValueError("pidf must be a PIDF payload") object.__setattr__(self, name, value) def __eq__(self, other): if isinstance(other, OfflineStatus): return self is other or (self.pidf == other.pidf) return NotImplemented def __ne__(self, other): equal = self.__eq__(other) return NotImplemented if equal is NotImplemented else not equal def __getstate__(self): return {'pidf': self.pidf.toxml()} def __setstate__(self, state): self.pidf = pidf.PIDFDocument.parse(state['pidf']) class Operation(object): __params__ = () def __init__(self, **params): - for name, value in params.iteritems(): + for name, value in list(params.items()): setattr(self, name, value) for param in set(self.__params__).difference(params): raise ValueError("missing operation parameter: '%s'" % param) self.applied = False self.timestamp = datetime.utcnow() class NormalizeOperation(Operation): __params__ = () class AddContactOperation(Operation): __params__ = ('contact',) class UpdateContactOperation(Operation): __params__ = ('contact', 'attributes') class RemoveContactOperation(Operation): __params__ = ('contact',) class AddContactURIOperation(Operation): __params__ = ('contact', 'uri') class UpdateContactURIOperation(Operation): __params__ = ('contact', 'uri', 'attributes') class RemoveContactURIOperation(Operation): __params__ = ('contact', 'uri') class AddGroupOperation(Operation): __params__ = ('group',) class UpdateGroupOperation(Operation): __params__ = ('group', 'attributes') class RemoveGroupOperation(Operation): __params__ = ('group',) class AddGroupMemberOperation(Operation): __params__ = ('group', 'contact') class RemoveGroupMemberOperation(Operation): __params__ = ('group', 'contact') class AddPolicyOperation(Operation): __params__ = ('policy',) class UpdatePolicyOperation(Operation): __params__ = ('policy', 'attributes') class RemovePolicyOperation(Operation): __params__ = ('policy',) class SetDefaultPresencePolicyOperation(Operation): __params__ = ('policy',) class SetDefaultDialogPolicyOperation(Operation): __params__ = ('policy',) class SetStatusIconOperation(Operation): __params__ = ('icon',) class SetOfflineStatusOperation(Operation): __params__ = ('status',) class XCAPSubscriber(Subscriber): __transports__ = frozenset(['tls', 'tcp']) @property def event(self): return 'xcap-diff' @property def content(self): rlist = resourcelists.List() for document in (doc for doc in self.account.xcap_manager.documents if doc.supported): rlist.add(resourcelists.Entry(document.relative_url)) return Content(resourcelists.ResourceLists([rlist]).toxml(), resourcelists.ResourceListsDocument.content_type) class XCAPManager(object): implements(IObserver) def __init__(self, account): from sipsimple.application import SIPApplication if SIPApplication.storage is None: raise RuntimeError("SIPApplication.storage must be defined before instantiating XCAPManager") storage = SIPApplication.storage.xcap_storage_factory(account.id) if not IXCAPStorage.providedBy(storage): raise TypeError("storage must implement the IXCAPStorage interface") self.account = account self.storage = storage self.storage_factory = SIPApplication.storage.xcap_storage_factory self.client = None self.command_proc = None self.command_channel = coros.queue() self.last_fetch_time = datetime.fromtimestamp(0) self.last_update_time = datetime.fromtimestamp(0) self.not_executed_fetch = None self.state = 'stopped' self.timer = None self.transaction_level = 0 self.xcap_subscriber = None self.server_caps = XCAPCapsDocument(self) self.dialog_rules = DialogRulesDocument(self) self.pidf_manipulation = PIDFManipulationDocument(self) self.pres_rules = PresRulesDocument(self) self.resource_lists = ResourceListsDocument(self) self.rls_services = RLSServicesDocument(self) self.status_icon = StatusIconDocument(self) for document in self.documents: document.load_from_cache() try: journal = self.storage.load('journal') except XCAPStorageError: self.journal = [] else: try: - self.journal = cPickle.loads(journal) + self.journal = pickle.loads(journal) except Exception: self.journal = [] for operation in self.journal: operation.applied = False notification_center = NotificationCenter() notification_center.add_observer(self, sender=account, name='CFGSettingsObjectDidChange') notification_center.add_observer(self, sender=account, name='CFGSettingsObjectWasDeleted') @property def state(self): return self.__dict__['state'] @state.setter def state(self, value): old_value = self.__dict__.get('state', Null) self.__dict__['state'] = value if old_value != value and old_value is not Null: notification_center = NotificationCenter() notification_center.post_notification('XCAPManagerDidChangeState', sender=self, data=NotificationData(prev_state=old_value, state=value)) @property def documents(self): return [self.resource_lists, self.rls_services, self.pres_rules, self.dialog_rules, self.pidf_manipulation, self.status_icon] @property def document_names(self): return [document.name for document in self.documents] @property def xcap_root(self): return getattr(self.client, 'root', None) @property def rls_presence_uri(self): return SIPAddress('%s+presence@%s' % (self.account.id.username, self.account.id.domain)) @property def rls_dialog_uri(self): return SIPAddress('%s+dialog@%s' % (self.account.id.username, self.account.id.domain)) @execute_once def init(self): """ Initializes the XCAP manager before it can be started. Needs to be called before any other method and in a green thread. """ self.command_proc = proc.spawn(self._run) def start(self): """ Starts the XCAP manager. This method needs to be called in a green thread. """ command = Command('start') self.command_channel.send(command) command.wait() def stop(self): """ Stops the XCAP manager. This method blocks until all the operations are stopped and needs to be called in a green thread. """ command = Command('stop') self.command_channel.send(command) command.wait() def transaction(self): return XCAPTransaction(self) @run_in_twisted_thread def start_transaction(self): self.transaction_level += 1 @run_in_twisted_thread def commit_transaction(self): if self.transaction_level == 0: return self.transaction_level -= 1 if self.transaction_level == 0 and self.journal: self._save_journal() self.command_channel.send(Command('update')) def add_contact(self, contact): self._schedule_operation(AddContactOperation(contact=contact)) def update_contact(self, contact, attributes): self._schedule_operation(UpdateContactOperation(contact=contact, attributes=attributes)) def remove_contact(self, contact): self._schedule_operation(RemoveContactOperation(contact=contact)) def add_contact_uri(self, contact, uri): self._schedule_operation(AddContactURIOperation(contact=contact, uri=uri)) def update_contact_uri(self, contact, uri, attributes): self._schedule_operation(UpdateContactURIOperation(contact=contact, uri=uri, attributes=attributes)) def remove_contact_uri(self, contact, uri): self._schedule_operation(RemoveContactURIOperation(contact=contact, uri=uri)) def add_group(self, group): self._schedule_operation(AddGroupOperation(group=group)) def update_group(self, group, attributes): self._schedule_operation(UpdateGroupOperation(group=group, attributes=attributes)) def remove_group(self, group): self._schedule_operation(RemoveGroupOperation(group=group)) def add_group_member(self, group, contact): self._schedule_operation(AddGroupMemberOperation(group=group, contact=contact)) def remove_group_member(self, group, contact): self._schedule_operation(RemoveGroupMemberOperation(group=group, contact=contact)) def add_policy(self, policy): self._schedule_operation(AddPolicyOperation(policy=policy)) def update_policy(self, policy, attributes): self._schedule_operation(UpdatePolicyOperation(policy=policy, attributes=attributes)) def remove_policy(self, policy): self._schedule_operation(RemovePolicyOperation(policy=policy)) def set_default_presence_policy(self, policy): self._schedule_operation(SetDefaultPresencePolicyOperation(policy=presrules.SubHandlingValue(policy))) def set_default_dialog_policy(self, policy): self._schedule_operation(SetDefaultDialogPolicyOperation(policy=dialogrules.SubHandlingValue(policy))) def set_status_icon(self, icon): self._schedule_operation(SetStatusIconOperation(icon=icon)) def set_offline_status(self, status): self._schedule_operation(SetOfflineStatusOperation(status=status)) @run_in_twisted_thread def _schedule_operation(self, operation): self.journal.append(operation) if self.transaction_level == 0: self._save_journal() self.command_channel.send(Command('update')) def _run(self): while True: command = self.command_channel.wait() try: handler = getattr(self, '_CH_%s' % command.name) handler(command) except: self.command_proc = None raise # Command handlers # def _CH_start(self, command): if self.state != 'stopped': command.signal() return self.state = 'initializing' self.xcap_subscriber = XCAPSubscriber(self.account) notification_center = NotificationCenter() notification_center.post_notification('XCAPManagerWillStart', sender=self) notification_center.add_observer(self, sender=self.xcap_subscriber) notification_center.add_observer(self, sender=SIPSimpleSettings(), name='CFGSettingsObjectDidChange') self.xcap_subscriber.start() self.command_channel.send(Command('initialize')) notification_center.post_notification('XCAPManagerDidStart', sender=self) command.signal() def _CH_stop(self, command): if self.state in ('stopped', 'terminated'): command.signal() return notification_center = NotificationCenter() notification_center.post_notification('XCAPManagerWillEnd', sender=self) notification_center.remove_observer(self, sender=self.xcap_subscriber) notification_center.remove_observer(self, sender=SIPSimpleSettings(), name='CFGSettingsObjectDidChange') if self.timer is not None and self.timer.active(): self.timer.cancel() self.timer = None self.xcap_subscriber.stop() self.xcap_subscriber = None self.client = None self.state = 'stopped' self._save_journal() notification_center.post_notification('XCAPManagerDidEnd', sender=self) command.signal() def _CH_cleanup(self, command): if self.state != 'stopped': command.signal() return try: self.storage.purge() except XCAPStorageError: pass self.journal = [] self.state = 'terminated' command.signal() raise proc.ProcExit def _CH_initialize(self, command): self.state = 'initializing' if self.timer is not None and self.timer.active(): self.timer.cancel() self.timer = None if self.account.xcap.xcap_root: self.client = XCAPClient(self.account.xcap.xcap_root, self.account.id, password=self.account.auth.password) else: try: lookup = DNSLookup() xcap_root = random.choice(lookup.lookup_xcap_server(self.account.uri).wait()) except DNSLookupError: self.timer = self._schedule_command(60, Command('initialize', command.event)) return else: self.client = XCAPClient(xcap_root, self.account.id, password=self.account.auth.password) try: self.server_caps.fetch() except XCAPError: self.timer = self._schedule_command(60, Command('initialize', command.event)) return else: if self.server_caps.content is None: # XCAP server must always return some content for xcap-caps self.timer = self._schedule_command(60, Command('initialize', command.event)) return if not set(self.server_caps.content.auids).issuperset(('resource-lists', 'rls-services', 'org.openmobilealliance.pres-rules')): # Server must support at least resource-lists, rls-services and org.openmobilealliance.pres-rules self.timer = self._schedule_command(3600, Command('initialize', command.event)) return self.server_caps.initialize() for document in self.documents: document.initialize(self.server_caps.content) notification_center = NotificationCenter() notification_center.post_notification('XCAPManagerDidDiscoverServerCapabilities', sender=self, data=NotificationData(auids=self.server_caps.content.auids)) self.state = 'fetching' self.command_channel.send(Command('fetch', documents=set(self.document_names))) self.xcap_subscriber.activate() def _CH_reload(self, command): if self.state == 'terminated': command.signal() return if '__id__' in command.modified: try: self.storage.purge() except XCAPStorageError: pass self.storage = self.storage_factory(self.account.id) self.journal = [] self._save_journal() if {'__id__', 'xcap.xcap_root'}.intersection(command.modified): for document in self.documents: document.reset() if self.state == 'stopped': command.signal() return if {'__id__', 'auth.username', 'auth.password', 'xcap.xcap_root'}.intersection(command.modified): self.state = 'initializing' self.command_channel.send(Command('initialize')) else: self.xcap_subscriber.resubscribe() command.signal() def _CH_fetch(self, command): if self.state not in ('insync', 'fetching'): if self.not_executed_fetch is not None: command.documents.update(self.not_executed_fetch.documents) self.not_executed_fetch = command return if self.not_executed_fetch is not None: command.documents.update(self.not_executed_fetch.documents) self.not_executed_fetch = None self.state = 'fetching' if self.timer is not None and self.timer.active(): command.documents.update(self.timer.command.documents) self.timer.cancel() self.timer = None try: self._fetch_documents(command.documents) except XCAPError: self.timer = self._schedule_command(60, Command('fetch', command.event, documents=command.documents)) return if not self.journal and self.last_fetch_time > datetime.fromtimestamp(0) and all(doc.fetch_time < command.timestamp for doc in self.documents): self.last_fetch_time = datetime.utcnow() self.state = 'insync' return else: self.last_fetch_time = datetime.utcnow() self.state = 'updating' if not self.journal or type(self.journal[0]) is not NormalizeOperation: self.journal.insert(0, NormalizeOperation()) self.command_channel.send(Command('update', command.event)) def _CH_update(self, command): if self.state not in ('insync', 'updating'): return if self.transaction_level != 0: return self.state = 'updating' if self.timer is not None and self.timer.active(): self.timer.cancel() self.timer = None journal = self.journal[:] for operation in (operation for operation in journal if not operation.applied): handler = getattr(self, '_OH_%s' % operation.__class__.__name__) try: handler(operation) except Exception: # Error while applying operation, needs to be logged -Luci log.exception() operation.applied = True api.sleep(0) # Operations are quite CPU intensive try: for document in (doc for doc in self.documents if doc.dirty and doc.supported): document.update() except FetchRequiredError: for document in (doc for doc in self.documents if doc.dirty and doc.supported): document.reset() for operation in journal: operation.applied = False self.state = 'fetching' self.command_channel.send(Command('fetch', documents=set(self.document_names))) # Try to fetch them all just in case except XCAPError: self.timer = self._schedule_command(60, Command('update')) else: del self.journal[:len(journal)] if not self.journal: self.state = 'insync' if any(max(doc.update_time, doc.fetch_time) > self.last_update_time for doc in self.documents): self._load_data() self.last_update_time = datetime.utcnow() command.signal() if self.not_executed_fetch is not None: self.command_channel.send(self.not_executed_fetch) self.not_executed_fetch = None self._save_journal() # Operation handlers # def _OH_NormalizeOperation(self, operation): # Normalize resource-lists # if self.resource_lists.content is None: self.resource_lists.content = resourcelists.ResourceLists() resource_lists = self.resource_lists.content try: oma_buddylist = resource_lists['oma_buddylist'] except KeyError: oma_buddylist = resourcelists.List(name='oma_buddylist') resource_lists.add(oma_buddylist) try: oma_grantedcontacts = resource_lists['oma_grantedcontacts'] except KeyError: oma_grantedcontacts = resourcelists.List(name='oma_grantedcontacts') resource_lists.add(oma_grantedcontacts) try: oma_blockedcontacts = resource_lists['oma_blockedcontacts'] except KeyError: oma_blockedcontacts = resourcelists.List(name='oma_blockedcontacts') resource_lists.add(oma_blockedcontacts) try: oma_allcontacts = resource_lists['oma_allcontacts'] except KeyError: oma_allcontacts = resourcelists.List(name='oma_allcontacts') oma_allcontacts.add(resourcelists.External(self.resource_lists.url + '/~~' + resource_lists.get_xpath(oma_buddylist))) oma_allcontacts.add(resourcelists.External(self.resource_lists.url + '/~~' + resource_lists.get_xpath(oma_grantedcontacts))) oma_allcontacts.add(resourcelists.External(self.resource_lists.url + '/~~' + resource_lists.get_xpath(oma_blockedcontacts))) resource_lists.add(oma_allcontacts) try: dialog_grantedcontacts = resource_lists['dialog_grantedcontacts'] except KeyError: dialog_grantedcontacts = resourcelists.List(name='dialog_grantedcontacts') resource_lists.add(dialog_grantedcontacts) try: dialog_blockedcontacts = resource_lists['dialog_blockedcontacts'] except KeyError: dialog_blockedcontacts = resourcelists.List(name='dialog_blockedcontacts') resource_lists.add(dialog_blockedcontacts) try: sipsimple_presence_rls = resource_lists['sipsimple_presence_rls'] except KeyError: sipsimple_presence_rls = resourcelists.List(name='sipsimple_presence_rls') resource_lists.add(sipsimple_presence_rls) try: sipsimple_dialog_rls = resource_lists['sipsimple_dialog_rls'] except KeyError: sipsimple_dialog_rls = resourcelists.List(name='sipsimple_dialog_rls') resource_lists.add(sipsimple_dialog_rls) try: sipsimple_addressbook = resource_lists['sipsimple_addressbook'] except KeyError: sipsimple_addressbook = resourcelists.List(name='sipsimple_addressbook') resource_lists.add(sipsimple_addressbook) for cls in (cls for cls in sipsimple_addressbook[IterateTypes] if cls not in (addressbook.Contact, addressbook.Group, addressbook.Policy)): del sipsimple_addressbook[cls, All] for cls in (cls for cls in oma_grantedcontacts[IterateTypes] if cls is not resourcelists.Entry): del oma_grantedcontacts[cls, All] for cls in (cls for cls in oma_blockedcontacts[IterateTypes] if cls is not resourcelists.Entry): del oma_blockedcontacts[cls, All] for cls in (cls for cls in dialog_grantedcontacts[IterateTypes] if cls is not resourcelists.Entry): del dialog_grantedcontacts[cls, All] for cls in (cls for cls in dialog_blockedcontacts[IterateTypes] if cls is not resourcelists.Entry): del dialog_blockedcontacts[cls, All] for cls in (cls for cls in sipsimple_presence_rls[IterateTypes] if cls is not resourcelists.Entry): del sipsimple_presence_rls[cls, All] for cls in (cls for cls in sipsimple_dialog_rls[IterateTypes] if cls is not resourcelists.Entry): del sipsimple_dialog_rls[cls, All] groups = ItemCollection(sipsimple_addressbook[addressbook.Group, IterateItems]) contacts = ItemCollection(sipsimple_addressbook[addressbook.Contact, IterateItems]) policies = ItemCollection(sipsimple_addressbook[addressbook.Policy, IterateItems]) for group, missing_id in [(group, missing_id) for group in groups for missing_id in (id for id in group.contacts if id not in contacts)]: group.contacts.remove(missing_id) all_contact_uris = set(uri.uri for contact in contacts for uri in contact.uris) contact_allow_presence_uris = set(uri.uri for contact in contacts for uri in contact.uris if contact.presence.policy=='allow') contact_block_presence_uris = set(uri.uri for contact in contacts for uri in contact.uris if contact.presence.policy=='block') contact_allow_dialog_uris = set(uri.uri for contact in contacts for uri in contact.uris if contact.dialog.policy=='allow') contact_block_dialog_uris = set(uri.uri for contact in contacts for uri in contact.uris if contact.dialog.policy=='block') contact_subscribe_presence_uris = set(uri.uri for contact in contacts for uri in contact.uris if contact.presence.subscribe==True) contact_subscribe_dialog_uris = set(uri.uri for contact in contacts for uri in contact.uris if contact.dialog.subscribe==True) policy_allow_presence_uris = set(policy.uri for policy in policies if policy.presence.policy=='allow') policy_block_presence_uris = set(policy.uri for policy in policies if policy.presence.policy=='block') policy_allow_dialog_uris = set(policy.uri for policy in policies if policy.dialog.policy=='allow') policy_block_dialog_uris = set(policy.uri for policy in policies if policy.dialog.policy=='block') policy_subscribe_presence_uris = set(policy.uri for policy in policies if policy.presence.subscribe==True) policy_subscribe_dialog_uris = set(policy.uri for policy in policies if policy.dialog.subscribe==True) allowed_presence_uris = contact_allow_presence_uris - contact_block_presence_uris | policy_allow_presence_uris - policy_block_presence_uris - all_contact_uris blocked_presence_uris = contact_block_presence_uris | policy_block_presence_uris - all_contact_uris allowed_dialog_uris = contact_allow_dialog_uris - contact_block_dialog_uris | policy_allow_dialog_uris - policy_block_dialog_uris - all_contact_uris blocked_dialog_uris = contact_block_dialog_uris | policy_block_dialog_uris - all_contact_uris subscribe_presence_uris = contact_subscribe_presence_uris | policy_subscribe_presence_uris - all_contact_uris subscribe_dialog_uris = contact_subscribe_dialog_uris | policy_subscribe_dialog_uris - all_contact_uris if allowed_presence_uris != set(entry.uri for entry in oma_grantedcontacts): oma_grantedcontacts.clear() oma_grantedcontacts.update(resourcelists.Entry(uri) for uri in allowed_presence_uris) if blocked_presence_uris != set(entry.uri for entry in oma_blockedcontacts): oma_blockedcontacts.clear() oma_blockedcontacts.update(resourcelists.Entry(uri) for uri in blocked_presence_uris) if allowed_dialog_uris != set(entry.uri for entry in dialog_grantedcontacts): dialog_grantedcontacts.clear() dialog_grantedcontacts.update(resourcelists.Entry(uri) for uri in allowed_dialog_uris) if blocked_dialog_uris != set(entry.uri for entry in dialog_blockedcontacts): dialog_blockedcontacts.clear() dialog_blockedcontacts.update(resourcelists.Entry(uri) for uri in blocked_dialog_uris) if subscribe_presence_uris != set(entry.uri for entry in sipsimple_presence_rls): sipsimple_presence_rls.clear() sipsimple_presence_rls.update(resourcelists.Entry(uri) for uri in subscribe_presence_uris) if subscribe_dialog_uris != set(entry.uri for entry in sipsimple_dialog_rls): sipsimple_dialog_rls.clear() sipsimple_dialog_rls.update(resourcelists.Entry(uri) for uri in subscribe_dialog_uris) # Normalize rls-services # if self.rls_services.content is None: self.rls_services.content = rlsservices.RLSServices() rls_services = self.rls_services.content rls_presence_uri = 'sip:' + self.rls_presence_uri rls_dialog_uri = 'sip:' + self.rls_dialog_uri rls_presence_list = rlsservices.ResourceList(self.resource_lists.url + '/~~' + resource_lists.get_xpath(sipsimple_presence_rls)) rls_dialog_list = rlsservices.ResourceList(self.resource_lists.url + '/~~' + resource_lists.get_xpath(sipsimple_dialog_rls)) try: rls_presence_service = rls_services[rls_presence_uri] except KeyError: rls_presence_service = rlsservices.Service(rls_presence_uri, list=rls_presence_list, packages=['presence']) rls_services.add(rls_presence_service) else: if rls_presence_service.list != rls_presence_list: rls_presence_service.list = rls_presence_list if list(rls_presence_service.packages) != ['presence']: rls_presence_service.packages = ['presence'] try: rls_dialog_service = rls_services[rls_dialog_uri] except KeyError: rls_dialog_service = rlsservices.Service(rls_dialog_uri, list=rls_dialog_list, packages=['dialog']) rls_services.add(rls_dialog_service) else: if rls_dialog_service.list != rls_dialog_list: rls_dialog_service.list = rls_dialog_list if list(rls_dialog_service.packages) != ['dialog']: rls_dialog_service.packages = ['dialog'] # Normalize pres-rules # if self.pres_rules.content is None: self.pres_rules.content = presrules.PresRules() def fix_subhandling(rule, valid_values=[]): subhandling_elements = sorted((item for item in rule.actions if isinstance(item, presrules.SubHandling)), key=attrgetter('value.priority')) if not subhandling_elements: subhandling_elements = [presrules.SubHandling('block')] # spec specifies that missing SubHandling means block rule.actions.update(subhandling_elements) subhandling = subhandling_elements.pop() for item in subhandling_elements: # remove any extraneous SubHandling elements rule.actions.remove(item) if subhandling.value not in valid_values: subhandling.value = valid_values[0] pres_rules = self.pres_rules.content oma_grantedcontacts_ref = omapolicy.ExternalList([self.resource_lists.url + '/~~' + resource_lists.get_xpath(oma_grantedcontacts)]) oma_blockedcontacts_ref = omapolicy.ExternalList([self.resource_lists.url + '/~~' + resource_lists.get_xpath(oma_blockedcontacts)]) try: wp_prs_grantedcontacts = pres_rules['wp_prs_grantedcontacts'] except KeyError: wp_prs_grantedcontacts = commonpolicy.Rule('wp_prs_grantedcontacts', conditions=[oma_grantedcontacts_ref], actions=[presrules.SubHandling('allow')]) pres_rules.add(wp_prs_grantedcontacts) else: fix_subhandling(wp_prs_grantedcontacts, valid_values=['allow']) if list(wp_prs_grantedcontacts.conditions) != [oma_grantedcontacts_ref]: wp_prs_grantedcontacts.conditions = [oma_grantedcontacts_ref] if wp_prs_grantedcontacts.transformations: wp_prs_grantedcontacts.transformations = None try: wp_prs_blockedcontacts = pres_rules['wp_prs_blockedcontacts'] except KeyError: wp_prs_blockedcontacts = commonpolicy.Rule('wp_prs_blockedcontacts', conditions=[oma_blockedcontacts_ref], actions=[presrules.SubHandling('polite-block')]) pres_rules.add(wp_prs_blockedcontacts) else: fix_subhandling(wp_prs_blockedcontacts, valid_values=['polite-block']) if list(wp_prs_blockedcontacts.conditions) != [oma_blockedcontacts_ref]: wp_prs_blockedcontacts.conditions = [oma_blockedcontacts_ref] if wp_prs_blockedcontacts.transformations: wp_prs_blockedcontacts.transformations = None wp_prs_unlisted = pres_rules.get('wp_prs_unlisted', None) wp_prs_allow_unlisted = pres_rules.get('wp_prs_allow_unlisted', None) if wp_prs_unlisted is not None and wp_prs_allow_unlisted is not None: pres_rules.remove(wp_prs_allow_unlisted) wp_prs_allow_unlisted = None wp_prs_unlisted_rule = wp_prs_unlisted or wp_prs_allow_unlisted if wp_prs_unlisted_rule is None: wp_prs_unlisted = commonpolicy.Rule('wp_prs_unlisted', conditions=[omapolicy.OtherIdentity()], actions=[presrules.SubHandling('confirm')]) pres_rules.add(wp_prs_unlisted) wp_prs_unlisted_rule = wp_prs_unlisted else: if wp_prs_unlisted_rule is wp_prs_unlisted: fix_subhandling(wp_prs_unlisted_rule, valid_values=['confirm', 'block', 'polite-block']) else: fix_subhandling(wp_prs_unlisted_rule, valid_values=['allow']) if list(wp_prs_unlisted_rule.conditions) != [omapolicy.OtherIdentity()]: wp_prs_unlisted_rule.conditions = [omapolicy.OtherIdentity()] if wp_prs_unlisted_rule.transformations: wp_prs_unlisted_rule.transformations = None match_anonymous = omapolicy.AnonymousRequest() try: wp_prs_block_anonymous = pres_rules['wp_prs_block_anonymous'] except KeyError: wp_prs_block_anonymous = commonpolicy.Rule('wp_prs_block_anonymous', conditions=[match_anonymous], actions=[presrules.SubHandling('block')]) pres_rules.add(wp_prs_block_anonymous) else: fix_subhandling(wp_prs_block_anonymous, valid_values=['block', 'polite-block']) if list(wp_prs_block_anonymous.conditions) != [match_anonymous]: wp_prs_block_anonymous.conditions = [match_anonymous] if wp_prs_block_anonymous.transformations: wp_prs_block_anonymous.transformations = None match_self = commonpolicy.Identity([commonpolicy.IdentityOne('sip:' + self.account.id)]) try: wp_prs_allow_own = pres_rules['wp_prs_allow_own'] except KeyError: wp_prs_allow_own = commonpolicy.Rule('wp_prs_allow_own', conditions=[match_self], actions=[presrules.SubHandling('allow')]) pres_rules.add(wp_prs_allow_own) else: fix_subhandling(wp_prs_allow_own, valid_values=['allow']) if list(wp_prs_allow_own.conditions) != [match_self]: wp_prs_allow_own.conditions = [match_self] if wp_prs_allow_own.transformations: wp_prs_allow_own.transformations = None # Remove any other rules all_rule_names = set(pres_rules[IterateIDs]) known_rule_names = {'wp_prs_grantedcontacts', 'wp_prs_blockedcontacts', 'wp_prs_unlisted', 'wp_prs_allow_unlisted', 'wp_prs_block_anonymous', 'wp_prs_allow_own'} for name in all_rule_names - known_rule_names: del pres_rules[name] del fix_subhandling # Normalize dialog-rules # if self.dialog_rules.supported: if self.dialog_rules.content is None: self.dialog_rules.content = dialogrules.DialogRules() elif self.dialog_rules.content.element.nsmap.get('dr') != dialogrules.namespace: # TODO: this elif branch should be removed in a later version as it is self.dialog_rules.content = dialogrules.DialogRules() # only used to discard documents created with the old namespace. -Dan def fix_subhandling(rule, valid_values=()): subhandling_elements = sorted((item for item in rule.actions if isinstance(item, dialogrules.SubHandling)), key=attrgetter('value.priority')) if not subhandling_elements: subhandling_elements = [dialogrules.SubHandling('block')] # spec specifies that missing SubHandling means block rule.actions.update(subhandling_elements) subhandling = subhandling_elements.pop() for item in subhandling_elements: # remove any extraneous SubHandling elements rule.actions.remove(item) if subhandling.value not in valid_values: subhandling.value = valid_values[0] dialog_rules = self.dialog_rules.content dialog_grantedcontacts_ref = omapolicy.ExternalList([self.resource_lists.url + '/~~' + resource_lists.get_xpath(dialog_grantedcontacts)]) dialog_blockedcontacts_ref = omapolicy.ExternalList([self.resource_lists.url + '/~~' + resource_lists.get_xpath(dialog_blockedcontacts)]) try: wp_dlg_grantedcontacts = dialog_rules['wp_dlg_grantedcontacts'] except KeyError: wp_dlg_grantedcontacts = commonpolicy.Rule('wp_dlg_grantedcontacts', conditions=[dialog_grantedcontacts_ref], actions=[dialogrules.SubHandling('allow')]) dialog_rules.add(wp_dlg_grantedcontacts) else: fix_subhandling(wp_dlg_grantedcontacts, valid_values=['allow']) if list(wp_dlg_grantedcontacts.conditions) != [dialog_grantedcontacts_ref]: wp_dlg_grantedcontacts.conditions = [dialog_grantedcontacts_ref] if wp_dlg_grantedcontacts.transformations: wp_dlg_grantedcontacts.transformations = None try: wp_dlg_blockedcontacts = dialog_rules['wp_dlg_blockedcontacts'] except KeyError: wp_dlg_blockedcontacts = commonpolicy.Rule('wp_dlg_blockedcontacts', conditions=[dialog_blockedcontacts_ref], actions=[dialogrules.SubHandling('polite-block')]) dialog_rules.add(wp_dlg_blockedcontacts) else: fix_subhandling(wp_dlg_blockedcontacts, valid_values=['polite-block']) if list(wp_dlg_blockedcontacts.conditions) != [dialog_blockedcontacts_ref]: wp_dlg_blockedcontacts.conditions = [dialog_blockedcontacts_ref] if wp_dlg_blockedcontacts.transformations: wp_dlg_blockedcontacts.transformations = None wp_dlg_unlisted = dialog_rules.get('wp_dlg_unlisted', None) wp_dlg_allow_unlisted = dialog_rules.get('wp_dlg_allow_unlisted', None) if wp_dlg_unlisted is not None and wp_dlg_allow_unlisted is not None: dialog_rules.remove(wp_dlg_allow_unlisted) wp_dlg_allow_unlisted = None wp_dlg_unlisted_rule = wp_dlg_unlisted or wp_dlg_allow_unlisted if wp_dlg_unlisted_rule is None: wp_dlg_unlisted = commonpolicy.Rule('wp_dlg_unlisted', conditions=[omapolicy.OtherIdentity()], actions=[dialogrules.SubHandling('confirm')]) dialog_rules.add(wp_dlg_unlisted) wp_dlg_unlisted_rule = wp_dlg_unlisted else: if wp_dlg_unlisted_rule is wp_dlg_unlisted: fix_subhandling(wp_dlg_unlisted_rule, valid_values=['confirm', 'block', 'polite-block']) else: fix_subhandling(wp_dlg_unlisted_rule, valid_values=['allow']) if list(wp_dlg_unlisted_rule.conditions) != [omapolicy.OtherIdentity()]: wp_dlg_unlisted_rule.conditions = [omapolicy.OtherIdentity()] if wp_dlg_unlisted_rule.transformations: wp_dlg_unlisted_rule.transformations = None match_anonymous = omapolicy.AnonymousRequest() try: wp_dlg_block_anonymous = dialog_rules['wp_dlg_block_anonymous'] except KeyError: wp_dlg_block_anonymous = commonpolicy.Rule('wp_dlg_block_anonymous', conditions=[match_anonymous], actions=[dialogrules.SubHandling('block')]) dialog_rules.add(wp_dlg_block_anonymous) else: fix_subhandling(wp_dlg_block_anonymous, valid_values=['block', 'polite-block']) if list(wp_dlg_block_anonymous.conditions) != [match_anonymous]: wp_dlg_block_anonymous.conditions = [match_anonymous] if wp_dlg_block_anonymous.transformations: wp_dlg_block_anonymous.transformations = None match_self = commonpolicy.Identity([commonpolicy.IdentityOne('sip:' + self.account.id)]) try: wp_dlg_allow_own = dialog_rules['wp_dlg_allow_own'] except KeyError: wp_dlg_allow_own = commonpolicy.Rule('wp_dlg_allow_own', conditions=[match_self], actions=[dialogrules.SubHandling('allow')]) dialog_rules.add(wp_dlg_allow_own) else: fix_subhandling(wp_dlg_allow_own, valid_values=['allow']) if list(wp_dlg_allow_own.conditions) != [match_self]: wp_dlg_allow_own.conditions = [match_self] if wp_dlg_allow_own.transformations: wp_dlg_allow_own.transformations = None # Remove any other rules all_rule_names = set(dialog_rules[IterateIDs]) known_rule_names = {'wp_dlg_grantedcontacts', 'wp_dlg_blockedcontacts', 'wp_dlg_unlisted', 'wp_dlg_allow_unlisted', 'wp_dlg_block_anonymous', 'wp_dlg_allow_own'} for name in all_rule_names - known_rule_names: del dialog_rules[name] # Normalize status icon # if self.status_icon.supported and self.status_icon.content is not None: content = self.status_icon.content if None in (content.encoding, content.mime_type) or content.encoding.value.lower() != 'base64' or content.mime_type.value.lower() not in Icon.__mimetypes__: self.status_icon.content = None self.status_icon.dirty = True def _OH_AddContactOperation(self, operation): sipsimple_addressbook = self.resource_lists.content['sipsimple_addressbook'] contact = operation.contact presence_handling = addressbook.PresenceHandling(contact.presence.policy, contact.presence.subscribe) dialog_handling = addressbook.DialogHandling(contact.dialog.policy, contact.dialog.subscribe) xml_contact = addressbook.Contact(contact.id, contact.name, presence_handling=presence_handling, dialog_handling=dialog_handling) for uri in contact.uris: contact_uri = addressbook.ContactURI(uri.id, uri.uri, uri.type) contact_uri.attributes = addressbook.ContactURI.attributes.type(uri.attributes) xml_contact.uris.add(contact_uri) xml_contact.uris.default = contact.uris.default xml_contact.attributes = addressbook.Contact.attributes.type(contact.attributes) sipsimple_addressbook.add(xml_contact) def _OH_UpdateContactOperation(self, operation): sipsimple_addressbook = self.resource_lists.content['sipsimple_addressbook'] try: contact = sipsimple_addressbook[addressbook.Contact, operation.contact.id] except KeyError: return attributes = dict(operation.attributes) attributes.pop('id', None) # id is never modified attributes.pop('uris', None) # uris are modified using dedicated methods if 'name' in attributes: contact.name = attributes.pop('name') if 'uris.default' in attributes: contact.uris.default = attributes.pop('uris.default') if 'presence.policy' in attributes: contact.presence.policy = attributes.pop('presence.policy') if 'presence.subscribe' in attributes: contact.presence.subscribe = attributes.pop('presence.subscribe') if 'dialog.policy' in attributes: contact.dialog.policy = attributes.pop('dialog.policy') if 'dialog.subscribe' in attributes: contact.dialog.subscribe = attributes.pop('dialog.subscribe') if contact.attributes is None: contact.attributes = addressbook.Contact.attributes.type() contact.attributes.update(attributes) def _OH_RemoveContactOperation(self, operation): sipsimple_addressbook = self.resource_lists.content['sipsimple_addressbook'] for group in (group for group in sipsimple_addressbook[addressbook.Group, IterateItems] if operation.contact.id in group.contacts): group.contacts.remove(operation.contact.id) try: del sipsimple_addressbook[addressbook.Contact, operation.contact.id] except KeyError: pass def _OH_AddContactURIOperation(self, operation): sipsimple_addressbook = self.resource_lists.content['sipsimple_addressbook'] try: contact = sipsimple_addressbook[addressbook.Contact, operation.contact.id] except KeyError: return uri = addressbook.ContactURI(operation.uri.id, operation.uri.uri, operation.uri.type) uri.attributes = addressbook.ContactURI.attributes.type(operation.uri.attributes) contact.uris.add(uri) def _OH_UpdateContactURIOperation(self, operation): sipsimple_addressbook = self.resource_lists.content['sipsimple_addressbook'] try: contact = sipsimple_addressbook[addressbook.Contact, operation.contact.id] uri = contact.uris[operation.uri.id] except KeyError: return attributes = dict(operation.attributes) attributes.pop('id', None) # id is never modified if 'uri' in attributes: uri.uri = attributes.pop('uri') if 'type' in attributes: uri.type = attributes.pop('type') if uri.attributes is None: uri.attributes = addressbook.ContactURI.attributes.type() uri.attributes.update(attributes) def _OH_RemoveContactURIOperation(self, operation): sipsimple_addressbook = self.resource_lists.content['sipsimple_addressbook'] try: contact = sipsimple_addressbook[addressbook.Contact, operation.contact.id] del contact.uris[operation.uri.id] except KeyError: pass def _OH_AddGroupOperation(self, operation): sipsimple_addressbook = self.resource_lists.content['sipsimple_addressbook'] group = addressbook.Group(operation.group.id, operation.group.name, [contact.id for contact in operation.group.contacts]) group.attributes = addressbook.Group.attributes.type(operation.group.attributes) sipsimple_addressbook.add(group) def _OH_UpdateGroupOperation(self, operation): sipsimple_addressbook = self.resource_lists.content['sipsimple_addressbook'] try: group = sipsimple_addressbook[addressbook.Group, operation.group.id] except KeyError: return attributes = dict(operation.attributes) attributes.pop('id', None) # id is never modified attributes.pop('contacts', None) # contacts are added/removed using dedicated methods if 'name' in attributes: group.name = attributes.pop('name') if group.attributes is None: group.attributes = addressbook.Group.attributes.type() group.attributes.update(attributes) def _OH_RemoveGroupOperation(self, operation): sipsimple_addressbook = self.resource_lists.content['sipsimple_addressbook'] try: del sipsimple_addressbook[addressbook.Group, operation.group.id] except KeyError: pass def _OH_AddGroupMemberOperation(self, operation): sipsimple_addressbook = self.resource_lists.content['sipsimple_addressbook'] try: group = sipsimple_addressbook[addressbook.Group, operation.group.id] except KeyError: return if operation.contact.id in group.contacts: return group.contacts.add(operation.contact.id) def _OH_RemoveGroupMemberOperation(self, operation): sipsimple_addressbook = self.resource_lists.content['sipsimple_addressbook'] try: group = sipsimple_addressbook[addressbook.Group, operation.group.id] group.contacts.remove(operation.contact.id) except KeyError: return def _OH_AddPolicyOperation(self, operation): sipsimple_addressbook = self.resource_lists.content['sipsimple_addressbook'] presence_handling = addressbook.PresenceHandling(operation.policy.presence.policy, operation.policy.presence.subscribe) dialog_handling = addressbook.DialogHandling(operation.policy.dialog.policy, operation.policy.dialog.subscribe) policy = addressbook.Policy(operation.policy.id, operation.policy.uri, operation.policy.name, presence_handling=presence_handling, dialog_handling=dialog_handling) policy.attributes = addressbook.Policy.attributes.type(operation.policy.attributes) sipsimple_addressbook.add(policy) def _OH_UpdatePolicyOperation(self, operation): sipsimple_addressbook = self.resource_lists.content['sipsimple_addressbook'] try: policy = sipsimple_addressbook[addressbook.Policy, operation.policy.id] except KeyError: return attributes = dict(operation.attributes) attributes.pop('id', None) # id is never modified if 'uri' in attributes: policy.uri = attributes.pop('uri') if 'name' in attributes: policy.name = attributes.pop('name') if 'presence.policy' in attributes: policy.presence.policy = attributes.pop('presence.policy') if 'presence.subscribe' in attributes: policy.presence.subscribe = attributes.pop('presence.subscribe') if 'dialog.policy' in attributes: policy.dialog.policy = attributes.pop('dialog.policy') if 'dialog.subscribe' in attributes: policy.dialog.subscribe = attributes.pop('dialog.subscribe') if policy.attributes is None: policy.attributes = addressbook.Policy.attributes.type() policy.attributes.update(attributes) def _OH_RemovePolicyOperation(self, operation): sipsimple_addressbook = self.resource_lists.content['sipsimple_addressbook'] try: del sipsimple_addressbook[addressbook.Policy, operation.policy.id] except KeyError: pass def _OH_SetStatusIconOperation(self, operation): if not self.status_icon.supported: return icon = operation.icon if icon is None or not icon.data: self.status_icon.dirty = self.status_icon.content is not None self.status_icon.content = None else: content = prescontent.PresenceContent(data=base64.encodestring(icon.data), mime_type=icon.mime_type, encoding='base64', description=icon.description) if self.status_icon.content == content: return self.status_icon.content = content def _OH_SetOfflineStatusOperation(self, operation): pidf = operation.status.pidf if operation.status is not None else None if not self.pidf_manipulation.supported or pidf == self.pidf_manipulation.content: return self.pidf_manipulation.content = pidf self.pidf_manipulation.dirty = True def _OH_SetDefaultPresencePolicyOperation(self, operation): pres_rules = self.pres_rules.content if operation.policy == 'allow': rule_id, other_rule_id = 'wp_prs_allow_unlisted', 'wp_prs_unlisted' else: rule_id, other_rule_id = 'wp_prs_unlisted', 'wp_prs_allow_unlisted' try: del pres_rules[other_rule_id] except KeyError: rule = pres_rules[rule_id] subhandling = next(item for item in rule.actions if isinstance(item, presrules.SubHandling)) subhandling.value = operation.policy else: rule = commonpolicy.Rule(rule_id, conditions=[omapolicy.OtherIdentity()], actions=[presrules.SubHandling(operation.policy)]) pres_rules.add(rule) def _OH_SetDefaultDialogPolicyOperation(self, operation): if not self.dialog_rules.supported: return dialog_rules = self.dialog_rules.content if operation.policy == 'allow': rule_id, other_rule_id = 'wp_dlg_allow_unlisted', 'wp_dlg_unlisted' else: rule_id, other_rule_id = 'wp_dlg_unlisted', 'wp_dlg_allow_unlisted' try: del dialog_rules[other_rule_id] except KeyError: rule = dialog_rules[rule_id] subhandling = next(item for item in rule.actions if isinstance(item, dialogrules.SubHandling)) subhandling.value = operation.policy else: rule = commonpolicy.Rule(rule_id, conditions=[omapolicy.OtherIdentity()], actions=[dialogrules.SubHandling(operation.policy)]) dialog_rules.add(rule) # Notification handlers # @run_in_twisted_thread def handle_notification(self, notification): handler = getattr(self, '_NH_%s' % notification.name, Null) handler(notification) @run_in_green_thread def _NH_CFGSettingsObjectDidChange(self, notification): if {'__id__', 'xcap.xcap_root', 'auth.username', 'auth.password', 'sip.subscribe_interval', 'sip.transport_list'}.intersection(notification.data.modified): self.command_channel.send(Command('reload', modified=notification.data.modified)) if 'enabled' in notification.data.modified: return # global account activation is handled separately by the account itself if self.account.enabled and 'xcap.enabled' in notification.data.modified: if self.account.xcap.enabled: self.start() else: self.stop() def _NH_CFGSettingsObjectWasDeleted(self, notification): notification.center.remove_observer(self, sender=self.account, name='CFGSettingsObjectDidChange') notification.center.remove_observer(self, sender=self.account, name='CFGSettingsObjectWasDeleted') self.command_channel.send(Command('stop')) self.command_channel.send(Command('cleanup')) def _NH_XCAPSubscriptionDidStart(self, notification): self.command_channel.send(Command('fetch', documents=set(self.document_names))) def _NH_XCAPSubscriptionDidFail(self, notification): self.command_channel.send(Command('fetch', documents=set(self.document_names))) def _NH_XCAPSubscriptionGotNotify(self, notification): if notification.data.content_type == xcapdiff.XCAPDiffDocument.content_type: try: xcap_diff = xcapdiff.XCAPDiffDocument.parse(notification.data.body) except ParserError: self.command_channel.send(Command('fetch', documents=set(self.document_names))) else: applications = set(child.selector.auid for child in xcap_diff if isinstance(child, xcapdiff.Document)) documents = set(document.name for document in self.documents if document.application in applications) self.command_channel.send(Command('fetch', documents=documents)) def _load_data(self): addressbook = Addressbook.from_payload(self.resource_lists.content['sipsimple_addressbook']) default_presence_rule = self.pres_rules.content.get('wp_prs_unlisted', None) or self.pres_rules.content.get('wp_prs_allow_unlisted', None) if self.dialog_rules.supported: default_dialog_rule = self.dialog_rules.content.get('wp_dlg_unlisted', None) or self.dialog_rules.content.get('wp_dlg_allow_unlisted', None) else: default_dialog_rule = None presence_rules = PresenceRules.from_payload(default_presence_rule) dialog_rules = DialogRules.from_payload(default_dialog_rule) if self.status_icon.supported and self.status_icon.content: status_icon = Icon.from_payload(self.status_icon.content) status_icon.url = self.status_icon.url status_icon.etag = self.status_icon.etag else: status_icon = None if self.pidf_manipulation.supported and self.pidf_manipulation.content: offline_status = OfflineStatus(self.pidf_manipulation.content) else: offline_status = None data=NotificationData(addressbook=addressbook, presence_rules=presence_rules, dialog_rules=dialog_rules, status_icon=status_icon, offline_status=offline_status) NotificationCenter().post_notification('XCAPManagerDidReloadData', sender=self, data=data) def _fetch_documents(self, documents): workers = [Worker.spawn(document.fetch) for document in (doc for doc in self.documents if doc.name in documents and doc.supported)] try: while workers: worker = workers.pop() worker.wait() finally: for worker in workers: worker.wait_ex() def _save_journal(self): try: - self.storage.save('journal', cPickle.dumps(self.journal)) + self.storage.save('journal', pickle.dumps(self.journal)) except XCAPStorageError: pass def _schedule_command(self, timeout, command): from twisted.internet import reactor timer = reactor.callLater(timeout, self.command_channel.send, command) timer.command = command return timer class XCAPTransaction(object): def __init__(self, xcap_manager): self.xcap_manager = xcap_manager def __enter__(self): self.xcap_manager.start_transaction() return self def __exit__(self, type, value, traceback): self.xcap_manager.commit_transaction() diff --git a/sipsimple/account/xcap/storage/file.py b/sipsimple/account/xcap/storage/file.py index e4244411..0867c574 100644 --- a/sipsimple/account/xcap/storage/file.py +++ b/sipsimple/account/xcap/storage/file.py @@ -1,87 +1,87 @@ """XCAP backend for storing data in files""" __all__ = ["FileStorage"] import errno import os import platform import random from application.system import makedirs, openfile, unlink from zope.interface import implements from sipsimple.account.xcap.storage import IXCAPStorage, XCAPStorageError class FileStorage(object): """Implementation of an XCAP backend that stores data in files.""" implements(IXCAPStorage) def __init__(self, directory, account_id): """Initialize the storage for the specified directory and account ID""" self.directory = directory self.account_id = account_id self.names = set() def load(self, name): """Read the file given by name and return its content.""" try: document = open(os.path.join(self.directory, self.account_id, name)).read() - except (IOError, OSError), e: + except (IOError, OSError) as e: raise XCAPStorageError("failed to load XCAP data for %s/%s: %s" % (self.account_id, name, str(e))) else: self.names.add(name) return document def save(self, name, data): """Write the data in a file identified by name.""" filename = os.path.join(self.directory, self.account_id, name) tmp_filename = '%s.%d.%08X' % (filename, os.getpid(), random.getrandbits(32)) try: makedirs(os.path.join(self.directory, self.account_id)) - file = openfile(tmp_filename, 'wb', permissions=0600) + file = openfile(tmp_filename, 'wb', permissions=0o600) file.write(data) file.close() if platform.system() == 'Windows': # os.rename does not work on Windows if the destination file already exists. # It seems there is no atomic way to do this on Windows. unlink(filename) os.rename(tmp_filename, filename) - except (IOError, OSError), e: + except (IOError, OSError) as e: raise XCAPStorageError("failed to save XCAP data for %s/%s: %s" % (self.account_id, name, str(e))) else: self.names.add(name) def delete(self, name): """Delete the data stored in the file identified by name""" try: os.unlink(os.path.join(self.directory, self.account_id, name)) - except OSError, e: + except OSError as e: if e.errno == errno.ENOENT: self.names.discard(name) return raise XCAPStorageError("failed to delete XCAP data for %s/%s: %s" % (self.account_id, name, str(e))) else: self.names.remove(name) def purge(self): """Delete all the files stored by the backend""" failed = [] for name in self.names: try: os.unlink(os.path.join(self.directory, self.account_id, name)) - except OSError, e: + except OSError as e: if e.errno == errno.ENOENT: continue failed.append(name) self.names.clear() try: os.rmdir(os.path.join(self.directory, self.account_id)) except OSError: pass if failed: raise XCAPStorageError("the following files could not be deleted for %s: %s" % (self.account_id, ', '.join(failed))) diff --git a/sipsimple/addressbook.py b/sipsimple/addressbook.py index 36e7ee10..528dc8d2 100644 --- a/sipsimple/addressbook.py +++ b/sipsimple/addressbook.py @@ -1,1367 +1,1365 @@ """Implementation of an addressbook management system""" -from __future__ import absolute_import + __all__ = ['AddressbookManager', 'Contact', 'ContactURI', 'Group', 'Policy', 'SharedSetting', 'ContactExtension', 'ContactURIExtension', 'GroupExtension', 'PolicyExtension'] from functools import reduce from operator import attrgetter from random import randint -from threading import Lock +from .threading import Lock from time import time from zope.interface import implements -from application.notification import IObserver, NotificationCenter, NotificationData -from application.python import Null -from application.python.decorator import execute_once -from application.python.types import Singleton, MarkerType -from application.python.weakref import defaultweakobjectmap +from .application.notification import IObserver, NotificationCenter, NotificationData +from .application.python import Null +from .application.python.decorator import execute_once +from .application.python.types import Singleton, MarkerType +from .application.python.weakref import defaultweakobjectmap from sipsimple import log from sipsimple.account import xcap, AccountManager from sipsimple.configuration import ConfigurationManager, ObjectNotFoundError, DuplicateIDError, PersistentKey, ModifiedValue, ModifiedList from sipsimple.configuration import AbstractSetting, RuntimeSetting, SettingsObjectImmutableID, SettingsGroup, SettingsGroupMeta, SettingsState, ItemCollection, ItemManagement from sipsimple.payloads.addressbook import PolicyValue, ElementAttributes from sipsimple.payloads.datatypes import ID from sipsimple.payloads.resourcelists import ResourceListsDocument from sipsimple.threading import run_in_thread def unique_id(prefix='id'): return "%s%d%06d" % (prefix, time()*1e6, randint(0, 999999)) def recursive_getattr(obj, name): return reduce(getattr, name.split('.'), obj) -class Local(object): - __metaclass__ = MarkerType +class Local(object, metaclass=MarkerType): + pass class Remote(object): def __init__(self, account, xcap_object): self.account = account self.xcap_object = xcap_object def __repr__(self): return "%s(%r, %r)" % (self.__class__.__name__, self.account, self.xcap_object) class Setting(AbstractSetting): """ Descriptor representing a setting in an addressbook object. Unlike a standard Setting, this one will only use the default value as a template to fill in a missing value and explicitly set it when saving if it was not specified explicitly prior to that. """ def __init__(self, type, default=None, nillable=False): if default is None and not nillable: raise TypeError("default must be specified if object is not nillable") self.type = type self.default = default self.nillable = nillable self.values = defaultweakobjectmap(lambda: default) self.oldvalues = defaultweakobjectmap(lambda: default) self.dirty = defaultweakobjectmap(bool) self.lock = Lock() def __get__(self, obj, objtype): if obj is None: return self with self.lock: return self.values[obj] def __set__(self, obj, value): if value is None and not self.nillable: raise ValueError("setting attribute is not nillable") if value is not None and not isinstance(value, self.type): value = self.type(value) with self.lock: self.values[obj] = value self.dirty[obj] = value != self.oldvalues[obj] def __getstate__(self, obj): with self.lock: value = self.values[obj] if value is None: pass elif issubclass(self.type, bool): - value = u'true' if value else u'false' - elif issubclass(self.type, (int, long, basestring)): - value = unicode(value) + value = 'true' if value else 'false' + elif issubclass(self.type, (int, int, str)): + value = str(value) elif hasattr(value, '__getstate__'): value = value.__getstate__() else: - value = unicode(value) + value = str(value) return value def __setstate__(self, obj, value): if value is None and not self.nillable: raise ValueError("setting attribute is not nillable") if value is None: pass elif issubclass(self.type, bool): if value.lower() in ('true', 'yes', 'on', '1'): value = True elif value.lower() in ('false', 'no', 'off', '0'): value = False else: raise ValueError("invalid boolean value: %s" % (value,)) - elif issubclass(self.type, (int, long, basestring)): + elif issubclass(self.type, (int, int, str)): value = self.type(value) elif hasattr(self.type, '__setstate__'): object = self.type.__new__(self.type) object.__setstate__(value) value = object else: value = self.type(value) with self.lock: self.oldvalues[obj] = self.values[obj] = value self.dirty[obj] = False def get_modified(self, obj): with self.lock: try: if self.dirty[obj]: return ModifiedValue(old=self.oldvalues[obj], new=self.values[obj]) else: return None finally: self.oldvalues[obj] = self.values[obj] self.dirty[obj] = False def get_old(self, obj): with self.lock: return self.oldvalues[obj] def undo(self, obj): with self.lock: self.values[obj] = self.oldvalues[obj] self.dirty[obj] = False class SharedSetting(Setting): """A setting that is shared by being also stored remotely in XCAP""" __namespace__ = None @classmethod def set_namespace(cls, namespace): """ Set the XML namespace to be used for the extra shared attributes of a contact, when storing it in XCAP """ if cls.__namespace__ is not None: raise RuntimeError("namespace already set to %s" % cls.__namespace__) cls.__namespace__ = namespace class ApplicationElementAttributes(ElementAttributes): _xml_namespace = 'urn:%s:xml:ns:addressbook' % namespace ResourceListsDocument.unregister_namespace(ElementAttributes._xml_namespace) ResourceListsDocument.register_namespace(ApplicationElementAttributes._xml_namespace, prefix=namespace.rpartition(':')[2]) - for cls, attribute_name in ((cls, name) for cls in ResourceListsDocument.element_map.values() for name, elem in cls._xml_element_children.items() if elem.type is ElementAttributes): + for cls, attribute_name in ((cls, name) for cls in list(ResourceListsDocument.element_map.values()) for name, elem in list(cls._xml_element_children.items()) if elem.type is ElementAttributes): cls.unregister_extension(attribute_name) cls.register_extension(attribute_name, ApplicationElementAttributes) class AddressbookKey(object): def __init__(self, section): self.group = 'Addressbook' self.section = section def __get__(self, obj, objtype): if obj is None: return [self.group, self.section] else: return [self.group, self.section, PersistentKey(obj.__id__)] def __set__(self, obj, value): raise AttributeError('cannot set attribute') def __delete__(self, obj): raise AttributeError('cannot delete attribute') class MultiAccountTransaction(object): def __init__(self, accounts): self.accounts = accounts def __enter__(self): for account in self.accounts: account.xcap_manager.start_transaction() return self def __exit__(self, exc_type, exc_value, traceback): for account in self.accounts: account.xcap_manager.commit_transaction() def __iter__(self): return iter(self.accounts) class XCAPGroup(xcap.Group): """An XCAP Group with attributes normalized to unicode""" __attributes__ = set() def __init__(self, id, name, contacts, **attributes): - normalized_attributes = dict((name, unicode(value) if value is not None else None) for name, value in attributes.iteritems() if name in self.__attributes__) + normalized_attributes = dict((name, str(value) if value is not None else None) for name, value in list(attributes.items()) if name in self.__attributes__) contacts = [XCAPContact.normalize(contact) for contact in contacts] super(XCAPGroup, self).__init__(id, name, contacts, **normalized_attributes) @classmethod def normalize(cls, group): return cls(group.id, group.name, group.contacts, **group.attributes) def get_modified(self, modified_keys): names = {'name'} attributes = dict((name, getattr(self, name)) for name in names.intersection(modified_keys)) attributes.update((name, self.attributes[name]) for name in self.__attributes__.intersection(modified_keys)) return attributes class XCAPContactURI(xcap.ContactURI): """An XCAP ContactURI with attributes normalized to unicode""" __attributes__ = set() def __init__(self, id, uri, type, **attributes): - normalized_attributes = dict((name, unicode(value) if value is not None else None) for name, value in attributes.iteritems() if name in self.__attributes__) + normalized_attributes = dict((name, str(value) if value is not None else None) for name, value in list(attributes.items()) if name in self.__attributes__) super(XCAPContactURI, self).__init__(id, uri, type, **normalized_attributes) @classmethod def normalize(cls, uri): return cls(uri.id, uri.uri, uri.type, **uri.attributes) def get_modified(self, modified_keys): names = {'uri', 'type'} attributes = dict((name, getattr(self, name)) for name in names.intersection(modified_keys)) attributes.update((name, self.attributes[name]) for name in self.__attributes__.intersection(modified_keys)) return attributes class XCAPContact(xcap.Contact): """An XCAP Contact with attributes normalized to unicode""" __attributes__ = set() def __init__(self, id, name, uris, presence_handling=None, dialog_handling=None, **attributes): - normalized_attributes = dict((name, unicode(value) if value is not None else None) for name, value in attributes.iteritems() if name in self.__attributes__) + normalized_attributes = dict((name, str(value) if value is not None else None) for name, value in list(attributes.items()) if name in self.__attributes__) uris = xcap.ContactURIList((XCAPContactURI.normalize(uri) for uri in uris), default=getattr(uris, 'default', None)) super(XCAPContact, self).__init__(id, name, uris, presence_handling, dialog_handling, **normalized_attributes) @classmethod def normalize(cls, contact): return cls(contact.id, contact.name, contact.uris, contact.presence, contact.dialog, **contact.attributes) def get_modified(self, modified_keys): names = {'name', 'uris.default', 'presence.policy', 'presence.subscribe', 'dialog.policy', 'dialog.subscribe'} attributes = dict((name, recursive_getattr(self, name)) for name in names.intersection(modified_keys)) attributes.update((name, self.attributes[name]) for name in self.__attributes__.intersection(modified_keys)) return attributes class XCAPPolicy(xcap.Policy): """An XCAP Policy with attributes normalized to unicode""" __attributes__ = set() def __init__(self, id, uri, name, presence_handling=None, dialog_handling=None, **attributes): - normalized_attributes = dict((name, unicode(value) if value is not None else None) for name, value in attributes.iteritems() if name in self.__attributes__) + normalized_attributes = dict((name, str(value) if value is not None else None) for name, value in list(attributes.items()) if name in self.__attributes__) super(XCAPPolicy, self).__init__(id, uri, name, presence_handling, dialog_handling, **normalized_attributes) @classmethod def normalize(cls, policy): return cls(policy.id, policy.uri, policy.name, policy.presence, policy.dialog, **policy.attributes) def get_modified(self, modified_keys): names = {'uri', 'name', 'presence.policy', 'presence.subscribe', 'dialog.policy', 'dialog.subscribe'} attributes = dict((name, recursive_getattr(self, name)) for name in names.intersection(modified_keys)) attributes.update((name, self.attributes[name]) for name in self.__attributes__.intersection(modified_keys)) return attributes class ContactListDescriptor(AbstractSetting): def __init__(self): self.values = defaultweakobjectmap(ContactList) self.oldvalues = defaultweakobjectmap(ContactList) self.lock = Lock() def __get__(self, obj, objtype): if obj is None: return self with self.lock: return self.values[obj] def __set__(self, obj, value): if value is None: raise ValueError("setting attribute is not nillable") elif not isinstance(value, ContactList): value = ContactList(value) with self.lock: self.values[obj] = value def __getstate__(self, obj): with self.lock: return self.values[obj].__getstate__() def __setstate__(self, obj, value): if value is None: raise ValueError("setting attribute is not nillable") object = ContactList.__new__(ContactList) object.__setstate__(value) with self.lock: self.values[obj] = object self.oldvalues[obj] = ContactList(object) def get_modified(self, obj): with self.lock: old = self.oldvalues[obj] new = self.values[obj] with new.lock: old_ids = set(old.ids()) new_ids = set(new.ids()) added_contacts = [new[id] for id in new_ids - old_ids] removed_contacts = [old[id] for id in old_ids - new_ids] try: if added_contacts or removed_contacts: return ModifiedList(added=added_contacts, removed=removed_contacts, modified=None) else: return None finally: self.oldvalues[obj] = ContactList(new) def get_old(self, obj): with self.lock: return self.oldvalues[obj] def undo(self, obj): with self.lock: self.values[obj] = ContactList(self.oldvalues[obj]) class ContactList(object): def __new__(cls, contacts=None): instance = object.__new__(cls) instance.lock = Lock() return instance def __init__(self, contacts=None): self.contacts = dict((contact.id, contact) for contact in contacts or [] if contact.__state__ != 'deleted') def __getitem__(self, key): return self.contacts[key] def __contains__(self, key): return key in self.contacts def __iter__(self): - return iter(sorted(self.contacts.values(), key=attrgetter('id'))) + return iter(sorted(list(self.contacts.values()), key=attrgetter('id'))) def __reversed__(self): - return iter(sorted(self.contacts.values(), key=attrgetter('id'), reverse=True)) + return iter(sorted(list(self.contacts.values()), key=attrgetter('id'), reverse=True)) __hash__ = None def __len__(self): return len(self.contacts) def __eq__(self, other): if isinstance(other, ContactList): return self.contacts == other.contacts return NotImplemented def __ne__(self, other): equal = self.__eq__(other) return NotImplemented if equal is NotImplemented else not equal def __repr__(self): - return "%s(%r)" % (self.__class__.__name__, sorted(self.contacts.values(), key=attrgetter('id'))) + return "%s(%r)" % (self.__class__.__name__, sorted(list(self.contacts.values()), key=attrgetter('id'))) def __getstate__(self): - return self.contacts.keys() + return list(self.contacts.keys()) def __setstate__(self, value): addressbook_manager = AddressbookManager() for id in [id for id in value if not addressbook_manager.has_contact(id)]: value.remove(id) with self.lock: self.contacts = dict((id, addressbook_manager.get_contact(id)) for id in value) def ids(self): return sorted(self.contacts.keys()) def add(self, contact): if contact.__state__ == 'deleted': return with self.lock: self.contacts[contact.id] = contact def remove(self, contact): with self.lock: self.contacts.pop(contact.id, None) class Group(SettingsState): __key__ = AddressbookKey('Groups') __id__ = SettingsObjectImmutableID(type=ID) id = __id__ - name = Setting(type=unicode, default='') + name = Setting(type=str, default='') contacts = ContactListDescriptor() def __new__(cls, id=None): with AddressbookManager.load.lock: if not AddressbookManager.load.called: raise RuntimeError("cannot instantiate %s before calling AddressbookManager.load" % cls.__name__) if id is None: id = unique_id() - elif not isinstance(id, basestring): + elif not isinstance(id, str): raise TypeError("id needs to be a string or unicode object") instance = SettingsState.__new__(cls) instance.__id__ = id instance.__state__ = 'new' instance.__xcapgroup__ = None configuration = ConfigurationManager() try: data = configuration.get(instance.__key__) except ObjectNotFoundError: pass else: instance.__setstate__(data) instance.__state__ = 'loaded' instance.__xcapgroup__ = instance.__toxcap__() return instance def __establish__(self): if self.__state__ == 'loaded': self.__state__ = 'active' notification_center = NotificationCenter() notification_center.post_notification('AddressbookGroupWasActivated', sender=self) def __repr__(self): return "%s(id=%r)" % (self.__class__.__name__, self.id) def __toxcap__(self): xcap_contacts = [contact.__xcapcontact__ for contact in self.contacts] - attributes = dict((name, getattr(self, name)) for name, attr in vars(self.__class__).iteritems() if isinstance(attr, SharedSetting)) + attributes = dict((name, getattr(self, name)) for name, attr in list(vars(self.__class__).items()) if isinstance(attr, SharedSetting)) return XCAPGroup(self.id, self.name, xcap_contacts, **attributes) @run_in_thread('file-io') def _internal_save(self, originator): if self.__state__ == 'deleted': return for contact in [contact for contact in self.contacts if contact.__state__ == 'deleted']: self.contacts.remove(contact) modified_settings = self.get_modified() if not modified_settings and self.__state__ != 'new': return account_manager = AccountManager() configuration = ConfigurationManager() notification_center = NotificationCenter() if originator is Local: originator_account = None previous_xcapgroup = self.__xcapgroup__ else: originator_account = originator.account previous_xcapgroup = originator.xcap_object xcap_accounts = [account for account in account_manager.get_accounts() if account.xcap.discovered] self.__xcapgroup__ = self.__toxcap__() if self.__state__ == 'new': configuration.update(self.__key__, self.__getstate__()) self.__state__ = 'active' for account in (account for account in xcap_accounts if account is not originator_account): account.xcap_manager.add_group(self.__xcapgroup__) modified_data = None notification_center.post_notification('AddressbookGroupWasActivated', sender=self) notification_center.post_notification('AddressbookGroupWasCreated', sender=self) elif all(isinstance(self.__settings__[key], RuntimeSetting) for key in modified_settings): notification_center.post_notification('AddressbookGroupDidChange', sender=self, data=NotificationData(modified=modified_settings)) return else: configuration.update(self.__key__, self.__getstate__()) attributes = self.__xcapgroup__.get_modified(modified_settings) if 'contacts' in modified_settings: added_contacts = [contact.__xcapcontact__ for contact in modified_settings['contacts'].added] removed_contacts = [contact.__xcapcontact__ for contact in modified_settings['contacts'].removed] else: added_contacts = [] removed_contacts = [] if self.__xcapgroup__ != previous_xcapgroup: outofsync_accounts = xcap_accounts elif originator is Local: outofsync_accounts = [] else: outofsync_accounts = list(account for account in xcap_accounts if account is not originator_account) with MultiAccountTransaction(outofsync_accounts): for account in outofsync_accounts: xcap_manager = account.xcap_manager for xcapcontact in added_contacts: xcap_manager.add_group_member(self.__xcapgroup__, xcapcontact) for xcapcontact in removed_contacts: xcap_manager.remove_group_member(self.__xcapgroup__, xcapcontact) if attributes: xcap_manager.update_group(self.__xcapgroup__, attributes) notification_center.post_notification('AddressbookGroupDidChange', sender=self, data=NotificationData(modified=modified_settings)) modified_data = modified_settings try: configuration.save() - except Exception, e: + except Exception as e: log.exception() notification_center.post_notification('CFGManagerSaveFailed', sender=configuration, data=NotificationData(object=self, operation='save', modified=modified_data, exception=e)) @run_in_thread('file-io') def _internal_delete(self, originator): if self.__state__ == 'deleted': return self.__state__ = 'deleted' configuration = ConfigurationManager() account_manager = AccountManager() notification_center = NotificationCenter() if originator is Local: originator_account = None else: originator_account = originator.account configuration.delete(self.__key__) for account in (account for account in account_manager.iter_accounts() if account.xcap.discovered and account is not originator_account): account.xcap_manager.remove_group(self.__xcapgroup__) notification_center.post_notification('AddressbookGroupWasDeleted', sender=self) try: configuration.save() - except Exception, e: + except Exception as e: log.exception() notification_center.post_notification('CFGManagerSaveFailed', sender=configuration, data=NotificationData(object=self, operation='delete', exception=e)) def save(self): """ Store the group into persistent storage (local and xcap). This method will post the AddressbookGroupWasCreated and AddressbookGroupWasActivated notifications on the first save or a AddressbookGroupDidChange notification on subsequent saves, regardless of whether the contact has been saved to persistent storage or not. A CFGManagerSaveFailed notification is posted if saving to the persistent configuration storage fails. """ self._internal_save(originator=Local) def delete(self): """Remove the group from the persistent storage.""" self._internal_delete(originator=Local) def clone(self, new_id=None): """Create a copy of this group and all its sub-settings.""" raise NotImplementedError @classmethod def register_extension(cls, extension): """ Register an extension for this class. All Settings and SettingsGroups defined in the extension will be added to this class, overwriting any attributes with the same name. Other attributes in the extension are ignored. """ if not issubclass(extension, GroupExtension): raise TypeError("expected subclass of GroupExtension, got %r" % (extension,)) for name in dir(extension): attribute = getattr(extension, name, None) if isinstance(attribute, SharedSetting): if SharedSetting.__namespace__ is None: raise RuntimeError("cannot use SharedSetting attributes without first calling SharedSetting.set_namespace") XCAPGroup.__attributes__.add(name) if isinstance(attribute, (AbstractSetting, SettingsGroupMeta)): setattr(cls, name, attribute) class GroupExtension(object): """Base class for extensions of Groups""" def __new__(cls, *args, **kw): raise TypeError("GroupExtension subclasses cannot be instantiated") class ContactURI(SettingsState): __id__ = SettingsObjectImmutableID(type=ID) id = __id__ - uri = Setting(type=unicode, default='') - type = Setting(type=unicode, default=None, nillable=True) + uri = Setting(type=str, default='') + type = Setting(type=str, default=None, nillable=True) def __new__(cls, id=None, **state): if id is None: id = unique_id() - elif not isinstance(id, basestring): + elif not isinstance(id, str): raise TypeError("id needs to be a string or unicode object") instance = SettingsState.__new__(cls) instance.__id__ = id instance.__setstate__(state) return instance def __repr__(self): return "%s(id=%r)" % (self.__class__.__name__, self.id) def __toxcap__(self): - attributes = dict((name, getattr(self, name)) for name, attr in vars(self.__class__).iteritems() if isinstance(attr, SharedSetting)) + attributes = dict((name, getattr(self, name)) for name, attr in list(vars(self.__class__).items()) if isinstance(attr, SharedSetting)) return XCAPContactURI(self.id, self.uri, self.type, **attributes) @classmethod def register_extension(cls, extension): """ Register an extension for this class. All Settings and SettingsGroups defined in the extension will be added to this class, overwriting any attributes with the same name. Other attributes in the extension are ignored. """ if not issubclass(extension, ContactURIExtension): raise TypeError("expected subclass of ContactURIExtension, got %r" % (extension,)) for name in dir(extension): attribute = getattr(extension, name, None) if isinstance(attribute, SharedSetting): if SharedSetting.__namespace__ is None: raise RuntimeError("cannot use SharedSetting attributes without first calling SharedSetting.set_namespace") XCAPContactURI.__attributes__.add(name) if isinstance(attribute, (AbstractSetting, SettingsGroupMeta)): setattr(cls, name, attribute) class ContactURIExtension(object): """Base class for extensions of ContactURIs""" def __new__(cls, *args, **kw): raise TypeError("ContactURIExtension subclasses cannot be instantiated") class DefaultContactURI(Setting): def __init__(self): super(DefaultContactURI, self).__init__(type=str, default=None, nillable=True) def __get__(self, obj, objtype): value = super(DefaultContactURI, self).__get__(obj, objtype) return value if value in (self, None) else obj._item_map.get(value) def __set__(self, obj, value): if value is not None: if not isinstance(value, ContactURI): raise TypeError("the default URI must be a ContactURI instance or None") with obj._lock: if value.id not in obj._item_map: raise ValueError("the default URI can only be set to one of the URIs of the contact") super(DefaultContactURI, self).__set__(obj, value.id) else: super(DefaultContactURI, self).__set__(obj, None) def get_modified(self, obj): modified_value = super(DefaultContactURI, self).get_modified(obj) if modified_value is not None: old_uri = obj._item_map.old.get(modified_value.old) if modified_value.old is not None else None new_uri = obj._item_map.get(modified_value.new) if modified_value.new is not None else None modified_value = ModifiedValue(old=old_uri, new=new_uri) return modified_value def get_old(self, obj): value = super(DefaultContactURI, self).get_old(obj) return value if value is None else obj._item_map.old.get(value) class ContactURIManagement(ItemManagement): def remove_item(self, item, collection): if collection.default is item: collection.default = None def set_items(self, items, collection): if collection.default is not None and collection.default not in items: collection.default = None class ContactURIList(ItemCollection): _item_type = ContactURI _item_management = ContactURIManagement() default = DefaultContactURI() class DialogSettings(SettingsGroup): policy = Setting(type=PolicyValue, default='default') subscribe = Setting(type=bool, default=False) class PresenceSettings(SettingsGroup): policy = Setting(type=PolicyValue, default='default') subscribe = Setting(type=bool, default=False) class Contact(SettingsState): __key__ = AddressbookKey('Contacts') __id__ = SettingsObjectImmutableID(type=ID) id = __id__ - name = Setting(type=unicode, default='') + name = Setting(type=str, default='') uris = ContactURIList dialog = DialogSettings presence = PresenceSettings def __new__(cls, id=None): with AddressbookManager.load.lock: if not AddressbookManager.load.called: raise RuntimeError("cannot instantiate %s before calling AddressbookManager.load" % cls.__name__) if id is None: id = unique_id() - elif not isinstance(id, basestring): + elif not isinstance(id, str): raise TypeError("id needs to be a string or unicode object") instance = SettingsState.__new__(cls) instance.__id__ = id instance.__state__ = 'new' instance.__xcapcontact__ = None configuration = ConfigurationManager() try: data = configuration.get(instance.__key__) except ObjectNotFoundError: pass else: instance.__setstate__(data) instance.__state__ = 'loaded' instance.__xcapcontact__ = instance.__toxcap__() return instance def __establish__(self): if self.__state__ == 'loaded': self.__state__ = 'active' notification_center = NotificationCenter() notification_center.post_notification('AddressbookContactWasActivated', sender=self) def __repr__(self): return "%s(id=%r)" % (self.__class__.__name__, self.id) def __toxcap__(self): contact_uris = xcap.ContactURIList((uri.__toxcap__() for uri in self.uris), default=self.uris.default.id if self.uris.default is not None else None) dialog_handling = xcap.EventHandling(self.dialog.policy, self.dialog.subscribe) presence_handling = xcap.EventHandling(self.presence.policy, self.presence.subscribe) - attributes = dict((name, getattr(self, name)) for name, attr in vars(self.__class__).iteritems() if isinstance(attr, SharedSetting)) + attributes = dict((name, getattr(self, name)) for name, attr in list(vars(self.__class__).items()) if isinstance(attr, SharedSetting)) return XCAPContact(self.id, self.name, contact_uris, presence_handling, dialog_handling, **attributes) @run_in_thread('file-io') def _internal_save(self, originator): if self.__state__ == 'deleted': return modified_settings = self.get_modified() if not modified_settings and self.__state__ != 'new': return account_manager = AccountManager() configuration = ConfigurationManager() notification_center = NotificationCenter() if originator is Local: originator_account = None previous_xcapcontact = self.__xcapcontact__ else: originator_account = originator.account previous_xcapcontact = originator.xcap_object xcap_accounts = [account for account in account_manager.get_accounts() if account.xcap.discovered] self.__xcapcontact__ = self.__toxcap__() if self.__state__ == 'new': configuration.update(self.__key__, self.__getstate__()) self.__state__ = 'active' for account in (account for account in xcap_accounts if account is not originator_account): account.xcap_manager.add_contact(self.__xcapcontact__) modified_data = None notification_center.post_notification('AddressbookContactWasActivated', sender=self) notification_center.post_notification('AddressbookContactWasCreated', sender=self) elif all(isinstance(self.__settings__[key], RuntimeSetting) for key in modified_settings): notification_center.post_notification('AddressbookContactDidChange', sender=self, data=NotificationData(modified=modified_settings)) return else: configuration.update(self.__key__, self.__getstate__()) contact_attributes = self.__xcapcontact__.get_modified(modified_settings) if 'uris' in modified_settings: xcap_uris = self.__xcapcontact__.uris added_uris = [xcap_uris[uri.id] for uri in modified_settings['uris'].added] removed_uris = [uri.__toxcap__() for uri in modified_settings['uris'].removed] - modified_uris = dict((xcap_uris[id], xcap_uris[id].get_modified(changemap)) for id, changemap in modified_settings['uris'].modified.iteritems()) + modified_uris = dict((xcap_uris[id], xcap_uris[id].get_modified(changemap)) for id, changemap in list(modified_settings['uris'].modified.items())) else: added_uris = [] removed_uris = [] modified_uris = {} if self.__xcapcontact__ != previous_xcapcontact: outofsync_accounts = xcap_accounts elif originator is Local: outofsync_accounts = [] else: outofsync_accounts = list(account for account in xcap_accounts if account is not originator_account) with MultiAccountTransaction(outofsync_accounts): for account in outofsync_accounts: xcap_manager = account.xcap_manager for xcapuri in added_uris: xcap_manager.add_contact_uri(self.__xcapcontact__, xcapuri) for xcapuri in removed_uris: xcap_manager.remove_contact_uri(self.__xcapcontact__, xcapuri) - for xcapuri, uri_attributes in modified_uris.iteritems(): + for xcapuri, uri_attributes in list(modified_uris.items()): xcap_manager.update_contact_uri(self.__xcapcontact__, xcapuri, uri_attributes) if contact_attributes: xcap_manager.update_contact(self.__xcapcontact__, contact_attributes) notification_center.post_notification('AddressbookContactDidChange', sender=self, data=NotificationData(modified=modified_settings)) modified_data = modified_settings try: configuration.save() - except Exception, e: + except Exception as e: log.exception() notification_center.post_notification('CFGManagerSaveFailed', sender=configuration, data=NotificationData(object=self, operation='save', modified=modified_data, exception=e)) @run_in_thread('file-io') def _internal_delete(self, originator): if self.__state__ == 'deleted': return self.__state__ = 'deleted' configuration = ConfigurationManager() account_manager = AccountManager() addressbook_manager = AddressbookManager() notification_center = NotificationCenter() if originator is Local: originator_account = None else: originator_account = originator.account configuration.delete(self.__key__) xcap_accounts = [account for account in account_manager.get_accounts() if account.xcap.discovered] with MultiAccountTransaction(xcap_accounts): for group in (group for group in addressbook_manager.get_groups() if self.id in group.contacts): group.contacts.remove(self) group.save() for account in (account for account in xcap_accounts if account is not originator_account): account.xcap_manager.remove_contact(self.__xcapcontact__) notification_center.post_notification('AddressbookContactWasDeleted', sender=self) try: configuration.save() - except Exception, e: + except Exception as e: log.exception() notification_center.post_notification('CFGManagerSaveFailed', sender=configuration, data=NotificationData(object=self, operation='delete', exception=e)) def save(self): """ Store the contact into persistent storage (local and xcap). This method will post the AddressbookContactWasCreated and AddressbookContactWasActivated notifications on the first save or a AddressbookContactDidChange notification on subsequent saves, regardless of whether the contact has been saved to persistent storage or not. A CFGManagerSaveFailed notification is posted if saving to the persistent configuration storage fails. """ self._internal_save(originator=Local) def delete(self): """Remove the contact from the persistent storage.""" self._internal_delete(originator=Local) def clone(self, new_id=None): """Create a copy of this contact and all its sub-settings.""" raise NotImplementedError @classmethod def register_extension(cls, extension): """ Register an extension for this class. All Settings and SettingsGroups defined in the extension will be added to this class, overwriting any attributes with the same name. Other attributes in the extension are ignored. """ if not issubclass(extension, ContactExtension): raise TypeError("expected subclass of ContactExtension, got %r" % (extension,)) for name in dir(extension): attribute = getattr(extension, name, None) if isinstance(attribute, SharedSetting): if SharedSetting.__namespace__ is None: raise RuntimeError("cannot use SharedSetting attributes without first calling SharedSetting.set_namespace") XCAPContact.__attributes__.add(name) if isinstance(attribute, (AbstractSetting, SettingsGroupMeta)): setattr(cls, name, attribute) class ContactExtension(object): """Base class for extensions of Contacts""" def __new__(cls, *args, **kw): raise TypeError("ContactExtension subclasses cannot be instantiated") class Policy(SettingsState): __key__ = AddressbookKey('Policies') __id__ = SettingsObjectImmutableID(type=ID) id = __id__ - uri = Setting(type=unicode, default='') - name = Setting(type=unicode, default='') + uri = Setting(type=str, default='') + name = Setting(type=str, default='') dialog = DialogSettings presence = PresenceSettings def __new__(cls, id=None): with AddressbookManager.load.lock: if not AddressbookManager.load.called: raise RuntimeError("cannot instantiate %s before calling AddressbookManager.load" % cls.__name__) if id is None: id = unique_id() - elif not isinstance(id, basestring): + elif not isinstance(id, str): raise TypeError("id needs to be a string or unicode object") instance = SettingsState.__new__(cls) instance.__id__ = id instance.__state__ = 'new' instance.__xcappolicy__ = None configuration = ConfigurationManager() try: data = configuration.get(instance.__key__) except ObjectNotFoundError: pass else: instance.__setstate__(data) instance.__state__ = 'loaded' instance.__xcappolicy__ = instance.__toxcap__() return instance def __establish__(self): if self.__state__ == 'loaded': self.__state__ = 'active' notification_center = NotificationCenter() notification_center.post_notification('AddressbookPolicyWasActivated', sender=self) def __repr__(self): return "%s(id=%r)" % (self.__class__.__name__, self.id) def __toxcap__(self): dialog_handling = xcap.EventHandling(self.dialog.policy, self.dialog.subscribe) presence_handling = xcap.EventHandling(self.presence.policy, self.presence.subscribe) - attributes = dict((name, getattr(self, name)) for name, attr in vars(self.__class__).iteritems() if isinstance(attr, SharedSetting)) + attributes = dict((name, getattr(self, name)) for name, attr in list(vars(self.__class__).items()) if isinstance(attr, SharedSetting)) return XCAPPolicy(self.id, self.uri, self.name, presence_handling, dialog_handling, **attributes) @run_in_thread('file-io') def _internal_save(self, originator): if self.__state__ == 'deleted': return modified_settings = self.get_modified() if not modified_settings and self.__state__ != 'new': return account_manager = AccountManager() configuration = ConfigurationManager() notification_center = NotificationCenter() if originator is Local: originator_account = None previous_xcappolicy = self.__xcappolicy__ else: originator_account = originator.account previous_xcappolicy = originator.xcap_object xcap_accounts = [account for account in account_manager.get_accounts() if account.xcap.discovered] self.__xcappolicy__ = self.__toxcap__() if self.__state__ == 'new': configuration.update(self.__key__, self.__getstate__()) self.__state__ = 'active' for account in (account for account in xcap_accounts if account is not originator_account): account.xcap_manager.add_policy(self.__xcappolicy__) modified_data = None notification_center.post_notification('AddressbookPolicyWasActivated', sender=self) notification_center.post_notification('AddressbookPolicyWasCreated', sender=self) elif all(isinstance(self.__settings__[key], RuntimeSetting) for key in modified_settings): notification_center.post_notification('AddressbookPolicyDidChange', sender=self, data=NotificationData(modified=modified_settings)) return else: configuration.update(self.__key__, self.__getstate__()) attributes = self.__xcappolicy__.get_modified(modified_settings) if self.__xcappolicy__ != previous_xcappolicy: outofsync_accounts = xcap_accounts elif originator is Local: outofsync_accounts = [] else: outofsync_accounts = list(account for account in xcap_accounts if account is not originator_account) for account in outofsync_accounts: account.xcap_manager.update_policy(self.__xcappolicy__, attributes) notification_center.post_notification('AddressbookPolicyDidChange', sender=self, data=NotificationData(modified=modified_settings)) modified_data = modified_settings try: configuration.save() - except Exception, e: + except Exception as e: log.exception() notification_center.post_notification('CFGManagerSaveFailed', sender=configuration, data=NotificationData(object=self, operation='save', modified=modified_data, exception=e)) @run_in_thread('file-io') def _internal_delete(self, originator): if self.__state__ == 'deleted': return self.__state__ = 'deleted' configuration = ConfigurationManager() account_manager = AccountManager() notification_center = NotificationCenter() if originator is Local: originator_account = None else: originator_account = originator.account configuration.delete(self.__key__) for account in (account for account in account_manager.iter_accounts() if account.xcap.discovered and account is not originator_account): account.xcap_manager.remove_policy(self.__xcappolicy__) notification_center.post_notification('AddressbookPolicyWasDeleted', sender=self) try: configuration.save() - except Exception, e: + except Exception as e: log.exception() notification_center.post_notification('CFGManagerSaveFailed', sender=configuration, data=NotificationData(object=self, operation='delete', exception=e)) def save(self): """ Store the policy into persistent storage (local and xcap). It will post the AddressbookPolicyWasCreated and AddressbookPolicyWasActivated notifications on the first save or a AddressbookPolicyDidChange notification on subsequent saves, regardless of whether the policy has been saved to persistent storage or not. A CFGManagerSaveFailed notification is posted if saving to the persistent configuration storage fails. """ self._internal_save(originator=Local) def delete(self): """Remove the policy from the persistent storage.""" self._internal_delete(originator=Local) def clone(self, new_id=None): """Create a copy of this policy and all its sub-settings.""" raise NotImplementedError @classmethod def register_extension(cls, extension): """ Register an extension for this class. All Settings and SettingsGroups defined in the extension will be added to this class, overwriting any attributes with the same name. Other attributes in the extension are ignored. """ if not issubclass(extension, PolicyExtension): raise TypeError("expected subclass of PolicyExtension, got %r" % (extension,)) for name in dir(extension): attribute = getattr(extension, name, None) if isinstance(attribute, SharedSetting): if SharedSetting.__namespace__ is None: raise RuntimeError("cannot use SharedSetting attributes without first calling SharedSetting.set_namespace") XCAPPolicy.__attributes__.add(name) if isinstance(attribute, (AbstractSetting, SettingsGroupMeta)): setattr(cls, name, attribute) class PolicyExtension(object): """Base class for extensions of Policies""" def __new__(cls, *args, **kw): raise TypeError("PolicyExtension subclasses cannot be instantiated") -class AddressbookManager(object): - __metaclass__ = Singleton - +class AddressbookManager(object, metaclass=Singleton): implements(IObserver) def __init__(self): self.contacts = {} self.groups = {} self.policies = {} self.__xcapaddressbook__ = None notification_center = NotificationCenter() notification_center.add_observer(self, name='AddressbookContactWasActivated') notification_center.add_observer(self, name='AddressbookContactWasDeleted') notification_center.add_observer(self, name='AddressbookGroupWasActivated') notification_center.add_observer(self, name='AddressbookGroupWasDeleted') notification_center.add_observer(self, name='AddressbookPolicyWasActivated') notification_center.add_observer(self, name='AddressbookPolicyWasDeleted') notification_center.add_observer(self, name='SIPAccountDidDiscoverXCAPSupport') notification_center.add_observer(self, name='XCAPManagerDidReloadData') @execute_once def load(self): configuration = ConfigurationManager() # temporary workaround to migrate contacts to the new format. to be removed later. -Dan if 'Contacts' in configuration.data or 'ContactGroups' in configuration.data: account_manager = AccountManager() old_data = dict(contacts=configuration.data.pop('Contacts', {}), groups=configuration.data.pop('ContactGroups', {})) if any(account.enabled and account.xcap.enabled and account.xcap.discovered for account in account_manager.get_accounts()): self.__old_data = old_data else: self.__migrate_contacts(old_data) return [Contact(id=id) for id in configuration.get_names(Contact.__key__)] [Group(id=id) for id in configuration.get_names(Group.__key__)] [Policy(id=id) for id in configuration.get_names(Policy.__key__)] def start(self): pass def stop(self): pass def has_contact(self, id): return id in self.contacts def get_contact(self, id): return self.contacts[id] def get_contacts(self): - return self.contacts.values() + return list(self.contacts.values()) def has_group(self, id): return id in self.groups def get_group(self, id): return self.groups[id] def get_groups(self): - return self.groups.values() + return list(self.groups.values()) def has_policy(self, id): return id in self.policies def get_policy(self, id): return self.policies[id] def get_policies(self): - return self.policies.values() + return list(self.policies.values()) @classmethod def transaction(cls): account_manager = AccountManager() xcap_accounts = [account for account in account_manager.get_accounts() if account.xcap.discovered] return MultiAccountTransaction(xcap_accounts) def handle_notification(self, notification): handler = getattr(self, '_NH_%s' % notification.name, Null) handler(notification) def _NH_AddressbookContactWasActivated(self, notification): contact = notification.sender self.contacts[contact.id] = contact notification.center.post_notification('AddressbookManagerDidAddContact', sender=self, data=NotificationData(contact=contact)) def _NH_AddressbookContactWasDeleted(self, notification): contact = notification.sender del self.contacts[contact.id] notification.center.post_notification('AddressbookManagerDidRemoveContact', sender=self, data=NotificationData(contact=contact)) def _NH_AddressbookGroupWasActivated(self, notification): group = notification.sender self.groups[group.id] = group notification.center.post_notification('AddressbookManagerDidAddGroup', sender=self, data=NotificationData(group=group)) def _NH_AddressbookGroupWasDeleted(self, notification): group = notification.sender del self.groups[group.id] notification.center.post_notification('AddressbookManagerDidRemoveGroup', sender=self, data=NotificationData(group=group)) def _NH_AddressbookPolicyWasActivated(self, notification): policy = notification.sender self.policies[policy.id] = policy notification.center.post_notification('AddressbookManagerDidAddPolicy', sender=self, data=NotificationData(policy=policy)) def _NH_AddressbookPolicyWasDeleted(self, notification): policy = notification.sender del self.policies[policy.id] notification.center.post_notification('AddressbookManagerDidRemovePolicy', sender=self, data=NotificationData(policy=policy)) @run_in_thread('file-io') def _NH_SIPAccountDidDiscoverXCAPSupport(self, notification): xcap_manager = notification.sender.xcap_manager with xcap_manager.transaction(): - for contact in self.contacts.values(): + for contact in list(self.contacts.values()): xcap_manager.add_contact(contact.__xcapcontact__) - for group in self.groups.values(): + for group in list(self.groups.values()): xcap_manager.add_group(group.__xcapgroup__) - for policy in self.policies.values(): + for policy in list(self.policies.values()): xcap_manager.add_policy(policy.__xcappolicy__) @run_in_thread('file-io') def _NH_XCAPManagerDidReloadData(self, notification): if notification.data.addressbook == self.__xcapaddressbook__: return self.__xcapaddressbook__ = notification.data.addressbook xcap_manager = notification.sender xcap_contacts = notification.data.addressbook.contacts xcap_groups = notification.data.addressbook.groups xcap_policies = notification.data.addressbook.policies account_manager = AccountManager() xcap_accounts = [account for account in account_manager.get_accounts() if account.xcap.discovered] # temporary workaround to migrate contacts to the new format. to be removed later. -Dan if hasattr(self, '_AddressbookManager__old_data'): old_data = self.__old_data del self.__old_data if not xcap_contacts and not xcap_groups: self.__migrate_contacts(old_data) return with MultiAccountTransaction(xcap_accounts): # because groups depend on contacts, operation order is add/update contacts, add/update/remove groups & policies, remove contacts -Dan for xcap_contact in xcap_contacts: xcap_contact = XCAPContact.normalize(xcap_contact) try: contact = self.contacts[xcap_contact.id] except KeyError: try: contact = Contact(xcap_contact.id) except DuplicateIDError: log.exception() continue contact.name = xcap_contact.name contact.presence.policy = xcap_contact.presence.policy contact.presence.subscribe = xcap_contact.presence.subscribe contact.dialog.policy = xcap_contact.dialog.policy contact.dialog.subscribe = xcap_contact.dialog.subscribe - for name, value in xcap_contact.attributes.iteritems(): + for name, value in list(xcap_contact.attributes.items()): setattr(contact, name, value) for xcap_uri in xcap_contact.uris: xcap_uri = XCAPContactURI.normalize(xcap_uri) try: uri = contact.uris[xcap_uri.id] except KeyError: try: uri = ContactURI(xcap_uri.id) except DuplicateIDError: log.exception() continue contact.uris.add(uri) uri.uri = xcap_uri.uri uri.type = xcap_uri.type - for name, value in xcap_uri.attributes.iteritems(): + for name, value in list(xcap_uri.attributes.items()): setattr(uri, name, value) for uri in (uri for uri in list(contact.uris) if uri.id not in xcap_contact.uris): contact.uris.remove(uri) contact.uris.default = contact.uris.get(xcap_contact.uris.default, None) contact._internal_save(originator=Remote(xcap_manager.account, xcap_contact)) for xcap_group in xcap_groups: xcap_group = XCAPGroup.normalize(xcap_group) try: group = self.groups[xcap_group.id] except KeyError: try: group = Group(xcap_group.id) except DuplicateIDError: log.exception() continue group.name = xcap_group.name - for name, value in xcap_group.attributes.iteritems(): + for name, value in list(xcap_group.attributes.items()): setattr(group, name, value) old_contact_ids = set(group.contacts.ids()) new_contact_ids = set(xcap_group.contacts.ids()) for contact in (self.contacts[id] for id in new_contact_ids - old_contact_ids): group.contacts.add(contact) for contact in (group.contacts[id] for id in old_contact_ids - new_contact_ids): group.contacts.remove(contact) group._internal_save(originator=Remote(xcap_manager.account, xcap_group)) for xcap_policy in xcap_policies: xcap_policy = XCAPPolicy.normalize(xcap_policy) try: policy = self.policies[xcap_policy.id] except KeyError: try: policy = Policy(xcap_policy.id) except DuplicateIDError: log.exception() continue policy.uri = xcap_policy.uri policy.name = xcap_policy.name policy.presence.policy = xcap_policy.presence.policy policy.presence.subscribe = xcap_policy.presence.subscribe policy.dialog.policy = xcap_policy.dialog.policy policy.dialog.subscribe = xcap_policy.dialog.subscribe - for name, value in xcap_policy.attributes.iteritems(): + for name, value in list(xcap_policy.attributes.items()): setattr(policy, name, value) policy._internal_save(originator=Remote(xcap_manager.account, xcap_policy)) originator = Remote(xcap_manager.account, None) - for policy in (policy for policy in self.policies.values() if policy.id not in xcap_policies): + for policy in (policy for policy in list(self.policies.values()) if policy.id not in xcap_policies): policy._internal_delete(originator=originator) - for group in (group for group in self.groups.values() if group.id not in xcap_groups): + for group in (group for group in list(self.groups.values()) if group.id not in xcap_groups): group._internal_delete(originator=originator) - for contact in (contact for contact in self.contacts.values() if contact.id not in xcap_contacts): + for contact in (contact for contact in list(self.contacts.values()) if contact.id not in xcap_contacts): contact._internal_delete(originator=originator) def __migrate_contacts(self, old_data): account_manager = AccountManager() xcap_accounts = [account for account in account_manager.get_accounts() if account.xcap.discovered] with MultiAccountTransaction(xcap_accounts): # restore the old contacts and groups old_groups = old_data['groups'] old_contacts = old_data['contacts'] group_idmap = {} - for group_id, group_state in old_groups.iteritems(): + for group_id, group_state in list(old_groups.items()): group_idmap[group_id] = group = Group() - for name, value in group_state.iteritems(): + for name, value in list(group_state.items()): try: setattr(group, name, value) except (ValueError, TypeError): pass - for account_id, account_contacts in old_contacts.iteritems(): - for group_id, contact_map in account_contacts.iteritems(): - for uri, contact_data in contact_map.iteritems(): + for account_id, account_contacts in list(old_contacts.items()): + for group_id, contact_map in list(account_contacts.items()): + for uri, contact_data in list(contact_map.items()): contact = Contact() - for name, value in contact_data.iteritems(): + for name, value in list(contact_data.items()): try: setattr(contact, name, value) except (ValueError, TypeError): pass contact.uris.add(ContactURI(uri=uri)) contact.save() group = group_idmap.get(group_id, Null) group.contacts.add(contact) - for group in group_idmap.itervalues(): + for group in list(group_idmap.values()): group.save() diff --git a/sipsimple/application.py b/sipsimple/application.py index 82fc0491..1798f1cd 100644 --- a/sipsimple/application.py +++ b/sipsimple/application.py @@ -1,529 +1,527 @@ """ Implements a high-level application responsible for starting and stopping various sub-systems required to implement a fully featured SIP User Agent application. """ -from __future__ import absolute_import + __all__ = ["SIPApplication"] import os -from application.notification import IObserver, NotificationCenter, NotificationData -from application.python import Null -from application.python.descriptor import classproperty -from application.python.types import Singleton +from .application.notification import IObserver, NotificationCenter, NotificationData +from .application.python import Null +from .application.python.descriptor import classproperty +from .application.python.types import Singleton from eventlib import proc from operator import attrgetter -from threading import RLock, Thread +from .threading import RLock, Thread from twisted.internet import reactor from uuid import uuid4 from xcaplib import client as xcap_client from zope.interface import implements from sipsimple.account import AccountManager from sipsimple.addressbook import AddressbookManager from sipsimple.audio import AudioDevice, RootAudioBridge from sipsimple.configuration import ConfigurationManager from sipsimple.configuration.settings import SIPSimpleSettings from sipsimple.core import AudioMixer, Engine from sipsimple.lookup import DNSManager from sipsimple.session import SessionManager from sipsimple.storage import ISIPSimpleStorage, ISIPSimpleApplicationDataStorage from sipsimple.threading import ThreadManager, run_in_thread, run_in_twisted_thread from sipsimple.threading.green import run_in_green_thread from sipsimple.video import VideoDevice class ApplicationAttribute(object): def __init__(self, value): self.value = value def __get__(self, obj, objtype): return self.value def __set__(self, obj, value): self.value = value def __delete__(self, obj): raise AttributeError('cannot delete attribute') -class SIPApplication(object): - __metaclass__ = Singleton - +class SIPApplication(object, metaclass=Singleton): implements(IObserver) storage = ApplicationAttribute(value=None) engine = ApplicationAttribute(value=None) thread = ApplicationAttribute(value=None) state = ApplicationAttribute(value=None) alert_audio_device = ApplicationAttribute(value=None) alert_audio_bridge = ApplicationAttribute(value=None) voice_audio_device = ApplicationAttribute(value=None) voice_audio_bridge = ApplicationAttribute(value=None) video_device = ApplicationAttribute(value=None) _lock = ApplicationAttribute(value=RLock()) _timer = ApplicationAttribute(value=None) _stop_pending = ApplicationAttribute(value=False) running = classproperty(lambda cls: cls.state == 'started') alert_audio_mixer = classproperty(lambda cls: cls.alert_audio_bridge.mixer if cls.alert_audio_bridge else None) voice_audio_mixer = classproperty(lambda cls: cls.voice_audio_bridge.mixer if cls.voice_audio_bridge else None) def start(self, storage): if not ISIPSimpleStorage.providedBy(storage): raise TypeError("storage must implement the ISIPSimpleStorage interface") with self._lock: if self.state is not None: raise RuntimeError("SIPApplication cannot be started from '%s' state" % self.state) self.state = 'starting' self.engine = Engine() self.storage = storage thread_manager = ThreadManager() thread_manager.start() configuration_manager = ConfigurationManager() addressbook_manager = AddressbookManager() account_manager = AccountManager() # load configuration and initialize core try: configuration_manager.start() SIPSimpleSettings() account_manager.load() addressbook_manager.load() except: self.engine = None self.state = None self.storage = None raise # run the reactor thread self.thread = Thread(name='Reactor Thread', target=self._run_reactor) self.thread.start() def stop(self): with self._lock: if self.state in (None, 'stopping', 'stopped'): return elif self.state == 'starting': self._stop_pending = True return self.state = 'stopping' notification_center = NotificationCenter() notification_center.post_notification('SIPApplicationWillEnd', sender=self) self._shutdown_subsystems() def _run_reactor(self): from eventlib.twistedutil import join_reactor; del join_reactor # imported for the side effect of making the twisted reactor green notification_center = NotificationCenter() notification_center.post_notification('SIPApplicationWillStart', sender=self) with self._lock: stop_pending = self._stop_pending if stop_pending: self.state = 'stopping' if stop_pending: notification_center.post_notification('SIPApplicationWillEnd', sender=self) else: self._initialize_core() reactor.run(installSignalHandlers=False) with self._lock: self.state = 'stopped' notification_center.post_notification('SIPApplicationDidEnd', sender=self) def _initialize_core(self): notification_center = NotificationCenter() settings = SIPSimpleSettings() # initialize core options = dict(# general user_agent=settings.user_agent, # SIP detect_sip_loops=True, 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), # logging log_level=settings.logs.pjsip_level if settings.logs.trace_pjsip else 0, trace_sip=settings.logs.trace_sip) notification_center.add_observer(self, sender=self.engine) self.engine.start(**options) def _initialize_tls(self): settings = SIPSimpleSettings() account_manager = AccountManager() account = account_manager.default_account if account is not None: try: self.engine.set_tls_options(port=settings.sip.tls_port, verify_server=account.tls.verify_server, ca_file=settings.tls.ca_list.normalized if settings.tls.ca_list else None, cert_file=account.tls.certificate.normalized if account.tls.certificate else None, privkey_file=account.tls.certificate.normalized if account.tls.certificate else None) - except Exception, e: + except Exception as e: notification_center = NotificationCenter() notification_center.post_notification('SIPApplicationFailedToStartTLS', sender=self, data=NotificationData(error=e)) @run_in_green_thread def _initialize_subsystems(self): notification_center = NotificationCenter() with self._lock: stop_pending = self._stop_pending if stop_pending: self.state = 'stopping' if stop_pending: notification_center.post_notification('SIPApplicationWillEnd', sender=self) # stop the subsystems we already started: threads, engine and reactor self.engine.stop() self.engine.join(timeout=5) thread_manager = ThreadManager() thread_manager.stop() reactor.stop() return account_manager = AccountManager() addressbook_manager = AddressbookManager() dns_manager = DNSManager() session_manager = SessionManager() settings = SIPSimpleSettings() xcap_client.DEFAULT_HEADERS = {'User-Agent': settings.user_agent} # initialize TLS self._initialize_tls() # initialize PJSIP internal resolver self.engine.set_nameservers(dns_manager.nameservers) # initialize audio objects alert_device = settings.audio.alert_device - if alert_device not in (None, u'system_default') and alert_device not in self.engine.output_devices: - alert_device = u'system_default' + if alert_device not in (None, 'system_default') and alert_device not in self.engine.output_devices: + alert_device = 'system_default' input_device = settings.audio.input_device - if input_device not in (None, u'system_default') and input_device not in self.engine.input_devices: - input_device = u'system_default' + if input_device not in (None, 'system_default') and input_device not in self.engine.input_devices: + input_device = 'system_default' output_device = settings.audio.output_device - if output_device not in (None, u'system_default') and output_device not in self.engine.output_devices: - output_device = u'system_default' + if output_device not in (None, 'system_default') and output_device not in self.engine.output_devices: + output_device = 'system_default' tail_length = settings.audio.echo_canceller.tail_length if settings.audio.echo_canceller.enabled else 0 voice_mixer = AudioMixer(input_device, output_device, settings.audio.sample_rate, tail_length) voice_mixer.muted = settings.audio.muted self.voice_audio_device = AudioDevice(voice_mixer) self.voice_audio_bridge = RootAudioBridge(voice_mixer) self.voice_audio_bridge.add(self.voice_audio_device) alert_mixer = AudioMixer(None, alert_device, settings.audio.sample_rate, 0) if settings.audio.silent: alert_mixer.output_volume = 0 self.alert_audio_device = AudioDevice(alert_mixer) self.alert_audio_bridge = RootAudioBridge(alert_mixer) self.alert_audio_bridge.add(self.alert_audio_device) settings.audio.input_device = voice_mixer.input_device settings.audio.output_device = voice_mixer.output_device settings.audio.alert_device = alert_mixer.output_device # initialize video self.video_device = VideoDevice(settings.video.device, settings.video.resolution, settings.video.framerate) self.video_device.muted = settings.video.muted settings.video.device = self.video_device.name self.engine.set_video_options(settings.video.resolution, settings.video.framerate, settings.video.max_bitrate) self.engine.set_h264_options(settings.video.h264.profile, settings.video.h264.level) # initialize instance id if not settings.instance_id: settings.instance_id = uuid4().urn # initialize path for ZRTP cache file if ISIPSimpleApplicationDataStorage.providedBy(self.storage): self.engine.zrtp_cache = os.path.join(self.storage.directory, 'zrtp.db') # save settings in case something was modified during startup settings.save() # initialize middleware components dns_manager.start() account_manager.start() addressbook_manager.start() session_manager.start() notification_center.add_observer(self, name='CFGSettingsObjectDidChange') notification_center.add_observer(self, name='DNSNameserversDidChange') notification_center.add_observer(self, name='SystemIPAddressDidChange') notification_center.add_observer(self, name='SystemDidWakeUpFromSleep') with self._lock: self.state = 'started' stop_pending = self._stop_pending notification_center.post_notification('SIPApplicationDidStart', sender=self) if stop_pending: self.stop() @run_in_green_thread def _shutdown_subsystems(self): # cleanup internals if self._timer is not None and self._timer.active(): self._timer.cancel() self._timer = None # shutdown middleware components dns_manager = DNSManager() account_manager = AccountManager() addressbook_manager = AddressbookManager() session_manager = SessionManager() procs = [proc.spawn(dns_manager.stop), proc.spawn(account_manager.stop), proc.spawn(addressbook_manager.stop), proc.spawn(session_manager.stop)] proc.waitall(procs) # stop video device self.video_device.producer.close() # shutdown engine self.engine.stop() self.engine.join(timeout=5) # stop threads thread_manager = ThreadManager() thread_manager.stop() # stop the reactor reactor.stop() def _network_conditions_changed(self): if self.running and self._timer is None: def notify(): if self.running: settings = SIPSimpleSettings() if 'tcp' in settings.sip.transport_list: self.engine.set_tcp_port(None) self.engine.set_tcp_port(settings.sip.tcp_port) if 'tls' in settings.sip.transport_list: self._initialize_tls() notification_center = NotificationCenter() notification_center.post_notification('NetworkConditionsDidChange', sender=self) self._timer = None self._timer = reactor.callLater(5, notify) @run_in_twisted_thread def handle_notification(self, notification): handler = getattr(self, '_NH_%s' % notification.name, Null) handler(notification) def _NH_SIPEngineDidStart(self, notification): self._initialize_subsystems() def _NH_SIPEngineDidFail(self, notification): with self._lock: if self.state == 'stopping': return self.state = 'stopping' notification.center.post_notification('SIPApplicationWillEnd', sender=self) # # In theory we need to stop the subsystems here, based on what subsystems are already running according to our state, # but in practice the majority of those subsystems need the engine even to stop and the engine has failed. # # Even the ThreadManager might have threads that try to execute operations on the engine, which could block indefinitely # waiting for an answer that will no longer arrive, thus blocking the ThreadManager stop operation. # # As a result the safest thing to do is to just stop the engine thread and the reactor, which means in this case we # will not cleanup properly (the engine thread should already have ended as a result of the failure, so stopping it # is technically a no-op). # self.engine.stop() self.engine.join(timeout=5) reactor.stop() def _NH_SIPEngineGotException(self, notification): notification.center.post_notification('SIPApplicationGotFatalError', sender=self, data=notification.data) @run_in_thread('device-io') def _NH_CFGSettingsObjectDidChange(self, notification): settings = SIPSimpleSettings() account_manager = AccountManager() if notification.sender is settings: if 'audio.sample_rate' in notification.data.modified: alert_device = settings.audio.alert_device - if alert_device not in (None, u'system_default') and alert_device not in self.engine.output_devices: - alert_device = u'system_default' + if alert_device not in (None, 'system_default') and alert_device not in self.engine.output_devices: + alert_device = 'system_default' input_device = settings.audio.input_device - if input_device not in (None, u'system_default') and input_device not in self.engine.input_devices: - input_device = u'system_default' + if input_device not in (None, 'system_default') and input_device not in self.engine.input_devices: + input_device = 'system_default' output_device = settings.audio.output_device - if output_device not in (None, u'system_default') and output_device not in self.engine.output_devices: - output_device = u'system_default' + if output_device not in (None, 'system_default') and output_device not in self.engine.output_devices: + output_device = 'system_default' tail_length = settings.audio.echo_canceller.tail_length if settings.audio.echo_canceller.enabled else 0 voice_mixer = AudioMixer(input_device, output_device, settings.audio.sample_rate, tail_length) voice_mixer.muted = settings.audio.muted self.voice_audio_device = AudioDevice(voice_mixer) self.voice_audio_bridge = RootAudioBridge(voice_mixer) self.voice_audio_bridge.add(self.voice_audio_device) alert_mixer = AudioMixer(None, alert_device, settings.audio.sample_rate, 0) self.alert_audio_device = AudioDevice(alert_mixer) self.alert_audio_bridge = RootAudioBridge(alert_mixer) self.alert_audio_bridge.add(self.alert_audio_device) if settings.audio.silent: alert_mixer.output_volume = 0 settings.audio.input_device = voice_mixer.input_device settings.audio.output_device = voice_mixer.output_device settings.audio.alert_device = alert_mixer.output_device settings.save() else: if {'audio.input_device', 'audio.output_device', 'audio.alert_device', 'audio.echo_canceller.enabled', 'audio.echo_canceller.tail_length'}.intersection(notification.data.modified): input_device = settings.audio.input_device - if input_device not in (None, u'system_default') and input_device not in self.engine.input_devices: - input_device = u'system_default' + if input_device not in (None, 'system_default') and input_device not in self.engine.input_devices: + input_device = 'system_default' output_device = settings.audio.output_device - if output_device not in (None, u'system_default') and output_device not in self.engine.output_devices: - output_device = u'system_default' + if output_device not in (None, 'system_default') and output_device not in self.engine.output_devices: + output_device = 'system_default' tail_length = settings.audio.echo_canceller.tail_length if settings.audio.echo_canceller.enabled else 0 if (input_device, output_device, tail_length) != attrgetter('input_device', 'output_device', 'ec_tail_length')(self.voice_audio_bridge.mixer): self.voice_audio_bridge.mixer.set_sound_devices(input_device, output_device, tail_length) settings.audio.input_device = self.voice_audio_bridge.mixer.input_device settings.audio.output_device = self.voice_audio_bridge.mixer.output_device settings.save() alert_device = settings.audio.alert_device - if alert_device not in (None, u'system_default') and alert_device not in self.engine.output_devices: - alert_device = u'system_default' + if alert_device not in (None, 'system_default') and alert_device not in self.engine.output_devices: + alert_device = 'system_default' if alert_device != self.alert_audio_bridge.mixer.output_device: self.alert_audio_bridge.mixer.set_sound_devices(None, alert_device, 0) settings.audio.alert_device = self.alert_audio_bridge.mixer.output_device settings.save() if 'audio.muted' in notification.data.modified: self.voice_audio_bridge.mixer.muted = settings.audio.muted if 'audio.silent' in notification.data.modified: if settings.audio.silent: self.alert_audio_bridge.mixer.output_volume = 0 else: self.alert_audio_bridge.mixer.output_volume = 100 if 'video.muted' in notification.data.modified: self.video_device.muted = settings.video.muted if {'video.h264.profile', 'video.h264.level'}.intersection(notification.data.modified): self.engine.set_h264_options(settings.video.h264.profile, settings.video.h264.level) if {'video.device', 'video.resolution', 'video.framerate', 'video.max_bitrate'}.intersection(notification.data.modified): if {'video.device', 'video.resolution', 'video.framerate'}.intersection(notification.data.modified) or settings.video.device != self.video_device.name: self.video_device.set_camera(settings.video.device, settings.video.resolution, settings.video.framerate) settings.video.device = self.video_device.name settings.save() self.engine.set_video_options(settings.video.resolution, settings.video.framerate, settings.video.max_bitrate) if 'user_agent' in notification.data.modified: self.engine.user_agent = settings.user_agent if 'sip.udp_port' in notification.data.modified: self.engine.set_udp_port(settings.sip.udp_port) if 'sip.tcp_port' in notification.data.modified: self.engine.set_tcp_port(settings.sip.tcp_port) if {'sip.tls_port', 'tls.ca_list', 'default_account'}.intersection(notification.data.modified): self._initialize_tls() if 'rtp.port_range' in notification.data.modified: self.engine.rtp_port_range = (settings.rtp.port_range.start, settings.rtp.port_range.end) if 'rtp.audio_codec_list' in notification.data.modified: self.engine.codecs = list(settings.rtp.audio_codec_list) if 'logs.trace_sip' in notification.data.modified: self.engine.trace_sip = settings.logs.trace_sip if {'logs.trace_pjsip', 'logs.pjsip_level'}.intersection(notification.data.modified): self.engine.log_level = settings.logs.pjsip_level if settings.logs.trace_pjsip else 0 elif notification.sender is account_manager.default_account: if {'tls.verify_server', 'tls.certificate'}.intersection(notification.data.modified): self._initialize_tls() @run_in_thread('device-io') def _NH_DefaultAudioDeviceDidChange(self, notification): if None in (self.voice_audio_bridge, self.alert_audio_bridge): return settings = SIPSimpleSettings() current_input_device = self.voice_audio_bridge.mixer.input_device current_output_device = self.voice_audio_bridge.mixer.output_device current_alert_device = self.alert_audio_bridge.mixer.output_device ec_tail_length = self.voice_audio_bridge.mixer.ec_tail_length - if notification.data.changed_input and u'system_default' in (current_input_device, settings.audio.input_device): - self.voice_audio_bridge.mixer.set_sound_devices(u'system_default', current_output_device, ec_tail_length) - if notification.data.changed_output and u'system_default' in (current_output_device, settings.audio.output_device): - self.voice_audio_bridge.mixer.set_sound_devices(current_input_device, u'system_default', ec_tail_length) - if notification.data.changed_output and u'system_default' in (current_alert_device, settings.audio.alert_device): - self.alert_audio_bridge.mixer.set_sound_devices(None, u'system_default', 0) + if notification.data.changed_input and 'system_default' in (current_input_device, settings.audio.input_device): + self.voice_audio_bridge.mixer.set_sound_devices('system_default', current_output_device, ec_tail_length) + if notification.data.changed_output and 'system_default' in (current_output_device, settings.audio.output_device): + self.voice_audio_bridge.mixer.set_sound_devices(current_input_device, 'system_default', ec_tail_length) + if notification.data.changed_output and 'system_default' in (current_alert_device, settings.audio.alert_device): + self.alert_audio_bridge.mixer.set_sound_devices(None, 'system_default', 0) @run_in_thread('device-io') def _NH_AudioDevicesDidChange(self, notification): old_devices = set(notification.data.old_devices) new_devices = set(notification.data.new_devices) removed_devices = old_devices - new_devices if not removed_devices: return input_device = self.voice_audio_bridge.mixer.input_device output_device = self.voice_audio_bridge.mixer.output_device alert_device = self.alert_audio_bridge.mixer.output_device if self.voice_audio_bridge.mixer.real_input_device in removed_devices: - input_device = u'system_default' if new_devices else None + input_device = 'system_default' if new_devices else None if self.voice_audio_bridge.mixer.real_output_device in removed_devices: - output_device = u'system_default' if new_devices else None + output_device = 'system_default' if new_devices else None if self.alert_audio_bridge.mixer.real_output_device in removed_devices: - alert_device = u'system_default' if new_devices else None + alert_device = 'system_default' if new_devices else None self.voice_audio_bridge.mixer.set_sound_devices(input_device, output_device, self.voice_audio_bridge.mixer.ec_tail_length) self.alert_audio_bridge.mixer.set_sound_devices(None, alert_device, 0) settings = SIPSimpleSettings() settings.audio.input_device = self.voice_audio_bridge.mixer.input_device settings.audio.output_device = self.voice_audio_bridge.mixer.output_device settings.audio.alert_device = self.alert_audio_bridge.mixer.output_device settings.save() @run_in_thread('device-io') def _NH_VideoDevicesDidChange(self, notification): old_devices = set(notification.data.old_devices) new_devices = set(notification.data.new_devices) removed_devices = old_devices - new_devices if not removed_devices: return device = self.video_device.name if self.video_device.real_name in removed_devices: - device = u'system_default' if new_devices else None + device = 'system_default' if new_devices else None settings = SIPSimpleSettings() self.video_device.set_camera(device, settings.video.resolution, settings.video.framerate) settings.video.device = self.video_device.name settings.save() def _NH_DNSNameserversDidChange(self, notification): if self.running: self.engine.set_nameservers(notification.data.nameservers) notification.center.post_notification('NetworkConditionsDidChange', sender=self) def _NH_SystemIPAddressDidChange(self, notification): self._network_conditions_changed() def _NH_SystemDidWakeUpFromSleep(self, notification): self._network_conditions_changed() diff --git a/sipsimple/audio.py b/sipsimple/audio.py index 66fd36fb..a77edf58 100644 --- a/sipsimple/audio.py +++ b/sipsimple/audio.py @@ -1,541 +1,541 @@ """Audio support""" -from __future__ import absolute_import + __all__ = ['IAudioPort', 'AudioDevice', 'AudioBridge', 'RootAudioBridge', 'AudioConference', 'WavePlayer', 'WavePlayerError', 'WaveRecorder'] import os import weakref from functools import partial from itertools import combinations -from threading import RLock +from .threading import RLock -from application.notification import IObserver, NotificationCenter, NotificationData, ObserverWeakrefProxy -from application.system import makedirs +from .application.notification import IObserver, NotificationCenter, NotificationData, ObserverWeakrefProxy +from .application.system import makedirs from eventlib import coros from twisted.internet import reactor from zope.interface import Attribute, Interface, implements from sipsimple.core import MixerPort, RecordingWaveFile, SIPCoreError, WaveFile from sipsimple.threading import run_in_twisted_thread from sipsimple.threading.green import Command, run_in_green_thread, run_in_waitable_green_thread class WavePlayerError(Exception): pass class IAudioPort(Interface): """ Interface describing an object which can produce and/or consume audio data. If an object cannot produce audio data, its producer_slot attribute must be None; similarly, if an object cannot consume audio data, its consumer_slot attribute must be None. As part of the interface, whenever an IAudioPort implementation changes its slot attributes, it must send a AudioPortDidChangeSlots notification with the following attributes in the notification data: * consumer_slot_changed * producer_slot_changed * old_consumer_slot (only required if consumer_slot_changed is True) * new_consumer_slot (only required if consumer_slot_changed is True) * old_producer_slot (only required if producer_slot_changed is True) * new_producer_slot (only required if producer_slot_changed is True) All attributes of this interface are read-only. """ mixer = Attribute("The mixer that is responsible for mixing the audio data to/from this audio port") consumer_slot = Attribute("The slot to which audio data can be written") producer_slot = Attribute("The slot from which audio data can be read") class AudioDevice(object): """ Objects of this class represent an audio device which can be used in an AudioBridge as they implement the IAudioPort interface. Since a mixer is connected to an audio device which provides the mixer's clock, an AudioDevice constructed for a specific mixer represents the device that mixer is using. """ implements(IAudioPort) def __init__(self, mixer, input_muted=False, output_muted=False): self.mixer = mixer self.__dict__['input_muted'] = input_muted self.__dict__['output_muted'] = output_muted @property def consumer_slot(self): return 0 if not self.output_muted else None @property def producer_slot(self): return 0 if not self.input_muted else None @property def input_muted(self): return self.__dict__['input_muted'] @input_muted.setter def input_muted(self, value): if not isinstance(value, bool): raise ValueError('illegal value for input_muted property: %r' % (value,)) if value == self.input_muted: return old_producer_slot = self.producer_slot self.__dict__['input_muted'] = value notification_center = NotificationCenter() notification_center.post_notification('AudioPortDidChangeSlots', sender=self, data=NotificationData(consumer_slot_changed=False, producer_slot_changed=True, old_producer_slot=old_producer_slot, new_producer_slot=self.producer_slot)) @property def output_muted(self): return self.__dict__['output_muted'] @output_muted.setter def output_muted(self, value): if not isinstance(value, bool): raise ValueError('illegal value for output_muted property: %r' % (value,)) if value == self.output_muted: return old_consumer_slot = self.consumer_slot self.__dict__['output_muted'] = value notification_center = NotificationCenter() notification_center.post_notification('AudioPortDidChangeSlots', sender=self, data=NotificationData(consumer_slot_changed=True, producer_slot_changed=False, old_consumer_slot=old_consumer_slot, new_consumer_slot=self.consumer_slot)) class AudioBridge(object): """ An AudioBridge is a container for objects providing the IAudioPort interface. It connects all such objects in a full-mesh such that all audio producers are connected to all consumers. AudioBridge implements the IAudioPort interface which means a bridge can contain another bridge. This must be done such that the resulting structure is a tree (i.e. no loops are allowed). All leafs of the tree will be connected as if they were the children of a single bridge. """ implements(IAudioPort, IObserver) def __init__(self, mixer): self._lock = RLock() self.ports = set() self.mixer = mixer self.multiplexer = MixerPort(mixer) self.demultiplexer = MixerPort(mixer) self.multiplexer.start() self.demultiplexer.start() notification_center = NotificationCenter() notification_center.add_observer(ObserverWeakrefProxy(self), name='AudioPortDidChangeSlots') def __del__(self): self.multiplexer.stop() self.demultiplexer.stop() if len(self.ports) >= 2: for port1, port2 in ((wr1(), wr2()) for wr1, wr2 in combinations(self.ports, 2)): if port1 is None or port2 is None: continue if port1.producer_slot is not None and port2.consumer_slot is not None: self.mixer.disconnect_slots(port1.producer_slot, port2.consumer_slot) if port2.producer_slot is not None and port1.consumer_slot is not None: self.mixer.disconnect_slots(port2.producer_slot, port1.consumer_slot) self.ports.clear() def __contains__(self, port): return weakref.ref(port) in self.ports @property def consumer_slot(self): return self.demultiplexer.slot if self.demultiplexer.is_active else None @property def producer_slot(self): return self.multiplexer.slot if self.multiplexer.is_active else None def add(self, port): with self._lock: if not IAudioPort.providedBy(port): raise TypeError("expected object implementing IAudioPort, got %s" % port.__class__.__name__) if port.mixer is not self.mixer: raise ValueError("expected port with Mixer %r, got %r" % (self.mixer, port.mixer)) if weakref.ref(port) in self.ports: return if port.consumer_slot is not None and self.demultiplexer.slot is not None: self.mixer.connect_slots(self.demultiplexer.slot, port.consumer_slot) if port.producer_slot is not None and self.multiplexer.slot is not None: self.mixer.connect_slots(port.producer_slot, self.multiplexer.slot) for other in (wr() for wr in self.ports): if other is None: continue if other.producer_slot is not None and port.consumer_slot is not None: self.mixer.connect_slots(other.producer_slot, port.consumer_slot) if port.producer_slot is not None and other.consumer_slot is not None: self.mixer.connect_slots(port.producer_slot, other.consumer_slot) # This hack is required because a weakly referenced object keeps a # strong reference to weak references of itself and thus to any # callbacks registered in those weak references. To be more # precise, we don't want the port to have a strong reference to # ourselves. -Luci self.ports.add(weakref.ref(port, partial(self._remove_port, weakref.ref(self)))) def remove(self, port): with self._lock: if weakref.ref(port) not in self.ports: raise ValueError("port %r is not part of this bridge" % port) if port.consumer_slot is not None and self.demultiplexer.slot is not None: self.mixer.disconnect_slots(self.demultiplexer.slot, port.consumer_slot) if port.producer_slot is not None and self.multiplexer.slot is not None: self.mixer.disconnect_slots(port.producer_slot, self.multiplexer.slot) for other in (wr() for wr in self.ports): if other is None: continue if other.producer_slot is not None and port.consumer_slot is not None: self.mixer.disconnect_slots(other.producer_slot, port.consumer_slot) if port.producer_slot is not None and other.consumer_slot is not None: self.mixer.disconnect_slots(port.producer_slot, other.consumer_slot) self.ports.remove(weakref.ref(port)) def stop(self): with self._lock: for port1 in (wr() for wr in self.ports): if port1 is None: continue for port2 in (wr() for wr in self.ports): if port2 is None or port2 is port1: continue if port1.producer_slot is not None and port2.consumer_slot is not None: self.mixer.disconnect_slots(port1.producer_slot, port2.consumer_slot) if port2.producer_slot is not None and port1.consumer_slot is not None: self.mixer.disconnect_slots(port2.producer_slot, port1.consumer_slot) self.ports.clear() self.multiplexer.stop() self.demultiplexer.stop() def handle_notification(self, notification): with self._lock: if weakref.ref(notification.sender) not in self.ports: return if notification.data.consumer_slot_changed: if notification.data.old_consumer_slot is not None and self.demultiplexer.slot is not None: self.mixer.disconnect_slots(self.demultiplexer.slot, notification.data.old_consumer_slot) if notification.data.new_consumer_slot is not None and self.demultiplexer.slot is not None: self.mixer.connect_slots(self.demultiplexer.slot, notification.data.new_consumer_slot) for other in (wr() for wr in self.ports): if other is None or other is notification.sender or other.producer_slot is None: continue if notification.data.old_consumer_slot is not None: self.mixer.disconnect_slots(other.producer_slot, notification.data.old_consumer_slot) if notification.data.new_consumer_slot is not None: self.mixer.connect_slots(other.producer_slot, notification.data.new_consumer_slot) if notification.data.producer_slot_changed: if notification.data.old_producer_slot is not None and self.multiplexer.slot is not None: self.mixer.disconnect_slots(notification.data.old_producer_slot, self.multiplexer.slot) if notification.data.new_producer_slot is not None and self.multiplexer.slot is not None: self.mixer.connect_slots(notification.data.new_producer_slot, self.multiplexer.slot) for other in (wr() for wr in self.ports): if other is None or other is notification.sender or other.consumer_slot is None: continue if notification.data.old_producer_slot is not None: self.mixer.disconnect_slots(notification.data.old_producer_slot, other.consumer_slot) if notification.data.new_producer_slot is not None: self.mixer.connect_slots(notification.data.new_producer_slot, other.consumer_slot) @staticmethod def _remove_port(selfwr, portwr): self = selfwr() if self is not None: with self._lock: self.ports.discard(portwr) class RootAudioBridge(object): """ A RootAudioBridge is a container for objects providing the IAudioPort interface. It connects all such objects in a full-mesh such that all audio producers are connected to all consumers. The difference between a RootAudioBridge and an AudioBridge is that the RootAudioBridge does not implement the IAudioPort interface. This makes it more efficient. """ implements(IObserver) def __init__(self, mixer): self.mixer = mixer self.ports = set() self._lock = RLock() notification_center = NotificationCenter() notification_center.add_observer(ObserverWeakrefProxy(self), name='AudioPortDidChangeSlots') def __del__(self): if len(self.ports) >= 2: for port1, port2 in ((wr1(), wr2()) for wr1, wr2 in combinations(self.ports, 2)): if port1 is None or port2 is None: continue if port1.producer_slot is not None and port2.consumer_slot is not None: self.mixer.disconnect_slots(port1.producer_slot, port2.consumer_slot) if port2.producer_slot is not None and port1.consumer_slot is not None: self.mixer.disconnect_slots(port2.producer_slot, port1.consumer_slot) self.ports.clear() def __contains__(self, port): return weakref.ref(port) in self.ports def add(self, port): with self._lock: if not IAudioPort.providedBy(port): raise TypeError("expected object implementing IAudioPort, got %s" % port.__class__.__name__) if port.mixer is not self.mixer: raise ValueError("expected port with Mixer %r, got %r" % (self.mixer, port.mixer)) if weakref.ref(port) in self.ports: return for other in (wr() for wr in self.ports): if other is None: continue if other.producer_slot is not None and port.consumer_slot is not None: self.mixer.connect_slots(other.producer_slot, port.consumer_slot) if port.producer_slot is not None and other.consumer_slot is not None: self.mixer.connect_slots(port.producer_slot, other.consumer_slot) # This hack is required because a weakly referenced object keeps a # strong reference to weak references of itself and thus to any # callbacks registered in those weak references. To be more # precise, we don't want the port to have a strong reference to # ourselves. -Luci self.ports.add(weakref.ref(port, partial(self._remove_port, weakref.ref(self)))) def remove(self, port): with self._lock: if weakref.ref(port) not in self.ports: raise ValueError("port %r is not part of this bridge" % port) for other in (wr() for wr in self.ports): if other is None: continue if other.producer_slot is not None and port.consumer_slot is not None: self.mixer.disconnect_slots(other.producer_slot, port.consumer_slot) if port.producer_slot is not None and other.consumer_slot is not None: self.mixer.disconnect_slots(port.producer_slot, other.consumer_slot) self.ports.remove(weakref.ref(port)) def handle_notification(self, notification): with self._lock: if weakref.ref(notification.sender) not in self.ports: return if notification.data.consumer_slot_changed: for other in (wr() for wr in self.ports): if other is None or other is notification.sender or other.producer_slot is None: continue if notification.data.old_consumer_slot is not None: self.mixer.disconnect_slots(other.producer_slot, notification.data.old_consumer_slot) if notification.data.new_consumer_slot is not None: self.mixer.connect_slots(other.producer_slot, notification.data.new_consumer_slot) if notification.data.producer_slot_changed: for other in (wr() for wr in self.ports): if other is None or other is notification.sender or other.consumer_slot is None: continue if notification.data.old_producer_slot is not None: self.mixer.disconnect_slots(notification.data.old_producer_slot, other.consumer_slot) if notification.data.new_producer_slot is not None: self.mixer.connect_slots(notification.data.new_producer_slot, other.consumer_slot) @staticmethod def _remove_port(selfwr, portwr): self = selfwr() if self is not None: with self._lock: self.ports.discard(portwr) class AudioConference(object): def __init__(self): from sipsimple.application import SIPApplication mixer = SIPApplication.voice_audio_mixer self.bridge = RootAudioBridge(mixer) self.device = AudioDevice(mixer) self.on_hold = False self.streams = [] self._lock = RLock() self.bridge.add(self.device) def add(self, stream): with self._lock: if stream in self.streams: return stream.bridge.remove(stream.device) self.bridge.add(stream.bridge) self.streams.append(stream) def remove(self, stream): with self._lock: self.streams.remove(stream) self.bridge.remove(stream.bridge) stream.bridge.add(stream.device) def hold(self): with self._lock: if self.on_hold: return self.bridge.remove(self.device) self.on_hold = True def unhold(self): with self._lock: if not self.on_hold: return self.bridge.add(self.device) self.on_hold = False class WavePlayer(object): """ An object capable of playing a WAV file. It can be used as part of an AudioBridge as it implements the IAudioPort interface. """ implements(IAudioPort, IObserver) def __init__(self, mixer, filename, volume=100, loop_count=1, pause_time=0, initial_delay=0): self.mixer = mixer self.filename = filename self.initial_delay = initial_delay self.loop_count = loop_count self.pause_time = pause_time self.volume = volume self._channel = None self._current_loop = 0 self._state = 'stopped' self._wave_file = None @property def is_active(self): return self._state == "started" @property def consumer_slot(self): return None @property def producer_slot(self): return self._wave_file.slot if self._wave_file else None def start(self): self.play() @run_in_green_thread # run stop in a green thread in order to be similar with start/play. this avoids start/stop running out of order. def stop(self): if self._state != 'started': return self._channel.send(Command('stop')) @run_in_waitable_green_thread def play(self): if self._state != 'stopped': raise WavePlayerError('already playing') self._state = 'started' self._channel = coros.queue() self._current_loop = 0 if self.initial_delay: reactor.callLater(self.initial_delay, self._channel.send, Command('play')) else: self._channel.send(Command('play')) self._run().wait() @run_in_waitable_green_thread def _run(self): notification_center = NotificationCenter() try: while True: command = self._channel.wait() if command.name == 'play': self._wave_file = WaveFile(self.mixer, self.filename) notification_center.add_observer(self, sender=self._wave_file, name='WaveFileDidFinishPlaying') self._wave_file.volume = self.volume try: self._wave_file.start() - except SIPCoreError, e: + except SIPCoreError as e: notification_center.post_notification('WavePlayerDidFail', sender=self, data=NotificationData(error=e)) raise WavePlayerError(e) else: if self._current_loop == 0: notification_center.post_notification('WavePlayerDidStart', sender=self) notification_center.post_notification('AudioPortDidChangeSlots', sender=self, data=NotificationData(consumer_slot_changed=False, producer_slot_changed=True, old_producer_slot=None, new_producer_slot=self._wave_file.slot)) elif command.name == 'reschedule': self._current_loop += 1 notification_center.remove_observer(self, sender=self._wave_file, name='WaveFileDidFinishPlaying') self._wave_file = None notification_center.post_notification('AudioPortDidChangeSlots', sender=self, data=NotificationData(consumer_slot_changed=False, producer_slot_changed=True, old_producer_slot=None, new_producer_slot=None)) if self.loop_count == 0 or self._current_loop < self.loop_count: reactor.callLater(self.pause_time, self._channel.send, Command('play')) else: notification_center.post_notification('WavePlayerDidEnd', sender=self) break elif command.name == 'stop': if self._wave_file is not None: notification_center.remove_observer(self, sender=self._wave_file, name='WaveFileDidFinishPlaying') self._wave_file.stop() self._wave_file = None notification_center.post_notification('AudioPortDidChangeSlots', sender=self, data=NotificationData(consumer_slot_changed=False, producer_slot_changed=True, old_producer_slot=None, new_producer_slot=None)) notification_center.post_notification('WavePlayerDidEnd', sender=self) break finally: self._channel = None self._state = 'stopped' @run_in_twisted_thread def handle_notification(self, notification): if self._channel is not None: self._channel.send(Command('reschedule')) class WaveRecorder(object): """ An object capable of recording to a WAV file. It can be used as part of an AudioBridge as it implements the IAudioPort interface. """ implements(IAudioPort) def __init__(self, mixer, filename): self.mixer = mixer self.filename = filename self._recording_wave_file = None @property def is_active(self): return bool(self._recording_wave_file and self._recording_wave_file.is_active) @property def consumer_slot(self): return self._recording_wave_file.slot if self._recording_wave_file else None @property def producer_slot(self): return None def start(self): # There is still a race condition here in that the directory can be removed # before the PJSIP opens the file. There's nothing that can be done about # it as long as PJSIP doesn't accept an already open file descriptor. -Luci makedirs(os.path.dirname(self.filename)) self._recording_wave_file = RecordingWaveFile(self.mixer, self.filename) self._recording_wave_file.start() notification_center = NotificationCenter() notification_center.post_notification('AudioPortDidChangeSlots', sender=self, data=NotificationData(consumer_slot_changed=True, producer_slot_changed=False, old_consumer_slot=None, new_consumer_slot=self._recording_wave_file.slot)) def stop(self): old_slot = self.consumer_slot self._recording_wave_file.stop() self._recording_wave_file = None notification_center = NotificationCenter() notification_center.post_notification('AudioPortDidChangeSlots', sender=self, data=NotificationData(consumer_slot_changed=True, producer_slot_changed=False, old_consumer_slot=old_slot, new_consumer_slot=None)) diff --git a/sipsimple/configuration/__init__.py b/sipsimple/configuration/__init__.py index 0544da55..cc82a307 100644 --- a/sipsimple/configuration/__init__.py +++ b/sipsimple/configuration/__init__.py @@ -1,1006 +1,996 @@ """Generic configuration management""" __all__ = ['ConfigurationManager', 'ConfigurationError', 'ObjectNotFoundError', 'DuplicateIDError', 'SettingsObjectID', 'SettingsObjectImmutableID', 'AbstractSetting', 'Setting', 'CorrelatedSetting', 'RuntimeSetting', 'SettingsStateMeta', 'SettingsState', 'SettingsGroup', 'ItemCollection', 'SettingsObject', 'SettingsObjectExtension', 'DefaultValue', 'ModifiedValue', 'ModifiedList', 'PersistentKey', 'ItemContainer', 'ItemManagement'] from abc import ABCMeta, abstractmethod from itertools import chain from operator import attrgetter from threading import Lock from weakref import WeakSet from application.notification import NotificationCenter, NotificationData from application.python.descriptor import isdescriptor from application.python.types import Singleton from application.python.weakref import weakobjectmap from sipsimple import log from sipsimple.threading import run_in_thread +from functools import reduce ## Exceptions class ConfigurationError(Exception): pass class ObjectNotFoundError(ConfigurationError): pass class DuplicateIDError(ValueError): pass ## Structure markers -class PersistentKey(unicode): +class PersistentKey(str): def __repr__(self): - return "%s(%s)" % (self.__class__.__name__, unicode.__repr__(self)) + return "%s(%s)" % (self.__class__.__name__, str.__repr__(self)) class ItemContainer(dict): def __repr__(self): return "%s(%s)" % (self.__class__.__name__, dict.__repr__(self)) ## ConfigurationManager -class ConfigurationManager(object): +class ConfigurationManager(object, metaclass=Singleton): """ Singleton class used for storing and retrieving options, organized in sections. A section contains a list of objects, each with an assigned name which allows access to the object. """ - __metaclass__ = Singleton def __init__(self): self.backend = None self.data = None def start(self): """ Initialize the ConfigurationManager to use the specified backend. This method can only be called once, with an object which provides IBackend. The other methods of the object cannot be used unless this method was called. """ from sipsimple.application import SIPApplication from sipsimple.configuration.backend import IConfigurationBackend if self.backend is not None: raise RuntimeError("ConfigurationManager already started") if SIPApplication.storage is None: raise RuntimeError("SIPApplication.storage must be defined before starting the ConfigurationManager") backend = SIPApplication.storage.configuration_backend if not IConfigurationBackend.providedBy(backend): raise TypeError("SIPApplication.storage.configuration_backend must implement the IConfigurationBackend interface") self.data = backend.load() self.backend = backend def update(self, key, data): """ Save the object's data under the tree path specified by key (a list of strings). Cannot be called before start(). """ if self.backend is None: raise RuntimeError("ConfigurationManager cannot be used unless started") if not key: raise KeyError("key cannot be empty") self._update(self.data, list(key), data) def rename(self, old_key, new_key): """ Rename the object identified by old_key to new_key (list of strings). Cannot be called before start(). """ if self.backend is None: raise RuntimeError("ConfigurationManager cannot be used unless started") if not old_key or not new_key: raise KeyError("old_key and/or new_key cannot be empty") try: data = self._pop(self.data, list(old_key)) except KeyError: raise ObjectNotFoundError("object %s does not exist" % '/'.join(old_key)) self._insert(self.data, list(new_key), data) def delete(self, key): """ Delete the object in the tree path specified by key (list of strings). Cannot be called before start(). """ if self.backend is None: raise RuntimeError("ConfigurationManager cannot be used unless started") if not key: raise KeyError("key cannot be empty") try: self._pop(self.data, list(key)) except KeyError: pass def get(self, key): """ Get the object in the tree path specified by key (list of strings). Raises ObjectNotFoundError if the object does not exist. Cannot be called before start(). """ if self.backend is None: raise RuntimeError("ConfigurationManager cannot be used unless started") if not key: raise KeyError("key cannot be empty") try: return self._get(self.data, list(key)) except KeyError: raise ObjectNotFoundError("object %s does not exist" % '/'.join(key)) def get_names(self, key): """ Get all the names under the specified key (a list of strings). Returns a list containing the names. Cannot be called before start(). """ if self.backend is None: raise RuntimeError("ConfigurationManager cannot be used unless started") if not key: raise KeyError("key cannot be empty") try: data = self._get(self.data, list(key)) - return data.keys() + return list(data.keys()) except KeyError: return [] def save(self): """ Flush the modified objects. Cannot be called before start(). """ if self.backend is None: raise RuntimeError("ConfigurationManager cannot be used unless started") self.backend.save(self.data) def _get(self, data_tree, key): subtree_key = key.pop(0) data_subtree = data_tree[subtree_key] if key: return self._get(data_subtree, key) else: return data_subtree def _insert(self, data_tree, key, data): subtree_key = key.pop(0) data_subtree = data_tree.setdefault(subtree_key, {}) if key: self._insert(data_subtree, key, data) else: data_subtree.update(data) def _pop(self, data_tree, key): subtree_key = key.pop(0) data_subtree = data_tree[subtree_key] if key: data = self._pop(data_subtree, key) if not isinstance(subtree_key, PersistentKey) and not data_subtree: del data_tree[subtree_key] return data else: return data_tree.pop(subtree_key) def _update(self, data_tree, key, data): subtree_key = key.pop(0) data_subtree = data_tree.setdefault(subtree_key, {}) if key: self._update(data_subtree, key, data) else: self._update_dict(data_subtree, data) if not isinstance(subtree_key, PersistentKey) and not data_subtree: del data_tree[subtree_key] def _update_dict(self, old_data, new_data): - for key, value in new_data.iteritems(): + for key, value in list(new_data.items()): if value is DefaultValue: old_data.pop(key, None) elif isinstance(value, ItemContainer): if key not in old_data or type(old_data[key]) is not dict: old_data[key] = {} key_subtree = old_data[key] for removed_key in set(key_subtree) - set(value): del key_subtree[removed_key] self._update_dict(key_subtree, value) if not key_subtree: del old_data[key] elif type(value) is dict: if key in old_data and type(old_data[key]) is not dict: del old_data[key] self._update_dict(old_data.setdefault(key, {}), value) if not old_data[key]: del old_data[key] else: old_data[key] = value # Descriptors and base classes used for representing configuration settings class DefaultValue(object): """ This object can be set as the value for a setting and it will reset the setting to the default value. """ class ModifiedValue(object): """ Instances of this class represent the state (the old and new values) of settings. """ __slots__ = ('old', 'new') def __init__(self, old, new): self.old = old self.new = new def __repr__(self): return '%s(old=%r, new=%r)' % (self.__class__.__name__, self.old, self.new) class ModifiedList(object): """ Represents the modified state (added, removed, modified) of list like settings. """ __slots__ = ('added', 'removed', 'modified') def __init__(self, added, removed, modified): self.added = added self.removed = removed self.modified = modified def __repr__(self): return '%s(added=%r, removed=%r, modified=%r)' % (self.__class__.__name__, self.added, self.removed, self.modified) class SettingsObjectID(object): """ Descriptor for dynamic configuration object IDs. """ def __init__(self, type): self.type = type self.values = weakobjectmap() self.oldvalues = weakobjectmap() self.dirty = weakobjectmap() self.lock = Lock() def __get__(self, obj, objtype): return self if obj is None else self.values[obj] def __set__(self, obj, value): with self.lock: if not isinstance(value, self.type): value = self.type(value) if obj in self.values and self.values[obj] == value: return if obj in self.oldvalues and self.oldvalues[obj] == value: self.values[obj] = self.oldvalues[obj] self.dirty[obj] = False return try: - other_obj = (key for key, val in chain(self.values.iteritems(), self.oldvalues.iteritems()) if val==value).next() + other_obj = next((key for key, val in chain(iter(list(self.values.items())), iter(list(self.oldvalues.items()))) if val==value)) except StopIteration: pass else: raise DuplicateIDError('SettingsObject ID already used by another %s' % other_obj.__class__.__name__) if obj in self.values: self.values[obj] = value self.dirty[obj] = True else: self.values[obj] = self.oldvalues[obj] = value self.dirty[obj] = False def __delete__(self, obj): raise AttributeError('cannot delete attribute') def get_modified(self, obj): """ Returns a ModifiedValue instance with references to the old and new values or None if not modified. """ with self.lock: try: if self.dirty.get(obj, False): return ModifiedValue(old=self.oldvalues[obj], new=self.values[obj]) else: return None finally: self.oldvalues[obj] = self.values[obj] self.dirty[obj] = False def get_old(self, obj): return self.oldvalues[obj] def undo(self, obj): with self.lock: self.values[obj] = self.oldvalues[obj] self.dirty[obj] = False class SettingsObjectImmutableID(object): """ Descriptor for immutable runtime allocated configuration object IDs. """ def __init__(self, type): self.type = type self.values = weakobjectmap() self.lock = Lock() def __get__(self, obj, objtype): return self if obj is None else self.values[obj] def __set__(self, obj, value): with self.lock: if obj in self.values: raise AttributeError('attribute is read-only') if not isinstance(value, self.type): value = self.type(value) try: - other_obj = (key for key, val in self.values.iteritems() if val==value).next() + other_obj = next((key for key, val in list(self.values.items()) if val==value)) except StopIteration: pass else: raise DuplicateIDError('SettingsObject ID already used by another %s' % other_obj.__class__.__name__) self.values[obj] = value def __delete__(self, obj): raise AttributeError('cannot delete attribute') -class AbstractSetting(object): +class AbstractSetting(object, metaclass=ABCMeta): """Abstract base class for setting type descriptors""" - __metaclass__ = ABCMeta - @abstractmethod def __get__(self, obj, objtype): raise NotImplementedError @abstractmethod def __set__(self, obj, value): raise NotImplementedError def __delete__(self, obj): raise AttributeError('cannot delete attribute') @abstractmethod def __getstate__(self, obj): raise NotImplementedError @abstractmethod def __setstate__(self, obj, value): raise NotImplementedError @abstractmethod def get_modified(self, obj): raise NotImplementedError @abstractmethod def get_old(self, obj): raise NotImplementedError @abstractmethod def undo(self, obj): raise NotImplementedError class Setting(AbstractSetting): """ Descriptor representing a setting in a configuration object. If a setting is set to the object DefaultValue, it will be reset to the default. Also, only Setting attributes with nillable=True can be assigned the value None. All other values are passed to the type specified. """ def __init__(self, type, default=None, nillable=False): if default is None and not nillable: raise TypeError("default must be specified if object is not nillable") self.type = type self.default = default self.nillable = nillable self.values = weakobjectmap() self.oldvalues = weakobjectmap() self.dirty = weakobjectmap() self.lock = Lock() def __get__(self, obj, objtype): if obj is None: return self return self.values.get(obj, self.default) def __set__(self, obj, value): with self.lock: if value is None and not self.nillable: raise ValueError("setting attribute is not nillable") if value is DefaultValue: if obj in self.values: self.values.pop(obj) self.dirty[obj] = obj in self.oldvalues return if value is not None and not isinstance(value, self.type): value = self.type(value) if obj in self.values and self.values[obj] == value: return self.values[obj] = value self.dirty[obj] = value != self.oldvalues.get(obj, DefaultValue) # if value changes from implicit default (DefaultValue) to explicit default (self.default) we mark it as dirty def __getstate__(self, obj): value = self.values.get(obj, DefaultValue) if value in (None, DefaultValue): pass elif issubclass(self.type, bool): - value = u'true' if value else u'false' - elif issubclass(self.type, (int, long, basestring)): - value = unicode(value) + value = 'true' if value else 'false' + elif issubclass(self.type, (int, int, str)): + value = str(value) elif hasattr(value, '__getstate__'): value = value.__getstate__() else: - value = unicode(value) + value = str(value) return value def __setstate__(self, obj, value): with self.lock: if value is None and not self.nillable: raise ValueError("setting attribute is not nillable") if value is None: pass elif issubclass(self.type, bool): if value.lower() in ('true', 'yes', 'on', '1'): value = True elif value.lower() in ('false', 'no', 'off', '0'): value = False else: raise ValueError("invalid boolean value: %s" % (value,)) - elif issubclass(self.type, (int, long, basestring)): + elif issubclass(self.type, (int, int, str)): value = self.type(value) elif hasattr(self.type, '__setstate__'): object = self.type.__new__(self.type) object.__setstate__(value) value = object else: value = self.type(value) self.oldvalues[obj] = self.values[obj] = value self.dirty[obj] = False def get_modified(self, obj): """ Returns a ModifiedValue instance with references to the old and new values or None if not modified. """ with self.lock: try: if self.dirty.get(obj, False): # if the object is dirty because it switched from implicit default to explicit default, ModifiedValue will have old==new==self.default (see __set__) return ModifiedValue(old=self.oldvalues.get(obj, self.default), new=self.values.get(obj, self.default)) else: return None finally: try: self.oldvalues[obj] = self.values[obj] except KeyError: self.oldvalues.pop(obj, None) self.dirty[obj] = False def get_old(self, obj): return self.oldvalues.get(obj, self.default) def undo(self, obj): with self.lock: if obj in self.oldvalues: self.values[obj] = self.oldvalues[obj] else: self.values.pop(obj, None) self.dirty[obj] = False class CorrelatedSetting(Setting): """ Descriptor representing a setting in a configuration object that is correlated with another setting on the same configuration object. Sibling is the name of the sibling setting and validator is a callable that will receive the setting value and the sibling setting value and should raise an exception if the setting value is not acceptable relative to the sibling setting value. If a setting is set to the object DefaultValue, it will be reset to the default. Also, only Setting attributes with nillable=True can be assigned the value None. All other values are passed to the type specified. """ correlation_lock = Lock() def __init__(self, type, sibling, validator, default=None, nillable=False): Setting.__init__(self, type, default, nillable) self.sibling = sibling self.validator = validator def __set__(self, obj, value): with self.correlation_lock: sibling_value = getattr(obj, self.sibling) self.validator(value, sibling_value) Setting.__set__(self, obj, value) class RuntimeSetting(Setting): """ Descriptor representing a runtime setting in a configuration object. Unlike the standard setting, the runtime setting is never written to nor read from the configuration file. It resides in memory only and its lifetime ends when the application exits. """ def __getstate__(self, obj): return DefaultValue def __setstate__(self, obj, value): pass class SettingsIndexer(object): __slots__ = ('__object__',) def __init__(self, object): self.__object__ = object def __getitem__(self, key): return reduce(getattr, key.split('.'), self.__object__.__class__) class SettingsStateMeta(type): __established__ = WeakSet() def __call__(cls, *args, **kw): instance = super(SettingsStateMeta, cls).__call__(*args, **kw) if hasattr(instance, '__establish__') and instance not in cls.__established__: cls.__established__.add(instance) instance.__establish__() return instance -class SettingsState(object): +class SettingsState(object, metaclass=SettingsStateMeta): """ This class represents configuration objects which can be saved and restored. """ - __metaclass__ = SettingsStateMeta - @property def __settings__(self): return SettingsIndexer(self) def get_modified(self): """ Returns a dictionary containing the settings which have been changed. The keys are the full paths to the attributes (from this object), which are mapped to a ModifiedValue instance with references to the old and new values. """ modified = {} for name in dir(self.__class__): attribute = getattr(self.__class__, name, None) if isinstance(attribute, SettingsGroupMeta): modified_settings = getattr(self, name).get_modified() - modified.update(dict((name+'.'+k if k else name, v) for k,v in modified_settings.iteritems())) + modified.update(dict((name+'.'+k if k else name, v) for k,v in list(modified_settings.items()))) elif isinstance(attribute, AbstractSetting): modified_value = attribute.get_modified(self) if modified_value is not None: modified[name] = modified_value return modified def clone(self): """ Create a copy of this object and all its sub settings. """ raise NotImplementedError def update(self, object): """ Update the settings and sub-settings of this settings object using the ones in the specified object. """ raise NotImplementedError def __getstate__(self): state = {} for name in dir(self.__class__): attribute = getattr(self.__class__, name, None) if isinstance(attribute, SettingsGroupMeta): state[name] = getattr(self, name).__getstate__() elif isinstance(attribute, AbstractSetting): state[name] = attribute.__getstate__(self) return state def __setstate__(self, state): configuration_manager = ConfigurationManager() notification_center = NotificationCenter() - for name, value in state.iteritems(): + for name, value in list(state.items()): attribute = getattr(self.__class__, name, None) if isinstance(attribute, SettingsGroupMeta): group = getattr(self, name) try: group.__setstate__(value) - except ValueError, e: + except ValueError as e: notification_center.post_notification('CFGManagerLoadFailed', sender=configuration_manager, data=NotificationData(attribute=name, container=self, error=e)) elif isinstance(attribute, AbstractSetting): try: attribute.__setstate__(self, value) - except ValueError, e: + except ValueError as e: notification_center.post_notification('CFGManagerLoadFailed', sender=configuration_manager, data=NotificationData(attribute=name, container=self, error=e)) class SettingsGroupMeta(SettingsStateMeta): """ Metaclass for SettingsGroup and its subclasses which allows them to be used as descriptor instances. """ def __init__(cls, name, bases, dct): super(SettingsGroupMeta, cls).__init__(name, bases, dct) cls.values = weakobjectmap() def __get__(cls, obj, objtype): if obj is None: return cls try: return cls.values[obj] except KeyError: return cls.values.setdefault(obj, cls()) def __set__(cls, obj, value): raise AttributeError("cannot overwrite group of settings") def __delete__(self, obj): raise AttributeError('cannot delete group of settings') -class SettingsGroup(SettingsState): +class SettingsGroup(SettingsState, metaclass=SettingsGroupMeta): """ Base class for settings groups, i.e. non-leaf and non-root nodes in the configuration tree. All SettingsGroup subclasses are descriptor instances which return an instance of the subclass type when accessed. All SettingsGroup instances are created without passing any arguments to the constructor. class ContainedGroup(SettingsGroup): pass class ContainingGroup(SettingsGroup): subgroup = ContainedGroup """ - __metaclass__ = SettingsGroupMeta - class ItemMap(dict): def __init__(self, *args, **kw): super(ItemMap, self).__init__(*args, **kw) self.old = dict(self) def get_modified(self): new_ids = set(self) old_ids = set(self.old) added_items = [self[id] for id in new_ids - old_ids] removed_items = [self.old[id] for id in old_ids - new_ids] modified_items = dict((id, modified) for id, modified in ((id, self[id].get_modified()) for id in new_ids & old_ids) if modified) for item in added_items: item.get_modified() # reset the dirty flag of the added items and sync their old and new values if added_items or removed_items or modified_items: self.old = dict(self) return ModifiedList(added=added_items, removed=removed_items, modified=modified_items) else: return None class ItemManagement(object): def add_item(self, item, collection): pass def remove_item(self, item, collection): pass def set_items(self, items, collection): pass class ItemCollectionMeta(SettingsGroupMeta): def __init__(cls, name, bases, dct): if cls._item_type is not None and not issubclass(cls._item_type, SettingsState): raise TypeError('_item_type must be a subclass of SettingsState') if cls._item_type is not None and not isinstance(getattr(cls._item_type, 'id', None), (SettingsObjectID, SettingsObjectImmutableID)): raise ValueError('the type in _item_type must have an id attribute of type SettingsObjectID or SettingsObjectImmutableID') if not isinstance(cls._item_management, ItemManagement): raise TypeError('_item_management must be an instance of a subclass of ItemManagement') super(ItemCollectionMeta, cls).__init__(name, bases, dct) def __set__(cls, obj, items): if not all(isinstance(item, cls._item_type) for item in items): raise TypeError("items must be instances of %s" % cls._item_type.__name__) if set(item.id for item in items).intersection(name for name in dir(cls) if isinstance(getattr(cls, name, None), (SettingsGroupMeta, AbstractSetting))): raise ValueError("item IDs cannot overlap with static setting names") collection = cls.__get__(obj, obj.__class__) with collection._lock: collection._item_management.set_items(items, collection) collection._item_map.clear() collection._item_map.update((item.id, item) for item in items) def __delete__(self, obj): raise AttributeError('cannot delete item collection') -class ItemCollection(SettingsGroup): +class ItemCollection(SettingsGroup, metaclass=ItemCollectionMeta): """A SettingsGroup that also contains a dynamic collection of sub-setting""" - __metaclass__ = ItemCollectionMeta - _item_type = None _item_management = ItemManagement() def __init__(self): self._item_map = ItemMap() self._lock = Lock() def __getitem__(self, key): return self._item_map[key] def __contains__(self, key): return key in self._item_map def __iter__(self): - return iter(sorted(self._item_map.values(), key=attrgetter('id'))) + return iter(sorted(list(self._item_map.values()), key=attrgetter('id'))) def __reversed__(self): - return iter(sorted(self._item_map.values(), key=attrgetter('id'), reverse=True)) + return iter(sorted(list(self._item_map.values()), key=attrgetter('id'), reverse=True)) __hash__ = None def __len__(self): return len(self._item_map) def __getstate__(self): with self._lock: - state = ItemContainer((id, item.__getstate__()) for id, item in self._item_map.iteritems()) + state = ItemContainer((id, item.__getstate__()) for id, item in list(self._item_map.items())) state.update(super(ItemCollection, self).__getstate__()) return state def __setstate__(self, state): with self._lock: super(ItemCollection, self).__setstate__(state) setting_names = set(name for name in dir(self.__class__) if isinstance(getattr(self.__class__, name, None), (SettingsGroupMeta, AbstractSetting))) - self._item_map = ItemMap((id, self._item_type(id, **item_state)) for id, item_state in state.iteritems() if id not in setting_names) + self._item_map = ItemMap((id, self._item_type(id, **item_state)) for id, item_state in list(state.items()) if id not in setting_names) def get_modified(self): with self._lock: modified_settings = super(ItemCollection, self).get_modified() modified_items = self._item_map.get_modified() if modified_items is not None: modified_settings[None] = modified_items return modified_settings def ids(self): return sorted(self._item_map.keys()) def get(self, key, default=None): return self._item_map.get(key, default) def add(self, item): if not isinstance(item, self._item_type): raise TypeError("item must be an instances of %s" % self._item_type.__name__) if item.id in set(name for name in dir(self.__class__) if isinstance(getattr(self.__class__, name, None), (SettingsGroupMeta, AbstractSetting))): raise ValueError("item IDs cannot overlap with static setting names") with self._lock: self._item_management.add_item(item, self) self._item_map[item.id] = item def remove(self, item): with self._lock: self._item_management.remove_item(item, self) self._item_map.pop(item.id, None) class ConditionalSingleton(type): """A conditional singleton based on cls.__id__ being static or not""" def __init__(cls, name, bases, dic): super(ConditionalSingleton, cls).__init__(name, bases, dic) cls.__instlock__ = Lock() cls.__instance__ = None def __call__(cls, *args, **kw): - if isinstance(cls.__id__, basestring): + if isinstance(cls.__id__, str): if args or kw: raise TypeError("cannot have arguments for %s because it is a singleton" % cls.__name__) with cls.__instlock__: if cls.__instance__ is None: cls.__instance__ = super(ConditionalSingleton, cls).__call__(*args, **kw) return cls.__instance__ else: return super(ConditionalSingleton, cls).__call__(*args, **kw) class SettingsObjectMeta(SettingsStateMeta, ConditionalSingleton): """Metaclass to singleton-ize SettingsObject subclasses with static ids""" def __init__(cls, name, bases, dic): - if not (cls.__id__ is None or isinstance(cls.__id__, basestring) or isdescriptor(cls.__id__)): + if not (cls.__id__ is None or isinstance(cls.__id__, str) or isdescriptor(cls.__id__)): raise TypeError("%s.__id__ must be None, a string instance or a descriptor" % name) super(SettingsObjectMeta, cls).__init__(name, bases, dic) -class SettingsObject(SettingsState): +class SettingsObject(SettingsState, metaclass=SettingsObjectMeta): """ Subclass for top-level configuration objects. These objects are identifiable by either a global id (set in the __id__ attribute of the class) or a local id passed as the sole argument when instantiating SettingsObjects. For SettingsObject subclasses which are meant to be used exclusively with a local id, the class attribute __id__ should be left to the value None; if __init__ is defined, it would have to accept exactly one argument: id. The local id takes precedence over the one specified as a class attribute. Note: __init__ and __new__ will be called not only when a new object is created (i.e. there weren't any settings saved in the configuration), but also when the object is retrieved from the configuration. """ - __metaclass__ = SettingsObjectMeta - __group__ = None __id__ = None def __new__(cls, id=None): id = id or cls.__id__ if id is None: raise ValueError("id is required for instantiating %s" % cls.__name__) - if not isinstance(id, basestring): + if not isinstance(id, str): raise TypeError("id needs to be a string instance") configuration = ConfigurationManager() instance = SettingsState.__new__(cls) instance.__id__ = id instance.__state__ = 'new' try: data = configuration.get(instance.__key__) except ObjectNotFoundError: pass else: instance.__setstate__(data) instance.__state__ = 'loaded' return instance def __establish__(self): if self.__state__ == 'loaded' or self.__instance__ is not None: self.__state__ = 'active' notification_center = NotificationCenter() notification_center.post_notification('CFGSettingsObjectWasActivated', sender=self) @property def __key__(self): if isinstance(self.__class__.__id__, (SettingsObjectID, SettingsObjectImmutableID)): id_key = PersistentKey(self.__id__) else: - id_key = unicode(self.__id__) + id_key = str(self.__id__) if self.__group__ is not None: return [self.__group__, id_key] else: return [id_key] @property def __oldkey__(self): if isinstance(self.__class__.__id__, SettingsObjectID): id_key = PersistentKey(self.__class__.__id__.get_old(self)) elif isinstance(self.__class__.__id__, SettingsObjectImmutableID): id_key = PersistentKey(self.__id__) else: - id_key = unicode(self.__id__) + id_key = str(self.__id__) if self.__group__ is not None: return [self.__group__, id_key] else: return [id_key] @run_in_thread('file-io') def save(self): """ Use the ConfigurationManager to store the object under its id in the specified group or top-level otherwise, depending on whether group is None. This method will also post a CFGSettingsObjectDidChange notification, regardless of whether the settings have been saved to persistent storage or not. If the save does fail, a CFGManagerSaveFailed notification is posted as well. """ if self.__state__ == 'deleted': return configuration = ConfigurationManager() notification_center = NotificationCenter() oldkey = self.__oldkey__ # save this here as get_modified will reset it modified_id = self.__class__.__id__.get_modified(self) if isinstance(self.__class__.__id__, SettingsObjectID) else None modified_settings = self.get_modified() if not modified_id and not modified_settings and self.__state__ != 'new': return if self.__state__ == 'new': configuration.update(self.__key__, self.__getstate__()) self.__state__ = 'active' notification_center.post_notification('CFGSettingsObjectWasActivated', sender=self) notification_center.post_notification('CFGSettingsObjectWasCreated', sender=self) modified_data = None elif not modified_id and all(isinstance(self.__settings__[key], RuntimeSetting) for key in modified_settings): notification_center.post_notification('CFGSettingsObjectDidChange', sender=self, data=NotificationData(modified=modified_settings)) return else: if modified_id: configuration.rename(oldkey, self.__key__) if modified_settings: configuration.update(self.__key__, self.__getstate__()) modified_data = modified_settings or {} if modified_id: modified_data['__id__'] = modified_id notification_center.post_notification('CFGSettingsObjectDidChange', sender=self, data=NotificationData(modified=modified_data)) try: configuration.save() - except Exception, e: + except Exception as e: log.exception() notification_center.post_notification('CFGManagerSaveFailed', sender=configuration, data=NotificationData(object=self, operation='save', modified=modified_data, exception=e)) @run_in_thread('file-io') def delete(self): """ Remove this object from the persistent configuration. """ if self.__id__ is self.__class__.__id__: raise TypeError("cannot delete %s instance with default id" % self.__class__.__name__) if self.__state__ == 'deleted': return self.__state__ = 'deleted' configuration = ConfigurationManager() notification_center = NotificationCenter() configuration.delete(self.__oldkey__) # we need the key that wasn't yet saved notification_center.post_notification('CFGSettingsObjectWasDeleted', sender=self) try: configuration.save() - except Exception, e: + except Exception as e: log.exception() notification_center.post_notification('CFGManagerSaveFailed', sender=configuration, data=NotificationData(object=self, operation='delete', exception=e)) def clone(self, new_id): """ Create a copy of this object and all its sub settings. """ raise NotImplementedError @classmethod def register_extension(cls, extension): """ Register an extension of this SettingsObject. All Settings and SettingsGroups defined in the extension will be added to this SettingsObject, overwriting any attributes with the same name. Other attributes in the extension are ignored. """ if not issubclass(extension, SettingsObjectExtension): raise TypeError("expected subclass of SettingsObjectExtension, got %r" % (extension,)) for name in dir(extension): attribute = getattr(extension, name, None) if isinstance(attribute, (AbstractSetting, SettingsGroupMeta)): setattr(cls, name, attribute) class SettingsObjectExtension(object): """ Base class for extensions of SettingsObjects. """ def __new__(self, *args, **kwargs): raise TypeError("SettingsObjectExtension subclasses cannot be instantiated") diff --git a/sipsimple/configuration/backend/file.py b/sipsimple/configuration/backend/file.py index 538f5d38..d3f99ac9 100644 --- a/sipsimple/configuration/backend/file.py +++ b/sipsimple/configuration/backend/file.py @@ -1,231 +1,231 @@ """Configuration backend for storing settings in a simple plain text format""" __all__ = ["FileParserError", "FileBuilderError", "FileBackend"] import errno import os import re import platform import random from collections import deque from application.system import makedirs, openfile, unlink from zope.interface import implements from sipsimple.configuration.backend import IConfigurationBackend, ConfigurationBackendError class FileParserError(ConfigurationBackendError): """Error raised when the configuration file cannot be parsed.""" class FileBuilderError(ConfigurationBackendError): """Error raised when the configuration data cannot be saved.""" class GroupState(object): """ Internal class used for keeping track of the containing groups while parsing. """ def __init__(self, indentation): self.indentation = indentation self.data = {} class Line(object): """Internal representation of lines in a configuration file""" def __init__(self, indentation, name, separator, value): self.indentation = indentation self.name = name self.separator = separator self.value = value def __repr__(self): return "%s(%r, %r, %r, %r)" % (self.__class__.__name__, self.indentation, self.name, self.separator, self.value) class FileBackend(object): """ Implementation of a configuration backend that stores data in a simple plain text format. """ implements(IConfigurationBackend) - escape_characters_re = re.compile(ur"""[,"'=: #\\\t\x0b\x0c\n\r]""") + escape_characters_re = re.compile(r"""[,"'=: #\\\t\x0b\x0c\n\r]""") def __init__(self, filename, encoding='utf-8'): """ Initialize the configuration backend with the specified file. The file is not read at this time, but rather each time the load method is called. """ self.filename = filename self.encoding = encoding def load(self): """ Read the file configured with this backend and parse it, returning a dictionary conforming to the IConfigurationBackend specification. """ try: file = open(self.filename) - except IOError, e: + except IOError as e: if e.errno == errno.ENOENT: return {} else: raise ConfigurationBackendError("failed to read configuration file: %s" % str(e)) state_stack = deque() state_stack.appendleft(GroupState(-1)) for lineno, line in enumerate(file, 1): line = self._parse_line(line, lineno) if not line.name: continue # find the container for this declaration while state_stack[0].indentation >= line.indentation: state_stack.popleft() - if line.separator == u':': + if line.separator == ':': new_group_state = GroupState(line.indentation) state_stack[0].data[line.name] = new_group_state.data state_stack.appendleft(new_group_state) - elif line.separator == u'=': + elif line.separator == '=': state_stack[0].data[line.name] = line.value return state_stack[-1].data def save(self, data): """ Given a dictionary conforming to the IConfigurationBackend specification, write the data to the file configured with this backend in a format suitable to be read back using load(). """ lines = self._build_group(data, 0) config_directory = os.path.dirname(self.filename) tmp_filename = '%s.%d.%08X' % (self.filename, os.getpid(), random.getrandbits(32)) try: if config_directory: makedirs(config_directory) - file = openfile(tmp_filename, 'wb', permissions=0600) + file = openfile(tmp_filename, 'wb', permissions=0o600) file.write((os.linesep.join(lines)+os.linesep).encode(self.encoding)) file.close() if platform.system() == 'Windows': # os.rename does not work on Windows if the destination file already exists. # It seems there is no atomic way to do this on Windows. unlink(self.filename) os.rename(tmp_filename, self.filename) - except (IOError, OSError), e: + except (IOError, OSError) as e: raise ConfigurationBackendError("failed to write configuration file: %s" % str(e)) def _parse_line(self, line, lineno): def advance_to_next_token(line): counter = 0 while line and line[0].isspace(): line.popleft() counter += 1 - if line and line[0] == u'#': + if line and line[0] == '#': line.clear() return counter def token_iterator(line, delimiter=''): quote_char = None while line: if quote_char is None and line[0] in delimiter: break char = line.popleft() - if char in u"'\"": + if char in "'\"": if quote_char is None: quote_char = char continue elif quote_char == char: quote_char = None continue else: yield char - elif char == u'\\': + elif char == '\\': if not line: raise FileParserError("unexpected `\\' at end of line %d" % lineno) char = line.popleft() if char == 'n': - yield u'\n' + yield '\n' elif char == 'r': - yield u'\r' + yield '\r' else: yield char - elif quote_char is None and char == u'#': + elif quote_char is None and char == '#': line.clear() break elif quote_char is None and char.isspace(): break else: yield char if quote_char is not None: raise FileParserError("missing ending quote at line %d" % lineno) line = deque(line.rstrip().decode(self.encoding)) indentation = advance_to_next_token(line) if not line: return Line(indentation, None, None, None) - name = u''.join(token_iterator(line, delimiter=u':=')) + name = ''.join(token_iterator(line, delimiter=':=')) advance_to_next_token(line) - if not line or line[0] not in u':=': + if not line or line[0] not in ':=': raise FileParserError("expected one of `:' or `=' at line %d" % lineno) if not name: raise FileParserError("missing setting/section name at line %d" % lineno) separator = line.popleft() advance_to_next_token(line) if not line: return Line(indentation, name, separator, None) - elif separator == u':': + elif separator == ':': raise FileParserError("unexpected characters after `:' at line %d" % lineno) value = None value_list = None while line: - value = u''.join(token_iterator(line, delimiter=u',')) + value = ''.join(token_iterator(line, delimiter=',')) advance_to_next_token(line) if line: - if line[0] == u',': + if line[0] == ',': line.popleft() advance_to_next_token(line) if value_list is None: value_list = [] else: raise FileParserError("unexpected characters after value at line %d" % lineno) if value_list is not None: value_list.append(value) value = value_list if value_list is not None else value return Line(indentation, name, separator, value) def _build_group(self, group, indentation): setting_lines = [] group_lines = [] - indent_spaces = u' '*4*indentation - for name, data in sorted(group.iteritems()): + indent_spaces = ' '*4*indentation + for name, data in sorted(group.items()): if data is None: - setting_lines.append(u'%s%s =' % (indent_spaces, self._escape(name))) + setting_lines.append('%s%s =' % (indent_spaces, self._escape(name))) elif type(data) is dict: - group_lines.append(u'%s%s:' % (indent_spaces, self._escape(name))) + group_lines.append('%s%s:' % (indent_spaces, self._escape(name))) group_lines.extend(self._build_group(data, indentation+1)) - group_lines.append(u'') + group_lines.append('') elif type(data) is list: - list_value = u', '.join(self._escape(item) for item in data) + list_value = ', '.join(self._escape(item) for item in data) if len(data) == 1: - list_value += u',' - setting_lines.append(u'%s%s = %s' % (indent_spaces, self._escape(name), list_value)) - elif type(data) is unicode: - setting_lines.append(u'%s%s = %s' % (indent_spaces, self._escape(name), self._escape(data))) + list_value += ',' + setting_lines.append('%s%s = %s' % (indent_spaces, self._escape(name), list_value)) + elif type(data) is str: + setting_lines.append('%s%s = %s' % (indent_spaces, self._escape(name), self._escape(data))) else: raise FileBuilderError("expected unicode, dict or list object, got %s" % type(data).__name__) return setting_lines + group_lines def _escape(self, value): - if value == u'': - return u'""' + if value == '': + return '""' elif self.escape_characters_re.search(value): - return u'"%s"' % value.replace(u'\\', u'\\\\').replace(u'"', u'\\"').replace(u'\n', u'\\n').replace(u'\r', u'\\r') + return '"%s"' % value.replace('\\', '\\\\').replace('"', '\\"').replace('\n', '\\n').replace('\r', '\\r') else: return value diff --git a/sipsimple/configuration/datatypes.py b/sipsimple/configuration/datatypes.py index 1d0dd385..63478d15 100644 --- a/sipsimple/configuration/datatypes.py +++ b/sipsimple/configuration/datatypes.py @@ -1,664 +1,664 @@ """Definitions of datatypes for use in configuration settings""" __all__ = [ # Base datatypes 'List', # Generic datatypes 'ContentType', 'ContentTypeList', 'CountryCode', 'NonNegativeInteger', 'PositiveInteger', 'SIPAddress', # Custom datatypes 'PJSIPLogLevel', # Audio datatypes 'AudioCodecList', 'SampleRate', # Video datatypes 'H264Profile', 'VideoResolution', 'VideoCodecList', # Address and transport datatypes 'Port', 'PortRange', 'Hostname', 'DomainList', 'EndpointAddress', 'EndpointIPAddress', 'MSRPRelayAddress', 'SIPProxyAddress', 'STUNServerAddress', 'STUNServerAddressList', 'XCAPRoot', 'MSRPConnectionModel', 'MSRPTransport', 'SIPTransport', 'SIPTransportList', # SRTP encryption 'SRTPKeyNegotiation', # Path datatypes 'Path'] import locale import os import re -import urlparse +import urllib.parse from operator import itemgetter # Base datatypes class List(object): - type = unicode + type = str def __init__(self, values=()): self.values = [item if isinstance(item, self.type) else self.type(item) for item in values] def __getstate__(self): state = [] for item in self: if item is None: pass elif issubclass(self.type, bool): - item = u'true' if item else u'false' - elif issubclass(self.type, (int, long, basestring)): - item = unicode(item) + item = 'true' if item else 'false' + elif issubclass(self.type, (int, int, str)): + item = str(item) elif hasattr(item, '__getstate__'): item = item.__getstate__() - if type(item) is not unicode: + if type(item) is not str: raise TypeError("Expected unicode type for list member, got %s" % item.__class__.__name__) else: - item = unicode(item) + item = str(item) state.append(item) return state def __setstate__(self, state): if not isinstance(state, list): state = [state] values = [] for item in state: if item is None: pass elif issubclass(self.type, bool): if item.lower() in ('true', 'yes', 'on', '1'): item = True elif item.lower() in ('false', 'no', 'off', '0'): item = False else: raise ValueError("invalid boolean value: %s" % (item,)) - elif issubclass(self.type, (int, long, basestring)): + elif issubclass(self.type, (int, int, str)): item = self.type(item) elif hasattr(self.type, '__setstate__'): object = self.type.__new__(self.type) object.__setstate__(item) item = object else: item = self.type(item) values.append(item) self.values = values def __add__(self, other): if isinstance(other, List): return self.__class__(self.values + other.values) else: return self.__class__(self.values + other) def __radd__(self, other): if isinstance(other, List): return self.__class__(other.values + self.values) else: return self.__class__(other + self.values) def __mul__(self, other): return self.__class__(self.values * other) def __rmul__(self, other): return self.__class__(other * self.values) def __eq__(self, other): if isinstance(other, List): return self.values == other.values else: return self.values == other def __ne__(self, other): return not self.__eq__(other) __hash__ = None def __iter__(self): return iter(self.values) def __contains__(self, value): return value in self.values def __getitem__(self, key): return self.values[key] def __len__(self): return len(self.values) def __repr__(self): return '%s(%r)' % (self.__class__.__name__, self.values) def __str__(self): return ', '.join(str(item) for item in self) def __unicode__(self): - return u', '.join(unicode(item) for item in self) + return ', '.join(str(item) for item in self) # Generic datatypes class ContentType(str): def __new__(cls, value): value = str(value) if value == '*': return value try: type, subtype = value.split('/') except ValueError: raise ValueError("illegal content-type: %s" % value) else: if type == '*': raise ValueError("illegal content-type: %s" % value) return value class ContentTypeList(List): type = ContentType class CountryCode(str): code_pattern = re.compile(r'[1-9][0-9]*') def __new__(cls, value): value = str(value) if cls.code_pattern.match(value) is None: raise ValueError("illegal country code: %s" % value) return value class NonNegativeInteger(int): def __new__(cls, value): value = int(value) if value < 0: raise ValueError("non-negative int expected, found %d" % value) return value class PositiveInteger(int): def __new__(cls, value): value = int(value) if value <= 0: raise ValueError("positive int expected, found %d" % value) return value class SIPAddress(str): def __new__(cls, address): address = str(address) address = address.replace('@', '%40', address.count('@')-1) try: username, domain = address.split('@') Hostname(domain) except ValueError: raise ValueError("illegal SIP address: %s, must be in user@domain format" % address) return super(SIPAddress, cls).__new__(cls, address) username = property(lambda self: self.split('@')[0]) domain = property(lambda self: self.split('@')[1]) # Custom datatypes class PJSIPLogLevel(int): def __new__(cls, value): value = int(value) if not (0 <= value <= 5): raise ValueError("expected an integer number between 0 and 5, found %d" % value) return value class CodecList(List): type = str available_values = None # to be defined in a subclass @property def values(self): return self.__dict__['values'] @values.setter def values(self, values): if not set(values).issubset(self.available_values): raise ValueError("illegal codec values: %s" % ', '.join(values)) self.__dict__['values'] = values # Audio datatypes class AudioCodecList(CodecList): available_values = {'opus', 'speex', 'G722', 'GSM', 'iLBC', 'PCMU', 'PCMA'} class SampleRate(int): valid_values = (16000, 32000, 44100, 48000) def __new__(cls, value): value = int(value) if value not in cls.valid_values: raise ValueError("illegal sample rate: %d" % value) return value # Video datatypes class H264Profile(str): valid_values = ('baseline', 'main', 'high') def __new__(cls, value): if value.lower() not in cls.valid_values: raise ValueError('invalid value, must be one of: {}'.format(', '.join(cls.valid_values))) return super(H264Profile, cls).__new__(cls, value.lower()) class VideoResolution(tuple): width = property(itemgetter(0)) height = property(itemgetter(1)) def __new__(cls, value): if isinstance(value, tuple): width, height = value - elif isinstance(value, basestring): + elif isinstance(value, str): width, height = value.split('x') else: raise ValueError('invalid value: %r' % value) return super(VideoResolution, cls).__new__(cls, (int(width), int(height))) def __repr__(self): return '%s(%d, %d)' % (self.__class__.__name__, self.width, self.height) def __str__(self): return '%dx%d' % (self.width, self.height) def __unicode__(self): - return u'%dx%d' % (self.width, self.height) + return '%dx%d' % (self.width, self.height) class VideoCodecList(CodecList): available_values = {'H264', 'VP8'} # Address and transport datatypes class Port(int): def __new__(cls, value): value = int(value) if not (0 <= value <= 65535): raise ValueError("illegal port value: %s" % value) return value class PortRange(object): def __init__(self, start, end): self.start = Port(start) self.end = Port(end) if self.start == 0: raise ValueError("illegal port value: 0") if self.end == 0: raise ValueError("illegal port value: 0") if self.start > self.end: raise ValueError("illegal port range: start port (%d) cannot be larger than end port (%d)" % (self.start, self.end)) def __getstate__(self): - return unicode(self) + return str(self) def __setstate__(self, state): self.__init__(*state.split('-')) def __eq__(self, other): if isinstance(other, PortRange): return self.start == other.start and self.end == other.end else: return NotImplemented def __ne__(self, other): equal = self.__eq__(other) return NotImplemented if equal is NotImplemented else not equal __hash__ = None def __repr__(self): return '%s(start=%r, end=%r)' % (self.__class__.__name__, self.start, self.end) def __str__(self): return '%d-%d' % (self.start, self.end) def __unicode__(self): - return u'%d-%d' % (self.start, self.end) + return '%d-%d' % (self.start, self.end) class Hostname(str): _host_re = re.compile(r"^(\d{1,3}\.\d{1,3}\.\d{1,3}\.\d{1,3})|([a-zA-Z0-9\-_]+(\.[a-zA-Z0-9\-_]+)*)$") def __new__(cls, value): value = str(value) if not cls._host_re.match(value): raise ValueError("illegal hostname or ip address: %s" % value) return value class IPAddress(str): _ip_re = re.compile(r"^(\d{1,3}\.\d{1,3}\.\d{1,3}\.\d{1,3})$") def __new__(cls, value): value = str(value) if not cls._ip_re.match(value): raise ValueError("illegal IP address: %s" % value) return value class DomainList(List): type = str _domain_re = re.compile(r"^[a-zA-Z0-9\-_]+(\.[a-zA-Z0-9\-_]+)*$") @property def values(self): return self.__dict__['values'] @values.setter def values(self, values): for value in values: if self._domain_re.match(value) is None: raise ValueError("illegal domain: %s" % value) self.__dict__['values'] = values class EndpointAddress(object): _description_re = re.compile(r"^(?P(\d{1,3}\.\d{1,3}\.\d{1,3}\.\d{1,3})|([a-zA-Z0-9\-_]+(\.[a-zA-Z0-9\-_]+)*))(:(?P\d+))?$") default_port = 0 def __init__(self, host, port=None): self.host = Hostname(host) self.port = Port(port if port is not None else self.default_port) if self.port == 0: raise ValueError("illegal port value: 0") def __getstate__(self): - return unicode(self) + return str(self) def __setstate__(self, state): match = self._description_re.match(state) if match is None: raise ValueError("illegal endpoint address: %s" % state) self.__init__(**match.groupdict()) def __eq__(self, other): if isinstance(other, EndpointAddress): return self.host == other.host and self.port == other.port else: return NotImplemented def __ne__(self, other): equal = self.__eq__(other) return NotImplemented if equal is NotImplemented else not equal __hash__ = None def __repr__(self): return '%s(%r, %r)' % (self.__class__.__name__, self.host, self.port) def __str__(self): return '%s:%d' % (self.host, self.port) def __unicode__(self): - return u'%s:%d' % (self.host, self.port) + return '%s:%d' % (self.host, self.port) @classmethod def from_description(cls, description): if not description: return None match = cls._description_re.match(description) if match is None: raise ValueError("illegal endpoint address: %s" % description) return cls(**match.groupdict()) class EndpointIPAddress(EndpointAddress): _description_re = re.compile(r"^(?P\d{1,3}\.\d{1,3}\.\d{1,3}\.\d{1,3})(:(?P\d+))?$") def __init__(self, host, port=None): self.host = IPAddress(host) self.port = Port(port if port is not None else self.default_port) if self.port == 0: raise ValueError("illegal port value: 0") def __setstate__(self, state): match = self._description_re.match(state) if match is None: raise ValueError("illegal value: %s, must be an IP address" % state) self.__init__(**match.groupdict()) @classmethod def from_description(cls, description): if not description: return None match = cls._description_re.match(description) if match is None: raise ValueError("illegal value: %s, must be an IP address" % description) return cls(**match.groupdict()) class MSRPRelayAddress(object): _description_re = re.compile(r"^(?P(\d{1,3}\.\d{1,3}\.\d{1,3}\.\d{1,3})|([a-zA-Z0-9\-_]+(\.[a-zA-Z0-9\-_]+)*))(:(?P\d+))?(;transport=(?P.+))?$") def __init__(self, host, port=2855, transport='tls'): self.host = Hostname(host) self.port = Port(port) self.transport = MSRPTransport(transport) def __getstate__(self): - return unicode(self) + return str(self) def __setstate__(self, state): match = self._description_re.match(state) if match is None: raise ValueError("illegal MSRP relay address: %s" % state) - self.__init__(**dict((k, v) for k, v in match.groupdict().iteritems() if v is not None)) + self.__init__(**dict((k, v) for k, v in list(match.groupdict().items()) if v is not None)) def __eq__(self, other): if isinstance(other, MSRPRelayAddress): return self.host == other.host and self.port == other.port and self.transport == other.transport else: return NotImplemented def __ne__(self, other): equal = self.__eq__(other) return NotImplemented if equal is NotImplemented else not equal __hash__ = None def __repr__(self): return '%s(%r, port=%r, transport=%r)' % (self.__class__.__name__, self.host, self.port, self.transport) def __str__(self): return '%s:%d;transport=%s' % (self.host, self.port, self.transport) def __unicode__(self): - return u'%s:%d;transport=%s' % (self.host, self.port, self.transport) + return '%s:%d;transport=%s' % (self.host, self.port, self.transport) @classmethod def from_description(cls, description): if not description: return None match = cls._description_re.match(description) if match is None: raise ValueError("illegal MSRP relay address: %s" % description) - return cls(**dict((k, v) for k, v in match.groupdict().iteritems() if v is not None)) + return cls(**dict((k, v) for k, v in list(match.groupdict().items()) if v is not None)) class SIPProxyAddress(object): _description_re = re.compile(r"^(?P(\d{1,3}\.\d{1,3}\.\d{1,3}\.\d{1,3})|([a-zA-Z0-9\-_]+(\.[a-zA-Z0-9\-_]+)*))(:(?P\d+))?(;transport=(?P.+))?$") def __init__(self, host, port=5060, transport='udp'): self.host = Hostname(host) self.port = Port(port) if self.port == 0: raise ValueError("illegal port value: 0") self.transport = SIPTransport(transport) def __getstate__(self): - return unicode(self) + return str(self) def __setstate__(self, state): match = self._description_re.match(state) if match is None: raise ValueError("illegal SIP proxy address: %s" % state) - self.__init__(**dict((k, v) for k, v in match.groupdict().iteritems() if v is not None)) + self.__init__(**dict((k, v) for k, v in list(match.groupdict().items()) if v is not None)) def __eq__(self, other): if isinstance(other, SIPProxyAddress): return self.host == other.host and self.port == other.port and self.transport == other.transport else: return NotImplemented def __ne__(self, other): equal = self.__eq__(other) return NotImplemented if equal is NotImplemented else not equal __hash__ = None def __repr__(self): return '%s(%r, port=%r, transport=%r)' % (self.__class__.__name__, self.host, self.port, self.transport) def __str__(self): return '%s:%d;transport=%s' % (self.host, self.port, self.transport) def __unicode__(self): - return u'%s:%d;transport=%s' % (self.host, self.port, self.transport) + return '%s:%d;transport=%s' % (self.host, self.port, self.transport) @classmethod def from_description(cls, description): if not description: return None match = cls._description_re.match(description) if match is None: raise ValueError("illegal SIP proxy address: %s" % description) - return cls(**dict((k, v) for k, v in match.groupdict().iteritems() if v is not None)) + return cls(**dict((k, v) for k, v in list(match.groupdict().items()) if v is not None)) class STUNServerAddress(object): _description_re = re.compile(r"^(?P(\d{1,3}\.\d{1,3}\.\d{1,3}\.\d{1,3})|([a-zA-Z0-9\-_]+(\.[a-zA-Z0-9\-_]+)*))(:(?P\d+))?$") default_port = 3478 def __init__(self, host, port=default_port): self.host = Hostname(host) self.port = Port(port) def __getstate__(self): - return unicode(self) + return str(self) def __setstate__(self, state): match = self._description_re.match(state) if match is None: raise ValueError("illegal STUN server address: %s" % state) - self.__init__(**dict((k, v) for k, v in match.groupdict().iteritems() if v is not None)) + self.__init__(**dict((k, v) for k, v in list(match.groupdict().items()) if v is not None)) def __eq__(self, other): if isinstance(other, STUNServerAddress): return self.host == other.host and self.port == other.port else: return NotImplemented def __ne__(self, other): equal = self.__eq__(other) return NotImplemented if equal is NotImplemented else not equal __hash__ = None def __repr__(self): return '%s(%r, port=%r)' % (self.__class__.__name__, self.host, self.port) def __str__(self): return '%s:%d' % (self.host, self.port) def __unicode__(self): - return u'%s:%d' % (self.host, self.port) + return '%s:%d' % (self.host, self.port) @classmethod def from_description(cls, description): if not description: return None match = cls._description_re.match(description) if match is None: raise ValueError("illegal STUN server address: %s" % description) - return cls(**dict((k, v) for k, v in match.groupdict().iteritems() if v is not None)) + return cls(**dict((k, v) for k, v in list(match.groupdict().items()) if v is not None)) class STUNServerAddressList(List): type = STUNServerAddress class XCAPRoot(str): def __new__(cls, value): value = str(value) - uri = urlparse.urlparse(value) - if uri.scheme not in (u'http', u'https'): + uri = urllib.parse.urlparse(value) + if uri.scheme not in ('http', 'https'): raise ValueError("illegal XCAP root scheme (http and https only): %s" % uri.scheme) if uri.params: raise ValueError("XCAP root must not contain parameters: %s" % (uri.params,)) if uri.query: raise ValueError("XCAP root must not contain query component: %s" % (uri.query,)) if uri.fragment: raise ValueError("XCAP root must not contain fragment component: %s" % (uri.fragment,)) # check port and hostname Hostname(uri.hostname) if uri.port is not None: port = Port(uri.port) if port == 0: raise ValueError("illegal port value: 0") return value class MSRPConnectionModel(str): available_values = ('relay', 'acm') def __new__(cls, value): value = str(value) if value not in cls.available_values: raise ValueError("illegal value for MSRP NAT model: %s" % value) return value class MSRPTransport(str): available_values = ('tls', 'tcp') def __new__(cls, value): value = str(value) if value not in cls.available_values: raise ValueError("illegal value for MSRP transport: %s" % value) return value class SIPTransport(str): available_values = ('udp', 'tcp', 'tls') def __new__(cls, value): value = str(value) if value not in cls.available_values: raise ValueError("illegal value for SIP transport: %s" % value) return value class SIPTransportList(List): type = SIPTransport available_values = SIPTransport.available_values class SRTPKeyNegotiation(str): available_values = ('opportunistic', 'sdes_optional', 'sdes_mandatory', 'zrtp') def __new__(cls, value): value = str(value) if value not in cls.available_values: raise ValueError("illegal value for SRTP key negotiation: %s" % value) return value # Path datatypes -class Path(unicode): +class Path(str): def __new__(cls, path): return super(Path, cls).__new__(cls, os.path.normpath(path)) @property def normalized(self): if not self.startswith('~'): return self encoding = locale.getpreferredencoding() or 'ascii' return os.path.expanduser(self.encode(encoding)).decode(encoding) diff --git a/sipsimple/configuration/settings.py b/sipsimple/configuration/settings.py index 4d9405a5..4b6d025d 100644 --- a/sipsimple/configuration/settings.py +++ b/sipsimple/configuration/settings.py @@ -1,108 +1,108 @@ """ SIP SIMPLE settings. Definition of general (non-account related) settings. """ from sipsimple import __version__ from sipsimple.configuration import CorrelatedSetting, RuntimeSetting, Setting, SettingsGroup, SettingsObject from sipsimple.configuration.datatypes import NonNegativeInteger, PJSIPLogLevel from sipsimple.configuration.datatypes import AudioCodecList, SampleRate, VideoCodecList from sipsimple.configuration.datatypes import Port, PortRange, SIPTransportList from sipsimple.configuration.datatypes import Path from sipsimple.configuration.datatypes import H264Profile, VideoResolution __all__ = ['SIPSimpleSettings'] class EchoCancellerSettings(SettingsGroup): enabled = Setting(type=bool, default=True) tail_length = Setting(type=NonNegativeInteger, default=2) class AudioSettings(SettingsGroup): - alert_device = Setting(type=unicode, default=u'system_default', nillable=True) - input_device = Setting(type=unicode, default=u'system_default', nillable=True) - output_device = Setting(type=unicode, default=u'system_default', nillable=True) + alert_device = Setting(type=str, default='system_default', nillable=True) + input_device = Setting(type=str, default='system_default', nillable=True) + output_device = Setting(type=str, default='system_default', nillable=True) sample_rate = Setting(type=SampleRate, default=44100) muted = RuntimeSetting(type=bool, default=False) silent = Setting(type=bool, default=False) echo_canceller = EchoCancellerSettings class H264Settings(SettingsGroup): profile = Setting(type=H264Profile, default='baseline') level = Setting(type=str, default='3.1') class VideoSettings(SettingsGroup): - device = Setting(type=unicode, default=u'system_default', nillable=True) + device = Setting(type=str, default='system_default', nillable=True) resolution = Setting(type=VideoResolution, default=VideoResolution('1280x720')) framerate = Setting(type=int, default=25) max_bitrate = Setting(type=float, default=None, nillable=True) muted = RuntimeSetting(type=bool, default=False) h264 = H264Settings class ChatSettings(SettingsGroup): pass class ScreenSharingSettings(SettingsGroup): pass class FileTransferSettings(SettingsGroup): directory = Setting(type=Path, default=Path('~/Downloads')) class LogsSettings(SettingsGroup): trace_msrp = Setting(type=bool, default=False) trace_sip = Setting(type=bool, default=False) trace_pjsip = Setting(type=bool, default=False) pjsip_level = Setting(type=PJSIPLogLevel, default=5) class RTPSettings(SettingsGroup): port_range = Setting(type=PortRange, default=PortRange(50000, 50500)) timeout = Setting(type=NonNegativeInteger, default=30) audio_codec_list = Setting(type=AudioCodecList, default=AudioCodecList(('opus', 'G722', 'PCMU', 'PCMA'))) video_codec_list = Setting(type=VideoCodecList, default=VideoCodecList(('H264', 'VP8'))) def sip_port_validator(port, sibling_port): if port == sibling_port != 0: raise ValueError("the TCP and TLS ports must be different") class SIPSettings(SettingsGroup): invite_timeout = Setting(type=NonNegativeInteger, default=90, nillable=True) udp_port = Setting(type=Port, default=0) tcp_port = CorrelatedSetting(type=Port, sibling='tls_port', validator=sip_port_validator, default=0) tls_port = CorrelatedSetting(type=Port, sibling='tcp_port', validator=sip_port_validator, default=0) transport_list = Setting(type=SIPTransportList, default=SIPTransportList(('tls', 'tcp', 'udp'))) class TLSSettings(SettingsGroup): ca_list = Setting(type=Path, default=None, nillable=True) class SIPSimpleSettings(SettingsObject): __id__ = 'SIPSimpleSettings' default_account = Setting(type=str, default='bonjour@local', nillable=True) user_agent = Setting(type=str, default='sipsimple %s' % __version__) instance_id = Setting(type=str, default='') audio = AudioSettings video = VideoSettings chat = ChatSettings screen_sharing = ScreenSharingSettings file_transfer = FileTransferSettings logs = LogsSettings rtp = RTPSettings sip = SIPSettings tls = TLSSettings diff --git a/sipsimple/core/_engine.py b/sipsimple/core/_engine.py index cbd3dcd0..9c2c99b5 100644 --- a/sipsimple/core/_engine.py +++ b/sipsimple/core/_engine.py @@ -1,139 +1,138 @@ """ Implements a mechanism for starting the SIP core engine based on PJSIP (http://pjsip.org) stack. """ __all__ = ["Engine"] import sys import traceback import atexit from application.notification import NotificationCenter, NotificationData from application.python.types import Singleton from threading import Thread, RLock from sipsimple import log, __version__ from sipsimple.core._core import PJSIPUA, PJ_VERSION, PJ_SVN_REVISION, SIPCoreError -class Engine(Thread): - __metaclass__ = Singleton +class Engine(Thread, metaclass=Singleton): default_start_options = {"ip_address": None, "udp_port": 0, "tcp_port": None, "tls_port": None, "tls_verify_server": False, "tls_ca_file": None, "tls_cert_file": None, "tls_privkey_file": None, "tls_timeout": 3000, "user_agent": "sipsimple-%s-pjsip-%s-r%s" % (__version__, PJ_VERSION, PJ_SVN_REVISION), "log_level": 0, "trace_sip": False, "detect_sip_loops": True, "rtp_port_range": (50000, 50500), "zrtp_cache": None, "codecs": ["G722", "speex", "PCMU", "PCMA"], "video_codecs": ["H264", "H263-1998", "VP8"], "enable_colorbar_device": False, "events": {"conference": ["application/conference-info+xml"], "message-summary": ["application/simple-message-summary"], "presence": ["multipart/related", "application/rlmi+xml", "application/pidf+xml"], "presence.winfo": ["application/watcherinfo+xml"], "dialog": ["multipart/related", "application/rlmi+xml", "application/dialog-info+xml"], "dialog.winfo": ["application/watcherinfo+xml"], "refer": ["message/sipfrag;version=2.0"], "xcap-diff": ["application/xcap-diff+xml"]}, "incoming_events": set(), "incoming_requests": set()} def __init__(self): self.notification_center = NotificationCenter() self._thread_started = False self._thread_stopping = False self._lock = RLock() self._options = None atexit.register(self.stop) super(Engine, self).__init__() self.daemon = True @property def is_running(self): return (hasattr(self, "_ua") and hasattr(self, "_thread_started") and self._thread_started and not self._thread_stopping) def __dir__(self): if hasattr(self, '_ua'): ua_attributes = [attr for attr in dir(self._ua) if not attr.startswith('__') and attr != 'poll'] else: ua_attributes = [] - return sorted(set(dir(self.__class__) + self.__dict__.keys() + ua_attributes)) + return sorted(set(dir(self.__class__) + list(self.__dict__.keys()) + ua_attributes)) def __getattr__(self, attr): if attr not in ["_ua", "poll"] and hasattr(self, "_ua") and attr in dir(self._ua): return getattr(self._ua, attr) raise AttributeError("'%s' object has no attribute '%s'" % (self.__class__.__name__, attr)) def __setattr__(self, attr, value): if attr not in ["_ua", "poll"] and hasattr(self, "_ua") and attr in dir(self._ua): setattr(self._ua, attr, value) return object.__setattr__(self, attr, value) def start(self, **kwargs): with self._lock: if self._thread_started: raise SIPCoreError("Worker thread was already started once") self._options = kwargs self._thread_started = True super(Engine, self).start() def stop(self): with self._lock: if self._thread_stopping: return if self._thread_started: self._thread_stopping = True # worker thread def run(self): self.notification_center.post_notification('SIPEngineWillStart', sender=self) init_options = Engine.default_start_options.copy() init_options.update(self._options) try: self._ua = PJSIPUA(self._handle_event, **init_options) except Exception: log.exception('Exception occurred while starting the Engine') exc_type, exc_val, exc_tb = sys.exc_info() exc_tb = "".join(traceback.format_exception(exc_type, exc_val, exc_tb)) self.notification_center.post_notification('SIPEngineGotException', sender=self, data=NotificationData(type=exc_type, value=exc_val, traceback=exc_tb)) self.notification_center.post_notification('SIPEngineDidFail', sender=self) return else: self.notification_center.post_notification('SIPEngineDidStart', sender=self) failed = False while not self._thread_stopping: try: failed = self._ua.poll() except: log.exception('Exception occurred while running the Engine') exc_type, exc_val, exc_tb = sys.exc_info() self.notification_center.post_notification('SIPEngineGotException', sender=self, data=NotificationData(type=exc_type, value=exc_val, traceback="".join(traceback.format_exception(exc_type, exc_val, exc_tb)))) failed = True if failed: self.notification_center.post_notification('SIPEngineDidFail', sender=self) break if not failed: self.notification_center.post_notification('SIPEngineWillEnd', sender=self) self._ua.dealloc() del self._ua self.notification_center.post_notification('SIPEngineDidEnd', sender=self) def _handle_event(self, event_name, **kwargs): sender = kwargs.pop("obj", None) if sender is None: sender = self self.notification_center.post_notification(event_name, sender, NotificationData(**kwargs)) diff --git a/sipsimple/core/_helpers.py b/sipsimple/core/_helpers.py index 1af5b650..d999edef 100644 --- a/sipsimple/core/_helpers.py +++ b/sipsimple/core/_helpers.py @@ -1,129 +1,129 @@ """Miscellaneous SIP related helpers""" import random import socket import string from application.python.types import MarkerType from application.system import host from sipsimple.core._core import SIPURI from sipsimple.core._engine import Engine __all__ = ['Route', 'ContactURIFactory', 'NoGRUU', 'PublicGRUU', 'TemporaryGRUU', 'PublicGRUUIfAvailable', 'TemporaryGRUUIfAvailable'] class Route(object): _default_ports = dict(udp=5060, tcp=5060, tls=5061) def __init__(self, address, port=None, transport='udp'): self.address = address self.port = port self.transport = transport @property def address(self): return self._address @address.setter def address(self, address): try: socket.inet_aton(address) except: raise ValueError('illegal address: %s' % address) self._address = address @property def port(self): if self._port is None: return 5061 if self.transport == 'tls' else 5060 else: return self._port @port.setter def port(self, port): port = int(port) if port is not None else None if port is not None and not (0 < port < 65536): raise ValueError('illegal port value: %d' % port) self._port = port @property def transport(self): return self._transport @transport.setter def transport(self, transport): if transport not in ('udp', 'tcp', 'tls'): raise ValueError('illegal transport value: %s' % transport) self._transport = transport @property def uri(self): port = None if self._default_ports[self.transport] == self.port else self.port parameters = {} if self.transport == 'udp' else {'transport': self.transport} return SIPURI(host=self.address, port=port, parameters=parameters) def __repr__(self): return '{0.__class__.__name__}({0.address!r}, port={0.port!r}, transport={0.transport!r})'.format(self) def __str__(self): return str(self.uri) class ContactURIType(MarkerType): pass -class NoGRUU: __metaclass__ = ContactURIType -class PublicGRUU: __metaclass__ = ContactURIType -class TemporaryGRUU: __metaclass__ = ContactURIType -class PublicGRUUIfAvailable: __metaclass__ = ContactURIType -class TemporaryGRUUIfAvailable: __metaclass__ = ContactURIType +class NoGRUU(metaclass=ContactURIType): pass +class PublicGRUU(metaclass=ContactURIType): pass +class TemporaryGRUU(metaclass=ContactURIType): pass +class PublicGRUUIfAvailable(metaclass=ContactURIType): pass +class TemporaryGRUUIfAvailable(metaclass=ContactURIType): pass class ContactURIFactory(object): def __init__(self, username=None): self.username = username or ''.join(random.sample(string.digits, 8)) self.public_gruu = None self.temporary_gruu = None def __repr__(self): return '{0.__class__.__name__}(username={0.username!r})'.format(self) def __getitem__(self, key): if isinstance(key, tuple): contact_type, key = key if not isinstance(contact_type, ContactURIType): raise KeyError("unsupported contact type: %r" % contact_type) else: contact_type = NoGRUU - if not isinstance(key, (basestring, Route)): + if not isinstance(key, (str, Route)): raise KeyError("key must be a transport name or Route instance") - transport = key if isinstance(key, basestring) else key.transport + transport = key if isinstance(key, str) else key.transport parameters = {} if transport == 'udp' else {'transport': transport} if contact_type is PublicGRUU: if self.public_gruu is None: raise KeyError("could not get Public GRUU") uri = SIPURI.new(self.public_gruu) elif contact_type is TemporaryGRUU: if self.temporary_gruu is None: raise KeyError("could not get Temporary GRUU") uri = SIPURI.new(self.temporary_gruu) elif contact_type is PublicGRUUIfAvailable and self.public_gruu is not None: uri = SIPURI.new(self.public_gruu) elif contact_type is TemporaryGRUUIfAvailable and self.temporary_gruu is not None: uri = SIPURI.new(self.temporary_gruu) else: - ip = host.default_ip if isinstance(key, basestring) else host.outgoing_ip_for(key.address) + ip = host.default_ip if isinstance(key, str) else host.outgoing_ip_for(key.address) if ip is None: raise KeyError("could not get outgoing IP address") port = getattr(Engine(), '%s_port' % transport, None) if port is None: raise KeyError("unsupported transport: %s" % transport) uri = SIPURI(user=self.username, host=ip, port=port) uri.parameters.update(parameters) return uri diff --git a/sipsimple/core/_primitives.py b/sipsimple/core/_primitives.py index 18033dcb..4c83526b 100644 --- a/sipsimple/core/_primitives.py +++ b/sipsimple/core/_primitives.py @@ -1,314 +1,314 @@ """ Implements a high-level mechanism for SIP methods that can be used for non-session based operations like REGISTER, SUBSCRIBE, PUBLISH and MESSAGE. """ __all__ = ["Message", "Registration", "Publication", "PublicationError", "PublicationETagError"] from threading import RLock from application.notification import IObserver, NotificationCenter, NotificationData from application.python import Null from zope.interface import implements from sipsimple.core._core import ContactHeader, Header, Request, RouteHeader, SIPCoreError, SIPURI, ToHeader class Registration(object): implements(IObserver) def __init__(self, from_header, credentials=None, duration=300, extra_headers=None): self.from_header = from_header self.credentials = credentials self.duration = duration self.extra_headers = extra_headers or [] self._current_request = None self._last_request = None self._unregistering = False self._lock = RLock() is_registered = property(lambda self: self._last_request is not None) contact_uri = property(lambda self: None if self._last_request is None else self._last_request.contact_uri) expires_in = property(lambda self: 0 if self._last_request is None else self._last_request.expires_in) peer_address = property(lambda self: None if self._last_request is None else self._last_request.peer_address) def register(self, contact_header, route_header, timeout=None): with self._lock: try: self._make_and_send_request(contact_header, route_header, timeout, True) - except SIPCoreError, e: + except SIPCoreError as e: notification_center = NotificationCenter() notification_center.post_notification('SIPRegistrationDidFail', sender=self, data=NotificationData(code=0, reason=e.args[0], route_header=route_header)) def end(self, timeout=None): with self._lock: if self._last_request is None: return notification_center = NotificationCenter() notification_center.post_notification('SIPRegistrationWillEnd', sender=self) try: self._make_and_send_request(ContactHeader.new(self._last_request.contact_header), RouteHeader.new(self._last_request.route_header), timeout, False) - except SIPCoreError, e: + except SIPCoreError as e: notification_center.post_notification('SIPRegistrationDidNotEnd', sender=self, data=NotificationData(code=0, reason=e.args[0])) def handle_notification(self, notification): handler = getattr(self, '_NH_%s' % notification.name, Null) handler(notification) def _NH_SIPRequestDidSucceed(self, notification): request = notification.sender with self._lock: if request is not self._current_request: return self._current_request = None if self._unregistering: if self._last_request is not None: self._last_request.end() self._last_request = None notification.center.post_notification('SIPRegistrationDidEnd', sender=self, data=NotificationData(expired=False)) else: self._last_request = request try: contact_header_list = notification.data.headers["Contact"] except KeyError: contact_header_list = [] notification.center.post_notification('SIPRegistrationDidSucceed', sender=self, data=NotificationData(code=notification.data.code, reason=notification.data.reason, contact_header=request.contact_header, contact_header_list=contact_header_list, expires_in=notification.data.expires, route_header=request.route_header)) def _NH_SIPRequestDidFail(self, notification): request = notification.sender with self._lock: if request is not self._current_request: return self._current_request = None if self._unregistering: notification.center.post_notification('SIPRegistrationDidNotEnd', sender=self, data=NotificationData(code=notification.data.code, reason=notification.data.reason)) else: if hasattr(notification.data, 'headers'): min_expires = notification.data.headers.get('Min-Expires', None) else: min_expires = None notification.center.post_notification('SIPRegistrationDidFail', sender=self, data=NotificationData(code=notification.data.code, reason=notification.data.reason, route_header=request.route_header, min_expires=min_expires)) def _NH_SIPRequestWillExpire(self, notification): with self._lock: if notification.sender is not self._last_request: return notification.center.post_notification('SIPRegistrationWillExpire', sender=self, data=NotificationData(expires=notification.data.expires)) def _NH_SIPRequestDidEnd(self, notification): request = notification.sender with self._lock: notification.center.remove_observer(self, sender=request) if request is not self._last_request: return self._last_request = None if self._current_request is not None: self._current_request.end() self._current_request = None notification.center.post_notification('SIPRegistrationDidEnd', sender=self, data=NotificationData(expired=True)) def _make_and_send_request(self, contact_header, route_header, timeout, do_register): notification_center = NotificationCenter() prev_request = self._current_request or self._last_request if prev_request is not None: call_id = prev_request.call_id cseq = prev_request.cseq + 1 else: call_id = None cseq = 1 extra_headers = [] extra_headers.append(Header("Expires", str(int(self.duration) if do_register else 0))) extra_headers.extend(self.extra_headers) request = Request("REGISTER", SIPURI(self.from_header.uri.host), self.from_header, ToHeader.new(self.from_header), route_header, credentials=self.credentials, contact_header=contact_header, call_id=call_id, cseq=cseq, extra_headers=extra_headers) notification_center.add_observer(self, sender=request) if self._current_request is not None: # we are trying to send something already, cancel whatever it is self._current_request.end() self._current_request = None try: request.send(timeout=timeout) except: notification_center.remove_observer(self, sender=request) raise self._unregistering = not do_register self._current_request = request class Message(object): implements(IObserver) def __init__(self, from_header, to_header, route_header, content_type, body, credentials=None, extra_headers=None): self._request = Request("MESSAGE", to_header.uri, from_header, to_header, route_header, credentials=credentials, extra_headers=extra_headers, content_type=content_type, body=body) self._lock = RLock() from_header = property(lambda self: self._request.from_header) to_header = property(lambda self: self._request.to_header) route_header = property(lambda self: self._request.route_header) content_type = property(lambda self: self._request.content_type) body = property(lambda self: self._request.body) credentials = property(lambda self: self._request.credentials) is_sent = property(lambda self: self._request.state != "INIT") in_progress = property(lambda self: self._request.state == "IN_PROGRESS") peer_address = property(lambda self: self._request.peer_address) def send(self, timeout=None): notification_center = NotificationCenter() with self._lock: if self.is_sent: raise RuntimeError("This MESSAGE was already sent") notification_center.add_observer(self, sender=self._request) try: self._request.send(timeout) except: notification_center.remove_observer(self, sender=self._request) raise def handle_notification(self, notification): handler = getattr(self, '_NH_%s' % notification.name, Null) handler(notification) def _NH_SIPRequestDidSucceed(self, notification): if notification.data.expires: # this shouldn't happen really notification.sender.end() notification.center.post_notification('SIPMessageDidSucceed', sender=self, data=notification.data) def _NH_SIPRequestDidFail(self, notification): notification.center.post_notification('SIPMessageDidFail', sender=self, data=notification.data) def _NH_SIPRequestDidEnd(self, notification): notification.center.remove_observer(self, sender=notification.sender) class PublicationError(Exception): pass class PublicationETagError(PublicationError): pass class Publication(object): implements(IObserver) def __init__(self, from_header, event, content_type, credentials=None, duration=300, extra_headers=None): self.from_header = from_header self.event = event self.content_type = content_type self.credentials = credentials self.duration = duration self.extra_headers = extra_headers or [] self._last_etag = None self._current_request = None self._last_request = None self._unpublishing = False self._lock = RLock() is_published = property(lambda self: self._last_request is not None) expires_in = property(lambda self: 0 if self._last_request is None else self._last_request.expires_in) peer_address = property(lambda self: None if self._last_request is None else self._last_request.peer_address) def publish(self, body, route_header, timeout=None): with self._lock: if body is None: if self._last_request is None: raise ValueError("Need body for initial PUBLISH") elif self._last_etag is None: raise PublicationETagError("Cannot refresh, last ETag was invalid") self._make_and_send_request(body, route_header, timeout, True) def end(self, timeout=None): with self._lock: if self._last_request is None: return notification_center = NotificationCenter() notification_center.post_notification('SIPPublicationWillEnd', sender=self) try: self._make_and_send_request(None, RouteHeader.new(self._last_request.route_header), timeout, False) - except SIPCoreError, e: + except SIPCoreError as e: notification_center.post_notification('SIPPublicationDidNotEnd', sender=self, data=NotificationData(code=0, reason=e.args[0])) def handle_notification(self, notification): handler = getattr(self, '_NH_%s' % notification.name, Null) handler(notification) def _NH_SIPRequestDidSucceed(self, notification): request = notification.sender with self._lock: if request is not self._current_request: return self._current_request = None if self._unpublishing: if self._last_request is not None: self._last_request.end() self._last_request = None self._last_etag = None notification.center.post_notification('SIPPublicationDidEnd', sender=self, data=NotificationData(expired=False)) else: self._last_request = request self._last_etag = notification.data.headers["SIP-ETag"].body if "SIP-ETag" in notification.data.headers else None # TODO: add more data? notification.center.post_notification('SIPPublicationDidSucceed', sender=self, data=NotificationData(code=notification.data.code, reason=notification.data.reason, expires_in=notification.data.expires, route_header=request.route_header)) def _NH_SIPRequestDidFail(self, notification): request = notification.sender with self._lock: if request is not self._current_request: return self._current_request = None if notification.data.code == 412: self._last_etag = None if self._unpublishing: notification.center.post_notification('SIPPublicationDidNotEnd', sender=self, data=NotificationData(code=notification.data.code, reason=notification.data.reason)) else: notification.center.post_notification('SIPPublicationDidFail', sender=self, data=NotificationData(code=notification.data.code, reason=notification.data.reason, route_header=request.route_header)) def _NH_SIPRequestWillExpire(self, notification): with self._lock: if notification.sender is not self._last_request: return notification.center.post_notification('SIPPublicationWillExpire', sender=self, data=NotificationData(expires=notification.data.expires)) def _NH_SIPRequestDidEnd(self, notification): with self._lock: notification.center.remove_observer(self, sender=notification.sender) if notification.sender is not self._last_request: return self._last_request = None if self._current_request is not None: self._current_request.end() self._current_request = None self._last_etag = None notification.center.post_notification('SIPPublicationDidEnd', sender=self, data=NotificationData(expired=True)) def _make_and_send_request(self, body, route_header, timeout, do_publish): notification_center = NotificationCenter() extra_headers = [] extra_headers.append(Header("Event", self.event)) extra_headers.append(Header("Expires", str(int(self.duration) if do_publish else 0))) if self._last_etag is not None: extra_headers.append(Header("SIP-If-Match", self._last_etag)) extra_headers.extend(self.extra_headers) content_type = (self.content_type if body is not None else None) request = Request("PUBLISH", self.from_header.uri, self.from_header, ToHeader.new(self.from_header), route_header, credentials=self.credentials, cseq=1, extra_headers=extra_headers, content_type=content_type, body=body) notification_center.add_observer(self, sender=request) if self._current_request is not None: # we are trying to send something already, cancel whatever it is self._current_request.end() self._current_request = None try: request.send(timeout=timeout) except: notification_center.remove_observer(self, sender=request) raise self._unpublishing = not do_publish self._current_request = request diff --git a/sipsimple/logging.py b/sipsimple/logging.py index e0239abc..34a0529b 100644 --- a/sipsimple/logging.py +++ b/sipsimple/logging.py @@ -1,7 +1,7 @@ -from __future__ import absolute_import -from application import log + +from .application import log # Use a named logger for sipsimple logging log = log.get_logger('sipsimple') diff --git a/sipsimple/lookup.py b/sipsimple/lookup.py index 3f2bcefb..7ef574f1 100644 --- a/sipsimple/lookup.py +++ b/sipsimple/lookup.py @@ -1,559 +1,557 @@ """ Implements DNS lookups in the context of SIP, STUN and MSRP relay based on RFC3263 and related standards. This can be used to determine the next hop(s) and failover for routing of SIP messages and reservation of network resources prior the starting of a SIP session. """ -from __future__ import absolute_import + import re from itertools import chain from time import time -from urlparse import urlparse +from urllib.parse import urlparse # patch dns.entropy module which is not thread-safe import dns import sys from functools import partial from random import randint, randrange dns.entropy = dns.__class__('dns.entropy') dns.entropy.__file__ = dns.__file__.replace('__init__.py', 'entropy.py') dns.entropy.__builtins__ = dns.__builtins__ dns.entropy.random_16 = partial(randrange, 2**16) dns.entropy.between = randint sys.modules['dns.entropy'] = dns.entropy del partial, randint, randrange, sys # replace standard select and socket modules with versions from eventlib from eventlib import coros, proc from eventlib.green import select from eventlib.green import socket import dns.name import dns.resolver import dns.query dns.resolver.socket = socket dns.query.socket = socket dns.query.select = select dns.query._set_polling_backend(dns.query._select_for) -from application.notification import IObserver, NotificationCenter, NotificationData -from application.python import Null, limit -from application.python.decorator import decorator, preserve_signature -from application.python.types import Singleton +from .application.notification import IObserver, NotificationCenter, NotificationData +from .application.python import Null, limit +from .application.python.decorator import decorator, preserve_signature +from .application.python.types import Singleton from dns import exception, rdatatype from twisted.internet import reactor from zope.interface import implements from sipsimple.core import Route from sipsimple.threading import run_in_twisted_thread from sipsimple.threading.green import Command, InterruptCommand, run_in_waitable_green_thread def domain_iterator(domain): """ A generator which returns the domain and its parent domains. """ while domain not in ('.', ''): yield domain domain = (domain.split('.', 1)+[''])[1] @decorator def post_dns_lookup_notifications(func): @preserve_signature(func) def wrapper(obj, *args, **kwargs): notification_center = NotificationCenter() try: result = func(obj, *args, **kwargs) - except DNSLookupError, e: + except DNSLookupError as e: notification_center.post_notification('DNSLookupDidFail', sender=obj, data=NotificationData(error=str(e))) raise else: notification_center.post_notification('DNSLookupDidSucceed', sender=obj, data=NotificationData(result=result)) return result return wrapper class DNSLookupError(Exception): """ The error raised by DNSLookup when a lookup cannot be performed. """ class DNSCache(object): """ A simple DNS cache which uses twisted's timers to invalidate its expired data. """ def __init__(self): self.data = {} def get(self, key): return self.data.get(key, None) def put(self, key, value): expiration = value.expiration-time() if expiration > 0: self.data[key] = value reactor.callLater(limit(expiration, max=3600), self.data.pop, key, None) def flush(self, key=None): if key is not None: self.data.pop(key, None) else: self.data = {} class InternalResolver(dns.resolver.Resolver): def __init__(self, *args, **kw): super(InternalResolver, self).__init__(*args, **kw) if self.domain.to_text().endswith('local.'): self.domain = dns.name.root self.search = [item for item in self.search if not item.to_text().endswith('local.')] class DNSResolver(dns.resolver.Resolver): """ The resolver used by DNSLookup. The lifetime setting on it applies to all the queries made on this resolver. Each time a query is performed, its duration is subtracted from the lifetime value. """ def __init__(self): dns.resolver.Resolver.__init__(self, configure=False) dns_manager = DNSManager() self.search = dns_manager.search self.domain = dns_manager.domain self.nameservers = dns_manager.nameservers def query(self, *args, **kw): start_time = time() try: return dns.resolver.Resolver.query(self, *args, **kw) finally: self.lifetime -= min(self.lifetime, time()-start_time) class SRVResult(object): """ Internal object used to save the result of SRV queries. """ def __init__(self, priority, weight, port, address): self.priority = priority self.weight = weight self.port = port self.address = address class NAPTRResult(object): """ Internal object used to save the result of NAPTR queries. """ def __init__(self, service, order, preference, priority, weight, port, address): self.service = service self.order = order self.preference = preference self.priority = priority self.weight = weight self.port = port self.address = address class DNSLookup(object): cache = DNSCache() @run_in_waitable_green_thread @post_dns_lookup_notifications def lookup_service(self, uri, service, timeout=3.0, lifetime=15.0): """ Performs an SRV query to determine the servers used for the specified service from the domain in uri.host. If this fails and falling back is supported, also performs an A query on uri.host, returning the default port of the service along with the IP addresses in the answer. The services supported are `stun' and 'msrprelay'. The DNSLookupDidSucceed notification contains a result attribute which is a list of (address, port) tuples. The DNSLookupDidFail notification contains an error attribute describing the error encountered. """ service_srv_record_map = {"stun": ("_stun._udp", 3478, False), "msrprelay": ("_msrps._tcp", 2855, True)} log_context = dict(context='lookup_service', service=service, uri=uri) try: service_prefix, service_port, service_fallback = service_srv_record_map[service] except KeyError: raise DNSLookupError("Unknown service: %s" % service) try: # If the host part of the URI is an IP address, we will not do any lookup if re.match("^\d{1,3}\.\d{1,3}\.\d{1,3}\.\d{1,3}$", uri.host): return [(uri.host, uri.port or service_port)] resolver = DNSResolver() resolver.cache = self.cache resolver.timeout = timeout resolver.lifetime = lifetime record_name = '%s.%s' % (service_prefix, uri.host) services = self._lookup_srv_records(resolver, [record_name], log_context=log_context) if services[record_name]: return [(result.address, result.port) for result in services[record_name]] elif service_fallback: addresses = self._lookup_a_records(resolver, [uri.host], log_context=log_context) if addresses[uri.host]: return [(addr, service_port) for addr in addresses[uri.host]] except dns.resolver.Timeout: raise DNSLookupError('Timeout in lookup for %s servers for domain %s' % (service, uri.host)) else: raise DNSLookupError('No %s servers found for domain %s' % (service, uri.host)) @run_in_waitable_green_thread @post_dns_lookup_notifications def lookup_sip_proxy(self, uri, supported_transports, timeout=3.0, lifetime=15.0): """ Performs an RFC 3263 compliant lookup of transport/ip/port combinations for a particular SIP URI. As arguments it takes a SIPURI object and a list of supported transports, in order of preference of the application. It returns a list of Route objects that can be used in order of preference. The DNSLookupDidSucceed notification contains a result attribute which is a list of Route objects. The DNSLookupDidFail notification contains an error attribute describing the error encountered. """ naptr_service_transport_map = {"sips+d2t": "tls", "sip+d2t": "tcp", "sip+d2u": "udp"} transport_service_map = {"udp": "_sip._udp", "tcp": "_sip._tcp", "tls": "_sips._tcp"} log_context = dict(context='lookup_sip_proxy', uri=uri) if not supported_transports: raise DNSLookupError("No transports are supported") supported_transports = [transport.lower() for transport in supported_transports] unknown_transports = set(supported_transports).difference(transport_service_map) if unknown_transports: raise DNSLookupError("Unknown transports: %s" % ', '.join(unknown_transports)) try: # If the host part of the URI is an IP address, we will not do any lookup if re.match("^\d{1,3}\.\d{1,3}\.\d{1,3}\.\d{1,3}$", uri.host): transport = 'tls' if uri.secure else uri.transport.lower() if transport not in supported_transports: raise DNSLookupError("Transport %s dictated by URI is not supported" % transport) port = uri.port or (5061 if transport=='tls' else 5060) return [Route(address=uri.host, port=port, transport=transport)] resolver = DNSResolver() resolver.cache = self.cache resolver.timeout = timeout resolver.lifetime = lifetime # If the port is specified in the URI, we will only do an A lookup if uri.port: transport = 'tls' if uri.secure else uri.transport.lower() if transport not in supported_transports: raise DNSLookupError("Transport %s dictated by URI is not supported" % transport) addresses = self._lookup_a_records(resolver, [uri.host], log_context=log_context) if addresses[uri.host]: return [Route(address=addr, port=uri.port, transport=transport) for addr in addresses[uri.host]] # If the transport was already set as a parameter on the SIP URI, only do SRV lookups elif 'transport' in uri.parameters: transport = uri.parameters['transport'].lower() if transport not in supported_transports: raise DNSLookupError("Requested lookup for URI with %s transport, but it is not supported" % transport) if uri.secure and transport != 'tls': raise DNSLookupError("Requested lookup for SIPS URI, but with %s transport parameter" % transport) record_name = '%s.%s' % (transport_service_map[transport], uri.host) services = self._lookup_srv_records(resolver, [record_name], log_context=log_context) if services[record_name]: return [Route(address=result.address, port=result.port, transport=transport) for result in services[record_name]] else: # If SRV lookup fails, try A lookup addresses = self._lookup_a_records(resolver, [uri.host], log_context=log_context) port = 5061 if transport=='tls' else 5060 if addresses[uri.host]: return [Route(address=addr, port=port, transport=transport) for addr in addresses[uri.host]] # Otherwise, it means we don't have a numeric IP address, a port isn't specified and neither is a transport. So we have to do a full NAPTR lookup else: # If the URI is a SIPS URI, we only support the TLS transport. if uri.secure: if 'tls' not in supported_transports: raise DNSLookupError("Requested lookup for SIPS URI, but TLS transport is not supported") supported_transports = ['tls'] # First try NAPTR lookup - naptr_services = [service for service, transport in naptr_service_transport_map.iteritems() if transport in supported_transports] + naptr_services = [service for service, transport in list(naptr_service_transport_map.items()) if transport in supported_transports] try: pointers = self._lookup_naptr_record(resolver, uri.host, naptr_services, log_context=log_context) except dns.resolver.Timeout: pointers = [] if pointers: return [Route(address=result.address, port=result.port, transport=naptr_service_transport_map[result.service]) for result in pointers] else: # If that fails, try SRV lookup routes = [] for transport in supported_transports: record_name = '%s.%s' % (transport_service_map[transport], uri.host) try: services = self._lookup_srv_records(resolver, [record_name], log_context=log_context) except dns.resolver.Timeout: continue if services[record_name]: routes.extend(Route(address=result.address, port=result.port, transport=transport) for result in services[record_name]) if routes: return routes else: # If SRV lookup fails, try A lookup transport = 'tls' if uri.secure else 'udp' if transport in supported_transports: addresses = self._lookup_a_records(resolver, [uri.host], log_context=log_context) port = 5061 if transport=='tls' else 5060 if addresses[uri.host]: return [Route(address=addr, port=port, transport=transport) for addr in addresses[uri.host]] except dns.resolver.Timeout: raise DNSLookupError("Timeout in lookup for routes for SIP URI %s" % uri) else: raise DNSLookupError("No routes found for SIP URI %s" % uri) @run_in_waitable_green_thread @post_dns_lookup_notifications def lookup_xcap_server(self, uri, timeout=3.0, lifetime=15.0): """ Performs a TXT query against xcap. and returns all results that look like HTTP URIs. """ log_context = dict(context='lookup_xcap_server', uri=uri) notification_center = NotificationCenter() try: # If the host part of the URI is an IP address, we cannot not do any lookup if re.match("^\d{1,3}\.\d{1,3}\.\d{1,3}\.\d{1,3}$", uri.host): raise DNSLookupError("Cannot perform DNS query because the host is an IP address") resolver = DNSResolver() resolver.cache = self.cache resolver.timeout = timeout resolver.lifetime = lifetime record_name = 'xcap.%s' % uri.host results = [] try: answer = resolver.query(record_name, rdatatype.TXT) - except dns.resolver.Timeout, e: + except dns.resolver.Timeout as e: notification_center.post_notification('DNSLookupTrace', sender=self, data=NotificationData(query_type='TXT', query_name=str(record_name), nameservers=resolver.nameservers, answer=None, error=e, **log_context)) raise - except exception.DNSException, e: + except exception.DNSException as e: notification_center.post_notification('DNSLookupTrace', sender=self, data=NotificationData(query_type='TXT', query_name=str(record_name), nameservers=resolver.nameservers, answer=None, error=e, **log_context)) else: notification_center.post_notification('DNSLookupTrace', sender=self, data=NotificationData(query_type='TXT', query_name=str(record_name), nameservers=resolver.nameservers, answer=answer, error=None, **log_context)) for result_uri in list(chain(*(r.strings for r in answer.rrset))): parsed_uri = urlparse(result_uri) if parsed_uri.scheme in ('http', 'https') and parsed_uri.netloc: results.append(result_uri) if not results: raise DNSLookupError('No XCAP servers found for domain %s' % uri.host) return results except dns.resolver.Timeout: raise DNSLookupError('Timeout in lookup for XCAP servers for domain %s' % uri.host) def _lookup_a_records(self, resolver, hostnames, additional_records=[], log_context={}): notification_center = NotificationCenter() additional_addresses = dict((rset.name.to_text(), rset) for rset in additional_records if rset.rdtype == rdatatype.A) addresses = {} for hostname in hostnames: if hostname in additional_addresses: addresses[hostname] = [r.address for r in additional_addresses[hostname]] else: try: answer = resolver.query(hostname, rdatatype.A) - except dns.resolver.Timeout, e: + except dns.resolver.Timeout as e: notification_center.post_notification('DNSLookupTrace', sender=self, data=NotificationData(query_type='A', query_name=str(hostname), nameservers=resolver.nameservers, answer=None, error=e, **log_context)) raise - except exception.DNSException, e: + except exception.DNSException as e: notification_center.post_notification('DNSLookupTrace', sender=self, data=NotificationData(query_type='A', query_name=str(hostname), nameservers=resolver.nameservers, answer=None, error=e, **log_context)) addresses[hostname] = [] else: notification_center.post_notification('DNSLookupTrace', sender=self, data=NotificationData(query_type='A', query_name=str(hostname), nameservers=resolver.nameservers, answer=answer, error=None, **log_context)) addresses[hostname] = [r.address for r in answer.rrset] return addresses def _lookup_srv_records(self, resolver, srv_names, additional_records=[], log_context={}): notification_center = NotificationCenter() additional_services = dict((rset.name.to_text(), rset) for rset in additional_records if rset.rdtype == rdatatype.SRV) services = {} for srv_name in srv_names: services[srv_name] = [] if srv_name in additional_services: addresses = self._lookup_a_records(resolver, [r.target.to_text() for r in additional_services[srv_name]], additional_records) for record in additional_services[srv_name]: services[srv_name].extend(SRVResult(record.priority, record.weight, record.port, addr) for addr in addresses.get(record.target.to_text(), ())) else: try: answer = resolver.query(srv_name, rdatatype.SRV) - except dns.resolver.Timeout, e: + except dns.resolver.Timeout as e: notification_center.post_notification('DNSLookupTrace', sender=self, data=NotificationData(query_type='SRV', query_name=str(srv_name), nameservers=resolver.nameservers, answer=None, error=e, **log_context)) raise - except exception.DNSException, e: + except exception.DNSException as e: notification_center.post_notification('DNSLookupTrace', sender=self, data=NotificationData(query_type='SRV', query_name=str(srv_name), nameservers=resolver.nameservers, answer=None, error=e, **log_context)) else: notification_center.post_notification('DNSLookupTrace', sender=self, data=NotificationData(query_type='SRV', query_name=str(srv_name), nameservers=resolver.nameservers, answer=answer, error=None, **log_context)) addresses = self._lookup_a_records(resolver, [r.target.to_text() for r in answer.rrset], answer.response.additional, log_context) for record in answer.rrset: services[srv_name].extend(SRVResult(record.priority, record.weight, record.port, addr) for addr in addresses.get(record.target.to_text(), ())) services[srv_name].sort(key=lambda result: (result.priority, -result.weight)) return services def _lookup_naptr_record(self, resolver, domain, services, log_context={}): notification_center = NotificationCenter() pointers = [] try: answer = resolver.query(domain, rdatatype.NAPTR) - except dns.resolver.Timeout, e: + except dns.resolver.Timeout as e: notification_center.post_notification('DNSLookupTrace', sender=self, data=NotificationData(query_type='NAPTR', query_name=str(domain), nameservers=resolver.nameservers, answer=None, error=e, **log_context)) raise - except exception.DNSException, e: + except exception.DNSException as e: notification_center.post_notification('DNSLookupTrace', sender=self, data=NotificationData(query_type='NAPTR', query_name=str(domain), nameservers=resolver.nameservers, answer=None, error=e, **log_context)) else: notification_center.post_notification('DNSLookupTrace', sender=self, data=NotificationData(query_type='NAPTR', query_name=str(domain), nameservers=resolver.nameservers, answer=answer, error=None, **log_context)) records = [r for r in answer.rrset if r.service.lower() in services] services = self._lookup_srv_records(resolver, [r.replacement.to_text() for r in records], answer.response.additional, log_context) for record in records: pointers.extend(NAPTRResult(record.service.lower(), record.order, record.preference, r.priority, r.weight, r.port, r.address) for r in services.get(record.replacement.to_text(), ())) pointers.sort(key=lambda result: (result.order, result.preference)) return pointers -class DNSManager(object): - __metaclass__ = Singleton - +class DNSManager(object, metaclass=Singleton): implements(IObserver) def __init__(self): default_resolver = InternalResolver() self.search = default_resolver.search self.domain = default_resolver.domain self.nameservers = default_resolver.nameservers self.google_nameservers = ['8.8.8.8', '8.8.4.4'] self.probed_domain = 'sip2sip.info.' self._channel = coros.queue() self._proc = None self._timer = None self._wakeup_timer = None notification_center = NotificationCenter() notification_center.add_observer(self, name='SystemIPAddressDidChange') notification_center.add_observer(self, name='SystemDidWakeUpFromSleep') @property def nameservers(self): return self.__dict__['nameservers'] @nameservers.setter def nameservers(self, value): old_value = self.__dict__.get('nameservers', Null) self.__dict__['nameservers'] = value if old_value is Null: NotificationCenter().post_notification('DNSResolverDidInitialize', sender=self, data=NotificationData(nameservers=value)) elif value != old_value: NotificationCenter().post_notification('DNSNameserversDidChange', sender=self, data=NotificationData(nameservers=value)) def start(self): self._proc = proc.spawn(self._run) self._channel.send(Command('probe_dns')) def stop(self): if self._proc is not None: self._proc.kill() self._proc = None if self._timer is not None and self._timer.active(): self._timer.cancel() self._timer = None if self._wakeup_timer is not None and self._wakeup_timer.active(): self._wakeup_timer.cancel() self._wakeup_timer = None def _run(self): while True: try: command = self._channel.wait() handler = getattr(self, '_CH_%s' % command.name) handler(command) except InterruptCommand: pass def _CH_probe_dns(self, command): if self._timer is not None and self._timer.active(): self._timer.cancel() self._timer = None resolver = InternalResolver() self.domain = resolver.domain self.search = resolver.search local_nameservers = resolver.nameservers # probe local resolver resolver.timeout = 1 resolver.lifetime = 3 try: answer = resolver.query(self.probed_domain, rdatatype.NAPTR) if not any(record.rdtype == rdatatype.NAPTR for record in answer.rrset): raise exception.DNSException("No NAPTR records found") answer = resolver.query("_sip._udp.%s" % self.probed_domain, rdatatype.SRV) if not any(record.rdtype == rdatatype.SRV for record in answer.rrset): raise exception.DNSException("No SRV records found") except (dns.resolver.Timeout, exception.DNSException): pass else: self.nameservers = resolver.nameservers return # local resolver failed. probe google resolver resolver.nameservers = self.google_nameservers resolver.timeout = 2 resolver.lifetime = 4 try: answer = resolver.query(self.probed_domain, rdatatype.NAPTR) if not any(record.rdtype == rdatatype.NAPTR for record in answer.rrset): raise exception.DNSException("No NAPTR records found") except (dns.resolver.Timeout, exception.DNSException): pass else: self.nameservers = resolver.nameservers return # google resolver failed. fallback to local resolver and schedule another probe for later self.nameservers = local_nameservers self._timer = reactor.callLater(15, self._channel.send, Command('probe_dns')) @run_in_twisted_thread def handle_notification(self, notification): handler = getattr(self, '_NH_%s' % notification.name, Null) handler(notification) def _NH_SystemIPAddressDidChange(self, notification): self._proc.kill(InterruptCommand) self._channel.send(Command('probe_dns')) def _NH_SystemDidWakeUpFromSleep(self, notification): if self._wakeup_timer is None: def wakeup_action(): self._proc.kill(InterruptCommand) self._channel.send(Command('probe_dns')) self._wakeup_timer = None self._wakeup_timer = reactor.callLater(5, wakeup_action) # wait for system to stabilize diff --git a/sipsimple/payloads/__init__.py b/sipsimple/payloads/__init__.py index 9f477815..48535dec 100644 --- a/sipsimple/payloads/__init__.py +++ b/sipsimple/payloads/__init__.py @@ -1,1215 +1,1201 @@ __all__ = ['ParserError', 'BuilderError', 'ValidationError', 'IterateTypes', 'IterateIDs', 'IterateItems', 'All', 'parse_qname', 'XMLDocument', 'XMLAttribute', 'XMLElementID', 'XMLElementChild', 'XMLElementChoiceChild', 'XMLStringChoiceChild', 'XMLElement', 'XMLRootElement', 'XMLSimpleElement', 'XMLStringElement', 'XMLLocalizedStringElement', 'XMLBooleanElement', 'XMLByteElement', 'XMLUnsignedByteElement', 'XMLShortElement', 'XMLUnsignedShortElement', 'XMLIntElement', 'XMLUnsignedIntElement', 'XMLLongElement', 'XMLUnsignedLongElement', 'XMLIntegerElement', 'XMLPositiveIntegerElement', 'XMLNegativeIntegerElement', 'XMLNonNegativeIntegerElement', 'XMLNonPositiveIntegerElement', 'XMLDecimalElement', 'XMLDateTimeElement', 'XMLAnyURIElement', 'XMLEmptyElement', 'XMLEmptyElementRegistryType', 'XMLListElement', 'XMLListRootElement', 'XMLStringListElement'] import os import sys -import urllib +import urllib.request, urllib.parse, urllib.error from collections import defaultdict, deque from copy import deepcopy from decimal import Decimal -from itertools import chain, izip +from itertools import chain from weakref import WeakValueDictionary from application.python import Null from application.python.descriptor import classproperty from application.python.types import MarkerType from application.python.weakref import weakobjectmap from lxml import etree from sipsimple.payloads.datatypes import Boolean, Byte, UnsignedByte, Short, UnsignedShort, Int, UnsignedInt, Long, UnsignedLong from sipsimple.payloads.datatypes import PositiveInteger, NegativeInteger, NonNegativeInteger, NonPositiveInteger, DateTime, AnyURI from sipsimple.util import All ## Exceptions class ParserError(Exception): pass class BuilderError(Exception): pass class ValidationError(ParserError): pass ## Markers -class IterateTypes: __metaclass__ = MarkerType -class IterateIDs: __metaclass__ = MarkerType -class IterateItems: __metaclass__ = MarkerType +class IterateTypes(metaclass=MarkerType): pass +class IterateIDs(metaclass=MarkerType): pass +class IterateItems(metaclass=MarkerType): pass -class StoredAttribute: __metaclass__ = MarkerType +class StoredAttribute(metaclass=MarkerType): pass ## Utilities def parse_qname(qname): if qname[0] == '{': qname = qname[1:] return qname.split('}') else: return None, qname ## XMLDocument class XMLDocumentType(type): def __init__(cls, name, bases, dct): cls.nsmap = {} cls.schema_map = {} cls.element_map = {} cls.root_element = None cls.schema = None cls.parser = None for base in reversed(bases): if hasattr(base, 'element_map'): cls.element_map.update(base.element_map) if hasattr(base, 'schema_map'): cls.schema_map.update(base.schema_map) if hasattr(base, 'nsmap'): cls.nsmap.update(base.nsmap) cls._update_schema() def __setattr__(cls, name, value): if name == 'schema_path': if cls is not XMLDocument: raise AttributeError("%s can only be changed on XMLDocument" % name) super(XMLDocumentType, cls).__setattr__(name, value) def update_schema(document_class): document_class._update_schema() for document_subclass in document_class.__subclasses__(): update_schema(document_subclass) update_schema(XMLDocument) else: super(XMLDocumentType, cls).__setattr__(name, value) def _update_schema(cls): if cls.schema_map: - location_map = {ns: urllib.quote(os.path.abspath(os.path.join(cls.schema_path, schema_file)).replace('\\', '//')) for ns, schema_file in cls.schema_map.iteritems()} + location_map = {ns: urllib.parse.quote(os.path.abspath(os.path.join(cls.schema_path, schema_file)).replace('\\', '//')) for ns, schema_file in list(cls.schema_map.items())} schema = """ %s - """ % '\r\n'.join('' % (namespace, schema_location) for namespace, schema_location in location_map.iteritems()) + """ % '\r\n'.join('' % (namespace, schema_location) for namespace, schema_location in list(location_map.items())) cls.schema = etree.XMLSchema(etree.XML(schema)) cls.parser = etree.XMLParser(schema=cls.schema, remove_blank_text=True) else: cls.schema = None cls.parser = etree.XMLParser(remove_blank_text=True) -class XMLDocument(object): - __metaclass__ = XMLDocumentType - +class XMLDocument(object, metaclass=XMLDocumentType): encoding = 'UTF-8' content_type = None schema_path = os.path.join(os.path.dirname(__file__), 'xml-schemas') @classmethod def parse(cls, document): try: if isinstance(document, str): xml = etree.XML(document, parser=cls.parser) - elif isinstance(document, unicode): + elif isinstance(document, str): xml = etree.XML(document.encode('utf-8'), parser=cls.parser) else: xml = etree.parse(document, parser=cls.parser).getroot() if cls.schema is not None: cls.schema.assertValid(xml) return cls.root_element.from_element(xml, xml_document=cls) - except (etree.DocumentInvalid, etree.XMLSyntaxError, ValueError), e: + except (etree.DocumentInvalid, etree.XMLSyntaxError, ValueError) as e: raise ParserError(str(e)) @classmethod def build(cls, root_element, encoding=None, pretty_print=False, validate=True): if type(root_element) is not cls.root_element: raise TypeError("can only build XML documents from root elements of type %s" % cls.root_element.__name__) element = root_element.to_element() if validate and cls.schema is not None: cls.schema.assertValid(element) # Cleanup namespaces and move element NS mappings to the global scope. - normalized_element = etree.Element(element.tag, attrib=element.attrib, nsmap=dict(chain(element.nsmap.iteritems(), cls.nsmap.iteritems()))) + normalized_element = etree.Element(element.tag, attrib=element.attrib, nsmap=dict(chain(iter(list(element.nsmap.items())), iter(list(cls.nsmap.items()))))) normalized_element.text = element.text normalized_element.tail = element.tail normalized_element.extend(deepcopy(child) for child in element) etree.cleanup_namespaces(normalized_element) return etree.tostring(normalized_element, encoding=encoding or cls.encoding, method='xml', xml_declaration=True, pretty_print=pretty_print) @classmethod def create(cls, build_kw={}, **kw): return cls.build(cls.root_element(**kw), **build_kw) @classmethod def register_element(cls, xml_class): cls.element_map[xml_class.qname] = xml_class for child in cls.__subclasses__(): child.register_element(xml_class) @classmethod def get_element(cls, qname, default=None): return cls.element_map.get(qname, default) @classmethod def register_namespace(cls, namespace, prefix=None, schema=None): if prefix in cls.nsmap: raise ValueError("prefix %s is already registered in %s" % (prefix, cls.__name__)) - if namespace in cls.nsmap.itervalues(): + if namespace in iter(list(cls.nsmap.values())): raise ValueError("namespace %s is already registered in %s" % (namespace, cls.__name__)) cls.nsmap[prefix] = namespace if schema is not None: cls.schema_map[namespace] = schema cls._update_schema() for child in cls.__subclasses__(): child.register_namespace(namespace, prefix, schema) @classmethod def unregister_namespace(cls, namespace): try: - prefix = (prefix for prefix in cls.nsmap if cls.nsmap[prefix]==namespace).next() + prefix = next((prefix for prefix in cls.nsmap if cls.nsmap[prefix]==namespace)) except StopIteration: raise KeyError("namespace %s is not registered in %s" % (namespace, cls.__name__)) del cls.nsmap[prefix] schema = cls.schema_map.pop(namespace, None) if schema is not None: cls._update_schema() for child in cls.__subclasses__(): try: child.unregister_namespace(namespace) except KeyError: pass ## Children descriptors class XMLAttribute(object): - def __init__(self, name, xmlname=None, type=unicode, default=None, required=False, test_equal=True, onset=None, ondel=None): + def __init__(self, name, xmlname=None, type=str, default=None, required=False, test_equal=True, onset=None, ondel=None): self.name = name self.xmlname = xmlname or name self.type = type self.default = default self.__xmlparse__ = getattr(type, '__xmlparse__', lambda value: value) - self.__xmlbuild__ = getattr(type, '__xmlbuild__', unicode) + self.__xmlbuild__ = getattr(type, '__xmlbuild__', str) self.required = required self.test_equal = test_equal self.onset = onset self.ondel = ondel self.values = weakobjectmap() def __get__(self, obj, objtype): if obj is None: return self try: return self.values[obj] except KeyError: value = self.values.setdefault(obj, self.default) if value is not None: obj.element.set(self.xmlname, self.build(value)) return value def __set__(self, obj, value): if value is not None and not isinstance(value, self.type): value = self.type(value) old_value = self.values.get(obj, self.default) if value == old_value: return if value is not None: obj.element.set(self.xmlname, self.build(value)) else: obj.element.attrib.pop(self.xmlname, None) self.values[obj] = value obj.__dirty__ = True if self.onset: self.onset(obj, self, value) def __delete__(self, obj): obj.element.attrib.pop(self.xmlname, None) try: value = self.values.pop(obj) except KeyError: pass else: if value != self.default: obj.__dirty__ = True if self.ondel: self.ondel(obj, self) def parse(self, xmlvalue): return self.__xmlparse__(xmlvalue) def build(self, value): return self.__xmlbuild__(value) class XMLElementID(XMLAttribute): """An XMLAttribute that represents the ID of an element (immutable).""" def __set__(self, obj, value): if obj in self.values: raise AttributeError("An XML element ID cannot be changed") super(XMLElementID, self).__set__(obj, value) def __delete__(self, obj): raise AttributeError("An XML element ID cannot be deleted") class XMLElementChild(object): def __init__(self, name, type, required=False, test_equal=True, onset=None, ondel=None): self.name = name self.type = type self.required = required self.test_equal = test_equal self.onset = onset self.ondel = ondel self.values = weakobjectmap() def __get__(self, obj, objtype): if obj is None: return self try: return self.values[obj] except KeyError: return None def __set__(self, obj, value): if value is not None and not isinstance(value, self.type): value = self.type(value) same_value = False old_value = self.values.get(obj) if value is old_value: return elif value is not None and value == old_value: value.__dirty__ = old_value.__dirty__ same_value = True if old_value is not None: obj.element.remove(old_value.element) if value is not None: obj._insert_element(value.element) self.values[obj] = value if not same_value: obj.__dirty__ = True if self.onset: self.onset(obj, self, value) def __delete__(self, obj): try: old_value = self.values.pop(obj) except KeyError: pass else: if old_value is not None: obj.element.remove(old_value.element) obj.__dirty__ = True if self.ondel: self.ondel(obj, self) class XMLElementChoiceChildWrapper(object): __slots__ = ('descriptor', 'type') def __init__(self, descriptor, type): self.descriptor = descriptor self.type = type def __getattribute__(self, name): if name in ('descriptor', 'type', 'register_extension', 'unregister_extension'): return super(XMLElementChoiceChildWrapper, self).__getattribute__(name) else: return self.descriptor.__getattribute__(name) def __setattr__(self, name, value): if name in ('descriptor', 'type'): super(XMLElementChoiceChildWrapper, self).__setattr__(name, value) else: setattr(self.descriptor, name, value) def __dir__(self): return dir(self.descriptor) + ['descriptor', 'type', 'register_extension', 'unregister_extension'] def register_extension(self, type): if self.extension_type is None: raise ValueError("The %s XML choice element of %s does not support extensions" % (self.name, self.type.__name__)) if not issubclass(type, XMLElement) or not issubclass(type, self.extension_type): raise TypeError("type is not a subclass of XMLElement and/or %s: %s" % (self.extension_type.__name__, type.__name__)) if type in self.types: raise ValueError("%s is already registered as a choice extension" % type.__name__) self.types.add(type) self.type._xml_children_qname_map[type.qname] = (self.descriptor, type) for child_class in self.type.__subclasses__(): child_class._xml_children_qname_map[type.qname] = (self.descriptor, type) def unregister_extension(self, type): if self.extension_type is None: raise ValueError("The %s XML choice element of %s does not support extensions" % (self.name, self.type.__name__)) try: self.types.remove(type) except ValueError: raise ValueError("%s is not a registered choice extension on %s" % (type.__name__, self.type.__name__)) del self.type._xml_children_qname_map[type.qname] for child_class in self.type.__subclasses__(): del child_class._xml_children_qname_map[type.qname] class XMLElementChoiceChild(object): def __init__(self, name, types, extension_type=None, required=False, test_equal=True, onset=None, ondel=None): self.name = name self.types = set(types) self.extension_type = extension_type self.required = required self.test_equal = test_equal self.onset = onset self.ondel = ondel self.values = weakobjectmap() def __get__(self, obj, objtype): if obj is None: return XMLElementChoiceChildWrapper(self, objtype) try: return self.values[obj] except KeyError: return None def __set__(self, obj, value): if value is not None and type(value) not in self.types: raise TypeError("%s is not an acceptable type for %s" % (value.__class__.__name__, obj.__class__.__name__)) same_value = False old_value = self.values.get(obj) if value is old_value: return elif value is not None and value == old_value: value.__dirty__ = old_value.__dirty__ same_value = True if old_value is not None: obj.element.remove(old_value.element) if value is not None: obj._insert_element(value.element) self.values[obj] = value if not same_value: obj.__dirty__ = True if self.onset: self.onset(obj, self, value) def __delete__(self, obj): try: old_value = self.values.pop(obj) except KeyError: pass else: if old_value is not None: obj.element.remove(old_value.element) obj.__dirty__ = True if self.ondel: self.ondel(obj, self) class XMLStringChoiceChild(XMLElementChoiceChild): """ A choice between keyword strings from a registry, custom strings from the other type and custom extensions. This descriptor will accept and return strings instead of requiring XMLElement instances for the values in the registry and the other type. Check XMLEmptyElementRegistryType for a metaclass for building registries of XMLEmptyElement classes for keywords. """ def __init__(self, name, registry=None, other_type=None, extension_type=None): self.registry = registry self.other_type = other_type self.extension_type = extension_type types = registry.classes if registry is not None else () types += (other_type,) if other_type is not None else () super(XMLStringChoiceChild, self).__init__(name, types, extension_type=extension_type, required=True, test_equal=True) def __get__(self, obj, objtype): value = super(XMLStringChoiceChild, self).__get__(obj, objtype) if obj is None or objtype is StoredAttribute or value is None or isinstance(value, self.extension_type or ()): return value else: - return unicode(value) + return str(value) def __set__(self, obj, value): - if isinstance(value, basestring): + if isinstance(value, str): if self.registry is not None and value in self.registry.names: value = self.registry.class_map[value]() elif self.other_type is not None: value = self.other_type.from_string(value) super(XMLStringChoiceChild, self).__set__(obj, value) ## XMLElement base classes class XMLElementBase(object): """ This class is used as a common ancestor for XML elements and provides the means for super() to find at least dummy implementations for the methods that are supposed to be implemented by subclasses, even when they are not implemented by any other ancestor class. This is necessary in order to simplify access to these methods when multiple inheritance is involved and none or only some of the classes implement them. The methods declared here should to be implemented in subclasses as necessary. """ def __get_dirty__(self): return False def __set_dirty__(self, dirty): return def _build_element(self): return def _parse_element(self, element): return class XMLElementType(type): def __init__(cls, name, bases, dct): super(XMLElementType, cls).__init__(name, bases, dct) # set dictionary of xml attributes and xml child elements cls._xml_attributes = {} cls._xml_element_children = {} cls._xml_children_qname_map = {} for base in reversed(bases): if hasattr(base, '_xml_attributes'): cls._xml_attributes.update(base._xml_attributes) if hasattr(base, '_xml_element_children') and hasattr(base, '_xml_children_qname_map'): cls._xml_element_children.update(base._xml_element_children) cls._xml_children_qname_map.update(base._xml_children_qname_map) - for name, value in dct.iteritems(): + for name, value in list(dct.items()): if isinstance(value, XMLElementID): if cls._xml_id is not None: raise AttributeError("Only one XMLElementID attribute can be defined in the %s class" % cls.__name__) cls._xml_id = value cls._xml_attributes[value.name] = value elif isinstance(value, XMLAttribute): cls._xml_attributes[value.name] = value elif isinstance(value, XMLElementChild): cls._xml_element_children[value.name] = value cls._xml_children_qname_map[value.type.qname] = (value, value.type) elif isinstance(value, XMLElementChoiceChild): cls._xml_element_children[value.name] = value for type in value.types: cls._xml_children_qname_map[type.qname] = (value, type) # register class in its XMLDocument if cls._xml_document is not None: cls._xml_document.register_element(cls) -class XMLElement(XMLElementBase): - __metaclass__ = XMLElementType - +class XMLElement(XMLElementBase, metaclass=XMLElementType): _xml_tag = None # To be defined in subclass _xml_namespace = None # To be defined in subclass _xml_document = None # To be defined in subclass _xml_extension_type = None # Can be defined in subclass _xml_id = None # Can be defined in subclass, or will be set by the metaclass to the XMLElementID attribute (if present) _xml_children_order = {} # Can be defined in subclass # dynamically generated _xml_attributes = {} _xml_element_children = {} _xml_children_qname_map = {} qname = classproperty(lambda cls: '{%s}%s' % (cls._xml_namespace, cls._xml_tag)) def __init__(self): self.element = etree.Element(self.qname, nsmap=self._xml_document.nsmap) self.__dirty__ = True def __get_dirty__(self): return (self.__dict__['__dirty__'] or any(child.__dirty__ for child in (getattr(self, name) for name in self._xml_element_children) if child is not None) or super(XMLElement, self).__get_dirty__()) def __set_dirty__(self, dirty): super(XMLElement, self).__set_dirty__(dirty) if not dirty: for child in (child for child in (getattr(self, name) for name in self._xml_element_children) if child is not None): child.__dirty__ = dirty self.__dict__['__dirty__'] = dirty __dirty__ = property(__get_dirty__, __set_dirty__) def check_validity(self): # check attributes - for name, attribute in self._xml_attributes.iteritems(): + for name, attribute in list(self._xml_attributes.items()): # if attribute has default but it was not set, will also be added with this occasion value = getattr(self, name, None) if attribute.required and value is None: raise ValidationError("required attribute %s of %s is not set" % (name, self.__class__.__name__)) # check element children - for name, element_child in self._xml_element_children.iteritems(): + for name, element_child in list(self._xml_element_children.items()): # if child has default but it was not set, will also be added with this occasion child = getattr(self, name, None) if child is None and element_child.required: raise ValidationError("element child %s of %s is not set" % (name, self.__class__.__name__)) def to_element(self): try: self.check_validity() - except ValidationError, e: + except ValidationError as e: raise BuilderError(str(e)) # build element children for name in self._xml_element_children: descriptor = getattr(self.__class__, name) child = descriptor.__get__(self, StoredAttribute) if child is not None: child.to_element() self._build_element() return self.element @classmethod def from_element(cls, element, xml_document=None): obj = cls.__new__(cls) obj._xml_document = xml_document if xml_document is not None else cls._xml_document obj.element = element # set known attributes - for name, attribute in cls._xml_attributes.iteritems(): + for name, attribute in list(cls._xml_attributes.items()): xmlvalue = element.get(attribute.xmlname, None) if xmlvalue is not None: try: setattr(obj, name, attribute.parse(xmlvalue)) except (ValueError, TypeError): raise ValidationError("got illegal value for attribute %s of %s: %s" % (name, cls.__name__, xmlvalue)) # set element children for child in element: element_child, type = cls._xml_children_qname_map.get(child.tag, (None, None)) if element_child is not None: try: value = type.from_element(child, xml_document=obj._xml_document) except ValidationError: pass # we should accept partially valid documents else: setattr(obj, element_child.name, value) obj._parse_element(element) obj.check_validity() obj.__dirty__ = False return obj @classmethod def _register_xml_attribute(cls, attribute, element): cls._xml_element_children[attribute] = element cls._xml_children_qname_map[element.type.qname] = (element, element.type) for subclass in cls.__subclasses__(): subclass._register_xml_attribute(attribute, element) @classmethod def _unregister_xml_attribute(cls, attribute): element = cls._xml_element_children.pop(attribute) del cls._xml_children_qname_map[element.type.qname] for subclass in cls.__subclasses__(): subclass._unregister_xml_attribute(attribute) @classmethod def register_extension(cls, attribute, type, test_equal=True): if cls._xml_extension_type is None: raise ValueError("XMLElement type %s does not support extensions (requested extension type %s)" % (cls.__name__, type.__name__)) elif not issubclass(type, cls._xml_extension_type): raise TypeError("XMLElement type %s only supports extensions of type %s (requested extension type %s)" % (cls.__name__, cls._xml_extension_type, type.__name__)) elif hasattr(cls, attribute): raise ValueError("XMLElement type %s already has an attribute named %s (requested extension type %s)" % (cls.__name__, attribute, type.__name__)) extension = XMLElementChild(attribute, type=type, required=False, test_equal=test_equal) setattr(cls, attribute, extension) cls._register_xml_attribute(attribute, extension) @classmethod def unregister_extension(cls, attribute): if cls._xml_extension_type is None: raise ValueError("XMLElement type %s does not support extensions" % cls.__name__) cls._unregister_xml_attribute(attribute) delattr(cls, attribute) def _insert_element(self, element): if element in self.element: return - order = self._xml_children_order.get(element.tag, self._xml_children_order.get(None, sys.maxint)) - for i in xrange(len(self.element)): - child_order = self._xml_children_order.get(self.element[i].tag, self._xml_children_order.get(None, sys.maxint)) + order = self._xml_children_order.get(element.tag, self._xml_children_order.get(None, sys.maxsize)) + for i in range(len(self.element)): + child_order = self._xml_children_order.get(self.element[i].tag, self._xml_children_order.get(None, sys.maxsize)) if child_order > order: position = i break else: position = len(self.element) self.element.insert(position, element) def __eq__(self, other): if isinstance(other, XMLElement): if self is other: return True - for name, attribute in self._xml_attributes.iteritems(): + for name, attribute in list(self._xml_attributes.items()): if attribute.test_equal: if not hasattr(other, name) or getattr(self, name) != getattr(other, name): return False - for name, element_child in self._xml_element_children.iteritems(): + for name, element_child in list(self._xml_element_children.items()): if element_child.test_equal: if not hasattr(other, name) or getattr(self, name) != getattr(other, name): return False try: __eq__ = super(XMLElement, self).__eq__ except AttributeError: return True else: return __eq__(other) elif self._xml_id is not None: return self._xml_id == other else: return NotImplemented def __ne__(self, other): equal = self.__eq__(other) return NotImplemented if equal is NotImplemented else not equal def __hash__(self): if self._xml_id is not None: return hash(self._xml_id) else: return object.__hash__(self) class XMLRootElementType(XMLElementType): def __init__(cls, name, bases, dct): super(XMLRootElementType, cls).__init__(name, bases, dct) if cls._xml_document is not None: if cls._xml_document.root_element is not None: raise TypeError('there is already a root element registered for %s' % cls.__name__) cls._xml_document.root_element = cls -class XMLRootElement(XMLElement): - __metaclass__ = XMLRootElementType - +class XMLRootElement(XMLElement, metaclass=XMLRootElementType): def __init__(self): XMLElement.__init__(self) self.__cache__ = WeakValueDictionary({self.element: self}) @classmethod def from_element(cls, element, xml_document=None): obj = super(XMLRootElement, cls).from_element(element, xml_document) obj.__cache__ = WeakValueDictionary({obj.element: obj}) return obj @classmethod def parse(cls, document): return cls._xml_document.parse(document) def toxml(self, encoding=None, pretty_print=False, validate=True): return self._xml_document.build(self, encoding=encoding, pretty_print=pretty_print, validate=validate) def xpath(self, xpath, namespaces=None): result = [] try: nodes = self.element.xpath(xpath, namespaces=namespaces) except etree.XPathError: raise ValueError("illegal XPath expression") for element in (node for node in nodes if isinstance(node, etree._Element)): if element in self.__cache__: result.append(self.__cache__[element]) continue if element is self.element: self.__cache__[element] = self result.append(self) continue for ancestor in element.iterancestors(): if ancestor in self.__cache__: container = self.__cache__[ancestor] break else: container = self notvisited = deque([container]) visited = set() while notvisited: container = notvisited.popleft() self.__cache__[container.element] = container if isinstance(container, XMLListMixin): children = set(child for child in container if isinstance(child, XMLElement) and child not in visited) visited.update(children) notvisited.extend(children) for child in container._xml_element_children: value = getattr(container, child) if value is not None and value not in visited: visited.add(value) notvisited.append(value) if element in self.__cache__: result.append(self.__cache__[element]) return result def get_xpath(self, element): raise NotImplementedError def find_parent(self, element): raise NotImplementedError ## Mixin classes class ThisClass(object): """ Special marker class that is used to indicate that an XMLListElement subclass can be an item of itself. This is necessary because a class cannot reference itself when defining _xml_item_type """ class XMLListMixinType(type): def __init__(cls, name, bases, dct): super(XMLListMixinType, cls).__init__(name, bases, dct) if '_xml_item_type' in dct: cls._xml_item_type = cls._xml_item_type # trigger __setattr__ def __setattr__(cls, name, value): if name == '_xml_item_type': if value is ThisClass: value = cls elif isinstance(value, tuple) and ThisClass in value: value = tuple(cls if type is ThisClass else type for type in value) if value is None: cls._xml_item_element_types = () cls._xml_item_extension_types = () else: item_types = value if isinstance(value, tuple) else (value,) cls._xml_item_element_types = tuple(type for type in item_types if issubclass(type, XMLElement)) cls._xml_item_extension_types = tuple(type for type in item_types if not issubclass(type, XMLElement)) super(XMLListMixinType, cls).__setattr__(name, value) -class XMLListMixin(XMLElementBase): +class XMLListMixin(XMLElementBase, metaclass=XMLListMixinType): """A mixin representing a list of other XML elements""" - __metaclass__ = XMLListMixinType - _xml_item_type = None def __new__(cls, *args, **kw): if cls._xml_item_type is None: raise TypeError("The %s class cannot be instantiated because it doesn't define the _xml_item_type attribute" % cls.__name__) instance = super(XMLListMixin, cls).__new__(cls) instance._element_map = {} instance._xmlid_map = defaultdict(dict) return instance def __contains__(self, item): - return item in self._element_map.itervalues() + return item in iter(list(self._element_map.values())) def __iter__(self): return (self._element_map[element] for element in self.element if element in self._element_map) def __len__(self): return len(self._element_map) def __repr__(self): return '%s(%r)' % (self.__class__.__name__, list(self)) def __eq__(self, other): if isinstance(other, XMLListMixin): - return self is other or (len(self) == len(other) and all(self_item == other_item for self_item, other_item in izip(self, other))) + return self is other or (len(self) == len(other) and all(self_item == other_item for self_item, other_item in zip(self, other))) else: return NotImplemented def __ne__(self, other): equal = self.__eq__(other) return NotImplemented if equal is NotImplemented else not equal def __getitem__(self, key): if key is IterateTypes: - return (cls for cls, mapping in self._xmlid_map.iteritems() if mapping) + return (cls for cls, mapping in list(self._xmlid_map.items()) if mapping) if not isinstance(key, tuple): raise KeyError(key) try: cls, id = key except ValueError: raise KeyError(key) if id is IterateIDs: - return self._xmlid_map[cls].iterkeys() + return iter(list(self._xmlid_map[cls].keys())) elif id is IterateItems: - return self._xmlid_map[cls].itervalues() + return iter(list(self._xmlid_map[cls].values())) else: return self._xmlid_map[cls][id] def __delitem__(self, key): if not isinstance(key, tuple): raise KeyError(key) try: cls, id = key except ValueError: raise KeyError(key) if id is All: - for item in self._xmlid_map[cls].values(): + for item in list(self._xmlid_map[cls].values()): self.remove(item) else: self.remove(self._xmlid_map[cls][id]) def __get_dirty__(self): - return any(item.__dirty__ for item in self._element_map.itervalues()) or super(XMLListMixin, self).__get_dirty__() + return any(item.__dirty__ for item in list(self._element_map.values())) or super(XMLListMixin, self).__get_dirty__() def __set_dirty__(self, dirty): super(XMLListMixin, self).__set_dirty__(dirty) if not dirty: - for item in self._element_map.itervalues(): + for item in list(self._element_map.values()): item.__dirty__ = dirty def _parse_element(self, element): super(XMLListMixin, self)._parse_element(element) self._element_map.clear() self._xmlid_map.clear() for child in element[:]: child_class = self._xml_document.get_element(child.tag, type(None)) if child_class in self._xml_item_element_types or issubclass(child_class, self._xml_item_extension_types): try: value = child_class.from_element(child, xml_document=self._xml_document) except ValidationError: pass else: if value._xml_id is not None and value._xml_id in self._xmlid_map[child_class]: element.remove(child) else: if value._xml_id is not None: self._xmlid_map[child_class][value._xml_id] = value self._element_map[value.element] = value def _build_element(self): super(XMLListMixin, self)._build_element() - for child in self._element_map.itervalues(): + for child in list(self._element_map.values()): child.to_element() def add(self, item): if not (item.__class__ in self._xml_item_element_types or isinstance(item, self._xml_item_extension_types)): raise TypeError("%s cannot add items of type %s" % (self.__class__.__name__, item.__class__.__name__)) same_value = False if item._xml_id is not None and item._xml_id in self._xmlid_map[item.__class__]: old_item = self._xmlid_map[item.__class__][item._xml_id] if item is old_item: return elif item == old_item: item.__dirty__ = old_item.__dirty__ same_value = True self.element.remove(old_item.element) del self._xmlid_map[item.__class__][item._xml_id] del self._element_map[old_item.element] self._insert_element(item.element) if item._xml_id is not None: self._xmlid_map[item.__class__][item._xml_id] = item self._element_map[item.element] = item if not same_value: self.__dirty__ = True def remove(self, item): self.element.remove(item.element) if item._xml_id is not None: del self._xmlid_map[item.__class__][item._xml_id] del self._element_map[item.element] self.__dirty__ = True def update(self, sequence): for item in sequence: self.add(item) def clear(self): - for item in self._element_map.values(): + for item in list(self._element_map.values()): self.remove(item) ## Element types class XMLSimpleElement(XMLElement): _xml_value_type = None # To be defined in subclass def __new__(cls, *args, **kw): if cls._xml_value_type is None: raise TypeError("The %s class cannot be instantiated because it doesn't define the _xml_value_type attribute" % cls.__name__) return super(XMLSimpleElement, cls).__new__(cls) def __init__(self, value): XMLElement.__init__(self) self.value = value def __eq__(self, other): if isinstance(other, XMLSimpleElement): return self is other or self.value == other.value else: return self.value == other - def __nonzero__(self): + def __bool__(self): return bool(self.value) def __repr__(self): return '%s(%r)' % (self.__class__.__name__, self.value) def __str__(self): return str(self.value) def __unicode__(self): - return unicode(self.value) + return str(self.value) @property def value(self): return self.__dict__['value'] @value.setter def value(self, value): if not isinstance(value, self._xml_value_type): value = self._xml_value_type(value) if self.__dict__.get('value', Null) == value: return self.__dict__['value'] = value self.__dirty__ = True def _parse_element(self, element): super(XMLSimpleElement, self)._parse_element(element) - value = element.text or u'' + value = element.text or '' if hasattr(self._xml_value_type, '__xmlparse__'): self.value = self._xml_value_type.__xmlparse__(value) else: self.value = self._xml_value_type(value) def _build_element(self): super(XMLSimpleElement, self)._build_element() if hasattr(self.value, '__xmlbuild__'): self.element.text = self.value.__xmlbuild__() else: - self.element.text = unicode(self.value) + self.element.text = str(self.value) class XMLStringElement(XMLSimpleElement): - _xml_value_type = unicode # Can be overwritten in subclasses + _xml_value_type = str # Can be overwritten in subclasses def __len__(self): return len(self.value) class XMLLocalizedStringElement(XMLStringElement): lang = XMLAttribute('lang', xmlname='{http://www.w3.org/XML/1998/namespace}lang', type=str, required=False, test_equal=True) def __init__(self, value, lang=None): XMLStringElement.__init__(self, value) self.lang = lang def __eq__(self, other): if isinstance(other, XMLLocalizedStringElement): return self is other or (self.lang == other.lang and self.value == other.value) elif self.lang is None: return XMLStringElement.__eq__(self, other) else: return NotImplemented def __repr__(self): return '%s(%r, %r)' % (self.__class__.__name__, self.value, self.lang) def _parse_element(self, element): super(XMLLocalizedStringElement, self)._parse_element(element) self.lang = element.get('{http://www.w3.org/XML/1998/namespace}lang', None) class XMLBooleanElement(XMLSimpleElement): _xml_value_type = Boolean class XMLByteElement(XMLSimpleElement): _xml_value_type = Byte class XMLUnsignedByteElement(XMLSimpleElement): _xml_value_type = UnsignedByte class XMLShortElement(XMLSimpleElement): _xml_value_type = Short class XMLUnsignedShortElement(XMLSimpleElement): _xml_value_type = UnsignedShort class XMLIntElement(XMLSimpleElement): _xml_value_type = Int class XMLUnsignedIntElement(XMLSimpleElement): _xml_value_type = UnsignedInt class XMLLongElement(XMLSimpleElement): _xml_value_type = Long class XMLUnsignedLongElement(XMLSimpleElement): _xml_value_type = UnsignedLong class XMLIntegerElement(XMLSimpleElement): _xml_value_type = int class XMLPositiveIntegerElement(XMLSimpleElement): _xml_value_type = PositiveInteger class XMLNegativeIntegerElement(XMLSimpleElement): _xml_value_type = NegativeInteger class XMLNonNegativeIntegerElement(XMLSimpleElement): _xml_value_type = NonNegativeInteger class XMLNonPositiveIntegerElement(XMLSimpleElement): _xml_value_type = NonPositiveInteger class XMLDecimalElement(XMLSimpleElement): _xml_value_type = Decimal class XMLDateTimeElement(XMLSimpleElement): _xml_value_type = DateTime class XMLAnyURIElement(XMLStringElement): _xml_value_type = AnyURI class XMLEmptyElement(XMLElement): def __repr__(self): return '%s()' % self.__class__.__name__ def __eq__(self, other): return type(self) is type(other) or NotImplemented def __hash__(self): return hash(self.__class__) class XMLEmptyElementRegistryType(type): """A metaclass for building registries of XMLEmptyElement subclasses from names""" def __init__(cls, name, bases, dct): super(XMLEmptyElementRegistryType, cls).__init__(name, bases, dct) typename = getattr(cls, '__typename__', name.partition('Registry')[0]).capitalize() class BaseElementType(XMLEmptyElement): def __str__(self): return self._xml_tag - def __unicode__(self): return unicode(self._xml_tag) + def __unicode__(self): return str(self._xml_tag) cls.__basetype__ = BaseElementType cls.__basetype__.__name__ = 'Base%sType' % typename cls.class_map = {} for name in cls.names: class ElementType(BaseElementType): _xml_tag = name _xml_namespace = cls._xml_namespace _xml_document = cls._xml_document _xml_id = name ElementType.__name__ = typename + name.title().translate(None, '-_') cls.class_map[name] = ElementType cls.classes = tuple(cls.class_map[name] for name in cls.names) ## Created using mixins class XMLListElementType(XMLElementType, XMLListMixinType): pass class XMLListRootElementType(XMLRootElementType, XMLListMixinType): pass -class XMLListElement(XMLElement, XMLListMixin): - __metaclass__ = XMLListElementType - - def __nonzero__(self): +class XMLListElement(XMLElement, XMLListMixin, metaclass=XMLListElementType): + def __bool__(self): if self._xml_attributes or self._xml_element_children: return True else: return len(self._element_map) != 0 -class XMLListRootElement(XMLRootElement, XMLListMixin): - __metaclass__ = XMLListRootElementType - - def __nonzero__(self): +class XMLListRootElement(XMLRootElement, XMLListMixin, metaclass=XMLListRootElementType): + def __bool__(self): if self._xml_attributes or self._xml_element_children: return True else: return len(self._element_map) != 0 class XMLStringListElementType(XMLListElementType): def __init__(cls, name, bases, dct): if cls._xml_item_type is not None: raise TypeError("The %s class should not define _xml_item_type, but define _xml_item_registry, _xml_item_other_type and _xml_item_extension_type instead" % cls.__name__) types = cls._xml_item_registry.classes if cls._xml_item_registry is not None else () types += tuple(type for type in (cls._xml_item_other_type, cls._xml_item_extension_type) if type is not None) cls._xml_item_type = types or None super(XMLStringListElementType, cls).__init__(name, bases, dct) -class XMLStringListElement(XMLListElement): - __metaclass__ = XMLStringListElementType - +class XMLStringListElement(XMLListElement, metaclass=XMLStringListElementType): _xml_item_registry = None _xml_item_other_type = None _xml_item_extension_type = None def __contains__(self, item): - if isinstance(item, basestring): + if isinstance(item, str): if self._xml_item_registry is not None and item in self._xml_item_registry.names: item = self._xml_item_registry.class_map[item]() elif self._xml_item_other_type is not None: item = self._xml_item_other_type.from_string(item) - return item in self._element_map.itervalues() + return item in iter(list(self._element_map.values())) def __iter__(self): - return (item if isinstance(item, self._xml_item_extension_types) else unicode(item) for item in super(XMLStringListElement, self).__iter__()) + return (item if isinstance(item, self._xml_item_extension_types) else str(item) for item in super(XMLStringListElement, self).__iter__()) def add(self, item): - if isinstance(item, basestring): + if isinstance(item, str): if self._xml_item_registry is not None and item in self._xml_item_registry.names: item = self._xml_item_registry.class_map[item]() elif self._xml_item_other_type is not None: item = self._xml_item_other_type.from_string(item) super(XMLStringListElement, self).add(item) def remove(self, item): - if isinstance(item, basestring): + if isinstance(item, str): if self._xml_item_registry is not None and item in self._xml_item_registry.names: xmlitem = self._xml_item_registry.class_map[item]() try: - item = (entry for entry in super(XMLStringListElement, self).__iter__() if entry == xmlitem).next() + item = next((entry for entry in super(XMLStringListElement, self).__iter__() if entry == xmlitem)) except StopIteration: raise KeyError(item) elif self._xml_item_other_type is not None: xmlitem = self._xml_item_other_type.from_string(item) try: - item = (entry for entry in super(XMLStringListElement, self).__iter__() if entry == xmlitem).next() + item = next((entry for entry in super(XMLStringListElement, self).__iter__() if entry == xmlitem)) except StopIteration: raise KeyError(item) super(XMLStringListElement, self).remove(item) diff --git a/sipsimple/payloads/addressbook.py b/sipsimple/payloads/addressbook.py index 5f5d6b03..9ab4fd13 100644 --- a/sipsimple/payloads/addressbook.py +++ b/sipsimple/payloads/addressbook.py @@ -1,368 +1,368 @@ """Addressbook related payload elements""" __all__ = ['namespace', 'Group', 'Contact', 'ContactURI', 'Policy', 'ElementExtension', 'ElementAttributes'] from application.python import Null from lxml import etree from sipsimple.payloads import XMLElement, XMLListElement, XMLStringElement, XMLBooleanElement, XMLElementID, XMLAttribute, XMLElementChild from sipsimple.payloads import IterateIDs, IterateItems, All from sipsimple.payloads.datatypes import AnyURI, ID from sipsimple.payloads.resourcelists import ResourceListsDocument, ListElement namespace = 'urn:ag-projects:xml:ns:addressbook' ResourceListsDocument.register_namespace(namespace, prefix='addressbook', schema='addressbook.xsd') class ElementExtension(object): pass class Name(XMLStringElement): _xml_tag = 'name' _xml_namespace = namespace _xml_document = ResourceListsDocument class ContactID(XMLStringElement): _xml_tag = 'contact_id' _xml_namespace = namespace _xml_document = ResourceListsDocument _xml_value_type = ID class ContactList(XMLListElement): _xml_tag = 'contacts' _xml_namespace = namespace _xml_document = ResourceListsDocument _xml_item_type = ContactID def __init__(self, contacts=[]): XMLListElement.__init__(self) self.update(contacts) def __contains__(self, item): - if isinstance(item, basestring): + if isinstance(item, str): item = ContactID(item) return super(ContactList, self).__contains__(item) def __iter__(self): return (item.value for item in super(ContactList, self).__iter__()) def add(self, item): - if isinstance(item, basestring): + if isinstance(item, str): item = ContactID(item) super(ContactList, self).add(item) def remove(self, item): - if isinstance(item, basestring): + if isinstance(item, str): try: - item = (entry for entry in super(ContactList, self).__iter__() if entry.value == item).next() + item = next((entry for entry in super(ContactList, self).__iter__() if entry.value == item)) except StopIteration: raise KeyError(item) super(ContactList, self).remove(item) class Group(XMLElement, ListElement): _xml_tag = 'group' _xml_namespace = namespace _xml_extension_type = ElementExtension _xml_document = ResourceListsDocument id = XMLElementID('id', type=ID, required=True, test_equal=True) name = XMLElementChild('name', type=Name, required=True, test_equal=True) contacts = XMLElementChild('contacts', type=ContactList, required=True, test_equal=True) def __init__(self, id, name, contacts=[]): XMLElement.__init__(self) self.id = id self.name = name self.contacts = ContactList(contacts) def __unicode__(self): - return unicode(self.name) + return str(self.name) def __repr__(self): return '%s(%r, %r, contacts=%r)' % (self.__class__.__name__, self.id, self.name, list(self.contacts)) class ContactURI(XMLElement): _xml_tag = 'uri' _xml_namespace = namespace _xml_extension_type = ElementExtension _xml_document = ResourceListsDocument id = XMLElementID('id', type=ID, required=True, test_equal=True) uri = XMLAttribute('uri', type=AnyURI, required=True, test_equal=True) - type = XMLAttribute('type', type=unicode, required=False, test_equal=True) + type = XMLAttribute('type', type=str, required=False, test_equal=True) def __init__(self, id, uri, type=None): XMLElement.__init__(self) self.id = id self.uri = uri self.type = type def __unicode__(self): - return unicode(self.uri) + return str(self.uri) def __repr__(self): return '%s(%r, %r, %r)' % (self.__class__.__name__, self.id, self.uri, self.type) class ContactURIList(XMLListElement): _xml_tag = 'uris' _xml_namespace = namespace _xml_document = ResourceListsDocument _xml_item_type = ContactURI default = XMLAttribute('default', type=str, required=False, test_equal=True) def __init__(self, uris=[], default=None): XMLListElement.__init__(self) self.update(uris) self.default = default def __getitem__(self, key): if key is IterateIDs: - return self._xmlid_map[ContactURI].iterkeys() + return iter(list(self._xmlid_map[ContactURI].keys())) elif key is IterateItems: - return self._xmlid_map[ContactURI].itervalues() + return iter(list(self._xmlid_map[ContactURI].values())) else: return self._xmlid_map[ContactURI][key] def __delitem__(self, key): if key is All: - for item in self._xmlid_map[ContactURI].values(): + for item in list(self._xmlid_map[ContactURI].values()): self.remove(item) else: self.remove(self._xmlid_map[ContactURI][key]) def get(self, key, default=None): return self._xmlid_map[ContactURI].get(key, default) class PolicyValue(str): def __new__(cls, value): if value not in ('allow', 'block', 'default'): raise ValueError("Invalid policy value: %s" % value) return super(PolicyValue, cls).__new__(cls, value) class PolicyString(XMLStringElement): _xml_tag = 'policy' _xml_namespace = namespace _xml_document = ResourceListsDocument _xml_value_type = PolicyValue class Subscribe(XMLBooleanElement): _xml_tag = 'subscribe' _xml_namespace = namespace _xml_document = ResourceListsDocument class DialogHandling(XMLElement): _xml_tag = 'dialog' _xml_namespace = namespace _xml_document = ResourceListsDocument policy = XMLElementChild('policy', type=PolicyString, required=True, test_equal=True) subscribe = XMLElementChild('subscribe', type=Subscribe, required=True, test_equal=True) def __init__(self, policy, subscribe): XMLElement.__init__(self) self.policy = policy self.subscribe = subscribe def __repr__(self): return '%s(%r, %r)' % (self.__class__.__name__, self.policy, self.subscribe) class PresenceHandling(XMLElement): _xml_tag = 'presence' _xml_namespace = namespace _xml_document = ResourceListsDocument policy = XMLElementChild('policy', type=PolicyString, required=True, test_equal=True) subscribe = XMLElementChild('subscribe', type=Subscribe, required=True, test_equal=True) def __init__(self, policy, subscribe): XMLElement.__init__(self) self.policy = policy self.subscribe = subscribe def __repr__(self): return '%s(%r, %r)' % (self.__class__.__name__, self.policy, self.subscribe) class Contact(XMLElement, ListElement): _xml_tag = 'contact' _xml_namespace = namespace _xml_extension_type = ElementExtension _xml_document = ResourceListsDocument id = XMLElementID('id', type=ID, required=True, test_equal=True) name = XMLElementChild('name', type=Name, required=True, test_equal=True) uris = XMLElementChild('uris', type=ContactURIList, required=True, test_equal=True) dialog = XMLElementChild('dialog', type=DialogHandling, required=True, test_equal=True) presence = XMLElementChild('presence', type=PresenceHandling, required=True, test_equal=True) def __init__(self, id, name, uris=[], presence_handling=None, dialog_handling=None): XMLElement.__init__(self) self.id = id self.name = name self.uris = uris self.dialog = dialog_handling or DialogHandling('default', False) self.presence = presence_handling or PresenceHandling('default', False) def __repr__(self): return '%s(%r, %r, %r, %r, %r)' % (self.__class__.__name__, self.id, self.name, list(self.uris), self.presence, self.dialog) class Policy(XMLElement, ListElement): _xml_tag = 'policy-element' _xml_namespace = namespace _xml_extension_type = ElementExtension _xml_document = ResourceListsDocument id = XMLElementID('id', type=ID, required=True, test_equal=True) uri = XMLAttribute('uri', type=AnyURI, required=True, test_equal=True) name = XMLElementChild('name', type=Name, required=True, test_equal=True) dialog = XMLElementChild('dialog', type=DialogHandling, required=True, test_equal=True) presence = XMLElementChild('presence', type=PresenceHandling, required=True, test_equal=True) def __init__(self, id, uri, name='', presence_handling=None, dialog_handling=None): XMLElement.__init__(self) self.id = id self.uri = uri self.name = name self.dialog = dialog_handling or DialogHandling('default', False) self.presence = presence_handling or PresenceHandling('default', False) def __unicode__(self): - return unicode(self.uri) + return str(self.uri) def __repr__(self): return '%s(%r, %r, %r, %r, %r)' % (self.__class__.__name__, self.id, self.uri, self.name, self.presence, self.dialog) # # Extensions # class ElementAttributes(XMLElement, ElementExtension): _xml_tag = 'attributes' _xml_namespace = 'urn:ag-projects:sipsimple:xml:ns:addressbook' _xml_document = ResourceListsDocument def __init__(self, iterable=(), **attributes): XMLElement.__init__(self) self._attributes = dict() self.update(iterable, **attributes) def _parse_element(self, element): self._attributes = dict() attribute_tag = '{%s}attribute' % self._xml_namespace for child in (child for child in element if child.tag == attribute_tag): if 'nil' in child.attrib: self[child.attrib['name']] = None else: - self[child.attrib['name']] = unicode(child.text or u'') + self[child.attrib['name']] = str(child.text or '') def _build_element(self): self.element.clear() attribute_tag = '{%s}attribute' % self._xml_namespace - for key, value in self.iteritems(): + for key, value in list(self.items()): child = etree.SubElement(self.element, attribute_tag, nsmap=self._xml_document.nsmap) child.attrib['name'] = key if value is None: child.attrib['nil'] = 'true' else: child.text = value def __contains__(self, key): return key in self._attributes def __iter__(self): return iter(self._attributes) def __len__(self): return len(self._attributes) def __getitem__(self, key): return self._attributes[key] def __setitem__(self, key, value): if self._attributes.get(key, Null) == value: return self._attributes[key] = value self.__dirty__ = True def __delitem__(self, key): del self._attributes[key] self.__dirty__ = True def __repr__(self): return "%s(%r)" % (self.__class__.__name__, dict(self)) def clear(self): if self._attributes: self._attributes.clear() self.__dirty__ = True def get(self, key, default=None): return self._attributes.get(key, default) def has_key(self, key): return key in self._attributes def items(self): - return self._attributes.items() + return list(self._attributes.items()) def iteritems(self): - return self._attributes.iteritems() + return iter(list(self._attributes.items())) def iterkeys(self): - return self._attributes.iterkeys() + return iter(list(self._attributes.keys())) def itervalues(self): - return self._attributes.itervalues() + return iter(list(self._attributes.values())) def keys(self): - return self._attributes.keys() + return list(self._attributes.keys()) def pop(self, key, *args): value = self._attributes.pop(key, *args) if not args or value is not args[0]: self.__dirty__ = True return value def popitem(self): value = self._attributes.popitem() self.__dirty__ = True return value def setdefault(self, key, default=None): value = self._attributes.setdefault(key, default) if value is default: self.__dirty__ = True return value def update(self, iterable=(), **attributes): self._attributes.update(iterable, **attributes) if iterable or attributes: self.__dirty__ = True ResourceListsDocument.register_namespace(ElementAttributes._xml_namespace, prefix='sipsimple') Group.register_extension('attributes', ElementAttributes) Contact.register_extension('attributes', ElementAttributes) ContactURI.register_extension('attributes', ElementAttributes) Policy.register_extension('attributes', ElementAttributes) diff --git a/sipsimple/payloads/caps.py b/sipsimple/payloads/caps.py index 93f5bce5..b87eb345 100644 --- a/sipsimple/payloads/caps.py +++ b/sipsimple/payloads/caps.py @@ -1,808 +1,794 @@ # This module is partially broken. It breaks the core assumption of the # payloads infrastructure, that an element qname is unique inside a given # application. Fortunately, the elements with duplicate qnames are used # as child elements for other elements, which are not affected by the # problem as each element keeps it's own qname mapping for its children # qnames. The problem only affects different elements with the same qname # that are used in list elements, as currently the list element uses the # application's qname mapping to find the classes and that mapping is # broken when multiple elements with the same qname are defined. # In other words, this module works, but the application qname mapping # that is generated by it is broken. # # -Dan """ User Agent Capability Extension handling according to RFC5196 This module provides an extension to PIDF to describe a user-agent capabilities in the PIDF documents. """ __all__ = ['namespace', 'Audio', 'Application', 'Data', 'Control', 'Video', 'Video', 'Text', 'Message', 'Type', 'Automata', 'Class', 'Duplex', 'Description', 'EventPackages', 'Priority', 'Methods', 'Extensions', 'Scheme', 'Schemes', 'Actor', 'IsFocus', 'Languages', 'Language', 'ServiceCapabilities', 'Mobility', 'DeviceCapabilities', 'ServiceCapabilitiesExtension', 'EventPackagesExtension', 'PriorityExtension', 'MethodsExtension', 'ExtensionsExtension', 'DeviceCapabilitiesExtension', 'MobilityExtension', # Extensions 'FileTransfer', 'ScreenSharingServer', 'ScreenSharingClient'] from sipsimple.payloads import XMLStringElement, XMLLocalizedStringElement, XMLBooleanElement, XMLElement, XMLEmptyElement from sipsimple.payloads import XMLElementChild, XMLListElement, XMLStringListElement, XMLAttribute, XMLEmptyElementRegistryType from sipsimple.payloads.pidf import PIDFDocument, ServiceExtension, Service, DeviceExtension, Device namespace = "urn:ietf:params:xml:ns:pidf:caps" PIDFDocument.register_namespace(namespace, prefix='caps', schema='caps.xsd') # Marker mixins class EventPackagesExtension(object): pass class PriorityExtension(object): pass class MethodsExtension(object): pass class ExtensionsExtension(object): pass class MobilityExtension(object): pass class DeviceCapabilitiesExtension(object): pass class ServiceCapabilitiesExtension(object): pass class ContentTypeValue(str): def __new__(cls, value): if len(value.split('/')) != 2: raise ValueError("illegal value for Content-Type: %s" % value) return str.__new__(cls, value) class Audio(XMLBooleanElement): _xml_tag = 'audio' _xml_namespace = namespace _xml_document = PIDFDocument class Application(XMLBooleanElement): _xml_tag = 'application' _xml_namespace = namespace _xml_document = PIDFDocument class Data(XMLBooleanElement): _xml_tag = 'data' _xml_namespace = namespace _xml_document = PIDFDocument class Control(XMLBooleanElement): _xml_tag = 'control' _xml_namespace = namespace _xml_document = PIDFDocument class Video(XMLBooleanElement): _xml_tag = 'video' _xml_namespace = namespace _xml_document = PIDFDocument class Text(XMLBooleanElement): _xml_tag = 'text' _xml_namespace = namespace _xml_document = PIDFDocument class Message(XMLBooleanElement): _xml_tag = 'message' _xml_namespace = namespace _xml_document = PIDFDocument class Automata(XMLBooleanElement): _xml_tag = 'automata' _xml_namespace = namespace _xml_document = PIDFDocument class Type(XMLStringElement): _xml_tag = 'type' _xml_namespace = namespace _xml_document = PIDFDocument _xml_value_type = ContentTypeValue -class ClassRegistry(object): - __metaclass__ = XMLEmptyElementRegistryType - +class ClassRegistry(object, metaclass=XMLEmptyElementRegistryType): _xml_namespace = namespace _xml_document = PIDFDocument names = ('business', 'personal') class ClassSupported(XMLStringListElement): _xml_tag = 'supported' _xml_namespace = namespace _xml_document = PIDFDocument _xml_item_registry = ClassRegistry def __init__(self, supported=[]): XMLStringListElement.__init__(self) self.update(supported) class ClassNotSupported(XMLStringListElement): _xml_tag = 'notsupported' _xml_namespace = namespace _xml_document = PIDFDocument _xml_item_registry = ClassRegistry def __init__(self, not_supported=[]): XMLStringListElement.__init__(self) self.update(not_supported) class Class(XMLElement): _xml_tag = 'class' _xml_namespace = namespace _xml_document = PIDFDocument supported = XMLElementChild('supported', type=ClassSupported, required=False, test_equal=True) not_supported = XMLElementChild('not_supported', type=ClassNotSupported, required=False, test_equal=True) def __init__(self, supported=None, not_supported=None): XMLElement.__init__(self) self.supported = supported self.not_supported = not_supported -class DuplexRegistry(object): - __metaclass__ = XMLEmptyElementRegistryType - +class DuplexRegistry(object, metaclass=XMLEmptyElementRegistryType): _xml_namespace = namespace _xml_document = PIDFDocument names = ('full', 'half', 'receive-only', 'send-only') class DuplexSupported(XMLStringListElement): _xml_tag = 'supported' _xml_namespace = namespace _xml_document = PIDFDocument _xml_item_registry = DuplexRegistry def __init__(self, supported=[]): XMLStringListElement.__init__(self) self.update(supported) class DuplexNotSupported(XMLStringListElement): _xml_tag = 'notsupported' _xml_namespace = namespace _xml_document = PIDFDocument _xml_item_registry = DuplexRegistry def __init__(self, not_supported=[]): XMLStringListElement.__init__(self) self.update(not_supported) class Duplex(XMLElement): _xml_tag = 'duplex' _xml_namespace = namespace _xml_document = PIDFDocument supported = XMLElementChild('supported', type=DuplexSupported, required=False, test_equal=True) not_supported = XMLElementChild('not_supported', type=DuplexNotSupported, required=False, test_equal=True) def __init__(self, supported=None, not_supported=None): XMLElement.__init__(self) self.supported = supported self.not_supported = not_supported -class EventRegistry(object): - __metaclass__ = XMLEmptyElementRegistryType - +class EventRegistry(object, metaclass=XMLEmptyElementRegistryType): _xml_namespace = namespace _xml_document = PIDFDocument names = ('conference', 'dialog', 'kpml', 'message-summary', 'poc-settings', 'presence', 'reg', 'refer', 'Siemens-RTP-Stats', 'spirits-INDPs', 'spirits-user-prof', 'winfo') class EventSupported(XMLStringListElement): _xml_tag = 'supported' _xml_namespace = namespace _xml_document = PIDFDocument _xml_item_registry = EventRegistry def __init__(self, supported=[]): XMLStringListElement.__init__(self) self.update(supported) class EventNotSupported(XMLStringListElement): _xml_tag = 'notsupported' _xml_namespace = namespace _xml_document = PIDFDocument _xml_item_registry = EventRegistry def __init__(self, not_supported=[]): XMLStringListElement.__init__(self) self.update(not_supported) class EventPackages(XMLElement): _xml_tag = 'event-packages' _xml_namespace = namespace _xml_document = PIDFDocument _xml_extension_type = EventPackagesExtension supported = XMLElementChild('supported', type=EventSupported, required=False, test_equal=True) not_supported = XMLElementChild('not_supported', type=EventNotSupported, required=False, test_equal=True) def __init__(self, supported=None, not_supported=None): XMLElement.__init__(self) self.supported = supported self.not_supported = not_supported class PriorityLowerthan(XMLEmptyElement): _xml_tag = 'lowerthan' _xml_namespace = namespace _xml_document = PIDFDocument maxvalue = XMLAttribute('maxvalue', type=int, required=True, test_equal=True) def __init__(self, maxvalue): XMLEmptyElement.__init__(self) self.maxvalue = maxvalue class PriorityHigherthan(XMLEmptyElement): _xml_tag = 'higherthan' _xml_namespace = namespace _xml_document = PIDFDocument minvalue = XMLAttribute('minvalue', type=int, required=True, test_equal=True) def __init__(self, minvalue): XMLEmptyElement.__init__(self) self.minvalue = minvalue class PriorityEquals(XMLEmptyElement): _xml_tag = 'equals' _xml_namespace = namespace _xml_document = PIDFDocument value = XMLAttribute('value', type=int, required=True, test_equal=True) def __init__(self, value): XMLEmptyElement.__init__(self) self.value = value class PriorityRange(XMLEmptyElement): _xml_tag = 'range' _xml_namespace = namespace _xml_document = PIDFDocument maxvalue = XMLAttribute('maxvalue', type=int, required=True, test_equal=True) minvalue = XMLAttribute('minvalue', type=int, required=True, test_equal=True) def __init__(self, maxvalue, minvalue): XMLEmptyElement.__init__(self) self.maxvalue = maxvalue self.minvalue = minvalue class PrioritySupported(XMLListElement): _xml_tag = 'supported' _xml_namespace = namespace _xml_document = PIDFDocument _xml_item_type = (PriorityLowerthan, PriorityHigherthan, PriorityEquals, PriorityRange) def __init__(self, supported=[]): XMLListElement.__init__(self) self.update(supported) class PriorityNotSupported(XMLListElement): _xml_tag = 'notsupported' _xml_namespace = namespace _xml_document = PIDFDocument _xml_item_type = (PriorityLowerthan, PriorityHigherthan, PriorityEquals, PriorityRange) def __init__(self, not_supported=[]): XMLListElement.__init__(self) self.update(not_supported) class Priority(XMLElement): _xml_tag = 'priority' _xml_namespace = namespace _xml_document = PIDFDocument _xml_extension_type = PriorityExtension supported = XMLElementChild('supported', type=PrioritySupported, required=False, test_equal=True) not_supported = XMLElementChild('not_supported', type=PriorityNotSupported, required=False, test_equal=True) def __init__(self, supported=None, not_supported=None): XMLElement.__init__(self) self.supported = supported self.not_supported = not_supported -class MethodRegistry(object): - __metaclass__ = XMLEmptyElementRegistryType - +class MethodRegistry(object, metaclass=XMLEmptyElementRegistryType): _xml_namespace = namespace _xml_document = PIDFDocument names = ('ACK', 'BYE', 'CANCEL', 'INFO', 'INVITE', 'MESSAGE', 'NOTIFY', 'OPTIONS', 'PRACK', 'PUBLISH', 'REFER', 'REGISTER', 'SUBSCRIBE', 'UPDATE') class MethodSupported(XMLStringListElement): _xml_tag = 'supported' _xml_namespace = namespace _xml_document = PIDFDocument _xml_item_registry = MethodRegistry def __init__(self, supported=[]): XMLStringListElement.__init__(self) self.update(supported) class MethodNotSupported(XMLStringListElement): _xml_tag = 'notsupported' _xml_namespace = namespace _xml_document = PIDFDocument _xml_item_registry = MethodRegistry def __init__(self, not_supported=[]): XMLStringListElement.__init__(self) self.update(not_supported) class Methods(XMLElement): _xml_tag = 'methods' _xml_namespace = namespace _xml_document = PIDFDocument _xml_extension_type = MethodsExtension supported = XMLElementChild('supported', type=MethodSupported, required=False, test_equal=True) not_supported = XMLElementChild('not_supported', type=MethodNotSupported, required=False, test_equal=True) def __init__(self, supported=None, not_supported=None): XMLElement.__init__(self) self.supported = supported self.not_supported = not_supported -class ExtensionRegistry(object): - __metaclass__ = XMLEmptyElementRegistryType - +class ExtensionRegistry(object, metaclass=XMLEmptyElementRegistryType): _xml_namespace = namespace _xml_document = PIDFDocument names = ('rel100', 'early-session', 'eventlist', 'from-change', 'gruu', 'histinfo', 'join', 'norefsub', 'path', 'precondition', 'pref', 'privacy', 'recipient-list-invite', 'recipient-list-subscribe', 'replaces', 'resource-priority', 'sdp-anat', 'sec-agree', 'tdialog', 'timer') class ExtensionSupported(XMLStringListElement): _xml_tag = 'supported' _xml_namespace = namespace _xml_document = PIDFDocument _xml_item_registry = ExtensionRegistry def __init__(self, supported=[]): XMLStringListElement.__init__(self) self.update(supported) class ExtensionNotSupported(XMLStringListElement): _xml_tag = 'notsupported' _xml_namespace = namespace _xml_document = PIDFDocument _xml_item_registry = ExtensionRegistry def __init__(self, not_supported=[]): XMLStringListElement.__init__(self) self.update(not_supported) class Extensions(XMLElement): _xml_tag = 'extensions' _xml_namespace = namespace _xml_document = PIDFDocument _xml_extension_type = ExtensionsExtension supported = XMLElementChild('supported', type=ExtensionSupported, required=False, test_equal=True) not_supported = XMLElementChild('not_supported', type=ExtensionNotSupported, required=False, test_equal=True) def __init__(self, supported=None, not_supported=None): XMLElement.__init__(self) self.supported = supported self.not_supported = not_supported class Scheme(XMLStringElement): _xml_tag = 's' _xml_namespace = namespace _xml_document = PIDFDocument class SchemeSupported(XMLListElement): _xml_tag = 'supported' _xml_namespace = namespace _xml_document = PIDFDocument _xml_item_type = Scheme def __init__(self, supported=[]): XMLListElement.__init__(self) self.update(supported) class SchemeNotSupported(XMLListElement): _xml_tag = 'notsupported' _xml_namespace = namespace _xml_document = PIDFDocument _xml_item_type = Scheme def __init__(self, not_supported=[]): XMLStringListElement.__init__(self) self.update(not_supported) class Schemes(XMLElement): _xml_tag = 'schemes' _xml_namespace = namespace _xml_document = PIDFDocument supported = XMLElementChild('supported', type=SchemeSupported, required=False, test_equal=True) not_supported = XMLElementChild('not_supported', type=SchemeNotSupported, required=False, test_equal=True) def __init__(self, supported=None, not_supported=None): XMLElement.__init__(self) self.supported = supported self.not_supported = not_supported -class ActorRegistry(object): - __metaclass__ = XMLEmptyElementRegistryType - +class ActorRegistry(object, metaclass=XMLEmptyElementRegistryType): _xml_namespace = namespace _xml_document = PIDFDocument names = ('principal', 'attendant', 'msg-taker', 'information') class ActorSupported(XMLStringListElement): _xml_tag = 'supported' _xml_namespace = namespace _xml_document = PIDFDocument _xml_item_registry = ActorRegistry def __init__(self, supported=[]): XMLStringListElement.__init__(self) self.update(supported) class ActorNotSupported(XMLStringListElement): _xml_tag = 'notsupported' _xml_namespace = namespace _xml_document = PIDFDocument _xml_item_registry = ActorRegistry def __init__(self, not_supported=[]): XMLStringListElement.__init__(self) self.update(not_supported) class Actor(XMLElement): _xml_tag = 'actor' _xml_namespace = namespace _xml_document = PIDFDocument supported = XMLElementChild('supported', type=ActorSupported, required=False, test_equal=True) not_supported = XMLElementChild('not_supported', type=ActorNotSupported, required=False, test_equal=True) def __init__(self, supported=None, not_supported=None): XMLElement.__init__(self) self.supported = supported self.not_supported = not_supported class Language(XMLStringElement): _xml_tag = 'l' _xml_namespace = namespace _xml_document = PIDFDocument class LanguageSupported(XMLListElement): _xml_tag = 'supported' _xml_namespace = namespace _xml_document = PIDFDocument _xml_item_type = Language def __init__(self, supported=[]): XMLListElement.__init__(self) self.update(supported) def __iter__(self): - return (unicode(item) for item in super(LanguageSupported, self).__iter__()) + return (str(item) for item in super(LanguageSupported, self).__iter__()) def add(self, item): - if isinstance(item, basestring): + if isinstance(item, str): item = Language(item) super(LanguageSupported, self).add(item) def remove(self, item): - if isinstance(item, basestring): + if isinstance(item, str): try: - item = (entry for entry in super(LanguageSupported, self).__iter__() if entry == item).next() + item = next((entry for entry in super(LanguageSupported, self).__iter__() if entry == item)) except StopIteration: raise KeyError(item) super(LanguageSupported, self).remove(item) class LanguageNotSupported(XMLListElement): _xml_tag = 'notsupported' _xml_namespace = namespace _xml_document = PIDFDocument _xml_item_type = Language def __init__(self, not_supported=[]): XMLListElement.__init__(self) self.update(not_supported) def __iter__(self): - return (unicode(item) for item in super(LanguageNotSupported, self).__iter__()) + return (str(item) for item in super(LanguageNotSupported, self).__iter__()) def add(self, item): - if isinstance(item, basestring): + if isinstance(item, str): item = Language(item) super(LanguageNotSupported, self).add(item) def remove(self, item): - if isinstance(item, basestring): + if isinstance(item, str): try: - item = (entry for entry in super(LanguageNotSupported, self).__iter__() if entry == item).next() + item = next((entry for entry in super(LanguageNotSupported, self).__iter__() if entry == item)) except StopIteration: raise KeyError(item) super(LanguageNotSupported, self).remove(item) class Languages(XMLElement): _xml_tag = 'languages' _xml_namespace = namespace _xml_document = PIDFDocument supported = XMLElementChild('supported', type=LanguageSupported, required=False, test_equal=True) not_supported = XMLElementChild('not_supported', type=LanguageNotSupported, required=False, test_equal=True) def __init__(self, supported=None, not_supported=None): XMLElement.__init__(self) self.supported = supported self.not_supported = not_supported class Description(XMLLocalizedStringElement): _xml_tag = 'description' _xml_namespace = namespace _xml_document = PIDFDocument class IsFocus(XMLBooleanElement): _xml_tag = 'isfocus' _xml_namespace = namespace _xml_document = PIDFDocument class ServiceCapabilities(XMLListElement, ServiceExtension): _xml_tag = 'servcaps' _xml_namespace = namespace _xml_document = PIDFDocument _xml_extension_type = ServiceCapabilitiesExtension _xml_item_type = Description _xml_children_order = {Actor.qname: 0, Application.qname: 1, Audio.qname: 2, Automata.qname: 3, Class.qname: 4, Control.qname: 5, Data.qname: 6, Description.qname: 7, Duplex.qname: 8, EventPackages.qname: 9, Extensions.qname: 10, IsFocus.qname: 11, Message.qname: 12, Methods.qname: 13, Languages.qname: 14, Priority.qname: 15, Schemes.qname: 16, Text.qname: 17, Type.qname: 18, Video.qname: 19, None: 20} audio = XMLElementChild('audio', type=Audio, required=False, test_equal=True) application = XMLElementChild('application', type=Application, required=False, test_equal=True) data = XMLElementChild('data', type=Data, required=False, test_equal=True) control = XMLElementChild('control', type=Control, required=False, test_equal=True) video = XMLElementChild('video', type=Video, required=False, test_equal=True) text = XMLElementChild('text', type=Text, required=False, test_equal=True) message = XMLElementChild('message', type=Message, required=False, test_equal=True) mime_type = XMLElementChild('mime_type', type=Type, required=False, test_equal=True) automata = XMLElementChild('automata', type=Automata, required=False, test_equal=True) communication_class = XMLElementChild('communication_class', type=Class, required=False, test_equal=True) duplex = XMLElementChild('duplex', type=Duplex, required=False, test_equal=True) event_packages = XMLElementChild('event_packages', type=EventPackages, required=False, test_equal=True) priority = XMLElementChild('priority', type=Priority, required=False, test_equal=True) methods = XMLElementChild('methods', type=Methods, required=False, test_equal=True) extensions = XMLElementChild('extensions', type=Extensions, required=False, test_equal=True) schemes = XMLElementChild('schemes', type=Schemes, required=False, test_equal=True) actor = XMLElementChild('actor', type=Actor, required=False, test_equal=True) is_focus = XMLElementChild('is_focus', type=IsFocus, required=False, test_equal=True) languages = XMLElementChild('languages', type=Languages, required=False, test_equal=True) def __init__(self, audio=None, application=None, data=None, control=None, video=None, text=None, message=None, mime_type=None, automata=None, communication_class=None, duplex=None, event_packages=None, priority=None, methods=None, extensions=None, schemes=None, actor=None, is_focus=None, languages=None, descriptions=[]): XMLListElement.__init__(self) self.audio = audio self.application = application self.data = data self.control = control self.video = video self.text = text self.message = message self.mime_type = mime_type self.automata = automata self.communication_class = communication_class self.duplex = duplex self.event_packages = event_packages self.priority = priority self.methods = methods self.extensions = extensions self.schemes = schemes self.actor = actor self.is_focus = is_focus self.languages = languages self.update(descriptions) Service.register_extension('capabilities', type=ServiceCapabilities) -class MobilityRegistry(object): - __metaclass__ = XMLEmptyElementRegistryType - +class MobilityRegistry(object, metaclass=XMLEmptyElementRegistryType): _xml_namespace = namespace _xml_document = PIDFDocument names = ('fixed', 'mobile') class MobilitySupported(XMLStringListElement): _xml_tag = 'supported' _xml_namespace = namespace _xml_document = PIDFDocument _xml_item_registry = MobilityRegistry def __init__(self, supported=[]): XMLStringListElement.__init__(self) self.update(supported) class MobilityNotSupported(XMLStringListElement): _xml_tag = 'notsupported' _xml_namespace = namespace _xml_document = PIDFDocument _xml_item_registry = MobilityRegistry def __init__(self, not_supported=[]): XMLStringListElement.__init__(self) self.update(not_supported) class Mobility(XMLElement): _xml_tag = 'mobility' _xml_namespace = namespace _xml_document = PIDFDocument _xml_extension_type = MobilityExtension supported = XMLElementChild('supported', type=MobilitySupported, required=False, test_equal=True) not_supported = XMLElementChild('not_supported', type=MobilityNotSupported, required=False, test_equal=True) def __init__(self, supported=None, not_supported=None): XMLElement.__init__(self) self.supported = supported() self.not_supported = not_supported class DeviceCapabilities(XMLListElement, DeviceExtension): _xml_tag = 'devcaps' _xml_namespace = namespace _xml_document = PIDFDocument _xml_extension_type = DeviceCapabilitiesExtension _xml_item_type = Description mobility = XMLElementChild('mobility', type=Mobility, required=False, test_equal=True) def __init__(self, mobility=None, descriptions=[]): XMLListElement.__init__(self) self.mobility = mobility self.update(descriptions) def __repr__(self): return "%s(%r, %r)" % (self.__class__.__name__, self.mobility, list(self)) Device.register_extension('capabilities', type=DeviceCapabilities) # # Extensions # agp_caps_namespace = 'urn:ag-projects:xml:ns:pidf:caps' PIDFDocument.register_namespace(agp_caps_namespace, prefix='agp-caps') class FileTransfer(XMLBooleanElement, ServiceCapabilitiesExtension): _xml_tag = 'file-transfer' _xml_namespace = agp_caps_namespace _xml_document = PIDFDocument class ScreenSharingServer(XMLBooleanElement, ServiceCapabilitiesExtension): _xml_tag = 'screen-sharing-server' _xml_namespace = agp_caps_namespace _xml_document = PIDFDocument class ScreenSharingClient(XMLBooleanElement, ServiceCapabilitiesExtension): _xml_tag = 'screen-sharing-client' _xml_namespace = agp_caps_namespace _xml_document = PIDFDocument ServiceCapabilities.register_extension('file_transfer', type=FileTransfer) ServiceCapabilities.register_extension('screen_sharing_server', type=ScreenSharingServer) ServiceCapabilities.register_extension('screen_sharing_client', type=ScreenSharingClient) diff --git a/sipsimple/payloads/commonpolicy.py b/sipsimple/payloads/commonpolicy.py index 03e5e094..1dfc8a58 100644 --- a/sipsimple/payloads/commonpolicy.py +++ b/sipsimple/payloads/commonpolicy.py @@ -1,365 +1,365 @@ """ Generic data types to be used in policy applications, according to RFC4745. """ __all__ = ['namespace', 'CommonPolicyDocument', 'ConditionElement', 'ActionElement', 'TransformationElement', 'RuleExtension', 'IdentityOne', 'IdentityExcept', 'IdentityMany', 'Identity', 'Validity', 'Conditions', 'Actions', 'Transformations', 'Rule', 'RuleSet', # Extensions 'FalseCondition', 'RuleDisplayName'] from sipsimple.payloads import ValidationError, XMLDocument, XMLElement, XMLListElement, XMLListRootElement, XMLAttribute, XMLElementID, XMLElementChild, XMLLocalizedStringElement, XMLDateTimeElement from sipsimple.payloads import IterateIDs, IterateItems, All from sipsimple.payloads.datatypes import AnyURI, ID namespace = 'urn:ietf:params:xml:ns:common-policy' class CommonPolicyDocument(XMLDocument): content_type = 'application/auth-policy+xml' CommonPolicyDocument.register_namespace(namespace, prefix='cp', schema='common-policy.xsd') ## Mixin types for extensibility class ConditionElement(object): pass class ActionElement(object): pass class TransformationElement(object): pass class RuleExtension(object): pass ## Elements class IdentityOne(XMLElement): _xml_tag = 'one' _xml_namespace = namespace _xml_document = CommonPolicyDocument id = XMLElementID('id', type=AnyURI, required=True, test_equal=True) def __init__(self, id): XMLElement.__init__(self) self.id = id def __repr__(self): return '%s(%r)' % (self.__class__.__name__, self.id) def __str__(self): return self.id def matches(self, uri): return self.id == uri class IdentityExcept(XMLElement): _xml_tag = 'except' _xml_namespace = namespace _xml_document = CommonPolicyDocument def _onset_id(self, attribute, value): if value is not None: self.domain = None id = XMLAttribute('id', type=str, required=False, test_equal=True, onset=_onset_id) del _onset_id def _onset_domain(self, attribute, value): if value is not None: self.id = None domain = XMLAttribute('domain', type=str, required=False, test_equal=True, onset=_onset_domain) del _onset_domain def __init__(self, id=None, domain=None): XMLElement.__init__(self) self.id = id self.domain = domain def __repr__(self): return '%s(%r, %r)' % (self.__class__.__name__, self.id, self.domain) def __str__(self): if self.id is not None: return self.id else: return self.domain def matches(self, uri): if self.id is not None: return self.id != uri else: return [self.domain] != uri.split('@', 1)[1:] class IdentityMany(XMLListElement): _xml_tag = 'many' _xml_namespace = namespace _xml_document = CommonPolicyDocument _xml_children_order = {IdentityExcept.qname: 0} _xml_item_type = IdentityExcept domain = XMLAttribute('domain', type=str, required=False, test_equal=True) def __init__(self, domain=None, exceptions=[]): XMLListElement.__init__(self) self.domain = domain self.update(exceptions) def __repr__(self): return '%s(%r, %s)' % (self.__class__.__name__, self.domain, list(self)) def matches(self, uri): if self.domain is not None: if self.domain != uri.partition('@')[2]: return False for child in self: if not child.matches(uri): return False return True class Identity(XMLListElement): _xml_tag = 'identity' _xml_namespace = namespace _xml_document = CommonPolicyDocument _xml_item_type = (IdentityOne, IdentityMany) def __init__(self, identities=[]): XMLListElement.__init__(self) self.update(identities) def matches(self, uri): for child in self: if child.matches(uri): return True return False class Sphere(XMLElement): _xml_tag = 'sphere' _xml_namespace = namespace _xml_document = CommonPolicyDocument value = XMLAttribute('value', type=str, required=True, test_equal=True) def __init__(self, value): XMLElement.__init__(self) self.value = value def __repr__(self): return '%s(%r)' % (self.__class__.__name__, self.value) class ValidFrom(XMLDateTimeElement): _xml_tag = 'from' _xml_namespace = namespace _xml_document = CommonPolicyDocument class ValidUntil(XMLDateTimeElement): _xml_tag = 'until' _xml_namespace = namespace _xml_document = CommonPolicyDocument class ValidityInterval(object): def __init__(self, from_timestamp, until_timestamp): self.valid_from = ValidFrom(from_timestamp) self.valid_until = ValidUntil(until_timestamp) def __eq__(self, other): if isinstance(other, ValidityInterval): return self is other or (self.valid_from == other.valid_from and self.valid_until == other.valid_until) return NotImplemented def __ne__(self, other): if isinstance(other, ValidityInterval): return self is not other and (self.valid_from != other.valid_from or self.valid_until != other.valid_until) return NotImplemented @classmethod def from_elements(cls, from_element, until_element, xml_document=None): instance = object.__new__(cls) instance.valid_from = ValidFrom.from_element(from_element, xml_document) instance.valid_until = ValidUntil.from_element(until_element, xml_document) return instance class Validity(XMLListElement): _xml_tag = 'validity' _xml_namespace = namespace _xml_document = CommonPolicyDocument _xml_item_type = ValidityInterval def __init__(self, children=[]): XMLListElement.__init__(self) self.update(children) def _parse_element(self, element): iterator = iter(element) for first_child in iterator: - second_child = iterator.next() + second_child = next(iterator) if first_child.tag == '{%s}from' % self._xml_namespace and second_child.tag == '{%s}until' % self._xml_namespace: try: item = ValidityInterval.from_elements(first_child, second_child, xml_document=self._xml_document) except: pass else: self._element_map[item.valid_from.element] = item def _build_element(self): for child in self: child.valid_from.to_element() child.valid_until.to_element() def add(self, item): if not isinstance(item, ValidityInterval): raise TypeError("Validity element must be a ValidityInterval instance") self._insert_element(item.valid_from.element) self._insert_element(item.valid_until.element) self._element_map[item.valid_from.element] = item self.__dirty__ = True def remove(self, item): self.element.remove(item.valid_from.element) self.element.remove(item.valid_until.element) del self._element_map[item.valid_from.element] self.__dirty__ = True def check_validity(self): if not self: raise ValidationError("cannot have Validity element without any children") super(Validity, self).check_validity(self) class Conditions(XMLListElement): _xml_tag = 'conditions' _xml_namespace = namespace _xml_document = CommonPolicyDocument _xml_children_order = {Identity.qname: 0, Sphere.qname: 1, Validity.qname: 2} _xml_item_type = (Identity, Sphere, Validity, ConditionElement) def __init__(self, conditions=[]): XMLListElement.__init__(self) self.update(conditions) class Actions(XMLListElement): _xml_tag = 'actions' _xml_namespace = namespace _xml_document = CommonPolicyDocument _xml_item_type = ActionElement def __init__(self, actions=[]): XMLListElement.__init__(self) self.update(actions) class Transformations(XMLListElement): _xml_tag = 'transformations' _xml_namespace = namespace _xml_document = CommonPolicyDocument _xml_item_type = TransformationElement def __init__(self, transformations=[]): XMLListElement.__init__(self) self.update(transformations) class Rule(XMLElement): _xml_tag = 'rule' _xml_namespace = namespace _xml_extension_type = RuleExtension _xml_document = CommonPolicyDocument _xml_children_order = {Conditions.qname: 0, Actions.qname: 1, Transformations.qname: 2} id = XMLElementID('id', type=ID, required=True, test_equal=True) conditions = XMLElementChild('conditions', type=Conditions, required=False, test_equal=True) actions = XMLElementChild('actions', type=Actions, required=False, test_equal=True) transformations = XMLElementChild('transformations', type=Transformations, required=False, test_equal=True) def __init__(self, id, conditions=None, actions=None, transformations=None): XMLElement.__init__(self) self.id = id self.conditions = conditions self.actions = actions self.transformations = transformations def __repr__(self): return '%s(%r, %r, %r, %r)' % (self.__class__.__name__, self.id, self.conditions, self.actions, self.transformations) class RuleSet(XMLListRootElement): _xml_tag = 'ruleset' _xml_namespace = namespace _xml_document = CommonPolicyDocument _xml_item_type = Rule def __init__(self, rules=[]): XMLListRootElement.__init__(self) self.update(rules) def __getitem__(self, key): if key is IterateIDs: - return self._xmlid_map[Rule].iterkeys() + return iter(list(self._xmlid_map[Rule].keys())) elif key is IterateItems: - return self._xmlid_map[Rule].itervalues() + return iter(list(self._xmlid_map[Rule].values())) else: return self._xmlid_map[Rule][key] def __delitem__(self, key): if key is All: - for item in self._xmlid_map[Rule].values(): + for item in list(self._xmlid_map[Rule].values()): self.remove(item) else: self.remove(self._xmlid_map[Rule][key]) def get(self, key, default=None): return self._xmlid_map[Rule].get(key, default) # # Extensions # agp_cp_namespace = 'urn:ag-projects:xml:ns:common-policy' CommonPolicyDocument.register_namespace(agp_cp_namespace, prefix='agp-cp') # A condition element in the AG Projects namespace, it will always be evaluated to false # because it's not understood by servers class FalseCondition(XMLElement, ConditionElement): _xml_tag = 'false-condition' _xml_namespace = agp_cp_namespace _xml_document = CommonPolicyDocument class RuleDisplayName(XMLLocalizedStringElement, RuleExtension): _xml_tag = 'display-name' _xml_namespace = agp_cp_namespace _xml_document = CommonPolicyDocument Rule.register_extension('display_name', RuleDisplayName) diff --git a/sipsimple/payloads/conference.py b/sipsimple/payloads/conference.py index b2daea19..3ae95d78 100644 --- a/sipsimple/payloads/conference.py +++ b/sipsimple/payloads/conference.py @@ -1,829 +1,829 @@ # This module is currently broken. It breaks the core assumption of the # payloads infrastructure, that there is only one element with a given qname # for every given application. # Currently this module defines at least 2 different elements with the same # qname (tag=entry) which are used inside list elements. As a result, when # the list element tries to lookup the class to use for a given qname, only # one list element will get the class right, the other will get a wrong class # because the list element uses the application qname map to determine the # class and that mapping can only contain 1 mapping from a given qname to a # class. Since the class it obtains is not the right one, it will be ignored # as it doesn't match the known item types for that list element and the # corresponding xml data will also be ignored. # # To make matters even worse, this module subclasses XMLElement classes # without changing their qname, which in turn generates even more overlapping # classes for a given qname. At least according to the xml schema, all these # subclasses (which seem to be defined in order to impose some restrictions # in different cases), seem to be unnecessary. The schema only defines one # type with a string element that has no resctrictions and is to be used # in all the places. The code however tries to use a variation of the type # with restrictions in different places and fails as the correct class cannot # be identified anymore (see for example all the UrisType subclasses or the # multiple classes to define purpose elements). # # -Dan # """Parses and produces conference-info messages according to RFC4575.""" __all__ = ['namespace', 'ConferenceDocument', 'ConferenceDescription', 'ConfUris', 'ConfUrisEntry', 'ServiceUris', 'ServiceUrisEntry', 'UrisTypeModified', 'UrisTypeEntry', 'AvailableMedia', 'AvailableMediaEntry', 'Users', 'User', 'UserExtension', 'AssociatedAors', 'Roles', 'Role', 'Endpoint', 'CallInfo', 'Sip', 'Referred', 'JoiningInfo', 'DisconnectionInfo', 'HostInfo', 'HostInfoUris', 'ConferenceState', 'SidebarsByRef', 'SidebarsByVal', 'Conference', 'ConferenceDescriptionExtension', # Extensions 'FileResource', 'FileResources', 'Resources'] from sipsimple.payloads import ValidationError, XMLDocument, XMLRootElement, XMLStringElement, XMLBooleanElement, XMLDateTimeElement, XMLUnsignedIntElement, XMLAnyURIElement from sipsimple.payloads import XMLElementChild, XMLElement, XMLListElement, XMLAttribute namespace = 'urn:ietf:params:xml:ns:conference-info' class ConferenceDocument(XMLDocument): content_type = "application/conference-info+xml" ConferenceDocument.register_namespace(namespace, prefix=None, schema='conference.xsd') # Marker mixins class UserExtension(object): pass class ConferenceDescriptionExtension(object): pass class State(str): def __new__(cls, value): if value not in ('full', 'partial', 'deleted'): raise ValueError("illegal value for state") return str.__new__(cls, value) class Version(str): def __new__(cls, value): return str.__new__(cls, int(value)) class When(XMLDateTimeElement): _xml_tag = 'when' _xml_namespace = namespace _xml_document = ConferenceDocument class Reason(XMLStringElement): _xml_tag = 'reason' _xml_namespace = namespace _xml_document = ConferenceDocument class By(XMLStringElement): _xml_tag = 'by' _xml_namespace = namespace _xml_document = ConferenceDocument class ExecutionType(XMLElement): _xml_tag = None # To be set by the subclass _xml_namespace = namespace _xml_document = ConferenceDocument when = XMLElementChild('when', type=When, required=False, test_equal=True) reason = XMLElementChild('reason', type=Reason, required=False, test_equal=True) by = XMLElementChild('by', type=By, required=False, test_equal=True) def __init__(self, when=None, reason=None, by=None): XMLElement.__init__(self) self.when = when self.reason = reason self.by = by class URI(XMLAnyURIElement): _xml_tag = 'uri' _xml_namespace = namespace _xml_document = ConferenceDocument class DisplayText(XMLStringElement): _xml_tag = 'display-text' _xml_namespace = namespace _xml_document = ConferenceDocument class UrisTypePurpose(XMLStringElement): _xml_tag = 'purpose' _xml_namespace = namespace _xml_document = ConferenceDocument class UrisTypeModified(ExecutionType): _xml_tag = 'modified' class UrisTypeEntry(XMLElement): _xml_tag = 'entry' _xml_namespace = namespace _xml_document = ConferenceDocument state = XMLAttribute('state', type=State, required=False, test_equal=False) uri = XMLElementChild('uri', type=URI, required=True, test_equal=True) display_text = XMLElementChild('display_text', type=DisplayText, required=False, test_equal=True) purpose = XMLElementChild('purpose', type=UrisTypePurpose, required=False, test_equal=True) modified = XMLElementChild('modified', type=UrisTypeModified, required=False, test_equal=True) def __init__(self, uri, state=None, display_text=None, purpose=None, modified=None): XMLElement.__init__(self) self.uri = uri self.state = state self.display_text = display_text self.purpose = purpose self.modified = modified class Subject(XMLStringElement): _xml_tag = 'subject' _xml_namespace = namespace _xml_document = ConferenceDocument class FreeText(XMLStringElement): _xml_tag = 'free-text' _xml_namespace = namespace _xml_document = ConferenceDocument class Keywords(XMLStringElement): _xml_tag = 'keywords' _xml_namespace = namespace _xml_document = ConferenceDocument class ConfUrisPurposeValue(str): def __new__(cls, value): if value not in ('participation', 'streaming'): raise ValueError("illegal value for purpose element") return str.__new__(cls, value) class ConfUrisPurpose(UrisTypePurpose): _xml_value_type = ConfUrisPurposeValue class ConfUrisEntry(UrisTypeEntry): purpose = XMLElementChild('purpose', type=ConfUrisPurpose, required=False, test_equal=True) class ConfUris(XMLListElement): _xml_tag = 'conf-uris' _xml_namespace = namespace _xml_document = ConferenceDocument _xml_item_type = ConfUrisEntry def __init__(self, entries=[]): XMLListElement.__init__(self) self.update(entries) class ServiceUrisPurposeValue(str): def __new__(cls, value): if value not in ('web-page', 'recording', 'event'): raise ValueError("illegal value for purpose element") return str.__new__(cls, value) class ServiceUrisPurpose(UrisTypePurpose): _xml_value_type = ServiceUrisPurposeValue class ServiceUrisEntry(UrisTypeEntry): purpose = XMLElementChild('purpose', type=ServiceUrisPurpose, required=False, test_equal=True) class ServiceUris(XMLListElement): _xml_tag = 'service-uris' _xml_namespace = namespace _xml_document = ConferenceDocument _xml_item_type = ServiceUrisEntry def __init__(self, entries=[]): XMLListElement.__init__(self) self.update(entries) class MaximumUserCount(XMLUnsignedIntElement): _xml_tag = 'maximum-user-count' _xml_namespace = namespace _xml_document = ConferenceDocument class MediaTypeValue(str): def __new__(cls, value): if value not in ('audio', 'video', 'text', 'message'): raise ValueError("illegal value for type element") return str.__new__(cls, value) class MediaType(XMLStringElement): _xml_tag = 'type' _xml_namespace = namespace _xml_document = ConferenceDocument _xml_value_type = MediaTypeValue class MediaTypeStatusValue(str): def __new__(cls, value): if value not in ('sendrecv', 'sendonly', 'recvonly', 'inactive'): raise ValueError("illegal value for status element") return str.__new__(cls, value) class MediaTypeStatus(XMLStringElement): _xml_tag = 'status' _xml_namespace = namespace _xml_document = ConferenceDocument _xml_value_type = MediaTypeStatusValue class AvailableMediaEntry(XMLElement): _xml_tag = 'entry' _xml_namespace = namespace _xml_document = ConferenceDocument _xml_children_order = {DisplayText.qname: 0, MediaType.qname: 1, MediaTypeStatus.qname: 2, None: 3} label = XMLAttribute('label', type=str, required=True, test_equal=False) media_type = XMLElementChild('media_type', type=MediaType, required=True, test_equal=True) display_text = XMLElementChild('display_text', type=DisplayText, required=False, test_equal=True) status = XMLElementChild('status', type=MediaTypeStatus, required=False, test_equal=True) def __init__(self, label, media_type, display_text=None, status=None): XMLElement.__init__(self) self.label = label self.media_type = media_type self.display_text = display_text self.status = status class AvailableMedia(XMLListElement): _xml_tag = 'available-media' _xml_namespace = namespace _xml_document = ConferenceDocument _xml_item_type = AvailableMediaEntry def __init__(self, entries=[]): XMLListElement.__init__(self) self.update(entries) class ConferenceDescription(XMLElement): _xml_tag = 'conference-description' _xml_namespace = namespace _xml_document = ConferenceDocument _xml_extension_type = ConferenceDescriptionExtension display_text = XMLElementChild('display_text', type=DisplayText, required=False, test_equal=True) subject = XMLElementChild('subject', type=Subject, required=False, test_equal=True) free_text = XMLElementChild('free_text', type=FreeText, required=False, test_equal=True) keywords = XMLElementChild('keywords', type=Keywords, required=False, test_equal=True) conf_uris = XMLElementChild('conf_uris', type=ConfUris, required=False, test_equal=True) service_uris = XMLElementChild('service_uris', type=ServiceUris, required=False, test_equal=True) maximum_user_count = XMLElementChild('maximum_user_count', type=MaximumUserCount, required=False, test_equal=True) available_media = XMLElementChild('available_media', type=AvailableMedia, required=False, test_equal=True) def __init__(self, display_text=None, subject=None, free_text=None, keywords=None, conf_uris=None, service_uris=None, maximum_user_count=None, available_media=None): XMLElement.__init__(self) self.display_text = display_text self.subject = subject self.free_text = free_text self.keywords = keywords self.conf_uris = conf_uris self.service_uris = service_uris self.maximum_user_count = maximum_user_count self.available_media = available_media class WebPage(XMLStringElement): _xml_tag = 'web-page' _xml_namespace = namespace _xml_document = ConferenceDocument class HostInfoUris(XMLListElement): _xml_tag = 'uris' _xml_namespace = namespace _xml_document = ConferenceDocument _xml_item_type = UrisTypeEntry def __init__(self, entries=[]): XMLListElement.__init__(self) self.update(entries) class HostInfo(XMLElement): _xml_tag = 'host-info' _xml_namespace = namespace _xml_document = ConferenceDocument display_text = XMLElementChild('display_text', type=DisplayText, required=False, test_equal=True) web_page = XMLElementChild('web_page', type=WebPage, required=False, test_equal=True) uris = XMLElementChild('uris', type=HostInfoUris, required=False, test_equal=True) def __init__(self, display_text=None, web_page=None, uris=None): XMLElement.__init__(self) self.display_text = display_text self.web_page = web_page self.uris = uris class UserCount(XMLUnsignedIntElement): _xml_tag = 'user-count' _xml_namespace = namespace _xml_document = ConferenceDocument class Active(XMLBooleanElement): _xml_tag = 'active' _xml_namespace = namespace _xml_document = ConferenceDocument class Locked(XMLBooleanElement): _xml_tag = 'locked' _xml_namespace = namespace _xml_document = ConferenceDocument class ConferenceState(XMLElement): _xml_tag = 'conference-state' _xml_namespace = namespace _xml_document = ConferenceDocument user_count = XMLElementChild('user_count', type=UserCount, required=False, test_equal=True) active = XMLElementChild('active', type=Active, required=False, test_equal=True) locked = XMLElementChild('locked', type=Locked, required=False, test_equal=True) def __init__(self, user_count=None, active=None, locked=None): XMLElement.__init__(self) self.user_count = user_count self.active = active self.locked = locked class AssociatedAors(XMLListElement): _xml_tag = 'associated-aors' _xml_namespace = namespace _xml_document = ConferenceDocument _xml_item_type = UrisTypeEntry def __init__(self, entries=[]): XMLListElement.__init__(self) self.update(entries) class Role(XMLStringElement): _xml_tag = 'entry' _xml_namespace = namespace _xml_document = ConferenceDocument class Roles(XMLListElement): _xml_tag = 'roles' _xml_namespace = namespace _xml_document = ConferenceDocument _xml_item_type = Role def __init__(self, roles=[]): XMLListElement.__init__(self) self.update(roles) class Languages(XMLStringElement): _xml_tag = 'languages' _xml_namespace = namespace _xml_document = ConferenceDocument class CascadedFocus(XMLStringElement): _xml_tag = 'cascaded-focus' _xml_namespace = namespace _xml_document = ConferenceDocument class Referred(ExecutionType): _xml_tag = 'referred' class EndpointStatusValue(str): def __new__(cls, value): if value not in ('connected', 'disconnected', 'on-hold', 'muted-via-focus', 'pending', 'alerting', 'dialing-in', 'dialing-out', 'disconnecting'): raise ValueError("illegal value for status element") return str.__new__(cls, value) class EndpointStatus(XMLStringElement): _xml_tag = 'status' _xml_namespace = namespace _xml_document = ConferenceDocument _xml_value_type = EndpointStatusValue class JoiningMethodValue(str): def __new__(cls, value): if value not in ('dialed-in', 'dialed-out', 'focus-owner'): raise ValueError("illegal value for joining method element") return str.__new__(cls, value) class JoiningMethod(XMLStringElement): _xml_tag = 'joining-method' _xml_namespace = namespace _xml_document = ConferenceDocument _xml_value_type = JoiningMethodValue class JoiningInfo(ExecutionType): _xml_tag = 'joining-info' class DisconnectionMethodValue(str): def __new__(cls, value): if value not in ('departed', 'booted', 'failed', 'busy'): raise ValueError("illegal value for disconnection method element") return str.__new__(cls, value) class DisconnectionMethod(XMLStringElement): _xml_tag = 'disconnection-method' _xml_namespace = namespace _xml_document = ConferenceDocument _xml_value_type = DisconnectionMethodValue class DisconnectionInfo(ExecutionType): _xml_tag = 'disconnection-info' class Label(XMLStringElement): _xml_tag = 'label' _xml_namespace = namespace _xml_document = ConferenceDocument class SrcId(XMLStringElement): _xml_tag = 'src-id' _xml_namespace = namespace _xml_document = ConferenceDocument class Media(XMLElement): _xml_tag = 'media' _xml_namespace = namespace _xml_document = ConferenceDocument id = XMLAttribute('id', type=str, required=True, test_equal=False) display_text = XMLElementChild('display_text', type=DisplayText, required=False, test_equal=True) media_type = XMLElementChild('media_type', type=MediaType, required=False, test_equal=True) label = XMLElementChild('label', type=Label, required=False, test_equal=True) src_id = XMLElementChild('src_id', type=SrcId, required=False, test_equal=True) status = XMLElementChild('status', type=MediaTypeStatus, required=False, test_equal=True) def __init__(self, id, display_text=None, media_type=None, label=None, src_id=None, status=None): XMLElement.__init__(self) self.id = id self.display_text = display_text self.media_type = media_type self.label = label self.src_id = src_id self.status = status class CallId(XMLStringElement): _xml_tag = 'call-id' _xml_namespace = namespace _xml_document = ConferenceDocument class FromTag(XMLStringElement): _xml_tag = 'from-tag' _xml_namespace = namespace _xml_document = ConferenceDocument class ToTag(XMLStringElement): _xml_tag = 'to-tag' _xml_namespace = namespace _xml_document = ConferenceDocument class Sip(XMLElement): _xml_tag = 'sip' _xml_namespace = namespace _xml_document = ConferenceDocument display_text = XMLElementChild('display_text', type=DisplayText, required=False, test_equal=True) call_id = XMLElementChild('call_id', type=CallId, required=False, test_equal=True) from_tag = XMLElementChild('from_tag', type=FromTag, required=False, test_equal=True) to_tag = XMLElementChild('to_tag', type=ToTag, required=False, test_equal=True) def __init__(self, display_text=None, call_id=None, from_tag=None, to_tag=None): XMLElement.__init__(self) self.display_text = display_text self.call_id = call_id self.from_tag = from_tag self.to_tag = to_tag class CallInfo(XMLElement): _xml_tag = 'call-info' _xml_namespace = namespace _xml_document = ConferenceDocument sip = XMLElementChild('sip', type=Sip, required=False, test_equal=True) def __init__(self, sip=None): XMLElement.__init__(self) self.sip = sip class Endpoint(XMLListElement): _xml_tag = 'endpoint' _xml_namespace = namespace _xml_document = ConferenceDocument _xml_item_type = Media entity = XMLAttribute('entity', type=str, required=True, test_equal=False) state = XMLAttribute('state', type=State, required=False, test_equal=False) display_text = XMLElementChild('display_text', type=DisplayText, required=False, test_equal=True) referred = XMLElementChild('referred', type=Referred, required=False, test_equal=True) status = XMLElementChild('status', type=EndpointStatus, required=False, test_equal=True) joining_method = XMLElementChild('joining_method', type=JoiningMethod, required=False, test_equal=True) joining_info = XMLElementChild('joining_info', type=JoiningInfo, required=False, test_equal=True) disconnection_method = XMLElementChild('disconnection_method', type=DisconnectionMethod, required=False, test_equal=True) disconnection_info = XMLElementChild('disconnection_info', type=DisconnectionInfo, required=False, test_equal=True) call_info = XMLElementChild('call_info', type=CallInfo, required=False, test_equal=True) def __init__(self, entity, state='full', display_text=None, referred=None, status=None, joining_method=None, joining_info=None, disconnection_method=None, disconnection_info=None, call_info=None, media=[]): XMLListElement.__init__(self) self.entity = entity self.state = state self.display_text = display_text self.referred = referred self.status = status self.joining_method = joining_method self.joining_info = joining_info self.disconnection_method = disconnection_method self.disconnection_info = disconnection_info self.call_info = call_info self.update(media) def __repr__(self): args = ('entity', 'state', 'display_text', 'referred', 'status', 'joining_method', 'joining_info', 'disconnection_method', 'disconnection_info', 'call_info') return "%s(%s, media=%r)" % (self.__class__.__name__, ', '.join("%s=%r" % (name, getattr(self, name)) for name in args), list(self)) class User(XMLListElement): _xml_tag = 'user' _xml_namespace = namespace _xml_document = ConferenceDocument _xml_item_type = Endpoint _xml_extension_type = UserExtension entity = XMLAttribute('entity', type=str, required=True, test_equal=False) state = XMLAttribute('state', type=State, required=False, test_equal=False) display_text = XMLElementChild('display_text', type=DisplayText, required=False, test_equal=True) associated_aors = XMLElementChild('associated_aors', type=AssociatedAors, required=False, test_equal=True) roles = XMLElementChild('roles', type=Roles, required=False, test_equal=True) languages = XMLElementChild('languages', type=Languages, required=False, test_equal=True) cascaded_focus = XMLElementChild('cascaded_focus', type=CascadedFocus, required=False, test_equal=True) def __init__(self, entity, state='full', display_text=None, associated_aors=None, roles=None, languages=None, cascaded_focus=None, endpoints=[]): XMLListElement.__init__(self) self.entity = entity self.state = state self.display_text = display_text self.associated_aors = associated_aors self.roles = roles self.languages = languages self.cascaded_focus = cascaded_focus self.update(endpoints) def __repr__(self): args = ('entity', 'state', 'display_text', 'associated_aors', 'roles', 'languages', 'cascaded_focus') return "%s(%s, endpoints=%r)" % (self.__class__.__name__, ', '.join("%s=%r" % (name, getattr(self, name)) for name in args), list(self)) class Users(XMLListElement): _xml_tag = 'users' _xml_namespace = namespace _xml_document = ConferenceDocument _xml_item_type = User state = XMLAttribute('state', type=State, required=False, test_equal=False) def __init__(self, state='full', users=[]): XMLListElement.__init__(self) self.state = state self.update(users) def __repr__(self): return "%s(state=%r, users=%r)" % (self.__class__.__name__, self.state, list(self)) class SidebarsByRef(XMLListElement): _xml_tag = 'sidebars-by-ref' _xml_namespace = namespace _xml_document = ConferenceDocument _xml_item_type = UrisTypeEntry def __init__(self, entries=[]): XMLListElement.__init__(self) self.update(entries) class SidebarsByVal(XMLListElement): _xml_tag = 'sidebars-by-val' _xml_namespace = namespace _xml_document = ConferenceDocument _xml_item_type = None # will be set later, after the item type is defined below state = XMLAttribute('state', type=State, required=False, test_equal=False) def __init__(self, state='full', entries=[]): XMLListElement.__init__(self) self.state = state self.update(entries) def __repr__(self): return "%s(state=%r, entries=%r)" % (self.__class__.__name__, self.state, list(self)) class SidebarsByValEntry(XMLElement): _xml_tag = 'entry' _xml_namespace = namespace _xml_document = ConferenceDocument entity = XMLAttribute('entity', type=str, required=True, test_equal=False) state = XMLAttribute('state', type=State, required=False, test_equal=False) conference_description = XMLElementChild('conference_description', type=ConferenceDescription, required=False, test_equal=True) host_info = XMLElementChild('host_info', type=HostInfo, required=False, test_equal=True) conference_state = XMLElementChild('conference_state', type=ConferenceState, required=False, test_equal=True) users = XMLElementChild('users', type=Users, required=False, test_equal=True) sidebars_by_ref = XMLElementChild('sidebars_by_ref', type=SidebarsByRef, required=False, test_equal=True) sidebars_by_val = XMLElementChild('sidebars_by_val', type=SidebarsByVal, required=False, test_equal=True) def __init__(self, entity, state='full', version=None, conference_description=None, host_info=None, conference_state=None, users=None, sidebars_by_ref=None, sidebars_by_val=None): XMLElement.__init__(self) self.entity = entity self.state = state self.version = version self.conference_description = conference_description self.host_info = host_info self.conference_state = conference_state self.users = users self.sidebars_by_ref = sidebars_by_ref self.sidebars_by_val = sidebars_by_val if self.state == "full" and (self.conference_description is None or self.users is None): raise ValidationError("A full conference document must at least include the and child elements.") SidebarsByVal._xml_item_type = SidebarsByValEntry class Conference(XMLRootElement): _xml_tag = 'conference-info' _xml_namespace = namespace _xml_document = ConferenceDocument _xml_children_order = {ConferenceDescription.qname: 0, HostInfo.qname: 1, ConferenceState.qname: 2, Users.qname: 3, SidebarsByRef.qname: 4, SidebarsByVal.qname: 5, None: 6} entity = XMLAttribute('entity', type=str, required=True, test_equal=False) state = XMLAttribute('state', type=State, required=False, test_equal=False) version = XMLAttribute('version', type=Version, required=False, test_equal=False) conference_description = XMLElementChild('conference_description', type=ConferenceDescription, required=False, test_equal=True) host_info = XMLElementChild('host_info', type=HostInfo, required=False, test_equal=True) conference_state = XMLElementChild('conference_state', type=ConferenceState, required=False, test_equal=True) users = XMLElementChild('users', type=Users, required=False, test_equal=True) sidebars_by_ref = XMLElementChild('sidebars_by_ref', type=SidebarsByRef, required=False, test_equal=True) sidebars_by_val = XMLElementChild('sidebars_by_val', type=SidebarsByVal, required=False, test_equal=True) def __init__(self, entity, state='full', version=None, conference_description=None, host_info=None, conference_state=None, users=None, sidebars_by_ref=None, sidebars_by_val=None): XMLRootElement.__init__(self) self.entity = entity self.state = state self.version = version self.conference_description = conference_description self.host_info = host_info self.conference_state = conference_state self.users = users self.sidebars_by_ref = sidebars_by_ref self.sidebars_by_val = sidebars_by_val if self.state == "full" and (self.conference_description is None or self.users is None): raise ValidationError("A full conference document must at least include the and child elements.") # # Extensions # agp_conf_namespace = 'urn:ag-projects:xml:ns:conference-info' ConferenceDocument.register_namespace(agp_conf_namespace, prefix='agp-conf') class FileResource(XMLElement): _xml_tag = 'file' _xml_namespace = agp_conf_namespace _xml_document = ConferenceDocument - name = XMLAttribute('name', type=unicode, required=True, test_equal=False) + name = XMLAttribute('name', type=str, required=True, test_equal=False) hash = XMLAttribute('hash', type=str, required=True, test_equal=False) size = XMLAttribute('size', type=int, required=True, test_equal=False) - sender = XMLAttribute('sender', type=unicode, required=True, test_equal=False) + sender = XMLAttribute('sender', type=str, required=True, test_equal=False) status = XMLAttribute('status', type=str, required=True, test_equal=False) def __init__(self, name, hash, size, sender, status): XMLElement.__init__(self) self.name = name self.hash = hash self.size = size self.sender = sender self.status = status class FileResources(XMLListElement): _xml_tag = 'files' _xml_namespace = agp_conf_namespace _xml_document = ConferenceDocument _xml_item_type = FileResource def __init__(self, files=[]): XMLListElement.__init__(self) self.update(files) class Resources(XMLElement, ConferenceDescriptionExtension): _xml_tag = 'resources' _xml_namespace = agp_conf_namespace _xml_document = ConferenceDocument files = XMLElementChild('files', type=FileResources, required=False, test_equal=True) def __init__(self, files=None): XMLElement.__init__(self) self.files = files ConferenceDescription.register_extension('resources', Resources) class ScreenImageURL(XMLStringElement, UserExtension): _xml_tag = 'screen_image_url' _xml_namespace = agp_conf_namespace _xml_document = ConferenceDocument User.register_extension('screen_image_url', ScreenImageURL) diff --git a/sipsimple/payloads/datatypes.py b/sipsimple/payloads/datatypes.py index 927774a3..6e232f2c 100644 --- a/sipsimple/payloads/datatypes.py +++ b/sipsimple/payloads/datatypes.py @@ -1,230 +1,230 @@ """Data types used for simple XML elements and for XML attributes""" __all__ = ['Boolean', 'DateTime', 'Byte', 'UnsignedByte', 'Short', 'UnsignedShort', 'Int', 'UnsignedInt', 'Long', 'UnsignedLong', 'PositiveInteger', 'NegativeInteger', 'NonNegativeInteger', 'NonPositiveInteger', 'ID', 'AnyURI', 'SIPURI', 'XCAPURI'] import re -import urllib -import urlparse +import urllib.request, urllib.parse, urllib.error +import urllib.parse from sipsimple.util import ISOTimestamp class Boolean(int): def __new__(cls, value): return int.__new__(cls, bool(value)) def __repr__(self): return 'True' if self else 'False' __str__ = __repr__ @classmethod def __xmlparse__(cls, value): if value in ('True', 'true'): return int.__new__(cls, 1) elif value in ('False', 'false'): return int.__new__(cls, 0) else: raise ValueError("Invalid boolean string representation: %s" % value) def __xmlbuild__(self): - return u'true' if self else u'false' + return 'true' if self else 'false' class DateTime(ISOTimestamp): pass class Byte(int): def __new__(cls, value): instance = int.__new__(cls, value) if not (-128 <= instance <= 127): raise ValueError("integer number must be a signed 8bit value") return instance class UnsignedByte(int): def __new__(cls, value): instance = int.__new__(cls, value) if not (0 <= instance <= 255): raise ValueError("integer number must be an unsigned 8bit value") return instance class Short(int): def __new__(cls, value): instance = int.__new__(cls, value) if not (-32768 <= instance <= 32767): raise ValueError("integer number must be a signed 16bit value") return instance class UnsignedShort(int): def __new__(cls, value): instance = int.__new__(cls, value) if not (0 <= instance <= 65535): raise ValueError("integer number must be an unsigned 16bit value") return instance class Int(long): def __new__(cls, value): - instance = long.__new__(cls, value) + instance = int.__new__(cls, value) if not (-2147483648 <= instance <= 2147483647): raise ValueError("integer number must be a signed 32bit value") return instance class UnsignedInt(long): def __new__(cls, value): - instance = long.__new__(cls, value) + instance = int.__new__(cls, value) if not (0 <= instance <= 4294967295): raise ValueError("integer number must be an unsigned 32bit value") return instance class Long(long): def __new__(cls, value): - instance = long.__new__(cls, value) + instance = int.__new__(cls, value) if not (-9223372036854775808 <= instance <= 9223372036854775807): raise ValueError("integer number must be a signed 64bit value") return instance class UnsignedLong(long): def __new__(cls, value): - instance = long.__new__(cls, value) + instance = int.__new__(cls, value) if not (0 <= instance <= 18446744073709551615): raise ValueError("integer number must be an unsigned 64bit value") return instance class PositiveInteger(long): def __new__(cls, value): - instance = long.__new__(cls, value) + instance = int.__new__(cls, value) if instance <= 0: raise ValueError("integer number must be a positive value") return instance class NegativeInteger(long): def __new__(cls, value): - instance = long.__new__(cls, value) + instance = int.__new__(cls, value) if instance >= 0: raise ValueError("integer number must be a negative value") return instance class NonNegativeInteger(long): def __new__(cls, value): - instance = long.__new__(cls, value) + instance = int.__new__(cls, value) if instance < 0: raise ValueError("integer number must be a non-negative value") return instance class NonPositiveInteger(long): def __new__(cls, value): - instance = long.__new__(cls, value) + instance = int.__new__(cls, value) if instance > 0: raise ValueError("integer number must be a non-positive value") return instance class ID(str): _id_regex = re.compile(r'^[a-z_][a-z0-9_.-]*$', re.I) def __new__(cls, value): if not cls._id_regex.match(value): raise ValueError("illegal ID value: %s" % value) return str.__new__(cls, value) -class AnyURI(unicode): +class AnyURI(str): @classmethod def __xmlparse__(cls, value): - return cls.__new__(cls, urllib.unquote(value).decode('utf-8')) + return cls.__new__(cls, urllib.parse.unquote(value).decode('utf-8')) def __xmlbuild__(self): - return urllib.quote(self.encode('utf-8')) + return urllib.parse.quote(self.encode('utf-8')) class SIPURI(AnyURI): _path_regex = re.compile(r'^((?P[^:@]+)(:(?P[^@]+))?@)?(?P.*)$') def __new__(cls, value): instance = AnyURI.__new__(cls, value) - uri = urlparse.urlparse(instance) + uri = urllib.parse.urlparse(instance) if uri.scheme not in ('sip', 'sips'): raise ValueError("illegal scheme for SIP URI: %s" % uri.scheme) instance.scheme = uri.scheme instance.__dict__.update(cls._path_regex.match(uri.path).groupdict()) instance.params = {} if uri.params: params = (param.split('=', 1) for param in uri.params.split(';')) for param in params: if not param[0]: raise ValueError("illegal SIP URI parameter name: %s" % param[0]) if len(param) == 1: param.append(None) elif '=' in param[1]: raise ValueError("illegal SIP URI parameter value: %s" % param[1]) instance.params[param[0]] = param[1] if uri.query: try: instance.headers = dict(header.split('=') for header in uri.query.split('&')) except ValueError: raise ValueError("illegal SIP URI headers: %s" % uri.query) else: - for name, value in instance.headers.iteritems(): + for name, value in list(instance.headers.items()): if not name or not value: raise ValueError("illegal URI header: %s=%s" % (name, value)) else: instance.headers = {} return instance class XCAPURI(AnyURI): _path_regex = re.compile(r'^(?P/(([^/]+)/)*)?(?P[^/]+)/((?Pglobal)|(users/(?P[^/]+)))/(?P~?(([^~]+~)|([^~]+))*)(/~~(?P.*))?$') def __new__(cls, value): instance = AnyURI.__new__(cls, value) - uri = urlparse.urlparse(instance) + uri = urllib.parse.urlparse(instance) if uri.scheme not in ('http', 'https', ''): raise ValueError("illegal scheme for XCAP URI: %s" % uri.scheme) instance.scheme = uri.scheme instance.username = uri.username instance.password = uri.password instance.hostname = uri.hostname instance.port = uri.port instance.__dict__.update(cls._path_regex.match(uri.path).groupdict()) instance.globaltree = instance.globaltree is not None if uri.query: try: instance.query = dict(header.split('=') for header in uri.query.split('&')) except ValueError: raise ValueError("illegal XCAP URI query string: %s" % uri.query) else: - for name, value in instance.query.iteritems(): + for name, value in list(instance.query.items()): if not name or not value: raise ValueError("illegal XCAP URI query parameter: %s=%s" % (name, value)) else: instance.query = {} return instance relative = property(lambda self: self.scheme == '') diff --git a/sipsimple/payloads/dialoginfo.py b/sipsimple/payloads/dialoginfo.py index 1aee80ea..b2f3b674 100644 --- a/sipsimple/payloads/dialoginfo.py +++ b/sipsimple/payloads/dialoginfo.py @@ -1,265 +1,265 @@ """Parses and produces dialog-info messages according to RFC4235.""" __all__ = ['namespace', 'DialogInfoDocument', 'DialogState', 'Replaces', 'ReferredBy', 'Identity', 'Param', 'Target', 'Local', 'Remote', 'Dialog', 'DialogInfo'] from sipsimple.payloads import XMLDocument, XMLListRootElement, XMLListElement, XMLStringElement, XMLNonNegativeIntegerElement, XMLElementChild, XMLEmptyElement, XMLElement, XMLElementID, XMLAttribute from sipsimple.payloads import IterateIDs, IterateItems, All namespace = 'urn:ietf:params:xml:ns:dialog-info' class DialogInfoDocument(XMLDocument): content_type = "application/dialog-info+xml" DialogInfoDocument.register_namespace(namespace, prefix=None, schema='dialog-info.xsd') # Attribute value types class StateValue(str): def __new__(cls, value): if value not in ('full', 'partial'): raise ValueError("illegal value for state") return str.__new__(cls, value) class VersionValue(int): def __new__(cls, value): value = int.__new__(cls, value) if value < 0: raise ValueError("illegal value for version") return value class DirectionValue(str): def __new__(cls, value): if value not in ('initiator', 'recipient'): raise ValueError("illegal value for direction") return str.__new__(cls, value) class DialogEventValue(str): def __new__(cls, value): if value not in ('rejected', 'cancelled', 'replaced', 'local-bye', 'remote-bye', 'error', 'timeout'): raise ValueError("illegal value for dialog state event") return str.__new__(cls, value) class DialogStateValue(str): def __new__(cls, value): if value not in ('trying', 'proceeding', 'early', 'confirmed', 'terminated'): raise ValueError("illegal value for dialog state") return str.__new__(cls, value) class CodeValue(int): def __new__(cls, value): value = int.__new__(cls, value) if value < 100 or value > 699: raise ValueError("illegal value for code") return value # Elements class CallId(XMLStringElement): _xml_tag = 'call-id' _xml_namespace = namespace _xml_document = DialogInfoDocument class LocalTag(XMLStringElement): _xml_tag = 'local-tag' _xml_namespace = namespace _xml_document = DialogInfoDocument class RemoteTag(XMLStringElement): _xml_tag = 'remote-tag' _xml_namespace = namespace _xml_document = DialogInfoDocument class DialogState(XMLStringElement): _xml_tag = 'state' _xml_namespace = namespace _xml_document = DialogInfoDocument _xml_value_type = DialogStateValue code = XMLAttribute('code', type=int, required=False, test_equal=True) event = XMLAttribute('event', type=DialogEventValue, required=False, test_equal=True) class Duration(XMLNonNegativeIntegerElement): _xml_tag = 'duration' _xml_namespace = namespace _xml_document = DialogInfoDocument class Replaces(XMLEmptyElement): _xml_tag = 'replaces' _xml_namespace = namespace _xml_document = DialogInfoDocument call_id = XMLAttribute('call_id', xmlname='call-id', type=str, required=True, test_equal=True) local_tag = XMLAttribute('local_tag', xmlname='local-tag', type=str, required=True, test_equal=True) remote_tag = XMLAttribute('remote_tag', xmlname='remote-tag', type=str, required=True, test_equal=True) def __init__(self, call_id, local_tag, remote_tag): XMLEmptyElement.__init__(self) self.call_id = call_id self.local_tag = local_tag self.remote_tag = remote_tag class ReferredBy(XMLStringElement): _xml_tag = 'referred-by' _xml_namespace = namespace _xml_document = DialogInfoDocument display = XMLAttribute('display', type=str, required=False, test_equal=True) class Identity(XMLStringElement): _xml_tag = 'identity' _xml_namespace = namespace _xml_document = DialogInfoDocument display = XMLAttribute('display', type=str, required=False, test_equal=True) class Param(XMLEmptyElement): _xml_tag = 'param' _xml_namespace = namespace _xml_document = DialogInfoDocument pname = XMLAttribute('pname', type=str, required=True, test_equal=True) pval = XMLAttribute('pval', type=str, required=True, test_equal=True) def __init__(self, pname, pval): XMLEmptyElement.__init__(self) self.pname = pname self.pval = pval class Target(XMLListElement): _xml_tag = 'target' _xml_namespace = namespace _xml_document = DialogInfoDocument _xml_item_type = Param uri = XMLAttribute('uri', type=str, required=True, test_equal=True) def __init__(self, uri, params=[]): self.uri = uri self.update(params) class Participant(XMLElement): identity = XMLElementChild('identity', type=Identity, required=False, test_equal=True) target = XMLElementChild('target', type=Target, required=False, test_equal=True) def __init__(self, identity=None, target=None): XMLElement.__init__(self) self.identity = identity self.target = target class Local(Participant): _xml_tag = 'local' _xml_namespace = namespace _xml_document = DialogInfoDocument class Remote(Participant): _xml_tag = 'remote' _xml_namespace = namespace _xml_document = DialogInfoDocument class Dialog(XMLElement): _xml_tag = 'dialog' _xml_namespace = namespace _xml_document = DialogInfoDocument id = XMLElementID('id', type=str, required=True, test_equal=True) call_id = XMLAttribute('call_id', xmlname='call-id', type=str, required=False, test_equal=True) local_tag = XMLAttribute('local_tag', xmlname='local-tag', type=str, required=False, test_equal=True) remote_tag = XMLAttribute('remote_tag', xmlname='remote-tag', type=str, required=False, test_equal=True) direction = XMLAttribute('direction', type=DirectionValue, required=False, test_equal=True) state = XMLElementChild('state', type=DialogState, required=True, test_equal=True) duration = XMLElementChild('duration', type=Duration, required=False, test_equal=True) replaces = XMLElementChild('replaces', type=Replaces, required=False, test_equal=True) referred_by = XMLElementChild('referred_by', type=ReferredBy, required=False, test_equal=True) local = XMLElementChild('local', type=Local, required=False, test_equal=True) remote = XMLElementChild('remote', type=Remote, required=False, test_equal=True) def __init__(self, id, state, call_id=None, local_tag=None, remote_tag=None, direction=None, duration=None, replaces=None, referred_by=None, local=None, remote=None): XMLElement.__init__(self) self.id = id self.state = state self.call_id = call_id self.local_tag = local_tag self.remote_tag = remote_tag self.direction = direction self.duration = duration self.replaces = replaces self.referred_by = referred_by self.local = local self.remote = remote class DialogInfo(XMLListRootElement): _xml_tag = 'dialog-info' _xml_namespace = namespace _xml_document = DialogInfoDocument _xml_children_order = {Dialog.qname: 0, None: 1} _xml_item_type = Dialog version = XMLAttribute('version', type=VersionValue, required=True, test_equal=True) state = XMLAttribute('state', type=StateValue, required=True, test_equal=True) entity = XMLAttribute('entity', type=str, required=True, test_equal=True) def __init__(self, version, state, entity, dialogs=[]): XMLListRootElement.__init__(self) self.version = version self.state = state self.entity = entity self.update(dialogs) def __getitem__(self, key): if key is IterateIDs: - return self._xmlid_map[Dialog].iterkeys() + return iter(list(self._xmlid_map[Dialog].keys())) elif key is IterateItems: - return self._xmlid_map[Dialog].itervalues() + return iter(list(self._xmlid_map[Dialog].values())) else: return self._xmlid_map[Dialog][key] def __delitem__(self, key): if key is All: - for item in self._xmlid_map[Dialog].values(): + for item in list(self._xmlid_map[Dialog].values()): self.remove(item) else: self.remove(self._xmlid_map[Dialog][key]) def get(self, key, default=None): return self._xmlid_map[Dialog].get(key, default) diff --git a/sipsimple/payloads/messagesummary.py b/sipsimple/payloads/messagesummary.py index 4905a81e..242e887b 100644 --- a/sipsimple/payloads/messagesummary.py +++ b/sipsimple/payloads/messagesummary.py @@ -1,71 +1,71 @@ """ Message summary and Message Waiting Indication handling according to RFC3842 """ import re -from cStringIO import StringIO +from io import StringIO from application.configuration.datatypes import Boolean from sipsimple.payloads import ValidationError class MessageSummary(object): content_type = "application/simple-message-summary" def __init__(self, messages_waiting=False, message_account=None, summaries=None, optional_headers=None): self.messages_waiting = messages_waiting self.message_account = message_account self.summaries = summaries if summaries is not None else {} self.optional_headers = optional_headers if optional_headers is not None else [] @classmethod def parse(cls, content): message = StringIO(content) summary = cls() tmp_headers = [] for line in message: if line == '\r\n': if tmp_headers: summary.optional_headers.append(tmp_headers) tmp_headers = [] else: field, sep, rest = line.partition(':') if not field and not rest: raise ValidationError("incorrect line format") field = field.strip() rest = rest.strip() if field.lower() == "messages-waiting": summary.messages_waiting = Boolean(rest) elif field.lower() == "message-account": summary.message_account = rest elif field.lower() in {"voice-message", "fax-message", "pager-message", "multimedia-message", "text-message", "none"}: m = re.match("((\d+)/(\d+))( \((\d+)/(\d+)\))?", rest) if m: summary.summaries[field.lower()] = dict(new_messages=m.groups()[1], old_messages=m.groups()[2], new_urgent_messages=m.groups()[4] or 0, old_urgent_messages=m.groups()[5] or 0) else: raise ValidationError("invalid message context class") else: tmp_headers.append(line.strip()) if tmp_headers: summary.optional_headers.append(tmp_headers) tmp_headers = [] return summary def to_string(self): data = "Messages-Waiting: %s\r\n" % 'yes' if self.messages_waiting else 'no' if self.message_account: data += "Message-Account: %s\r\n" % self.message_account if self.summaries: - for k, v in self.summaries.iteritems(): + for k, v in list(self.summaries.items()): data += "%s: %s/%s (%s/%s)\r\n" % (k.title(), v['new_messages'], v['old_messages'], v['new_urgent_messages'], v['old_urgent_messages']) if self.optional_headers: data += "\r\n" for headers in self.optional_headers: for h in headers: data += "%s\r\n" % h data += "\r\n" return data diff --git a/sipsimple/payloads/omapolicy.py b/sipsimple/payloads/omapolicy.py index f369ed87..0797f5c5 100644 --- a/sipsimple/payloads/omapolicy.py +++ b/sipsimple/payloads/omapolicy.py @@ -1,81 +1,81 @@ """ Conditions extension handling according to OMA-TS-Presence_SIMPLE_XDM-V1_1 This module provides an extension to RFC4745 (Common Policy) to support condition extensions defined by OMA. """ __all__ = ['namespace', 'OtherIdentity', 'ExternalList', 'AnonymousRequest'] from sipsimple.payloads import XMLElement, XMLEmptyElement, XMLListElement, XMLElementID from sipsimple.payloads.datatypes import AnyURI from sipsimple.payloads.commonpolicy import ConditionElement from sipsimple.payloads.presrules import PresRulesDocument namespace = 'urn:oma:xml:xdm:common-policy' PresRulesDocument.register_namespace(namespace, prefix='ocp', schema='oma-common-policy.xsd') class OtherIdentity(XMLEmptyElement, ConditionElement): _xml_tag = 'other-identity' _xml_namespace = namespace _xml_document = PresRulesDocument class Entry(XMLElement): _xml_tag = 'entry' _xml_namespace = namespace _xml_document = PresRulesDocument uri = XMLElementID('uri', xmlname='anc', type=AnyURI, required=True, test_equal=True) def __init__(self, uri): XMLElement.__init__(self) self.uri = uri def __unicode__(self): return self.uri def __str__(self): return str(self.uri) class ExternalList(XMLListElement, ConditionElement): _xml_tag = 'external-list' _xml_namespace = namespace _xml_document = PresRulesDocument _xml_item_type = Entry def __init__(self, entries=[]): XMLListElement.__init__(self) self.update(entries) def __iter__(self): - return (unicode(item) for item in super(ExternalList, self).__iter__()) + return (str(item) for item in super(ExternalList, self).__iter__()) def __repr__(self): return '%s(%r)' % (self.__class__.__name__, list(self)) def add(self, item): - if isinstance(item, basestring): + if isinstance(item, str): item = Entry(item) super(ExternalList, self).add(item) def remove(self, item): - if isinstance(item, basestring): + if isinstance(item, str): try: - item = (entry for entry in super(ExternalList, self).__iter__() if entry == item).next() + item = next((entry for entry in super(ExternalList, self).__iter__() if entry == item)) except StopIteration: raise KeyError(item) super(ExternalList, self).remove(item) class AnonymousRequest(XMLEmptyElement, ConditionElement): _xml_tag = 'anonymous-request' _xml_namespace = namespace _xml_document = PresRulesDocument diff --git a/sipsimple/payloads/pidf.py b/sipsimple/payloads/pidf.py index d8f35bae..cdf11ad1 100644 --- a/sipsimple/payloads/pidf.py +++ b/sipsimple/payloads/pidf.py @@ -1,535 +1,535 @@ """PIDF handling according to RFC3863 and RFC4479""" __all__ = ['pidf_namespace', 'dm_namespace', 'PIDFDocument', 'ServiceExtension', 'DeviceExtension', 'PersonExtension', 'StatusExtension', 'Note', 'DeviceID', 'Status', 'Basic', 'Contact', 'ServiceTimestamp', 'Service', 'DeviceTimestamp', 'Device', 'PersonTimestamp', 'Person', 'PIDF', # Extensions 'ExtendedStatus', 'StatusType', 'DeviceInfo'] -from itertools import izip + from application.python.weakref import weakobjectmap from sipsimple.payloads import ValidationError, XMLDocument, XMLListRootElement, XMLListElement, XMLElement, XMLAttribute, XMLElementID, XMLElementChild from sipsimple.payloads import XMLStringElement, XMLLocalizedStringElement, XMLDateTimeElement, XMLAnyURIElement from sipsimple.payloads.datatypes import AnyURI, ID, DateTime pidf_namespace = 'urn:ietf:params:xml:ns:pidf' dm_namespace = 'urn:ietf:params:xml:ns:pidf:data-model' class PIDFDocument(XMLDocument): content_type = 'application/pidf+xml' PIDFDocument.register_namespace(pidf_namespace, prefix=None, schema='pidf.xsd') PIDFDocument.register_namespace(dm_namespace, prefix='dm', schema='data-model.xsd') ## Marker mixin class ServiceExtension(object): pass class ServiceItemExtension(object): pass class DeviceExtension(object): pass class PersonExtension(object): pass class StatusExtension(object): pass ## Attribute value types class BasicStatusValue(str): def __new__(cls, value): if value not in ('closed', 'open'): raise ValueError('illegal BasicStatusValue') return str.__new__(cls, value) ## General elements -class Note(unicode): +class Note(str): def __new__(cls, value, lang=None): - instance = unicode.__new__(cls, value) + instance = str.__new__(cls, value) instance.lang = lang return instance def __repr__(self): - return "%s(%s, lang=%r)" % (self.__class__.__name__, unicode.__repr__(self), self.lang) + return "%s(%s, lang=%r)" % (self.__class__.__name__, str.__repr__(self), self.lang) def __eq__(self, other): if isinstance(other, Note): - return unicode.__eq__(self, other) and self.lang == other.lang - elif isinstance(other, basestring): - return self.lang is None and unicode.__eq__(self, other) + return str.__eq__(self, other) and self.lang == other.lang + elif isinstance(other, str): + return self.lang is None and str.__eq__(self, other) else: return NotImplemented def __ne__(self, other): equal = self.__eq__(other) return NotImplemented if equal is NotImplemented else not equal class PIDFNote(XMLLocalizedStringElement): _xml_tag = 'note' _xml_namespace = pidf_namespace _xml_document = PIDFDocument def __unicode__(self): return Note(self.value, self.lang) class DMNote(XMLLocalizedStringElement): _xml_tag = 'note' _xml_namespace = dm_namespace _xml_document = PIDFDocument def __unicode__(self): return Note(self.value, self.lang) class NoteMap(object): """Descriptor to be used for _note_map attributes on XML elements with notes""" def __init__(self): self.object_map = weakobjectmap() def __get__(self, obj, type): if obj is None: return self try: return self.object_map[obj] except KeyError: return self.object_map.setdefault(obj, {}) def __set__(self, obj, value): raise AttributeError("cannot set attribute") def __delete__(self, obj): raise AttributeError("cannot delete attribute") class NoteList(object): def __init__(self, xml_element, note_type): self.xml_element = xml_element self.note_type = note_type def __contains__(self, item): if isinstance(item, Note): item = self.note_type(item, item.lang) - elif isinstance(item, basestring): + elif isinstance(item, str): item = self.note_type(item) - return item in self.xml_element._note_map.itervalues() + return item in iter(list(self.xml_element._note_map.values())) def __iter__(self): - return (unicode(self.xml_element._note_map[element]) for element in self.xml_element.element if element in self.xml_element._note_map) + return (str(self.xml_element._note_map[element]) for element in self.xml_element.element if element in self.xml_element._note_map) def __len__(self): return len(self.xml_element._note_map) def __eq__(self, other): if isinstance(other, NoteList): - return self is other or (len(self) == len(other) and all(self_item == other_item for self_item, other_item in izip(self, other))) + return self is other or (len(self) == len(other) and all(self_item == other_item for self_item, other_item in zip(self, other))) else: return NotImplemented def __ne__(self, other): equal = self.__eq__(other) return NotImplemented if equal is NotImplemented else not equal def _parse_element(self, element): self.xml_element._note_map.clear() for child in element: if child.tag == self.note_type.qname: try: note = self.note_type.from_element(child, xml_document=self.xml_element._xml_document) except ValidationError: pass else: self.xml_element._note_map[note.element] = note def _build_element(self): - for note in self.xml_element._note_map.itervalues(): + for note in list(self.xml_element._note_map.values()): note.to_element() def add(self, item): if isinstance(item, Note): item = self.note_type(item, item.lang) - elif isinstance(item, basestring): + elif isinstance(item, str): item = self.note_type(item) if type(item) is not self.note_type: raise TypeError("%s cannot add notes of type %s" % (self.xml_element.__class__.__name__, item.__class__.__name__)) self.xml_element._insert_element(item.element) self.xml_element._note_map[item.element] = item self.xml_element.__dirty__ = True def remove(self, item): if isinstance(item, Note): try: - item = (entry for entry in self.xml_element._note_map.itervalues() if unicode(entry) == item).next() + item = next((entry for entry in list(self.xml_element._note_map.values()) if str(entry) == item)) except StopIteration: raise KeyError(item) - elif isinstance(item, basestring): + elif isinstance(item, str): try: - item = (entry for entry in self.xml_element._note_map.itervalues() if entry == item).next() + item = next((entry for entry in list(self.xml_element._note_map.values()) if entry == item)) except StopIteration: raise KeyError(item) if type(item) is not self.note_type: raise KeyError(item) self.xml_element.element.remove(item.element) del self.xml_element._note_map[item.element] self.xml_element.__dirty__ = True def update(self, sequence): for item in sequence: self.add(item) def clear(self): - for item in self.xml_element._note_map.values(): + for item in list(self.xml_element._note_map.values()): self.remove(item) class DeviceID(XMLStringElement): _xml_tag = 'deviceID' _xml_namespace = dm_namespace _xml_document = PIDFDocument ## Service elements class Basic(XMLStringElement): _xml_tag = 'basic' _xml_namespace = pidf_namespace _xml_document = PIDFDocument _xml_value_type = BasicStatusValue class Status(XMLElement): _xml_tag = 'status' _xml_namespace = pidf_namespace _xml_document = PIDFDocument _xml_extension_type = StatusExtension _xml_children_order = {Basic.qname: 0} basic = XMLElementChild('basic', type=Basic, required=False, test_equal=True) def __init__(self, basic=None): XMLElement.__init__(self) self.basic = basic def check_validity(self): if len(self.element) == 0: raise ValidationError("Status objects must have at least one child") super(Status, self).check_validity() def __repr__(self): return '%s(%r)' % (self.__class__.__name__, self.basic) class Contact(XMLAnyURIElement): _xml_tag = 'contact' _xml_namespace = pidf_namespace _xml_document = PIDFDocument priority = XMLAttribute('priority', type=float, required=False, test_equal=False) class ServiceTimestamp(XMLDateTimeElement): _xml_tag = 'timestamp' _xml_namespace = pidf_namespace _xml_document = PIDFDocument class Service(XMLListElement): _xml_tag = 'tuple' _xml_namespace = pidf_namespace _xml_document = PIDFDocument _xml_extension_type = ServiceExtension _xml_item_type = (DeviceID, ServiceItemExtension) _xml_children_order = {Status.qname: 0, None: 1, Contact.qname: 2, PIDFNote.qname: 3, ServiceTimestamp.qname: 4} id = XMLElementID('id', type=ID, required=True, test_equal=True) status = XMLElementChild('status', type=Status, required=True, test_equal=True) contact = XMLElementChild('contact', type=Contact, required=False, test_equal=True) timestamp = XMLElementChild('timestamp', type=ServiceTimestamp, required=False, test_equal=True) _note_map = NoteMap() def __init__(self, id, notes=[], status=None, contact=None, timestamp=None): XMLListElement.__init__(self) self.id = id self.status = status self.contact = contact self.timestamp = timestamp self.notes.update(notes) @property def notes(self): return NoteList(self, PIDFNote) def __eq__(self, other): if isinstance(other, Service): return super(Service, self).__eq__(other) and self.notes == other.notes else: return self.id == other def __repr__(self): return '%s(%r, %r, %r, %r, %r)' % (self.__class__.__name__, self.id, list(self.notes), self.status, self.contact, self.timestamp) def _parse_element(self, element): super(Service, self)._parse_element(element) self.notes._parse_element(element) def _build_element(self): super(Service, self)._build_element() self.notes._build_element() class DeviceTimestamp(XMLDateTimeElement): _xml_tag = 'timestamp' _xml_namespace = dm_namespace _xml_document = PIDFDocument class Device(XMLElement): _xml_tag = 'device' _xml_namespace = dm_namespace _xml_document = PIDFDocument _xml_extension_type = DeviceExtension _xml_children_order = {None: 0, DeviceID.qname: 1, DMNote.qname: 2, DeviceTimestamp.qname: 3} id = XMLElementID('id', type=ID, required=True, test_equal=True) device_id = XMLElementChild('device_id', type=DeviceID, required=False, test_equal=True) timestamp = XMLElementChild('timestamp', type=DeviceTimestamp, required=False, test_equal=True) _note_map = NoteMap() def __init__(self, id, device_id=None, notes=[], timestamp=None): XMLElement.__init__(self) self.id = id self.device_id = device_id self.timestamp = timestamp self.notes.update(notes) @property def notes(self): return NoteList(self, DMNote) def __eq__(self, other): if isinstance(other, Device): return super(Device, self).__eq__(other) and self.notes == other.notes else: return self.id == other def __repr__(self): return '%s(%r, %r, %r, %r)' % (self.__class__.__name__, self.id, self.device_id, list(self.notes), self.timestamp) def _parse_element(self, element): super(Device, self)._parse_element(element) self.notes._parse_element(element) def _build_element(self): super(Device, self)._build_element() self.notes._build_element() class PersonTimestamp(XMLDateTimeElement): _xml_tag = 'timestamp' _xml_namespace = dm_namespace _xml_document = PIDFDocument class Person(XMLElement): _xml_tag = 'person' _xml_namespace = dm_namespace _xml_document = PIDFDocument _xml_extension_type = PersonExtension _xml_children_order = {None: 0, DMNote.qname: 1, PersonTimestamp.qname: 2} id = XMLElementID('id', type=ID, required=True, test_equal=True) timestamp = XMLElementChild('timestamp', type=PersonTimestamp, required=False, test_equal=True) _note_map = NoteMap() def __init__(self, id, notes=[], timestamp=None): XMLElement.__init__(self) self.id = id self.timestamp = timestamp self.notes.update(notes) @property def notes(self): return NoteList(self, DMNote) def __eq__(self, other): if isinstance(other, Person): return super(Person, self).__eq__(other) and self.notes == other.notes else: return self.id == other def __repr__(self): return '%s(%r, %r, %r)' % (self.__class__.__name__, self.id, list(self.notes), self.timestamp) def _parse_element(self, element): super(Person, self)._parse_element(element) self.notes._parse_element(element) def _build_element(self): super(Person, self)._build_element() self.notes._build_element() class PIDF(XMLListRootElement): _xml_tag = 'presence' _xml_namespace = pidf_namespace _xml_document = PIDFDocument _xml_children_order = {Service.qname: 0, PIDFNote.qname: 1, Person.qname: 2, Device.qname: 3} _xml_item_type = (Service, PIDFNote, Person, Device) entity = XMLAttribute('entity', type=AnyURI, required=True, test_equal=True) services = property(lambda self: (item for item in self if type(item) is Service)) notes = property(lambda self: (item for item in self if type(item) is Note)) persons = property(lambda self: (item for item in self if type(item) is Person)) devices = property(lambda self: (item for item in self if type(item) is Device)) def __init__(self, entity, elements=[]): XMLListRootElement.__init__(self) self.entity = entity self.update(elements) def __contains__(self, item): if isinstance(item, Note): item = PIDFNote(item, item.lang) return super(PIDF, self).__contains__(item) def __iter__(self): - return (unicode(item) if type(item) is PIDFNote else item for item in super(PIDF, self).__iter__()) + return (str(item) if type(item) is PIDFNote else item for item in super(PIDF, self).__iter__()) def __repr__(self): return '%s(%r, %r)' % (self.__class__.__name__, self.entity, list(self)) def add(self, item): if isinstance(item, Note): item = PIDFNote(item, item.lang) super(PIDF, self).add(item) def remove(self, item): if isinstance(item, Note): try: - item = (entry for entry in super(PIDF, self).__iter__() if type(entry) is PIDFNote and unicode(entry) == item).next() + item = next((entry for entry in super(PIDF, self).__iter__() if type(entry) is PIDFNote and str(entry) == item)) except StopIteration: raise KeyError(item) super(PIDF, self).remove(item) # # Extensions # agp_pidf_namespace = 'urn:ag-projects:xml:ns:pidf' PIDFDocument.register_namespace(agp_pidf_namespace, prefix='agp-pidf') class ExtendedStatusValue(str): def __new__(cls, value): if value not in ('available', 'offline', 'away', 'busy'): raise ValueError("illegal value for extended status") return str.__new__(cls, value) class ExtendedStatus(XMLStringElement, StatusExtension): _xml_tag = 'extended' _xml_namespace = agp_pidf_namespace _xml_document = PIDFDocument _xml_value_type = ExtendedStatusValue class StatusType(XMLStringElement, StatusExtension): _xml_tag = 'type' _xml_namespace = agp_pidf_namespace _xml_document = PIDFDocument Status.register_extension('extended', type=ExtendedStatus) Status.register_extension('type', type=StatusType) class Description(XMLStringElement): _xml_tag = 'description' _xml_namespace = agp_pidf_namespace _xml_document = PIDFDocument class UserAgent(XMLStringElement): _xml_tag = 'user-agent' _xml_namespace = agp_pidf_namespace _xml_document = PIDFDocument class TimeOffset(XMLStringElement): _xml_tag = 'time-offset' _xml_namespace = agp_pidf_namespace _xml_document = PIDFDocument - description = XMLAttribute('description', type=unicode, required=False, test_equal=True) + description = XMLAttribute('description', type=str, required=False, test_equal=True) def __init__(self, value=None, description=None): if value is None: value = DateTime.now().utcoffset().seconds / 60 XMLStringElement.__init__(self, str(value)) self.description = description def __int__(self): return int(self.value) class DeviceInfo(XMLElement, ServiceExtension): _xml_tag = 'device-info' _xml_namespace = agp_pidf_namespace _xml_document = PIDFDocument _xml_children_order = {Description.qname: 0, UserAgent.qname: 1} id = XMLElementID('id', type=str, required=True, test_equal=True) description = XMLElementChild('description', type=Description, required=False, test_equal=True) user_agent = XMLElementChild('user_agent', type=UserAgent, required=False, test_equal=True) time_offset = XMLElementChild('time_offset', type=TimeOffset, required=False, test_equal=True) def __init__(self, id, description=None, user_agent=None, time_offset=None): XMLElement.__init__(self) self.id = id self.description = description self.user_agent = user_agent self.time_offset = time_offset def __repr__(self): return '%s(%r, %r, %r, %r)' % (self.__class__.__name__, self.id, self.description, self.user_agent, self.time_offset) Service.register_extension('device_info', type=DeviceInfo) diff --git a/sipsimple/payloads/presrules.py b/sipsimple/payloads/presrules.py index c296d3d7..fd1bb6bb 100644 --- a/sipsimple/payloads/presrules.py +++ b/sipsimple/payloads/presrules.py @@ -1,358 +1,358 @@ """Parses and produces Presence Authorization Rules documents according to RFC5025.""" __all__ = ['namespace', 'PresRulesDocument', 'SubHandling', 'DeviceID', 'Class', 'All', 'ProvideDevices', 'OccurenceID', 'ProvidePersons', 'ServiceURI', 'ServiceURIScheme', 'ProvideServices', 'ProvideActivities', 'ProvideClass', 'ProvideDeviceID', 'ProvideMood', 'ProvidePlaceIs', 'ProvidePlaceType', 'ProvidePrivacy', 'ProvideRelationship', 'ProvideStatusIcon', 'ProvideSphere', 'ProvideTimeOffset', 'ProvideUserInput', 'ProvideUnknownAttribute', 'ProvideAllAttributes', 'PresRules'] from sipsimple.payloads import XMLListElement, XMLStringElement, XMLBooleanElement, XMLEmptyElement, XMLAttribute from sipsimple.payloads import commonpolicy from sipsimple.util import All namespace = 'urn:ietf:params:xml:ns:pres-rules' class PresRulesDocument(commonpolicy.CommonPolicyDocument): pass PresRulesDocument.register_namespace(namespace, prefix='pr', schema='pres-rules.xsd') ## Marker mixins class ProvideDeviceElement(object): pass class ProvidePersonElement(object): pass class ProvideServiceElement(object): pass ## Attribute value types class SubHandlingValue(str): __prioritymap__ = {'block': 0, 'confirm': 10, 'polite-block': 20, 'allow': 30} def __new__(cls, value): if value not in cls.__prioritymap__: raise ValueError("illegal value for SubHandling element") return str.__new__(cls, value) @property def priority(self): return self.__prioritymap__[self] class ProvideUserInputValue(str): def __new__(cls, value): if value not in ('false', 'bare', 'thresholds', 'full'): raise ValueError("illegal value for ProvideUserInput element") return str.__new__(cls, value) ## Action Elements class SubHandling(XMLStringElement, commonpolicy.ActionElement): _xml_tag = 'sub-handling' _xml_namespace = namespace _xml_document = PresRulesDocument _xml_value_type = SubHandlingValue ## Transformation Elements class Class(XMLStringElement): _xml_tag = 'class' _xml_namespace = namespace _xml_document = PresRulesDocument class OccurenceID(XMLStringElement): _xml_tag = 'occurence-id' _xml_namespace = namespace _xml_document = PresRulesDocument ## Devices element class DeviceID(XMLStringElement): _xml_tag = 'deviceID' _xml_namespace = namespace _xml_document = PresRulesDocument class AllDevices(XMLEmptyElement): _xml_tag = 'all-devices' _xml_namespace = namespace _xml_document = PresRulesDocument class ProvideDevices(XMLListElement, commonpolicy.TransformationElement): _xml_tag = 'provide-devices' _xml_namespace = namespace _xml_document = PresRulesDocument _xml_item_type = (DeviceID, OccurenceID, Class, AllDevices, ProvideDeviceElement) def __init__(self, provides=[]): XMLListElement.__init__(self) self.update(provides) def __contains__(self, item): if item == All: item = AllDevices() return super(ProvideDevices, self).__contains__(item) def __iter__(self): return (All if type(item) is AllDevices else item for item in super(ProvideDevices, self).__iter__()) def add(self, item): if item == All: item = AllDevices() if type(item) is AllDevices: self.clear() else: try: self.remove(All) except KeyError: pass super(ProvideDevices, self).add(item) def remove(self, item): if item == All: try: - item = (item for item in super(ProvideDevices, self).__iter__() if type(item) is AllDevices).next() + item = next((item for item in super(ProvideDevices, self).__iter__() if type(item) is AllDevices)) except StopIteration: raise KeyError(item) super(ProvideDevices, self).remove(item) ## Persons elmeent class AllPersons(XMLEmptyElement): _xml_tag = 'all-persons' _xml_namespace = namespace _xml_document = PresRulesDocument class ProvidePersons(XMLListElement, commonpolicy.TransformationElement): _xml_tag = 'provide-persons' _xml_namespace = namespace _xml_document = PresRulesDocument _xml_item_type = (OccurenceID, Class, AllPersons, ProvidePersonElement) def __init__(self, provides=[]): XMLListElement.__init__(self) self.update(provides) def __contains__(self, item): if item == All: item = AllPersons() return super(ProvidePersons, self).__contains__(item) def __iter__(self): return (All if type(item) is AllPersons else item for item in super(ProvidePersons, self).__iter__()) def add(self, item): if item == All: item = AllPersons() if type(item) is AllPersons: self.clear() else: try: self.remove(All) except KeyError: pass super(ProvidePersons, self).add(item) def remove(self, item): if item == All: try: - item = (item for item in super(ProvidePersons, self).__iter__() if type(item) is AllPersons).next() + item = next((item for item in super(ProvidePersons, self).__iter__() if type(item) is AllPersons)) except StopIteration: raise KeyError(item) super(ProvidePersons, self).remove(item) ## Service elements class ServiceURI(XMLStringElement): _xml_tag = 'service-uri' _xml_namespace = namespace _xml_document = PresRulesDocument class ServiceURIScheme(XMLStringElement): _xml_tag = 'service-uri-scheme' _xml_namespace = namespace _xml_document = PresRulesDocument class AllServices(XMLEmptyElement): _xml_tag = 'all-services' _xml_namespace = namespace _xml_document = PresRulesDocument class ProvideServices(XMLListElement, commonpolicy.TransformationElement): _xml_tag = 'provide-services' _xml_namespace = namespace _xml_document = PresRulesDocument _xml_item_type = (ServiceURI, ServiceURIScheme, OccurenceID, Class, AllServices, ProvideServiceElement) def __init__(self, provides=[]): XMLListElement.__init__(self) self.update(provides) def __contains__(self, item): if item == All: item = AllServices() return super(ProvideServices, self).__contains__(item) def __iter__(self): return (All if type(item) is AllServices else item for item in super(ProvideServices, self).__iter__()) def add(self, item): if item == All: item = AllServices() if type(item) is AllServices: self.clear() else: try: self.remove(All) except KeyError: pass super(ProvideServices, self).add(item) def remove(self, item): if item == All: try: - item = (item for item in super(ProvideServices, self).__iter__() if type(item) is AllServices).next() + item = next((item for item in super(ProvideServices, self).__iter__() if type(item) is AllServices)) except StopIteration: raise KeyError(item) super(ProvideServices, self).remove(item) ## Transformation elements class ProvideActivities(XMLBooleanElement, commonpolicy.TransformationElement): _xml_tag = 'provide-activities' _xml_namespace = namespace _xml_document = PresRulesDocument class ProvideClass(XMLBooleanElement, commonpolicy.TransformationElement): _xml_tag = 'provide-class' _xml_namespace = namespace _xml_document = PresRulesDocument class ProvideDeviceID(XMLBooleanElement, commonpolicy.TransformationElement): _xml_tag = 'provide-deviceID' _xml_namespace = namespace _xml_document = PresRulesDocument class ProvideMood(XMLBooleanElement, commonpolicy.TransformationElement): _xml_tag = 'provide-mood' _xml_namespace = namespace _xml_document = PresRulesDocument class ProvidePlaceIs(XMLBooleanElement, commonpolicy.TransformationElement): _xml_tag = 'provide-place-is' _xml_namespace = namespace _xml_document = PresRulesDocument class ProvidePlaceType(XMLBooleanElement, commonpolicy.TransformationElement): _xml_tag = 'provide-place-type' _xml_namespace = namespace _xml_document = PresRulesDocument class ProvidePrivacy(XMLBooleanElement, commonpolicy.TransformationElement): _xml_tag = 'provide-privacy' _xml_namespace = namespace _xml_document = PresRulesDocument class ProvideRelationship(XMLBooleanElement, commonpolicy.TransformationElement): _xml_tag = 'provide-relationship' _xml_namespace = namespace _xml_document = PresRulesDocument class ProvideStatusIcon(XMLBooleanElement, commonpolicy.TransformationElement): _xml_tag = 'provide-status-icon' _xml_namespace = namespace _xml_document = PresRulesDocument class ProvideSphere(XMLBooleanElement, commonpolicy.TransformationElement): _xml_tag = 'provide-sphere' _xml_namespace = namespace _xml_document = PresRulesDocument class ProvideTimeOffset(XMLBooleanElement, commonpolicy.TransformationElement): _xml_tag = 'provide-time-offset' _xml_namespace = namespace _xml_document = PresRulesDocument class ProvideUserInput(XMLStringElement, commonpolicy.TransformationElement): _xml_tag = 'provide-user-input' _xml_namespace = namespace _xml_document = PresRulesDocument _xml_value_type = ProvideUserInputValue class ProvideUnknownAttribute(XMLBooleanElement, commonpolicy.TransformationElement): _xml_tag = 'provide-unknown-attribute' _xml_namespace = namespace _xml_document = PresRulesDocument name = XMLAttribute('name', type=str, required=True, test_equal=True) ns = XMLAttribute('ns', type=str, required=True, test_equal=True) def __init__(self, ns, name, value): XMLBooleanElement.__init__(self, value) self.ns = ns self.name = name def __repr__(self): return '%s(%r, %r, %r)' % (self.__class__.__name__, self.ns, self.name, self.value) class ProvideAllAttributes(XMLEmptyElement, commonpolicy.TransformationElement): _xml_tag = 'provide-all-attributes' _xml_namespace = namespace _xml_document = PresRulesDocument class PresRules(commonpolicy.RuleSet): _xml_document = PresRulesDocument diff --git a/sipsimple/payloads/resourcelists.py b/sipsimple/payloads/resourcelists.py index 3382c6c5..78f200a9 100644 --- a/sipsimple/payloads/resourcelists.py +++ b/sipsimple/payloads/resourcelists.py @@ -1,378 +1,378 @@ """Resource lists (rfc4826) handling""" __all__ = ['namespace', 'ResourceListsDocument', 'DisplayName', 'Entry', 'EntryRef', 'External', 'List', 'ResourceLists', # Extensions 'EntryAttributes'] from collections import deque from lxml import etree from xml.sax.saxutils import quoteattr from application.python import Null from sipsimple.payloads import XMLDocument, XMLListRootElement, XMLElement, XMLListElement, XMLLocalizedStringElement, XMLElementID, XMLElementChild, ThisClass from sipsimple.payloads import IterateIDs, IterateItems, All from sipsimple.payloads.datatypes import AnyURI namespace = 'urn:ietf:params:xml:ns:resource-lists' # excerpt from the RFC: # # attribute "name" - optional, unique among the same level # body: optional , the sequence of entry/list/entry-ref/external # # attribute xml:lang - optional # body: utf8 string # # attribute "uri" - mandatory, unique among all other within the same parent # body: optional # # attribute "ref" - mandatory, unique among all other within the same parent # body: optional # ref is a relative URI that resolves into # # attribute "anchor" - mandatory, unique among all other anchor in within the same parent # anchor must be an absolute http uri that resolves into class ResourceListsDocument(XMLDocument): content_type = 'application/resource-lists+xml' ResourceListsDocument.register_namespace(namespace, prefix='rl', schema='resourcelists.xsd') ## Marker mixins class ListElement(object): pass class EntryExtension(object): pass ## Elements class DisplayName(XMLLocalizedStringElement): _xml_tag = 'display-name' _xml_namespace = namespace _xml_document = ResourceListsDocument class Entry(XMLElement): _xml_tag = 'entry' _xml_namespace = namespace _xml_extension_type = EntryExtension _xml_document = ResourceListsDocument _xml_children_order = {DisplayName.qname: 0} uri = XMLElementID('uri', type=AnyURI, required=True, test_equal=True) display_name = XMLElementChild('display_name', type=DisplayName, required=False, test_equal=False) def __init__(self, uri, display_name=None): XMLElement.__init__(self) self.uri = uri self.display_name = display_name def __unicode__(self): - return self.display_name and u'"%s" <%s>' % (self.display_name, self.uri) or self.uri + return self.display_name and '"%s" <%s>' % (self.display_name, self.uri) or self.uri def __repr__(self): return '%s(%r, %r)' % (self.__class__.__name__, self.uri, self.display_name) class EntryRef(XMLElement): _xml_tag = 'entry-ref' _xml_namespace = namespace _xml_document = ResourceListsDocument _xml_children_order = {DisplayName.qname: 0} ref = XMLElementID('ref', type=AnyURI, required=True, test_equal=True) display_name = XMLElementChild('display_name', type=DisplayName, required=False, test_equal=False) def __init__(self, ref, display_name=None): XMLElement.__init__(self) self.ref = ref self.display_name = display_name def __unicode__(self): return self.display_name and '"%s" <%s>' % (self.display_name, self.ref) or self.ref def __repr__(self): return '%s(%r, %r)' % (self.__class__.__name__, self.ref, self.display_name) class External(XMLElement): _xml_tag = 'external' _xml_namespace = namespace _xml_document = ResourceListsDocument _xml_children_order = {DisplayName.qname: 0} anchor = XMLElementID('anchor', type=AnyURI, required=True, test_equal=True) display_name = XMLElementChild('display_name', type=DisplayName, required=False, test_equal=False) def __init__(self, anchor, display_name=None): XMLElement.__init__(self) self.anchor = anchor self.display_name = display_name def __unicode__(self): return self.display_name and '"%s" <%s>' % (self.display_name, self.anchor) or self.anchor def __repr__(self): return '%s(%r, %r)' % (self.__class__.__name__, self.anchor, self.display_name) List = ThisClass # a List can contain items of its own kind class List(XMLListElement): _xml_tag = 'list' _xml_namespace = namespace _xml_document = ResourceListsDocument _xml_children_order = {DisplayName.qname: 0, Entry.qname: 1, EntryRef.qname: 1, External.qname: 1} _xml_item_type = (Entry, EntryRef, External, List, ListElement) - name = XMLElementID('name', type=unicode, required=False, test_equal=True) + name = XMLElementID('name', type=str, required=False, test_equal=True) display_name = XMLElementChild('display_name', type=DisplayName, required=False, test_equal=False) def __init__(self, entries=[], name=None, display_name=None): XMLListElement.__init__(self) self.name = name self.display_name = display_name self.update(entries) def __repr__(self): return '%s(%s, %r, %r)' % (self.__class__.__name__, list(self), self.name, self.display_name) def __unicode__(self): - name = u'List element' + name = 'List element' if self.name is not None: - name += u' %s' % self.name + name += ' %s' % self.name if self.display_name is not None: - name += u' (%s)' % self.display_name + name += ' (%s)' % self.display_name return name List._xml_children_order[List.qname] = 1 # cannot self reference in declaration class ResourceLists(XMLListRootElement): _xml_tag = 'resource-lists' _xml_namespace = namespace _xml_document = ResourceListsDocument _xml_children_order = {List.qname: 0} _xml_item_type = List def __init__(self, lists=[]): XMLListRootElement.__init__(self) self.update(lists) def __getitem__(self, key): if key is IterateIDs: - return self._xmlid_map[List].iterkeys() + return iter(list(self._xmlid_map[List].keys())) elif key is IterateItems: - return self._xmlid_map[List].itervalues() + return iter(list(self._xmlid_map[List].values())) else: return self._xmlid_map[List][key] def __delitem__(self, key): if key is All: - for item in self._xmlid_map[List].values(): + for item in list(self._xmlid_map[List].values()): self.remove(item) else: self.remove(self._xmlid_map[List][key]) def get(self, key, default=None): return self._xmlid_map[List].get(key, default) def get_xpath(self, element): if not isinstance(element, (List, Entry, EntryRef, External, ResourceLists)): raise ValueError('can only find xpath for List, Entry, EntryRef or External elements') - nsmap = dict((namespace, prefix) for prefix, namespace in self._xml_document.nsmap.iteritems()) + nsmap = dict((namespace, prefix) for prefix, namespace in list(self._xml_document.nsmap.items())) nsmap[self._xml_namespace] = None xpath_nsmap = {} root_xpath = '/' + self._xml_tag if element is self: return root_xpath notexpanded = deque([self]) visited = set(notexpanded) parents = {self: None} obj = None while notexpanded: list = notexpanded.popleft() for child in list: if child is element: parents[child] = list obj = child notexpanded.clear() break elif isinstance(child, List) and child not in visited: parents[child] = list notexpanded.append(child) visited.add(child) if obj is None: return None components = [] while obj is not self: prefix = nsmap[obj._xml_namespace] if prefix: name = '%s:%s' % (prefix, obj._xml_tag) xpath_nsmap[obj._xml_namespace] = prefix else: name = obj._xml_tag if isinstance(obj, List): if obj.name is not None: components.append('/%s[@%s=%s]' % (name, List.name.xmlname, quoteattr(obj.name))) else: siblings = [l for l in parents[obj] if isinstance(l, List)] components.append('/%s[%d]' % (name, siblings.index(obj)+1)) elif isinstance(obj, Entry): components.append('/%s[@%s=%s]' % (name, Entry.uri.xmlname, quoteattr(obj.uri))) elif isinstance(obj, EntryRef): components.append('/%s[@%s=%s]' % (name, EntryRef.ref.xmlname, quoteattr(obj.ref))) elif isinstance(obj, External): components.append('/%s[@%s=%s]' % (name, External.anchor.xmlname, quoteattr(obj.anchor))) obj = parents[obj] components.reverse() - return root_xpath + ''.join(components) + ('?' + ''.join('xmlns(%s=%s)' % (prefix, namespace) for namespace, prefix in xpath_nsmap.iteritems()) if xpath_nsmap else '') + return root_xpath + ''.join(components) + ('?' + ''.join('xmlns(%s=%s)' % (prefix, namespace) for namespace, prefix in list(xpath_nsmap.items())) if xpath_nsmap else '') def find_parent(self, element): if not isinstance(element, (List, Entry, EntryRef, External)): raise ValueError('can only find parent for List, Entry, EntryRef or External elements') if element is self: return None notexpanded = deque([self]) visited = set(notexpanded) while notexpanded: list = notexpanded.popleft() for child in list: if child is element: return list elif isinstance(child, List) and child not in visited: notexpanded.append(child) visited.add(child) return None # # Extensions # class EntryAttributes(XMLElement, EntryExtension): _xml_tag = 'attributes' _xml_namespace = 'urn:ag-projects:xml:ns:resource-lists' _xml_document = ResourceListsDocument def __init__(self, iterable=(), **attributes): XMLElement.__init__(self) self._attributes = dict() self.update(iterable, **attributes) def _parse_element(self, element): self._attributes = dict() attribute_tag = '{%s}attribute' % self._xml_namespace for child in (child for child in element if child.tag == attribute_tag): if 'nil' in child.attrib: self[child.attrib['name']] = None else: - self[child.attrib['name']] = unicode(child.text or u'') + self[child.attrib['name']] = str(child.text or '') def _build_element(self): self.element.clear() attribute_tag = '{%s}attribute' % self._xml_namespace - for key, value in self.iteritems(): + for key, value in list(self.items()): child = etree.SubElement(self.element, attribute_tag, nsmap=self._xml_document.nsmap) child.attrib['name'] = key if value is None: child.attrib['nil'] = 'true' else: child.text = value def __contains__(self, key): return key in self._attributes def __iter__(self): return iter(self._attributes) def __len__(self): return len(self._attributes) def __getitem__(self, key): return self._attributes[key] def __setitem__(self, key, value): if self._attributes.get(key, Null) == value: return self._attributes[key] = value self.__dirty__ = True def __delitem__(self, key): del self._attributes[key] self.__dirty__ = True def clear(self): if self._attributes: self._attributes.clear() self.__dirty__ = True def get(self, key, default=None): return self._attributes.get(key, default) def has_key(self, key): return key in self._attributes def items(self): - return self._attributes.items() + return list(self._attributes.items()) def iteritems(self): - return self._attributes.iteritems() + return iter(list(self._attributes.items())) def iterkeys(self): - return self._attributes.iterkeys() + return iter(list(self._attributes.keys())) def itervalues(self): - return self._attributes.itervalues() + return iter(list(self._attributes.values())) def keys(self): - return self._attributes.keys() + return list(self._attributes.keys()) def pop(self, key, *args): value = self._attributes.pop(key, *args) if not args or value is not args[0]: self.__dirty__ = True return value def popitem(self): value = self._attributes.popitem() self.__dirty__ = True return value def setdefault(self, key, default=None): value = self._attributes.setdefault(key, default) if value is default: self.__dirty__ = True return value def update(self, iterable=(), **attributes): self._attributes.update(iterable, **attributes) if iterable or attributes: self.__dirty__ = True ResourceListsDocument.register_namespace(EntryAttributes._xml_namespace, prefix='agp-rl') Entry.register_extension('attributes', EntryAttributes) diff --git a/sipsimple/payloads/rlsnotify.py b/sipsimple/payloads/rlsnotify.py index ccc620e1..2986c40b 100644 --- a/sipsimple/payloads/rlsnotify.py +++ b/sipsimple/payloads/rlsnotify.py @@ -1,104 +1,104 @@ """Payload of the RLS notify messages.""" __all__ = ['RLSNotify'] import email from sipsimple.payloads import IterateItems, ParserError from sipsimple.payloads import rlmi, pidf from sipsimple.payloads import rpid; rpid # needs to be imported to register its namespace -class ResourceURI(unicode): +class ResourceURI(str): def __eq__(self, other): return super(ResourceURI, self).__eq__(other) or self.rpartition('sip:')[2] == other def __ne__(self, other): equal = self.__eq__(other) return NotImplemented if equal is NotImplemented else not equal class Resource(object): __prioritymap__ = dict(active=10, pending=20, terminated=30) def __init__(self, uri, name=None, state=None, reason=None, pidf_list=None): self.uri = ResourceURI(uri) self.name = name self.state = state self.reason = reason self.pidf_list = pidf_list or [] @classmethod def from_payload(cls, xml_element, payload_map): try: name = next(element for element in xml_element if isinstance(element, rlmi.Name)) except StopIteration: name = None instances = list(xml_element[rlmi.Instance, IterateItems]) if len(instances) == 0: state = None reason = None elif len(instances) == 1: instance = instances[0] state = instance.state reason = instance.reason else: instance = sorted(instances, key=lambda item: cls.__prioritymap__[item.state])[0] state = instance.state reason = instance.reason pidf_list = [] for instance in (instance for instance in instances if instance.cid is not None): try: payload = payload_map['<%s>' % instance.cid].get_payload() except KeyError: continue try: pidf_list.append(pidf.PIDFDocument.parse(payload)) except ParserError: pass return cls(xml_element.uri, name, state, reason, pidf_list) class RLSNotify(object): """The payload from RLS notify messages""" content_type = 'multipart/related' def __init__(self, uri, version, full_state, resources): self.uri = ResourceURI(uri) self.version = version self.full_state = full_state self.resources = resources def __iter__(self): return iter(self.resources) def __len__(self): return len(self.resources) @classmethod def parse(cls, payload): message = email.message_from_string(payload) if message.get_content_type() != cls.content_type: raise ParserError("expected multipart/related content, got %s" % message.get_content_type()) payloads = message.get_payload() if len(payloads) == 0: raise ParserError("multipart/related body contains no parts") payload_map = dict((payload['Content-ID'], payload) for payload in payloads if payload['Content-ID'] is not None) root_id = message.get_param('start') root_type = message.get_param('type', '').lower() if root_id is not None: try: root = payload_map[root_id] except KeyError: raise ParserError('cannot find root element') else: root = payloads[0] if root_type != rlmi.RLMIDocument.content_type != root.get_content_type(): raise ParserError("the multipart/related root element must be of type %s" % rlmi.RLMIDocument.content_type) rlmi_document = rlmi.RLMIDocument.parse(root.get_payload()) resources = [Resource.from_payload(xml_element, payload_map) for xml_element in rlmi_document[rlmi.Resource, IterateItems]] return cls(rlmi_document.uri, rlmi_document.version, rlmi_document.full_state, resources) diff --git a/sipsimple/payloads/rlsservices.py b/sipsimple/payloads/rlsservices.py index 9555aec2..1786e950 100644 --- a/sipsimple/payloads/rlsservices.py +++ b/sipsimple/payloads/rlsservices.py @@ -1,135 +1,135 @@ """RFC4826 compliant parser/builder for application/rls-services+xml documents.""" __all__ = ['namespace', 'RLSServicesDocument', 'Packages', 'ResourceList', 'List', 'Service', 'RLSServices'] from sipsimple.payloads import XMLListRootElement, XMLElement, XMLListElement, XMLStringElement, XMLAnyURIElement, XMLElementID, XMLElementChild, XMLElementChoiceChild from sipsimple.payloads import IterateIDs, IterateItems, All from sipsimple.payloads import resourcelists from sipsimple.payloads.datatypes import AnyURI namespace = 'urn:ietf:params:xml:ns:rls-services' class RLSServicesDocument(resourcelists.ResourceListsDocument): content_type = 'application/rls-services+xml' RLSServicesDocument.register_namespace(namespace, prefix=None, schema='rlsservices.xsd') ## Marker mixins class PackagesElement(object): pass ## Elements class Package(XMLStringElement): _xml_tag = 'package' _xml_namespace = namespace _xml_document = RLSServicesDocument class Packages(XMLListElement): _xml_tag = 'packages' _xml_namespace = namespace _xml_document = RLSServicesDocument _xml_children_order = {Package.qname: 0} _xml_item_type = (Package, PackagesElement) def __init__(self, packages=[]): XMLListElement.__init__(self) self.update(packages) def __iter__(self): - return (unicode(item) if type(item) is Package else item for item in super(Packages, self).__iter__()) + return (str(item) if type(item) is Package else item for item in super(Packages, self).__iter__()) def add(self, item): - if isinstance(item, basestring): + if isinstance(item, str): item = Package(item) super(Packages, self).add(item) def remove(self, item): - if isinstance(item, basestring): + if isinstance(item, str): package = Package(item) try: - item = (entry for entry in super(Packages, self).__iter__() if entry == package).next() + item = next((entry for entry in super(Packages, self).__iter__() if entry == package)) except StopIteration: raise KeyError(item) super(Packages, self).remove(item) class ResourceList(XMLAnyURIElement): _xml_tag = 'resource-list' _xml_namespace = namespace _xml_document = RLSServicesDocument # This is identical to the list element in resourcelists, except for the # namespace. We'll redefine the xml tag just for readability purposes. class List(resourcelists.List): _xml_tag = 'list' _xml_namespace = namespace _xml_document = RLSServicesDocument class Service(XMLElement): _xml_tag = 'service' _xml_namespace = namespace _xml_document = RLSServicesDocument _xml_children_order = {List.qname: 0, ResourceList.qname: 0, Packages.qname: 1} uri = XMLElementID('uri', type=AnyURI, required=True, test_equal=True) list = XMLElementChoiceChild('list', types=(ResourceList, List), required=True, test_equal=True) packages = XMLElementChild('packages', type=Packages, required=False, test_equal=True) def __init__(self, uri, list=None, packages=None): XMLElement.__init__(self) self.uri = uri self.list = list if list is not None else List() self.packages = packages if packages is not None else Packages() def __repr__(self): return '%s(%r, %r, %r)' % (self.__class__.__name__, self.uri, self.list, self.packages) class RLSServices(XMLListRootElement): _xml_tag = 'rls-services' _xml_namespace = namespace _xml_document = RLSServicesDocument _xml_children_order = {Service.qname: 0} _xml_item_type = Service def __init__(self, services=[]): XMLListRootElement.__init__(self) self.update(services) def __getitem__(self, key): if key is IterateIDs: - return self._xmlid_map[Service].iterkeys() + return iter(list(self._xmlid_map[Service].keys())) elif key is IterateItems: - return self._xmlid_map[Service].itervalues() + return iter(list(self._xmlid_map[Service].values())) else: return self._xmlid_map[Service][key] def __delitem__(self, key): if key is All: - for item in self._xmlid_map[Service].values(): + for item in list(self._xmlid_map[Service].values()): self.remove(item) else: self.remove(self._xmlid_map[Service][key]) def get(self, key, default=None): return self._xmlid_map[Service].get(key, default) diff --git a/sipsimple/payloads/rpid.py b/sipsimple/payloads/rpid.py index ddb3a8f9..ce600b10 100644 --- a/sipsimple/payloads/rpid.py +++ b/sipsimple/payloads/rpid.py @@ -1,712 +1,700 @@ """ RPID handling according to RFC4480 This module provides an extension to PIDF to support rich presence. """ __all__ = ['namespace', 'ActivityElement', 'MoodElement', 'PlaceTypeElement', 'PrivacyElement', 'RelationshipElement', 'ServiceClassElement', 'SphereElement', 'Note', 'Other', 'Activities', 'Mood', 'PlaceIs', 'AudioPlaceInformation', 'VideoPlaceInformation', 'TextPlaceInformation', 'PlaceType', 'AudioPrivacy', 'TextPrivacy', 'VideoPrivacy', 'Privacy', 'Relationship', 'ServiceClass', 'Sphere', 'StatusIcon', 'TimeOffset', 'UserInput', 'Class'] from lxml import etree from sipsimple.payloads import ValidationError, XMLElementType, XMLEmptyElementRegistryType, XMLAttribute, XMLElementChild, XMLStringChoiceChild from sipsimple.payloads import XMLElement, XMLEmptyElement, XMLStringElement, XMLLocalizedStringElement, XMLStringListElement from sipsimple.payloads.pidf import PIDFDocument, ServiceExtension, PersonExtension, DeviceExtension, Note, NoteMap, NoteList, Service, Person, Device from sipsimple.payloads.datatypes import UnsignedLong, DateTime, ID namespace = 'urn:ietf:params:xml:ns:pidf:rpid' PIDFDocument.register_namespace(namespace, prefix='rpid', schema='rpid.xsd') ## Marker mixins class ActivityElement(object): pass class MoodElement(object): pass class PlaceTypeElement(object): pass class PrivacyElement(object): pass class RelationshipElement(object): pass class ServiceClassElement(object): pass class SphereElement(object): pass ## Attribute value types class AudioPlaceValue(str): def __new__(cls, value): if value not in ('noisy', 'ok', 'quiet', 'unknown'): raise ValueError("illegal value for audio place-is") return str.__new__(cls, value) class VideoPlaceValue(str): def __new__(cls, value): if value not in ('toobright', 'ok', 'dark', 'unknown'): raise ValueError("illegal value for video place-is") return str.__new__(cls, value) class TextPlaceValue(str): def __new__(cls, value): if value not in ('uncomfortable', 'inappropriate', 'ok', 'unknown'): raise ValueError("illegal value for text place-is") return str.__new__(cls, value) class UserInputValue(str): def __new__(cls, value): if value not in ('active', 'idle'): raise ValueError("illegal value for user-input") return str.__new__(cls, value) ## Elements class RPIDNote(XMLLocalizedStringElement): _xml_tag = 'note' _xml_namespace = namespace _xml_document = PIDFDocument def __unicode__(self): return Note(self.value, self.lang) @classmethod def from_string(cls, value): if isinstance(value, Note): return cls(value, value.lang) - elif isinstance(value, basestring): + elif isinstance(value, str): return cls(value) else: raise ValueError("expected str/unicode instance, got %s instead" % value.__class__.__name__) class RPIDOther(XMLLocalizedStringElement): _xml_tag = 'other' _xml_namespace = namespace _xml_document = PIDFDocument def __unicode__(self): return Other(self.value, self.lang) @classmethod def from_string(cls, value): if isinstance(value, Other): return cls(value, value.lang) - elif isinstance(value, basestring): + elif isinstance(value, str): return cls(value) else: raise ValueError("expected str/unicode instance, got %s instead" % value.__class__.__name__) class Other(Note): pass -class ActivityRegistry(object): - __metaclass__ = XMLEmptyElementRegistryType - +class ActivityRegistry(object, metaclass=XMLEmptyElementRegistryType): _xml_namespace = namespace _xml_document = PIDFDocument names = ('appointment', 'away', 'breakfast', 'busy', 'dinner', 'holiday', 'in-transit', 'looking-for-work', 'meal', 'meeting', 'on-the-phone', 'performance', 'permanent-absence', 'playing', 'presentation', 'shopping', 'sleeping', 'spectator', 'steering', 'travel', 'tv', 'vacation', 'working', 'worship', 'unknown') class Activities(XMLStringListElement, PersonExtension): _xml_tag = 'activities' _xml_namespace = namespace _xml_document = PIDFDocument _xml_children_order = {RPIDNote.qname: 0} _xml_item_registry = ActivityRegistry _xml_item_other_type = RPIDOther _xml_item_extension_type = ActivityElement id = XMLAttribute('id', type=str, required=False, test_equal=True) since = XMLAttribute('since', xmlname='from', type=DateTime, required=False, test_equal=True) until = XMLAttribute('until', type=DateTime, required=False, test_equal=True) _note_map = NoteMap() def __init__(self, id=None, since=None, until=None, activities=[], notes=[]): XMLElement.__init__(self) self.id = id self.since = since self.until = until self.update(activities) self.notes.update(notes) @property def notes(self): return NoteList(self, RPIDNote) def __eq__(self, other): if isinstance(other, Activities): return super(Activities, self).__eq__(other) and self.notes == other.notes else: return NotImplemented def __repr__(self): return '%s(%r, %r, %r, %r, %r)' % (self.__class__.__name__, self.id, self.since, self.until, list(self), list(self.notes)) def _parse_element(self, element): super(Activities, self)._parse_element(element) self.notes._parse_element(element) def _build_element(self): super(Activities, self)._build_element() self.notes._build_element() def add(self, activity): - if isinstance(activity, basestring): + if isinstance(activity, str): if activity in self._xml_item_registry.names: activity = self._xml_item_registry.class_map[activity]() else: activity = self._xml_item_other_type.from_string(activity) unknown_activity = self._xml_item_registry.class_map['unknown']() - if activity == unknown_activity or unknown_activity in self._element_map.itervalues(): + if activity == unknown_activity or unknown_activity in iter(list(self._element_map.values())): self.clear() super(Activities, self).add(activity) def check_validity(self): if not self: raise ValidationError("Activity element must have at least one value") super(Activities, self).check_validity() Person.register_extension('activities', type=Activities) -class MoodRegistry(object): - __metaclass__ = XMLEmptyElementRegistryType - +class MoodRegistry(object, metaclass=XMLEmptyElementRegistryType): _xml_namespace = namespace _xml_document = PIDFDocument names = ('afraid', 'amazed', 'angry', 'annoyed', 'anxious', 'ashamed', 'bored', 'brave', 'calm', 'cold', 'confused', 'contended', 'cranky', 'curious', 'depressed', 'disappointed', 'disgusted', 'distracted', 'embarrassed', 'excited', 'flirtatious', 'frustrated', 'grumpy', 'guilty', 'happy', 'hot', 'humbled', 'humiliated', 'hungry', 'hurt', 'impressed', 'in_awe', 'in_love', 'indignant', 'interested', 'invisible', 'jealous', 'lonely', 'mean', 'moody', 'nervous', 'neutral', 'offended', 'playful', 'proud', 'relieved', 'remorseful', 'restless', 'sad', 'sarcastic', 'serious', 'shocked', 'shy', 'sick', 'sleepy', 'stressed', 'surprised', 'thirsty', 'worried', 'unknown') class Mood(XMLStringListElement, PersonExtension): _xml_tag = 'mood' _xml_namespace = namespace _xml_document = PIDFDocument _xml_extension_type = MoodElement _xml_children_order = {RPIDNote.qname: 0} _xml_item_registry = MoodRegistry _xml_item_other_type = RPIDOther _xml_item_extension_type = MoodElement id = XMLAttribute('id', type=str, required=False, test_equal=True) since = XMLAttribute('since', xmlname='from', type=DateTime, required=False, test_equal=True) until = XMLAttribute('until', type=DateTime, required=False, test_equal=True) _note_map = NoteMap() def __init__(self, id=None, since=None, until=None, moods=[], notes=[]): XMLElement.__init__(self) self.id = id self.since = since self.until = until self.update(moods) self.notes.update(notes) @property def notes(self): return NoteList(self, RPIDNote) def __eq__(self, other): if isinstance(other, Mood): return super(Mood, self).__eq__(other) and self.notes == other.notes else: return NotImplemented def __repr__(self): return '%s(%r, %r, %r, %r, %r)' % (self.__class__.__name__, self.id, self.since, self.until, list(self), list(self.notes)) def _parse_element(self, element): super(Mood, self)._parse_element(element) self.notes._parse_element(element) def _build_element(self): super(Mood, self)._build_element() self.notes._build_element() def add(self, mood): - if isinstance(mood, basestring): + if isinstance(mood, str): if mood in self._xml_item_registry.names: mood = self._xml_item_registry.class_map[mood]() else: mood = self._xml_item_other_type.from_string(mood) unknown_mood = self._xml_item_registry.class_map['unknown']() - if mood == unknown_mood or unknown_mood in self._element_map.itervalues(): + if mood == unknown_mood or unknown_mood in iter(list(self._element_map.values())): self.clear() super(Mood, self).add(mood) def check_validity(self): if not self: raise ValidationError("Mood element must have at least one value") super(Mood, self).check_validity() Person.register_extension('mood', type=Mood) class AudioPlaceInformation(XMLStringElement): _xml_tag = 'audio' _xml_namespace = namespace _xml_document = PIDFDocument _xml_value_type = AudioPlaceValue class VideoPlaceInformation(XMLStringElement): _xml_tag = 'video' _xml_namespace = namespace _xml_document = PIDFDocument _xml_value_type = VideoPlaceValue class TextPlaceInformation(XMLStringElement): _xml_tag = 'text' _xml_namespace = namespace _xml_document = PIDFDocument _xml_value_type = TextPlaceValue class PlaceIs(XMLElement, PersonExtension): _xml_tag = 'place-is' _xml_namespace = namespace _xml_document = PIDFDocument _xml_children_order = {RPIDNote.qname: 0, AudioPlaceInformation.qname: 1, VideoPlaceInformation.qname: 2, TextPlaceInformation.qname: 3} id = XMLAttribute('id', type=str, required=False, test_equal=True) since = XMLAttribute('since', xmlname='from', type=DateTime, required=False, test_equal=True) until = XMLAttribute('until', type=DateTime, required=False, test_equal=True) audio = XMLElementChild('audio', type=AudioPlaceInformation, required=False, test_equal=True) video = XMLElementChild('video', type=VideoPlaceInformation, required=False, test_equal=True) text = XMLElementChild('text', type=TextPlaceInformation, required=False, test_equal=True) _note_map = NoteMap() def __init__(self, id=None, since=None, until=None, audio=None, video=None, text=None, notes=[]): XMLElement.__init__(self) self.id = id self.since = since self.until = until self.audio = audio self.video = video self.text = text self.notes.update(notes) @property def notes(self): return NoteList(self, RPIDNote) def __eq__(self, other): if isinstance(other, PlaceIs): return super(PlaceIs, self).__eq__(other) and self.notes == other.notes else: return NotImplemented def __repr__(self): return '%s(%r, %r, %r, %r, %r, %r, %r)' % (self.__class__.__name__, self.id, self.since, self.until, self.audio, self.video, self.text, list(self.notes)) def _parse_element(self, element): self.notes._parse_element(element) def _build_element(self): self.notes._build_element() Person.register_extension('place_is', type=PlaceIs) class PlaceType(XMLElement, PersonExtension): _xml_tag = 'place-type' _xml_namespace = namespace _xml_document = PIDFDocument _xml_children_order = {RPIDNote.qname: 0} id = XMLAttribute('id', type=str, required=False, test_equal=True) since = XMLAttribute('since', xmlname='from', type=DateTime, required=False, test_equal=True) until = XMLAttribute('until', type=DateTime, required=False, test_equal=True) value = XMLStringChoiceChild('value', other_type=RPIDOther, extension_type=PlaceTypeElement) _note_map = NoteMap() def __init__(self, id=None, since=None, until=None, placetype=None, notes=[]): super(PlaceType, self).__init__() self.id = id self.since = since self.until = until self.value = placetype self.notes.update(notes) @property def notes(self): return NoteList(self, RPIDNote) def __eq__(self, other): if isinstance(other, PlaceType): return super(PlaceType, self).__eq__(other) and self.notes == other.notes else: return NotImplemented def __repr__(self): return '%s(%r, %r, %r, %r, %r)' % (self.__class__.__name__, self.id, self.since, self.until, self.value, list(self.notes)) def _parse_element(self, element): self.notes._parse_element(element) def _build_element(self): self.notes._build_element() Person.register_extension('place_type', type=PlaceType) class AudioPrivacy(XMLEmptyElement): _xml_tag = 'audio' _xml_namespace = namespace _xml_document = PIDFDocument def __init__(self, private=True): XMLEmptyElement.__init__(self) def __new__(cls, private=True): if not private: return None return XMLEmptyElement.__new__(cls) class TextPrivacy(XMLEmptyElement): _xml_tag = 'text' _xml_namespace = namespace _xml_document = PIDFDocument def __init__(self, private=True): XMLEmptyElement.__init__(self) def __new__(cls, private=True): if not private: return None return XMLEmptyElement.__new__(cls) class VideoPrivacy(XMLEmptyElement): _xml_tag = 'video' _xml_namespace = namespace _xml_document = PIDFDocument def __init__(self, private=True): XMLEmptyElement.__init__(self) def __new__(cls, private=True): if not private: return None return XMLEmptyElement.__new__(cls) class PrivacyType(XMLElementType): def __init__(cls, name, bases, dct): super(PrivacyType, cls).__init__(name, bases, dct) child_attributes = (getattr(cls, name) for name in dir(cls) if type(getattr(cls, name)) is XMLElementChild) cls._privacy_attributes = tuple(attr.name for attr in child_attributes if attr.name in ('audio', 'text', 'video') or issubclass(attr.type, PrivacyElement)) -class Privacy(XMLElement, PersonExtension): - __metaclass__ = PrivacyType - +class Privacy(XMLElement, PersonExtension, metaclass=PrivacyType): _xml_tag = 'privacy' _xml_namespace = namespace _xml_document = PIDFDocument _xml_children_order = {RPIDNote.qname: 0, AudioPrivacy.qname: 1, TextPrivacy.qname: 2, VideoPrivacy.qname: 3} id = XMLAttribute('id', type=str, required=False, test_equal=True) since = XMLAttribute('since', xmlname='from', type=DateTime, required=False, test_equal=True) until = XMLAttribute('until', type=DateTime, required=False, test_equal=True) audio = XMLElementChild('audio', type=AudioPrivacy, required=False, test_equal=True) text = XMLElementChild('text', type=TextPrivacy, required=False, test_equal=True) video = XMLElementChild('video', type=VideoPrivacy, required=False, test_equal=True) unknown = property(lambda self: all(getattr(self, name) is None for name in self._privacy_attributes)) _note_map = NoteMap() def __init__(self, id=None, since=None, until=None, notes=[], audio=False, text=False, video=False): super(Privacy, self).__init__() self.id = id self.since = since self.until = until self.audio = audio self.text = text self.video = video self.notes.update(notes) @property def notes(self): return NoteList(self, RPIDNote) def __eq__(self, other): if isinstance(other, Privacy): return super(Privacy, self).__eq__(other) and self.notes == other.notes else: return NotImplemented def __repr__(self): return '%s(%r, %r, %r, %r, %r, %r, %r)' % (self.__class__.__name__, self.id, self.since, self.until, list(self.notes), self.audio, self.text, self.video) def _parse_element(self, element): self.notes._parse_element(element) def _build_element(self): if self.unknown: if self.element.find('{%s}unknown' % self._xml_namespace) is None: etree.SubElement(self.element, '{%s}unknown' % self._xml_namespace, nsmap=self._xml_document.nsmap) else: unknown_element = self.element.find('{%s}unknown' % self._xml_namespace) if unknown_element is not None: self.element.remove(unknown_element) self.notes._build_element() Person.register_extension('privacy', type=Privacy) -class RelationshipRegistry(object): - __metaclass__ = XMLEmptyElementRegistryType - +class RelationshipRegistry(object, metaclass=XMLEmptyElementRegistryType): _xml_namespace = namespace _xml_document = PIDFDocument names = ('assistant', 'associate', 'family', 'friend', 'self', 'supervisor', 'unknown') class Relationship(XMLElement, ServiceExtension): _xml_tag = 'relationship' _xml_namespace = namespace _xml_document = PIDFDocument _xml_children_order = {RPIDNote: 0} value = XMLStringChoiceChild('value', registry=RelationshipRegistry, other_type=RPIDOther, extension_type=RelationshipElement) _note_map = NoteMap() def __init__(self, relationship='self', notes=[]): XMLElement.__init__(self) self.value = relationship self.notes.update(notes) @property def notes(self): return NoteList(self, RPIDNote) def __eq__(self, other): if isinstance(other, Relationship): return super(Relationship, self).__eq__(other) and self.notes == other.notes else: return NotImplemented def __repr__(self): return '%s(%r, %r)' % (self.__class__.__name__, self.value, list(self.notes)) def _parse_element(self, element): self.notes._parse_element(element) def _build_element(self): self.notes._build_element() Service.register_extension('relationship', type=Relationship) -class ServiceClassRegistry(object): - __metaclass__ = XMLEmptyElementRegistryType - +class ServiceClassRegistry(object, metaclass=XMLEmptyElementRegistryType): _xml_namespace = namespace _xml_document = PIDFDocument names = ('courier', 'electronic', 'freight', 'in-person', 'postal', 'unknown') class ServiceClass(XMLElement, ServiceExtension): _xml_tag = 'service-class' _xml_namespace = namespace _xml_document = PIDFDocument value = XMLStringChoiceChild('value', registry=ServiceClassRegistry, extension_type=ServiceClassElement) _note_map = NoteMap() def __init__(self, service_class=None, notes=[]): XMLElement.__init__(self) self.value = service_class self.notes.update(notes) @property def notes(self): return NoteList(self, RPIDNote) def __eq__(self, other): if isinstance(other, ServiceClass): return super(ServiceClass, self).__eq__(other) and self.notes == other.notes else: return NotImplemented def __repr__(self): return '%s(%r, %r)' % (self.__class__.__name__, self.value, list(self.notes)) def _parse_element(self, element): self.notes._parse_element(element) def _build_element(self): self.notes._build_element() Service.register_extension('service_class', type=ServiceClass) -class SphereRegistry(object): - __metaclass__ = XMLEmptyElementRegistryType - +class SphereRegistry(object, metaclass=XMLEmptyElementRegistryType): _xml_namespace = namespace _xml_document = PIDFDocument names = ('home', 'work', 'unknown') class Sphere(XMLElement, PersonExtension): _xml_tag = 'sphere' _xml_namespace = namespace _xml_document = PIDFDocument id = XMLAttribute('id', type=ID, required=False, test_equal=True) since = XMLAttribute('since', xmlname='from', type=DateTime, required=False, test_equal=True) until = XMLAttribute('until', type=DateTime, required=False, test_equal=True) value = XMLStringChoiceChild('value', registry=SphereRegistry, extension_type=SphereElement) def __init__(self, value=None, id=None, since=None, until=None): XMLElement.__init__(self) self.id = id self.since = since self.until = until self.value = value def __repr__(self): return '%s(%r, %r, %r, %r)' % (self.__class__.__name__, self.value, self.id, self.since, self.until) Person.register_extension('sphere', type=Sphere) class StatusIcon(XMLStringElement, ServiceExtension, PersonExtension): _xml_tag = 'status-icon' _xml_namespace = namespace _xml_document = PIDFDocument id = XMLAttribute('id', type=str, required=False, test_equal=True) since = XMLAttribute('since', xmlname='from', type=DateTime, required=False, test_equal=True) until = XMLAttribute('until', type=DateTime, required=False, test_equal=True) def __init__(self, value=None, id=None, since=None, until=None): XMLStringElement.__init__(self, value) self.id = id self.since = since self.until = until Person.register_extension('status_icon', type=StatusIcon) Service.register_extension('status_icon', type=StatusIcon) class TimeOffset(XMLStringElement, PersonExtension): _xml_tag = 'time-offset' _xml_namespace = namespace _xml_document = PIDFDocument id = XMLAttribute('id', type=str, required=False, test_equal=True) since = XMLAttribute('since', xmlname='from', type=DateTime, required=False, test_equal=True) until = XMLAttribute('until', type=DateTime, required=False, test_equal=True) description = XMLAttribute('description', type=str, required=False, test_equal=True) def __init__(self, value=None, id=None, since=None, until=None, description=None): if value is None: value = DateTime.now().utcoffset().seconds / 60 XMLStringElement.__init__(self, str(value)) self.id = id self.since = since self.until = until self.description = description def __int__(self): return int(self.value) Person.register_extension('time_offset', type=TimeOffset) class UserInput(XMLStringElement, ServiceExtension, PersonExtension, DeviceExtension): _xml_tag = 'user-input' _xml_namespace = namespace _xml_document = PIDFDocument _xml_value_type = UserInputValue id = XMLAttribute('id', type=str, required=False, test_equal=True) last_input = XMLAttribute('last_input', xmlname='last-input', type=DateTime, required=False, test_equal=True) idle_threshold = XMLAttribute('idle_threshold', xmlname='idle-threshold', type=UnsignedLong, required=False, test_equal=True) def __init__(self, value='active', id=None, last_input=None, idle_threshold=None): XMLStringElement.__init__(self, value) self.id = id self.last_input = last_input self.idle_threshold = idle_threshold Service.register_extension('user_input', type=UserInput) Person.register_extension('user_input', type=UserInput) Device.register_extension('user_input', type=UserInput) class Class(XMLStringElement, ServiceExtension, PersonExtension, DeviceExtension): _xml_tag = 'class' _xml_namespace = namespace _xml_document = PIDFDocument Service.register_extension('rpid_class', type=Class) Person.register_extension('rpid_class', type=Class) Device.register_extension('rpid_class', type=Class) diff --git a/sipsimple/payloads/watcherinfo.py b/sipsimple/payloads/watcherinfo.py index e0eeda8e..929ddb54 100644 --- a/sipsimple/payloads/watcherinfo.py +++ b/sipsimple/payloads/watcherinfo.py @@ -1,214 +1,214 @@ """Parses application/watcherinfo+xml documents according to RFC3857 and RFC3858.""" __all__ = ['namespace', 'NeedFullUpdateError', 'WatcherInfoDocument', 'Watcher', 'WatcherList', 'WatcherInfo'] from sipsimple.payloads import XMLDocument, XMLAnyURIElement, XMLListElement, XMLListRootElement, XMLElementID, XMLAttribute from sipsimple.payloads import IterateIDs, IterateItems, All from sipsimple.payloads.datatypes import NonNegativeInteger, UnsignedLong, SIPURI namespace = 'urn:ietf:params:xml:ns:watcherinfo' class NeedFullUpdateError(Exception): pass class WatcherInfoDocument(XMLDocument): content_type = 'application/watcherinfo+xml' WatcherInfoDocument.register_namespace(namespace, prefix=None, schema='watcherinfo.xsd') ## Attribute value types class WatcherStatus(str): def __new__(cls, value): if value not in ('pending', 'active', 'waiting', 'terminated'): raise ValueError('illegal status value for watcher') return str.__new__(cls, value) class WatcherEvent(str): def __new__(cls, value): if value not in ('subscribe', 'approved', 'deactivated', 'probation', 'rejected', 'timeout', 'giveup', 'noresource'): raise ValueError('illegal event value for watcher') return str.__new__(cls, value) class WatcherInfoState(str): def __new__(cls, value): if value not in ('full', 'partial'): raise ValueError('illegal state value for watcherinfo') return str.__new__(cls, value) ## XMLElements class Watcher(XMLAnyURIElement): """ Definition for a watcher in a watcherinfo document Provides the attributes: * id * status * event * display_name * expiration * duration * sipuri Can be transformed to a string with the format DISPLAY_NAME . """ _xml_tag = 'watcher' _xml_namespace = namespace _xml_document = WatcherInfoDocument _xml_value_type = SIPURI id = XMLElementID('id', type=str, required=True, test_equal=True) status = XMLAttribute('status', type=WatcherStatus, required=True, test_equal=True) event = XMLAttribute('event', type=WatcherEvent, required=True, test_equal=True) display_name = XMLAttribute('display_name', xmlname='display-name', type=str, required=False, test_equal=True) expiration = XMLAttribute('expiration', type=UnsignedLong, required=False, test_equal=False) duration = XMLAttribute('duration', xmlname='duration-subscribed', type=UnsignedLong, required=False, test_equal=False) sipuri = XMLAnyURIElement.value def __init__(self, sipuri, id, status, event, display_name=None, expiration=None, duration=None): XMLAnyURIElement.__init__(self) self.sipuri = sipuri self.id = id self.status = status self.event = event self.display_name = display_name self.expiration = expiration self.duration = duration def __repr__(self): return '%s(%r, %r, %r, %r, %r, %r, %r)' % (self.__class__.__name__, self.sipuri, self.id, self.status, self.event, self.display_name, self.expiration, self.duration) def __str__(self): return '"%s" <%s>' % (self.display_name, self.sipuri) if self.display_name else self.sipuri class WatcherList(XMLListElement): """ Definition for a list of watchers in a watcherinfo document It behaves like a list in that it can be indexed by a number, can be iterated and counted. It also provides the properties pending, active and terminated which are generators returning Watcher objects with the corresponding status. """ _xml_tag = 'watcher-list' _xml_namespace = namespace _xml_document = WatcherInfoDocument _xml_children_order = {Watcher.qname: 0} _xml_item_type = Watcher resource = XMLElementID('resource', type=SIPURI, required=True, test_equal=True) package = XMLAttribute('package', type=str, required=True, test_equal=True) def __init__(self, resource, package, watchers=[]): XMLListElement.__init__(self) self.resource = resource self.package = package self.update(watchers) def __repr__(self): return '%s(%r, %r, %r)' % (self.__class__.__name__, self.resource, self.package, list(self)) def __getitem__(self, key): if key is IterateIDs: - return self._xmlid_map[Watcher].iterkeys() + return iter(list(self._xmlid_map[Watcher].keys())) elif key is IterateItems: - return self._xmlid_map[Watcher].itervalues() + return iter(list(self._xmlid_map[Watcher].values())) else: return self._xmlid_map[Watcher][key] def __delitem__(self, key): if key is All: - for item in self._xmlid_map[Watcher].values(): + for item in list(self._xmlid_map[Watcher].values()): self.remove(item) else: self.remove(self._xmlid_map[Watcher][key]) def get(self, key, default=None): return self._xmlid_map[Watcher].get(key, default) pending = property(lambda self: (watcher for watcher in self if watcher.status == 'pending')) waiting = property(lambda self: (watcher for watcher in self if watcher.status == 'waiting')) active = property(lambda self: (watcher for watcher in self if watcher.status == 'active')) terminated = property(lambda self: (watcher for watcher in self if watcher.status == 'terminated')) class WatcherInfo(XMLListRootElement): """ Definition for watcher info: a list of WatcherList elements The user agent instantiates this class once it subscribes to a *.winfo event and calls its update() method with the application/watcherinfo+xml documents it receives via NOTIFY. The watchers can be accessed in two ways: 1. via the wlists property, which returns a list of WatcherList elements; 2. via the pending, active and terminated properties, which return dictionaries, mapping WatcherList objects to lists of Watcher objects. Since WatcherList objects can be compared for equality to SIP URI strings, representing the presentity to which the watchers have subscribed, the dictionaries can also be indexed by such strings. """ _xml_tag = 'watcherinfo' _xml_namespace = namespace _xml_document = WatcherInfoDocument _xml_children_order = {WatcherList.qname: 0} _xml_item_type = WatcherList version = XMLAttribute('version', type=NonNegativeInteger, required=True, test_equal=True) state = XMLAttribute('state', type=WatcherInfoState, required=True, test_equal=True) def __init__(self, version=0, state='full', wlists=[]): XMLListRootElement.__init__(self) self.version = version self.state = state self.update(wlists) def __repr__(self): return '%s(%r, %r, %r)' % (self.__class__.__name__, self.version, self.state, list(self)) def __getitem__(self, key): if key is IterateIDs: - return self._xmlid_map[WatcherList].iterkeys() + return iter(list(self._xmlid_map[WatcherList].keys())) elif key is IterateItems: - return self._xmlid_map[WatcherList].itervalues() + return iter(list(self._xmlid_map[WatcherList].values())) else: return self._xmlid_map[WatcherList][key] def __delitem__(self, key): if key is All: - for item in self._xmlid_map[WatcherList].values(): + for item in list(self._xmlid_map[WatcherList].values()): self.remove(item) else: self.remove(self._xmlid_map[WatcherList][key]) def get(self, key, default=None): return self._xmlid_map[WatcherList].get(key, default) - wlists = property(lambda self: self._element_map.values()) - pending = property(lambda self: dict((wlist, list(wlist.pending)) for wlist in self._element_map.itervalues())) - waiting = property(lambda self: dict((wlist, list(wlist.waiting)) for wlist in self._element_map.itervalues())) - active = property(lambda self: dict((wlist, list(wlist.active)) for wlist in self._element_map.itervalues())) - terminated = property(lambda self: dict((wlist, list(wlist.terminated)) for wlist in self._element_map.itervalues())) + wlists = property(lambda self: list(self._element_map.values())) + pending = property(lambda self: dict((wlist, list(wlist.pending)) for wlist in list(self._element_map.values()))) + waiting = property(lambda self: dict((wlist, list(wlist.waiting)) for wlist in list(self._element_map.values()))) + active = property(lambda self: dict((wlist, list(wlist.active)) for wlist in list(self._element_map.values()))) + terminated = property(lambda self: dict((wlist, list(wlist.terminated)) for wlist in list(self._element_map.values()))) diff --git a/sipsimple/payloads/xcapcaps.py b/sipsimple/payloads/xcapcaps.py index f7dfaa1c..0f370915 100644 --- a/sipsimple/payloads/xcapcaps.py +++ b/sipsimple/payloads/xcapcaps.py @@ -1,143 +1,143 @@ """Support for parsing and building xcap-caps documents, as defined by RFC4825.""" __all__ = ['XCAPCapabilitiesDocument', 'AUIDS', 'Extensions', 'Namespaces', 'XCAPCapabilities'] from sipsimple.payloads import XMLDocument, XMLElementChild, XMLListElement, XMLRootElement, XMLStringElement namespace = 'urn:ietf:params:xml:ns:xcap-caps' class XCAPCapabilitiesDocument(XMLDocument): content_type = 'application/xcap-caps+xml' XCAPCapabilitiesDocument.register_namespace(namespace, prefix=None, schema='xcap-caps.xsd') ## Elements class AUID(XMLStringElement): _xml_tag = 'auid' _xml_namespace = namespace _xml_document = XCAPCapabilitiesDocument class AUIDS(XMLListElement): _xml_tag = 'auids' _xml_namespace = namespace _xml_document = XCAPCapabilitiesDocument _xml_item_type = AUID def __init__(self, children=[]): XMLListElement.__init__(self) self.update(children) def __iter__(self): - return (unicode(item) for item in super(AUIDS, self).__iter__()) + return (str(item) for item in super(AUIDS, self).__iter__()) def add(self, item): - if isinstance(item, basestring): + if isinstance(item, str): item = AUID(item) super(AUIDS, self).add(item) def remove(self, item): - if isinstance(item, basestring): + if isinstance(item, str): try: - item = (entry for entry in super(AUIDS, self).__iter__() if entry == item).next() + item = next((entry for entry in super(AUIDS, self).__iter__() if entry == item)) except StopIteration: raise KeyError(item) super(AUIDS, self).remove(item) class Extension(XMLStringElement): _xml_tag = 'extension' _xml_namespace = namespace _xml_document = XCAPCapabilitiesDocument class Extensions(XMLListElement): _xml_tag = 'extensions' _xml_namespace = namespace _xml_document = XCAPCapabilitiesDocument _xml_item_type = Extension def __init__(self, children=[]): XMLListElement.__init__(self) self.update(children) def __iter__(self): - return (unicode(item) for item in super(Extensions, self).__iter__()) + return (str(item) for item in super(Extensions, self).__iter__()) def add(self, item): - if isinstance(item, basestring): + if isinstance(item, str): item = Extension(item) super(Extensions, self).add(item) def remove(self, item): - if isinstance(item, basestring): + if isinstance(item, str): try: - item = (entry for entry in super(Extensions, self).__iter__() if entry == item).next() + item = next((entry for entry in super(Extensions, self).__iter__() if entry == item)) except StopIteration: raise KeyError(item) super(Extensions, self).remove(item) class Namespace(XMLStringElement): _xml_tag = 'extension' _xml_namespace = namespace _xml_document = XCAPCapabilitiesDocument class Namespaces(XMLListElement): _xml_tag = 'namespaces' _xml_namespace = namespace _xml_document = XCAPCapabilitiesDocument _xml_item_type = Namespace def __init__(self, children=[]): XMLListElement.__init__(self) self.update(children) def __iter__(self): - return (unicode(item) for item in super(Namespaces, self).__iter__()) + return (str(item) for item in super(Namespaces, self).__iter__()) def add(self, item): - if isinstance(item, basestring): + if isinstance(item, str): item = Namespace(item) super(Namespaces, self).add(item) def remove(self, item): - if isinstance(item, basestring): + if isinstance(item, str): try: - item = (entry for entry in super(Namespaces, self).__iter__() if entry == item).next() + item = next((entry for entry in super(Namespaces, self).__iter__() if entry == item)) except StopIteration: raise KeyError(item) super(Namespaces, self).remove(item) class XCAPCapabilities(XMLRootElement): _xml_tag = 'xcap-caps' _xml_namespace = namespace _xml_document = XCAPCapabilitiesDocument _xml_children_order = {AUIDS.qname: 0, Extensions.qname: 1, Namespaces.qname: 2} auids = XMLElementChild('auids', type=AUIDS, required=True, test_equal=True) extensions = XMLElementChild('extensions', type=Extensions, required=False, test_equal=True) namespaces = XMLElementChild('namespaces', type=Namespaces, required=True, test_equal=True) def __init__(self, auids=[], extensions=[], namespaces=[]): XMLRootElement.__init__(self) self.auids = AUIDS(auids) self.extensions = Extensions(extensions) self.namespaces = Namespaces(namespaces) def __repr__(self): return '%s(%r, %r, %r)' % (self.__class__.__name__, self.auids, self.extensions, self.namespaces) diff --git a/sipsimple/session.py b/sipsimple/session.py index 1e74baf3..609447f6 100644 --- a/sipsimple/session.py +++ b/sipsimple/session.py @@ -1,2730 +1,2729 @@ """ Implements an asynchronous notification based mechanism for establishment, modification and termination of sessions using Session Initiation Protocol (SIP) standardized in RFC3261. """ -from __future__ import absolute_import + __all__ = ['Session', 'SessionManager'] import random -from threading import RLock +from .threading import RLock from time import time -from application.notification import IObserver, Notification, NotificationCenter, NotificationData -from application.python import Null, limit -from application.python.decorator import decorator, preserve_signature -from application.python.types import Singleton -from application.system import host +from .application.notification import IObserver, Notification, NotificationCenter, NotificationData +from .application.python import Null, limit +from .application.python.decorator import decorator, preserve_signature +from .application.python.types import Singleton +from .application.system import host from eventlib import api, coros, proc from twisted.internet import reactor from zope.interface import implements from sipsimple import log from sipsimple.account import AccountManager, BonjourAccount from sipsimple.configuration.settings import SIPSimpleSettings from sipsimple.core import DialogID, Engine, Invitation, Referral, Subscription, PJSIPError, SIPCoreError, SIPCoreInvalidStateError, SIPURI, sip_status_messages, sipfrag_re from sipsimple.core import ContactHeader, FromHeader, Header, ReasonHeader, ReferToHeader, ReplacesHeader, RouteHeader, ToHeader, WarningHeader from sipsimple.core import SDPConnection, SDPMediaStream, SDPSession from sipsimple.core import PublicGRUU, PublicGRUUIfAvailable, NoGRUU from sipsimple.lookup import DNSLookup, DNSLookupError from sipsimple.payloads import ParserError from sipsimple.payloads.conference import ConferenceDocument from sipsimple.streams import MediaStreamRegistry, InvalidStreamError, UnknownStreamError from sipsimple.threading import run_in_twisted_thread from sipsimple.threading.green import Command, run_in_green_thread from sipsimple.util import ISOTimestamp class InvitationDisconnectedError(Exception): def __init__(self, invitation, data): self.invitation = invitation self.data = data class MediaStreamDidNotInitializeError(Exception): def __init__(self, stream, data): self.stream = stream self.data = data class MediaStreamDidFailError(Exception): def __init__(self, stream, data): self.stream = stream self.data = data class SubscriptionError(Exception): def __init__(self, error, timeout, **attributes): self.error = error self.timeout = timeout self.attributes = attributes class SIPSubscriptionDidFail(Exception): def __init__(self, data): self.data = data class InterruptSubscription(Exception): pass class TerminateSubscription(Exception): pass class ReferralError(Exception): def __init__(self, error, code=0): self.error = error self.code = code class TerminateReferral(Exception): pass class SIPReferralDidFail(Exception): def __init__(self, data): self.data = data class IllegalStateError(RuntimeError): pass class IllegalDirectionError(RuntimeError): pass class SIPInvitationTransferDidFail(Exception): def __init__(self, data): self.data = data @decorator def transition_state(required_state, new_state): def state_transitioner(func): @preserve_signature(func) def wrapper(obj, *args, **kwargs): with obj._lock: if obj.state != required_state: raise IllegalStateError('cannot call %s in %s state' % (func.__name__, obj.state)) obj.state = new_state return func(obj, *args, **kwargs) return wrapper return state_transitioner @decorator def check_state(required_states): def state_checker(func): @preserve_signature(func) def wrapper(obj, *args, **kwargs): if obj.state not in required_states: raise IllegalStateError('cannot call %s in %s state' % (func.__name__, obj.state)) return func(obj, *args, **kwargs) return wrapper return state_checker @decorator def check_transfer_state(direction, state): def state_checker(func): @preserve_signature(func) def wrapper(obj, *args, **kwargs): if obj.transfer_handler.direction != direction: raise IllegalDirectionError('cannot transfer in %s direction' % obj.transfer_handler.direction) if obj.transfer_handler.state != state: raise IllegalStateError('cannot transfer in %s state' % obj.transfer_handler.state) return func(obj, *args, **kwargs) return wrapper return state_checker class AddParticipantOperation(object): pass class RemoveParticipantOperation(object): pass class ReferralHandler(object): implements(IObserver) def __init__(self, session, participant_uri, operation): self.participant_uri = participant_uri if not isinstance(self.participant_uri, SIPURI): if not self.participant_uri.startswith(('sip:', 'sips:')): self.participant_uri = 'sip:%s' % self.participant_uri try: self.participant_uri = SIPURI.parse(self.participant_uri) except SIPCoreError: notification_center = NotificationCenter() if operation is AddParticipantOperation: notification_center.post_notification('SIPConferenceDidNotAddParticipant', sender=session, data=NotificationData(participant=self.participant_uri, code=0, reason='invalid participant URI')) else: notification_center.post_notification('SIPConferenceDidNotRemoveParticipant', sender=session, data=NotificationData(participant=self.participant_uri, code=0, reason='invalid participant URI')) return self.session = session self.operation = operation self.active = False self.route = None self._channel = coros.queue() self._referral = None def start(self): notification_center = NotificationCenter() if not self.session.remote_focus: if self.operation is AddParticipantOperation: notification_center.post_notification('SIPConferenceDidNotAddParticipant', sender=self.session, data=NotificationData(participant=self.participant_uri, code=0, reason='remote endpoint is not a focus')) else: notification_center.post_notification('SIPConferenceDidNotRemoveParticipant', sender=self.session, data=NotificationData(participant=self.participant_uri, code=0, reason='remote endpoint is not a focus')) self.session = None return notification_center.add_observer(self, sender=self.session) notification_center.add_observer(self, name='NetworkConditionsDidChange') proc.spawn(self._run) def _run(self): notification_center = NotificationCenter() settings = SIPSimpleSettings() try: # Lookup routes account = self.session.account if account is BonjourAccount(): uri = SIPURI.new(self.session._invitation.remote_contact_header.uri) elif account.sip.outbound_proxy is not None and account.sip.outbound_proxy.transport in settings.sip.transport_list: uri = SIPURI(host=account.sip.outbound_proxy.host, port=account.sip.outbound_proxy.port, parameters={'transport': account.sip.outbound_proxy.transport}) elif account.sip.always_use_my_proxy: uri = SIPURI(host=account.id.domain) else: uri = SIPURI.new(self.session.remote_identity.uri) lookup = DNSLookup() try: routes = lookup.lookup_sip_proxy(uri, settings.sip.transport_list).wait() - except DNSLookupError, e: + except DNSLookupError as e: timeout = random.uniform(15, 30) raise ReferralError(error='DNS lookup failed: %s' % e) target_uri = SIPURI.new(self.session.remote_identity.uri) timeout = time() + 30 for route in routes: self.route = route remaining_time = timeout - time() if remaining_time > 0: try: contact_uri = account.contact[NoGRUU, route] except KeyError: continue refer_to_header = ReferToHeader(str(self.participant_uri)) refer_to_header.parameters['method'] = 'INVITE' if self.operation is AddParticipantOperation else 'BYE' referral = Referral(target_uri, FromHeader(account.uri, account.display_name), ToHeader(target_uri), refer_to_header, ContactHeader(contact_uri), RouteHeader(route.uri), account.credentials) notification_center.add_observer(self, sender=referral) try: referral.send_refer(timeout=limit(remaining_time, min=1, max=5)) except SIPCoreError: notification_center.remove_observer(self, sender=referral) timeout = 5 raise ReferralError(error='Internal error') self._referral = referral try: while True: notification = self._channel.wait() if notification.name == 'SIPReferralDidStart': break - except SIPReferralDidFail, e: + except SIPReferralDidFail as e: notification_center.remove_observer(self, sender=referral) self._referral = None if e.data.code in (403, 405): raise ReferralError(error=sip_status_messages[e.data.code], code=e.data.code) else: # Otherwise just try the next route continue else: break else: self.route = None raise ReferralError(error='No more routes to try') # At this point it is subscribed. Handle notifications and ending/failures. try: self.active = True while True: notification = self._channel.wait() if notification.name == 'SIPReferralGotNotify': if notification.data.event == 'refer' and notification.data.body: match = sipfrag_re.match(notification.data.body) if match: code = int(match.group('code')) reason = match.group('reason') if code/100 > 2: continue if self.operation is AddParticipantOperation: notification_center.post_notification('SIPConferenceGotAddParticipantProgress', sender=self.session, data=NotificationData(participant=self.participant_uri, code=code, reason=reason)) else: notification_center.post_notification('SIPConferenceGotRemoveParticipantProgress', sender=self.session, data=NotificationData(participant=self.participant_uri, code=code, reason=reason)) elif notification.name == 'SIPReferralDidEnd': break - except SIPReferralDidFail, e: + except SIPReferralDidFail as e: notification_center.remove_observer(self, sender=self._referral) raise ReferralError(error=e.data.reason, code=e.data.code) else: notification_center.remove_observer(self, sender=self._referral) if self.operation is AddParticipantOperation: notification_center.post_notification('SIPConferenceDidAddParticipant', sender=self.session, data=NotificationData(participant=self.participant_uri)) else: notification_center.post_notification('SIPConferenceDidRemoveParticipant', sender=self.session, data=NotificationData(participant=self.participant_uri)) finally: self.active = False except TerminateReferral: if self._referral is not None: try: self._referral.end(timeout=2) except SIPCoreError: pass else: try: while True: notification = self._channel.wait() if notification.name == 'SIPReferralDidEnd': break except SIPReferralDidFail: pass finally: notification_center.remove_observer(self, sender=self._referral) if self.operation is AddParticipantOperation: notification_center.post_notification('SIPConferenceDidNotAddParticipant', sender=self.session, data=NotificationData(participant=self.participant_uri, code=0, reason='error')) else: notification_center.post_notification('SIPConferenceDidNotRemoveParticipant', sender=self.session, data=NotificationData(participant=self.participant_uri, code=0, reason='error')) - except ReferralError, e: + except ReferralError as e: if self.operation is AddParticipantOperation: notification_center.post_notification('SIPConferenceDidNotAddParticipant', sender=self.session, data=NotificationData(participant=self.participant_uri, code=e.code, reason=e.error)) else: notification_center.post_notification('SIPConferenceDidNotRemoveParticipant', sender=self.session, data=NotificationData(participant=self.participant_uri, code=e.code, reason=e.error)) finally: notification_center.remove_observer(self, sender=self.session) notification_center.remove_observer(self, name='NetworkConditionsDidChange') self.session = None self._referral = None def _refresh(self): try: contact_header = ContactHeader(self.session.account.contact[NoGRUU, self.route]) except KeyError: pass else: try: self._referral.refresh(contact_header=contact_header, timeout=2) except (SIPCoreError, SIPCoreInvalidStateError): pass @run_in_twisted_thread def handle_notification(self, notification): handler = getattr(self, '_NH_%s' % notification.name, Null) handler(notification) def _NH_SIPReferralDidStart(self, notification): self._channel.send(notification) def _NH_SIPReferralDidEnd(self, notification): self._channel.send(notification) def _NH_SIPReferralDidFail(self, notification): self._channel.send_exception(SIPReferralDidFail(notification.data)) def _NH_SIPReferralGotNotify(self, notification): self._channel.send(notification) def _NH_SIPSessionDidFail(self, notification): self._channel.send_exception(TerminateReferral()) def _NH_SIPSessionWillEnd(self, notification): self._channel.send_exception(TerminateReferral()) def _NH_NetworkConditionsDidChange(self, notification): if self.active: self._refresh() class ConferenceHandler(object): implements(IObserver) def __init__(self, session): self.session = session self.active = False self.subscribed = False self._command_proc = None self._command_channel = coros.queue() self._data_channel = coros.queue() self._subscription = None self._subscription_proc = None self._subscription_timer = None notification_center = NotificationCenter() notification_center.add_observer(self, sender=self.session) notification_center.add_observer(self, name='NetworkConditionsDidChange') self._command_proc = proc.spawn(self._run) @run_in_green_thread def add_participant(self, participant_uri): referral_handler = ReferralHandler(self.session, participant_uri, AddParticipantOperation) referral_handler.start() @run_in_green_thread def remove_participant(self, participant_uri): referral_handler = ReferralHandler(self.session, participant_uri, RemoveParticipantOperation) referral_handler.start() def _run(self): while True: command = self._command_channel.wait() handler = getattr(self, '_CH_%s' % command.name) handler(command) def _activate(self): self.active = True command = Command('subscribe') self._command_channel.send(command) return command def _deactivate(self): self.active = False command = Command('unsubscribe') self._command_channel.send(command) return command def _resubscribe(self): command = Command('subscribe') self._command_channel.send(command) return command def _terminate(self): notification_center = NotificationCenter() notification_center.remove_observer(self, sender=self.session) notification_center.remove_observer(self, name='NetworkConditionsDidChange') self._deactivate() command = Command('terminate') self._command_channel.send(command) command.wait() self.session = None def _CH_subscribe(self, command): if self._subscription_timer is not None and self._subscription_timer.active(): self._subscription_timer.cancel() self._subscription_timer = None if self._subscription_proc is not None: subscription_proc = self._subscription_proc subscription_proc.kill(InterruptSubscription) subscription_proc.wait() self._subscription_proc = proc.spawn(self._subscription_handler, command) def _CH_unsubscribe(self, command): # Cancel any timer which would restart the subscription process if self._subscription_timer is not None and self._subscription_timer.active(): self._subscription_timer.cancel() self._subscription_timer = None if self._subscription_proc is not None: subscription_proc = self._subscription_proc subscription_proc.kill(TerminateSubscription) subscription_proc.wait() self._subscription_proc = None command.signal() def _CH_terminate(self, command): command.signal() raise proc.ProcExit() def _subscription_handler(self, command): notification_center = NotificationCenter() settings = SIPSimpleSettings() try: # Lookup routes account = self.session.account if account is BonjourAccount(): uri = SIPURI.new(self.session._invitation.remote_contact_header.uri) elif account.sip.outbound_proxy is not None and account.sip.outbound_proxy.transport in settings.sip.transport_list: uri = SIPURI(host=account.sip.outbound_proxy.host, port=account.sip.outbound_proxy.port, parameters={'transport': account.sip.outbound_proxy.transport}) elif account.sip.always_use_my_proxy: uri = SIPURI(host=account.id.domain) else: uri = SIPURI.new(self.session.remote_identity.uri) lookup = DNSLookup() try: routes = lookup.lookup_sip_proxy(uri, settings.sip.transport_list).wait() - except DNSLookupError, e: + except DNSLookupError as e: timeout = random.uniform(15, 30) raise SubscriptionError(error='DNS lookup failed: %s' % e, timeout=timeout) target_uri = SIPURI.new(self.session.remote_identity.uri) default_interval = 600 if account is BonjourAccount() else account.sip.subscribe_interval refresh_interval = getattr(command, 'refresh_interval', default_interval) timeout = time() + 30 for route in routes: remaining_time = timeout - time() if remaining_time > 0: try: contact_uri = account.contact[NoGRUU, route] except KeyError: continue subscription = Subscription(target_uri, FromHeader(account.uri, account.display_name), ToHeader(target_uri), ContactHeader(contact_uri), 'conference', RouteHeader(route.uri), credentials=account.credentials, refresh=refresh_interval) notification_center.add_observer(self, sender=subscription) try: subscription.subscribe(timeout=limit(remaining_time, min=1, max=5)) except SIPCoreError: notification_center.remove_observer(self, sender=subscription) timeout = 5 raise SubscriptionError(error='Internal error', timeout=timeout) self._subscription = subscription try: while True: notification = self._data_channel.wait() if notification.sender is subscription and notification.name == 'SIPSubscriptionDidStart': break - except SIPSubscriptionDidFail, e: + except SIPSubscriptionDidFail as e: notification_center.remove_observer(self, sender=subscription) self._subscription = None if e.data.code == 407: # Authentication failed, so retry the subscription in some time timeout = random.uniform(60, 120) raise SubscriptionError(error='Authentication failed', timeout=timeout) elif e.data.code == 423: # Get the value of the Min-Expires header timeout = random.uniform(60, 120) if e.data.min_expires is not None and e.data.min_expires > refresh_interval: raise SubscriptionError(error='Interval too short', timeout=timeout, min_expires=e.data.min_expires) else: raise SubscriptionError(error='Interval too short', timeout=timeout) elif e.data.code in (405, 406, 489, 1400): command.signal(e) return else: # Otherwise just try the next route continue else: self.subscribed = True command.signal() break else: # There are no more routes to try, reschedule the subscription timeout = random.uniform(60, 180) raise SubscriptionError(error='No more routes to try', timeout=timeout) # At this point it is subscribed. Handle notifications and ending/failures. try: while True: notification = self._data_channel.wait() if notification.sender is not self._subscription: continue if notification.name == 'SIPSubscriptionGotNotify': if notification.data.event == 'conference' and notification.data.body: try: conference_info = ConferenceDocument.parse(notification.data.body) except ParserError: pass else: notification_center.post_notification('SIPSessionGotConferenceInfo', sender=self.session, data=NotificationData(conference_info=conference_info)) elif notification.name == 'SIPSubscriptionDidEnd': break except SIPSubscriptionDidFail: self._command_channel.send(Command('subscribe')) notification_center.remove_observer(self, sender=self._subscription) - except InterruptSubscription, e: + except InterruptSubscription as e: if not self.subscribed: command.signal(e) if self._subscription is not None: notification_center.remove_observer(self, sender=self._subscription) try: self._subscription.end(timeout=2) except SIPCoreError: pass - except TerminateSubscription, e: + except TerminateSubscription as e: if not self.subscribed: command.signal(e) if self._subscription is not None: try: self._subscription.end(timeout=2) except SIPCoreError: pass else: try: while True: notification = self._data_channel.wait() if notification.sender is self._subscription and notification.name == 'SIPSubscriptionDidEnd': break except SIPSubscriptionDidFail: pass finally: notification_center.remove_observer(self, sender=self._subscription) - except SubscriptionError, e: + except SubscriptionError as e: if 'min_expires' in e.attributes: command = Command('subscribe', command.event, refresh_interval=e.attributes['min_expires']) else: command = Command('subscribe', command.event) self._subscription_timer = reactor.callLater(e.timeout, self._command_channel.send, command) finally: self.subscribed = False self._subscription = None self._subscription_proc = None @run_in_twisted_thread def handle_notification(self, notification): handler = getattr(self, '_NH_%s' % notification.name, Null) handler(notification) def _NH_SIPSubscriptionDidStart(self, notification): self._data_channel.send(notification) def _NH_SIPSubscriptionDidEnd(self, notification): self._data_channel.send(notification) def _NH_SIPSubscriptionDidFail(self, notification): self._data_channel.send_exception(SIPSubscriptionDidFail(notification.data)) def _NH_SIPSubscriptionGotNotify(self, notification): self._data_channel.send(notification) def _NH_SIPSessionDidStart(self, notification): if self.session.remote_focus: self._activate() @run_in_green_thread def _NH_SIPSessionDidFail(self, notification): self._terminate() @run_in_green_thread def _NH_SIPSessionDidEnd(self, notification): self._terminate() def _NH_SIPSessionDidRenegotiateStreams(self, notification): if self.session.remote_focus and not self.active: self._activate() elif not self.session.remote_focus and self.active: self._deactivate() def _NH_NetworkConditionsDidChange(self, notification): if self.active: self._resubscribe() class TransferInfo(object): def __init__(self, referred_by=None, replaced_dialog_id=None): self.referred_by = referred_by self.replaced_dialog_id = replaced_dialog_id class TransferHandler(object): implements(IObserver) def __init__(self, session): self.state = None self.direction = None self.new_session = None self.session = session notification_center = NotificationCenter() notification_center.add_observer(self, sender=self.session) notification_center.add_observer(self, sender=self.session._invitation) self._command_channel = coros.queue() self._data_channel = coros.queue() self._proc = proc.spawn(self._run) def _run(self): while True: command = self._command_channel.wait() handler = getattr(self, '_CH_%s' % command.name) handler(command) self.direction = None self.state = None def _CH_incoming_transfer(self, command): self.direction = 'incoming' notification_center = NotificationCenter() refer_to_hdr = command.data.headers.get('Refer-To') target = SIPURI.parse(refer_to_hdr.uri) referred_by_hdr = command.data.headers.get('Referred-By', None) if referred_by_hdr is not None: origin = referred_by_hdr.body else: origin = str(self.session.remote_identity.uri) try: while True: try: notification = self._data_channel.wait() except SIPInvitationTransferDidFail: self.state = 'failed' return else: if notification.name == 'SIPInvitationTransferDidStart': self.state = 'starting' refer_to_uri = SIPURI.new(target) refer_to_uri.headers = {} refer_to_uri.parameters = {} notification_center.post_notification('SIPSessionTransferNewIncoming', self.session, NotificationData(transfer_destination=refer_to_uri)) elif notification.name == 'SIPSessionTransferDidStart': break elif notification.name == 'SIPSessionTransferDidFail': self.state = 'failed' try: self.session._invitation.notify_transfer_progress(notification.data.code, notification.data.reason) except SIPCoreError: return while True: try: notification = self._data_channel.wait() except SIPInvitationTransferDidFail: return self.state = 'started' transfer_info = TransferInfo(referred_by=origin) try: replaces_hdr = target.headers.pop('Replaces') call_id, rest = replaces_hdr.split(';', 1) params = dict((item.split('=') for item in rest.split(';'))) to_tag = params.get('to-tag') from_tag = params.get('from-tag') except (KeyError, ValueError): pass else: transfer_info.replaced_dialog_id = DialogID(call_id, local_tag=from_tag, remote_tag=to_tag) settings = SIPSimpleSettings() account = self.session.account if account is BonjourAccount(): uri = target elif account.sip.outbound_proxy is not None and account.sip.outbound_proxy.transport in settings.sip.transport_list: uri = SIPURI(host=account.sip.outbound_proxy.host, port=account.sip.outbound_proxy.port, parameters={'transport': account.sip.outbound_proxy.transport}) elif account.sip.always_use_my_proxy: uri = SIPURI(host=account.id.domain) else: uri = target lookup = DNSLookup() try: routes = lookup.lookup_sip_proxy(uri, settings.sip.transport_list).wait() - except DNSLookupError, e: + except DNSLookupError as e: self.state = 'failed' notification_center.post_notification('SIPSessionTransferDidFail', sender=self.session, data=NotificationData(code=0, reason="DNS lookup failed: {}".format(e))) try: self.session._invitation.notify_transfer_progress(480) except SIPCoreError: return while True: try: self._data_channel.wait() except SIPInvitationTransferDidFail: return return self.new_session = Session(account) notification_center = NotificationCenter() notification_center.add_observer(self, sender=self.new_session) self.new_session.connect(ToHeader(target), routes=routes, streams=[MediaStreamRegistry.AudioStream()], transfer_info=transfer_info) while True: try: notification = self._data_channel.wait() except SIPInvitationTransferDidFail: return if notification.name == 'SIPInvitationTransferDidEnd': return except proc.ProcExit: if self.new_session is not None: notification_center.remove_observer(self, sender=self.new_session) self.new_session = None raise def _CH_outgoing_transfer(self, command): self.direction = 'outgoing' notification_center = NotificationCenter() self.state = 'starting' while True: try: notification = self._data_channel.wait() - except SIPInvitationTransferDidFail, e: + except SIPInvitationTransferDidFail as e: self.state = 'failed' notification_center.post_notification('SIPSessionTransferDidFail', sender=self.session, data=NotificationData(code=e.data.code, reason=e.data.reason)) return if notification.name == 'SIPInvitationTransferDidStart': self.state = 'started' notification_center.post_notification('SIPSessionTransferDidStart', sender=self.session) elif notification.name == 'SIPInvitationTransferDidEnd': self.state = 'ended' self.session.end() notification_center.post_notification('SIPSessionTransferDidEnd', sender=self.session) return def _terminate(self): notification_center = NotificationCenter() notification_center.remove_observer(self, sender=self.session._invitation) notification_center.remove_observer(self, sender=self.session) self._proc.kill() self._proc = None self._command_channel = None self._data_channel = None self.session = None @run_in_twisted_thread def handle_notification(self, notification): handler = getattr(self, '_NH_%s' % notification.name, Null) handler(notification) def _NH_SIPInvitationTransferNewIncoming(self, notification): self._command_channel.send(Command('incoming_transfer', data=notification.data)) def _NH_SIPInvitationTransferNewOutgoing(self, notification): self._command_channel.send(Command('outgoing_transfer', data=notification.data)) def _NH_SIPInvitationTransferDidStart(self, notification): self._data_channel.send(notification) def _NH_SIPInvitationTransferDidFail(self, notification): self._data_channel.send_exception(SIPInvitationTransferDidFail(notification.data)) def _NH_SIPInvitationTransferDidEnd(self, notification): self._data_channel.send(notification) def _NH_SIPInvitationTransferGotNotify(self, notification): if notification.data.event == 'refer' and notification.data.body: match = sipfrag_re.match(notification.data.body) if match: code = int(match.group('code')) reason = match.group('reason') notification.center.post_notification('SIPSessionTransferGotProgress', sender=self.session, data=NotificationData(code=code, reason=reason)) def _NH_SIPSessionTransferDidStart(self, notification): if notification.sender is self.session and self.state == 'starting': self._data_channel.send(notification) def _NH_SIPSessionTransferDidFail(self, notification): if notification.sender is self.session and self.state == 'starting': self._data_channel.send(notification) def _NH_SIPSessionGotRingIndication(self, notification): if notification.sender is self.new_session and self.session is not None: try: self.session._invitation.notify_transfer_progress(180) except SIPCoreError: pass def _NH_SIPSessionGotProvisionalResponse(self, notification): if notification.sender is self.new_session and self.session is not None: try: self.session._invitation.notify_transfer_progress(notification.data.code, notification.data.reason) except SIPCoreError: pass def _NH_SIPSessionDidStart(self, notification): if notification.sender is self.new_session: notification.center.remove_observer(self, sender=notification.sender) self.new_session = None if self.session is not None: notification.center.post_notification('SIPSessionTransferDidEnd', sender=self.session) if self.state == 'started': try: self.session._invitation.notify_transfer_progress(200) except SIPCoreError: pass self.state = 'ended' self.session.end() def _NH_SIPSessionDidEnd(self, notification): if notification.sender is self.new_session: # If any stream fails to start we won't get SIPSessionDidFail, we'll get here instead notification.center.remove_observer(self, sender=notification.sender) self.new_session = None if self.session is not None: notification.center.post_notification('SIPSessionTransferDidFail', sender=self.session, data=NotificationData(code=500, reason='internal error')) if self.state == 'started': try: self.session._invitation.notify_transfer_progress(500) except SIPCoreError: pass self.state = 'failed' else: self._terminate() def _NH_SIPSessionDidFail(self, notification): if notification.sender is self.new_session: notification.center.remove_observer(self, sender=notification.sender) self.new_session = None if self.session is not None: notification.center.post_notification('SIPSessionTransferDidFail', sender=self.session, data=NotificationData(code=notification.data.code or 500, reason=notification.data.reason)) if self.state == 'started': try: self.session._invitation.notify_transfer_progress(notification.data.code or 500, notification.data.reason) except SIPCoreError: pass self.state = 'failed' else: self._terminate() class OptionalTag(str): def __eq__(self, other): return other is None or super(OptionalTag, self).__eq__(other) def __ne__(self, other): return not self == other def __repr__(self): return '{}({})'.format(self.__class__.__name__, super(OptionalTag, self).__repr__()) class SessionReplaceHandler(object): implements(IObserver) def __init__(self, session): self.session = session def start(self): notification_center = NotificationCenter() notification_center.add_observer(self, sender=self.session) notification_center.add_observer(self, sender=self.session.replaced_session) def handle_notification(self, notification): handler = getattr(self, '_NH_%s' % notification.name, Null) handler(notification) def _NH_SIPSessionDidStart(self, notification): notification.center.remove_observer(self, sender=self.session) notification.center.remove_observer(self, sender=self.session.replaced_session) self.session.replaced_session.end() self.session.replaced_session = None self.session = None def _NH_SIPSessionDidFail(self, notification): if notification.sender is self.session: notification.center.remove_observer(self, sender=self.session) notification.center.remove_observer(self, sender=self.session.replaced_session) self.session.replaced_session = None self.session = None _NH_SIPSessionDidEnd = _NH_SIPSessionDidFail class Session(object): implements(IObserver) media_stream_timeout = 15 short_reinvite_timeout = 5 def __init__(self, account): self.account = account self.direction = None self.end_time = None self.on_hold = False self.proposed_streams = None self.route = None self.state = None self.start_time = None self.streams = None self.transport = None self.local_focus = False self.remote_focus = False self.greenlet = None self.conference = None self.replaced_session = None self.transfer_handler = None self.transfer_info = None self._channel = coros.queue() self._hold_in_progress = False self._invitation = None self._local_identity = None self._remote_identity = None self._lock = RLock() def init_incoming(self, invitation, data): notification_center = NotificationCenter() remote_sdp = invitation.sdp.proposed_remote self.proposed_streams = [] if remote_sdp: for index, media_stream in enumerate(remote_sdp.media): if media_stream.port != 0: for stream_type in MediaStreamRegistry: try: stream = stream_type.new_from_sdp(self, remote_sdp, index) except UnknownStreamError: continue except InvalidStreamError as e: log.error("Invalid stream: {}".format(e)) break except Exception as e: log.exception("Exception occurred while setting up stream from SDP: {}".format(e)) break else: stream.index = index self.proposed_streams.append(stream) break self.direction = 'incoming' self.state = 'incoming' self.transport = invitation.transport self._invitation = invitation self.conference = ConferenceHandler(self) self.transfer_handler = TransferHandler(self) if 'isfocus' in invitation.remote_contact_header.parameters: self.remote_focus = True if 'Referred-By' in data.headers or 'Replaces' in data.headers: self.transfer_info = TransferInfo() if 'Referred-By' in data.headers: self.transfer_info.referred_by = data.headers['Referred-By'].body if 'Replaces' in data.headers: replaces_header = data.headers.get('Replaces') # Because we only allow the remote tag to be optional, it can only match established dialogs and early outgoing dialogs, but not early incoming dialogs, # which according to RFC3891 should be rejected with 481 (which will happen automatically by never matching them). if replaces_header.early_only or replaces_header.from_tag == '0': replaced_dialog_id = DialogID(replaces_header.call_id, local_tag=replaces_header.to_tag, remote_tag=OptionalTag(replaces_header.from_tag)) else: replaced_dialog_id = DialogID(replaces_header.call_id, local_tag=replaces_header.to_tag, remote_tag=replaces_header.from_tag) session_manager = SessionManager() try: replaced_session = next(session for session in session_manager.sessions if session.dialog_id == replaced_dialog_id) except StopIteration: invitation.send_response(481) return else: # Any matched dialog at this point is either established, terminated or early outgoing. if replaced_session.state in ('terminating', 'terminated'): invitation.send_response(603) return elif replaced_session.dialog_id.remote_tag is not None and replaces_header.early_only: # The replaced dialog is established, but the early-only flag is set invitation.send_response(486) return self.replaced_session = replaced_session self.transfer_info.replaced_dialog_id = replaced_dialog_id replace_handler = SessionReplaceHandler(self) replace_handler.start() notification_center.add_observer(self, sender=invitation) notification_center.post_notification('SIPSessionNewIncoming', sender=self, data=NotificationData(streams=self.proposed_streams[:], headers=data.headers)) @transition_state(None, 'connecting') @run_in_green_thread def connect(self, to_header, routes, streams, is_focus=False, transfer_info=None, extra_headers=None): self.greenlet = api.getcurrent() notification_center = NotificationCenter() settings = SIPSimpleSettings() connected = False received_code = 0 received_reason = None unhandled_notifications = [] extra_headers = extra_headers or [] if {'to', 'from', 'via', 'contact', 'route', 'record-route'}.intersection(header.name.lower() for header in extra_headers): raise RuntimeError('invalid header in extra_headers: To, From, Via, Contact, Route and Record-Route headers are not allowed') self.direction = 'outgoing' self.proposed_streams = streams self.route = routes[0] self.transport = self.route.transport self.local_focus = is_focus self._invitation = Invitation() self._local_identity = FromHeader(self.account.uri, self.account.display_name) self._remote_identity = to_header self.conference = ConferenceHandler(self) self.transfer_handler = TransferHandler(self) self.transfer_info = transfer_info notification_center.add_observer(self, sender=self._invitation) notification_center.post_notification('SIPSessionNewOutgoing', sender=self, data=NotificationData(streams=streams[:])) for stream in self.proposed_streams: notification_center.add_observer(self, sender=stream) stream.initialize(self, direction='outgoing') try: wait_count = len(self.proposed_streams) while wait_count > 0: notification = self._channel.wait() if notification.name == 'MediaStreamDidInitialize': wait_count -= 1 try: contact_uri = self.account.contact[PublicGRUUIfAvailable, self.route] local_ip = host.outgoing_ip_for(self.route.address) if local_ip is None: raise ValueError("could not get outgoing IP address") - except (KeyError, ValueError), e: + except (KeyError, ValueError) as e: for stream in self.proposed_streams: notification_center.remove_observer(self, sender=stream) stream.deactivate() stream.end() self._fail(originator='local', code=480, reason=sip_status_messages[480], error=str(e)) return connection = SDPConnection(local_ip) local_sdp = SDPSession(local_ip, name=settings.user_agent) for index, stream in enumerate(self.proposed_streams): stream.index = index media = stream.get_local_media(remote_sdp=None, index=index) if media.connection is None or (media.connection is not None and not media.has_ice_attributes and not media.has_ice_candidates): media.connection = connection local_sdp.media.append(media) from_header = FromHeader(self.account.uri, self.account.display_name) route_header = RouteHeader(self.route.uri) contact_header = ContactHeader(contact_uri) if is_focus: contact_header.parameters['isfocus'] = None if self.transfer_info is not None: if self.transfer_info.referred_by is not None: extra_headers.append(Header('Referred-By', self.transfer_info.referred_by)) if self.transfer_info.replaced_dialog_id is not None: dialog_id = self.transfer_info.replaced_dialog_id extra_headers.append(ReplacesHeader(dialog_id.call_id, dialog_id.local_tag, dialog_id.remote_tag)) self._invitation.send_invite(to_header.uri, from_header, to_header, route_header, contact_header, local_sdp, self.account.credentials, extra_headers) try: with api.timeout(settings.sip.invite_timeout): while True: notification = self._channel.wait() if notification.name == 'SIPInvitationGotSDPUpdate': if notification.data.succeeded: local_sdp = notification.data.local_sdp remote_sdp = notification.data.remote_sdp break else: for stream in self.proposed_streams: notification_center.remove_observer(self, sender=stream) stream.deactivate() stream.end() self._fail(originator='remote', code=0, reason=None, error='SDP negotiation failed: %s' % notification.data.error) return elif notification.name == 'SIPInvitationChangedState': if notification.data.state == 'early': if notification.data.code == 180: notification_center.post_notification('SIPSessionGotRingIndication', self) notification_center.post_notification('SIPSessionGotProvisionalResponse', self, NotificationData(code=notification.data.code, reason=notification.data.reason)) elif notification.data.state == 'connecting': received_code = notification.data.code received_reason = notification.data.reason elif notification.data.state == 'connected': if not connected: connected = True notification_center.post_notification('SIPSessionDidProcessTransaction', self, NotificationData(originator='local', method='INVITE', code=received_code, reason=received_reason)) else: unhandled_notifications.append(notification) elif notification.data.state == 'disconnected': raise InvitationDisconnectedError(notification.sender, notification.data) except api.TimeoutError: self.end() return notification_center.post_notification('SIPSessionWillStart', sender=self) stream_map = dict((stream.index, stream) for stream in self.proposed_streams) for index, local_media in enumerate(local_sdp.media): remote_media = remote_sdp.media[index] stream = stream_map[index] if remote_media.port: # TODO: check if port is also 0 in local_sdp. In that case PJSIP disabled the stream because # negotiation failed. If there are more streams, however, the negotiation is considered successful as a # whole, so while we built a normal SDP, PJSIP modified it and sent it to the other side. That's kind io # OK, but we cannot really start the stream. -Saul stream.start(local_sdp, remote_sdp, index) else: notification_center.remove_observer(self, sender=stream) self.proposed_streams.remove(stream) del stream_map[stream.index] stream.deactivate() stream.end() removed_streams = [stream for stream in self.proposed_streams if stream.index >= len(local_sdp.media)] for stream in removed_streams: notification_center.remove_observer(self, sender=stream) self.proposed_streams.remove(stream) del stream_map[stream.index] stream.deactivate() stream.end() invitation_notifications = [] with api.timeout(self.media_stream_timeout): wait_count = len(self.proposed_streams) while wait_count > 0: notification = self._channel.wait() if notification.name == 'MediaStreamDidStart': wait_count -= 1 elif notification.name == 'SIPInvitationChangedState': invitation_notifications.append(notification) for notification in invitation_notifications: self._channel.send(notification) while not connected or self._channel: notification = self._channel.wait() if notification.name == 'SIPInvitationChangedState': if notification.data.state == 'early': if notification.data.code == 180: notification_center.post_notification('SIPSessionGotRingIndication', self) notification_center.post_notification('SIPSessionGotProvisionalResponse', self, NotificationData(code=notification.data.code, reason=notification.data.reason)) elif notification.data.state == 'connecting': received_code = notification.data.code received_reason = notification.data.reason elif notification.data.state == 'connected': if not connected: connected = True notification_center.post_notification('SIPSessionDidProcessTransaction', self, NotificationData(originator='local', method='INVITE', code=received_code, reason=received_reason)) else: unhandled_notifications.append(notification) elif notification.data.state == 'disconnected': raise InvitationDisconnectedError(notification.sender, notification.data) - except (MediaStreamDidNotInitializeError, MediaStreamDidFailError, api.TimeoutError), e: + except (MediaStreamDidNotInitializeError, MediaStreamDidFailError, api.TimeoutError) as e: for stream in self.proposed_streams: notification_center.remove_observer(self, sender=stream) stream.deactivate() stream.end() if isinstance(e, api.TimeoutError): error = 'media stream timed-out while starting' elif isinstance(e, MediaStreamDidNotInitializeError): error = 'media stream did not initialize: %s' % e.data.reason else: error = 'media stream failed: %s' % e.data.reason self._fail(originator='local', code=0, reason=None, error=error) - except InvitationDisconnectedError, e: + except InvitationDisconnectedError as e: notification_center.remove_observer(self, sender=self._invitation) for stream in self.proposed_streams: notification_center.remove_observer(self, sender=stream) stream.deactivate() stream.end() self.state = 'terminated' # As weird as it may sound, PJSIP accepts a BYE even without receiving a final response to the INVITE if e.data.prev_state in ('connecting', 'connected') or getattr(e.data, 'method', None) == 'BYE': notification_center.post_notification('SIPSessionWillEnd', self, NotificationData(originator=e.data.originator)) if e.data.originator == 'remote': notification_center.post_notification('SIPSessionDidProcessTransaction', self, NotificationData(originator='remote', method=e.data.method, code=200, reason=sip_status_messages[200])) self.end_time = ISOTimestamp.now() notification_center.post_notification('SIPSessionDidEnd', self, NotificationData(originator=e.data.originator, end_reason=e.data.disconnect_reason)) else: if e.data.originator == 'remote': notification_center.post_notification('SIPSessionDidProcessTransaction', self, NotificationData(originator='local', method='INVITE', code=e.data.code, reason=e.data.reason)) code = e.data.code reason = e.data.reason elif e.data.disconnect_reason == 'timeout': code = 408 reason = 'timeout' else: # TODO: we should know *exactly* when there are set -Saul code = getattr(e.data, 'code', 0) reason = getattr(e.data, 'reason', 'Session disconnected') if e.data.originator == 'remote' and code // 100 == 3: redirect_identities = e.data.headers.get('Contact', []) else: redirect_identities = None notification_center.post_notification('SIPSessionDidFail', self, NotificationData(originator=e.data.originator, code=code, reason=reason, failure_reason=e.data.disconnect_reason, redirect_identities=redirect_identities)) self.greenlet = None - except SIPCoreError, e: + except SIPCoreError as e: for stream in self.proposed_streams: notification_center.remove_observer(self, sender=stream) stream.deactivate() stream.end() self._fail(originator='local', code=0, reason=None, error='SIP core error: %s' % str(e)) else: self.greenlet = None self.state = 'connected' self.streams = self.proposed_streams self.proposed_streams = None self.start_time = ISOTimestamp.now() any_stream_ice = any(getattr(stream, 'ice_active', False) for stream in self.streams) if any_stream_ice: self._reinvite_after_ice() notification_center.post_notification('SIPSessionDidStart', self, NotificationData(streams=self.streams[:])) for notification in unhandled_notifications: self.handle_notification(notification) if self._hold_in_progress: self._send_hold() def _reinvite_after_ice(self): # This function does not do any error checking, it's designed to be called at the end of connect and add_stream self.state = 'sending_proposal' self.greenlet = api.getcurrent() notification_center = NotificationCenter() local_sdp = SDPSession.new(self._invitation.sdp.active_local) local_sdp.version += 1 for index, stream in enumerate(self.streams): local_sdp.media[index] = stream.get_local_media(remote_sdp=None, index=index) self._invitation.send_reinvite(sdp=local_sdp) received_invitation_state = False received_sdp_update = False try: with api.timeout(self.short_reinvite_timeout): while not received_invitation_state or not received_sdp_update: notification = self._channel.wait() if notification.name == 'SIPInvitationGotSDPUpdate': received_sdp_update = True if notification.data.succeeded: local_sdp = notification.data.local_sdp remote_sdp = notification.data.remote_sdp for index, stream in enumerate(self.streams): stream.update(local_sdp, remote_sdp, index) else: return elif notification.name == 'SIPInvitationChangedState': if notification.data.state == 'connected' and notification.data.sub_state == 'normal': received_invitation_state = True notification_center.post_notification('SIPSessionDidProcessTransaction', self, NotificationData(originator='local', method='INVITE', code=notification.data.code, reason=notification.data.reason)) elif notification.data.state == 'disconnected': self.end() return except Exception: pass finally: self.state = 'connected' self.greenlet = None @check_state(['incoming', 'received_proposal']) @run_in_green_thread def send_ring_indication(self): try: self._invitation.send_response(180) except SIPCoreInvalidStateError: pass # The INVITE session might have already been cancelled; ignore the error @transition_state('incoming', 'accepting') @run_in_green_thread def accept(self, streams, is_focus=False, extra_headers=None): self.greenlet = api.getcurrent() notification_center = NotificationCenter() settings = SIPSimpleSettings() self.local_focus = is_focus connected = False unhandled_notifications = [] extra_headers = extra_headers or [] if {'to', 'from', 'via', 'contact', 'route', 'record-route'}.intersection(header.name.lower() for header in extra_headers): raise RuntimeError('invalid header in extra_headers: To, From, Via, Contact, Route and Record-Route headers are not allowed') if self.proposed_streams: for stream in self.proposed_streams: if stream in streams: notification_center.add_observer(self, sender=stream) stream.initialize(self, direction='incoming') else: for index, stream in enumerate(streams): notification_center.add_observer(self, sender=stream) stream.index = index stream.initialize(self, direction='outgoing') self.proposed_streams = streams wait_count = len(self.proposed_streams) try: while wait_count > 0: notification = self._channel.wait() if notification.name == 'MediaStreamDidInitialize': wait_count -= 1 remote_sdp = self._invitation.sdp.proposed_remote - sdp_connection = remote_sdp.connection or (media.connection for media in remote_sdp.media if media.connection is not None).next() + sdp_connection = remote_sdp.connection or next((media.connection for media in remote_sdp.media if media.connection is not None)) local_ip = host.outgoing_ip_for(sdp_connection.address) if sdp_connection.address != '0.0.0.0' else sdp_connection.address if local_ip is None: for stream in self.proposed_streams: notification_center.remove_observer(self, sender=stream) stream.deactivate() stream.end() self._fail(originator='local', code=500, reason=sip_status_messages[500], error='could not get local IP address') return connection = SDPConnection(local_ip) local_sdp = SDPSession(local_ip, name=settings.user_agent) if remote_sdp: stream_map = dict((stream.index, stream) for stream in self.proposed_streams) for index, media in enumerate(remote_sdp.media): stream = stream_map.get(index, None) if stream is not None: media = stream.get_local_media(remote_sdp=remote_sdp, index=index) if not media.has_ice_attributes and not media.has_ice_candidates: media.connection = connection else: media = SDPMediaStream.new(media) media.connection = connection media.port = 0 media.attributes = [] media.bandwidth_info = [] local_sdp.media.append(media) else: for index, stream in enumerate(self.proposed_streams): stream.index = index media = stream.get_local_media(remote_sdp=None, index=index) if media.connection is None or (media.connection is not None and not media.has_ice_attributes and not media.has_ice_candidates): media.connection = connection local_sdp.media.append(media) contact_header = ContactHeader.new(self._invitation.local_contact_header) try: local_contact_uri = self.account.contact[PublicGRUU, self._invitation.transport] except KeyError: pass else: contact_header.uri = local_contact_uri if is_focus: contact_header.parameters['isfocus'] = None self._invitation.send_response(200, contact_header=contact_header, sdp=local_sdp, extra_headers=extra_headers) notification_center.post_notification('SIPSessionWillStart', sender=self) # Local and remote SDPs will be set after the 200 OK is sent while True: notification = self._channel.wait() if notification.name == 'SIPInvitationGotSDPUpdate': if notification.data.succeeded: local_sdp = notification.data.local_sdp remote_sdp = notification.data.remote_sdp break else: if not connected: # we could not have got a SIPInvitationGotSDPUpdate if we did not get an ACK connected = True notification_center.post_notification('SIPSessionDidProcessTransaction', self, NotificationData(originator='remote', method='INVITE', code=200, reason=sip_status_messages[200], ack_received=True)) for stream in self.proposed_streams: notification_center.remove_observer(self, sender=stream) stream.deactivate() stream.end() self._fail(originator='remote', code=0, reason=None, error='SDP negotiation failed: %s' % notification.data.error) return elif notification.name == 'SIPInvitationChangedState': if notification.data.state == 'connected': if not connected: connected = True notification_center.post_notification('SIPSessionDidProcessTransaction', self, NotificationData(originator='remote', method='INVITE', code=200, reason=sip_status_messages[200], ack_received=True)) elif notification.data.prev_state == 'connected': unhandled_notifications.append(notification) elif notification.data.state == 'disconnected': raise InvitationDisconnectedError(notification.sender, notification.data) wait_count = 0 stream_map = dict((stream.index, stream) for stream in self.proposed_streams) for index, local_media in enumerate(local_sdp.media): remote_media = remote_sdp.media[index] stream = stream_map.get(index, None) if stream is not None: if remote_media.port: wait_count += 1 stream.start(local_sdp, remote_sdp, index) else: notification_center.remove_observer(self, sender=stream) self.proposed_streams.remove(stream) del stream_map[stream.index] stream.deactivate() stream.end() removed_streams = [stream for stream in self.proposed_streams if stream.index >= len(local_sdp.media)] for stream in removed_streams: notification_center.remove_observer(self, sender=stream) self.proposed_streams.remove(stream) del stream_map[stream.index] stream.deactivate() stream.end() with api.timeout(self.media_stream_timeout): while wait_count > 0 or not connected or self._channel: notification = self._channel.wait() if notification.name == 'MediaStreamDidStart': wait_count -= 1 elif notification.name == 'SIPInvitationChangedState': if notification.data.state == 'connected': if not connected: connected = True notification_center.post_notification('SIPSessionDidProcessTransaction', self, NotificationData(originator='remote', method='INVITE', code=200, reason='OK', ack_received=True)) elif notification.data.prev_state == 'connected': unhandled_notifications.append(notification) elif notification.data.state == 'disconnected': raise InvitationDisconnectedError(notification.sender, notification.data) else: unhandled_notifications.append(notification) - except (MediaStreamDidNotInitializeError, MediaStreamDidFailError, api.TimeoutError), e: + except (MediaStreamDidNotInitializeError, MediaStreamDidFailError, api.TimeoutError) as e: if self._invitation.state == 'connecting': ack_received = False if isinstance(e, api.TimeoutError) and wait_count == 0 else 'unknown' # pjsip's invite session object does not inform us whether the ACK was received or not notification_center.post_notification('SIPSessionDidProcessTransaction', self, NotificationData(originator='remote', method='INVITE', code=200, reason='OK', ack_received=ack_received)) elif self._invitation.state == 'connected' and not connected: # we didn't yet get to process the SIPInvitationChangedState (state -> connected) notification notification_center.post_notification('SIPSessionDidProcessTransaction', self, NotificationData(originator='remote', method='INVITE', code=200, reason='OK', ack_received=True)) for stream in self.proposed_streams: notification_center.remove_observer(self, sender=stream) stream.deactivate() stream.end() reason_header = None if isinstance(e, api.TimeoutError): if wait_count > 0: error = 'media stream timed-out while starting' else: error = 'No ACK received' reason_header = ReasonHeader('SIP') reason_header.cause = 500 reason_header.text = 'Missing ACK' elif isinstance(e, MediaStreamDidNotInitializeError): error = 'media stream did not initialize: %s' % e.data.reason reason_header = ReasonHeader('SIP') reason_header.cause = 500 reason_header.text = 'media stream did not initialize' else: error = 'media stream failed: %s' % e.data.reason reason_header = ReasonHeader('SIP') reason_header.cause = 500 reason_header.text = 'media stream failed to start' self.start_time = ISOTimestamp.now() if self._invitation.state in ('incoming', 'early'): self._fail(originator='local', code=500, reason=sip_status_messages[500], error=error, reason_header=reason_header) else: self._fail(originator='local', code=0, reason=None, error=error, reason_header=reason_header) - except InvitationDisconnectedError, e: + except InvitationDisconnectedError as e: notification_center.remove_observer(self, sender=self._invitation) for stream in self.proposed_streams: notification_center.remove_observer(self, sender=stream) stream.deactivate() stream.end() self.state = 'terminated' if e.data.prev_state in ('incoming', 'early'): notification_center.post_notification('SIPSessionDidProcessTransaction', self, NotificationData(originator='remote', method='INVITE', code=487, reason='Session Cancelled', ack_received='unknown')) notification_center.post_notification('SIPSessionDidFail', self, NotificationData(originator='remote', code=487, reason='Session Cancelled', failure_reason=e.data.disconnect_reason, redirect_identities=None)) elif e.data.prev_state == 'connecting' and e.data.disconnect_reason == 'missing ACK': notification_center.post_notification('SIPSessionDidProcessTransaction', self, NotificationData(originator='remote', method='INVITE', code=200, reason='OK', ack_received=False)) notification_center.post_notification('SIPSessionDidFail', self, NotificationData(originator='local', code=200, reason=sip_status_messages[200], failure_reason=e.data.disconnect_reason, redirect_identities=None)) else: notification_center.post_notification('SIPSessionWillEnd', self, NotificationData(originator='remote')) notification_center.post_notification('SIPSessionDidProcessTransaction', self, NotificationData(originator='remote', method=getattr(e.data, 'method', 'INVITE'), code=200, reason='OK')) self.end_time = ISOTimestamp.now() notification_center.post_notification('SIPSessionDidEnd', self, NotificationData(originator='remote', end_reason=e.data.disconnect_reason)) self.greenlet = None except SIPCoreInvalidStateError: # the only reason for which this error can be thrown is if invitation.send_response was called after the INVITE session was cancelled by the remote party notification_center.remove_observer(self, sender=self._invitation) for stream in self.proposed_streams: notification_center.remove_observer(self, sender=stream) stream.deactivate() stream.end() self.greenlet = None self.state = 'terminated' notification_center.post_notification('SIPSessionDidProcessTransaction', self, NotificationData(originator='remote', method='INVITE', code=487, reason='Session Cancelled', ack_received='unknown')) notification_center.post_notification('SIPSessionDidFail', self, NotificationData(originator='remote', code=487, reason='Session Cancelled', failure_reason='user request', redirect_identities=None)) - except SIPCoreError, e: + except SIPCoreError as e: for stream in self.proposed_streams: notification_center.remove_observer(self, sender=stream) stream.deactivate() stream.end() self._fail(originator='local', code=500, reason=sip_status_messages[500], error='SIP core error: %s' % str(e)) else: self.greenlet = None self.state = 'connected' self.streams = self.proposed_streams self.proposed_streams = None self.start_time = ISOTimestamp.now() notification_center.post_notification('SIPSessionDidStart', self, NotificationData(streams=self.streams[:])) for notification in unhandled_notifications: self.handle_notification(notification) if self._hold_in_progress: self._send_hold() finally: self.greenlet = None @transition_state('incoming', 'terminating') @run_in_green_thread def reject(self, code=603, reason=None): self.greenlet = api.getcurrent() notification_center = NotificationCenter() try: self._invitation.send_response(code, reason) with api.timeout(1): while True: notification = self._channel.wait() if notification.name == 'SIPInvitationChangedState': if notification.data.state == 'disconnected': ack_received = notification.data.disconnect_reason != 'missing ACK' notification_center.post_notification('SIPSessionDidProcessTransaction', self, NotificationData(originator='remote', method='INVITE', code=code, reason=sip_status_messages[code], ack_received=ack_received)) break except SIPCoreInvalidStateError: # the only reason for which this error can be thrown is if invitation.send_response was called after the INVITE session was cancelled by the remote party self.greenlet = None - except SIPCoreError, e: + except SIPCoreError as e: self._fail(originator='local', code=500, reason=sip_status_messages[500], error='SIP core error: %s' % str(e)) except api.TimeoutError: notification_center.remove_observer(self, sender=self._invitation) self.greenlet = None self.state = 'terminated' notification_center.post_notification('SIPSessionDidProcessTransaction', self, NotificationData(originator='remote', method='INVITE', code=code, reason=sip_status_messages[code], ack_received=False)) notification_center.post_notification('SIPSessionDidFail', self, NotificationData(originator='local', code=code, reason=sip_status_messages[code], failure_reason='timeout', redirect_identities=None)) else: notification_center.remove_observer(self, sender=self._invitation) self.greenlet = None self.state = 'terminated' notification_center.post_notification('SIPSessionDidFail', self, NotificationData(originator='local', code=code, reason=sip_status_messages[code], failure_reason='user request', redirect_identities=None)) finally: self.greenlet = None @transition_state('received_proposal', 'accepting_proposal') @run_in_green_thread def accept_proposal(self, streams): self.greenlet = api.getcurrent() notification_center = NotificationCenter() unhandled_notifications = [] streams = [stream for stream in streams if stream in self.proposed_streams] for stream in streams: notification_center.add_observer(self, sender=stream) stream.initialize(self, direction='incoming') try: wait_count = len(streams) while wait_count > 0: notification = self._channel.wait() if notification.name == 'MediaStreamDidInitialize': wait_count -= 1 local_sdp = SDPSession.new(self._invitation.sdp.active_local) local_sdp.version += 1 remote_sdp = self._invitation.sdp.proposed_remote connection = SDPConnection(local_sdp.address) stream_map = dict((stream.index, stream) for stream in streams) for index, media in enumerate(remote_sdp.media): stream = stream_map.get(index, None) if stream is not None: media = stream.get_local_media(remote_sdp=remote_sdp, index=index) if not media.has_ice_attributes and not media.has_ice_candidates: media.connection = connection if index < len(local_sdp.media): local_sdp.media[index] = media else: local_sdp.media.append(media) elif index >= len(local_sdp.media): # actually == is sufficient media = SDPMediaStream.new(media) media.connection = connection media.port = 0 media.attributes = [] media.bandwidth_info = [] local_sdp.media.append(media) self._invitation.send_response(200, sdp=local_sdp) prev_on_hold_streams = set(stream for stream in self.streams if stream.hold_supported and stream.on_hold_by_remote) received_invitation_state = False received_sdp_update = False while not received_invitation_state or not received_sdp_update: notification = self._channel.wait() if notification.name == 'SIPInvitationGotSDPUpdate': received_sdp_update = True if notification.data.succeeded: local_sdp = notification.data.local_sdp remote_sdp = notification.data.remote_sdp for stream in self.streams: stream.update(local_sdp, remote_sdp, stream.index) else: self._fail_proposal(originator='remote', error='SDP negotiation failed: %s' % notification.data.error) return elif notification.name == 'SIPInvitationChangedState': if notification.data.state == 'connected' and notification.data.sub_state == 'normal': notification_center.post_notification('SIPSessionDidProcessTransaction', self, NotificationData(originator='remote', method='INVITE', code=200, reason=sip_status_messages[200], ack_received='unknown')) received_invitation_state = True elif notification.data.state == 'disconnected': raise InvitationDisconnectedError(notification.sender, notification.data) on_hold_streams = set(stream for stream in self.streams if stream.hold_supported and stream.on_hold_by_remote) if on_hold_streams != prev_on_hold_streams: hold_supported_streams = (stream for stream in self.streams if stream.hold_supported) notification_center.post_notification('SIPSessionDidChangeHoldState', self, NotificationData(originator='remote', on_hold=bool(on_hold_streams), partial=bool(on_hold_streams) and any(not stream.on_hold_by_remote for stream in hold_supported_streams))) for stream in streams: # TODO: check if port is 0 in local_sdp. In that case PJSIP disabled the stream because # negotiation failed. If there are more streams, however, the negotiation is considered successful as a # whole, so while we built a normal SDP, PJSIP modified it and sent it to the other side. That's kind of # OK, but we cannot really start the stream. -Saul stream.start(local_sdp, remote_sdp, stream.index) with api.timeout(self.media_stream_timeout): wait_count = len(streams) while wait_count > 0 or self._channel: notification = self._channel.wait() if notification.name == 'MediaStreamDidStart': wait_count -= 1 else: unhandled_notifications.append(notification) except api.TimeoutError: self._fail_proposal(originator='remote', error='media stream timed-out while starting') except MediaStreamDidNotInitializeError as e: self._fail_proposal(originator='remote', error='media stream did not initialize: {.data.reason}'.format(e)) except MediaStreamDidFailError as e: self._fail_proposal(originator='remote', error='media stream failed: {.data.reason}'.format(e)) - except InvitationDisconnectedError, e: + except InvitationDisconnectedError as e: self._fail_proposal(originator='remote', error='session ended') notification = Notification('SIPInvitationChangedState', e.invitation, e.data) notification.center = notification_center self.handle_notification(notification) - except SIPCoreError, e: + except SIPCoreError as e: self._fail_proposal(originator='remote', error='SIP core error: %s' % str(e)) else: proposed_streams = self.proposed_streams self.proposed_streams = None self.streams = self.streams + streams self.greenlet = None self.state = 'connected' notification_center.post_notification('SIPSessionProposalAccepted', self, NotificationData(originator='remote', accepted_streams=streams, proposed_streams=proposed_streams)) notification_center.post_notification('SIPSessionDidRenegotiateStreams', self, NotificationData(originator='remote', added_streams=streams, removed_streams=[])) for notification in unhandled_notifications: self.handle_notification(notification) if self._hold_in_progress: self._send_hold() finally: self.greenlet = None @transition_state('received_proposal', 'rejecting_proposal') @run_in_green_thread def reject_proposal(self, code=488, reason=None): self.greenlet = api.getcurrent() notification_center = NotificationCenter() try: self._invitation.send_response(code, reason) with api.timeout(1, None): while True: notification = self._channel.wait() if notification.name == 'SIPInvitationChangedState': if notification.data.state == 'connected' and notification.data.sub_state == 'normal': notification_center.post_notification('SIPSessionDidProcessTransaction', self, NotificationData(originator='remote', method='INVITE', code=code, reason=sip_status_messages[code], ack_received='unknown')) break - except SIPCoreError, e: + except SIPCoreError as e: self._fail_proposal(originator='remote', error='SIP core error: %s' % str(e)) else: proposed_streams = self.proposed_streams self.proposed_streams = None self.greenlet = None self.state = 'connected' notification_center.post_notification('SIPSessionProposalRejected', self, NotificationData(originator='remote', code=code, reason=sip_status_messages[code], proposed_streams=proposed_streams)) if self._hold_in_progress: self._send_hold() finally: self.greenlet = None def add_stream(self, stream): self.add_streams([stream]) @transition_state('connected', 'sending_proposal') @run_in_green_thread def add_streams(self, streams): streams = list(set(streams).difference(self.streams)) if not streams: self.state = 'connected' return self.greenlet = api.getcurrent() notification_center = NotificationCenter() settings = SIPSimpleSettings() unhandled_notifications = [] self.proposed_streams = streams for stream in self.proposed_streams: notification_center.add_observer(self, sender=stream) stream.initialize(self, direction='outgoing') try: wait_count = len(self.proposed_streams) while wait_count > 0: notification = self._channel.wait() if notification.name == 'MediaStreamDidInitialize': wait_count -= 1 elif notification.name == 'SIPInvitationChangedState': # This is actually the only reason for which this notification could be received if notification.data.state == 'connected' and notification.data.sub_state == 'received_proposal': self._fail_proposal(originator='local', error='received stream proposal') self.handle_notification(notification) return local_sdp = SDPSession.new(self._invitation.sdp.active_local) local_sdp.version += 1 for stream in self.proposed_streams: # Try to reuse a disabled media stream to avoid an ever-growing SDP try: index = next(index for index, media in enumerate(local_sdp.media) if media.port == 0) reuse_media = True except StopIteration: index = len(local_sdp.media) reuse_media = False stream.index = index media = stream.get_local_media(remote_sdp=None, index=index) if reuse_media: local_sdp.media[index] = media else: local_sdp.media.append(media) self._invitation.send_reinvite(sdp=local_sdp) notification_center.post_notification('SIPSessionNewProposal', sender=self, data=NotificationData(originator='local', proposed_streams=self.proposed_streams[:])) received_invitation_state = False received_sdp_update = False try: with api.timeout(settings.sip.invite_timeout): while not received_invitation_state or not received_sdp_update: notification = self._channel.wait() if notification.name == 'SIPInvitationGotSDPUpdate': received_sdp_update = True if notification.data.succeeded: local_sdp = notification.data.local_sdp remote_sdp = notification.data.remote_sdp for s in self.streams: s.update(local_sdp, remote_sdp, s.index) else: self._fail_proposal(originator='local', error='SDP negotiation failed: %s' % notification.data.error) return elif notification.name == 'SIPInvitationChangedState': if notification.data.state == 'connected' and notification.data.sub_state == 'normal': received_invitation_state = True notification_center.post_notification('SIPSessionDidProcessTransaction', self, NotificationData(originator='local', method='INVITE', code=notification.data.code, reason=notification.data.reason)) if notification.data.code >= 300: proposed_streams = self.proposed_streams for stream in proposed_streams: notification_center.remove_observer(self, sender=stream) stream.deactivate() stream.end() self.proposed_streams = None self.greenlet = None self.state = 'connected' notification_center.post_notification('SIPSessionProposalRejected', self, NotificationData(originator='local', code=notification.data.code, reason=notification.data.reason, proposed_streams=proposed_streams)) return elif notification.data.state == 'disconnected': raise InvitationDisconnectedError(notification.sender, notification.data) except api.TimeoutError: self.cancel_proposal() return accepted_streams = [] for stream in self.proposed_streams: try: remote_media = remote_sdp.media[stream.index] except IndexError: self._fail_proposal(originator='local', error='SDP media missing in answer') return else: if remote_media.port: stream.start(local_sdp, remote_sdp, stream.index) accepted_streams.append(stream) else: notification_center.remove_observer(self, sender=stream) stream.deactivate() stream.end() with api.timeout(self.media_stream_timeout): wait_count = len(accepted_streams) while wait_count > 0: notification = self._channel.wait() if notification.name == 'MediaStreamDidStart': wait_count -= 1 except api.TimeoutError: self._fail_proposal(originator='local', error='media stream timed-out while starting') except MediaStreamDidNotInitializeError as e: self._fail_proposal(originator='local', error='media stream did not initialize: {.data.reason}'.format(e)) except MediaStreamDidFailError as e: self._fail_proposal(originator='local', error='media stream failed: {.data.reason}'.format(e)) - except InvitationDisconnectedError, e: + except InvitationDisconnectedError as e: self._fail_proposal(originator='local', error='session ended') notification = Notification('SIPInvitationChangedState', e.invitation, e.data) notification.center = notification_center self.handle_notification(notification) - except SIPCoreError, e: + except SIPCoreError as e: self._fail_proposal(originator='local', error='SIP core error: %s' % str(e)) else: self.greenlet = None self.state = 'connected' self.streams += accepted_streams proposed_streams = self.proposed_streams self.proposed_streams = None any_stream_ice = any(getattr(stream, 'ice_active', False) for stream in accepted_streams) if any_stream_ice: self._reinvite_after_ice() notification_center.post_notification('SIPSessionProposalAccepted', self, NotificationData(originator='local', accepted_streams=accepted_streams, proposed_streams=proposed_streams)) notification_center.post_notification('SIPSessionDidRenegotiateStreams', self, NotificationData(originator='local', added_streams=accepted_streams, removed_streams=[])) for notification in unhandled_notifications: self.handle_notification(notification) if self._hold_in_progress: self._send_hold() finally: self.greenlet = None def remove_stream(self, stream): self.remove_streams([stream]) @transition_state('connected', 'sending_proposal') @run_in_green_thread def remove_streams(self, streams): streams = list(set(streams).intersection(self.streams)) if not streams: self.state = 'connected' return self.greenlet = api.getcurrent() notification_center = NotificationCenter() unhandled_notifications = [] try: local_sdp = SDPSession.new(self._invitation.sdp.active_local) local_sdp.version += 1 for stream in streams: notification_center.remove_observer(self, sender=stream) stream.deactivate() self.streams.remove(stream) media = local_sdp.media[stream.index] media.port = 0 media.attributes = [] media.bandwidth_info = [] self._invitation.send_reinvite(sdp=local_sdp) received_invitation_state = False received_sdp_update = False with api.timeout(self.short_reinvite_timeout): while not received_invitation_state or not received_sdp_update: notification = self._channel.wait() if notification.name == 'SIPInvitationGotSDPUpdate': received_sdp_update = True if notification.data.succeeded: local_sdp = notification.data.local_sdp remote_sdp = notification.data.remote_sdp for s in self.streams: s.update(local_sdp, remote_sdp, s.index) elif notification.name == 'SIPInvitationChangedState': if notification.data.state == 'connected' and notification.data.sub_state == 'normal': received_invitation_state = True notification_center.post_notification('SIPSessionDidProcessTransaction', self, NotificationData(originator='local', method='INVITE', code=notification.data.code, reason=notification.data.reason)) if not (200 <= notification.data.code < 300): break elif notification.data.state == 'disconnected': raise InvitationDisconnectedError(notification.sender, notification.data) - except InvitationDisconnectedError, e: + except InvitationDisconnectedError as e: for stream in streams: stream.end() self.greenlet = None notification = Notification('SIPInvitationChangedState', e.invitation, e.data) notification.center = notification_center self.handle_notification(notification) except (api.TimeoutError, MediaStreamDidFailError, SIPCoreError): for stream in streams: stream.end() self.end() else: for stream in streams: stream.end() self.greenlet = None self.state = 'connected' notification_center.post_notification('SIPSessionDidRenegotiateStreams', self, NotificationData(originator='local', added_streams=[], removed_streams=streams)) for notification in unhandled_notifications: self.handle_notification(notification) if self._hold_in_progress: self._send_hold() finally: self.greenlet = None @transition_state('sending_proposal', 'cancelling_proposal') @run_in_green_thread def cancel_proposal(self): if self.greenlet is not None: api.kill(self.greenlet, api.GreenletExit()) self.greenlet = api.getcurrent() notification_center = NotificationCenter() try: self._invitation.cancel_reinvite() while True: try: notification = self._channel.wait() except MediaStreamDidFailError: continue if notification.name == 'SIPInvitationChangedState': if notification.data.state == 'connected' and notification.data.sub_state == 'normal': notification_center.post_notification('SIPSessionDidProcessTransaction', self, NotificationData(originator='remote', method='INVITE', code=notification.data.code, reason=notification.data.reason)) if notification.data.code == 487: proposed_streams = self.proposed_streams or [] for stream in proposed_streams: notification_center.remove_observer(self, sender=stream) stream.deactivate() stream.end() self.proposed_streams = None self.state = 'connected' notification_center.post_notification('SIPSessionProposalRejected', self, NotificationData(originator='local', code=notification.data.code, reason=notification.data.reason, proposed_streams=proposed_streams)) elif notification.data.code == 200: self.end() elif notification.data.state == 'disconnected': raise InvitationDisconnectedError(notification.sender, notification.data) break - except SIPCoreError, e: + except SIPCoreError as e: notification_center.post_notification('SIPSessionDidProcessTransaction', self, NotificationData(originator='local', code=0, reason=None, failure_reason='SIP core error: %s' % str(e), redirect_identities=None)) proposed_streams = self.proposed_streams or [] for stream in proposed_streams: notification_center.remove_observer(self, sender=stream) stream.deactivate() stream.end() self.proposed_streams = None self.greenlet = None self.state = 'connected' notification_center.post_notification('SIPSessionProposalRejected', self, NotificationData(originator='local', code=0, reason='SIP core error: %s' % str(e), proposed_streams=proposed_streams)) - except InvitationDisconnectedError, e: + except InvitationDisconnectedError as e: for stream in self.proposed_streams or []: notification_center.remove_observer(self, sender=stream) stream.deactivate() stream.end() self.proposed_streams = None self.greenlet = None notification = Notification('SIPInvitationChangedState', e.invitation, e.data) notification.center = notification_center self.handle_notification(notification) else: for stream in self.proposed_streams or []: notification_center.remove_observer(self, sender=stream) stream.deactivate() stream.end() self.proposed_streams = None self.greenlet = None self.state = 'connected' finally: self.greenlet = None if self._hold_in_progress: self._send_hold() @run_in_green_thread def hold(self): if self.on_hold or self._hold_in_progress: return self._hold_in_progress = True streams = (self.streams or []) + (self.proposed_streams or []) if not streams: return for stream in streams: stream.hold() if self.state == 'connected': self._send_hold() @run_in_green_thread def unhold(self): if not self.on_hold and not self._hold_in_progress: return self._hold_in_progress = False streams = (self.streams or []) + (self.proposed_streams or []) if not streams: return for stream in streams: stream.unhold() if self.state == 'connected': self._send_unhold() @run_in_green_thread def end(self): if self.state in (None, 'terminating', 'terminated'): return if self.greenlet is not None: api.kill(self.greenlet, api.GreenletExit()) self.greenlet = None notification_center = NotificationCenter() if self._invitation is None: # The invitation was not yet constructed self.state = 'terminated' notification_center.post_notification('SIPSessionDidFail', self, NotificationData(originator='local', code=487, reason='Session Cancelled', failure_reason='user request', redirect_identities=None)) return elif self._invitation.state is None: # The invitation was built but never sent streams = (self.streams or []) + (self.proposed_streams or []) for stream in streams[:]: try: notification_center.remove_observer(self, sender=stream) except KeyError: streams.remove(stream) else: stream.deactivate() stream.end() self.state = 'terminated' notification_center.post_notification('SIPSessionDidFail', self, NotificationData(originator='local', code=487, reason='Session Cancelled', failure_reason='user request', redirect_identities=None)) return invitation_state = self._invitation.state if invitation_state in ('disconnecting', 'disconnected'): return self.greenlet = api.getcurrent() self.state = 'terminating' if invitation_state == 'connected': notification_center.post_notification('SIPSessionWillEnd', self, NotificationData(originator='local')) streams = (self.streams or []) + (self.proposed_streams or []) for stream in streams[:]: try: notification_center.remove_observer(self, sender=stream) except KeyError: streams.remove(stream) else: stream.deactivate() cancelling = invitation_state != 'connected' and self.direction == 'outgoing' try: self._invitation.end(timeout=1) while True: try: notification = self._channel.wait() except MediaStreamDidFailError: continue if notification.name == 'SIPInvitationChangedState' and notification.data.state == 'disconnected': if notification.data.disconnect_reason in ('internal error', 'missing ACK'): pass elif notification.data.disconnect_reason == 'timeout': notification_center.post_notification('SIPSessionDidProcessTransaction', self, NotificationData(originator='local' if self.direction=='outgoing' else 'remote', method='INVITE', code=408, reason='Timeout')) elif cancelling: notification_center.post_notification('SIPSessionDidProcessTransaction', self, NotificationData(originator='local', method='INVITE', code=notification.data.code, reason=notification.data.reason)) elif hasattr(notification.data, 'method'): notification_center.post_notification('SIPSessionDidProcessTransaction', self, NotificationData(originator='remote', method=notification.data.method, code=200, reason=sip_status_messages[200])) elif notification.data.disconnect_reason == 'user request': notification_center.post_notification('SIPSessionDidProcessTransaction', self, NotificationData(originator='local', method='BYE', code=notification.data.code, reason=notification.data.reason)) break - except SIPCoreError, e: + except SIPCoreError as e: if cancelling: notification_center.post_notification('SIPSessionDidFail', self, NotificationData(originator='local', code=0, reason=None, failure_reason='SIP core error: %s' % str(e), redirect_identities=None)) else: self.end_time = ISOTimestamp.now() notification_center.post_notification('SIPSessionDidEnd', self, NotificationData(originator='local', end_reason='SIP core error: %s' % str(e))) - except InvitationDisconnectedError, e: + except InvitationDisconnectedError as e: # As it weird as it may sound, PJSIP accepts a BYE even without receiving a final response to the INVITE if e.data.prev_state == 'connected': if e.data.originator == 'remote': notification_center.post_notification('SIPSessionDidProcessTransaction', self, NotificationData(originator=e.data.originator, method=e.data.method, code=200, reason=sip_status_messages[200])) self.end_time = ISOTimestamp.now() notification_center.post_notification('SIPSessionDidEnd', self, NotificationData(originator=e.data.originator, end_reason=e.data.disconnect_reason)) elif getattr(e.data, 'method', None) == 'BYE' and e.data.originator == 'remote': notification_center.post_notification('SIPSessionDidProcessTransaction', self, NotificationData(originator=e.data.originator, method=e.data.method, code=200, reason=sip_status_messages[200])) notification_center.post_notification('SIPSessionDidFail', self, NotificationData(originator=e.data.originator, code=0, reason=None, failure_reason=e.data.disconnect_reason, redirect_identities=None)) else: if e.data.originator == 'remote': code = e.data.code reason = e.data.reason elif e.data.disconnect_reason == 'timeout': code = 408 reason = 'timeout' else: code = 0 reason = None if e.data.originator == 'remote' and code // 100 == 3: redirect_identities = e.data.headers.get('Contact', []) else: redirect_identities = None notification_center.post_notification('SIPSessionDidProcessTransaction', self, NotificationData(originator='local', method='INVITE', code=code, reason=reason)) notification_center.post_notification('SIPSessionDidFail', self, NotificationData(originator=e.data.originator, code=code, reason=reason, failure_reason=e.data.disconnect_reason, redirect_identities=redirect_identities)) else: if cancelling: notification_center.post_notification('SIPSessionDidFail', self, NotificationData(originator='local', code=487, reason='Session Cancelled', failure_reason='user request', redirect_identities=None)) else: self.end_time = ISOTimestamp.now() notification_center.post_notification('SIPSessionDidEnd', self, NotificationData(originator='local', end_reason='user request')) finally: for stream in streams: stream.end() notification_center.remove_observer(self, sender=self._invitation) self.greenlet = None self.state = 'terminated' @check_state(['connected']) @check_transfer_state(None, None) @run_in_twisted_thread def transfer(self, target_uri, replaced_session=None): notification_center = NotificationCenter() notification_center.post_notification('SIPSessionTransferNewOutgoing', self, NotificationData(transfer_destination=target_uri)) try: self._invitation.transfer(target_uri, replaced_session.dialog_id if replaced_session is not None else None) - except SIPCoreError, e: + except SIPCoreError as e: notification_center.post_notification('SIPSessionTransferDidFail', sender=self, data=NotificationData(code=500, reason=str(e))) @check_state(['connected', 'received_proposal', 'sending_proposal', 'accepting_proposal', 'rejecting_proposal', 'cancelling_proposal']) @check_transfer_state('incoming', 'starting') def accept_transfer(self): notification_center = NotificationCenter() notification_center.post_notification('SIPSessionTransferDidStart', sender=self) @check_state(['connected', 'received_proposal', 'sending_proposal', 'accepting_proposal', 'rejecting_proposal', 'cancelling_proposal']) @check_transfer_state('incoming', 'starting') def reject_transfer(self, code=603, reason=None): notification_center = NotificationCenter() notification_center.post_notification('SIPSessionTransferDidFail', self, NotificationData(code=code, reason=reason or sip_status_messages[code])) @property def dialog_id(self): return self._invitation.dialog_id if self._invitation is not None else None @property def local_identity(self): if self._invitation is not None and self._invitation.local_identity is not None: return self._invitation.local_identity else: return self._local_identity @property def peer_address(self): return self._invitation.peer_address if self._invitation is not None else None @property def remote_identity(self): if self._invitation is not None and self._invitation.remote_identity is not None: return self._invitation.remote_identity else: return self._remote_identity @property def remote_user_agent(self): return self._invitation.remote_user_agent if self._invitation is not None else None def _cancel_hold(self): notification_center = NotificationCenter() try: self._invitation.cancel_reinvite() while True: try: notification = self._channel.wait() except MediaStreamDidFailError: continue if notification.name == 'SIPInvitationChangedState': if notification.data.state == 'connected' and notification.data.sub_state == 'normal': notification_center.post_notification('SIPSessionDidProcessTransaction', self, NotificationData(originator='remote', method='INVITE', code=notification.data.code, reason=notification.data.reason)) if notification.data.code == 200: self.end() return False elif notification.data.state == 'disconnected': raise InvitationDisconnectedError(notification.sender, notification.data) break - except SIPCoreError, e: + except SIPCoreError as e: notification_center.post_notification('SIPSessionDidProcessTransaction', self, NotificationData(originator='local', code=0, reason=None, failure_reason='SIP core error: %s' % str(e), redirect_identities=None)) - except InvitationDisconnectedError, e: + except InvitationDisconnectedError as e: self.greenlet = None notification = Notification('SIPInvitationChangedState', e.invitation, e.data) notification.center = notification_center self.handle_notification(notification) return False return True def _send_hold(self): self.state = 'sending_proposal' self.greenlet = api.getcurrent() notification_center = NotificationCenter() unhandled_notifications = [] try: local_sdp = SDPSession.new(self._invitation.sdp.active_local) local_sdp.version += 1 for stream in self.streams: local_sdp.media[stream.index] = stream.get_local_media(remote_sdp=None, index=stream.index) self._invitation.send_reinvite(sdp=local_sdp) received_invitation_state = False received_sdp_update = False with api.timeout(self.short_reinvite_timeout): while not received_invitation_state or not received_sdp_update: notification = self._channel.wait() if notification.name == 'SIPInvitationGotSDPUpdate': received_sdp_update = True if notification.data.succeeded: local_sdp = notification.data.local_sdp remote_sdp = notification.data.remote_sdp for stream in self.streams: stream.update(local_sdp, remote_sdp, stream.index) elif notification.name == 'SIPInvitationChangedState': if notification.data.state == 'connected' and notification.data.sub_state == 'normal': received_invitation_state = True notification_center.post_notification('SIPSessionDidProcessTransaction', self, NotificationData(originator='local', method='INVITE', code=notification.data.code, reason=notification.data.reason)) elif notification.data.state == 'disconnected': raise InvitationDisconnectedError(notification.sender, notification.data) - except InvitationDisconnectedError, e: + except InvitationDisconnectedError as e: self.greenlet = None notification = Notification('SIPInvitationChangedState', e.invitation, e.data) notification.center = notification_center self.handle_notification(notification) return except api.TimeoutError: if not self._cancel_hold(): return except SIPCoreError: pass self.greenlet = None self.on_hold = True self.state = 'connected' hold_supported_streams = (stream for stream in self.streams if stream.hold_supported) notification_center.post_notification('SIPSessionDidChangeHoldState', self, NotificationData(originator='local', on_hold=True, partial=any(not stream.on_hold_by_local for stream in hold_supported_streams))) for notification in unhandled_notifications: self.handle_notification(notification) if self._hold_in_progress: self._hold_in_progress = False else: for stream in self.streams: stream.unhold() self._send_unhold() def _send_unhold(self): self.state = 'sending_proposal' self.greenlet = api.getcurrent() notification_center = NotificationCenter() unhandled_notifications = [] try: local_sdp = SDPSession.new(self._invitation.sdp.active_local) local_sdp.version += 1 for stream in self.streams: local_sdp.media[stream.index] = stream.get_local_media(remote_sdp=None, index=stream.index) self._invitation.send_reinvite(sdp=local_sdp) received_invitation_state = False received_sdp_update = False with api.timeout(self.short_reinvite_timeout): while not received_invitation_state or not received_sdp_update: notification = self._channel.wait() if notification.name == 'SIPInvitationGotSDPUpdate': received_sdp_update = True if notification.data.succeeded: local_sdp = notification.data.local_sdp remote_sdp = notification.data.remote_sdp for stream in self.streams: stream.update(local_sdp, remote_sdp, stream.index) elif notification.name == 'SIPInvitationChangedState': if notification.data.state == 'connected' and notification.data.sub_state == 'normal': received_invitation_state = True notification_center.post_notification('SIPSessionDidProcessTransaction', self, NotificationData(originator='local', method='INVITE', code=notification.data.code, reason=notification.data.reason)) elif notification.data.state == 'disconnected': raise InvitationDisconnectedError(notification.sender, notification.data) - except InvitationDisconnectedError, e: + except InvitationDisconnectedError as e: self.greenlet = None notification = Notification('SIPInvitationChangedState', e.invitation, e.data) notification.center = notification_center self.handle_notification(notification) return except api.TimeoutError: if not self._cancel_hold(): return except SIPCoreError: pass self.greenlet = None self.on_hold = False self.state = 'connected' notification_center.post_notification('SIPSessionDidChangeHoldState', self, NotificationData(originator='local', on_hold=False, partial=False)) for notification in unhandled_notifications: self.handle_notification(notification) if self._hold_in_progress: for stream in self.streams: stream.hold() self._send_hold() def _fail(self, originator, code, reason, error, reason_header=None): notification_center = NotificationCenter() prev_inv_state = self._invitation.state self.state = 'terminating' if prev_inv_state not in (None, 'incoming', 'outgoing', 'early', 'connecting'): notification_center.post_notification('SIPSessionWillEnd', self, NotificationData(originator=originator)) if self._invitation.state not in (None, 'disconnecting', 'disconnected'): try: if self._invitation.direction == 'incoming' and self._invitation.state in ('incoming', 'early'): if 400<=code<=699 and reason is not None: self._invitation.send_response(code, extra_headers=[reason_header] if reason_header is not None else []) else: self._invitation.end(extra_headers=[reason_header] if reason_header is not None else []) with api.timeout(1): while True: notification = self._channel.wait() if notification.name == 'SIPInvitationChangedState' and notification.data.state == 'disconnected': if prev_inv_state in ('connecting', 'connected'): if notification.data.disconnect_reason in ('timeout', 'missing ACK'): sip_code = 200 sip_reason = 'OK' originator = 'local' elif hasattr(notification.data, 'method'): sip_code = 200 sip_reason = 'OK' originator = 'remote' else: sip_code = notification.data.code sip_reason = notification.data.reason originator = 'local' notification_center.post_notification('SIPSessionDidProcessTransaction', self, NotificationData(originator=originator, method='BYE', code=sip_code, reason=sip_reason)) elif self._invitation.direction == 'incoming' and prev_inv_state in ('incoming', 'early'): ack_received = notification.data.disconnect_reason != 'missing ACK' notification_center.post_notification('SIPSessionDidProcessTransaction', self, NotificationData(originator='remote', method='INVITE', code=code, reason=reason, ack_received=ack_received)) elif self._invitation.direction == 'outgoing' and prev_inv_state in ('outgoing', 'early'): notification_center.post_notification('SIPSessionDidProcessTransaction', self, NotificationData(originator='local', method='INVITE', code=487, reason='Session Cancelled')) break except SIPCoreError: pass except api.TimeoutError: if prev_inv_state in ('connecting', 'connected'): notification_center.post_notification('SIPSessionDidProcessTransaction', self, NotificationData(originator='local', method='BYE', code=408, reason=sip_status_messages[408])) notification_center.remove_observer(self, sender=self._invitation) self.state = 'terminated' notification_center.post_notification('SIPSessionDidFail', self, NotificationData(originator=originator, code=code, reason=reason, failure_reason=error, redirect_identities=None)) self.greenlet = None def _fail_proposal(self, originator, error): notification_center = NotificationCenter() for stream in self.proposed_streams: try: notification_center.remove_observer(self, sender=stream) except KeyError: # _fail_proposal can be called from reject_proposal, which means the stream will # not have been initialized or the session registered as an observer for it. pass else: stream.deactivate() stream.end() if originator == 'remote' and self._invitation.sub_state == 'received_proposal': try: self._invitation.send_response(488 if self.proposed_streams else 500) except SIPCoreError: pass else: notification_center.post_notification('SIPSessionDidProcessTransaction', self, NotificationData(originator='remote', method='INVITE', code=500, reason=sip_status_messages[500], ack_received='unknown')) notification_center.post_notification('SIPSessionHadProposalFailure', self, NotificationData(originator=originator, failure_reason=error, proposed_streams=self.proposed_streams[:])) self.state = 'connected' self.proposed_streams = None self.greenlet = None @run_in_green_thread def handle_notification(self, notification): handler = getattr(self, '_NH_%s' % notification.name, Null) handler(notification) def _NH_SIPInvitationChangedState(self, notification): if self.state == 'terminated': return if notification.data.originator == 'remote' and notification.data.state not in ('disconnecting', 'disconnected'): contact_header = notification.data.headers.get('Contact', None) if contact_header and 'isfocus' in contact_header[0].parameters: self.remote_focus = True if self.greenlet is not None: if notification.data.state == 'disconnected' and notification.data.prev_state != 'disconnecting': self._channel.send_exception(InvitationDisconnectedError(notification.sender, notification.data)) else: self._channel.send(notification) else: self.greenlet = api.getcurrent() unhandled_notifications = [] try: if notification.data.state == 'connected' and notification.data.sub_state == 'received_proposal': self.state = 'received_proposal' try: proposed_remote_sdp = self._invitation.sdp.proposed_remote active_remote_sdp = self._invitation.sdp.active_remote if len(proposed_remote_sdp.media) < len(active_remote_sdp.media): engine = Engine() self._invitation.send_response(488, extra_headers=[WarningHeader(399, engine.user_agent, 'Streams cannot be deleted from the SDP')]) self.state = 'connected' notification.center.post_notification('SIPSessionDidProcessTransaction', self, NotificationData(originator='remote', method='INVITE', code=488, reason=sip_status_messages[488], ack_received='unknown')) return for stream in self.streams: if not stream.validate_update(proposed_remote_sdp, stream.index): engine = Engine() self._invitation.send_response(488, extra_headers=[WarningHeader(399, engine.user_agent, 'Failed to update media stream index %d' % stream.index)]) self.state = 'connected' notification.center.post_notification('SIPSessionDidProcessTransaction', self, NotificationData(originator='remote', method='INVITE', code=488, reason=sip_status_messages[488], ack_received='unknown')) return added_media_indexes = set() removed_media_indexes = set() reused_media_indexes = set() for index, media_stream in enumerate(proposed_remote_sdp.media): if index >= len(active_remote_sdp.media): added_media_indexes.add(index) elif media_stream.port == 0 and active_remote_sdp.media[index].port > 0: removed_media_indexes.add(index) elif media_stream.port > 0 and active_remote_sdp.media[index].port == 0: reused_media_indexes.add(index) elif media_stream.media != active_remote_sdp.media[index].media: added_media_indexes.add(index) removed_media_indexes.add(index) if added_media_indexes | reused_media_indexes and removed_media_indexes: engine = Engine() self._invitation.send_response(488, extra_headers=[WarningHeader(399, engine.user_agent, 'Both removing AND adding a media stream is currently not supported')]) self.state = 'connected' notification.center.post_notification('SIPSessionDidProcessTransaction', self, NotificationData(originator='remote', method='INVITE', code=488, reason=sip_status_messages[488], ack_received='unknown')) return elif added_media_indexes | reused_media_indexes: self.proposed_streams = [] for index in added_media_indexes | reused_media_indexes: media_stream = proposed_remote_sdp.media[index] if media_stream.port != 0: for stream_type in MediaStreamRegistry: try: stream = stream_type.new_from_sdp(self, proposed_remote_sdp, index) except UnknownStreamError: continue except InvalidStreamError as e: log.error("Invalid stream: {}".format(e)) break except Exception as e: log.exception("Exception occurred while setting up stream from SDP: {}".format(e)) break else: stream.index = index self.proposed_streams.append(stream) break if self.proposed_streams: self._invitation.send_response(100) notification.center.post_notification('SIPSessionNewProposal', sender=self, data=NotificationData(originator='remote', proposed_streams=self.proposed_streams[:])) else: self._invitation.send_response(488) self.state = 'connected' notification.center.post_notification('SIPSessionDidProcessTransaction', self, NotificationData(originator='remote', method='INVITE', code=488, reason=sip_status_messages[488], ack_received='unknown')) return else: local_sdp = SDPSession.new(self._invitation.sdp.active_local) local_sdp.version += 1 removed_streams = [stream for stream in self.streams if stream.index in removed_media_indexes] prev_on_hold_streams = set(stream for stream in self.streams if stream.hold_supported and stream.on_hold_by_remote) for stream in removed_streams: notification.center.remove_observer(self, sender=stream) stream.deactivate() media = local_sdp.media[stream.index] media.port = 0 media.attributes = [] media.bandwidth_info = [] for stream in self.streams: local_sdp.media[stream.index] = stream.get_local_media(remote_sdp=proposed_remote_sdp, index=stream.index) try: self._invitation.send_response(200, sdp=local_sdp) except PJSIPError: for stream in removed_streams: self.streams.remove(stream) stream.end() if removed_streams: self.end() return else: try: self._invitation.send_response(488) except PJSIPError: self.end() return else: for stream in removed_streams: self.streams.remove(stream) stream.end() notification.center.post_notification('SIPSessionDidProcessTransaction', self, NotificationData(originator='remote', method='INVITE', code=200, reason=sip_status_messages[200], ack_received='unknown')) received_invitation_state = False received_sdp_update = False while not received_sdp_update or not received_invitation_state or self._channel: notification = self._channel.wait() if notification.name == 'SIPInvitationGotSDPUpdate': received_sdp_update = True if notification.data.succeeded: local_sdp = notification.data.local_sdp remote_sdp = notification.data.remote_sdp for stream in self.streams: stream.update(local_sdp, remote_sdp, stream.index) elif notification.name == 'SIPInvitationChangedState': if notification.data.state == 'connected' and notification.data.sub_state == 'normal': received_invitation_state = True elif notification.data.state == 'disconnected': raise InvitationDisconnectedError(notification.sender, notification.data) else: unhandled_notifications.append(notification) else: unhandled_notifications.append(notification) on_hold_streams = set(stream for stream in self.streams if stream.hold_supported and stream.on_hold_by_remote) if on_hold_streams != prev_on_hold_streams: hold_supported_streams = (stream for stream in self.streams if stream.hold_supported) notification.center.post_notification('SIPSessionDidChangeHoldState', self, NotificationData(originator='remote', on_hold=bool(on_hold_streams), partial=bool(on_hold_streams) and any(not stream.on_hold_by_remote for stream in hold_supported_streams))) if removed_media_indexes: notification.center.post_notification('SIPSessionDidRenegotiateStreams', self, NotificationData(originator='remote', added_streams=[], removed_streams=removed_streams)) - except InvitationDisconnectedError, e: + except InvitationDisconnectedError as e: self.greenlet = None self.state = 'connected' notification = Notification('SIPInvitationChangedState', e.invitation, e.data) notification.center = NotificationCenter() self.handle_notification(notification) except SIPCoreError: self.end() else: self.state = 'connected' elif notification.data.state == 'connected' and notification.data.sub_state == 'received_proposal_request': self.state = 'received_proposal_request' try: # An empty proposal was received, generate an offer self._invitation.send_response(100) local_sdp = SDPSession.new(self._invitation.sdp.active_local) local_sdp.version += 1 connection_address = host.outgoing_ip_for(self._invitation.peer_address.ip) if local_sdp.connection is not None: local_sdp.connection.address = connection_address for index, stream in enumerate(self.streams): stream.reset(index) media = stream.get_local_media(remote_sdp=None, index=index) if media.connection is not None: media.connection.address = connection_address local_sdp.media[stream.index] = media self._invitation.send_response(200, sdp=local_sdp) notification.center.post_notification('SIPSessionDidProcessTransaction', self, NotificationData(originator='remote', method='INVITE', code=200, reason=sip_status_messages[200], ack_received='unknown')) received_invitation_state = False received_sdp_update = False while not received_sdp_update or not received_invitation_state or self._channel: notification = self._channel.wait() if notification.name == 'SIPInvitationGotSDPUpdate': received_sdp_update = True if notification.data.succeeded: local_sdp = notification.data.local_sdp remote_sdp = notification.data.remote_sdp for stream in self.streams: stream.update(local_sdp, remote_sdp, stream.index) elif notification.name == 'SIPInvitationChangedState': if notification.data.state == 'connected' and notification.data.sub_state == 'normal': received_invitation_state = True elif notification.data.state == 'disconnected': raise InvitationDisconnectedError(notification.sender, notification.data) else: unhandled_notifications.append(notification) else: unhandled_notifications.append(notification) - except InvitationDisconnectedError, e: + except InvitationDisconnectedError as e: self.greenlet = None self.state = 'connected' notification = Notification('SIPInvitationChangedState', e.invitation, e.data) notification.center = NotificationCenter() self.handle_notification(notification) except SIPCoreError: raise # FIXME else: self.state = 'connected' elif notification.data.prev_state == notification.data.state == 'connected' and notification.data.prev_sub_state == 'received_proposal' and notification.data.sub_state == 'normal': if notification.data.originator == 'local' and notification.data.code == 487: proposed_streams = self.proposed_streams self.proposed_streams = None self.state = 'connected' notification.center.post_notification('SIPSessionProposalRejected', self, NotificationData(originator='remote', code=notification.data.code, reason=notification.data.reason, proposed_streams=proposed_streams)) if self._hold_in_progress: self._send_hold() elif notification.data.state == 'disconnected': if self.state == 'incoming': self.state = 'terminated' if notification.data.originator == 'remote': notification.center.post_notification('SIPSessionDidProcessTransaction', self, NotificationData(originator='remote', method='INVITE', code=487, reason='Session Cancelled', ack_received='unknown')) notification.center.post_notification('SIPSessionDidFail', self, NotificationData(originator='remote', code=487, reason='Session Cancelled', failure_reason=notification.data.disconnect_reason, redirect_identities=None)) else: # There must have been an error involved notification.center.post_notification('SIPSessionDidFail', self, NotificationData(originator='local', code=0, reason=None, failure_reason=notification.data.disconnect_reason, redirect_identities=None)) else: self.state = 'terminated' notification.center.post_notification('SIPSessionWillEnd', self, NotificationData(originator=notification.data.originator)) for stream in self.streams: notification.center.remove_observer(self, sender=stream) stream.deactivate() stream.end() if notification.data.originator == 'remote': if hasattr(notification.data, 'method'): notification.center.post_notification('SIPSessionDidProcessTransaction', self, NotificationData(originator=notification.data.originator, method=notification.data.method, code=200, reason=sip_status_messages[200])) else: notification.center.post_notification('SIPSessionDidProcessTransaction', self, NotificationData(originator=notification.data.originator, method='INVITE', code=notification.data.code, reason=notification.data.reason)) self.end_time = ISOTimestamp.now() notification.center.post_notification('SIPSessionDidEnd', self, NotificationData(originator=notification.data.originator, end_reason=notification.data.disconnect_reason)) notification.center.remove_observer(self, sender=self._invitation) finally: self.greenlet = None for notification in unhandled_notifications: self.handle_notification(notification) def _NH_SIPInvitationGotSDPUpdate(self, notification): if self.greenlet is not None: self._channel.send(notification) def _NH_MediaStreamDidInitialize(self, notification): if self.greenlet is not None: self._channel.send(notification) def _NH_RTPStreamDidEnableEncryption(self, notification): if notification.sender.type != 'audio': return audio_stream = notification.sender if audio_stream.encryption.type == 'ZRTP': # start ZRTP on the video stream, if applicable try: video_stream = next(stream for stream in self.streams or [] if stream.type=='video') except StopIteration: return if video_stream.encryption.type == 'ZRTP' and not video_stream.encryption.active: video_stream.encryption.zrtp._enable(audio_stream) def _NH_MediaStreamDidStart(self, notification): stream = notification.sender if stream.type == 'audio' and stream.encryption.type == 'ZRTP': stream.encryption.zrtp._enable() elif stream.type == 'video' and stream.encryption.type == 'ZRTP': # start ZRTP on the video stream, if applicable try: audio_stream = next(stream for stream in self.streams or [] if stream.type=='audio') except StopIteration: pass else: if audio_stream.encryption.type == 'ZRTP' and audio_stream.encryption.active: stream.encryption.zrtp._enable(audio_stream) if self.greenlet is not None: self._channel.send(notification) def _NH_MediaStreamDidNotInitialize(self, notification): if self.greenlet is not None and self.state not in ('terminating', 'terminated'): self._channel.send_exception(MediaStreamDidNotInitializeError(notification.sender, notification.data)) def _NH_MediaStreamDidFail(self, notification): if self.greenlet is not None: if self.state not in ('terminating', 'terminated'): self._channel.send_exception(MediaStreamDidFailError(notification.sender, notification.data)) else: stream = notification.sender if self.streams == [stream]: self.end() else: try: self.remove_stream(stream) except IllegalStateError: self.end() -class SessionManager(object): - __metaclass__ = Singleton +class SessionManager(object, metaclass=Singleton): implements(IObserver) def __init__(self): self.sessions = [] self.state = None self._channel = coros.queue() def start(self): self.state = 'starting' notification_center = NotificationCenter() notification_center.post_notification('SIPSessionManagerWillStart', sender=self) notification_center.add_observer(self, 'SIPInvitationChangedState') notification_center.add_observer(self, 'SIPSessionNewIncoming') notification_center.add_observer(self, 'SIPSessionNewOutgoing') notification_center.add_observer(self, 'SIPSessionDidFail') notification_center.add_observer(self, 'SIPSessionDidEnd') self.state = 'started' notification_center.post_notification('SIPSessionManagerDidStart', sender=self) def stop(self): self.state = 'stopping' notification_center = NotificationCenter() notification_center.post_notification('SIPSessionManagerWillEnd', sender=self) for session in self.sessions: session.end() while self.sessions: self._channel.wait() notification_center.remove_observer(self, 'SIPInvitationChangedState') notification_center.remove_observer(self, 'SIPSessionNewIncoming') notification_center.remove_observer(self, 'SIPSessionNewOutgoing') notification_center.remove_observer(self, 'SIPSessionDidFail') notification_center.remove_observer(self, 'SIPSessionDidEnd') self.state = 'stopped' notification_center.post_notification('SIPSessionManagerDidEnd', sender=self) @run_in_twisted_thread def handle_notification(self, notification): if notification.name == 'SIPInvitationChangedState' and notification.data.state == 'incoming': account_manager = AccountManager() account = account_manager.find_account(notification.data.request_uri) if account is None: notification.sender.send_response(404) return notification.sender.send_response(100) session = Session(account) session.init_incoming(notification.sender, notification.data) elif notification.name in ('SIPSessionNewIncoming', 'SIPSessionNewOutgoing'): self.sessions.append(notification.sender) elif notification.name in ('SIPSessionDidFail', 'SIPSessionDidEnd'): self.sessions.remove(notification.sender) if self.state == 'stopping': self._channel.send(notification) diff --git a/sipsimple/streams/msrp/__init__.py b/sipsimple/streams/msrp/__init__.py index 2cfdcaae..748e57f1 100644 --- a/sipsimple/streams/msrp/__init__.py +++ b/sipsimple/streams/msrp/__init__.py @@ -1,403 +1,401 @@ """ Handling of MSRP media streams according to RFC4975, RFC4976, RFC5547 and RFC3994. """ __all__ = ['MSRPStreamError', 'MSRPStreamBase'] import traceback from application.notification import NotificationCenter, NotificationData, IObserver from application.python import Null from application.system import host from twisted.internet.error import ConnectionDone from zope.interface import implements from eventlib import api from msrplib.connect import DirectConnector, DirectAcceptor, RelayConnection, MSRPRelaySettings from msrplib.protocol import URI from msrplib.session import contains_mime_type from sipsimple.account import Account, BonjourAccount from sipsimple.configuration.settings import SIPSimpleSettings from sipsimple.core import SDPAttribute, SDPConnection, SDPMediaStream from sipsimple.streams import IMediaStream, MediaStreamType, StreamError from sipsimple.threading.green import run_in_green_thread class MSRPStreamError(StreamError): pass -class MSRPStreamBase(object): - __metaclass__ = MediaStreamType - +class MSRPStreamBase(object, metaclass=MediaStreamType): implements(IMediaStream, IObserver) # Attributes that need to be defined by each MSRP stream type type = None priority = None msrp_session_class = None media_type = None accept_types = None accept_wrapped_types = None # These attributes are always False for any MSRP stream hold_supported = False on_hold = False on_hold_by_local = False on_hold_by_remote = False def __new__(cls, *args, **kw): if cls is MSRPStreamBase: raise TypeError("MSRPStreamBase cannot be instantiated directly") return object.__new__(cls) def __init__(self, direction='sendrecv'): self.direction = direction self.greenlet = None self.local_media = None self.remote_media = None self.msrp = None # Placeholder for the MSRPTransport that will be set when started self.msrp_connector = None self.cpim_enabled = None # Boolean value. None means it was not negotiated yet self.session = None self.msrp_session = None self.shutting_down = False self.local_role = None self.remote_role = None self.transport = None self.remote_accept_types = None self.remote_accept_wrapped_types = None self._initialize_done = False self._done = False self._failure_reason = None @property def local_uri(self): msrp = self.msrp or self.msrp_connector return msrp.local_uri if msrp is not None else None def _create_local_media(self, uri_path): transport = "TCP/TLS/MSRP" if uri_path[-1].use_tls else "TCP/MSRP" attributes = [SDPAttribute("path", " ".join(str(uri) for uri in uri_path))] if self.direction not in [None, 'sendrecv']: attributes.append(SDPAttribute(self.direction, '')) if self.accept_types is not None: attributes.append(SDPAttribute("accept-types", " ".join(self.accept_types))) if self.accept_wrapped_types is not None: attributes.append(SDPAttribute("accept-wrapped-types", " ".join(self.accept_wrapped_types))) attributes.append(SDPAttribute("setup", self.local_role)) local_ip = uri_path[-1].host connection = SDPConnection(local_ip) return SDPMediaStream(self.media_type, uri_path[-1].port or 2855, transport, connection=connection, formats=["*"], attributes=attributes) # The public API (the IMediaStream interface) # noinspection PyUnusedLocal def get_local_media(self, remote_sdp=None, index=0): return self.local_media def new_from_sdp(self, session, remote_sdp, stream_index): raise NotImplementedError @run_in_green_thread def initialize(self, session, direction): self.greenlet = api.getcurrent() notification_center = NotificationCenter() notification_center.add_observer(self, sender=self) try: self.session = session self.transport = self.session.account.msrp.transport outgoing = direction == 'outgoing' logger = NotificationProxyLogger() if self.session.account is BonjourAccount(): if outgoing: self.msrp_connector = DirectConnector(logger=logger) self.local_role = 'active' else: if self.transport == 'tls' and None in (self.session.account.tls_credentials.cert, self.session.account.tls_credentials.key): raise MSRPStreamError("Cannot accept MSRP connection without a TLS certificate") self.msrp_connector = DirectAcceptor(logger=logger) self.local_role = 'passive' else: if self.session.account.msrp.connection_model == 'relay': if not outgoing and self.remote_role in ('actpass', 'passive'): # 'passive' not allowed by the RFC but play nice for interoperability. -Saul self.msrp_connector = DirectConnector(logger=logger, use_sessmatch=True) self.local_role = 'active' elif outgoing and not self.session.account.nat_traversal.use_msrp_relay_for_outbound: self.msrp_connector = DirectConnector(logger=logger, use_sessmatch=True) self.local_role = 'active' else: if self.session.account.nat_traversal.msrp_relay is None: relay_host = relay_port = None else: if self.transport != self.session.account.nat_traversal.msrp_relay.transport: raise MSRPStreamError("MSRP relay transport conflicts with MSRP transport setting") relay_host = self.session.account.nat_traversal.msrp_relay.host relay_port = self.session.account.nat_traversal.msrp_relay.port relay = MSRPRelaySettings(domain=self.session.account.uri.host, username=self.session.account.uri.user, password=self.session.account.credentials.password, host=relay_host, port=relay_port, use_tls=self.transport=='tls') self.msrp_connector = RelayConnection(relay, 'passive', logger=logger, use_sessmatch=True) self.local_role = 'actpass' if outgoing else 'passive' else: if not outgoing and self.remote_role in ('actpass', 'passive'): # 'passive' not allowed by the RFC but play nice for interoperability. -Saul self.msrp_connector = DirectConnector(logger=logger, use_sessmatch=True) self.local_role = 'active' else: if not outgoing and self.transport == 'tls' and None in (self.session.account.tls_credentials.cert, self.session.account.tls_credentials.key): raise MSRPStreamError("Cannot accept MSRP connection without a TLS certificate") self.msrp_connector = DirectAcceptor(logger=logger, use_sessmatch=True) self.local_role = 'actpass' if outgoing else 'passive' full_local_path = self.msrp_connector.prepare(local_uri=URI(host=host.default_ip, port=0, use_tls=self.transport=='tls', credentials=self.session.account.tls_credentials)) self.local_media = self._create_local_media(full_local_path) - except Exception, e: + except Exception as e: notification_center.post_notification('MediaStreamDidNotInitialize', sender=self, data=NotificationData(reason=str(e))) else: notification_center.post_notification('MediaStreamDidInitialize', sender=self) finally: self._initialize_done = True self.greenlet = None # noinspection PyUnusedLocal @run_in_green_thread def start(self, local_sdp, remote_sdp, stream_index): self.greenlet = api.getcurrent() notification_center = NotificationCenter() context = 'sdp_negotiation' try: remote_media = remote_sdp.media[stream_index] self.remote_media = remote_media self.remote_accept_types = remote_media.attributes.getfirst('accept-types', '').split() self.remote_accept_wrapped_types = remote_media.attributes.getfirst('accept-wrapped-types', '').split() self.cpim_enabled = contains_mime_type(self.accept_types, 'message/cpim') and contains_mime_type(self.remote_accept_types, 'message/cpim') remote_uri_path = remote_media.attributes.getfirst('path') if remote_uri_path is None: raise AttributeError("remote SDP media does not have 'path' attribute") full_remote_path = [URI.parse(uri) for uri in remote_uri_path.split()] remote_transport = 'tls' if full_remote_path[0].use_tls else 'tcp' if self.transport != remote_transport: raise MSRPStreamError("remote transport ('%s') different from local transport ('%s')" % (remote_transport, self.transport)) if isinstance(self.session.account, Account) and self.local_role == 'actpass': remote_setup = remote_media.attributes.getfirst('setup', 'passive') if remote_setup == 'passive': # If actpass is offered connectors are always started as passive # We need to switch to active if the remote answers with passive if self.session.account.msrp.connection_model == 'relay': self.msrp_connector.mode = 'active' else: local_uri = self.msrp_connector.local_uri logger = self.msrp_connector.logger self.msrp_connector = DirectConnector(logger=logger, use_sessmatch=True) self.msrp_connector.prepare(local_uri) context = 'start' self.msrp = self.msrp_connector.complete(full_remote_path) if self.msrp_session_class is not None: self.msrp_session = self.msrp_session_class(self.msrp, accept_types=self.accept_types, on_incoming_cb=self._handle_incoming, automatic_reports=False) self.msrp_connector = None - except Exception, e: + except Exception as e: self._failure_reason = str(e) notification_center.post_notification('MediaStreamDidFail', sender=self, data=NotificationData(context=context, reason=self._failure_reason)) else: notification_center.post_notification('MediaStreamDidStart', sender=self) finally: self.greenlet = None def deactivate(self): self.shutting_down = True @run_in_green_thread def end(self): if self._done: return self._done = True notification_center = NotificationCenter() if not self._initialize_done: # we are in the middle of initialize() try: msrp_connector = self.msrp_connector if self.greenlet is not None: api.kill(self.greenlet) if msrp_connector is not None: msrp_connector.cleanup() finally: notification_center.post_notification('MediaStreamDidNotInitialize', sender=self, data=NotificationData(reason='Interrupted')) notification_center.remove_observer(self, sender=self) self.msrp_connector = None self.greenlet = None else: notification_center.post_notification('MediaStreamWillEnd', sender=self) msrp = self.msrp msrp_session = self.msrp_session msrp_connector = self.msrp_connector try: if self.greenlet is not None: api.kill(self.greenlet) if msrp_session is not None: msrp_session.shutdown() elif msrp is not None: msrp.loseConnection(wait=False) if msrp_connector is not None: msrp_connector.cleanup() finally: notification_center.post_notification('MediaStreamDidEnd', sender=self, data=NotificationData(error=self._failure_reason)) notification_center.remove_observer(self, sender=self) self.msrp = None self.msrp_session = None self.msrp_connector = None self.session = None self.greenlet = None # noinspection PyMethodMayBeStatic,PyUnusedLocal def validate_update(self, remote_sdp, stream_index): return True # TODO def update(self, local_sdp, remote_sdp, stream_index): pass # TODO def hold(self): pass def unhold(self): pass def reset(self, stream_index): pass # Internal IObserver interface def handle_notification(self, notification): handler = getattr(self, '_NH_%s' % notification.name, Null) handler(notification) # Internal message handlers def _handle_incoming(self, chunk=None, error=None): notification_center = NotificationCenter() if error is not None: if self.shutting_down and isinstance(error.value, ConnectionDone): return self._failure_reason = error.getErrorMessage() notification_center.post_notification('MediaStreamDidFail', sender=self, data=NotificationData(context='reading', reason=self._failure_reason)) elif chunk is not None: method_handler = getattr(self, '_handle_%s' % chunk.method, None) if method_handler is not None: method_handler(chunk) def _handle_REPORT(self, chunk): pass def _handle_SEND(self, chunk): pass # temporary solution. to be replaced later by a better logging system in msrplib -Dan # class ChunkInfo(object): __slots__ = 'content_type', 'header', 'footer', 'data' def __init__(self, content_type, header='', footer='', data=''): self.content_type = content_type self.header = header self.footer = footer self.data = data def __repr__(self): return "{0.__class__.__name__}(content_type={0.content_type!r}, header={0.header!r}, footer={0.footer!r}, data={0.data!r})".format(self) @property def content(self): return self.header + self.data + self.footer @property def normalized_content(self): if not self.data: return self.header + self.footer elif self.content_type == 'message/cpim': headers, sep, body = self.data.partition('\r\n\r\n') if not sep: return self.header + self.data + self.footer mime_headers, mime_sep, mime_body = body.partition('\n\n') if not mime_sep: return self.header + self.data + self.footer for mime_header in mime_headers.lower().splitlines(): if mime_header.startswith('content-type:'): wrapped_content_type = mime_header[13:].partition(';')[0].strip() break else: wrapped_content_type = None if wrapped_content_type is None or wrapped_content_type == 'application/im-iscomposing+xml' or wrapped_content_type.startswith(('text/', 'message/')): data = self.data else: data = headers + sep + mime_headers + mime_sep + '<<>>' return self.header + data + self.footer elif self.content_type is None or self.content_type == 'application/im-iscomposing+xml' or self.content_type.startswith(('text/', 'message/')): return self.header + self.data + self.footer else: return self.header + '<<>>' + self.footer class NotificationProxyLogger(object): def __init__(self): from application import log self.level = log.level self.notification_center = NotificationCenter() self.log_settings = SIPSimpleSettings().logs def received_chunk(self, data, transport): if self.log_settings.trace_msrp: chunk_info = ChunkInfo(data.content_type, header=data.chunk_header, footer=data.chunk_footer, data=data.data) notification_data = NotificationData(direction='incoming', local_address=transport.getHost(), remote_address=transport.getPeer(), data=chunk_info.normalized_content, illegal=False) self.notification_center.post_notification('MSRPTransportTrace', sender=transport, data=notification_data) def sent_chunk(self, data, transport): if self.log_settings.trace_msrp: chunk_info = ChunkInfo(data.content_type, header=data.encoded_header, footer=data.encoded_footer, data=data.data) notification_data = NotificationData(direction='outgoing', local_address=transport.getHost(), remote_address=transport.getPeer(), data=chunk_info.normalized_content, illegal=False) self.notification_center.post_notification('MSRPTransportTrace', sender=transport, data=notification_data) def received_illegal_data(self, data, transport): if self.log_settings.trace_msrp: notification_data = NotificationData(direction='incoming', local_address=transport.getHost(), remote_address=transport.getPeer(), data=data, illegal=True) self.notification_center.post_notification('MSRPTransportTrace', sender=transport, data=notification_data) def debug(self, message, *args, **kw): pass def info(self, message, *args, **kw): if self.log_settings.trace_msrp: self.notification_center.post_notification('MSRPLibraryLog', data=NotificationData(message=message % args if args else message, level=self.level.INFO)) def warning(self, message, *args, **kw): if self.log_settings.trace_msrp: self.notification_center.post_notification('MSRPLibraryLog', data=NotificationData(message=message % args if args else message, level=self.level.WARNING)) warn = warning def error(self, message, *args, **kw): if self.log_settings.trace_msrp: self.notification_center.post_notification('MSRPLibraryLog', data=NotificationData(message=message % args if args else message, level=self.level.ERROR)) def exception(self, message='', *args, **kw): if self.log_settings.trace_msrp: message = message % args if args else message exception = traceback.format_exc() self.notification_center.post_notification('MSRPLibraryLog', data=NotificationData(message=message + '\n' + exception if message else exception, level=self.level.ERROR)) def critical(self, message, *args, **kw): if self.log_settings.trace_msrp: self.notification_center.post_notification('MSRPLibraryLog', data=NotificationData(message=message % args if args else message, level=self.level.CRITICAL)) fatal = critical from sipsimple.streams.msrp import chat, filetransfer, screensharing diff --git a/sipsimple/streams/msrp/chat.py b/sipsimple/streams/msrp/chat.py index 2ea63a5a..ff98fbbf 100644 --- a/sipsimple/streams/msrp/chat.py +++ b/sipsimple/streams/msrp/chat.py @@ -1,908 +1,906 @@ """ This module provides classes to parse and generate SDP related to SIP sessions that negotiate Instant Messaging, including CPIM as defined in RFC3862 """ -import cPickle as pickle +import pickle as pickle import codecs import os import random import re from application.python.descriptor import WriteOnceAttribute from application.notification import IObserver, NotificationCenter, NotificationData from application.python import Null from application.python.types import Singleton from application.system import openfile from collections import defaultdict from email.message import Message as EmailMessage from email.parser import Parser as EmailParser from eventlib.coros import queue from eventlib.proc import spawn, ProcExit from functools import partial from msrplib.protocol import FailureReportHeader, SuccessReportHeader, UseNicknameHeader from msrplib.session import MSRPSession, contains_mime_type from otr import OTRSession, OTRTransport, OTRState, SMPStatus from otr.cryptography import DSAPrivateKey from otr.exceptions import IgnoreMessage, UnencryptedMessage, EncryptedMessageError, OTRError from zope.interface import implements from sipsimple.core import SIPURI, BaseSIPURI from sipsimple.payloads import ParserError from sipsimple.payloads.iscomposing import IsComposingDocument, State, LastActive, Refresh, ContentType from sipsimple.storage import ISIPSimpleApplicationDataStorage from sipsimple.streams import InvalidStreamError, UnknownStreamError from sipsimple.streams.msrp import MSRPStreamError, MSRPStreamBase from sipsimple.threading import run_in_thread, run_in_twisted_thread from sipsimple.threading.green import run_in_green_thread from sipsimple.util import MultilingualText, ISOTimestamp __all__ = ['ChatStream', 'ChatStreamError', 'ChatIdentity', 'CPIMPayload', 'CPIMHeader', 'CPIMNamespace', 'CPIMParserError', 'OTRState', 'SMPStatus'] class OTRTrustedPeer(object): fingerprint = WriteOnceAttribute() # in order to be hashable this needs to be immutable - def __init__(self, fingerprint, description=u'', **kw): - if not isinstance(fingerprint, basestring): + def __init__(self, fingerprint, description='', **kw): + if not isinstance(fingerprint, str): raise TypeError("fingerprint must be a string") self.fingerprint = fingerprint self.description = description self.__dict__.update(kw) def __hash__(self): return hash(self.fingerprint) def __eq__(self, other): if isinstance(other, OTRTrustedPeer): return self.fingerprint == other.fingerprint - elif isinstance(other, basestring): + elif isinstance(other, str): return self.fingerprint == other else: return NotImplemented def __ne__(self, other): return not (self == other) def __repr__(self): return "{0.__class__.__name__}({0.fingerprint!r}, description={0.description!r})".format(self) def __reduce__(self): return self.__class__, (self.fingerprint,), self.__dict__ class OTRTrustedPeerSet(object): def __init__(self, iterable=()): self.__data__ = {} self.update(iterable) def __repr__(self): - return "{}({})".format(self.__class__.__name__, self.__data__.values()) + return "{}({})".format(self.__class__.__name__, list(self.__data__.values())) def __contains__(self, item): return item in self.__data__ def __getitem__(self, item): return self.__data__[item] def __iter__(self): - return self.__data__.itervalues() + return iter(list(self.__data__.values())) def __len__(self): return len(self.__data__) def get(self, item, default=None): return self.__data__.get(item, default) def add(self, item): if not isinstance(item, OTRTrustedPeer): raise TypeError("item should be and instance of OTRTrustedPeer") self.__data__[item.fingerprint] = item def remove(self, item): del self.__data__[item] def discard(self, item): self.__data__.pop(item, None) def update(self, iterable=()): for item in iterable: self.add(item) -class OTRCache(object): - __metaclass__ = Singleton - +class OTRCache(object, metaclass=Singleton): def __init__(self): from sipsimple.application import SIPApplication if SIPApplication.storage is None: raise RuntimeError("Cannot access the OTR cache before SIPApplication.storage is defined") if ISIPSimpleApplicationDataStorage.providedBy(SIPApplication.storage): self.key_file = os.path.join(SIPApplication.storage.directory, 'otr.key') self.trusted_file = os.path.join(SIPApplication.storage.directory, 'otr.trusted') try: self.private_key = DSAPrivateKey.load(self.key_file) if self.private_key.key_size != 1024: raise ValueError except (EnvironmentError, ValueError): self.private_key = DSAPrivateKey.generate() self.private_key.save(self.key_file) try: self.trusted_peers = pickle.load(open(self.trusted_file, 'rb')) if not isinstance(self.trusted_peers, OTRTrustedPeerSet) or not all(isinstance(item, OTRTrustedPeer) for item in self.trusted_peers): raise ValueError("invalid OTR trusted peers file") except Exception: self.trusted_peers = OTRTrustedPeerSet() self.save() else: self.key_file = self.trusted_file = None self.private_key = DSAPrivateKey.generate() self.trusted_peers = OTRTrustedPeerSet() # def generate_private_key(self): # self.private_key = DSAPrivateKey.generate() # if self.key_file: # self.private_key.save(self.key_file) @run_in_thread('file-io') def save(self): if self.trusted_file is not None: - with openfile(self.trusted_file, 'wb', permissions=0600) as trusted_file: + with openfile(self.trusted_file, 'wb', permissions=0o600) as trusted_file: pickle.dump(self.trusted_peers, trusted_file) class OTREncryption(object): implements(IObserver) def __init__(self, stream): self.stream = stream self.otr_cache = OTRCache() self.otr_session = OTRSession(self.otr_cache.private_key, self.stream, supported_versions={3}) # we need at least OTR-v3 for question based SMP notification_center = NotificationCenter() notification_center.add_observer(self, sender=stream) notification_center.add_observer(self, sender=self.otr_session) @property def active(self): try: return self.otr_session.encrypted except AttributeError: return False @property def cipher(self): return 'AES-128-CTR' if self.active else None @property def key_fingerprint(self): try: return self.otr_session.local_private_key.public_key.fingerprint except AttributeError: return None @property def peer_fingerprint(self): try: return self.otr_session.remote_public_key.fingerprint except AttributeError: return None @property def peer_name(self): try: return self.__dict__['peer_name'] except KeyError: trusted_peer = self.otr_cache.trusted_peers.get(self.peer_fingerprint, None) if trusted_peer is None: - return u'' + return '' else: return self.__dict__.setdefault('peer_name', trusted_peer.description) @peer_name.setter def peer_name(self, name): old_name = self.peer_name new_name = self.__dict__['peer_name'] = name if old_name != new_name: trusted_peer = self.otr_cache.trusted_peers.get(self.peer_fingerprint, None) if trusted_peer is not None: trusted_peer.description = new_name self.otr_cache.save() notification_center = NotificationCenter() notification_center.post_notification("ChatStreamOTRPeerNameChanged", sender=self.stream, data=NotificationData(name=name)) @property def verified(self): return self.peer_fingerprint in self.otr_cache.trusted_peers @verified.setter def verified(self, value): peer_fingerprint = self.peer_fingerprint old_verified = peer_fingerprint in self.otr_cache.trusted_peers new_verified = bool(value) if peer_fingerprint is None or new_verified == old_verified: return if new_verified: self.otr_cache.trusted_peers.add(OTRTrustedPeer(peer_fingerprint, description=self.peer_name)) else: self.otr_cache.trusted_peers.remove(peer_fingerprint) self.otr_cache.save() notification_center = NotificationCenter() notification_center.post_notification("ChatStreamOTRVerifiedStateChanged", sender=self.stream, data=NotificationData(verified=new_verified)) @run_in_twisted_thread def start(self): if self.otr_session is not None: self.otr_session.start() @run_in_twisted_thread def stop(self): if self.otr_session is not None: self.otr_session.stop() @run_in_twisted_thread def smp_verify(self, secret, question=None): self.otr_session.smp_verify(secret, question) @run_in_twisted_thread def smp_answer(self, secret): self.otr_session.smp_answer(secret) @run_in_twisted_thread def smp_abort(self): self.otr_session.smp_abort() def handle_notification(self, notification): handler = getattr(self, '_NH_%s' % notification.name, Null) handler(notification) def _NH_MediaStreamDidStart(self, notification): if self.stream.start_otr: self.otr_session.start() def _NH_MediaStreamDidEnd(self, notification): notification.center.remove_observer(self, sender=self.stream) notification.center.remove_observer(self, sender=self.otr_session) self.otr_session.stop() self.otr_session = None self.stream = None _NH_MediaStreamDidNotInitialize = _NH_MediaStreamDidEnd def _NH_OTRSessionStateChanged(self, notification): notification.center.post_notification('ChatStreamOTREncryptionStateChanged', sender=self.stream, data=notification.data) def _NH_OTRSessionSMPVerificationDidStart(self, notification): notification.center.post_notification('ChatStreamSMPVerificationDidStart', sender=self.stream, data=notification.data) def _NH_OTRSessionSMPVerificationDidNotStart(self, notification): notification.center.post_notification('ChatStreamSMPVerificationDidNotStart', sender=self.stream, data=notification.data) def _NH_OTRSessionSMPVerificationDidEnd(self, notification): notification.center.post_notification('ChatStreamSMPVerificationDidEnd', sender=self.stream, data=notification.data) class ChatStreamError(MSRPStreamError): pass class ChatStream(MSRPStreamBase): type = 'chat' priority = 1 msrp_session_class = MSRPSession media_type = 'message' accept_types = ['message/cpim', 'text/*', 'image/*', 'application/im-iscomposing+xml'] accept_wrapped_types = ['text/*', 'image/*', 'application/im-iscomposing+xml'] prefer_cpim = True start_otr = True def __init__(self): super(ChatStream, self).__init__(direction='sendrecv') self.message_queue = queue() self.sent_messages = set() self.incoming_queue = defaultdict(list) self.message_queue_thread = None self.encryption = OTREncryption(self) @classmethod def new_from_sdp(cls, session, remote_sdp, stream_index): remote_stream = remote_sdp.media[stream_index] if remote_stream.media != 'message': raise UnknownStreamError expected_transport = 'TCP/TLS/MSRP' if session.account.msrp.transport=='tls' else 'TCP/MSRP' if remote_stream.transport != expected_transport: raise InvalidStreamError("expected %s transport in chat stream, got %s" % (expected_transport, remote_stream.transport)) if remote_stream.formats != ['*']: raise InvalidStreamError("wrong format list specified") stream = cls() stream.remote_role = remote_stream.attributes.getfirst('setup', 'active') if remote_stream.direction != 'sendrecv': raise InvalidStreamError("Unsupported direction for chat stream: %s" % remote_stream.direction) remote_accept_types = remote_stream.attributes.getfirst('accept-types') if remote_accept_types is None: raise InvalidStreamError("remote SDP media does not have 'accept-types' attribute") if not any(contains_mime_type(cls.accept_types, mime_type) for mime_type in remote_accept_types.split()): raise InvalidStreamError("no compatible media types found") return stream @property def local_identity(self): try: return ChatIdentity(self.session.local_identity.uri, self.session.local_identity.display_name) except AttributeError: return None @property def remote_identity(self): try: return ChatIdentity(self.session.remote_identity.uri, self.session.remote_identity.display_name) except AttributeError: return None @property def private_messages_allowed(self): return 'private-messages' in self.chatroom_capabilities @property def nickname_allowed(self): return 'nickname' in self.chatroom_capabilities @property def chatroom_capabilities(self): try: if self.cpim_enabled and self.session.remote_focus: return ' '.join(self.remote_media.attributes.getall('chatroom')).split() except AttributeError: pass return [] def _NH_MediaStreamDidStart(self, notification): self.message_queue_thread = spawn(self._message_queue_handler) def _NH_MediaStreamDidNotInitialize(self, notification): message_queue, self.message_queue = self.message_queue, queue() while message_queue: message = message_queue.wait() if message.notify_progress: data = NotificationData(message_id=message.id, message=None, code=0, reason='Stream was closed') notification.center.post_notification('ChatStreamDidNotDeliverMessage', sender=self, data=data) def _NH_MediaStreamDidEnd(self, notification): if self.message_queue_thread is not None: self.message_queue_thread.kill() else: message_queue, self.message_queue = self.message_queue, queue() while message_queue: message = message_queue.wait() if message.notify_progress: data = NotificationData(message_id=message.id, message=None, code=0, reason='Stream ended') notification.center.post_notification('ChatStreamDidNotDeliverMessage', sender=self, data=data) def _handle_REPORT(self, chunk): # in theory, REPORT can come with Byte-Range which would limit the scope of the REPORT to the part of the message. if chunk.message_id in self.sent_messages: self.sent_messages.remove(chunk.message_id) notification_center = NotificationCenter() data = NotificationData(message_id=chunk.message_id, message=chunk, code=chunk.status.code, reason=chunk.status.comment) if chunk.status.code == 200: notification_center.post_notification('ChatStreamDidDeliverMessage', sender=self, data=data) else: notification_center.post_notification('ChatStreamDidNotDeliverMessage', sender=self, data=data) def _handle_SEND(self, chunk): if chunk.size == 0: # keep-alive self.msrp_session.send_report(chunk, 200, 'OK') return content_type = chunk.content_type.lower() if not contains_mime_type(self.accept_types, content_type): self.msrp_session.send_report(chunk, 413, 'Unwanted Message') return if chunk.contflag == '#': self.incoming_queue.pop(chunk.message_id, None) self.msrp_session.send_report(chunk, 200, 'OK') return elif chunk.contflag == '+': self.incoming_queue[chunk.message_id].append(chunk.data) self.msrp_session.send_report(chunk, 200, 'OK') return else: data = ''.join(self.incoming_queue.pop(chunk.message_id, [])) + chunk.data if content_type == 'message/cpim': try: payload = CPIMPayload.decode(data) except CPIMParserError: self.msrp_session.send_report(chunk, 400, 'CPIM Parser Error') return else: message = Message(**{name: getattr(payload, name) for name in Message.__slots__}) if not contains_mime_type(self.accept_wrapped_types, message.content_type): self.msrp_session.send_report(chunk, 413, 'Unwanted Message') return if message.timestamp is None: message.timestamp = ISOTimestamp.now() if message.sender is None: message.sender = self.remote_identity private = self.session.remote_focus and len(message.recipients) == 1 and message.recipients[0] != self.remote_identity else: payload = SimplePayload.decode(data, content_type) message = Message(payload.content, payload.content_type, sender=self.remote_identity, recipients=[self.local_identity], timestamp=ISOTimestamp.now()) private = False try: message.content = self.encryption.otr_session.handle_input(message.content, message.content_type) except IgnoreMessage: self.msrp_session.send_report(chunk, 200, 'OK') return except UnencryptedMessage: encrypted = False encryption_active = True - except EncryptedMessageError, e: + except EncryptedMessageError as e: self.msrp_session.send_report(chunk, 400, str(e)) notification_center = NotificationCenter() notification_center.post_notification('ChatStreamOTRError', sender=self, data=NotificationData(error=str(e))) return - except OTRError, e: + except OTRError as e: self.msrp_session.send_report(chunk, 200, 'OK') notification_center = NotificationCenter() notification_center.post_notification('ChatStreamOTRError', sender=self, data=NotificationData(error=str(e))) return else: encrypted = encryption_active = self.encryption.active if payload.charset is not None: message.content = message.content.decode(payload.charset) elif payload.content_type.startswith('text/'): message.content.decode('utf8') notification_center = NotificationCenter() if message.content_type.lower() == IsComposingDocument.content_type: try: document = IsComposingDocument.parse(message.content) except ParserError as e: self.msrp_session.send_report(chunk, 400, str(e)) return self.msrp_session.send_report(chunk, 200, 'OK') data = NotificationData(state=document.state.value, refresh=document.refresh.value if document.refresh is not None else 120, content_type=document.content_type.value if document.content_type is not None else None, last_active=document.last_active.value if document.last_active is not None else None, sender=message.sender, recipients=message.recipients, private=private, encrypted=encrypted, encryption_active=encryption_active) notification_center.post_notification('ChatStreamGotComposingIndication', sender=self, data=data) else: self.msrp_session.send_report(chunk, 200, 'OK') data = NotificationData(message=message, private=private, encrypted=encrypted, encryption_active=encryption_active) notification_center.post_notification('ChatStreamGotMessage', sender=self, data=data) def _on_transaction_response(self, message_id, response): if message_id in self.sent_messages and response.code != 200: self.sent_messages.remove(message_id) data = NotificationData(message_id=message_id, message=response, code=response.code, reason=response.comment) NotificationCenter().post_notification('ChatStreamDidNotDeliverMessage', sender=self, data=data) def _on_nickname_transaction_response(self, message_id, response): notification_center = NotificationCenter() if response.code == 200: notification_center.post_notification('ChatStreamDidSetNickname', sender=self, data=NotificationData(message_id=message_id, response=response)) else: notification_center.post_notification('ChatStreamDidNotSetNickname', sender=self, data=NotificationData(message_id=message_id, message=response, code=response.code, reason=response.comment)) def _message_queue_handler(self): notification_center = NotificationCenter() try: while True: message = self.message_queue.wait() if self.msrp_session is None: if message.notify_progress: data = NotificationData(message_id=message.id, message=None, code=0, reason='Stream ended') notification_center.post_notification('ChatStreamDidNotDeliverMessage', sender=self, data=data) break try: - if isinstance(message.content, unicode): + if isinstance(message.content, str): message.content = message.content.encode('utf8') charset = 'utf8' else: charset = None if not isinstance(message, QueuedOTRInternalMessage): try: message.content = self.encryption.otr_session.handle_output(message.content, message.content_type) - except OTRError, e: + except OTRError as e: raise ChatStreamError(str(e)) message.sender = message.sender or self.local_identity message.recipients = message.recipients or [self.remote_identity] # check if we MUST use CPIM need_cpim = (message.sender != self.local_identity or message.recipients != [self.remote_identity] or message.courtesy_recipients or message.subject or message.timestamp or message.required or message.additional_headers) if need_cpim or not contains_mime_type(self.remote_accept_types, message.content_type): if not contains_mime_type(self.remote_accept_wrapped_types, message.content_type): raise ChatStreamError('Unsupported content_type for outgoing message: %r' % message.content_type) if not self.cpim_enabled: raise ChatStreamError('Additional message meta-data cannot be sent, because the CPIM wrapper is not used') if not self.private_messages_allowed and message.recipients != [self.remote_identity]: raise ChatStreamError('The remote end does not support private messages') if message.timestamp is None: message.timestamp = ISOTimestamp.now() payload = CPIMPayload(charset=charset, **{name: getattr(message, name) for name in Message.__slots__}) elif self.prefer_cpim and self.cpim_enabled and contains_mime_type(self.remote_accept_wrapped_types, message.content_type): if message.timestamp is None: message.timestamp = ISOTimestamp.now() payload = CPIMPayload(charset=charset, **{name: getattr(message, name) for name in Message.__slots__}) else: payload = SimplePayload(message.content, message.content_type, charset) - except ChatStreamError, e: + except ChatStreamError as e: if message.notify_progress: data = NotificationData(message_id=message.id, message=None, code=0, reason=e.args[0]) notification_center.post_notification('ChatStreamDidNotDeliverMessage', sender=self, data=data) continue else: content, content_type = payload.encode() message_id = message.id notify_progress = message.notify_progress report = 'yes' if notify_progress else 'no' chunk = self.msrp_session.make_message(content, content_type=content_type, message_id=message_id) chunk.add_header(FailureReportHeader(report)) chunk.add_header(SuccessReportHeader(report)) try: self.msrp_session.send_chunk(chunk, response_cb=partial(self._on_transaction_response, message_id)) - except Exception, e: + except Exception as e: if notify_progress: data = NotificationData(message_id=message_id, message=None, code=0, reason=str(e)) notification_center.post_notification('ChatStreamDidNotDeliverMessage', sender=self, data=data) except ProcExit: if notify_progress: data = NotificationData(message_id=message_id, message=None, code=0, reason='Stream ended') notification_center.post_notification('ChatStreamDidNotDeliverMessage', sender=self, data=data) raise else: if notify_progress: self.sent_messages.add(message_id) notification_center.post_notification('ChatStreamDidSendMessage', sender=self, data=NotificationData(message=chunk)) finally: self.message_queue_thread = None while self.sent_messages: message_id = self.sent_messages.pop() data = NotificationData(message_id=message_id, message=None, code=0, reason='Stream ended') notification_center.post_notification('ChatStreamDidNotDeliverMessage', sender=self, data=data) message_queue, self.message_queue = self.message_queue, queue() while message_queue: message = message_queue.wait() if message.notify_progress: data = NotificationData(message_id=message.id, message=None, code=0, reason='Stream ended') notification_center.post_notification('ChatStreamDidNotDeliverMessage', sender=self, data=data) @run_in_twisted_thread def _enqueue_message(self, message): if self._done: if message.notify_progress: data = NotificationData(message_id=message.id, message=None, code=0, reason='Stream ended') NotificationCenter().post_notification('ChatStreamDidNotDeliverMessage', sender=self, data=data) else: self.message_queue.send(message) @run_in_green_thread def _set_local_nickname(self, nickname, message_id): if self.msrp_session is None: # should we generate ChatStreamDidNotSetNickname here? return chunk = self.msrp.make_request('NICKNAME') - chunk.add_header(UseNicknameHeader(nickname or u'')) + chunk.add_header(UseNicknameHeader(nickname or '')) try: self.msrp_session.send_chunk(chunk, response_cb=partial(self._on_nickname_transaction_response, message_id)) - except Exception, e: + except Exception as e: self._failure_reason = str(e) NotificationCenter().post_notification('MediaStreamDidFail', sender=self, data=NotificationData(context='sending', reason=self._failure_reason)) def inject_otr_message(self, data): message = QueuedOTRInternalMessage(data) self._enqueue_message(message) def send_message(self, content, content_type='text/plain', recipients=None, courtesy_recipients=None, subject=None, timestamp=None, required=None, additional_headers=None): message = QueuedMessage(content, content_type, recipients=recipients, courtesy_recipients=courtesy_recipients, subject=subject, timestamp=timestamp, required=required, additional_headers=additional_headers, notify_progress=True) self._enqueue_message(message) return message.id def send_composing_indication(self, state, refresh=None, last_active=None, recipients=None): content = IsComposingDocument.create(state=State(state), refresh=Refresh(refresh) if refresh is not None else None, last_active=LastActive(last_active) if last_active is not None else None, content_type=ContentType('text')) message = QueuedMessage(content, IsComposingDocument.content_type, recipients=recipients, notify_progress=False) self._enqueue_message(message) return message.id def set_local_nickname(self, nickname): if not self.nickname_allowed: raise ChatStreamError('Setting nickname is not supported') message_id = '%x' % random.getrandbits(64) self._set_local_nickname(nickname, message_id) return message_id OTRTransport.register(ChatStream) # Chat related objects, including CPIM support as defined in RFC3862 # class ChatIdentity(object): _format_re = re.compile(r'^(?:"?(?P[^<]*[^"\s])"?)?\s*<(?Psips?:.+)>$') def __init__(self, uri, display_name=None): self.uri = uri self.display_name = display_name def __eq__(self, other): if isinstance(other, ChatIdentity): return self.uri.user == other.uri.user and self.uri.host == other.uri.host elif isinstance(other, BaseSIPURI): return self.uri.user == other.user and self.uri.host == other.host - elif isinstance(other, basestring): + elif isinstance(other, str): try: other_uri = SIPURI.parse(other) except Exception: return False else: return self.uri.user == other_uri.user and self.uri.host == other_uri.host else: return NotImplemented def __ne__(self, other): return not (self == other) def __repr__(self): return '{0.__class__.__name__}(uri={0.uri!r}, display_name={0.display_name!r})'.format(self) def __str__(self): return self.__unicode__().encode('utf-8') def __unicode__(self): if self.display_name: - return u'{0.display_name} <{0.uri}>'.format(self) + return '{0.display_name} <{0.uri}>'.format(self) else: - return u'<{0.uri}>'.format(self) + return '<{0.uri}>'.format(self) @classmethod def parse(cls, value): match = cls._format_re.match(value) if match is None: raise ValueError('Cannot parse identity value: %r' % value) return cls(SIPURI.parse(match.group('uri')), match.group('display_name')) class Message(object): __slots__ = 'content', 'content_type', 'sender', 'recipients', 'courtesy_recipients', 'subject', 'timestamp', 'required', 'additional_headers' def __init__(self, content, content_type, sender=None, recipients=None, courtesy_recipients=None, subject=None, timestamp=None, required=None, additional_headers=None): self.content = content self.content_type = content_type self.sender = sender self.recipients = recipients or [] self.courtesy_recipients = courtesy_recipients or [] self.subject = subject self.timestamp = ISOTimestamp(timestamp) if timestamp is not None else None self.required = required or [] self.additional_headers = additional_headers or [] class QueuedMessage(Message): __slots__ = 'id', 'notify_progress' def __init__(self, content, content_type, sender=None, recipients=None, courtesy_recipients=None, subject=None, timestamp=None, required=None, additional_headers=None, id=None, notify_progress=True): super(QueuedMessage, self).__init__(content, content_type, sender, recipients, courtesy_recipients, subject, timestamp, required, additional_headers) self.id = id or '%x' % random.getrandbits(64) self.notify_progress = notify_progress class QueuedOTRInternalMessage(QueuedMessage): def __init__(self, content): super(QueuedOTRInternalMessage, self).__init__(content, 'text/plain', notify_progress=False) class SimplePayload(object): def __init__(self, content, content_type, charset=None): if not isinstance(content, bytes): raise TypeError("content should be an instance of bytes") self.content = content self.content_type = content_type self.charset = charset def encode(self): if self.charset is not None: return self.content, '{0.content_type}; charset="{0.charset}"'.format(self) else: return self.content, str(self.content_type) @classmethod def decode(cls, content, content_type): if not isinstance(content, bytes): raise TypeError("content should be an instance of bytes") type_helper = EmailParser().parsestr('Content-Type: {}'.format(content_type)) content_type = type_helper.get_content_type() charset = type_helper.get_content_charset() return cls(content, content_type, charset) class CPIMPayload(object): - standard_namespace = u'urn:ietf:params:cpim-headers:' + standard_namespace = 'urn:ietf:params:cpim-headers:' headers_re = re.compile(r'(?:([^:]+?)\.)?(.+?):\s*(.+?)(?:\r\n|$)') subject_re = re.compile(r'^(?:;lang=([a-z]{1,8}(?:-[a-z0-9]{1,8})*)\s+)?(.*)$') namespace_re = re.compile(r'^(?:(\S+) ?)?<(.*)>$') def __init__(self, content, content_type, charset=None, sender=None, recipients=None, courtesy_recipients=None, subject=None, timestamp=None, required=None, additional_headers=None): if not isinstance(content, bytes): raise TypeError("content should be an instance of bytes") self.content = content self.content_type = content_type self.charset = charset self.sender = sender self.recipients = recipients or [] self.courtesy_recipients = courtesy_recipients or [] self.subject = subject if isinstance(subject, (MultilingualText, type(None))) else MultilingualText(subject) self.timestamp = ISOTimestamp(timestamp) if timestamp is not None else None self.required = required or [] self.additional_headers = additional_headers or [] def encode(self): - namespaces = {u'': CPIMNamespace(self.standard_namespace)} + namespaces = {'': CPIMNamespace(self.standard_namespace)} header_list = [] if self.sender is not None: - header_list.append(u'From: {}'.format(self.sender)) - header_list.extend(u'To: {}'.format(recipient) for recipient in self.recipients) - header_list.extend(u'cc: {}'.format(recipient) for recipient in self.courtesy_recipients) + header_list.append('From: {}'.format(self.sender)) + header_list.extend('To: {}'.format(recipient) for recipient in self.recipients) + header_list.extend('cc: {}'.format(recipient) for recipient in self.courtesy_recipients) if self.subject is not None: - header_list.append(u'Subject: {}'.format(self.subject)) - header_list.extend(u'Subject:;lang={} {}'.format(language, translation) for language, translation in self.subject.translations.iteritems()) + header_list.append('Subject: {}'.format(self.subject)) + header_list.extend('Subject:;lang={} {}'.format(language, translation) for language, translation in list(self.subject.translations.items())) if self.timestamp is not None: - header_list.append(u'DateTime: {}'.format(self.timestamp)) + header_list.append('DateTime: {}'.format(self.timestamp)) if self.required: - header_list.append(u'Required: {}'.format(','.join(self.required))) + header_list.append('Required: {}'.format(','.join(self.required))) for header in self.additional_headers: if namespaces.get(header.namespace.prefix) != header.namespace: if header.namespace.prefix: - header_list.append(u'NS: {0.namespace.prefix} <{0.namespace}>'.format(header)) + header_list.append('NS: {0.namespace.prefix} <{0.namespace}>'.format(header)) else: - header_list.append(u'NS: <{0.namespace}>'.format(header)) + header_list.append('NS: <{0.namespace}>'.format(header)) namespaces[header.namespace.prefix] = header.namespace if header.namespace.prefix: - header_list.append(u'{0.namespace.prefix}.{0.name}: {0.value}'.format(header)) + header_list.append('{0.namespace.prefix}.{0.name}: {0.value}'.format(header)) else: - header_list.append(u'{0.name}: {0.value}'.format(header)) + header_list.append('{0.name}: {0.value}'.format(header)) headers = '\r\n'.join(header.encode('cpim-header') for header in header_list) mime_message = EmailMessage() mime_message.set_payload(self.content) mime_message.set_type(self.content_type) if self.charset is not None: mime_message.set_param('charset', self.charset) return headers + '\r\n\r\n' + mime_message.as_string(), 'message/cpim' @classmethod def decode(cls, message): if not isinstance(message, bytes): raise TypeError("message should be an instance of bytes") headers, separator, body = message.partition('\r\n\r\n') if not separator: raise CPIMParserError('Invalid CPIM message') sender = None recipients = [] courtesy_recipients = [] subject = None timestamp = None required = [] additional_headers = [] - namespaces = {u'': CPIMNamespace(cls.standard_namespace)} + namespaces = {'': CPIMNamespace(cls.standard_namespace)} subjects = {} for prefix, name, value in cls.headers_re.findall(headers): namespace = namespaces.get(prefix) if namespace is None or '.' in name: continue try: value = value.decode('cpim-header') if namespace == cls.standard_namespace: if name == 'From': sender = ChatIdentity.parse(value) elif name == 'To': recipients.append(ChatIdentity.parse(value)) elif name == 'cc': courtesy_recipients.append(ChatIdentity.parse(value)) elif name == 'Subject': match = cls.subject_re.match(value) if match is None: raise ValueError('Illegal Subject header: %r' % value) lang, subject = match.groups() # language tags must be ASCII subjects[str(lang) if lang is not None else None] = subject elif name == 'DateTime': timestamp = ISOTimestamp(value) elif name == 'Required': required.extend(re.split(r'\s*,\s*', value)) elif name == 'NS': match = cls.namespace_re.match(value) if match is None: raise ValueError('Illegal NS header: %r' % value) prefix, uri = match.groups() namespaces[prefix] = CPIMNamespace(uri, prefix) else: additional_headers.append(CPIMHeader(name, namespace, value)) else: additional_headers.append(CPIMHeader(name, namespace, value)) except ValueError: pass if None in subjects: subject = MultilingualText(subjects.pop(None), **subjects) elif subjects: subject = MultilingualText(**subjects) mime_message = EmailParser().parsestr(body) content_type = mime_message.get_content_type() if content_type is None: raise CPIMParserError("CPIM message missing Content-Type MIME header") content = mime_message.get_payload() charset = mime_message.get_content_charset() return cls(content, content_type, charset, sender, recipients, courtesy_recipients, subject, timestamp, required, additional_headers) -class CPIMParserError(StandardError): pass +class CPIMParserError(Exception): pass -class CPIMNamespace(unicode): - def __new__(cls, value, prefix=u''): - obj = unicode.__new__(cls, value) +class CPIMNamespace(str): + def __new__(cls, value, prefix=''): + obj = str.__new__(cls, value) obj.prefix = prefix return obj class CPIMHeader(object): def __init__(self, name, namespace, value): self.name = name self.namespace = namespace self.value = value class CPIMCodec(codecs.Codec): - character_map = {c: u'\\u{:04x}'.format(c) for c in range(32) + [127]} - character_map[ord(u'\\')] = u'\\\\' + character_map = {c: '\\u{:04x}'.format(c) for c in list(range(32)) + [127]} + character_map[ord('\\')] = '\\\\' @classmethod def encode(cls, input, errors='strict'): return input.translate(cls.character_map).encode('utf-8', errors), len(input) @classmethod def decode(cls, input, errors='strict'): return input.decode('utf-8', errors).encode('raw-unicode-escape', errors).decode('unicode-escape', errors), len(input) def cpim_codec_search(name): if name.lower() in ('cpim-header', 'cpim_header'): return codecs.CodecInfo(name='CPIM-header', encode=CPIMCodec.encode, decode=CPIMCodec.decode, incrementalencoder=codecs.IncrementalEncoder, incrementaldecoder=codecs.IncrementalDecoder, streamwriter=codecs.StreamWriter, streamreader=codecs.StreamReader) codecs.register(cpim_codec_search) del cpim_codec_search diff --git a/sipsimple/streams/msrp/filetransfer.py b/sipsimple/streams/msrp/filetransfer.py index 01d29fdc..af2cdd93 100644 --- a/sipsimple/streams/msrp/filetransfer.py +++ b/sipsimple/streams/msrp/filetransfer.py @@ -1,737 +1,735 @@ """ This module provides classes to parse and generate SDP related to SIP sessions that negotiate File Transfer. """ __all__ = ['FileTransferStream', 'FileSelector'] -import cPickle as pickle +import pickle as pickle import hashlib import mimetypes import os import random import re import time import uuid from abc import ABCMeta, abstractmethod from application.notification import NotificationCenter, NotificationData, IObserver from application.python.threadpool import ThreadPool, run_in_threadpool from application.python.types import MarkerType from application.system import FileExistsError, makedirs, openfile, unlink from itertools import count from msrplib.protocol import FailureReportHeader, SuccessReportHeader, ContentTypeHeader, IntegerHeaderType, MSRPNamedHeader, HeaderParsingError from msrplib.session import MSRPSession from msrplib.transport import make_response -from Queue import Queue +from queue import Queue from threading import Event, Lock from zope.interface import implements from sipsimple.configuration.settings import SIPSimpleSettings from sipsimple.core import SDPAttribute from sipsimple.storage import ISIPSimpleApplicationDataStorage from sipsimple.streams import InvalidStreamError, UnknownStreamError from sipsimple.streams.msrp import MSRPStreamBase from sipsimple.threading import run_in_twisted_thread, run_in_thread from sipsimple.util import sha1 HASH = type(hashlib.sha1()) -class RandomID: __metaclass__ = MarkerType +class RandomID(metaclass=MarkerType): pass class FileSelectorHash(str): _hash_re = re.compile(r'^sha-1(:[0-9A-F]{2}){20}$') _byte_re = re.compile(r'..') def __new__(cls, value): if isinstance(value, str): if value.startswith('sha1:'): # backward compatibility hack (sort of). value = 'sha-1' + value[len('sha1'):] if not cls._hash_re.match(value): raise ValueError("Invalid hash value: {!r}".format(value)) return super(FileSelectorHash, cls).__new__(cls, value) elif isinstance(value, (HASH, sha1)): return super(FileSelectorHash, cls).__new__(cls, cls.encode_hash(value)) else: raise ValueError("Invalid hash value: {!r}".format(value)) def __eq__(self, other): if isinstance(other, str): return super(FileSelectorHash, self).__eq__(other) elif isinstance(other, (HASH, sha1)) and other.name.lower() == 'sha1': return super(FileSelectorHash, self).__eq__(self.encode_hash(other)) else: return NotImplemented def __ne__(self, other): return not self == other @classmethod def encode_hash(cls, hash_instance): if hash_instance.name.lower() != 'sha1': raise TypeError("Invalid hash type: {.name} (only sha1 hashes are supported).".format(hash_instance)) # unexpected as it may be, using a regular expression is the fastest method to do this return 'sha-1:' + ':'.join(cls._byte_re.findall(hash_instance.hexdigest().upper())) class FileSelector(object): _name_re = re.compile(r'name:"([^"]+)"') _size_re = re.compile(r'size:(\d+)') _type_re = re.compile(r'type:([^ ]+)') _hash_re = re.compile(r'hash:([^ ]+)') def __init__(self, name=None, type=None, size=None, hash=None, fd=None): # If present, hash should be a sha1 object or a string in the form: sha-1:72:24:5F:E8:65:3D:DA:F3:71:36:2F:86:D4:71:91:3E:E4:A2:CE:2E # According to the specification, only sha1 is supported ATM. self.name = name self.type = type self.size = size self.hash = hash self.fd = fd @property def hash(self): return self.__dict__['hash'] @hash.setter def hash(self, value): self.__dict__['hash'] = None if value is None else FileSelectorHash(value) @classmethod def parse(cls, string): name_match = cls._name_re.search(string) size_match = cls._size_re.search(string) type_match = cls._type_re.search(string) hash_match = cls._hash_re.search(string) name = name_match and name_match.group(1).decode('utf-8') size = size_match and int(size_match.group(1)) type = type_match and type_match.group(1) hash = hash_match and hash_match.group(1) return cls(name, type, size, hash) @classmethod def for_file(cls, path, type=None, hash=None): - name = unicode(path) + name = str(path) fd = open(name, 'rb') size = os.fstat(fd.fileno()).st_size if type is None: mime_type, encoding = mimetypes.guess_type(name) if encoding is not None: type = 'application/x-%s' % encoding elif mime_type is not None: type = mime_type else: type = 'application/octet-stream' return cls(name, type, size, hash, fd) @property def sdp_repr(self): items = [('name', self.name and '"%s"' % os.path.basename(self.name).encode('utf-8')), ('type', self.type), ('size', self.size), ('hash', self.hash)] return ' '.join('%s:%s' % (name, value) for name, value in items if value is not None) class UniqueFilenameGenerator(object): @classmethod def generate(cls, name): yield name prefix, extension = os.path.splitext(name) for x in count(1): yield "%s-%d%s" % (prefix, x, extension) class FileMetadataEntry(object): def __init__(self, hash, filename, partial_hash=None): self.hash = hash self.filename = filename self.mtime = os.path.getmtime(self.filename) self.partial_hash = partial_hash @classmethod def from_selector(cls, file_selector): return cls(file_selector.hash.lower(), file_selector.name) class FileTransfersMetadata(object): __filename__ = 'transfer_metadata' __lifetime__ = 60*60*24*7 def __init__(self): self.data = {} self.lock = Lock() self.loaded = False self.directory = None def _load(self): if self.loaded: return from sipsimple.application import SIPApplication if ISIPSimpleApplicationDataStorage.providedBy(SIPApplication.storage): self.directory = SIPApplication.storage.directory if self.directory is not None: try: with open(os.path.join(self.directory, self.__filename__), 'rb') as f: data = pickle.loads(f.read()) except Exception: data = {} now = time.time() - for hash, entry in data.items(): + for hash, entry in list(data.items()): try: mtime = os.path.getmtime(entry.filename) except OSError: data.pop(hash) else: if mtime != entry.mtime or now - mtime > self.__lifetime__: data.pop(hash) self.data.update(data) self.loaded = True @run_in_thread('file-io') def _save(self, data): if self.directory is not None: with open(os.path.join(self.directory, self.__filename__), 'wb') as f: f.write(data) def __enter__(self): self.lock.acquire() self._load() return self.data def __exit__(self, exc_type, exc_val, exc_tb): if None is exc_type is exc_val is exc_tb: self._save(pickle.dumps(self.data)) self.lock.release() -class FileTransferHandler(object): - __metaclass__ = ABCMeta - +class FileTransferHandler(object, metaclass=ABCMeta): implements(IObserver) threadpool = ThreadPool(name='FileTransfers', min_threads=0, max_threads=100) threadpool.start() def __init__(self): self.stream = None self.session = None self._started = False self._session_started = False self._initialize_done = False self._initialize_successful = False def initialize(self, stream, session): self.stream = stream self.session = session notification_center = NotificationCenter() notification_center.add_observer(self, sender=stream) notification_center.add_observer(self, sender=session) notification_center.add_observer(self, sender=self) @property def filename(self): return self.stream.file_selector.name if self.stream is not None else None @abstractmethod def start(self): raise NotImplementedError @abstractmethod def end(self): raise NotImplementedError @abstractmethod def process_chunk(self, chunk): raise NotImplementedError def __terminate(self): notification_center = NotificationCenter() notification_center.remove_observer(self, sender=self.stream) notification_center.remove_observer(self, sender=self.session) notification_center.remove_observer(self, sender=self) try: self.stream.file_selector.fd.close() except AttributeError: # when self.stream.file_selector.fd is None pass except IOError: # we can get this if we try to close while another thread is reading from it pass self.stream = None self.session = None @run_in_twisted_thread def handle_notification(self, notification): handler = getattr(self, '_NH_%s' % notification.name, None) if handler is not None: handler(notification) def _NH_MediaStreamDidNotInitialize(self, notification): if not self._initialize_done: self.end() self.__terminate() def _NH_MediaStreamDidStart(self, notification): self._started = True self.start() def _NH_MediaStreamWillEnd(self, notification): if self._started: self.end() elif self._session_started: notification.center.post_notification('FileTransferHandlerDidEnd', sender=self, data=NotificationData(error=True, reason='Refused')) def _NH_SIPSessionWillStart(self, notification): self._session_started = True def _NH_SIPSessionDidFail(self, notification): if not self._session_started and self._initialize_successful: if notification.data.code == 487: reason = 'Cancelled' else: reason = notification.data.reason or 'Failed' notification.center.post_notification('FileTransferHandlerDidEnd', sender=self, data=NotificationData(error=True, reason=reason)) def _NH_FileTransferHandlerDidInitialize(self, notification): self._initialize_done = True self._initialize_successful = True def _NH_FileTransferHandlerDidNotInitialize(self, notification): self._initialize_done = True self._initialize_successful = False def _NH_FileTransferHandlerDidEnd(self, notification): self.__terminate() class OffsetHeader(MSRPNamedHeader): name = 'Offset' type = IntegerHeaderType -class EndTransfer: __metaclass__ = MarkerType +class EndTransfer(metaclass=MarkerType): pass class IncomingFileTransferHandler(FileTransferHandler): metadata = FileTransfersMetadata() def __init__(self): super(IncomingFileTransferHandler, self).__init__() self.hash = sha1() self.queue = Queue() self.offset = 0 self.received_chunks = 0 @property def save_directory(self): return self.__dict__.get('save_directory') @save_directory.setter def save_directory(self, value): if self.stream is not None: raise AttributeError('cannot set save_directory, transfer is in progress') self.__dict__['save_directory'] = value def initialize(self, stream, session): super(IncomingFileTransferHandler, self).initialize(stream, session) try: directory = self.save_directory or SIPSimpleSettings().file_transfer.directory.normalized makedirs(directory) with self.metadata as metadata: try: prev_file = metadata.pop(stream.file_selector.hash.lower()) mtime = os.path.getmtime(prev_file.filename) if mtime != prev_file.mtime: raise ValueError('file was modified') filename = os.path.join(directory, os.path.basename(stream.file_selector.name)) try: os.link(prev_file.filename, filename) except (AttributeError, OSError): stream.file_selector.name = prev_file.filename else: stream.file_selector.name = filename unlink(prev_file.filename) stream.file_selector.fd = openfile(stream.file_selector.name, 'ab') # open doesn't seek to END in append mode on win32 until first write, but openfile does self.offset = stream.file_selector.fd.tell() self.hash = prev_file.partial_hash except (KeyError, EnvironmentError, ValueError): for name in UniqueFilenameGenerator.generate(os.path.join(directory, os.path.basename(stream.file_selector.name))): try: stream.file_selector.fd = openfile(name, 'xb') except FileExistsError: continue else: stream.file_selector.name = name break - except Exception, e: + except Exception as e: NotificationCenter().post_notification('FileTransferHandlerDidNotInitialize', sender=self, data=NotificationData(reason=str(e))) else: NotificationCenter().post_notification('FileTransferHandlerDidInitialize', sender=self) def end(self): self.queue.put(EndTransfer) def process_chunk(self, chunk): if chunk.method == 'SEND': if not self.received_chunks and chunk.byte_range.start == 1: self.stream.file_selector.fd.truncate(0) self.stream.file_selector.fd.seek(0) self.hash = sha1() self.offset = 0 self.received_chunks += 1 self.queue.put(chunk) elif chunk.method == 'FILE_OFFSET': if self.received_chunks > 0: response = make_response(chunk, 413, 'Unwanted message') else: offset = self.stream.file_selector.fd.tell() response = make_response(chunk, 200, 'OK') response.add_header(OffsetHeader(offset)) self.stream.msrp_session.send_chunk(response) @run_in_threadpool(FileTransferHandler.threadpool) def start(self): notification_center = NotificationCenter() notification_center.post_notification('FileTransferHandlerDidStart', sender=self) file_selector = self.stream.file_selector fd = file_selector.fd while True: chunk = self.queue.get() if chunk is EndTransfer: break try: fd.write(chunk.data) - except EnvironmentError, e: + except EnvironmentError as e: fd.close() notification_center.post_notification('FileTransferHandlerError', sender=self, data=NotificationData(error=str(e))) notification_center.post_notification('FileTransferHandlerDidEnd', sender=self, data=NotificationData(error=True, reason=str(e))) return self.hash.update(chunk.data) self.offset += chunk.size transferred_bytes = chunk.byte_range.start + chunk.size - 1 total_bytes = file_selector.size = chunk.byte_range.total notification_center.post_notification('FileTransferHandlerProgress', sender=self, data=NotificationData(transferred_bytes=transferred_bytes, total_bytes=total_bytes)) if transferred_bytes == total_bytes: break fd.close() # Transfer is finished if self.offset != self.stream.file_selector.size: notification_center.post_notification('FileTransferHandlerDidEnd', sender=self, data=NotificationData(error=True, reason='Incomplete file')) return if self.hash != self.stream.file_selector.hash: unlink(self.filename) # something got corrupted, better delete the file notification_center.post_notification('FileTransferHandlerDidEnd', sender=self, data=NotificationData(error=True, reason='File hash mismatch')) return notification_center.post_notification('FileTransferHandlerDidEnd', sender=self, data=NotificationData(error=False, reason=None)) def _NH_MediaStreamDidNotInitialize(self, notification): if self.stream.file_selector.fd is not None: position = self.stream.file_selector.fd.tell() self.stream.file_selector.fd.close() if position == 0: unlink(self.stream.file_selector.name) super(IncomingFileTransferHandler, self)._NH_MediaStreamDidNotInitialize(notification) def _NH_FileTransferHandlerDidEnd(self, notification): if notification.data.error and self.stream.file_selector.hash is not None: if os.path.getsize(self.stream.file_selector.name) == 0: unlink(self.stream.file_selector.name) else: with self.metadata as metadata: entry = FileMetadataEntry.from_selector(self.stream.file_selector) entry.partial_hash = self.hash metadata[entry.hash] = entry super(IncomingFileTransferHandler, self)._NH_FileTransferHandlerDidEnd(notification) class OutgoingFileTransferHandler(FileTransferHandler): file_part_size = 64*1024 def __init__(self): super(OutgoingFileTransferHandler, self).__init__() self.stop_event = Event() self.finished_event = Event() self.file_offset_event = Event() self.message_id = '%x' % random.getrandbits(64) self.offset = 0 def initialize(self, stream, session): super(OutgoingFileTransferHandler, self).initialize(stream, session) if stream.file_selector.fd is None: NotificationCenter().post_notification('FileTransferHandlerDidNotInitialize', sender=self, data=NotificationData(reason='file descriptor not specified')) return if stream.file_selector.size == 0: NotificationCenter().post_notification('FileTransferHandlerDidNotInitialize', sender=self, data=NotificationData(reason='file is empty')) return if stream.file_selector.hash is None: self._calculate_file_hash() else: NotificationCenter().post_notification('FileTransferHandlerDidInitialize', sender=self) @run_in_threadpool(FileTransferHandler.threadpool) def _calculate_file_hash(self): file_hash = hashlib.sha1() processed = 0 notification_center = NotificationCenter() notification_center.post_notification('FileTransferHandlerHashProgress', sender=self, data=NotificationData(processed=0, total=self.stream.file_selector.size)) file_selector = self.stream.file_selector fd = file_selector.fd while not self.stop_event.is_set(): try: content = fd.read(self.file_part_size) - except EnvironmentError, e: + except EnvironmentError as e: fd.close() notification_center.post_notification('FileTransferHandlerDidNotInitialize', sender=self, data=NotificationData(reason=str(e))) return if not content: file_selector.hash = file_hash notification_center.post_notification('FileTransferHandlerDidInitialize', sender=self) break file_hash.update(content) processed += len(content) notification_center.post_notification('FileTransferHandlerHashProgress', sender=self, data=NotificationData(processed=processed, total=file_selector.size)) else: fd.close() notification_center.post_notification('FileTransferHandlerDidNotInitialize', sender=self, data=NotificationData(reason='Interrupted transfer')) def end(self): self.stop_event.set() self.file_offset_event.set() # in case we are busy waiting on it @run_in_threadpool(FileTransferHandler.threadpool) def start(self): notification_center = NotificationCenter() notification_center.post_notification('FileTransferHandlerDidStart', sender=self) if self.stream.file_offset_supported: self._send_file_offset_chunk() self.file_offset_event.wait() finished = False failure_reason = None fd = self.stream.file_selector.fd fd.seek(self.offset) try: while not self.stop_event.is_set(): try: data = fd.read(self.file_part_size) - except EnvironmentError, e: + except EnvironmentError as e: failure_reason = str(e) break if not data: finished = True break self._send_chunk(data) finally: fd.close() if not finished: notification_center.post_notification('FileTransferHandlerDidEnd', sender=self, data=NotificationData(error=True, reason=failure_reason or 'Interrupted transfer')) return # Wait until the stream ends or we get all reports self.stop_event.wait() if self.finished_event.is_set(): notification_center.post_notification('FileTransferHandlerDidEnd', sender=self, data=NotificationData(error=False, reason=None)) else: notification_center.post_notification('FileTransferHandlerDidEnd', sender=self, data=NotificationData(error=True, reason='Incomplete transfer')) def _on_transaction_response(self, response): if self.stop_event.is_set(): return if response.code != 200: NotificationCenter().post_notification('FileTransferHandlerError', sender=self, data=NotificationData(error=response.comment)) self.end() @run_in_twisted_thread def _send_chunk(self, data): if self.stop_event.is_set(): return data_len = len(data) chunk = self.stream.msrp.make_send_request(message_id=self.message_id, data=data, start=self.offset+1, end=self.offset+data_len, length=self.stream.file_selector.size) chunk.add_header(ContentTypeHeader(self.stream.file_selector.type)) chunk.add_header(SuccessReportHeader('yes')) chunk.add_header(FailureReportHeader('yes')) try: self.stream.msrp_session.send_chunk(chunk, response_cb=self._on_transaction_response) - except Exception, e: + except Exception as e: NotificationCenter().post_notification('FileTransferHandlerError', sender=self, data=NotificationData(error=str(e))) else: self.offset += data_len @run_in_twisted_thread def _send_file_offset_chunk(self): def response_cb(response): if not self.stop_event.is_set() and response.code == 200: try: offset = response.headers['Offset'].decoded except (KeyError, HeaderParsingError): offset = 0 self.offset = offset self.file_offset_event.set() if self.stop_event.is_set(): self.file_offset_event.set() return chunk = self.stream.msrp.make_request('FILE_OFFSET') # TODO: _ is illegal in MSRP method names according to RFC 4975 try: self.stream.msrp_session.send_chunk(chunk, response_cb=response_cb) - except Exception, e: + except Exception as e: NotificationCenter().post_notification('FileTransferHandlerError', sender=self, data=NotificationData(error=str(e))) def process_chunk(self, chunk): # here we process the REPORT chunks notification_center = NotificationCenter() if chunk.status.code == 200: transferred_bytes = chunk.byte_range.end total_bytes = chunk.byte_range.total notification_center.post_notification('FileTransferHandlerProgress', sender=self, data=NotificationData(transferred_bytes=transferred_bytes, total_bytes=total_bytes)) if transferred_bytes == total_bytes: self.finished_event.set() self.end() else: notification_center.post_notification('FileTransferHandlerError', sender=self, data=NotificationData(error=chunk.status.comment)) self.end() class FileTransferMSRPSession(MSRPSession): def _handle_incoming_FILE_OFFSET(self, chunk): self._on_incoming_cb(chunk) class FileTransferStream(MSRPStreamBase): type = 'file-transfer' priority = 10 msrp_session_class = FileTransferMSRPSession media_type = 'message' accept_types = ['*'] accept_wrapped_types = None IncomingTransferHandler = IncomingFileTransferHandler OutgoingTransferHandler = OutgoingFileTransferHandler def __init__(self, file_selector, direction, transfer_id=RandomID): if direction not in ('sendonly', 'recvonly'): raise ValueError("direction must be one of 'sendonly' or 'recvonly'") super(FileTransferStream, self).__init__(direction=direction) self.file_selector = file_selector self.transfer_id = transfer_id if transfer_id is not RandomID else str(uuid.uuid4()) if direction == 'sendonly': self.handler = self.OutgoingTransferHandler() else: self.handler = self.IncomingTransferHandler() @classmethod def new_from_sdp(cls, session, remote_sdp, stream_index): remote_stream = remote_sdp.media[stream_index] if remote_stream.media != 'message' or 'file-selector' not in remote_stream.attributes: raise UnknownStreamError expected_transport = 'TCP/TLS/MSRP' if session.account.msrp.transport == 'tls' else 'TCP/MSRP' if remote_stream.transport != expected_transport: raise InvalidStreamError("expected %s transport in file transfer stream, got %s" % (expected_transport, remote_stream.transport)) if remote_stream.formats != ['*']: raise InvalidStreamError("wrong format list specified") try: file_selector = FileSelector.parse(remote_stream.attributes.getfirst('file-selector')) except Exception as e: raise InvalidStreamError("error parsing file-selector: {}".format(e)) transfer_id = remote_stream.attributes.getfirst('file-transfer-id', None) if remote_stream.direction == 'sendonly': stream = cls(file_selector, 'recvonly', transfer_id) elif remote_stream.direction == 'recvonly': stream = cls(file_selector, 'sendonly', transfer_id) else: raise InvalidStreamError("wrong stream direction specified") stream.remote_role = remote_stream.attributes.getfirst('setup', 'active') return stream def initialize(self, session, direction): self._initialize_args = session, direction NotificationCenter().add_observer(self, sender=self.handler) self.handler.initialize(self, session) def _create_local_media(self, uri_path): local_media = super(FileTransferStream, self)._create_local_media(uri_path) local_media.attributes.append(SDPAttribute('file-selector', self.file_selector.sdp_repr)) local_media.attributes.append(SDPAttribute('x-file-offset', '')) if self.transfer_id is not None: local_media.attributes.append(SDPAttribute('file-transfer-id', self.transfer_id)) return local_media @property def file_offset_supported(self): try: return 'x-file-offset' in self.remote_media.attributes except AttributeError: return False @run_in_twisted_thread def _NH_FileTransferHandlerDidInitialize(self, notification): session, direction = self._initialize_args del self._initialize_args if not self._done: super(FileTransferStream, self).initialize(session, direction) @run_in_twisted_thread def _NH_FileTransferHandlerDidNotInitialize(self, notification): del self._initialize_args if not self._done: notification.center.post_notification('MediaStreamDidNotInitialize', sender=self, data=notification.data) @run_in_twisted_thread def _NH_FileTransferHandlerError(self, notification): self._failure_reason = notification.data.error notification.center.post_notification('MediaStreamDidFail', sender=self, data=NotificationData(context='transferring', reason=self._failure_reason)) def _NH_MediaStreamDidNotInitialize(self, notification): notification.center.remove_observer(self, sender=self.handler) def _NH_MediaStreamWillEnd(self, notification): notification.center.remove_observer(self, sender=self.handler) def _handle_REPORT(self, chunk): # in theory, REPORT can come with Byte-Range which would limit the scope of the REPORT to the part of the message. self.handler.process_chunk(chunk) def _handle_SEND(self, chunk): notification_center = NotificationCenter() if chunk.size == 0: # keep-alive self.msrp_session.send_report(chunk, 200, 'OK') return if self.direction=='sendonly': self.msrp_session.send_report(chunk, 413, 'Unwanted Message') return if chunk.content_type.lower() == 'message/cpim': # In order to properly support the CPIM wrapper, msrplib needs to be refactored. -Luci self.msrp_session.send_report(chunk, 415, 'Invalid Content-Type') self._failure_reason = "CPIM wrapper is not supported" notification_center.post_notification('MediaStreamDidFail', sender=self, data=NotificationData(context='reading', reason=self._failure_reason)) return try: self.msrp_session.send_report(chunk, 200, 'OK') except Exception: pass # Best effort approach: even if we couldn't send the REPORT keep writing the chunks, we might have them all -Saul self.handler.process_chunk(chunk) def _handle_FILE_OFFSET(self, chunk): if self.direction != 'recvonly': response = make_response(chunk, 413, 'Unwanted message') self.msrp_session.send_chunk(response) return self.handler.process_chunk(chunk) diff --git a/sipsimple/streams/msrp/screensharing.py b/sipsimple/streams/msrp/screensharing.py index d55eaa72..3cbfecaf 100644 --- a/sipsimple/streams/msrp/screensharing.py +++ b/sipsimple/streams/msrp/screensharing.py @@ -1,353 +1,351 @@ """ This module provides classes to parse and generate SDP related to SIP sessions that negotiate Screen Sharing. """ __all__ = ['ScreenSharingStream', 'VNCConnectionError', 'ScreenSharingHandler', 'ScreenSharingServerHandler', 'ScreenSharingViewerHandler', 'InternalVNCViewerHandler', 'InternalVNCServerHandler', 'ExternalVNCViewerHandler', 'ExternalVNCServerHandler'] from abc import ABCMeta, abstractmethod, abstractproperty from application.notification import NotificationCenter, NotificationData, IObserver from application.python.descriptor import WriteOnceAttribute from eventlib.coros import queue from eventlib.greenio import GreenSocket from eventlib.proc import spawn from eventlib.util import tcp_socket, set_reuse_addr from msrplib.protocol import FailureReportHeader, SuccessReportHeader, ContentTypeHeader from msrplib.transport import make_response, make_report from twisted.internet.error import ConnectionDone from zope.interface import implements from sipsimple.core import SDPAttribute from sipsimple.streams import InvalidStreamError, UnknownStreamError from sipsimple.streams.msrp import MSRPStreamBase from sipsimple.threading import run_in_twisted_thread class VNCConnectionError(Exception): pass -class ScreenSharingHandler(object): - __metaclass__ = ABCMeta - +class ScreenSharingHandler(object, metaclass=ABCMeta): implements(IObserver) def __init__(self): self.incoming_msrp_queue = None self.outgoing_msrp_queue = None self.msrp_reader_thread = None self.msrp_writer_thread = None def initialize(self, stream): self.incoming_msrp_queue = stream.incoming_queue self.outgoing_msrp_queue = stream.outgoing_queue NotificationCenter().add_observer(self, sender=stream) @abstractproperty def type(self): raise NotImplementedError @abstractmethod def _msrp_reader(self): raise NotImplementedError @abstractmethod def _msrp_writer(self): raise NotImplementedError def handle_notification(self, notification): handler = getattr(self, '_NH_%s' % notification.name, None) if handler is not None: handler(notification) def _NH_MediaStreamDidStart(self, notification): self.msrp_reader_thread = spawn(self._msrp_reader) self.msrp_writer_thread = spawn(self._msrp_writer) def _NH_MediaStreamWillEnd(self, notification): notification.center.remove_observer(self, sender=notification.sender) if self.msrp_reader_thread is not None: self.msrp_reader_thread.kill() self.msrp_reader_thread = None if self.msrp_writer_thread is not None: self.msrp_writer_thread.kill() self.msrp_writer_thread = None class ScreenSharingServerHandler(ScreenSharingHandler): type = property(lambda self: 'passive') class ScreenSharingViewerHandler(ScreenSharingHandler): type = property(lambda self: 'active') class InternalVNCViewerHandler(ScreenSharingViewerHandler): @run_in_twisted_thread def send(self, data): self.outgoing_msrp_queue.send(data) def _msrp_reader(self): notification_center = NotificationCenter() while True: data = self.incoming_msrp_queue.wait() notification_center.post_notification('ScreenSharingStreamGotData', sender=self, data=NotificationData(data=data)) def _msrp_writer(self): pass class InternalVNCServerHandler(ScreenSharingServerHandler): @run_in_twisted_thread def send(self, data): self.outgoing_msrp_queue.send(data) def _msrp_reader(self): notification_center = NotificationCenter() while True: data = self.incoming_msrp_queue.wait() notification_center.post_notification('ScreenSharingStreamGotData', sender=self, data=NotificationData(data=data)) def _msrp_writer(self): pass class ExternalVNCViewerHandler(ScreenSharingViewerHandler): address = ('localhost', 0) connect_timeout = 5 def __init__(self): super(ExternalVNCViewerHandler, self).__init__() self.vnc_starter_thread = None self.vnc_socket = GreenSocket(tcp_socket()) set_reuse_addr(self.vnc_socket) self.vnc_socket.settimeout(self.connect_timeout) self.vnc_socket.bind(self.address) self.vnc_socket.listen(1) self.address = self.vnc_socket.getsockname() def _msrp_reader(self): while True: try: data = self.incoming_msrp_queue.wait() self.vnc_socket.sendall(data) - except Exception, e: + except Exception as e: self.msrp_reader_thread = None # avoid issues caused by the notification handler killing this greenlet during post_notification NotificationCenter().post_notification('ScreenSharingHandlerDidFail', sender=self, data=NotificationData(context='sending', reason=str(e))) break def _msrp_writer(self): while True: try: data = self.vnc_socket.recv(2048) if not data: raise VNCConnectionError("connection with the VNC viewer was closed") self.outgoing_msrp_queue.send(data) - except Exception, e: + except Exception as e: self.msrp_writer_thread = None # avoid issues caused by the notification handler killing this greenlet during post_notification NotificationCenter().post_notification('ScreenSharingHandlerDidFail', sender=self, data=NotificationData(context='reading', reason=str(e))) break def _start_vnc_connection(self): try: sock, addr = self.vnc_socket.accept() self.vnc_socket.close() self.vnc_socket = sock self.vnc_socket.settimeout(None) - except Exception, e: + except Exception as e: self.vnc_starter_thread = None # avoid issues caused by the notification handler killing this greenlet during post_notification NotificationCenter().post_notification('ScreenSharingHandlerDidFail', sender=self, data=NotificationData(context='connecting', reason=str(e))) else: self.msrp_reader_thread = spawn(self._msrp_reader) self.msrp_writer_thread = spawn(self._msrp_writer) finally: self.vnc_starter_thread = None def _NH_MediaStreamDidStart(self, notification): self.vnc_starter_thread = spawn(self._start_vnc_connection) def _NH_MediaStreamWillEnd(self, notification): if self.vnc_starter_thread is not None: self.vnc_starter_thread.kill() self.vnc_starter_thread = None super(ExternalVNCViewerHandler, self)._NH_MediaStreamWillEnd(notification) self.vnc_socket.close() class ExternalVNCServerHandler(ScreenSharingServerHandler): address = ('localhost', 5900) connect_timeout = 5 def __init__(self): super(ExternalVNCServerHandler, self).__init__() self.vnc_starter_thread = None self.vnc_socket = None def _msrp_reader(self): while True: try: data = self.incoming_msrp_queue.wait() self.vnc_socket.sendall(data) - except Exception, e: + except Exception as e: self.msrp_reader_thread = None # avoid issues caused by the notification handler killing this greenlet during post_notification NotificationCenter().post_notification('ScreenSharingHandlerDidFail', sender=self, data=NotificationData(context='sending', reason=str(e))) break def _msrp_writer(self): while True: try: data = self.vnc_socket.recv(2048) if not data: raise VNCConnectionError("connection to the VNC server was closed") self.outgoing_msrp_queue.send(data) - except Exception, e: + except Exception as e: self.msrp_writer_thread = None # avoid issues caused by the notification handler killing this greenlet during post_notification NotificationCenter().post_notification('ScreenSharingHandlerDidFail', sender=self, data=NotificationData(context='reading', reason=str(e))) break def _start_vnc_connection(self): try: self.vnc_socket = GreenSocket(tcp_socket()) self.vnc_socket.settimeout(self.connect_timeout) self.vnc_socket.connect(self.address) self.vnc_socket.settimeout(None) - except Exception, e: + except Exception as e: self.vnc_starter_thread = None # avoid issues caused by the notification handler killing this greenlet during post_notification NotificationCenter().post_notification('ScreenSharingHandlerDidFail', sender=self, data=NotificationData(context='connecting', reason=str(e))) else: self.msrp_reader_thread = spawn(self._msrp_reader) self.msrp_writer_thread = spawn(self._msrp_writer) finally: self.vnc_starter_thread = None def _NH_MediaStreamDidStart(self, notification): self.vnc_starter_thread = spawn(self._start_vnc_connection) def _NH_MediaStreamWillEnd(self, notification): if self.vnc_starter_thread is not None: self.vnc_starter_thread.kill() self.vnc_starter_thread = None super(ExternalVNCServerHandler, self)._NH_MediaStreamWillEnd(notification) if self.vnc_socket is not None: self.vnc_socket.close() class ScreenSharingStream(MSRPStreamBase): type = 'screen-sharing' priority = 1 media_type = 'application' accept_types = ['application/x-rfb'] accept_wrapped_types = None ServerHandler = InternalVNCServerHandler ViewerHandler = InternalVNCViewerHandler handler = WriteOnceAttribute() def __init__(self, mode): if mode not in ('viewer', 'server'): raise ValueError("mode should be 'viewer' or 'server' not '%s'" % mode) super(ScreenSharingStream, self).__init__(direction='sendrecv') self.handler = self.ViewerHandler() if mode=='viewer' else self.ServerHandler() self.incoming_queue = queue() self.outgoing_queue = queue() self.msrp_reader_thread = None self.msrp_writer_thread = None @classmethod def new_from_sdp(cls, session, remote_sdp, stream_index): remote_stream = remote_sdp.media[stream_index] if remote_stream.media != 'application': raise UnknownStreamError accept_types = remote_stream.attributes.getfirst('accept-types', None) if accept_types is None or 'application/x-rfb' not in accept_types.split(): raise UnknownStreamError expected_transport = 'TCP/TLS/MSRP' if session.account.msrp.transport=='tls' else 'TCP/MSRP' if remote_stream.transport != expected_transport: raise InvalidStreamError("expected %s transport in chat stream, got %s" % (expected_transport, remote_stream.transport)) if remote_stream.formats != ['*']: raise InvalidStreamError("wrong format list specified") remote_rfbsetup = remote_stream.attributes.getfirst('rfbsetup', 'active') if remote_rfbsetup == 'active': stream = cls(mode='server') elif remote_rfbsetup == 'passive': stream = cls(mode='viewer') else: raise InvalidStreamError("unknown rfbsetup attribute in the remote screen sharing stream") stream.remote_role = remote_stream.attributes.getfirst('setup', 'active') return stream def _create_local_media(self, uri_path): local_media = super(ScreenSharingStream, self)._create_local_media(uri_path) local_media.attributes.append(SDPAttribute('rfbsetup', self.handler.type)) return local_media def _msrp_reader(self): while True: try: chunk = self.msrp.read_chunk() if chunk.method in (None, 'REPORT'): continue elif chunk.method == 'SEND': if chunk.content_type in self.accept_types: self.incoming_queue.send(chunk.data) response = make_response(chunk, 200, 'OK') report = make_report(chunk, 200, 'OK') else: response = make_response(chunk, 415, 'Invalid Content-Type') report = None else: response = make_response(chunk, 501, 'Unknown method') report = None if response is not None: self.msrp.write_chunk(response) if report is not None: self.msrp.write_chunk(report) - except Exception, e: + except Exception as e: self.msrp_reader_thread = None # avoid issues caused by the notification handler killing this greenlet during post_notification if self.shutting_down and isinstance(e, ConnectionDone): break self._failure_reason = str(e) NotificationCenter().post_notification('MediaStreamDidFail', sender=self, data=NotificationData(context='reading', reason=self._failure_reason)) break def _msrp_writer(self): while True: try: data = self.outgoing_queue.wait() chunk = self.msrp.make_send_request(data=data) chunk.add_header(SuccessReportHeader('no')) chunk.add_header(FailureReportHeader('partial')) chunk.add_header(ContentTypeHeader('application/x-rfb')) self.msrp.write_chunk(chunk) - except Exception, e: + except Exception as e: self.msrp_writer_thread = None # avoid issues caused by the notification handler killing this greenlet during post_notification if self.shutting_down and isinstance(e, ConnectionDone): break self._failure_reason = str(e) NotificationCenter().post_notification('MediaStreamDidFail', sender=self, data=NotificationData(context='sending', reason=self._failure_reason)) break def _NH_MediaStreamDidInitialize(self, notification): notification.center.add_observer(self, sender=self.handler) self.handler.initialize(self) def _NH_MediaStreamDidStart(self, notification): self.msrp_reader_thread = spawn(self._msrp_reader) self.msrp_writer_thread = spawn(self._msrp_writer) def _NH_MediaStreamWillEnd(self, notification): notification.center.remove_observer(self, sender=self.handler) if self.msrp_reader_thread is not None: self.msrp_reader_thread.kill() self.msrp_reader_thread = None if self.msrp_writer_thread is not None: self.msrp_writer_thread.kill() self.msrp_writer_thread = None def _NH_ScreenSharingHandlerDidFail(self, notification): self._failure_reason = notification.data.reason notification.center.post_notification('MediaStreamDidFail', sender=self, data=notification.data) diff --git a/sipsimple/streams/rtp/__init__.py b/sipsimple/streams/rtp/__init__.py index 0516817d..ba1d24a1 100644 --- a/sipsimple/streams/rtp/__init__.py +++ b/sipsimple/streams/rtp/__init__.py @@ -1,618 +1,617 @@ """ Handling of RTP media streams according to RFC3550, RFC3605, RFC3581, RFC2833 and RFC3711, RFC3489 and RFC5245. """ __all__ = ['RTPStream'] import weakref from abc import ABCMeta, abstractmethod from application.notification import IObserver, NotificationCenter, NotificationData, ObserverWeakrefProxy from application.python import Null from threading import RLock from zope.interface import implements from sipsimple.account import BonjourAccount from sipsimple.configuration.settings import SIPSimpleSettings from sipsimple.core import RTPTransport, SIPCoreError, SIPURI from sipsimple.lookup import DNSLookup from sipsimple.streams import IMediaStream, InvalidStreamError, MediaStreamType, UnknownStreamError from sipsimple.threading import run_in_thread class ZRTPStreamOptions(object): implements(IObserver) def __init__(self, stream): self._stream = stream self.__dict__['master'] = None self.__dict__['sas'] = None self.__dict__['verified'] = False self.__dict__['peer_name'] = '' @property def sas(self): if self.master is not None: return self.master.encryption.zrtp.sas return self.__dict__['sas'] @property def verified(self): if self.master is not None: return self.master.encryption.zrtp.verified return self.__dict__['verified'] @verified.setter def verified(self, verified): if self.__dict__['verified'] == verified: return if self.sas is None: raise AttributeError('Cannot verify peer before SAS is received') if self.master is not None: self.master.encryption.zrtp.verified = verified else: rtp_transport = self._stream._rtp_transport if rtp_transport is not None: @run_in_thread('file-io') def update_verified(rtp_transport, verified): rtp_transport.set_zrtp_sas_verified(verified) notification_center = NotificationCenter() notification_center.post_notification('RTPStreamZRTPVerifiedStateChanged', sender=self._stream, data=NotificationData(verified=verified)) self.__dict__['verified'] = verified update_verified(rtp_transport, verified) @property def peer_id(self): if self.master is not None: return self.master.encryption.zrtp.peer_id rtp_transport = self._stream._rtp_transport if rtp_transport is None: return None return rtp_transport.zrtp_peer_id @property def peer_name(self): if self.master is not None: return self.master.encryption.zrtp.peer_name return self.__dict__['peer_name'] @peer_name.setter def peer_name(self, name): if self.__dict__['peer_name'] == name: return if self.master is not None: self.master.encryption.zrtp.peer_name = name else: rtp_transport = self._stream._rtp_transport if rtp_transport is not None: @run_in_thread('file-io') def update_name(rtp_transport, name): rtp_transport.zrtp_peer_name = name notification_center = NotificationCenter() notification_center.post_notification('RTPStreamZRTPPeerNameChanged', sender=self._stream, data=NotificationData(name=name)) self.__dict__['peer_name'] = name update_name(rtp_transport, name) @property def master(self): return self.__dict__['master'] @master.setter def master(self, master): old_master = self.__dict__['master'] if old_master is master: return notification_center = NotificationCenter() if old_master is not None: notification_center.remove_observer(self, sender=old_master) if master is not None: notification_center.add_observer(self, sender=master) self.__dict__['master'] = master def _enable(self, master_stream=None): rtp_transport = self._stream._rtp_transport if rtp_transport is None: return if master_stream is not None and not (master_stream.encryption.active and master_stream.encryption.type == 'ZRTP'): raise RuntimeError('Master stream must have ZRTP encryption activated') rtp_transport.set_zrtp_enabled(True, master_stream) self.master = master_stream def handle_notification(self, notification): handler = getattr(self, '_NH_%s' % notification.name, Null) handler(notification) def _NH_RTPStreamZRTPReceivedSAS(self, notification): # ZRTP begins on the audio stream, so this notification will only be processed # by the other streams self.__dict__['sas'] = notification.data.sas self.__dict__['verified'] = notification.data.verified self.__dict__['peer_name'] = notification.data.peer_name notification.center.post_notification(notification.name, sender=self._stream, data=notification.data) def _NH_RTPStreamZRTPVerifiedStateChanged(self, notification): self.__dict__['verified'] = notification.data.verified notification.center.post_notification(notification.name, sender=self._stream, data=notification.data) def _NH_RTPStreamZRTPPeerNameChanged(self, notification): self.__dict__['peer_name'] = notification.data.name notification.center.post_notification(notification.name, sender=self._stream, data=notification.data) def _NH_MediaStreamDidEnd(self, notification): self.master = None class RTPStreamEncryption(object): implements(IObserver) def __init__(self, stream): self._stream_ref = weakref.ref(stream) # Keep a weak reference before the stream is initialized to avoid a memory cycle that would delay releasing audio resources self._stream = None # We will store the actual reference once it's initialized and we're guaranteed to get MediaStreamDidEnd and do the cleanup self._rtp_transport = None self.__dict__['type'] = None self.__dict__['zrtp'] = None notification_center = NotificationCenter() notification_center.add_observer(ObserverWeakrefProxy(self), name='MediaStreamDidInitialize') notification_center.add_observer(ObserverWeakrefProxy(self), name='MediaStreamDidNotInitialize') @property def active(self): rtp_transport = self._rtp_transport if rtp_transport is None: return False if self.type == 'SRTP/SDES': return rtp_transport.srtp_active elif self.type == 'ZRTP': return rtp_transport.zrtp_active return False @property def cipher(self): rtp_transport = self._rtp_transport if rtp_transport is None: return None if self.type == 'SRTP/SDES': return rtp_transport.srtp_cipher elif self.type == 'ZRTP': return rtp_transport.zrtp_cipher return None @property def type(self): return self.__dict__['type'] @property def zrtp(self): zrtp = self.__dict__['zrtp'] if zrtp is None: raise RuntimeError('ZRTP options have not been initialized') return zrtp def handle_notification(self, notification): handler = getattr(self, '_NH_%s' % notification.name, Null) handler(notification) def _NH_MediaStreamDidInitialize(self, notification): stream = notification.sender if stream is self._stream_ref(): self._stream = stream self._rtp_transport = stream._rtp_transport notification.center.remove_observer(ObserverWeakrefProxy(self), name='MediaStreamDidInitialize') notification.center.remove_observer(ObserverWeakrefProxy(self), name='MediaStreamDidNotInitialize') notification.center.add_observer(self, sender=self._stream) notification.center.add_observer(self, sender=self._rtp_transport) encryption = stream._srtp_encryption or '' if encryption.startswith('sdes'): self.__dict__['type'] = 'SRTP/SDES' elif encryption == 'zrtp': self.__dict__['type'] = 'ZRTP' self.__dict__['zrtp'] = ZRTPStreamOptions(stream) def _NH_MediaStreamDidNotInitialize(self, notification): if notification.sender is self._stream_ref(): notification.center.remove_observer(ObserverWeakrefProxy(self), name='MediaStreamDidInitialize') notification.center.remove_observer(ObserverWeakrefProxy(self), name='MediaStreamDidNotInitialize') self._stream_ref = None self._stream = None def _NH_MediaStreamDidStart(self, notification): if self.type == 'SRTP/SDES': stream = notification.sender if self.active: notification.center.post_notification('RTPStreamDidEnableEncryption', sender=stream) else: reason = 'Not supported by remote' notification.center.post_notification('RTPStreamDidNotEnableEncryption', sender=stream, data=NotificationData(reason=reason)) def _NH_MediaStreamDidEnd(self, notification): notification.center.remove_observer(self, sender=self._stream) notification.center.remove_observer(self, sender=self._rtp_transport) self._stream = None self._stream_ref = None self._rtp_transport = None self.__dict__['type'] = None self.__dict__['zrtp'] = None def _NH_RTPTransportZRTPSecureOn(self, notification): stream = self._stream with stream._lock: if stream.state == "ENDED": return notification.center.post_notification('RTPStreamDidEnableEncryption', sender=stream) def _NH_RTPTransportZRTPSecureOff(self, notification): # We should never get here because we don't allow disabling encryption -Saul pass def _NH_RTPTransportZRTPReceivedSAS(self, notification): stream = self._stream with stream._lock: if stream.state == "ENDED": return self.zrtp.__dict__['sas'] = sas = notification.data.sas self.zrtp.__dict__['verified'] = verified = notification.data.verified self.zrtp.__dict__['peer_name'] = peer_name = notification.sender.zrtp_peer_name notification.center.post_notification('RTPStreamZRTPReceivedSAS', sender=stream, data=NotificationData(sas=sas, verified=verified, peer_name=peer_name)) def _NH_RTPTransportZRTPLog(self, notification): stream = self._stream with stream._lock: if stream.state == "ENDED": return notification.center.post_notification('RTPStreamZRTPLog', sender=stream, data=notification.data) def _NH_RTPTransportZRTPNegotiationFailed(self, notification): stream = self._stream with stream._lock: if stream.state == "ENDED": return reason = 'Negotiation failed: %s' % notification.data.reason notification.center.post_notification('RTPStreamDidNotEnableEncryption', sender=stream, data=NotificationData(reason=reason)) def _NH_RTPTransportZRTPNotSupportedByRemote(self, notification): stream = self._stream with stream._lock: if stream.state == "ENDED": return reason = 'ZRTP not supported by remote' notification.center.post_notification('RTPStreamDidNotEnableEncryption', sender=stream, data=NotificationData(reason=reason)) class RTPStreamType(ABCMeta, MediaStreamType): pass -class RTPStream(object): - __metaclass__ = RTPStreamType +class RTPStream(object, metaclass=RTPStreamType): implements(IMediaStream, IObserver) type = None priority = None hold_supported = True def __init__(self): self.notification_center = NotificationCenter() self.on_hold_by_local = False self.on_hold_by_remote = False self.direction = None self.state = "NULL" self.session = None self.encryption = RTPStreamEncryption(self) self._transport = None self._hold_request = None self._ice_state = "NULL" self._lock = RLock() self._rtp_transport = None self._try_ice = False self._srtp_encryption = None self._remote_rtp_address_sdp = None self._remote_rtp_port_sdp = None self._initialized = False self._done = False self._failure_reason = None @property def codec(self): return self._transport.codec if self._transport else None @property def sample_rate(self): return self._transport.sample_rate if self._transport else None @property def statistics(self): return self._transport.statistics if self._transport else None @property def local_rtp_address(self): return self._rtp_transport.local_rtp_address if self._rtp_transport else None @property def local_rtp_port(self): return self._rtp_transport.local_rtp_port if self._rtp_transport else None @property def local_rtp_candidate(self): return self._rtp_transport.local_rtp_candidate if self._rtp_transport else None @property def remote_rtp_address(self): if self._ice_state == "IN_USE": return self._rtp_transport.remote_rtp_address if self._rtp_transport else None return self._remote_rtp_address_sdp if self._rtp_transport else None @property def remote_rtp_port(self): if self._ice_state == "IN_USE": return self._rtp_transport.remote_rtp_port if self._rtp_transport else None return self._remote_rtp_port_sdp if self._rtp_transport else None @property def remote_rtp_candidate(self): return self._rtp_transport.remote_rtp_candidate if self._rtp_transport else None @property def ice_active(self): return self._ice_state == "IN_USE" @property def on_hold(self): return self.on_hold_by_local or self.on_hold_by_remote @abstractmethod def start(self, local_sdp, remote_sdp, stream_index): raise NotImplementedError @abstractmethod def update(self, local_sdp, remote_sdp, stream_index): raise NotImplementedError @abstractmethod def validate_update(self, remote_sdp, stream_index): raise NotImplementedError @abstractmethod def deactivate(self): raise NotImplementedError @abstractmethod def end(self): raise NotImplementedError @abstractmethod def reset(self, stream_index): raise NotImplementedError def hold(self): with self._lock: if self.on_hold_by_local or self._hold_request == 'hold': return if self.state == "ESTABLISHED" and self.direction != "inactive": self._pause() self._hold_request = 'hold' def unhold(self): with self._lock: if (not self.on_hold_by_local and self._hold_request != 'hold') or self._hold_request == 'unhold': return if self.state == "ESTABLISHED" and self._hold_request == 'hold': self._resume() self._hold_request = None if self._hold_request == 'hold' else 'unhold' @classmethod def new_from_sdp(cls, session, remote_sdp, stream_index): # TODO: actually validate the SDP settings = SIPSimpleSettings() remote_stream = remote_sdp.media[stream_index] if remote_stream.media != cls.type: raise UnknownStreamError if remote_stream.transport not in ('RTP/AVP', 'RTP/SAVP'): raise InvalidStreamError("expected RTP/AVP or RTP/SAVP transport in %s stream, got %s" % (cls.type, remote_stream.transport)) local_encryption_policy = session.account.rtp.encryption.key_negotiation if session.account.rtp.encryption.enabled else None if local_encryption_policy == "sdes_mandatory" and not "crypto" in remote_stream.attributes: raise InvalidStreamError("SRTP/SDES is locally mandatory but it's not remotely enabled") if remote_stream.transport == 'RTP/SAVP' and "crypto" in remote_stream.attributes and local_encryption_policy not in ("opportunistic", "sdes_optional", "sdes_mandatory"): raise InvalidStreamError("SRTP/SDES is remotely mandatory but it's not locally enabled") account_preferred_codecs = getattr(session.account.rtp, '%s_codec_list' % cls.type) general_codecs = getattr(settings.rtp, '%s_codec_list' % cls.type) supported_codecs = account_preferred_codecs or general_codecs if not any(codec for codec in remote_stream.codec_list if codec in supported_codecs): raise InvalidStreamError("no compatible codecs found") stream = cls() stream._incoming_remote_sdp = remote_sdp stream._incoming_stream_index = stream_index return stream def initialize(self, session, direction): with self._lock: if self.state != "NULL": raise RuntimeError("%sStream.initialize() may only be called in the NULL state" % self.type.capitalize()) self.state = "INITIALIZING" self.session = session local_encryption_policy = session.account.rtp.encryption.key_negotiation if session.account.rtp.encryption.enabled else None if hasattr(self, "_incoming_remote_sdp") and hasattr(self, '_incoming_stream_index'): # ICE attributes could come at the session level or at the media level remote_stream = self._incoming_remote_sdp.media[self._incoming_stream_index] self._try_ice = self.session.account.nat_traversal.use_ice and ((remote_stream.has_ice_attributes or self._incoming_remote_sdp.has_ice_attributes) and remote_stream.has_ice_candidates) if "zrtp-hash" in remote_stream.attributes: incoming_stream_encryption = 'zrtp' elif "crypto" in remote_stream.attributes: incoming_stream_encryption = 'sdes_mandatory' if remote_stream.transport == 'RTP/SAVP' else 'sdes_optional' else: incoming_stream_encryption = None if incoming_stream_encryption is not None and local_encryption_policy == 'opportunistic': self._srtp_encryption = incoming_stream_encryption else: self._srtp_encryption = 'zrtp' if local_encryption_policy == 'opportunistic' else local_encryption_policy else: self._try_ice = self.session.account.nat_traversal.use_ice self._srtp_encryption = 'zrtp' if local_encryption_policy == 'opportunistic' else local_encryption_policy if self._try_ice: if self.session.account.nat_traversal.stun_server_list: stun_servers = list((server.host, server.port) for server in self.session.account.nat_traversal.stun_server_list) self._init_rtp_transport(stun_servers) elif not isinstance(self.session.account, BonjourAccount): dns_lookup = DNSLookup() self.notification_center.add_observer(self, sender=dns_lookup) dns_lookup.lookup_service(SIPURI(self.session.account.id.domain), "stun") else: self._init_rtp_transport() def get_local_media(self, remote_sdp=None, index=0): with self._lock: if self.state not in ("INITIALIZED", "WAIT_ICE", "ESTABLISHED"): raise RuntimeError("%sStream.get_local_media() may only be called in the INITIALIZED, WAIT_ICE or ESTABLISHED states" % self.type.capitalize()) if remote_sdp is None: # offer old_direction = self._transport.direction if old_direction is None: new_direction = "sendrecv" elif "send" in old_direction: new_direction = ("sendonly" if (self._hold_request == 'hold' or (self._hold_request is None and self.on_hold_by_local)) else "sendrecv") else: new_direction = ("inactive" if (self._hold_request == 'hold' or (self._hold_request is None and self.on_hold_by_local)) else "recvonly") else: new_direction = None return self._transport.get_local_media(remote_sdp, index, new_direction) # Notifications def handle_notification(self, notification): handler = getattr(self, '_NH_%s' % notification.name, Null) handler(notification) def _NH_DNSLookupDidFail(self, notification): self.notification_center.remove_observer(self, sender=notification.sender) with self._lock: if self.state == "ENDED": return self._init_rtp_transport() def _NH_DNSLookupDidSucceed(self, notification): self.notification_center.remove_observer(self, sender=notification.sender) with self._lock: if self.state == "ENDED": return self._init_rtp_transport(notification.data.result) def _NH_RTPTransportDidInitialize(self, notification): rtp_transport = notification.sender with self._lock: if self.state == "ENDED": self.notification_center.remove_observer(self, sender=rtp_transport) return del self._rtp_args del self._stun_servers remote_sdp = self.__dict__.pop('_incoming_remote_sdp', None) stream_index = self.__dict__.pop('_incoming_stream_index', None) try: if remote_sdp is not None: transport = self._create_transport(rtp_transport, remote_sdp=remote_sdp, stream_index=stream_index) self._save_remote_sdp_rtp_info(remote_sdp, stream_index) else: transport = self._create_transport(rtp_transport) - except SIPCoreError, e: + except SIPCoreError as e: self.state = "ENDED" self.notification_center.remove_observer(self, sender=rtp_transport) self.notification_center.post_notification('MediaStreamDidNotInitialize', sender=self, data=NotificationData(reason=e.args[0])) return self._rtp_transport = rtp_transport self._transport = transport self.notification_center.add_observer(self, sender=transport) self._initialized = True self.state = "INITIALIZED" self.notification_center.post_notification('MediaStreamDidInitialize', sender=self) def _NH_RTPTransportDidFail(self, notification): self.notification_center.remove_observer(self, sender=notification.sender) with self._lock: if self.state == "ENDED": return self._try_next_rtp_transport(notification.data.reason) def _NH_RTPTransportICENegotiationStateDidChange(self, notification): with self._lock: if self._ice_state != "NULL" or self.state not in ("INITIALIZING", "INITIALIZED", "WAIT_ICE"): return self.notification_center.post_notification('RTPStreamICENegotiationStateDidChange', sender=self, data=notification.data) def _NH_RTPTransportICENegotiationDidSucceed(self, notification): with self._lock: if self.state != "WAIT_ICE": return self._ice_state = "IN_USE" self.state = 'ESTABLISHED' self.notification_center.post_notification('RTPStreamICENegotiationDidSucceed', sender=self, data=notification.data) self.notification_center.post_notification('MediaStreamDidStart', sender=self) def _NH_RTPTransportICENegotiationDidFail(self, notification): with self._lock: if self.state != "WAIT_ICE": return self._ice_state = "FAILED" self.state = 'ESTABLISHED' self.notification_center.post_notification('RTPStreamICENegotiationDidFail', sender=self, data=notification.data) self.notification_center.post_notification('MediaStreamDidStart', sender=self) # Private methods def _init_rtp_transport(self, stun_servers=None): self._rtp_args = dict() self._rtp_args["encryption"] = self._srtp_encryption self._rtp_args["use_ice"] = self._try_ice self._stun_servers = [(None, None)] if stun_servers: self._stun_servers.extend(reversed(stun_servers)) self._try_next_rtp_transport() def _try_next_rtp_transport(self, failure_reason=None): if self._stun_servers: stun_address, stun_port = self._stun_servers.pop() try: rtp_transport = RTPTransport(ice_stun_address=stun_address, ice_stun_port=stun_port, **self._rtp_args) - except SIPCoreError, e: + except SIPCoreError as e: self._try_next_rtp_transport(e.args[0]) else: self.notification_center.add_observer(self, sender=rtp_transport) try: rtp_transport.set_INIT() - except SIPCoreError, e: + except SIPCoreError as e: self.notification_center.remove_observer(self, sender=rtp_transport) self._try_next_rtp_transport(e.args[0]) else: self.state = "ENDED" self.notification_center.post_notification('MediaStreamDidNotInitialize', sender=self, data=NotificationData(reason=failure_reason)) def _save_remote_sdp_rtp_info(self, remote_sdp, index): connection = remote_sdp.media[index].connection or remote_sdp.connection self._remote_rtp_address_sdp = connection.address self._remote_rtp_port_sdp = remote_sdp.media[index].port @abstractmethod def _create_transport(self, rtp_transport, remote_sdp=None, stream_index=None): raise NotImplementedError @abstractmethod def _check_hold(self, direction, is_initial): raise NotImplementedError @abstractmethod def _pause(self): raise NotImplementedError @abstractmethod def _resume(self): raise NotImplementedError from sipsimple.streams.rtp import audio, video diff --git a/sipsimple/streams/rtp/audio.py b/sipsimple/streams/rtp/audio.py index 7610a735..40051c7d 100644 --- a/sipsimple/streams/rtp/audio.py +++ b/sipsimple/streams/rtp/audio.py @@ -1,236 +1,236 @@ __all__ = ['AudioStream'] from application.notification import NotificationCenter, NotificationData from zope.interface import implements from sipsimple.audio import AudioBridge, AudioDevice, IAudioPort, WaveRecorder from sipsimple.configuration.settings import SIPSimpleSettings from sipsimple.core import AudioTransport, PJSIPError, SIPCoreError from sipsimple.streams.rtp import RTPStream class AudioStream(RTPStream): implements(IAudioPort) type = 'audio' priority = 1 def __init__(self): super(AudioStream, self).__init__() from sipsimple.application import SIPApplication self.mixer = SIPApplication.voice_audio_mixer self.bridge = AudioBridge(self.mixer) self.device = AudioDevice(self.mixer) self._audio_rec = None self.bridge.add(self.device) @property def muted(self): return self.__dict__.get('muted', False) @muted.setter def muted(self, value): if not isinstance(value, bool): raise ValueError("illegal value for muted property: %r" % (value,)) if value == self.muted: return old_producer_slot = self.producer_slot self.__dict__['muted'] = value notification_center = NotificationCenter() data = NotificationData(consumer_slot_changed=False, producer_slot_changed=True, old_producer_slot=old_producer_slot, new_producer_slot=self.producer_slot) notification_center.post_notification('AudioPortDidChangeSlots', sender=self, data=data) @property def consumer_slot(self): return self._transport.slot if self._transport else None @property def producer_slot(self): return self._transport.slot if self._transport and not self.muted else None @property def recorder(self): return self._audio_rec def start(self, local_sdp, remote_sdp, stream_index): with self._lock: if self.state != "INITIALIZED": raise RuntimeError("AudioStream.start() may only be called in the INITIALIZED state") settings = SIPSimpleSettings() self._transport.start(local_sdp, remote_sdp, stream_index, timeout=settings.rtp.timeout) self._save_remote_sdp_rtp_info(remote_sdp, stream_index) self._check_hold(self._transport.direction, True) if self._try_ice and self._ice_state == "NULL": self.state = 'WAIT_ICE' else: self.state = 'ESTABLISHED' self.notification_center.post_notification('MediaStreamDidStart', sender=self) def validate_update(self, remote_sdp, stream_index): with self._lock: # TODO: implement return True def update(self, local_sdp, remote_sdp, stream_index): with self._lock: connection = remote_sdp.media[stream_index].connection or remote_sdp.connection if not self._rtp_transport.ice_active and (connection.address != self._remote_rtp_address_sdp or self._remote_rtp_port_sdp != remote_sdp.media[stream_index].port): settings = SIPSimpleSettings() if self._audio_rec is not None: self.bridge.remove(self._audio_rec) old_consumer_slot = self.consumer_slot old_producer_slot = self.producer_slot self.notification_center.remove_observer(self, sender=self._transport) self._transport.stop() try: self._transport = AudioTransport(self.mixer, self._rtp_transport, remote_sdp, stream_index, codecs=list(self.session.account.rtp.audio_codec_list or settings.rtp.audio_codec_list)) - except SIPCoreError, e: + except SIPCoreError as e: self.state = "ENDED" self._failure_reason = e.args[0] self.notification_center.post_notification('MediaStreamDidFail', sender=self, data=NotificationData(context='update', reason=self._failure_reason)) return self.notification_center.add_observer(self, sender=self._transport) self._transport.start(local_sdp, remote_sdp, stream_index, timeout=settings.rtp.timeout) self.notification_center.post_notification('AudioPortDidChangeSlots', sender=self, data=NotificationData(consumer_slot_changed=True, producer_slot_changed=True, old_consumer_slot=old_consumer_slot, new_consumer_slot=self.consumer_slot, old_producer_slot=old_producer_slot, new_producer_slot=self.producer_slot)) if connection.address == '0.0.0.0' and remote_sdp.media[stream_index].direction == 'sendrecv': self._transport.update_direction('recvonly') self._check_hold(self._transport.direction, False) self.notification_center.post_notification('RTPStreamDidChangeRTPParameters', sender=self) else: new_direction = local_sdp.media[stream_index].direction self._transport.update_direction(new_direction) self._check_hold(new_direction, False) self._save_remote_sdp_rtp_info(remote_sdp, stream_index) self._transport.update_sdp(local_sdp, remote_sdp, stream_index) self._hold_request = None def deactivate(self): with self._lock: self.bridge.stop() def end(self): with self._lock: if self.state == "ENDED" or self._done: return self._done = True if not self._initialized: self.state = "ENDED" self.notification_center.post_notification('MediaStreamDidNotInitialize', sender=self, data=NotificationData(reason='Interrupted')) return self.notification_center.post_notification('MediaStreamWillEnd', sender=self) if self._transport is not None: if self._audio_rec is not None: self._stop_recording() self.notification_center.remove_observer(self, sender=self._transport) self.notification_center.remove_observer(self, sender=self._rtp_transport) self._transport.stop() self._transport = None self._rtp_transport = None self.state = "ENDED" self.notification_center.post_notification('MediaStreamDidEnd', sender=self, data=NotificationData(error=self._failure_reason)) self.session = None def reset(self, stream_index): with self._lock: if self.direction == "inactive" and not self.on_hold_by_local: new_direction = "sendrecv" self._transport.update_direction(new_direction) self._check_hold(new_direction, False) # TODO: do a full reset, re-creating the AudioTransport, so that a new offer # would contain all codecs and ICE would be renegotiated -Saul def send_dtmf(self, digit): with self._lock: if self.state != "ESTABLISHED": raise RuntimeError("AudioStream.send_dtmf() cannot be used in %s state" % self.state) try: self._transport.send_dtmf(digit) - except PJSIPError, e: + except PJSIPError as e: if not e.args[0].endswith("(PJ_ETOOMANY)"): raise def start_recording(self, filename): with self._lock: if self.state == "ENDED": raise RuntimeError("AudioStream.start_recording() may not be called in the ENDED state") if self._audio_rec is not None: raise RuntimeError("Already recording audio to a file") self._audio_rec = WaveRecorder(self.mixer, filename) if self.state == "ESTABLISHED": self._check_recording() def stop_recording(self): with self._lock: if self._audio_rec is None: raise RuntimeError("Not recording any audio") self._stop_recording() def _NH_RTPAudioStreamGotDTMF(self, notification): notification.center.post_notification('AudioStreamGotDTMF', sender=self, data=NotificationData(digit=notification.data.digit)) def _NH_RTPAudioTransportDidTimeout(self, notification): notification.center.post_notification('RTPStreamDidTimeout', sender=self) # Private methods # def _create_transport(self, rtp_transport, remote_sdp=None, stream_index=None): settings = SIPSimpleSettings() codecs = list(self.session.account.rtp.audio_codec_list or settings.rtp.audio_codec_list) return AudioTransport(self.mixer, rtp_transport, remote_sdp=remote_sdp, sdp_index=stream_index or 0, codecs=codecs) def _check_hold(self, direction, is_initial): was_on_hold_by_local = self.on_hold_by_local was_on_hold_by_remote = self.on_hold_by_remote was_inactive = self.direction == "inactive" self.direction = direction inactive = self.direction == "inactive" self.on_hold_by_local = was_on_hold_by_local if inactive else direction == "sendonly" self.on_hold_by_remote = "send" not in direction if (is_initial or was_on_hold_by_local or was_inactive) and not inactive and not self.on_hold_by_local and self._hold_request != 'hold': self._resume() if not was_on_hold_by_local and self.on_hold_by_local: self.notification_center.post_notification('RTPStreamDidChangeHoldState', sender=self, data=NotificationData(originator="local", on_hold=True)) if was_on_hold_by_local and not self.on_hold_by_local: self.notification_center.post_notification('RTPStreamDidChangeHoldState', sender=self, data=NotificationData(originator="local", on_hold=False)) if not was_on_hold_by_remote and self.on_hold_by_remote: self.notification_center.post_notification('RTPStreamDidChangeHoldState', sender=self, data=NotificationData(originator="remote", on_hold=True)) if was_on_hold_by_remote and not self.on_hold_by_remote: self.notification_center.post_notification('RTPStreamDidChangeHoldState', sender=self, data=NotificationData(originator="remote", on_hold=False)) if self._audio_rec is not None: self._check_recording() def _check_recording(self): if not self._audio_rec.is_active: self.notification_center.post_notification('AudioStreamWillStartRecording', sender=self, data=NotificationData(filename=self._audio_rec.filename)) try: self._audio_rec.start() - except SIPCoreError, e: + except SIPCoreError as e: self._audio_rec = None self.notification_center.post_notification('AudioStreamDidStopRecording', sender=self, data=NotificationData(filename=self._audio_rec.filename, reason=e.args[0])) return self.notification_center.post_notification('AudioStreamDidStartRecording', sender=self, data=NotificationData(filename=self._audio_rec.filename)) if not self.on_hold: self.bridge.add(self._audio_rec) elif self._audio_rec in self.bridge: self.bridge.remove(self._audio_rec) def _stop_recording(self): self.notification_center.post_notification('AudioStreamWillStopRecording', sender=self, data=NotificationData(filename=self._audio_rec.filename)) try: if self._audio_rec.is_active: self._audio_rec.stop() finally: self.notification_center.post_notification('AudioStreamDidStopRecording', sender=self, data=NotificationData(filename=self._audio_rec.filename)) self._audio_rec = None def _pause(self): self.bridge.remove(self) def _resume(self): self.bridge.add(self) diff --git a/sipsimple/threading/__init__.py b/sipsimple/threading/__init__.py index 8c25057c..4c5ede57 100644 --- a/sipsimple/threading/__init__.py +++ b/sipsimple/threading/__init__.py @@ -1,120 +1,118 @@ """Thread management""" __all__ = ["ThreadManager", "run_in_thread", "call_in_thread", "run_in_twisted_thread", "call_in_twisted_thread"] from application.python import Null from application.python.decorator import decorator, preserve_signature from application.python.queue import EventQueue from application.python.types import Singleton from threading import Lock, current_thread from twisted.python import threadable from sipsimple import log class CallFunctionEvent(object): __slots__ = ('function', 'args', 'kw') def __init__(self, function, args, kw): self.function = function self.args = args self.kw = kw -class ThreadManager(object): - __metaclass__ = Singleton - +class ThreadManager(object, metaclass=Singleton): def __init__(self): self.threads = {} self.lock = Lock() def _event_handler(self, event): handler = getattr(self, '_EH_%s' % event.__class__.__name__, Null) handler(event) def _EH_CallFunctionEvent(self, event): try: event.function(*event.args, **event.kw) except: log.exception('Exception occurred while calling %r in the %r thread' % (event.function, current_thread().name)) def start(self): pass def stop(self): with self.lock: - threads = self.threads.values() + threads = list(self.threads.values()) self.threads = {} for thread in threads: thread.stop() for thread in threads: thread.join() def get_thread(self, thread_id): with self.lock: try: thread = self.threads[thread_id] except KeyError: self.threads[thread_id] = thread = EventQueue(handler=self._event_handler, name=thread_id) thread.start() return thread def stop_thread(self, thread_id): if thread_id == 'thread-ops': raise RuntimeError("Won't stop internal 'thread-ops' thread") thread = self.threads.pop(thread_id) thread.stop() call_in_thread('thread-ops', thread.join) @decorator def run_in_thread(thread_id, scheduled=False): def thread_decorator(function): @preserve_signature(function) def wrapper(*args, **kw): thread_manager = ThreadManager() thread = thread_manager.get_thread(thread_id) if thread is current_thread() and not scheduled: function(*args, **kw) else: thread.put(CallFunctionEvent(function, args, kw)) return wrapper return thread_decorator def call_in_thread(thread_id, function, *args, **kw): thread_manager = ThreadManager() thread = thread_manager.get_thread(thread_id) if thread is current_thread(): function(*args, **kw) else: thread.put(CallFunctionEvent(function, args, kw)) def schedule_in_thread(thread_id, function, *args, **kw): thread_manager = ThreadManager() thread = thread_manager.get_thread(thread_id) thread.put(CallFunctionEvent(function, args, kw)) @decorator def run_in_twisted_thread(func): @preserve_signature(func) def wrapper(*args, **kwargs): if threadable.isInIOThread(): func(*args, **kwargs) else: from twisted.internet import reactor reactor.callFromThread(func, *args, **kwargs) return wrapper def call_in_twisted_thread(func, *args, **kwargs): if threadable.isInIOThread(): func(*args, **kwargs) else: from twisted.internet import reactor reactor.callFromThread(func, *args, **kwargs) diff --git a/sipsimple/util/__init__.py b/sipsimple/util/__init__.py index 62fadf24..82b9fb18 100644 --- a/sipsimple/util/__init__.py +++ b/sipsimple/util/__init__.py @@ -1,148 +1,146 @@ """Implements utilities commonly used in various parts of the library""" -from __future__ import absolute_import + __all__ = ["All", "Any", "ExponentialTimer", "ISOTimestamp", "MultilingualText", "user_info", "sha1"] import os import platform import sys import dateutil.parser from application.notification import NotificationCenter from application.python.types import Singleton, MarkerType from datetime import datetime from dateutil.tz import tzlocal, tzutc from twisted.internet import reactor from sipsimple.util._sha1 import sha1 # Utility classes # -class All(object): - __metaclass__ = MarkerType +class All(object, metaclass=MarkerType): + pass -class Any(object): - __metaclass__ = MarkerType +class Any(object, metaclass=MarkerType): + pass class ISOTimestamp(datetime): def __new__(cls, *args, **kw): if len(args) == 1: value = args[0] if isinstance(value, cls): return value - elif isinstance(value, basestring): + elif isinstance(value, str): value = dateutil.parser.parse(value) return cls(value.year, value.month, value.day, value.hour, value.minute, value.second, value.microsecond, value.tzinfo) elif isinstance(value, datetime): return cls(value.year, value.month, value.day, value.hour, value.minute, value.second, value.microsecond, value.tzinfo or tzlocal()) else: return datetime.__new__(cls, *args, **kw) else: if len(args) < 8 and 'tzinfo' not in kw: kw['tzinfo'] = tzlocal() return datetime.__new__(cls, *args, **kw) def __str__(self): return self.isoformat() @classmethod def now(cls): return cls(datetime.now(tzlocal())) @classmethod def utcnow(cls): return cls(datetime.now(tzutc())) -class MultilingualText(unicode): +class MultilingualText(str): def __new__(cls, *args, **translations): if len(args) > 1: raise TypeError("%s.__new__ takes at most 1 positional argument (%d given)" % (cls.__name__, len(args))) - default = args[0] if args else translations.get('en', u'') - obj = unicode.__new__(cls, default) + default = args[0] if args else translations.get('en', '') + obj = str.__new__(cls, default) obj.translations = translations return obj def get_translation(self, language): return self.translations.get(language, self) class ExponentialTimer(object): def __init__(self): self._timer = None self._limit_timer = None self._interval = 0 self._iterations = None def _step(self): if self._iterations is not None: self._iterations -= 1 if self._iterations == 0: self.stop() else: self._interval *= 2 self._timer = reactor.callLater(self._interval, self._step) NotificationCenter().post_notification('ExponentialTimerDidTimeout', sender=self) @property def active(self): return self._timer is not None def start(self, base_interval, immediate=False, iterations=None, time_limit=None): assert base_interval > 0 assert iterations is None or iterations > 0 assert time_limit is None or time_limit > 0 if self._timer is not None: self.stop() self._interval = base_interval / 2.0 if immediate else base_interval self._iterations = iterations if time_limit is not None: self._limit_timer = reactor.callLater(time_limit, self.stop) self._timer = reactor.callLater(0 if immediate else base_interval, self._step) def stop(self): if self._timer is not None and self._timer.active(): self._timer.cancel() if self._limit_timer is not None and self._limit_timer.active(): self._limit_timer.cancel() self._timer = None self._limit_timer = None # Utility objects # -class UserInfo(object): - __metaclass__ = Singleton - +class UserInfo(object, metaclass=Singleton): def __repr__(self): return ''.format(self) @property def username(self): if platform.system() == 'Windows': name = os.getenv('USERNAME') else: import pwd name = pwd.getpwuid(os.getuid()).pw_name return name.decode(sys.getfilesystemencoding()) @property def fullname(self): if platform.system() == 'Windows': name = os.getenv('USERNAME') else: import pwd name = pwd.getpwuid(os.getuid()).pw_gecos.split(',', 1)[0] or pwd.getpwuid(os.getuid()).pw_name return name.decode(sys.getfilesystemencoding()) user_info = UserInfo() del UserInfo diff --git a/sipsimple/video.py b/sipsimple/video.py index 1f56a803..6caabdf1 100644 --- a/sipsimple/video.py +++ b/sipsimple/video.py @@ -1,79 +1,79 @@ """Video support""" -from __future__ import absolute_import + __all__ = ['IVideoProducer', 'VideoDevice', 'VideoError'] -from application.notification import NotificationCenter, NotificationData +from .application.notification import NotificationCenter, NotificationData from zope.interface import Attribute, Interface, implements from sipsimple.core import SIPCoreError, VideoCamera class IVideoProducer(Interface): """ Interface describing an object which can produce video data. All attributes of this interface are read-only. """ producer = Attribute("The core producer object which can be connected to a consumer") class VideoError(Exception): pass class VideoDevice(object): implements(IVideoProducer) def __init__(self, device_name, resolution, framerate): self._camera = self._open_camera(device_name, resolution, framerate) self._camera.start() def _open_camera(self, device_name, resolution, framerate): try: return VideoCamera(device_name, resolution, framerate) except SIPCoreError: try: - return VideoCamera(u'system_default', resolution, framerate) + return VideoCamera('system_default', resolution, framerate) except SIPCoreError: return VideoCamera(None, resolution, framerate) def set_camera(self, device_name, resolution, framerate): old_camera = self._camera old_camera.close() new_camera = self._open_camera(device_name, resolution, framerate) if not self.muted: new_camera.start() self._camera = new_camera notification_center = NotificationCenter() notification_center.post_notification('VideoDeviceDidChangeCamera', sender=self, data=NotificationData(old_camera=old_camera, new_camera=new_camera)) @property def producer(self): return self._camera @property def name(self): return self._camera.name @property def real_name(self): return self._camera.real_name @property def muted(self): return self.__dict__.get('muted', False) @muted.setter def muted(self, value): if not isinstance(value, bool): raise ValueError('illegal value for muted property: %r' % (value,)) if value == self.muted: return if value: self._camera.stop() else: self._camera.start() self.__dict__['muted'] = value