d05f97d5fbf5d68f33a4e4e0324335ca46dfc52f
[asterisk/asterisk.git] / contrib / scripts / sip_to_pjsip / sip_to_pjsip.py
1 #!/usr/bin/env python
2
3 import optparse
4 import socket
5 try:
6     from urllib.parse import urlparse
7 except ImportError:
8     from urlparse import urlparse # Python 2.7 required for Literal IPv6 Addresses
9 import astdicts
10 import astconfigparser
11
12 PREFIX = 'pjsip_'
13
14 ###############################################################################
15 ### some utility functions
16 ###############################################################################
17
18
19 def section_by_type(section, pjsip, type):
20     """Finds a section based upon the given type, adding it if not found."""
21     def __find_dict(mdicts, key, val):
22         """Given a list of multi-dicts, return the multi-dict that contains
23            the given key/value pair."""
24
25         def found(d):
26             return key in d and val in d[key]
27
28         try:
29             return [d for d in mdicts if found(d)][0]
30         except IndexError:
31             raise LookupError("Dictionary not located for key = %s, value = %s"
32                               % (key, val))
33
34     try:
35         return __find_dict(pjsip.section(section), 'type', type)
36     except LookupError:
37         # section for type doesn't exist, so add
38         sect = pjsip.add_section(section)
39         sect['type'] = type
40         return sect
41
42
43 def set_value(key=None, val=None, section=None, pjsip=None,
44               nmapped=None, type='endpoint'):
45     """Sets the key to the value within the section in pjsip.conf"""
46     def _set_value(k, v, s, r, n):
47         set_value(key if key else k, v, s, r, n, type)
48
49     # if no value or section return the set_value
50     # function with the enclosed key and type
51     if not val and not section:
52         return _set_value
53
54     # otherwise try to set the value
55     section_by_type(section, pjsip, type)[key] = \
56         val[0] if isinstance(val, list) else val
57
58
59 def merge_value(key=None, val=None, section=None, pjsip=None,
60                 nmapped=None, type='endpoint', section_to=None,
61                 key_to=None):
62     """Merge values from the given section with those from the default."""
63     def _merge_value(k, v, s, r, n):
64         merge_value(key if key else k, v, s, r, n, type, section_to, key_to)
65
66     # if no value or section return the merge_value
67     # function with the enclosed key and type
68     if not val and not section:
69         return _merge_value
70
71     # should return a single value section list
72     try:
73         sect = sip.section(section)[0]
74     except LookupError:
75         sect = sip.default(section)[0]
76     # for each merged value add it to pjsip.conf
77     for i in sect.get_merged(key):
78         set_value(key_to if key_to else key, i,
79                   section_to if section_to else section,
80                   pjsip, nmapped, type)
81
82 def merge_codec_value(key=None, val=None, section=None, pjsip=None,
83                 nmapped=None, type='endpoint', section_to=None,
84                 key_to=None):
85     """Merge values from allow/deny with those from the default. Special treatment for all"""
86     def _merge_codec_value(k, v, s, r, n):
87         merge_codec_value(key if key else k, v, s, r, n, type, section_to, key_to)
88
89     # if no value or section return the merge_codec_value
90     # function with the enclosed key and type
91     if not val and not section:
92         return _merge_codec_value
93
94     if key == 'allow':
95         try:
96             disallow = sip.get(section, 'disallow')[0]
97             if disallow == 'all':
98                 #don't inherit
99                 for i in sip.get(section, 'allow'):
100                     set_value(key, i, section, pjsip, nmapped, type)
101             else:
102                 merge_value(key, val, section, pjsip, nmapped, type, section_to, key_to)
103         except LookupError:
104             print("lookup error")
105             merge_value(key, val, section, pjsip, nmapped, type, section_to, key_to)
106             return
107     elif key == 'disallow':
108         try:
109             allow = sip.get(section, 'allow')[0]
110             if allow == 'all':
111                 #don't inherit
112                 for i in sip.get(section, 'disallow'):
113                     set_value(key, i, section, pjsip, nmapped, type)
114             else:
115                 merge_value(key, val, section, pjsip, nmapped, type, section_to, key_to)
116         except LookupError:
117             merge_value(key, val, section, pjsip, nmapped, type, section_to, key_to)
118             return
119     else:
120         merge_value(key, val, section, pjsip, nmapped, type, section_to, key_to)
121
122
123 def non_mapped(nmapped):
124     """Write non-mapped sip.conf values to the non-mapped object"""
125     def _non_mapped(section, key, val):
126         """Writes a non-mapped value from sip.conf to the non-mapped object."""
127         if section not in nmapped:
128             nmapped[section] = astconfigparser.Section()
129             if isinstance(val, list):
130                 for v in val:
131                     # since coming from sip.conf we can assume
132                     # single section lists
133                     nmapped[section][0][key] = v
134             else:
135                 nmapped[section][0][key] = val
136     return _non_mapped
137
138 ###############################################################################
139 ### mapping functions -
140 ###      define f(key, val, section) where key/val are the key/value pair to
141 ###      write to given section in pjsip.conf
142 ###############################################################################
143
144
145 def set_dtmfmode(key, val, section, pjsip, nmapped):
146     """
147     Sets the dtmfmode value.  If value matches allowable option in pjsip
148     then map it, otherwise set it to none.
149     """
150     key = 'dtmf_mode'
151     # available pjsip.conf values: rfc4733, inband, info, none
152     if val == 'inband' or val == 'info':
153         set_value(key, val, section, pjsip, nmapped)
154     elif val == 'rfc2833':
155         set_value(key, 'rfc4733', section, pjsip, nmapped)
156     else:
157         nmapped(section, key, val + " ; did not fully map - set to none")
158         set_value(key, 'none', section, pjsip, nmapped)
159
160
161 def setup_udptl(section, pjsip, nmapped):
162     """Sets values from udptl into the appropriate pjsip.conf options."""
163     try:
164         val = sip.get(section, 't38pt_udptl')[0]
165     except LookupError:
166         try:
167             val = sip.get('general', 't38pt_udptl')[0]
168         except LookupError:
169             return
170
171     ec = 'none'
172     if 'yes' in val:
173         set_value('t38_udptl', 'yes', section, pjsip, nmapped)
174     if 'no' in val:
175         set_value('t38_udptl', 'no', section, pjsip, nmapped)
176     if 'redundancy' in val:
177         ec = 'redundancy'
178     if 'fec' in val:
179         ec = 'fec'
180     set_value('t38_udptl_ec', ec, section, pjsip, nmapped)
181
182 def from_nat(key, val, section, pjsip, nmapped):
183     """Sets values from nat into the appropriate pjsip.conf options."""
184     # nat from sip.conf can be comma separated list of values:
185     # yes/no, [auto_]force_rport, [auto_]comedia
186     if 'yes' in val:
187         set_value('rtp_symmetric', 'yes', section, pjsip, nmapped)
188         set_value('rewrite_contact', 'yes', section, pjsip, nmapped)
189     if 'comedia' in val:
190         set_value('rtp_symmetric', 'yes', section, pjsip, nmapped)
191     if 'force_rport' in val:
192         set_value('force_rport', 'yes', section, pjsip, nmapped)
193         set_value('rewrite_contact', 'yes', section, pjsip, nmapped)
194
195
196 def set_timers(key, val, section, pjsip, nmapped):
197     """
198     Sets the timers in pjsip.conf from the session-timers option
199     found in sip.conf.
200     """
201     # pjsip.conf values can be yes/no, required, always
202     # 'required' is a new feature of chan_pjsip, which rejects
203     #            all SIP clients not supporting Session Timers
204     # 'Accept' is the default value of chan_sip and maps to 'yes'
205     # chan_sip ignores the case, for example 'session-timers=Refuse'
206     val = val.lower()
207     if val == 'originate':
208         set_value('timers', 'always', section, pjsip, nmapped)
209     elif val == 'refuse':
210         set_value('timers', 'no', section, pjsip, nmapped)
211     else:
212         set_value('timers', 'yes', section, pjsip, nmapped)
213
214
215 def set_direct_media(key, val, section, pjsip, nmapped):
216     """
217     Maps values from the sip.conf comma separated direct_media option
218     into pjsip.conf direct_media options.
219     """
220     if 'yes' in val:
221         set_value('direct_media', 'yes', section, pjsip, nmapped)
222     if 'update' in val:
223         set_value('direct_media_method', 'update', section, pjsip, nmapped)
224     if 'outgoing' in val:
225         set_value('directed_media_glare_mitigation', 'outgoing', section,
226                   pjsip, nmapped)
227     if 'nonat' in val:
228         set_value('disable_directed_media_on_nat', 'yes', section, pjsip,
229                   nmapped)
230     if 'no' in val:
231         set_value('direct_media', 'no', section, pjsip, nmapped)
232
233
234 def from_sendrpid(key, val, section, pjsip, nmapped):
235     """Sets the send_rpid/pai values in pjsip.conf."""
236     if val == 'yes' or val == 'rpid':
237         set_value('send_rpid', 'yes', section, pjsip, nmapped)
238     elif val == 'pai':
239         set_value('send_pai', 'yes', section, pjsip, nmapped)
240
241
242 def set_media_encryption(key, val, section, pjsip, nmapped):
243     """Sets the media_encryption value in pjsip.conf"""
244     try:
245         dtls = sip.get(section, 'dtlsenable')[0]
246         if dtls == 'yes':
247             # If DTLS is enabled, then that overrides SDES encryption.
248             return
249     except LookupError:
250         pass
251
252     if val == 'yes':
253         set_value('media_encryption', 'sdes', section, pjsip, nmapped)
254
255
256 def from_recordfeature(key, val, section, pjsip, nmapped):
257     """
258     If record on/off feature is set to automixmon then set
259     one_touch_recording, otherwise it can't be mapped.
260     """
261     set_value('one_touch_recording', 'yes', section, pjsip, nmapped)
262     set_value(key, val, section, pjsip, nmapped)
263
264 def set_record_on_feature(key, val, section, pjsip, nmapped):
265     """Sets the record_on_feature in pjsip.conf"""
266     from_recordfeature('record_on_feature', val, section, pjsip, nmapped)
267
268 def set_record_off_feature(key, val, section, pjsip, nmapped):
269     """Sets the record_off_feature in pjsip.conf"""
270     from_recordfeature('record_off_feature', val, section, pjsip, nmapped)
271
272 def from_progressinband(key, val, section, pjsip, nmapped):
273     """Sets the inband_progress value in pjsip.conf"""
274     # progressinband can = yes/no/never
275     if val == 'never':
276         val = 'no'
277     set_value('inband_progress', val, section, pjsip, nmapped)
278
279
280 def build_host(config, host, section='general', port_key=None):
281     """
282     Returns a string composed of a host:port. This assumes that the host
283     may have a port as part of the initial value. The port_key overrides
284     a port in host, see parameter 'bindport' in chan_sip.
285     """
286     try:
287         socket.inet_pton(socket.AF_INET6, host)
288         if not host.startswith('['):
289             # SIP URI will need brackets.
290             host = '[' + host + ']'
291     except socket.error:
292         pass
293
294     # Literal IPv6 (like [::]), IPv4, or hostname
295     # does not work for IPv6 without brackets; case catched above
296     url = urlparse('sip://' + host)
297
298     if port_key:
299         try:
300             port = config.get(section, port_key)[0]
301             host = url.hostname # no port, but perhaps no brackets
302             try:
303                 socket.inet_pton(socket.AF_INET6, host)
304                 if not host.startswith('['):
305                     # SIP URI will need brackets.
306                     host = '[' + host + ']'
307             except socket.error:
308                 pass
309             return host + ':' + port
310         except LookupError:
311             pass
312
313     # Returns host:port, in brackets if required
314     # TODO Does not compress IPv6, for example 0:0:0:0:0:0:0:0 should get [::]
315     return url.netloc
316
317
318 def from_host(key, val, section, pjsip, nmapped):
319     """
320     Sets contact info in an AOR section in pjsip.conf using 'host'
321     and 'port' data from sip.conf
322     """
323     # all aors have the same name as the endpoint so makes
324     # it easy to set endpoint's 'aors' value
325     set_value('aors', section, section, pjsip, nmapped)
326     if val == 'dynamic':
327         # Easy case. Just set the max_contacts on the aor and we're done
328         set_value('max_contacts', 1, section, pjsip, nmapped, 'aor')
329         return
330
331     result = 'sip:'
332
333     # More difficult case. The host will be either a hostname or
334     # IP address and may or may not have a port specified. pjsip.conf
335     # expects the contact to be a SIP URI.
336
337     user = None
338
339     try:
340         user = sip.multi_get(section, ['defaultuser', 'username'])[0]
341         result += user + '@'
342     except LookupError:
343         # It's fine if there's no user name
344         pass
345
346     result += build_host(sip, val, section, 'port')
347
348     set_value('contact', result, section, pjsip, nmapped, 'aor')
349
350
351 def from_mailbox(key, val, section, pjsip, nmapped):
352     """
353     Determines whether a mailbox configured in sip.conf should map to
354     an endpoint or aor in pjsip.conf. If subscribemwi is true, then the
355     mailboxes are set on an aor. Otherwise the mailboxes are set on the
356     endpoint.
357     """
358
359     try:
360         subscribemwi = sip.get(section, 'subscribemwi')[0]
361     except LookupError:
362         # No subscribemwi option means default it to 'no'
363         subscribemwi = 'no'
364
365     set_value('mailboxes', val, section, pjsip, nmapped, 'aor'
366               if subscribemwi == 'yes' else 'endpoint')
367
368
369 def setup_auth(key, val, section, pjsip, nmapped):
370     """
371     Sets up authentication information for a specific endpoint based on the
372     'secret' setting on a peer in sip.conf
373     """
374     set_value('username', section, section, pjsip, nmapped, 'auth')
375     # In chan_sip, if a secret and an md5secret are both specified on a peer,
376     # then in practice, only the md5secret is used. If both are encountered
377     # then we build an auth section that has both an md5_cred and password.
378     # However, the auth_type will indicate to authenticators to use the
379     # md5_cred, so like with sip.conf, the password will be there but have
380     # no purpose.
381     if key == 'secret':
382         set_value('password', val, section, pjsip, nmapped, 'auth')
383     else:
384         set_value('md5_cred', val, section, pjsip, nmapped, 'auth')
385         set_value('auth_type', 'md5', section, pjsip, nmapped, 'auth')
386
387     realms = [section]
388     try:
389         auths = sip.get('authentication', 'auth')
390         for i in auths:
391             user, at, realm = i.partition('@')
392             realms.append(realm)
393     except LookupError:
394         pass
395
396     realm_str = ','.join(realms)
397
398     set_value('auth', section, section, pjsip, nmapped)
399     set_value('outbound_auth', realm_str, section, pjsip, nmapped)
400
401
402 def setup_ident(key, val, section, pjsip, nmapped):
403     """
404     Examines the 'type' field for a sip.conf peer and creates an identify
405     section if the type is either 'peer' or 'friend'. The identify section uses
406     either the host or defaultip field of the sip.conf peer.
407     """
408     if val != 'peer' and val != 'friend':
409         return
410
411     try:
412         ip = sip.get(section, 'host')[0]
413     except LookupError:
414         return
415
416     if ip == 'dynamic':
417         try:
418             ip = sip.get(section, 'defaultip')[0]
419         except LookupError:
420             return
421
422     set_value('endpoint', section, section, pjsip, nmapped, 'identify')
423     set_value('match', ip, section, pjsip, nmapped, 'identify')
424
425
426 def from_encryption_taglen(key, val, section, pjsip, nmapped):
427     """Sets the srtp_tag32 option based on sip.conf encryption_taglen"""
428     if val == '32':
429         set_value('srtp_tag_32', 'yes', section, pjsip, nmapped)
430
431
432 def from_dtlsenable(key, val, section, pjsip, nmapped):
433     """Optionally sets media_encryption=dtls based on sip.conf dtlsenable"""
434     if val == 'yes':
435         set_value('media_encryption', 'dtls', section, pjsip, nmapped)
436
437 ###############################################################################
438
439 # options in pjsip.conf on an endpoint that have no sip.conf equivalent:
440 # type, 100rel, trust_id_outbound, aggregate_mwi, connected_line_method
441
442 # known sip.conf peer keys that can be mapped to a pjsip.conf section/key
443 peer_map = [
444     # sip.conf option      mapping function     pjsip.conf option(s)
445     ###########################################################################
446     ['context',            set_value],
447     ['dtmfmode',           set_dtmfmode],
448     ['disallow',           merge_codec_value],
449     ['allow',              merge_codec_value],
450     ['nat',                from_nat],            # rtp_symmetric, force_rport,
451                                                  # rewrite_contact
452     ['rtptimeout',         set_value('rtp_timeout')],
453     ['icesupport',         set_value('ice_support')],
454     ['autoframing',        set_value('use_ptime')],
455     ['outboundproxy',      set_value('outbound_proxy')],
456     ['mohsuggest',         set_value('moh_suggest')],
457     ['session-timers',     set_timers],          # timers
458     ['session-minse',      set_value('timers_min_se')],
459     ['session-expires',    set_value('timers_sess_expires')],
460     # identify_by ?
461     ['canreinvite',        set_direct_media],    # direct_media alias
462     ['directmedia',        set_direct_media],    # direct_media
463                                                  # direct_media_method
464                                                  # directed_media_glare_mitigation
465                                                  # disable_directed_media_on_nat
466     ['callerid',           set_value],           # callerid
467     ['callingpres',        set_value('callerid_privacy')],
468     ['cid_tag',            set_value('callerid_tag')],
469     ['trustpid',           set_value('trust_id_inbound')],
470     ['sendrpid',           from_sendrpid],       # send_pai, send_rpid
471     ['send_diversion',     set_value],
472     ['encryption',         set_media_encryption],
473     ['avpf',               set_value('use_avpf')],
474     ['recordonfeature',    set_record_on_feature],  # automixon
475     ['recordofffeature',   set_record_off_feature],  # automixon
476     ['progressinband',     from_progressinband], # in_band_progress
477     ['callgroup',          set_value('call_group')],
478     ['pickupgroup',        set_value('pickup_group')],
479     ['namedcallgroup',     set_value('named_call_group')],
480     ['namedpickupgroup',   set_value('named_pickup_group')],
481     ['allowtransfer',      set_value('allow_transfer')],
482     ['fromuser',           set_value('from_user')],
483     ['fromdomain',         set_value('from_domain')],
484     ['mwifrom',            set_value('mwi_from_user')],
485     ['tos_audio',          set_value],
486     ['tos_video',          set_value],
487     ['cos_audio',          set_value],
488     ['cos_video',          set_value],
489     ['sdpowner',           set_value('sdp_owner')],
490     ['sdpsession',         set_value('sdp_session')],
491     ['tonezone',           set_value('tone_zone')],
492     ['language',           set_value],
493     ['allowsubscribe',     set_value('allow_subscribe')],
494     ['subminexpiry',       set_value('sub_min_expiry')],
495     ['rtp_engine',         set_value],
496     ['mailbox',            from_mailbox],
497     ['busylevel',          set_value('device_state_busy_at')],
498     ['secret',             setup_auth],
499     ['md5secret',          setup_auth],
500     ['type',               setup_ident],
501     ['dtlsenable',         from_dtlsenable],
502     ['dtlsverify',         set_value('dtls_verify')],
503     ['dtlsrekey',          set_value('dtls_rekey')],
504     ['dtlscertfile',       set_value('dtls_cert_file')],
505     ['dtlsprivatekey',     set_value('dtls_private_key')],
506     ['dtlscipher',         set_value('dtls_cipher')],
507     ['dtlscafile',         set_value('dtls_ca_file')],
508     ['dtlscapath',         set_value('dtls_ca_path')],
509     ['dtlssetup',          set_value('dtls_setup')],
510     ['encryption_taglen',  from_encryption_taglen],
511
512 ############################ maps to an aor ###################################
513
514     ['host',               from_host],           # contact, max_contacts
515     ['qualifyfreq',        set_value('qualify_frequency', type='aor')],
516     ['maxexpiry',          set_value('maximum_expiration', type='aor')],
517     ['minexpiry',          set_value('minimum_expiration', type='aor')],
518     ['defaultexpiry',      set_value('default_expiration', type='aor')],
519
520 ############################# maps to auth#####################################
521 #        type = auth
522 #        username
523 #        password
524 #        md5_cred
525 #        realm
526 #        nonce_lifetime
527 #        auth_type
528 ######################### maps to acl/security ################################
529
530     ['permit',             merge_value(type='acl', section_to='acl')],
531     ['deny',               merge_value(type='acl', section_to='acl')],
532     ['acl',                merge_value(type='acl', section_to='acl')],
533     ['contactpermit',      merge_value(type='acl', section_to='acl', key_to='contact_permit')],
534     ['contactdeny',        merge_value(type='acl', section_to='acl', key_to='contact_deny')],
535     ['contactacl',         merge_value(type='acl', section_to='acl', key_to='contact_acl')],
536
537 ########################### maps to transport #################################
538 #        type = transport
539 #        protocol
540 #        bind
541 #        async_operations
542 #        ca_list_file
543 #        ca_list_path
544 #        cert_file
545 #        privkey_file
546 #        password
547 #        external_signaling_address - externip & externhost
548 #        external_signaling_port
549 #        external_media_address
550 #        domain
551 #        verify_server
552 #        verify_client
553 #        require_client_cert
554 #        method
555 #        cipher
556 #        localnet
557 ######################### maps to domain_alias ################################
558 #        type = domain_alias
559 #        domain
560 ######################### maps to registration ################################
561 #        type = registration
562 #        server_uri
563 #        client_uri
564 #        contact_user
565 #        transport
566 #        outbound_proxy
567 #        expiration
568 #        retry_interval
569 #        max_retries
570 #        auth_rejection_permanent
571 #        outbound_auth
572 ########################### maps to identify ##################################
573 #        type = identify
574 #        endpoint
575 #        match
576 ]
577
578
579 def split_hostport(addr):
580     """
581     Given an address in the form 'host:port' separate the host and port
582     components.
583     Returns a two-tuple of strings, (host, port). If no port is present in the
584     string, then the port section of the tuple is None.
585     """
586     try:
587         socket.inet_pton(socket.AF_INET6, addr)
588         if not addr.startswith('['):
589             return (addr, None)
590     except socket.error:
591         pass
592
593     # Literal IPv6 (like [::]), IPv4, or hostname
594     # does not work for IPv6 without brackets; case catched above
595     url = urlparse('sip://' + addr)
596     # TODO Does not compress IPv6, for example 0:0:0:0:0:0:0:0 should get [::]
597     return (url.hostname, url.port)
598
599
600 def set_transport_common(section, sip, pjsip, protocol, nmapped):
601     """
602     sip.conf has several global settings that in pjsip.conf apply to individual
603     transports. This function adds these global settings to each individual
604     transport.
605
606     The settings included are:
607     externaddr (or externip)
608     externhost
609     externtcpport for TCP
610     externtlsport for TLS
611     localnet
612     tos_sip
613     cos_sip
614     """
615     try:
616         extern_addr = sip.multi_get('general', ['externaddr', 'externip',
617                                                 'externhost'])[0]
618         host, port = split_hostport(extern_addr)
619         try:
620             port = sip.get('general', 'extern' + protocol + 'port')[0]
621         except LookupError:
622             pass
623         set_value('external_media_address', host, section, pjsip,
624                   nmapped, 'transport')
625         set_value('external_signaling_address', host, section, pjsip,
626                   nmapped, 'transport')
627         if port:
628             set_value('external_signaling_port', port, section, pjsip,
629                       nmapped, 'transport')
630     except LookupError:
631         pass
632
633     try:
634         merge_value('localnet', sip.get('general', 'localnet')[0], 'general',
635                     pjsip, nmapped, 'transport', section, "local_net")
636     except LookupError:
637         # No localnet options configured. Move on.
638         pass
639
640     try:
641         set_value('tos', sip.get('general', 'tos_sip')[0], section, pjsip,
642                   nmapped, 'transport')
643     except LookupError:
644         pass
645
646     try:
647         set_value('cos', sip.get('general', 'cos_sip')[0], section, pjsip,
648                   nmapped, 'transport')
649     except LookupError:
650         pass
651
652
653 def get_bind(sip, pjsip, protocol):
654     """
655     Given the protocol (udp, tcp, or tls), return
656     - the bind address, like [::] or 0.0.0.0
657     - name of the section to be created
658     """
659     section = 'transport-' + protocol
660
661     # UDP cannot be disabled in chan_sip
662     if protocol != 'udp':
663         try:
664             enabled = sip.get('general', protocol + 'enable')[0]
665         except LookupError:
666             # No value means disabled by default. Don't create this transport
667             return (None, section)
668         if enabled != 'yes':
669             return (None, section)
670
671     try:
672         bind = pjsip.get(section, 'bind')[0]
673         # The first run created an transport already but this
674         # server was not configured for IPv4/IPv6 Dual Stack
675         return (None, section)
676     except LookupError:
677         pass
678
679     try:
680         bind = pjsip.get(section + '6', 'bind')[0]
681         # The first run created an IPv6 transport, because
682         # the server was configured with :: as bindaddr.
683         # Now, re-use its port and create the IPv4 transport
684         host, port = split_hostport(bind)
685         bind = '0.0.0.0'
686         if port:
687             bind += ':' + str(port)
688     except LookupError:
689         # This is the first run, no transport in pjsip exists.
690         try:
691             bind = sip.get('general', protocol + 'bindaddr')[0]
692         except LookupError:
693             if protocol == 'udp':
694                 try:
695                     bind = sip.get('general', 'bindaddr')[0]
696                 except LookupError:
697                     bind = '0.0.0.0'
698             else:
699                 try:
700                     bind = pjsip.get('transport-udp6', 'bind')[0]
701                 except LookupError:
702                     bind = pjsip.get('transport-udp', 'bind')[0]
703                 # Only TCP reuses host:port of UDP, others reuse just host
704                 if protocol == 'tls':
705                     bind, port = split_hostport(bind)
706         host, port = split_hostport(bind)
707         if host == '::':
708             section += '6'
709
710     if protocol == 'udp':
711         host = build_host(sip, bind, 'general', 'bindport')
712     else:
713         host = build_host(sip, bind)
714
715     return (host, section)
716
717
718 def create_udp(sip, pjsip, nmapped):
719     """
720     Creates a 'transport-udp' section in the pjsip.conf file based
721     on the following settings from sip.conf:
722
723     bindaddr (or udpbindaddr)
724     bindport
725     """
726     protocol = 'udp'
727     bind, section = get_bind(sip, pjsip, protocol)
728
729     set_value('protocol', protocol, section, pjsip, nmapped, 'transport')
730     set_value('bind', bind, section, pjsip, nmapped, 'transport')
731     set_transport_common(section, sip, pjsip, protocol, nmapped)
732
733
734 def create_tcp(sip, pjsip, nmapped):
735     """
736     Creates a 'transport-tcp' section in the pjsip.conf file based
737     on the following settings from sip.conf:
738
739     tcpenable
740     tcpbindaddr (or bindaddr)
741     """
742     protocol = 'tcp'
743     bind, section = get_bind(sip, pjsip, protocol)
744     if not bind:
745         return
746
747     set_value('protocol', protocol, section, pjsip, nmapped, 'transport')
748     set_value('bind', bind, section, pjsip, nmapped, 'transport')
749     set_transport_common(section, sip, pjsip, protocol, nmapped)
750
751
752 def set_tls_cert_file(val, pjsip, section, nmapped):
753     """Sets cert_file based on sip.conf tlscertfile"""
754     set_value('cert_file', val, section, pjsip, nmapped,
755               'transport')
756
757
758 def set_tls_private_key(val, pjsip, section, nmapped):
759     """Sets privkey_file based on sip.conf tlsprivatekey or sslprivatekey"""
760     set_value('priv_key_file', val, section, pjsip, nmapped,
761               'transport')
762
763
764 def set_tls_cipher(val, pjsip, section, nmapped):
765     """Sets cipher based on sip.conf tlscipher or sslcipher"""
766     set_value('cipher', val, section, pjsip, nmapped, 'transport')
767
768
769 def set_tls_cafile(val, pjsip, section, nmapped):
770     """Sets ca_list_file based on sip.conf tlscafile"""
771     set_value('ca_list_file', val, section, pjsip, nmapped,
772               'transport')
773
774
775 def set_tls_capath(val, pjsip, section, nmapped):
776     """Sets ca_list_path based on sip.conf tlscapath"""
777     set_value('ca_list_path', val, section, pjsip, nmapped,
778               'transport')
779
780
781 def set_tls_verifyclient(val, pjsip, section, nmapped):
782     """Sets verify_client based on sip.conf tlsverifyclient"""
783     set_value('verify_client', val, section, pjsip, nmapped,
784               'transport')
785
786
787 def set_tls_verifyserver(val, pjsip, section, nmapped):
788     """Sets verify_server based on sip.conf tlsdontverifyserver"""
789
790     if val == 'no':
791         set_value('verify_server', 'yes', section, pjsip, nmapped,
792                   'transport')
793     else:
794         set_value('verify_server', 'no', section, pjsip, nmapped,
795                   'transport')
796
797
798 def create_tls(sip, pjsip, nmapped):
799     """
800     Creates a 'transport-tls' section in pjsip.conf based on the following
801     settings from sip.conf:
802
803     tlsenable (or sslenable)
804     tlsbindaddr (or sslbindaddr or bindaddr)
805     tlsprivatekey (or sslprivatekey)
806     tlscipher (or sslcipher)
807     tlscafile
808     tlscapath (or tlscadir)
809     tlscertfile (or sslcert or tlscert)
810     tlsverifyclient
811     tlsdontverifyserver
812     tlsclientmethod (or sslclientmethod)
813     """
814     protocol = 'tls'
815     bind, section = get_bind(sip, pjsip, protocol)
816     if not bind:
817         return
818
819     set_value('protocol', protocol, section, pjsip, nmapped, 'transport')
820     set_value('bind', bind, section, pjsip, nmapped, 'transport')
821     set_transport_common(section, sip, pjsip, protocol, nmapped)
822
823     tls_map = [
824         (['tlscertfile', 'sslcert', 'tlscert'], set_tls_cert_file),
825         (['tlsprivatekey', 'sslprivatekey'], set_tls_private_key),
826         (['tlscipher', 'sslcipher'], set_tls_cipher),
827         (['tlscafile'], set_tls_cafile),
828         (['tlscapath', 'tlscadir'], set_tls_capath),
829         (['tlsverifyclient'], set_tls_verifyclient),
830         (['tlsdontverifyserver'], set_tls_verifyserver)
831     ]
832
833     for i in tls_map:
834         try:
835             i[1](sip.multi_get('general', i[0])[0], pjsip, section, nmapped)
836         except LookupError:
837             pass
838
839     try:
840         method = sip.multi_get('general', ['tlsclientmethod',
841                                            'sslclientmethod'])[0]
842         if section != 'transport-' + protocol + '6':  # print only once
843             print('In chan_sip, you specified the TLS version. With chan_sip,' \
844                   ' this was just for outbound client connections. In' \
845                   ' chan_pjsip, this value is for client and server. Instead,' \
846                   ' consider not to specify \'tlsclientmethod\' for chan_sip' \
847                   ' and \'method = sslv23\' for chan_pjsip.')
848     except LookupError:
849         """
850         OpenSSL emerged during the 90s. SSLv2 and SSLv3 were the only
851         existing methods at that time. The OpenSSL project continued. And as
852         of today (OpenSSL 1.0.2) this does not start SSLv2 and SSLv3 anymore
853         but TLSv1.0 and v1.2. Or stated differently: This method should
854         have been called 'method = secure' or 'method = automatic' back in
855         the 90s. The PJProject did not realize this and uses 'tlsv1' as
856         default when unspecified, which disables TLSv1.2. chan_sip used
857         'sslv23' as default when unspecified, which gives TLSv1.0 and v1.2.
858         """
859         method = 'sslv23'
860     set_value('method', method, section, pjsip, nmapped, 'transport')
861
862
863 def map_transports(sip, pjsip, nmapped):
864     """
865     Finds options in sip.conf general section pertaining to
866     transport configuration and creates appropriate transport
867     configuration sections in pjsip.conf.
868
869     sip.conf only allows a single UDP transport, TCP transport,
870     and TLS transport for each IP version. As such, the mapping
871     into PJSIP can be made consistent by defining six sections:
872
873     transport-udp6
874     transport-udp
875     transport-tcp6
876     transport-tcp
877     transport-tls6
878     transport-tls
879
880     To accommodate the default behaviors in sip.conf, we'll need to
881     create the UDP transports first, followed by the TCP and TLS transports.
882     """
883
884     # First create a UDP transport. Even if no bind parameters were provided
885     # in sip.conf, chan_sip would always bind to UDP 0.0.0.0:5060
886     create_udp(sip, pjsip, nmapped)
887     create_udp(sip, pjsip, nmapped)
888
889     # TCP settings may be dependent on UDP settings, so do it second.
890     create_tcp(sip, pjsip, nmapped)
891     create_tcp(sip, pjsip, nmapped)
892     create_tls(sip, pjsip, nmapped)
893     create_tls(sip, pjsip, nmapped)
894
895
896 def map_auth(sip, pjsip, nmapped):
897     """
898     Creates auth sections based on entries in the authentication section of
899     sip.conf. pjsip.conf section names consist of "auth_" followed by the name
900     of the realm.
901     """
902     try:
903         auths = sip.get('authentication', 'auth')
904     except LookupError:
905         return
906
907     for i in auths:
908         creds, at, realm = i.partition('@')
909         if not at and not realm:
910             # Invalid. Move on
911             continue
912         user, colon, secret = creds.partition(':')
913         if not secret:
914             user, sharp, md5 = creds.partition('#')
915             if not md5:
916                 #Invalid. move on
917                 continue
918         section = "auth_" + realm
919
920         set_value('realm', realm, section, pjsip, nmapped, 'auth')
921         set_value('username', user, section, pjsip, nmapped, 'auth')
922         if secret:
923             set_value('password', secret, section, pjsip, nmapped, 'auth')
924         else:
925             set_value('md5_cred', md5, section, pjsip, nmapped, 'auth')
926             set_value('auth_type', 'md5', section, pjsip, nmapped, 'auth')
927
928
929 class Registration:
930     """
931     Class for parsing and storing information in a register line in sip.conf.
932     """
933     def __init__(self, line, retry_interval, max_attempts, outbound_proxy):
934         self.retry_interval = retry_interval
935         self.max_attempts = max_attempts
936         self.outbound_proxy = outbound_proxy
937         self.parse(line)
938
939     def parse(self, line):
940         """
941         Initial parsing routine for register lines in sip.conf.
942
943         This splits the line into the part before the host, and the part
944         after the '@' symbol. These two parts are then passed to their
945         own parsing routines
946         """
947
948         # register =>
949         # [peer?][transport://]user[@domain][:secret[:authuser]]@host[:port][/extension][~expiry]
950
951         prehost, at, host_part = line.rpartition('@')
952         if not prehost:
953             raise
954
955         self.parse_host_part(host_part)
956         self.parse_user_part(prehost)
957
958     def parse_host_part(self, host_part):
959         """
960         Parsing routine for the part after the final '@' in a register line.
961         The strategy is to use partition calls to peel away the data starting
962         from the right and working to the left.
963         """
964         pre_expiry, sep, expiry = host_part.partition('~')
965         pre_extension, sep, self.extension = pre_expiry.partition('/')
966         self.host, sep, self.port = pre_extension.partition(':')
967
968         self.expiry = expiry if expiry else '120'
969
970     def parse_user_part(self, user_part):
971         """
972         Parsing routine for the part before the final '@' in a register line.
973         The only mandatory part of this line is the user portion. The strategy
974         here is to start by using partition calls to remove everything to
975         the right of the user, then finish by using rpartition calls to remove
976         everything to the left of the user.
977         """
978         self.peer = ''
979         self.protocol = 'udp'
980         protocols = ['udp', 'tcp', 'tls']
981         for protocol in protocols:
982             position = user_part.find(protocol + '://')
983             if -1 < position:
984                 post_transport = user_part[position + 6:]
985                 self.peer, sep, self.protocol = user_part[:position + 3].rpartition('?')
986                 user_part = post_transport
987                 break
988
989         colons = user_part.count(':')
990         if (colons == 3):
991             # :domainport:secret:authuser
992             pre_auth, sep, port_auth = user_part.partition(':')
993             self.domainport, sep, auth = port_auth.partition(':')
994             self.secret, sep, self.authuser = auth.partition(':')
995         elif (colons == 2):
996             # :secret:authuser
997             pre_auth, sep, auth = user_part.partition(':')
998             self.secret, sep, self.authuser = auth.partition(':')
999         elif (colons == 1):
1000             # :secret
1001             pre_auth, sep, self.secret = user_part.partition(':')
1002         elif (colons == 0):
1003             # No port, secret, or authuser
1004             pre_auth = user_part
1005         else:
1006             # Invalid setting
1007             raise
1008
1009         self.user, sep, self.domain = pre_auth.partition('@')
1010
1011     def write(self, pjsip, nmapped):
1012         """
1013         Write parsed registration data into a section in pjsip.conf
1014
1015         Most of the data in self will get written to a registration section.
1016         However, there will also need to be an auth section created if a
1017         secret or authuser is present.
1018
1019         General mapping of values:
1020         A combination of self.host and self.port is server_uri
1021         A combination of self.user, self.domain, and self.domainport is
1022           client_uri
1023         self.expiry is expiration
1024         self.extension is contact_user
1025         self.protocol will map to one of the mapped transports
1026         self.secret and self.authuser will result in a new auth section, and
1027           outbound_auth will point to that section.
1028         XXX self.peer really doesn't map to anything :(
1029         """
1030
1031         section = 'reg_' + self.host
1032
1033         set_value('retry_interval', self.retry_interval, section, pjsip,
1034                   nmapped, 'registration')
1035         set_value('max_retries', self.max_attempts, section, pjsip, nmapped,
1036                   'registration')
1037         if self.extension:
1038             set_value('contact_user', self.extension, section, pjsip, nmapped,
1039                       'registration')
1040
1041         set_value('expiration', self.expiry, section, pjsip, nmapped,
1042                   'registration')
1043
1044         if self.protocol == 'udp':
1045             set_value('transport', 'transport-udp', section, pjsip, nmapped,
1046                       'registration')
1047         elif self.protocol == 'tcp':
1048             set_value('transport', 'transport-tcp', section, pjsip, nmapped,
1049                       'registration')
1050         elif self.protocol == 'tls':
1051             set_value('transport', 'transport-tls', section, pjsip, nmapped,
1052                       'registration')
1053
1054         auth_section = 'auth_reg_' + self.host
1055
1056         if hasattr(self, 'secret') and self.secret:
1057             set_value('password', self.secret, auth_section, pjsip, nmapped,
1058                       'auth')
1059             set_value('username', self.authuser if hasattr(self, 'authuser')
1060                       else self.user, auth_section, pjsip, nmapped, 'auth')
1061             set_value('outbound_auth', auth_section, section, pjsip, nmapped,
1062                       'registration')
1063
1064         client_uri = "sip:%s@" % self.user
1065         if self.domain:
1066             client_uri += self.domain
1067         else:
1068             client_uri += self.host
1069
1070         if hasattr(self, 'domainport') and self.domainport:
1071             client_uri += ":" + self.domainport
1072         elif self.port:
1073             client_uri += ":" + self.port
1074
1075         set_value('client_uri', client_uri, section, pjsip, nmapped,
1076                   'registration')
1077
1078         server_uri = "sip:%s" % self.host
1079         if self.port:
1080             server_uri += ":" + self.port
1081
1082         set_value('server_uri', server_uri, section, pjsip, nmapped,
1083                   'registration')
1084
1085         if self.outbound_proxy:
1086             set_value('outboundproxy', self.outbound_proxy, section, pjsip,
1087                       nmapped, 'registration')
1088
1089
1090 def map_registrations(sip, pjsip, nmapped):
1091     """
1092     Gathers all necessary outbound registration data in sip.conf and creates
1093     corresponding registration sections in pjsip.conf
1094     """
1095     try:
1096         regs = sip.get('general', 'register')
1097     except LookupError:
1098         return
1099
1100     try:
1101         retry_interval = sip.get('general', 'registertimeout')[0]
1102     except LookupError:
1103         retry_interval = '20'
1104
1105     try:
1106         max_attempts = sip.get('general', 'registerattempts')[0]
1107     except LookupError:
1108         max_attempts = '10'
1109
1110     try:
1111         outbound_proxy = sip.get('general', 'outboundproxy')[0]
1112     except LookupError:
1113         outbound_proxy = ''
1114
1115     for i in regs:
1116         reg = Registration(i, retry_interval, max_attempts, outbound_proxy)
1117         reg.write(pjsip, nmapped)
1118
1119
1120 def map_peer(sip, section, pjsip, nmapped):
1121     """
1122     Map the options from a peer section in sip.conf into the appropriate
1123     sections in pjsip.conf
1124     """
1125     for i in peer_map:
1126         try:
1127             # coming from sip.conf the values should mostly be a list with a
1128             # single value.  In the few cases that they are not a specialized
1129             # function (see merge_value) is used to retrieve the values.
1130             i[1](i[0], sip.get(section, i[0])[0], section, pjsip, nmapped)
1131         except LookupError:
1132             pass  # key not found in sip.conf
1133
1134     setup_udptl(section, pjsip, nmapped)
1135
1136 def find_non_mapped(sections, nmapped):
1137     """
1138     Determine sip.conf options that were not properly mapped to pjsip.conf
1139     options.
1140     """
1141     for section, sect in sections.iteritems():
1142         try:
1143             # since we are pulling from sip.conf this should always
1144             # be a single value list
1145             sect = sect[0]
1146             # loop through the section and store any values that were not
1147             # mapped
1148             for key in sect.keys(True):
1149                 for i in peer_map:
1150                     if i[0] == key:
1151                         break
1152                 else:
1153                     nmapped(section, key, sect[key])
1154         except LookupError:
1155             pass
1156
1157
1158 def map_system(sip, pjsip, nmapped):
1159     section = 'system' # Just a label; you as user can change that
1160     type = 'system' # Not a label, therefore not the same as section
1161
1162     try:
1163         user_agent = sip.get('general', 'useragent')[0]
1164         set_value('user_agent', user_agent, 'global', pjsip, nmapped, 'global')
1165     except LookupError:
1166         pass
1167
1168
1169     try:
1170         sipdebug = sip.get('general', 'sipdebug')[0]
1171         set_value('debug', sipdebug, 'global', pjsip, nmapped, 'global')
1172     except LookupError:
1173         pass
1174
1175     try:
1176         useroption_parsing = sip.get('general', 'legacy_useroption_parsing')[0]
1177         set_value('ignore_uri_user_options', useroption_parsing, 'global', pjsip, nmapped, 'global')
1178     except LookupError:
1179         pass
1180
1181     try:
1182         timer_t1 = sip.get('general', 'timert1')[0]
1183         set_value('timer_t1', timer_t1, section, pjsip, nmapped, type)
1184     except LookupError:
1185         pass
1186
1187     try:
1188         timer_b = sip.get('general', 'timerb')[0]
1189         set_value('timer_b', timer_b, section, pjsip, nmapped, type)
1190     except LookupError:
1191         pass
1192
1193     try:
1194         compact_headers = sip.get('general', 'compactheaders')[0]
1195         set_value('compact_headers', compact_headers, section, pjsip, nmapped, type)
1196     except LookupError:
1197         pass
1198
1199
1200 def convert(sip, filename, non_mappings, include):
1201     """
1202     Entry point for configuration file conversion. This
1203     function will create a pjsip.conf object and begin to
1204     map specific sections from sip.conf into it.
1205     Returns the new pjsip.conf object once completed
1206     """
1207     pjsip = sip.__class__()
1208     non_mappings[filename] = astdicts.MultiOrderedDict()
1209     nmapped = non_mapped(non_mappings[filename])
1210     if not include:
1211         # Don't duplicate transport and registration configs
1212         map_system(sip, pjsip, nmapped)
1213         map_transports(sip, pjsip, nmapped)
1214         map_registrations(sip, pjsip, nmapped)
1215     map_auth(sip, pjsip, nmapped)
1216     for section in sip.sections():
1217         if section == 'authentication':
1218             pass
1219         else:
1220             map_peer(sip, section, pjsip, nmapped)
1221
1222     find_non_mapped(sip.defaults(), nmapped)
1223     find_non_mapped(sip.sections(), nmapped)
1224
1225     for key, val in sip.includes().iteritems():
1226         pjsip.add_include(PREFIX + key, convert(val, PREFIX + key,
1227                           non_mappings, True)[0])
1228     return pjsip, non_mappings
1229
1230
1231 def write_pjsip(filename, pjsip, non_mappings):
1232     """
1233     Write pjsip.conf file to disk
1234     """
1235     try:
1236         with open(filename, 'wt') as fp:
1237             fp.write(';--\n')
1238             fp.write(';;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;\n')
1239             fp.write('Non mapped elements start\n')
1240             fp.write(';;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;\n\n')
1241             astconfigparser.write_dicts(fp, non_mappings[filename])
1242             fp.write(';;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;\n')
1243             fp.write('Non mapped elements end\n')
1244             fp.write(';;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;\n')
1245             fp.write('--;\n\n')
1246             # write out include file(s)
1247             pjsip.write(fp)
1248
1249     except IOError:
1250         print("Could not open file " + filename + " for writing")
1251
1252 ###############################################################################
1253
1254
1255 def cli_options():
1256     """
1257     Parse command line options and apply them. If invalid input is given,
1258     print usage information
1259     """
1260     global PREFIX
1261     usage = "usage: %prog [options] [input-file [output-file]]\n\n" \
1262         "Converts the chan_sip configuration input-file to the chan_pjsip output-file.\n" \
1263         "The input-file defaults to 'sip.conf'.\n" \
1264         "The output-file defaults to 'pjsip.conf'."
1265     parser = optparse.OptionParser(usage=usage)
1266     parser.add_option('-p', '--prefix', dest='prefix', default=PREFIX,
1267                       help='output prefix for include files')
1268
1269     options, args = parser.parse_args()
1270     PREFIX = options.prefix
1271
1272     sip_filename = args[0] if len(args) else 'sip.conf'
1273     pjsip_filename = args[1] if len(args) == 2 else 'pjsip.conf'
1274
1275     return sip_filename, pjsip_filename
1276
1277 if __name__ == "__main__":
1278     sip_filename, pjsip_filename = cli_options()
1279     # configuration parser for sip.conf
1280     sip = astconfigparser.MultiOrderedConfigParser()
1281     print('Please, report any issue at:')
1282     print('    https://issues.asterisk.org/')
1283     print('Reading ' + sip_filename)
1284     sip.read(sip_filename)
1285     print('Converting to PJSIP...')
1286     pjsip, non_mappings = convert(sip, pjsip_filename, dict(), False)
1287     print('Writing ' + pjsip_filename)
1288     write_pjsip(pjsip_filename, pjsip, non_mappings)