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