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