summaryrefslogtreecommitdiff
path: root/contrib/scripts/sip_to_pjsip
diff options
context:
space:
mode:
authorMark Michelson <mmichelson@digium.com>2013-10-31 22:09:47 +0000
committerMark Michelson <mmichelson@digium.com>2013-10-31 22:09:47 +0000
commitdd221c74c53113b0109f1ca1b392d43dd768cef5 (patch)
tree2f59a5e44d9f828c52c21d09183bb23bb60a56b9 /contrib/scripts/sip_to_pjsip
parente9fc32105353a65b89f546008ca98ffadf359704 (diff)
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
Diffstat (limited to 'contrib/scripts/sip_to_pjsip')
-rw-r--r--contrib/scripts/sip_to_pjsip/astconfigparser.py467
-rw-r--r--contrib/scripts/sip_to_pjsip/astdicts.py298
-rwxr-xr-xcontrib/scripts/sip_to_pjsip/sip_to_pjsip.py1151
3 files changed, 1916 insertions, 0 deletions
diff --git a/contrib/scripts/sip_to_pjsip/astconfigparser.py b/contrib/scripts/sip_to_pjsip/astconfigparser.py
new file mode 100644
index 000000000..c93173dee
--- /dev/null
+++ b/contrib/scripts/sip_to_pjsip/astconfigparser.py
@@ -0,0 +1,467 @@
+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
+ vals0 = left[key] if key in left else []
+ vals1 = right[key] if key in right else []
+
+ return vals0 + [i for i in vals1 if i not in vals0]
+
+###############################################################################
+
+
+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.
+ """
+ count = 0
+
+ def __init__(self, defaults=None, templates=None):
+ MultiOrderedDict.__init__(self)
+ # track an ordered id of sections
+ Section.count += 1
+ self.id = Section.count
+ self._defaults = [] if defaults is None else defaults
+ 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):
+ """
+ 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)
+
+ if from_templates:
+ if self in self._templates:
+ return []
+ for t in self._templates:
+ try:
+ # fail if not found on the search - doing it this way
+ # allows template's templates to be searched.
+ return t.get(key, True, from_templates, from_defaults)
+ except KeyError:
+ pass
+
+ if from_defaults:
+ for d in self._defaults:
+ try:
+ return d.get(key, True, from_templates, from_defaults)
+ except KeyError:
+ pass
+
+ 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.
+ """
+ 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
+
+ for d in self._templates:
+ for key in d.keys():
+ if key not in res:
+ res.append(key)
+
+ for d in self._defaults:
+ for key in d.keys():
+ if key not in res:
+ res.append(key)
+ 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):
+ """
+ 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)"""
+ # first merge key/values from defaults together
+ merged = []
+ for i in reversed(self._defaults):
+ if not merged:
+ merged = i
+ continue
+ merged = merge_values(merged, i, key)
+
+ for i in reversed(self._templates):
+ if not merged:
+ merged = i
+ continue
+ merged = merge_values(merged, i, key)
+
+ # then merge self in
+ return merge_values(merged, self, key)
+
+###############################################################################
+
+COMMENT = ';'
+COMMENT_START = ';--'
+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 is_comment:
+ part = line.partition(COMMENT_END)
+ if part[1]:
+ # found multi-line comment end check string after it
+ return remove_comment(part[2], False)
+ return "", True
+
+ part = line.partition(COMMENT_START)
+ if part[1]:
+ # found multi-line comment start check string before
+ # it to make sure there wasn't an eol comment in it
+ has_comment = part[0].partition(COMMENT)
+ if has_comment[1]:
+ # eol comment found return anything before it
+ return has_comment[0], False
+
+ # check string after it to see if the comment ends
+ line, is_comment = remove_comment(part[2], True)
+ if is_comment:
+ # return possible string data before comment
+ return part[0].strip(), True
+
+ # otherwise it was an embedded comment so combine
+ return ''.join([part[0].strip(), ' ', line]).rstrip(), False
+
+ # 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.
+ """
+
+ match = re.match('^#include\s*[<"]?(.*)[>"]?$', line)
+ return match.group(1) if match else None
+
+
+def try_section(line):
+ """
+ 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('['):
+ return None, False, []
+
+ section, delim, templates = line.partition(']')
+ if not templates:
+ return section[1:], False, []
+
+ # strip out the parens and parse into an array
+ templates = templates.replace('(', "").replace(')', "").split(',')
+ # go ahead and remove extra whitespace
+ templates = [i.strip() for i in templates]
+ try:
+ templates.remove('!')
+ return section[1:], True, templates
+ 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)
+ # should split in two (key/val), but either way use first two elements
+ return data[0].rstrip(), data[1].lstrip()
+
+###############################################################################
+
+
+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))
+
+
+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:
+ 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)
+ config_file.write("%s\n" % (key_val))
+ config_file.write("\n")
+
+###############################################################################
+
+
+class MultiOrderedConfigParser:
+ def __init__(self, parent=None):
+ self._parent = parent
+ self._defaults = MultiOrderedDict()
+ 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 self.get_defaults(key)
+
+ def add_default(self, key, template_keys=None):
+ """
+ Adds a default section to defaults, returning the
+ default Section object.
+ """
+ if template_keys is None:
+ template_keys = []
+ return self.add_section(key, template_keys, self._defaults)
+
+ def sections(self):
+ return self._sections
+
+ def section(self, key):
+ """Retrieves a list of dictionaries for a section."""
+ 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(self.get_defaults(t))
+ res.add_defaults(self.get_defaults(DEFAULTSECT))
+ mdicts.insert(0, key, res)
+ return res
+
+ def includes(self):
+ 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
+
+ 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 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 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)
+ # TODO - set in both sections and defaults?
+ if section in self._sections:
+ self.section(section)[0][key] = val
+ else:
+ self.defaults(section)[0][key] = val
+
+ def read(self, filename):
+ """Parse configuration information from a file"""
+ try:
+ with open(filename, 'rt') as config_file:
+ self._read(config_file)
+ except IOError:
+ print "Could not open file ", filename, " for reading"
+
+ 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
+ continue
+
+ include_name = try_include(line)
+ if include_name:
+ parser = self.add_include(include_name)
+ parser.read(include_name)
+ continue
+
+ section, is_template, templates = try_section(line)
+ if section:
+ if section == DEFAULTSECT or is_template:
+ sect = self.add_default(section, templates)
+ else:
+ sect = self.add_section(section, templates)
+ continue
+
+ key, val = try_option(line)
+ sect[key] = val
+
+ def write(self, config_file):
+ """Write configuration information out to a file"""
+ try:
+ for key, val in self._includes.iteritems():
+ val.write(key)
+ config_file.write('#include "%s"\n' % key)
+
+ config_file.write('\n')
+ write_dicts(config_file, self._defaults)
+ write_dicts(config_file, self._sections)
+ except:
+ try:
+ with open(config_file, 'wt') as fp:
+ self.write(fp)
+ except IOError:
+ print "Could not open file ", config_file, " for writing"
diff --git a/contrib/scripts/sip_to_pjsip/astdicts.py b/contrib/scripts/sip_to_pjsip/astdicts.py
new file mode 100644
index 000000000..ae630755d
--- /dev/null
+++ b/contrib/scripts/sip_to_pjsip/astdicts.py
@@ -0,0 +1,298 @@
+# Backport of OrderedDict() class that runs on Python 2.4, 2.5, 2.6, 2.7 and pypy.
+# Passes Python2.7's test suite and incorporates all the latest updates.
+# copied from http://code.activestate.com/recipes/576693/
+
+try:
+ from thread import get_ident as _get_ident
+except ImportError:
+ from dummy_thread import get_ident as _get_ident
+
+try:
+ from _abcoll import KeysView, ValuesView, ItemsView
+except ImportError:
+ pass
+
+
+class OrderedDict(dict):
+ 'Dictionary that remembers insertion order'
+ # An inherited dict maps keys to values.
+ # The inherited dict provides __getitem__, __len__, __contains__, and get.
+ # The remaining methods are order-aware.
+ # Big-O running times for all methods are the same as for regular dictionaries.
+
+ # The internal self.__map dictionary maps keys to links in a doubly linked list.
+ # The circular doubly linked list starts and ends with a sentinel element.
+ # The sentinel element never gets deleted (this simplifies the algorithm).
+ # Each link is stored as a list of length three: [PREV, NEXT, KEY].
+
+ def __init__(self, *args, **kwds):
+ '''Initialize an ordered dictionary. Signature is the same as for
+ regular dictionaries, but keyword arguments are not recommended
+ because their insertion order is arbitrary.
+
+ '''
+ if len(args) > 1:
+ raise TypeError('expected at most 1 arguments, got %d' % len(args))
+ try:
+ self.__root
+ except AttributeError:
+ self.__root = root = [] # sentinel node
+ root[:] = [root, root, None]
+ self.__map = {}
+ self.__update(*args, **kwds)
+
+ def __setitem__(self, key, value, dict_setitem=dict.__setitem__):
+ 'od.__setitem__(i, y) <==> od[i]=y'
+ # Setting a new item creates a new link which goes at the end of the linked
+ # list, and the inherited dictionary is updated with the new key/value pair.
+ if key not in self:
+ root = self.__root
+ last = root[0]
+ last[1] = root[0] = self.__map[key] = [last, root, key]
+ dict_setitem(self, key, value)
+
+ def __delitem__(self, key, dict_delitem=dict.__delitem__):
+ 'od.__delitem__(y) <==> del od[y]'
+ # Deleting an existing item uses self.__map to find the link which is
+ # then removed by updating the links in the predecessor and successor nodes.
+ dict_delitem(self, key)
+ link_prev, link_next, key = self.__map.pop(key)
+ link_prev[1] = link_next
+ link_next[0] = link_prev
+
+ def __iter__(self):
+ 'od.__iter__() <==> iter(od)'
+ root = self.__root
+ curr = root[1]
+ while curr is not root:
+ yield curr[2]
+ curr = curr[1]
+
+ def __reversed__(self):
+ 'od.__reversed__() <==> reversed(od)'
+ root = self.__root
+ curr = root[0]
+ while curr is not root:
+ yield curr[2]
+ curr = curr[0]
+
+ def clear(self):
+ 'od.clear() -> None. Remove all items from od.'
+ try:
+ for node in self.__map.itervalues():
+ del node[:]
+ root = self.__root
+ root[:] = [root, root, None]
+ self.__map.clear()
+ except AttributeError:
+ pass
+ dict.clear(self)
+
+ def popitem(self, last=True):
+ '''od.popitem() -> (k, v), return and remove a (key, value) pair.
+ Pairs are returned in LIFO order if last is true or FIFO order if false.
+
+ '''
+ if not self:
+ raise KeyError('dictionary is empty')
+ root = self.__root
+ if last:
+ link = root[0]
+ link_prev = link[0]
+ link_prev[1] = root
+ root[0] = link_prev
+ else:
+ link = root[1]
+ link_next = link[1]
+ root[1] = link_next
+ link_next[0] = root
+ key = link[2]
+ del self.__map[key]
+ value = dict.pop(self, key)
+ return key, value
+
+ # -- the following methods do not depend on the internal structure --
+
+ def keys(self):
+ 'od.keys() -> list of keys in od'
+ return list(self)
+
+ def values(self):
+ 'od.values() -> list of values in od'
+ return [self[key] for key in self]
+
+ def items(self):
+ 'od.items() -> list of (key, value) pairs in od'
+ return [(key, self[key]) for key in self]
+
+ def iterkeys(self):
+ 'od.iterkeys() -> an iterator over the keys in od'
+ return iter(self)
+
+ def itervalues(self):
+ 'od.itervalues -> an iterator over the values in od'
+ for k in self:
+ yield self[k]
+
+ def iteritems(self):
+ 'od.iteritems -> an iterator over the (key, value) items in od'
+ for k in self:
+ yield (k, self[k])
+
+ def update(*args, **kwds):
+ '''od.update(E, **F) -> None. Update od from dict/iterable E and F.
+
+ If E is a dict instance, does: for k in E: od[k] = E[k]
+ If E has a .keys() method, does: for k in E.keys(): od[k] = E[k]
+ Or if E is an iterable of items, does: for k, v in E: od[k] = v
+ In either case, this is followed by: for k, v in F.items(): od[k] = v
+
+ '''
+ if len(args) > 2:
+ raise TypeError('update() takes at most 2 positional '
+ 'arguments (%d given)' % (len(args),))
+ elif not args:
+ raise TypeError('update() takes at least 1 argument (0 given)')
+ self = args[0]
+ # Make progressively weaker assumptions about "other"
+ other = ()
+ if len(args) == 2:
+ other = args[1]
+ if isinstance(other, dict):
+ for key in other:
+ self[key] = other[key]
+ elif hasattr(other, 'keys'):
+ for key in other.keys():
+ self[key] = other[key]
+ else:
+ for key, value in other:
+ self[key] = value
+ for key, value in kwds.items():
+ self[key] = value
+
+ __update = update # let subclasses override update without breaking __init__
+
+ __marker = object()
+
+ def pop(self, key, default=__marker):
+ '''od.pop(k[,d]) -> v, remove specified key and return the corresponding value.
+ If key is not found, d is returned if given, otherwise KeyError is raised.
+
+ '''
+ if key in self:
+ result = self[key]
+ del self[key]
+ return result
+ if default is self.__marker:
+ raise KeyError(key)
+ return default
+
+ def setdefault(self, key, default=None):
+ 'od.setdefault(k[,d]) -> od.get(k,d), also set od[k]=d if k not in od'
+ if key in self:
+ return self[key]
+ self[key] = default
+ return default
+
+ def __repr__(self, _repr_running={}):
+ 'od.__repr__() <==> repr(od)'
+ call_key = id(self), _get_ident()
+ if call_key in _repr_running:
+ return '...'
+ _repr_running[call_key] = 1
+ try:
+ if not self:
+ return '%s()' % (self.__class__.__name__,)
+ return '%s(%r)' % (self.__class__.__name__, self.items())
+ finally:
+ del _repr_running[call_key]
+
+ def __reduce__(self):
+ 'Return state information for pickling'
+ items = [[k, self[k]] for k in self]
+ inst_dict = vars(self).copy()
+ for k in vars(OrderedDict()):
+ inst_dict.pop(k, None)
+ if inst_dict:
+ return (self.__class__, (items,), inst_dict)
+ return self.__class__, (items,)
+
+ def copy(self):
+ 'od.copy() -> a shallow copy of od'
+ return self.__class__(self)
+
+ @classmethod
+ def fromkeys(cls, iterable, value=None):
+ '''OD.fromkeys(S[, v]) -> New ordered dictionary with keys from S
+ and values equal to v (which defaults to None).
+
+ '''
+ d = cls()
+ for key in iterable:
+ d[key] = value
+ return d
+
+ def __eq__(self, other):
+ '''od.__eq__(y) <==> od==y. Comparison to another OD is order-sensitive
+ while comparison to a regular mapping is order-insensitive.
+
+ '''
+ if isinstance(other, OrderedDict):
+ return len(self)==len(other) and self.items() == other.items()
+ return dict.__eq__(self, other)
+
+ def __ne__(self, other):
+ return not self == other
+
+ # -- the following methods are only used in Python 2.7 --
+
+ def viewkeys(self):
+ "od.viewkeys() -> a set-like object providing a view on od's keys"
+ return KeysView(self)
+
+ def viewvalues(self):
+ "od.viewvalues() -> an object providing a view on od's values"
+ return ValuesView(self)
+
+ def viewitems(self):
+ "od.viewitems() -> a set-like object providing a view on od's items"
+ return ItemsView(self)
+
+###############################################################################
+### MultiOrderedDict
+###############################################################################
+class MultiOrderedDict(OrderedDict):
+ def __init__(self, *args, **kwds):
+ OrderedDict.__init__(self, *args, **kwds)
+
+ def __setitem__(self, key, val, i=None):
+ if key not in self:
+# print "__setitem__ key = ", key, " val = ", val
+ OrderedDict.__setitem__(
+ self, key, val if isinstance(val, list) else [val])
+ return
+# print "inserting key = ", key, " val = ", val
+ vals = self[key]
+ if i is None:
+ i = len(vals)
+
+ if not isinstance(val, list):
+ if val not in vals:
+ vals.insert(i, val)
+ else:
+ for j in val.reverse():
+ if j not in vals:
+ vals.insert(i, j)
+
+
+ def insert(self, i, key, val):
+ self.__setitem__(key, val, i)
+
+ def copy(self):
+ # TODO - find out why for some reason copies
+ # the [] as an [[]], so do manually
+ c = MultiOrderedDict() #self.__class__(self)
+ for key, val in self.iteritems():
+ for v in val:
+ c[key] = v
+ return c
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 000000000..96a9a02ad
--- /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)