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