From dd221c74c53113b0109f1ca1b392d43dd768cef5 Mon Sep 17 00:00:00 2001 From: Mark Michelson Date: Thu, 31 Oct 2013 22:09:47 +0000 Subject: [PATCH] Update the conversion script from sip.conf to pjsip.conf (closes issue ASTERISK-22374) Reported by Matt Jordan Review: https://reviewboard.asterisk.org/r/2846 ........ Merged revisions 402327 from http://svn.asterisk.org/svn/asterisk/branches/12 git-svn-id: https://origsvn.digium.com/svn/asterisk/trunk@402328 65c4cc65-6c06-0410-ace0-fbb531ad65f3 --- .../astconfigparser.py | 265 ++-- .../astdicts.py | 0 contrib/scripts/sip_to_pjsip/sip_to_pjsip.py | 1151 +++++++++++++++++ .../scripts/sip_to_res_sip/sip_to_res_sip.py | 392 ------ 4 files changed, 1320 insertions(+), 488 deletions(-) rename contrib/scripts/{sip_to_res_sip => sip_to_pjsip}/astconfigparser.py (59%) rename contrib/scripts/{sip_to_res_sip => sip_to_pjsip}/astdicts.py (100%) create mode 100755 contrib/scripts/sip_to_pjsip/sip_to_pjsip.py delete mode 100755 contrib/scripts/sip_to_res_sip/sip_to_res_sip.py diff --git a/contrib/scripts/sip_to_res_sip/astconfigparser.py b/contrib/scripts/sip_to_pjsip/astconfigparser.py similarity index 59% rename from contrib/scripts/sip_to_res_sip/astconfigparser.py rename to contrib/scripts/sip_to_pjsip/astconfigparser.py index 4a324e2ec6..c93173dee9 100644 --- a/contrib/scripts/sip_to_res_sip/astconfigparser.py +++ b/contrib/scripts/sip_to_pjsip/astconfigparser.py @@ -3,11 +3,12 @@ import re from astdicts import OrderedDict from astdicts import MultiOrderedDict + def merge_values(left, right, key): """Merges values from right into left.""" if isinstance(left, list): vals0 = left - else: # assume dictionary + else: # assume dictionary vals0 = left[key] if key in left else [] vals1 = right[key] if key in right else [] @@ -15,14 +16,16 @@ def merge_values(left, right, key): ############################################################################### + class Section(MultiOrderedDict): - """A Section is a MultiOrderedDict itself that maintains a list of - key/value options. However, in the case of an Asterisk config - file a section may have other defaults sections that is can pull - data from (i.e. templates). So when an option is looked up by key - it first checks the base section and if not found looks in the - added default sections. If not found at that point then a 'KeyError' - exception is raised. + """ + A Section is a MultiOrderedDict itself that maintains a list of + key/value options. However, in the case of an Asterisk config + file a section may have other defaults sections that is can pull + data from (i.e. templates). So when an option is looked up by key + it first checks the base section and if not found looks in the + added default sections. If not found at that point then a 'KeyError' + exception is raised. """ count = 0 @@ -35,9 +38,24 @@ class Section(MultiOrderedDict): self._templates = [] if templates is None else templates def __cmp__(self, other): + """ + Use self.id as means of determining equality + """ return cmp(self.id, other.id) - def get(self, key, from_self=True, from_templates=True, from_defaults=True): + def get(self, key, from_self=True, from_templates=True, + from_defaults=True): + """ + Get the values corresponding to a given key. The parameters to this + function form a hierarchy that determines priority of the search. + from_self takes priority over from_templates, and from_templates takes + priority over from_defaults. + + Parameters: + from_self - If True, search within the given section. + from_templates - If True, search in this section's templates. + from_defaults - If True, search within this section's defaults. + """ if from_self and key in self: return MultiOrderedDict.__getitem__(self, key) @@ -62,13 +80,19 @@ class Section(MultiOrderedDict): raise KeyError(key) def __getitem__(self, key): - """Get the value for the given key. If it is not found in the 'self' - then check inside templates and defaults before declaring raising - a KeyError exception. + """ + Get the value for the given key. If it is not found in the 'self' + then check inside templates and defaults before declaring raising + a KeyError exception. """ return self.get(key) def keys(self, self_only=False): + """ + Get the keys from this section. If self_only is True, then + keys from this section's defaults and templates are not + included in the returned value + """ res = MultiOrderedDict.keys(self) if self_only: return res @@ -85,13 +109,21 @@ class Section(MultiOrderedDict): return res def add_defaults(self, defaults): + """ + Add a list of defaults to the section. Defaults are + sections such as 'general' + """ defaults.sort() for i in defaults: self._defaults.insert(0, i) def add_templates(self, templates): - templates.sort(reverse=True); - self._templates.extend(templates) + """ + Add a list of templates to the section. + """ + templates.sort() + for i in templates: + self._templates.insert(0, i) def get_merged(self, key): """Return a list of values for a given key merged from default(s)""" @@ -120,9 +152,11 @@ COMMENT_END = '--;' DEFAULTSECT = 'general' + def remove_comment(line, is_comment): """Remove any commented elements from the line.""" - if not line: return line, is_comment + if not line: + return line, is_comment if is_comment: part = line.partition(COMMENT_END) @@ -152,23 +186,21 @@ def remove_comment(line, is_comment): # check for eol comment return line.partition(COMMENT)[0].strip(), False + def try_include(line): - """Checks to see if the given line is an include. If so return the - included filename, otherwise None. """ - if not line.startswith('#'): - return None + Checks to see if the given line is an include. If so return the + included filename, otherwise None. + """ + + match = re.match('^#include\s*[<"]?(.*)[>"]?$', line) + return match.group(1) if match else None - # it is an include - get file name - try: - return line[line.index('"') + 1:line.rindex('"')] - except ValueError: - print "Invalid include - could not parse filename." - return None def try_section(line): - """Checks to see if the given line is a section. If so return the section - name, otherwise return 'None'. + """ + Checks to see if the given line is a section. If so return the section + name, otherwise return 'None'. """ # leading spaces were stripped when checking for comments if not line.startswith('['): @@ -188,6 +220,7 @@ def try_section(line): except: return section[1:], False, templates + def try_option(line): """Parses the line as an option, returning the key/value pair.""" data = re.split('=>?', line) @@ -196,30 +229,12 @@ def try_option(line): ############################################################################### -def find_value(sections, key): - """Given a list of sections, try to find value(s) for the given key.""" - # always start looking in the last one added - sections.sort(reverse=True); - for s in sections: - try: - # try to find in section and section's templates - return s.get(key, from_defaults=False) - except KeyError: - pass - - # wasn't found in sections or a section's templates so check in defaults - for s in sections: - try: - # try to find in section's defaultsects - return s.get(key, from_self=False, from_templates=False) - except KeyError: - pass - - raise KeyError(key) def find_dict(mdicts, key, val): - """Given a list of mult-dicts, return the multi-dict that contains - the given key/value pair.""" + """ + Given a list of mult-dicts, return the multi-dict that contains + the given key/value pair. + """ def found(d): return key in d and val in d[key] @@ -230,44 +245,25 @@ def find_dict(mdicts, key, val): raise LookupError("Dictionary not located for key = %s, value = %s" % (key, val)) -def get_sections(parser, key, attr='_sections', searched=None): - if searched is None: - searched = [] - if parser is None or parser in searched: - return [] - try: - sections = getattr(parser, attr) - res = sections[key] if key in sections else [] - searched.append(parser) - return res + get_sections(parser._includes, key, attr, searched) \ - + get_sections(parser._parent, key, attr, searched) - except: - # assume ordereddict of parsers - res = [] - for p in parser.itervalues(): - res.extend(get_sections(p, key, attr, searched)) - return res - -def get_defaults(parser, key): - return get_sections(parser, key, '_defaults') - -def write_dicts(file, mdicts): +def write_dicts(config_file, mdicts): + """Write the contents of the mdicts to the specified config file""" for section, sect_list in mdicts.iteritems(): # every section contains a list of dictionaries for sect in sect_list: - file.write("[%s]\n" % section) + config_file.write("[%s]\n" % section) for key, val_list in sect.iteritems(): # every value is also a list for v in val_list: key_val = key if v is not None: key_val += " = " + str(v) - file.write("%s\n" % (key_val)) - file.write("\n") + config_file.write("%s\n" % (key_val)) + config_file.write("\n") ############################################################################### + class MultiOrderedConfigParser: def __init__(self, parent=None): self._parent = parent @@ -275,16 +271,39 @@ class MultiOrderedConfigParser: self._sections = MultiOrderedDict() self._includes = OrderedDict() + def find_value(self, sections, key): + """Given a list of sections, try to find value(s) for the given key.""" + # always start looking in the last one added + sections.sort(reverse=True) + for s in sections: + try: + # try to find in section and section's templates + return s.get(key, from_defaults=False) + except KeyError: + pass + + # wasn't found in sections or a section's templates so check in + # defaults + for s in sections: + try: + # try to find in section's defaultsects + return s.get(key, from_self=False, from_templates=False) + except KeyError: + pass + + raise KeyError(key) + def defaults(self): return self._defaults def default(self, key): """Retrieves a list of dictionaries for a default section.""" - return get_defaults(self, key) + return self.get_defaults(key) def add_default(self, key, template_keys=None): - """Adds a default section to defaults, returning the - default Section object. + """ + Adds a default section to defaults, returning the + default Section object. """ if template_keys is None: template_keys = [] @@ -295,17 +314,47 @@ class MultiOrderedConfigParser: def section(self, key): """Retrieves a list of dictionaries for a section.""" - return get_sections(self, key) + return self.get_sections(key) + + def get_sections(self, key, attr='_sections', searched=None): + """ + Retrieve a list of sections that have values for the given key. + The attr parameter can be used to control what part of the parser + to retrieve values from. + """ + if searched is None: + searched = [] + if self in searched: + return [] + + sections = getattr(self, attr) + res = sections[key] if key in sections else [] + searched.append(self) + if self._includes: + res += self._includes.get_sections(key, attr, searched) + if self._parent: + res += self._parent.get_sections(key, attr, searched) + return res + + def get_defaults(self, key): + """ + Retrieve a list of defaults that have values for the given key. + """ + return self.get_sections(key, '_defaults') def add_section(self, key, template_keys=None, mdicts=None): + """ + Create a new section in the configuration. The name of the + new section is the 'key' parameter. + """ if template_keys is None: template_keys = [] if mdicts is None: mdicts = self._sections res = Section() for t in template_keys: - res.add_templates(get_defaults(self, t)) - res.add_defaults(get_defaults(self, DEFAULTSECT)) + res.add_templates(self.get_defaults(t)) + res.add_defaults(self.get_defaults(DEFAULTSECT)) mdicts.insert(0, key, res) return res @@ -313,29 +362,50 @@ class MultiOrderedConfigParser: return self._includes def add_include(self, filename, parser=None): + """ + Add a new #include file to the configuration. + """ if filename in self._includes: return self._includes[filename] self._includes[filename] = res = \ - MultiOrderedConfigParser(self) if parser is None else parser - return res; + MultiOrderedConfigParser(self) if parser is None else parser + return res def get(self, section, key): """Retrieves the list of values from a section for a key.""" try: # search for the value in the list of sections - return find_value(self.section(section), key) + return self.find_value(self.section(section), key) except KeyError: pass try: # section may be a default section so, search # for the value in the list of defaults - return find_value(self.default(section), key) + return self.find_value(self.default(section), key) except KeyError: raise LookupError("key %r not found for section %r" % (key, section)) + def multi_get(self, section, key_list): + """ + Retrieves the list of values from a section for a list of keys. + This method is intended to be used for equivalent keys. Thus, as soon + as any match is found for any key in the key_list, the match is + returned. This does not concatenate the lookups of all of the keys + together. + """ + for i in key_list: + try: + return self.get(section, i) + except LookupError: + pass + + # Making it here means all lookups failed. + raise LookupError("keys %r not found for section %r" % + (key_list, section)) + def set(self, section, key, val): """Sets an option in the given section.""" # TODO - set in multiple sections? (for now set in first) @@ -346,15 +416,17 @@ class MultiOrderedConfigParser: self.defaults(section)[0][key] = val def read(self, filename): + """Parse configuration information from a file""" try: - with open(filename, 'rt') as file: - self._read(file, filename) + with open(filename, 'rt') as config_file: + self._read(config_file) except IOError: print "Could not open file ", filename, " for reading" - def _read(self, file, filename): - is_comment = False # used for multi-lined comments - for line in file: + def _read(self, config_file): + """Parse configuration information from the config_file""" + is_comment = False # used for multi-lined comments + for line in config_file: line, is_comment = remove_comment(line, is_comment) if not line: # line was empty or was a comment @@ -377,18 +449,19 @@ class MultiOrderedConfigParser: key, val = try_option(line) sect[key] = val - def write(self, f): + def write(self, config_file): + """Write configuration information out to a file""" try: for key, val in self._includes.iteritems(): val.write(key) - f.write('#include "%s"\n' % key) + config_file.write('#include "%s"\n' % key) - f.write('\n') - write_dicts(f, self._defaults) - write_dicts(f, self._sections) + config_file.write('\n') + write_dicts(config_file, self._defaults) + write_dicts(config_file, self._sections) except: try: - with open(f, 'wt') as fp: + with open(config_file, 'wt') as fp: self.write(fp) except IOError: - print "Could not open file ", f, " for writing" + print "Could not open file ", config_file, " for writing" diff --git a/contrib/scripts/sip_to_res_sip/astdicts.py b/contrib/scripts/sip_to_pjsip/astdicts.py similarity index 100% rename from contrib/scripts/sip_to_res_sip/astdicts.py rename to contrib/scripts/sip_to_pjsip/astdicts.py diff --git a/contrib/scripts/sip_to_pjsip/sip_to_pjsip.py b/contrib/scripts/sip_to_pjsip/sip_to_pjsip.py new file mode 100755 index 0000000000..96a9a02ad7 --- /dev/null +++ b/contrib/scripts/sip_to_pjsip/sip_to_pjsip.py @@ -0,0 +1,1151 @@ +#!/usr/bin/python + +import optparse +import astdicts +import astconfigparser +import socket +import re + +PREFIX = 'pjsip_' + +############################################################################### +### some utility functions +############################################################################### + + +def section_by_type(section, pjsip, type): + """Finds a section based upon the given type, adding it if not found.""" + def __find_dict(mdicts, key, val): + """Given a list of mult-dicts, return the multi-dict that contains + the given key/value pair.""" + + def found(d): + return key in d and val in d[key] + + try: + return [d for d in mdicts if found(d)][0] + except IndexError: + raise LookupError("Dictionary not located for key = %s, value = %s" + % (key, val)) + + try: + return __find_dict(pjsip.section(section), 'type', type) + except LookupError: + # section for type doesn't exist, so add + sect = pjsip.add_section(section) + sect['type'] = type + return sect + + +def set_value(key=None, val=None, section=None, pjsip=None, + nmapped=None, type='endpoint'): + """Sets the key to the value within the section in pjsip.conf""" + def _set_value(k, v, s, r, n): + set_value(key if key else k, v, s, r, n, type) + + # if no value or section return the set_value + # function with the enclosed key and type + if not val and not section: + return _set_value + + # otherwise try to set the value + section_by_type(section, pjsip, type)[key] = \ + val[0] if isinstance(val, list) else val + + +def merge_value(key=None, val=None, section=None, pjsip=None, + nmapped=None, type='endpoint', section_to=None): + """Merge values from the given section with those from the default.""" + def _merge_value(k, v, s, r, n): + merge_value(key if key else k, v, s, r, n, type, section_to) + + # if no value or section return the merge_value + # function with the enclosed key and type + if not val and not section: + return _merge_value + + # should return a single value section list + try: + sect = sip.section(section)[0] + except LookupError: + sect = sip.default(section)[0] + # for each merged value add it to pjsip.conf + for i in sect.get_merged(key): + set_value(key, i, section_to if section_to else section, + pjsip, nmapped, type) + + +def non_mapped(nmapped): + """Write non-mapped sip.conf values to the non-mapped object""" + def _non_mapped(section, key, val): + """Writes a non-mapped value from sip.conf to the non-mapped object.""" + if section not in nmapped: + nmapped[section] = astconfigparser.Section() + if isinstance(val, list): + for v in val: + # since coming from sip.conf we can assume + # single section lists + nmapped[section][0][key] = v + else: + nmapped[section][0][key] = val + return _non_mapped + +############################################################################### +### mapping functions - +### define f(key, val, section) where key/val are the key/value pair to +### write to given section in pjsip.conf +############################################################################### + + +def set_dtmfmode(key, val, section, pjsip, nmapped): + """ + Sets the dtmfmode value. If value matches allowable option in pjsip + then map it, otherwise set it to none. + """ + # available pjsip.conf values: rfc4733, inband, info, none + if val == 'inband' or val == 'info': + set_value(key, val, section, pjsip, nmapped) + elif val == 'rfc2833': + set_value(key, 'rfc4733', section, pjsip, nmapped) + else: + nmapped(section, key, val + " ; did not fully map - set to none") + set_value(key, 'none', section, pjsip, nmapped) + + +def from_nat(key, val, section, pjsip, nmapped): + """Sets values from nat into the appropriate pjsip.conf options.""" + # nat from sip.conf can be comma separated list of values: + # yes/no, [auto_]force_rport, [auto_]comedia + if 'yes' in val: + set_value('rtp_symmetric', 'yes', section, pjsip, nmapped) + set_value('rewrite_contact', 'yes', section, pjsip, nmapped) + if 'comedia' in val: + set_value('rtp_symmetric', 'yes', section, pjsip, nmapped) + if 'force_rport' in val: + set_value('force_rport', 'yes', section, pjsip, nmapped) + set_value('rewrite_contact', 'yes', section, pjsip, nmapped) + + +def set_timers(key, val, section, pjsip, nmapped): + """ + Sets the timers in pjsip.conf from the session-timers option + found in sip.conf. + """ + # pjsip.conf values can be yes/no, required, always + if val == 'originate': + set_value('timers', 'always', section, pjsip, nmapped) + elif val == 'accept': + set_value('timers', 'required', section, pjsip, nmapped) + elif val == 'never': + set_value('timers', 'no', section, pjsip, nmapped) + else: + set_value('timers', 'yes', section, pjsip, nmapped) + + +def set_direct_media(key, val, section, pjsip, nmapped): + """ + Maps values from the sip.conf comma separated direct_media option + into pjsip.conf direct_media options. + """ + if 'yes' in val: + set_value('direct_media', 'yes', section, pjsip, nmapped) + if 'update' in val: + set_value('direct_media_method', 'update', section, pjsip, nmapped) + if 'outgoing' in val: + set_value('directed_media_glare_mitigation', 'outgoing', section, + pjsip, nmapped) + if 'nonat' in val: + set_value('disable_directed_media_on_nat', 'yes', section, pjsip, + nmapped) + if 'no' in val: + set_value('direct_media', 'no', section, pjsip, nmapped) + + +def from_sendrpid(key, val, section, pjsip, nmapped): + """Sets the send_rpid/pai values in pjsip.conf.""" + if val == 'yes' or val == 'rpid': + set_value('send_rpid', 'yes', section, pjsip, nmapped) + elif val == 'pai': + set_value('send_pai', 'yes', section, pjsip, nmapped) + + +def set_media_encryption(key, val, section, pjsip, nmapped): + """Sets the media_encryption value in pjsip.conf""" + try: + dtls = sip.get(section, 'dtlsenable')[0] + if dtls == 'yes': + # If DTLS is enabled, then that overrides SDES encryption. + return + except LookupError: + pass + + if val == 'yes': + set_value('media_encryption', 'sdes', section, pjsip, nmapped) + + +def from_recordfeature(key, val, section, pjsip, nmapped): + """ + If record on/off feature is set to automixmon then set + one_touch_recording, otherwise it can't be mapped. + """ + set_value('one_touch_recording', 'yes', section, pjsip, nmapped) + set_value(key, val, section, pjsip, nmapped) + + +def from_progressinband(key, val, section, pjsip, nmapped): + """Sets the inband_progress value in pjsip.conf""" + # progressinband can = yes/no/never + if val == 'never': + val = 'no' + set_value('inband_progress', val, section, pjsip, nmapped) + + +def build_host(config, host, section, port_key): + """ + Returns a string composed of a host:port. This assumes that the host + may have a port as part of the initial value. The port_key is only used + if the host does not already have a port set on it. + Throws a LookupError if the key does not exist + """ + port = None + + try: + socket.inet_pton(socket.AF_INET6, host) + if not host.startswith('['): + # SIP URI will need brackets. + host = '[' + host + ']' + else: + # If brackets are present, there may be a port as well + port = re.match('\[.*\]:(\d+)', host) + except socket.error: + # No biggie. It's just not an IPv6 address + port = re.match('.*:(\d+)', host) + + result = host + + if not port: + try: + port = config.get(section, port_key)[0] + result += ':' + port + except LookupError: + pass + + return result + + +def from_host(key, val, section, pjsip, nmapped): + """ + Sets contact info in an AOR section in pjsip.conf using 'host' + and 'port' data from sip.conf + """ + # all aors have the same name as the endpoint so makes + # it easy to set endpoint's 'aors' value + set_value('aors', section, section, pjsip, nmapped) + if val == 'dynamic': + # Easy case. Just set the max_contacts on the aor and we're done + set_value('max_contacts', 1, section, pjsip, nmapped, 'aor') + return + + result = 'sip:' + + # More difficult case. The host will be either a hostname or + # IP address and may or may not have a port specified. pjsip.conf + # expects the contact to be a SIP URI. + + user = None + + try: + user = sip.multi_get(section, ['defaultuser', 'username'])[0] + result += user + '@' + except LookupError: + # It's fine if there's no user name + pass + + result += build_host(sip, val, section, 'port') + + set_value('contact', result, section, pjsip, nmapped, 'aor') + + +def from_mailbox(key, val, section, pjsip, nmapped): + """ + Determines whether a mailbox configured in sip.conf should map to + an endpoint or aor in pjsip.conf. If subscribemwi is true, then the + mailboxes are set on an aor. Otherwise the mailboxes are set on the + endpoint. + """ + + try: + subscribemwi = sip.get(section, 'subscribemwi')[0] + except LookupError: + # No subscribemwi option means default it to 'no' + subscribemwi = 'no' + + set_value('mailboxes', val, section, pjsip, nmapped, 'aor' + if subscribemwi == 'yes' else 'endpoint') + + +def setup_auth(key, val, section, pjsip, nmapped): + """ + Sets up authentication information for a specific endpoint based on the + 'secret' setting on a peer in sip.conf + """ + set_value('username', section, section, pjsip, nmapped, 'auth') + # In chan_sip, if a secret and an md5secret are both specified on a peer, + # then in practice, only the md5secret is used. If both are encountered + # then we build an auth section that has both an md5_cred and password. + # However, the auth_type will indicate to authenticators to use the + # md5_cred, so like with sip.conf, the password will be there but have + # no purpose. + if key == 'secret': + set_value('password', val, section, pjsip, nmapped, 'auth') + else: + set_value('md5_cred', val, section, pjsip, nmapped, 'auth') + set_value('auth_type', 'md5', section, pjsip, nmapped, 'auth') + + realms = [section] + try: + auths = sip.get('authentication', 'auth') + for i in auths: + user, at, realm = i.partition('@') + realms.append(realm) + except LookupError: + pass + + realm_str = ','.join(realms) + + set_value('auth', section, section, pjsip, nmapped) + set_value('outbound_auth', realm_str, section, pjsip, nmapped) + + +def setup_ident(key, val, section, pjsip, nmapped): + """ + Examines the 'type' field for a sip.conf peer and creates an identify + section if the type is either 'peer' or 'friend'. The identify section uses + either the host or defaultip field of the sip.conf peer. + """ + if val != 'peer' and val != 'friend': + return + + try: + ip = sip.get(section, 'host')[0] + except LookupError: + return + + if ip == 'dynamic': + try: + ip = sip.get(section, 'defaultip')[0] + except LookupError: + return + + set_value('endpoint', section, section, pjsip, nmapped, 'identify') + set_value('match', ip, section, pjsip, nmapped, 'identify') + + +def from_encryption_taglen(key, val, section, pjsip, nmapped): + """Sets the srtp_tag32 option based on sip.conf encryption_taglen""" + if val == '32': + set_value('srtp_tag_32', 'yes', section, pjsip, nmapped) + + +def from_dtlsenable(key, val, section, pjsip, nmapped): + """Optionally sets media_encryption=dtls based on sip.conf dtlsenable""" + if val == 'yes': + set_value('media_encryption', 'dtls', section, pjsip, nmapped) + +############################################################################### + +# options in pjsip.conf on an endpoint that have no sip.conf equivalent: +# type, rtp_ipv6, 100rel, trust_id_outbound, aggregate_mwi, +# connected_line_method + +# known sip.conf peer keys that can be mapped to a pjsip.conf section/key +peer_map = [ + # sip.conf option mapping function pjsip.conf option(s) + ########################################################################### + ['context', set_value], + ['dtmfmode', set_dtmfmode], + ['disallow', merge_value], + ['allow', merge_value], + ['nat', from_nat], # rtp_symmetric, force_rport, + # rewrite_contact + ['icesupport', set_value('ice_support')], + ['autoframing', set_value('use_ptime')], + ['outboundproxy', set_value('outbound_proxy')], + ['mohsuggest', set_value], + ['session-timers', set_timers], # timers + ['session-minse', set_value('timers_min_se')], + ['session-expires', set_value('timers_sess_expires')], + ['externip', set_value('external_media_address')], + ['externhost', set_value('external_media_address')], + # identify_by ? + ['directmedia', set_direct_media], # direct_media + # direct_media_method + # directed_media_glare_mitigation + # disable_directed_media_on_nat + ['callerid', set_value], # callerid + ['callingpres', set_value('callerid_privacy')], + ['cid_tag', set_value('callerid_tag')], + ['trustpid', set_value('trust_id_inbound')], + ['sendrpid', from_sendrpid], # send_pai, send_rpid + ['send_diversion', set_value], + ['encrpytion', set_media_encryption], + ['avpf', set_value('use_avpf')], + ['recordonfeature', from_recordfeature], # automixon + ['recordofffeature', from_recordfeature], # automixon + ['progressinband', from_progressinband], # in_band_progress + ['callgroup', set_value], + ['pickupgroup', set_value], + ['namedcallgroup', set_value], + ['namedpickupgroup', set_value], + ['allowtransfer', set_value], + ['fromuser', set_value], + ['fromdomain', set_value], + ['mwifrom', set_value('mwifromuser')], + ['tos_audio', set_value], + ['tos_video', set_value], + ['cos_audio', set_value], + ['cos_video', set_value], + ['sdpowner', set_value], + ['sdpsession', set_value], + ['tonezone', set_value], + ['language', set_value], + ['allowsubscribe', set_value], + ['subminexpiry', set_value], + ['rtp_engine', set_value('rtpengine')], + ['mailbox', from_mailbox], + ['busylevel', set_value('devicestate_busy_at')], + ['secret', setup_auth], + ['md5secret', setup_auth], + ['type', setup_ident], + ['dtlsenable', from_dtlsenable], + ['dtlsverify', set_value], + ['dtlsrekey', set_value], + ['dtlscertfile', set_value], + ['dtlsprivatekey', set_value], + ['dtlscipher', set_value], + ['dtlscafile', set_value], + ['dtlscapath', set_value], + ['dtlssetup', set_value], + ['encryption_taglen', from_encryption_taglen], + +############################ maps to an aor ################################### + + ['host', from_host], # contact, max_contacts + ['qualifyfreq', set_value('qualify_frequency', type='aor')], + +############################# maps to auth##################################### +# type = auth +# username +# password +# md5_cred +# realm +# nonce_lifetime +# auth_type +######################### maps to acl/security ################################ + + ['permit', merge_value(type='acl', section_to='acl')], + ['deny', merge_value(type='acl', section_to='acl')], + ['acl', merge_value(type='acl', section_to='acl')], + ['contactpermit', merge_value(type='acl', section_to='acl')], + ['contactdeny', merge_value(type='acl', section_to='acl')], + ['contactacl', merge_value(type='acl', section_to='acl')], + +########################### maps to transport ################################# +# type = transport +# protocol +# bind +# async_operations +# ca_list_file +# cert_file +# privkey_file +# password +# external_signaling_address - externip & externhost +# external_signaling_port +# external_media_address +# domain +# verify_server +# verify_client +# require_client_cert +# method +# cipher +# localnet +######################### maps to domain_alias ################################ +# type = domain_alias +# domain +######################### maps to registration ################################ +# type = registration +# server_uri +# client_uri +# contact_user +# transport +# outbound_proxy +# expiration +# retry_interval +# max_retries +# auth_rejection_permanent +# outbound_auth +########################### maps to identify ################################## +# type = identify +# endpoint +# match +] + + +def add_localnet(section, pjsip, nmapped): + """ + Adds localnet values from sip.conf's general section to a transport in + pjsip.conf. Ideally, we would have just created a template with the + localnet sections, but because this is a script, it's not hard to add + the same thing on to every transport. + """ + try: + merge_value('localnet', sip.get('general', 'localnet')[0], 'general', + pjsip, nmapped, 'transport', section) + except LookupError: + # No localnet options configured. No biggie! + pass + + +def set_transport_common(section, pjsip, nmapped): + """ + sip.conf has several global settings that in pjsip.conf apply to individual + transports. This function adds these global settings to each individual + transport. + + The settings included are: + localnet + tos_sip + cos_sip + """ + + try: + merge_value('localnet', sip.get('general', 'localnet')[0], 'general', + pjsip, nmapped, 'transport', section) + except LookupError: + # No localnet options configured. Move on. + pass + + try: + set_value('tos', sip.get('general', 'sip_tos')[0], 'general', pjsip, + nmapped, 'transport', section) + except LookupError: + pass + + try: + set_value('cos', sip.get('general', 'sip_cos')[0], 'general', pjsip, + nmapped, 'transport', section) + except LookupError: + pass + + +def split_hostport(addr): + """ + Given an address in the form 'addr:port' separate the addr and port + components. + Returns a two-tuple of strings, (addr, port). If no port is present in the + string, then the port section of the tuple is None. + """ + try: + socket.inet_pton(socket.AF_INET6, addr) + if not addr.startswith('['): + return (addr, None) + else: + # If brackets are present, there may be a port as well + match = re.match('\[(.*\)]:(\d+)', addr) + if match: + return (match.group(1), match.group(2)) + else: + return (addr, None) + except socket.error: + pass + + # IPv4 address or hostname + host, sep, port = addr.rpartition(':') + + if not sep and not port: + return (host, None) + else: + return (host, port) + + +def create_udp(sip, pjsip, nmapped): + """ + Creates a 'transport-udp' section in the pjsip.conf file based + on the following settings from sip.conf: + + bindaddr (or udpbindaddr) + bindport + externaddr (or externip) + externhost + """ + + bind = sip.multi_get('general', ['udpbindaddr', 'bindaddr'])[0] + bind = build_host(sip, bind, 'general', 'bindport') + + try: + extern_addr = sip.multi_get('general', ['externaddr', 'externip', + 'externhost'])[0] + host, port = split_hostport(extern_addr) + set_value('external_signaling_address', host, 'transport-udp', pjsip, + nmapped, 'transport') + if port: + set_value('external_signaling_port', port, 'transport-udp', pjsip, + nmapped, 'transport') + except LookupError: + pass + + set_value('protocol', 'udp', 'transport-udp', pjsip, nmapped, 'transport') + set_value('bind', bind, 'transport-udp', pjsip, nmapped, 'transport') + set_transport_common('transport-udp', pjsip, nmapped) + + +def create_tcp(sip, pjsip, nmapped): + """ + Creates a 'transport-tcp' section in the pjsip.conf file based + on the following settings from sip.conf: + + tcpenable + tcpbindaddr + externtcpport + """ + + try: + enabled = sip.get('general', 'tcpenable')[0] + except: + # No value means disabled by default. No need for a tranport + return + + if enabled == 'no': + return + + try: + bind = sip.get('general', 'tcpbindaddr')[0] + bind = build_host(sip, bind, 'general', 'bindport') + except LookupError: + # No tcpbindaddr means to default to the udpbindaddr + bind = pjsip.get('transport-udp', 'bind')[0] + + try: + extern_addr = sip.multi_get('general', ['externaddr', 'externip', + 'externhost'])[0] + host, port = split_hostport(extern_addr) + try: + tcpport = sip.get('general', 'externtcpport')[0] + except: + tcpport = port + set_value('external_signaling_address', host, 'transport-tcp', pjsip, + nmapped, 'transport') + if tcpport: + set_value('external_signaling_port', tcpport, 'transport-tcp', + pjsip, nmapped, 'transport') + except LookupError: + pass + + set_value('protocol', 'tcp', 'transport-tcp', pjsip, nmapped, 'transport') + set_value('bind', bind, 'transport-tcp', pjsip, nmapped, 'transport') + set_transport_common('transport-tcp', pjsip, nmapped) + + +def set_tls_bindaddr(val, pjsip, nmapped): + """ + Creates the TCP bind address. This has two possible methods of + working: + Use the 'tlsbindaddr' option from sip.conf directly if it has both + an address and port. If no port is present, use 5061 + If there is no 'tlsbindaddr' option present in sip.conf, use the + previously-established UDP bind address and port 5061 + """ + try: + bind = sip.get('general', 'tlsbindaddr')[0] + explicit = True + except LookupError: + # No tlsbindaddr means to default to the bindaddr but with standard TLS + # port + bind = pjsip.get('transport-udp', 'bind')[0] + explicit = False + + matchv4 = re.match('\d+\.\d+\.\d+\.\d+:\d+', bind) + matchv6 = re.match('\[.*\]:d+', bind) + if matchv4 or matchv6: + if explicit: + # They provided a port. We'll just use it. + set_value('bind', bind, 'transport-tls', pjsip, nmapped, + 'transport') + return + else: + # Need to strip the port from the UDP address + index = bind.rfind(':') + bind = bind[:index] + + # Reaching this point means either there was no port provided or we + # stripped the port off. We need to add on the default 5061 port + + bind += ':5061' + + set_value('bind', bind, 'transport-tls', pjsip, nmapped, 'transport') + + +def set_tls_private_key(val, pjsip, nmapped): + """Sets privkey_file based on sip.conf tlsprivatekey or sslprivatekey""" + set_value('privkey_file', val, 'transport-tls', pjsip, nmapped, + 'transport') + + +def set_tls_cipher(val, pjsip, nmapped): + """Sets cipher based on sip.conf tlscipher or sslcipher""" + set_value('cipher', val, 'transport-tls', pjsip, nmapped, 'transport') + + +def set_tls_cafile(val, pjsip, nmapped): + """Sets ca_list_file based on sip.conf tlscafile""" + set_value('ca_list_file', val, 'transport-tls', pjsip, nmapped, + 'transport') + + +def set_tls_verifyclient(val, pjsip, nmapped): + """Sets verify_client based on sip.conf tlsverifyclient""" + set_value('verify_client', val, 'transport-tls', pjsip, nmapped, + 'transport') + + +def set_tls_verifyserver(val, pjsip, nmapped): + """Sets verify_server based on sip.conf tlsdontverifyserver""" + + if val == 'no': + set_value('verify_server', 'yes', 'transport-tls', pjsip, nmapped, + 'transport') + else: + set_value('verify_server', 'no', 'transport-tls', pjsip, nmapped, + 'transport') + + +def set_tls_method(val, pjsip, nmapped): + """Sets method based on sip.conf tlsclientmethod or sslclientmethod""" + set_value('method', val, 'transport-tls', pjsip, nmapped, 'transport') + + +def create_tls(sip, pjsip, nmapped): + """ + Creates a 'transport-tls' section in pjsip.conf based on the following + settings from sip.conf: + + tlsenable (or sslenable) + tlsbindaddr (or sslbindaddr) + tlsprivatekey (or sslprivatekey) + tlscipher (or sslcipher) + tlscafile + tlscapath (or tlscadir) + tlscertfile (or sslcert or tlscert) + tlsverifyclient + tlsdontverifyserver + tlsclientmethod (or sslclientmethod) + """ + + tls_map = [ + (['tlsbindaddr', 'sslbindaddr'], set_tls_bindaddr), + (['tlsprivatekey', 'sslprivatekey'], set_tls_private_key), + (['tlscipher', 'sslcipher'], set_tls_cipher), + (['tlscafile'], set_tls_cafile), + (['tlsverifyclient'], set_tls_verifyclient), + (['tlsdontverifyserver'], set_tls_verifyserver), + (['tlsclientmethod', 'sslclientmethod'], set_tls_method) + ] + + try: + enabled = sip.multi_get('general', ['tlsenable', 'sslenable'])[0] + except LookupError: + # Not enabled. Don't create a transport + return + + if enabled == 'no': + return + + set_value('protocol', 'tls', 'transport-tls', pjsip, nmapped, 'transport') + + for i in tls_map: + try: + i[1](sip.multi_get('general', i[0])[0], pjsip, nmapped) + except LookupError: + pass + + set_transport_common('transport-tls', pjsip, nmapped) + try: + extern_addr = sip.multi_get('general', ['externaddr', 'externip', + 'externhost'])[0] + host, port = split_hostport(extern_addr) + try: + tlsport = sip.get('general', 'externtlsport')[0] + except: + tlsport = port + set_value('external_signaling_address', host, 'transport-tls', pjsip, + nmapped, 'transport') + if tlsport: + set_value('external_signaling_port', tlsport, 'transport-tls', + pjsip, nmapped, 'transport') + except LookupError: + pass + + +def map_transports(sip, pjsip, nmapped): + """ + Finds options in sip.conf general section pertaining to + transport configuration and creates appropriate transport + configuration sections in pjsip.conf. + + sip.conf only allows a single UDP transport, TCP transport, + and TLS transport. As such, the mapping into PJSIP can be made + consistent by defining three sections: + + transport-udp + transport-tcp + transport-tls + + To accommodate the default behaviors in sip.conf, we'll need to + create the UDP transport first, followed by the TCP and TLS transports. + """ + + # First create a UDP transport. Even if no bind parameters were provided + # in sip.conf, chan_sip would always bind to UDP 0.0.0.0:5060 + create_udp(sip, pjsip, nmapped) + + # TCP settings may be dependent on UDP settings, so do it second. + create_tcp(sip, pjsip, nmapped) + create_tls(sip, pjsip, nmapped) + + +def map_auth(sip, pjsip, nmapped): + """ + Creates auth sections based on entries in the authentication section of + sip.conf. pjsip.conf section names consist of "auth_" followed by the name + of the realm. + """ + try: + auths = sip.get('authentication', 'auth') + except LookupError: + return + + for i in auths: + creds, at, realm = i.partition('@') + if not at and not realm: + # Invalid. Move on + continue + user, colon, secret = creds.partition(':') + if not secret: + user, sharp, md5 = creds.partition('#') + if not md5: + #Invalid. move on + continue + section = "auth_" + realm + + set_value('realm', realm, section, pjsip, nmapped, 'auth') + set_value('username', user, section, pjsip, nmapped, 'auth') + if secret: + set_value('password', secret, section, pjsip, nmapped, 'auth') + else: + set_value('md5_cred', md5, section, pjsip, nmapped, 'auth') + set_value('auth_type', 'md5', section, pjsip, nmapped, 'auth') + + +class Registration: + """ + Class for parsing and storing information in a register line in sip.conf. + """ + def __init__(self, line, retry_interval, max_attempts, outbound_proxy): + self.retry_interval = retry_interval + self.max_attempts = max_attempts + self.outbound_proxy = outbound_proxy + self.parse(line) + + def parse(self, line): + """ + Initial parsing routine for register lines in sip.conf. + + This splits the line into the part before the host, and the part + after the '@' symbol. These two parts are then passed to their + own parsing routines + """ + + # register => + # [peer?][transport://]user[@domain][:secret[:authuser]]@host[:port][/extension][~expiry] + + prehost, at, host_part = line.rpartition('@') + if not prehost: + raise + + self.parse_host_part(host_part) + self.parse_user_part(prehost) + + def parse_host_part(self, host_part): + """ + Parsing routine for the part after the final '@' in a register line. + The strategy is to use partition calls to peel away the data starting + from the right and working to the left. + """ + pre_expiry, sep, expiry = host_part.partition('~') + pre_extension, sep, self.extension = pre_expiry.partition('/') + self.host, sep, self.port = pre_extension.partition(':') + + self.expiry = expiry if expiry else '120' + + def parse_user_part(self, user_part): + """ + Parsing routine for the part before the final '@' in a register line. + The only mandatory part of this line is the user portion. The strategy + here is to start by using partition calls to remove everything to + the right of the user, then finish by using rpartition calls to remove + everything to the left of the user. + """ + colons = user_part.count(':') + if (colons == 3): + # :domainport:secret:authuser + pre_auth, sep, port_auth = user_part.partition(':') + self.domainport, sep, auth = port_auth.partition(':') + self.secret, sep, self.authuser = auth.partition(':') + elif (colons == 2): + # :secret:authuser + pre_auth, sep, auth = user_part.partition(':') + self.secret, sep, self.authuser = auth.partition(':') + elif (colons == 1): + # :secret + pre_auth, sep, self.secret = user_part.partition(':') + elif (colons == 0): + # No port, secret, or authuser + pre_auth = user_part + else: + # Invalid setting + raise + + pre_domain, sep, self.domain = pre_auth.partition('@') + self.peer, sep, post_peer = pre_domain.rpartition('?') + transport, sep, self.user = post_peer.rpartition('://') + + self.protocol = transport if transport else 'udp' + + def write(self, pjsip, nmapped): + """ + Write parsed registration data into a section in pjsip.conf + + Most of the data in self will get written to a registration section. + However, there will also need to be an auth section created if a + secret or authuser is present. + + General mapping of values: + A combination of self.host and self.port is server_uri + A combination of self.user, self.domain, and self.domainport is + client_uri + self.expiry is expiration + self.extension is contact_user + self.protocol will map to one of the mapped transports + self.secret and self.authuser will result in a new auth section, and + outbound_auth will point to that section. + XXX self.peer really doesn't map to anything :( + """ + + section = 'reg_' + self.host + + set_value('retry_interval', self.retry_interval, section, pjsip, + nmapped, 'registration') + set_value('max_retries', self.max_attempts, section, pjsip, nmapped, + 'registration') + if self.extension: + set_value('contact_user', self.extension, section, pjsip, nmapped, + 'registration') + + set_value('expiration', self.expiry, section, pjsip, nmapped, + 'registration') + + if self.protocol == 'udp': + set_value('transport', 'transport-udp', section, pjsip, nmapped, + 'registration') + elif self.protocol == 'tcp': + set_value('transport', 'transport-tcp', section, pjsip, nmapped, + 'registration') + elif self.protocol == 'tls': + set_value('transport', 'transport-tls', section, pjsip, nmapped, + 'registration') + + auth_section = 'auth_reg_' + self.host + + if self.secret: + set_value('password', self.secret, auth_section, pjsip, nmapped, + 'auth') + set_value('username', self.authuser or self.user, auth_section, + pjsip, nmapped, 'auth') + set_value('outbound_auth', auth_section, section, pjsip, nmapped, + 'registration') + + client_uri = "sip:%s@" % self.user + if self.domain: + client_uri += self.domain + else: + client_uri += self.host + + if self.domainport: + client_uri += ":" + self.domainport + elif self.port: + client_uri += ":" + self.port + + set_value('client_uri', client_uri, section, pjsip, nmapped, + 'registration') + + server_uri = "sip:%s" % self.host + if self.port: + server_uri += ":" + self.port + + set_value('server_uri', server_uri, section, pjsip, nmapped, + 'registration') + + if self.outbound_proxy: + set_value('outboundproxy', self.outbound_proxy, section, pjsip, + nmapped, 'registartion') + + +def map_registrations(sip, pjsip, nmapped): + """ + Gathers all necessary outbound registration data in sip.conf and creates + corresponding registration sections in pjsip.conf + """ + try: + regs = sip.get('general', 'register') + except LookupError: + return + + try: + retry_interval = sip.get('general', 'registertimeout')[0] + except LookupError: + retry_interval = '20' + + try: + max_attempts = sip.get('general', 'registerattempts')[0] + except LookupError: + max_attempts = '10' + + try: + outbound_proxy = sip.get('general', 'outboundproxy')[0] + except LookupError: + outbound_proxy = '' + + for i in regs: + reg = Registration(i, retry_interval, max_attempts, outbound_proxy) + reg.write(pjsip, nmapped) + + +def map_peer(sip, section, pjsip, nmapped): + """ + Map the options from a peer section in sip.conf into the appropriate + sections in pjsip.conf + """ + for i in peer_map: + try: + # coming from sip.conf the values should mostly be a list with a + # single value. In the few cases that they are not a specialized + # function (see merge_value) is used to retrieve the values. + i[1](i[0], sip.get(section, i[0])[0], section, pjsip, nmapped) + except LookupError: + pass # key not found in sip.conf + + +def find_non_mapped(sections, nmapped): + """ + Determine sip.conf options that were not properly mapped to pjsip.conf + options. + """ + for section, sect in sections.iteritems(): + try: + # since we are pulling from sip.conf this should always + # be a single value list + sect = sect[0] + # loop through the section and store any values that were not + # mapped + for key in sect.keys(True): + for i in peer_map: + if i[0] == key: + break + else: + nmapped(section, key, sect[key]) + except LookupError: + pass + + +def convert(sip, filename, non_mappings, include): + """ + Entry point for configuration file conversion. This + function will create a pjsip.conf object and begin to + map specific sections from sip.conf into it. + Returns the new pjsip.conf object once completed + """ + pjsip = astconfigparser.MultiOrderedConfigParser() + non_mappings[filename] = astdicts.MultiOrderedDict() + nmapped = non_mapped(non_mappings[filename]) + if not include: + # Don't duplicate transport and registration configs + map_transports(sip, pjsip, nmapped) + map_registrations(sip, pjsip, nmapped) + map_auth(sip, pjsip, nmapped) + for section in sip.sections(): + if section == 'authentication': + pass + else: + map_peer(sip, section, pjsip, nmapped) + + find_non_mapped(sip.defaults(), nmapped) + find_non_mapped(sip.sections(), nmapped) + + for key, val in sip.includes().iteritems(): + pjsip.add_include(PREFIX + key, convert(val, PREFIX + key, + non_mappings, True)[0]) + return pjsip, non_mappings + + +def write_pjsip(filename, pjsip, non_mappings): + """ + Write pjsip.conf file to disk + """ + try: + with open(filename, 'wt') as fp: + fp.write(';--\n') + fp.write(';;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;\n') + fp.write('Non mapped elements start\n') + fp.write(';;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;\n\n') + astconfigparser.write_dicts(fp, non_mappings[filename]) + fp.write(';;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;\n') + fp.write('Non mapped elements end\n') + fp.write(';;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;\n') + fp.write('--;\n\n') + # write out include file(s) + pjsip.write(fp) + + except IOError: + print "Could not open file ", filename, " for writing" + +############################################################################### + + +def cli_options(): + """ + Parse command line options and apply them. If invalid input is given, + print usage information + """ + global PREFIX + usage = "usage: %prog [options] [input-file [output-file]]\n\n" \ + "input-file defaults to 'sip.conf'\n" \ + "output-file defaults to 'pjsip.conf'" + parser = optparse.OptionParser(usage=usage) + parser.add_option('-p', '--prefix', dest='prefix', default=PREFIX, + help='output prefix for include files') + + options, args = parser.parse_args() + PREFIX = options.prefix + + sip_filename = args[0] if len(args) else 'sip.conf' + pjsip_filename = args[1] if len(args) == 2 else 'pjsip.conf' + + return sip_filename, pjsip_filename + +if __name__ == "__main__": + sip_filename, pjsip_filename = cli_options() + # configuration parser for sip.conf + sip = astconfigparser.MultiOrderedConfigParser() + sip.read(sip_filename) + pjsip, non_mappings = convert(sip, pjsip_filename, dict(), False) + write_pjsip(pjsip_filename, pjsip, non_mappings) diff --git a/contrib/scripts/sip_to_res_sip/sip_to_res_sip.py b/contrib/scripts/sip_to_res_sip/sip_to_res_sip.py deleted file mode 100755 index ca253bdd06..0000000000 --- a/contrib/scripts/sip_to_res_sip/sip_to_res_sip.py +++ /dev/null @@ -1,392 +0,0 @@ -#!/usr/bin/python - -############################################################################### -# TODO: -# (1) There is more work to do here, at least for the sip.conf items that -# aren't currently parsed. An issue will be created for that. -# (2) All of the scripts should probably be passed through pylint and have -# as many PEP8 issues fixed as possible -# (3) A public review is probably warranted at that point of the entire script -############################################################################### - -import optparse -import astdicts -import astconfigparser - -PREFIX = 'res_sip_' - -############################################################################### -### some utility functions -############################################################################### -def section_by_type(section, res_sip, type): - """Finds a section based upon the given type, adding it if not found.""" - try: - return astconfigparser.find_dict( - res_sip.section(section), 'type', type) - except LookupError: - # section for type doesn't exist, so add - sect = res_sip.add_section(section) - sect['type'] = type - return sect - -def set_value(key=None, val=None, section=None, res_sip=None, - nmapped=None, type='endpoint'): - """Sets the key to the value within the section in res_sip.conf""" - def _set_value(k, v, s, r, n): - set_value(key if key else k, v, s, r, n, type) - - # if no value or section return the set_value - # function with the enclosed key and type - if not val and not section: - return _set_value - - # otherwise try to set the value - section_by_type(section, res_sip, type)[key] = \ - val[0] if isinstance(val, list) else val - -def merge_value(key=None, val=None, section=None, res_sip=None, - nmapped=None, type='endpoint', section_to=None): - """Merge values from the given section with those from the default.""" - def _merge_value(k, v, s, r, n): - merge_value(key if key else k, v, s, r, n, type, section_to) - - # if no value or section return the merge_value - # function with the enclosed key and type - if not val and not section: - return _merge_value - - # should return a single value section list - sect = sip.section(section)[0] - # for each merged value add it to res_sip.conf - for i in sect.get_merged(key): - set_value(key, i, section_to if section_to else section, - res_sip, nmapped, type) - -def is_in(s, sub): - """Returns true if 'sub' is in 's'""" - return s.find(sub) != -1 - -def non_mapped(nmapped): - def _non_mapped(section, key, val): - """Writes a non-mapped value from sip.conf to the non-mapped object.""" - if section not in nmapped: - nmapped[section] = astconfigparser.Section() - if isinstance(val, list): - for v in val: - # since coming from sip.conf we can assume - # single section lists - nmapped[section][0][key] = v - else: - nmapped[section][0][key] = val - return _non_mapped - -############################################################################### -### mapping functions - -### define f(key, val, section) where key/val are the key/value pair to -### write to given section in res_sip.conf -############################################################################### - -def set_dtmfmode(key, val, section, res_sip, nmapped): - """Sets the dtmfmode value. If value matches allowable option in res_sip - then map it, otherwise set it to none. - """ - # available res_sip.conf values: frc4733, inband, info, none - if val != 'inband' or val != 'info': - nmapped(section, key, val + " ; did not fully map - set to none") - val = 'none' - set_value(key, val, section, res_sip, nmapped) - -def from_nat(key, val, section, res_sip, nmapped): - """Sets values from nat into the appropriate res_sip.conf options.""" - # nat from sip.conf can be comma separated list of values: - # yes/no, [auto_]force_rport, [auto_]comedia - if is_in(val, 'yes'): - set_value('rtp_symmetric', 'yes', section, res_sip, nmapped) - set_value('rewrite_contact', 'yes', section, res_sip, nmapped) - if is_in(val, 'comedia'): - set_value('rtp_symmetric', 'yes', section, res_sip, nmapped) - if is_in(val, 'force_rport'): - set_value('force_rport', 'yes', section, res_sip, nmapped) - set_value('rewrite_contact', 'yes', section, res_sip, nmapped) - -def set_timers(key, val, section, res_sip, nmapped): - """Sets the timers in res_sip.conf from the session-timers option - found in sip.conf. - """ - # res_sip.conf values can be yes/no, required, always - if val == 'originate': - set_value('timers', 'always', section, res_sip, nmapped) - elif val == 'accept': - set_value('timers', 'required', section, res_sip, nmapped) - elif val == 'never': - set_value('timers', 'no', section, res_sip, nmapped) - else: - set_value('timers', 'yes', section, res_sip, nmapped) - -def set_direct_media(key, val, section, res_sip, nmapped): - """Maps values from the sip.conf comma separated direct_media option - into res_sip.conf direct_media options. - """ - if is_in(val, 'yes'): - set_value('direct_media', 'yes', section, res_sip, nmapped) - if is_in(val, 'update'): - set_value('direct_media_method', 'update', section, res_sip, nmapped) - if is_in(val, 'outgoing'): - set_value('directed_media_glare_mitigation', 'outgoing', section, res_sip, nmapped) - if is_in(val, 'nonat'): - set_value('disable_directed_media_on_nat','yes', section, res_sip, nmapped) - if (val == 'no'): - set_value('direct_media', 'no', section, res_sip, nmapped) - -def from_sendrpid(key, val, section, res_sip, nmapped): - """Sets the send_rpid/pai values in res_sip.conf.""" - if val == 'yes' or val == 'rpid': - set_value('send_rpid', 'yes', section, res_sip, nmapped) - elif val == 'pai': - set_value('send_pai', 'yes', section, res_sip, nmapped) - -def set_media_encryption(key, val, section, res_sip, nmapped): - """Sets the media_encryption value in res_sip.conf""" - if val == 'yes': - set_value('media_encryption', 'sdes', section, res_sip, nmapped) - -def from_recordfeature(key, val, section, res_sip, nmapped): - """If record on/off feature is set to automixmon then set - one_touch_recording, otherwise it can't be mapped. - """ - if val == 'automixmon': - set_value('one_touch_recording', 'yes', section, res_sip, nmapped) - else: - nmapped(section, key, val + " ; could not be fully mapped") - -def from_progressinband(key, val, section, res_sip, nmapped): - """Sets the inband_progress value in res_sip.conf""" - # progressinband can = yes/no/never - if val == 'never': - val = 'no' - set_value('inband_progress', val, section, res_sip, nmapped) - -def from_host(key, val, section, res_sip, nmapped): - """Sets contact info in an AOR section in in res_sip.conf using 'host' - data from sip.conf - """ - # all aors have the same name as the endpoint so makes - # it easy to endpoint's 'aors' value - set_value('aors', section, section, res_sip, nmapped) - if val != 'dynamic': - set_value('contact', val, section, res_sip, nmapped, 'aor') - else: - set_value('max_contacts', 1, section, res_sip, nmapped, 'aor') - -def from_subscribemwi(key, val, section, res_sip, nmapped): - """Checks the subscribemwi value in sip.conf. If yes places the - mailbox value in mailboxes within the endpoint, otherwise puts - it in the aor. - """ - mailboxes = sip.get('mailbox', section, res_sip) - type = 'endpoint' if val == 'yes' else 'aor' - set_value('mailboxes', mailboxes, section, res_sip, nmapped, type) - -############################################################################### - -# options in res_sip.conf on an endpoint that have no sip.conf equivalent: -# type, rtp_ipv6, 100rel, trust_id_outbound, aggregate_mwi, -# connected_line_method - -# known sip.conf peer keys that can be mapped to a res_sip.conf section/key -peer_map = [ - # sip.conf option mapping function res_sip.conf option(s) - ########################################################################### - ['context', set_value], - ['dtmfmode', set_dtmfmode], - ['disallow', merge_value], - ['allow', merge_value], - ['nat', from_nat], # rtp_symmetric, force_rport, - # rewrite_contact - ['icesupport', set_value('ice_support')], - ['autoframing', set_value('use_ptime')], - ['outboundproxy', set_value('outbound_proxy')], - ['mohsuggest', set_value], - ['session-timers', set_timers], # timers - ['session-minse', set_value('timers_min_se')], - ['session-expires', set_value('timers_sess_expires')], - ['externip', set_value('external_media_address')], - ['externhost', set_value('external_media_address')], - # identify_by ? - ['directmedia', set_direct_media], # direct_media - # direct_media_method - # directed_media_glare_mitigation - # disable_directed_media_on_nat - ['callerid', set_value], # callerid - ['callingpres', set_value('callerid_privacy')], - ['cid_tag', set_value('callerid_tag')], - ['trustpid', set_value('trust_id_inbound')], - ['sendrpid', from_sendrpid], # send_pai, send_rpid - ['send_diversion', set_value], - ['encrpytion', set_media_encryption], - ['use_avpf', set_value], - ['recordonfeature', from_recordfeature], # automixon - ['recordofffeature', from_recordfeature], # automixon - ['progressinband', from_progressinband], # in_band_progress - ['callgroup', set_value], - ['pickupgroup', set_value], - ['namedcallgroup', set_value], - ['namedpickupgroup', set_value], - ['busylevel', set_value('devicestate_busy_at')], - -############################ maps to an aor ################################### - - ['host', from_host], # contact, max_contacts - ['subscribemwi', from_subscribemwi], # mailboxes - ['qualifyfreq', set_value('qualify_frequency', type='aor')], - -############################# maps to auth##################################### -# type = auth -# username -# password -# md5_cred -# realm -# nonce_lifetime -# auth_type -######################### maps to acl/security ################################ - - ['permit', merge_value(type='security', section_to='acl')], - ['deny', merge_value(type='security', section_to='acl')], - ['acl', merge_value(type='security', section_to='acl')], - ['contactpermit', merge_value(type='security', section_to='acl')], - ['contactdeny', merge_value(type='security', section_to='acl')], - ['contactacl', merge_value(type='security', section_to='acl')], - -########################### maps to transport ################################# -# type = transport -# protocol -# bind -# async_operations -# ca_list_file -# cert_file -# privkey_file -# password -# external_signaling_address - externip & externhost -# external_signaling_port -# external_media_address -# domain -# verify_server -# verify_client -# require_client_cert -# method -# cipher -# localnet -######################### maps to domain_alias ################################ -# type = domain_alias -# domain -######################### maps to registration ################################ -# type = registration -# server_uri -# client_uri -# contact_user -# transport -# outbound_proxy -# expiration -# retry_interval -# max_retries -# auth_rejection_permanent -# outbound_auth -########################### maps to identify ################################## -# type = identify -# endpoint -# match -] - -def map_peer(sip, section, res_sip, nmapped): - for i in peer_map: - try: - # coming from sip.conf the values should mostly be a list with a - # single value. In the few cases that they are not a specialized - # function (see merge_value) is used to retrieve the values. - i[1](i[0], sip.get(section, i[0])[0], section, res_sip, nmapped) - except LookupError: - pass # key not found in sip.conf - -def find_non_mapped(sections, nmapped): - for section, sect in sections.iteritems(): - try: - # since we are pulling from sip.conf this should always - # be a single value list - sect = sect[0] - # loop through the section and store any values that were not mapped - for key in sect.keys(True): - for i in peer_map: - if i[0] == key: - break; - else: - nmapped(section, key, sect[key]) - except LookupError: - pass - -def convert(sip, filename, non_mappings): - res_sip = astconfigparser.MultiOrderedConfigParser() - non_mappings[filename] = astdicts.MultiOrderedDict() - nmapped = non_mapped(non_mappings[filename]) - for section in sip.sections(): - if section == 'authentication': - pass - else: - map_peer(sip, section, res_sip, nmapped) - - find_non_mapped(sip.defaults(), nmapped) - find_non_mapped(sip.sections(), nmapped) - - for key, val in sip.includes().iteritems(): - res_sip.add_include(PREFIX + key, convert(val, PREFIX + key, non_mappings)[0]) - return res_sip, non_mappings - -def write_res_sip(filename, res_sip, non_mappings): - try: - with open(filename, 'wt') as fp: - fp.write(';--\n') - fp.write(';;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;\n') - fp.write('Non mapped elements start\n') - fp.write(';;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;\n\n') - astconfigparser.write_dicts(fp, non_mappings[filename]) - fp.write(';;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;\n') - fp.write('Non mapped elements end\n') - fp.write(';;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;\n') - fp.write('--;\n\n') - # write out include file(s) - for key, val in res_sip.includes().iteritems(): - write_res_sip(key, val, non_mappings) - fp.write('#include "%s"\n' % key) - fp.write('\n') - # write out mapped data elements - astconfigparser.write_dicts(fp, res_sip.defaults()) - astconfigparser.write_dicts(fp, res_sip.sections()) - - except IOError: - print "Could not open file ", filename, " for writing" - -############################################################################### - -def cli_options(): - global PREFIX - usage = "usage: %prog [options] [input-file [output-file]]\n\n" \ - "input-file defaults to 'sip.conf'\n" \ - "output-file defaults to 'res_sip.conf'" - parser = optparse.OptionParser(usage=usage) - parser.add_option('-p', '--prefix', dest='prefix', default=PREFIX, - help='output prefix for include files') - - options, args = parser.parse_args() - PREFIX = options.prefix - - sip_filename = args[0] if len(args) else 'sip.conf' - res_sip_filename = args[1] if len(args) == 2 else 'res_sip.conf' - - return sip_filename, res_sip_filename - -if __name__ == "__main__": - sip_filename, res_sip_filename = cli_options() - # configuration parser for sip.conf - sip = astconfigparser.MultiOrderedConfigParser() - sip.read(sip_filename) - res_sip, non_mappings = convert(sip, res_sip_filename, dict()) - write_res_sip(res_sip_filename, res_sip, non_mappings)