diff --git a/sip-register3 b/sip-register3 index 744f1e9..8736900 100755 --- a/sip-register3 +++ b/sip-register3 @@ -1,369 +1,374 @@ #!/usr/bin/env python3 import atexit import os import select import signal import sys import termios from datetime import datetime from optparse import OptionParser from threading import Thread from time import sleep from application import log from application.notification import NotificationCenter, NotificationData from application.python.queue import EventQueue from sipsimple.account import Account, AccountManager, BonjourAccount from sipsimple.application import SIPApplication from sipsimple.configuration import ConfigurationError from sipsimple.configuration.settings import SIPSimpleSettings from sipsimple.core import Engine from sipsimple.storage import FileStorage from sipclient.configuration import config_directory from sipclient.configuration.account import AccountExtension from sipclient.configuration.settings import SIPSimpleSettingsExtension from sipclient.log import Logger from sipclient.system import IPAddressMonitor class BonjourNeighbour(object): def __init__(self, neighbour, uri, display_name, host): self.display_name = display_name self.host = host self.neighbour = neighbour self.uri = uri class InputThread(Thread): def __init__(self): Thread.__init__(self) self.daemon = True self._old_terminal_settings = None def start(self): atexit.register(self._termios_restore) Thread.start(self) def run(self): notification_center = NotificationCenter() while True: for char in self._getchars(): notification_center.post_notification('SIPApplicationGotInput', sender=self, data=NotificationData(input=char)) def stop(self): self._termios_restore() def _termios_restore(self): if self._old_terminal_settings is not None: termios.tcsetattr(sys.stdin.fileno(), termios.TCSADRAIN, self._old_terminal_settings) def _getchars(self): fd = sys.stdin.fileno() if os.isatty(fd): self._old_terminal_settings = termios.tcgetattr(fd) new = termios.tcgetattr(fd) new[3] = new[3] & ~termios.ICANON & ~termios.ECHO new[6][termios.VMIN] = b'\000' try: termios.tcsetattr(fd, termios.TCSADRAIN, new) if select.select([fd], [], [], None)[0]: return sys.stdin.read(4192) finally: self._termios_restore() else: return os.read(fd, 4192) class RegistrationApplication(SIPApplication): def __init__(self): self.account = None self.options = None self.input = None self.output = None self.ip_address_monitor = IPAddressMonitor() self.logger = None self.max_registers = None self.neighbours = {} self.success = False def _write(self, message): sys.stdout.write(message) sys.stdout.flush() def start(self, options): notification_center = NotificationCenter() self.options = options self.max_registers = options.max_registers if options.max_registers > 0 else None self.input = InputThread() if not options.batch_mode else None self.output = EventQueue(self._write) self.logger = Logger(sip_to_stdout=options.trace_sip, pjsip_to_stdout=options.trace_pjsip, notifications_to_stdout=options.trace_notifications) notification_center.add_observer(self, sender=self) notification_center.add_observer(self, sender=self.input) notification_center.add_observer(self, name='SIPSessionNewIncoming') notification_center.add_observer(self, name='DNSLookupDidSucceed') if self.input: self.input.start() self.output.start() log.level.current = log.level.WARNING # get rid of twisted messages Account.register_extension(AccountExtension) BonjourAccount.register_extension(AccountExtension) SIPSimpleSettings.register_extension(SIPSimpleSettingsExtension) try: SIPApplication.start(self, FileStorage(options.config_directory or config_directory)) except ConfigurationError as e: self.output.put("Failed to load sipclient's configuration: %s\n" % str(e)) self.output.put("If an old configuration file is in place, delete it or move it and recreate the configuration using the sip_settings script.\n") self.output.stop() def print_help(self): message = 'Available control keys:\n' message += ' s: toggle SIP trace on the console\n' message += ' j: toggle PJSIP trace on the console\n' message += ' n: toggle notifications trace on the console\n' message += ' Ctrl-d: quit the program\n' message += ' ?: display this help message\n' self.output.put('\n'+message) def _NH_SIPApplicationWillStart(self, notification): account_manager = AccountManager() notification_center = NotificationCenter() settings = SIPSimpleSettings() if self.options.account is None: self.account = account_manager.default_account else: possible_accounts = [account for account in account_manager.iter_accounts() if self.options.account in account.id and account.enabled] if len(possible_accounts) > 1: self.output.put('More than one account exists which matches %s: %s\n' % (self.options.account, ', '.join(sorted(account.id for account in possible_accounts)))) self.output.stop() self.stop() return elif len(possible_accounts) == 0: self.output.put('No enabled account that matches %s was found. Available and enabled accounts: %s\n' % (self.options.account, ', '.join(sorted(account.id for account in account_manager.get_accounts() if account.enabled)))) self.output.stop() self.stop() return else: self.account = possible_accounts[0] for account in account_manager.iter_accounts(): if account is self.account: if isinstance(account, Account): account.sip.register = True account.message_summary.enabled = False account.presence.enabled = False account.xcap.enabled = False account.enabled = True else: account.enabled = False self.output.put('Using account %s\n' % self.account.id) notification_center.add_observer(self, sender=self.account) if self.account is BonjourAccount() and self.max_registers is not None: if self.max_registers == 1: self.max_registers = len([transport for transport in settings.sip.transport_list if (transport!='tls' or self.account.tls.certificate is not None)]) else: self.output.put('--max-registers option only accepts 0 or 1 if using Bonjour account\n') self.output.stop() self.stop() return # start logging self.logger.start() if settings.logs.trace_sip and self.logger._siptrace_filename is not None: self.output.put('Logging SIP trace to file "%s"\n' % self.logger._siptrace_filename) if settings.logs.trace_pjsip and self.logger._pjsiptrace_filename is not None: self.output.put('Logging PJSIP trace to file "%s"\n' % self.logger._pjsiptrace_filename) if settings.logs.trace_notifications and self.logger._notifications_filename is not None: self.output.put('Logging notifications trace to file "%s"\n' % self.logger._notifications_filename) def _NH_SIPApplicationDidStart(self, notification): engine = Engine() settings = SIPSimpleSettings() engine.trace_sip = self.logger.sip_to_stdout or settings.logs.trace_sip engine.log_level = settings.logs.pjsip_level if (self.logger.pjsip_to_stdout or settings.logs.trace_pjsip) else 0 self.ip_address_monitor.start() if self.account is not BonjourAccount() and self.max_registers != 1 and not self.options.batch_mode: self.print_help() elif self.account is BonjourAccount() and self.max_registers is None: self.print_help() def _NH_SIPApplicationWillEnd(self, notification): self.ip_address_monitor.stop() def _NH_SIPApplicationDidEnd(self, notification): if self.input: self.input.stop() self.output.stop() self.output.join() def _NH_DNSLookupDidSucceed(self, notification): notification_center = NotificationCenter() targets = {} for result in notification.data.result: try: ports = targets[result.address] except KeyError: targets[result.address] = set() port = '%s-%s' % (result.port, result.transport.upper()) targets[result.address].add(port) result_text = '' i = 1 for t in list(targets.keys()): result_text = result_text + ' ' + str(i) + ') ' + t + ': ' result_text = result_text + ' '.join(sorted(list(targets[t]))) i = i + 1 self.output.put("\n%s DNS lookup for %s succeeded:%s\n" % (datetime.now().replace(microsecond=0), self.account.id.domain, result_text)) def _NH_SIPApplicationGotInput(self, notification): engine = Engine() settings = SIPSimpleSettings() key = notification.data.input if key == '\x04': self.stop() elif key == 's': self.logger.sip_to_stdout = not self.logger.sip_to_stdout engine.trace_sip = self.logger.sip_to_stdout or settings.logs.trace_sip self.output.put('SIP tracing to console is now %s.\n' % ('activated' if self.logger.sip_to_stdout else 'deactivated')) elif key == 'j': self.logger.pjsip_to_stdout = not self.logger.pjsip_to_stdout engine.log_level = settings.logs.pjsip_level if (self.logger.pjsip_to_stdout or settings.logs.trace_pjsip) else 0 self.output.put('PJSIP tracing to console is now %s.\n' % ('activated' if self.logger.pjsip_to_stdout else 'deactivated')) elif key == 'n': self.logger.notifications_to_stdout = not self.logger.notifications_to_stdout self.output.put('Notification tracing to console is now %s.\n' % ('activated' if self.logger.notifications_to_stdout else 'deactivated')) elif key == '?': self.print_help() def _NH_SIPAccountRegistrationDidSucceed(self, notification): contact_header = notification.data.contact_header contact_header_list = notification.data.contact_header_list expires = notification.data.expires registrar = notification.data.registrar if not self.success: message = '%s Registered contact "%s" at %s:%d;transport=%s for %d seconds\n' % (datetime.now().replace(microsecond=0), contact_header.uri, registrar.address, registrar.port, registrar.transport, expires) if notification.sender.contact.public_gruu: message += '%s Public GRUU: "%s"\n' % (datetime.now().replace(microsecond=0), notification.sender.contact.public_gruu) if notification.sender.contact.temporary_gruu: message += '%s Temporary GRUU: "%s"\n' % (datetime.now().replace(microsecond=0), notification.sender.contact.temporary_gruu) if len(contact_header_list) > 1: message += 'Other registered contacts:\n%s\n' % '\n'.join([' %s (expires in %s seconds)' % (str(other_contact_header.uri), other_contact_header.expires) for other_contact_header in contact_header_list if other_contact_header.uri != notification.data.contact_header.uri]) self.output.put(message) self.success = True else: self.output.put('%s Refreshed contact "%s" at %s:%d;transport=%s for %d seconds\n' % (datetime.now().replace(microsecond=0), contact_header.uri, registrar.address, registrar.port, registrar.transport, expires)) if self.max_registers is not None: self.max_registers -= 1 if self.max_registers == 0: self.stop() def _NH_SIPAccountRegistrationGotAnswer(self, notification): if notification.data.code >= 300: registrar = notification.data.registrar code = notification.data.code - reason = notification.data.reason + reason = notification.data.reason.decode() if isinstance(notification.data.reason, bytes) else notification.data.reason self.output.put('%s Registration failed at %s:%d;transport=%s: %d %s\n' % (datetime.now().replace(microsecond=0), registrar.address, registrar.port, registrar.transport, code, reason)) def _NH_SIPAccountRegistrationDidFail(self, notification): - self.output.put('%s Failed to register contact for sip:%s: %s (retrying in %.2f seconds)\n' % (datetime.now().replace(microsecond=0), self.account.id, notification.data.error, notification.data.retry_after)) + reason = notification.data.error.decode() if isinstance(notification.data.error, bytes) else notification.data.error self.success = False - if self.max_registers is not None: self.max_registers -= 1 if self.max_registers == 0: + self.output.put('%s Failed to register contact for sip:%s: %s\n' % (datetime.now().replace(microsecond=0), self.account.id, reason)) self.stop() + else: + self.output.put('%s Failed to register contact for sip:%s: %s (retrying in %.2f seconds)\n' % (datetime.now().replace(microsecond=0), self.account.id, reason, notification.data.retry_after)) + else: + self.output.put('%s Failed to register contact for sip:%s: %s\n' % (datetime.now().replace(microsecond=0), self.account.id, reason)) + self.stop() def _NH_SIPAccountRegistrationDidEnd(self, notification): self.output.put('%s Registration ended.\n' % datetime.now().replace(microsecond=0)) def _NH_BonjourAccountRegistrationDidSucceed(self, notification): self.output.put('%s Registered Bonjour contact "%s"\n' % (datetime.now().replace(microsecond=0), notification.data.name)) if self.max_registers is not None: self.max_registers -= 1 if self.max_registers == 0: self.stop() def _NH_BonjourAccountRegistrationDidFail(self, notification): self.output.put('%s Failed to register Bonjour contact: %s\n' % (datetime.now().replace(microsecond=0), notification.data.reason)) if self.max_registers is not None: self.max_registers -= 1 if self.max_registers == 0: self.stop() def _NH_BonjourAccountRegistrationDidEnd(self, notification): self.output.put('%s Registration ended.\n' % datetime.now().replace(microsecond=0)) def _NH_BonjourAccountDidAddNeighbour(self, notification): neighbour, record = notification.data.neighbour, notification.data.record now = datetime.now().replace(microsecond=0) self.output.put('%s Discovered Bonjour neighbour: "%s (%s)" <%s>\n' % (now, record.name, record.host, record.uri)) self.neighbours[neighbour] = BonjourNeighbour(neighbour, record.uri, record.name, record.host) def _NH_BonjourAccountDidUpdateNeighbour(self, notification): neighbour, record = notification.data.neighbour, notification.data.record now = datetime.now().replace(microsecond=0) try: bonjour_neighbour = self.neighbours[neighbour] except KeyError: self.output.put('%s Discovered Bonjour neighbour: "%s (%s)" <%s>\n' % (now, record.name, record.host, record.uri)) self.neighbours[neighbour] = BonjourNeighbour(neighbour, record.uri, record.name, record.host) else: self.output.put('%s Updated Bonjour neighbour: "%s (%s)" <%s>\n' % (now, record.name, record.host, record.uri)) bonjour_neighbour.display_name = record.name bonjour_neighbour.host = record.host bonjour_neighbour.uri = record.uri def _NH_BonjourAccountDidRemoveNeighbour(self, notification): neighbour = notification.data.neighbour now = datetime.now().replace(microsecond=0) try: bonjour_neighbour = self.neighbours.pop(neighbour) except KeyError: pass else: self.output.put('%s Bonjour neighbour left: "%s (%s)" <%s>\n' % (now, bonjour_neighbour.display_name, bonjour_neighbour.host, bonjour_neighbour.uri)) def _NH_SIPEngineGotException(self, notification): self.output.put('%s An exception occured within the SIP core:\n%s\n' % (datetime.now().replace(microsecond=0), notification.data.traceback)) if __name__ == "__main__": description = 'This script registers the contact address of the given SIP account to the SIP registrar and refresh it while the program is running. When Ctrl+D is pressed it will unregister.' usage = '%prog [options]' parser = OptionParser(usage=usage, description=description) parser.print_usage = parser.print_help parser.add_option('-a', '--account', type='string', dest='account', help='The name of the account to use. If not supplied, the default account will be used.', metavar='NAME') parser.add_option('-c', '--config-directory', type='string', dest='config_directory', help='The configuration directory to use. This overrides the default location.') parser.add_option('-s', '--trace-sip', action='store_true', dest='trace_sip', default=False, help='Dump the raw contents of incoming and outgoing SIP messages (disabled by default).') parser.add_option('-j', '--trace-pjsip', action='store_true', dest='trace_pjsip', default=False, help='Print PJSIP logging output (disabled by default).') parser.add_option('-n', '--trace-notifications', action='store_true', dest='trace_notifications', default=False, help='Print all notifications (disabled by default).') parser.add_option('-r', '--max-registers', type='int', dest='max_registers', default=1, help='Max number of REGISTERs sent (default 1, set to 0 for infinite).') parser.add_option('-b', '--batch', action='store_true', dest='batch_mode', default=False, help='Run the program in batch mode: reading input from the console is disabled. This is particularly useful when running this script in a non-interactive environment.') options, args = parser.parse_args() application = RegistrationApplication() application.start(options) signal.signal(signal.SIGINT, signal.SIG_DFL) application.output.join() sleep(0.1) sys.exit(0 if application.success else 1)