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     ['encrpytion',         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
444 ############################# maps to auth#####################################
445 #        type = auth
446 #        username
447 #        password
448 #        md5_cred
449 #        realm
450 #        nonce_lifetime
451 #        auth_type
452 ######################### maps to acl/security ################################
453
454     ['permit',             merge_value(type='acl', section_to='acl')],
455     ['deny',               merge_value(type='acl', section_to='acl')],
456     ['acl',                merge_value(type='acl', section_to='acl')],
457     ['contactpermit',      merge_value('contact_permit', type='acl', section_to='acl')],
458     ['contactdeny',        merge_value('contact_deny', type='acl', section_to='acl')],
459     ['contactacl',         merge_value('contact_acl', type='acl', section_to='acl')],
460
461 ########################### maps to transport #################################
462 #        type = transport
463 #        protocol
464 #        bind
465 #        async_operations
466 #        ca_list_file
467 #        ca_list_path
468 #        cert_file
469 #        privkey_file
470 #        password
471 #        external_signaling_address - externip & externhost
472 #        external_signaling_port
473 #        external_media_address
474 #        domain
475 #        verify_server
476 #        verify_client
477 #        require_client_cert
478 #        method
479 #        cipher
480 #        localnet
481 ######################### maps to domain_alias ################################
482 #        type = domain_alias
483 #        domain
484 ######################### maps to registration ################################
485 #        type = registration
486 #        server_uri
487 #        client_uri
488 #        contact_user
489 #        transport
490 #        outbound_proxy
491 #        expiration
492 #        retry_interval
493 #        max_retries
494 #        auth_rejection_permanent
495 #        outbound_auth
496 ########################### maps to identify ##################################
497 #        type = identify
498 #        endpoint
499 #        match
500 ]
501
502
503 def add_localnet(section, pjsip, nmapped):
504     """
505     Adds localnet values from sip.conf's general section to a transport in
506     pjsip.conf. Ideally, we would have just created a template with the
507     localnet sections, but because this is a script, it's not hard to add
508     the same thing on to every transport.
509     """
510     try:
511         merge_value('local_net', sip.get('general', 'localnet')[0], 'general',
512                     pjsip, nmapped, 'transport', section)
513     except LookupError:
514         # No localnet options configured. No biggie!
515         pass
516
517
518 def set_transport_common(section, pjsip, nmapped):
519     """
520     sip.conf has several global settings that in pjsip.conf apply to individual
521     transports. This function adds these global settings to each individual
522     transport.
523
524     The settings included are:
525     localnet
526     tos_sip
527     cos_sip
528     """
529
530     try:
531         merge_value('local_net', sip.get('general', 'localnet')[0], 'general',
532                     pjsip, nmapped, 'transport', section)
533     except LookupError:
534         # No localnet options configured. Move on.
535         pass
536
537     try:
538         set_value('tos', sip.get('general', 'sip_tos')[0], 'general', pjsip,
539                   nmapped, 'transport', section)
540     except LookupError:
541         pass
542
543     try:
544         set_value('cos', sip.get('general', 'sip_cos')[0], 'general', pjsip,
545                   nmapped, 'transport', section)
546     except LookupError:
547         pass
548
549
550 def split_hostport(addr):
551     """
552     Given an address in the form 'addr:port' separate the addr and port
553     components.
554     Returns a two-tuple of strings, (addr, port). If no port is present in the
555     string, then the port section of the tuple is None.
556     """
557     try:
558         socket.inet_pton(socket.AF_INET6, addr)
559         if not addr.startswith('['):
560             return (addr, None)
561         else:
562             # If brackets are present, there may be a port as well
563             match = re.match('\[(.*\)]:(\d+)', addr)
564             if match:
565                 return (match.group(1), match.group(2))
566             else:
567                 return (addr, None)
568     except socket.error:
569         pass
570
571     # IPv4 address or hostname
572     host, sep, port = addr.rpartition(':')
573
574     if not sep and not port:
575         return (host, None)
576     else:
577         return (host, port)
578
579
580 def create_udp(sip, pjsip, nmapped):
581     """
582     Creates a 'transport-udp' section in the pjsip.conf file based
583     on the following settings from sip.conf:
584
585     bindaddr (or udpbindaddr)
586     bindport
587     externaddr (or externip)
588     externhost
589     """
590
591     try:
592         bind = sip.multi_get('general', ['udpbindaddr', 'bindaddr'])[0]
593     except LookupError:
594         bind = ''
595
596     bind = build_host(sip, bind, 'general', 'bindport')
597
598     try:
599         extern_addr = sip.multi_get('general', ['externaddr', 'externip',
600                                     'externhost'])[0]
601         host, port = split_hostport(extern_addr)
602         set_value('external_signaling_address', host, 'transport-udp', pjsip,
603                   nmapped, 'transport')
604         if port:
605             set_value('external_signaling_port', port, 'transport-udp', pjsip,
606                       nmapped, 'transport')
607     except LookupError:
608         pass
609
610     set_value('protocol', 'udp', 'transport-udp', pjsip, nmapped, 'transport')
611     set_value('bind', bind, 'transport-udp', pjsip, nmapped, 'transport')
612     set_transport_common('transport-udp', pjsip, nmapped)
613
614
615 def create_tcp(sip, pjsip, nmapped):
616     """
617     Creates a 'transport-tcp' section in the pjsip.conf file based
618     on the following settings from sip.conf:
619
620     tcpenable
621     tcpbindaddr
622     externtcpport
623     """
624
625     try:
626         enabled = sip.get('general', 'tcpenable')[0]
627     except:
628         # No value means disabled by default. No need for a tranport
629         return
630
631     if enabled == 'no':
632         return
633
634     try:
635         bind = sip.get('general', 'tcpbindaddr')[0]
636         bind = build_host(sip, bind, 'general', 'bindport')
637     except LookupError:
638         # No tcpbindaddr means to default to the udpbindaddr
639         bind = pjsip.get('transport-udp', 'bind')[0]
640
641     try:
642         extern_addr = sip.multi_get('general', ['externaddr', 'externip',
643                                     'externhost'])[0]
644         host, port = split_hostport(extern_addr)
645         try:
646             tcpport = sip.get('general', 'externtcpport')[0]
647         except:
648             tcpport = port
649         set_value('external_signaling_address', host, 'transport-tcp', pjsip,
650                   nmapped, 'transport')
651         if tcpport:
652             set_value('external_signaling_port', tcpport, 'transport-tcp',
653                       pjsip, nmapped, 'transport')
654     except LookupError:
655         pass
656
657     set_value('protocol', 'tcp', 'transport-tcp', pjsip, nmapped, 'transport')
658     set_value('bind', bind, 'transport-tcp', pjsip, nmapped, 'transport')
659     set_transport_common('transport-tcp', pjsip, nmapped)
660
661
662 def set_tls_bindaddr(val, pjsip, nmapped):
663     """
664     Creates the TCP bind address. This has two possible methods of
665     working:
666     Use the 'tlsbindaddr' option from sip.conf directly if it has both
667     an address and port. If no port is present, use 5061
668     If there is no 'tlsbindaddr' option present in sip.conf, use the
669     previously-established UDP bind address and port 5061
670     """
671     try:
672         bind = sip.get('general', 'tlsbindaddr')[0]
673         explicit = True
674     except LookupError:
675         # No tlsbindaddr means to default to the bindaddr but with standard TLS
676         # port
677         bind = pjsip.get('transport-udp', 'bind')[0]
678         explicit = False
679
680     matchv4 = re.match('\d+\.\d+\.\d+\.\d+:\d+', bind)
681     matchv6 = re.match('\[.*\]:d+', bind)
682     if matchv4 or matchv6:
683         if explicit:
684             # They provided a port. We'll just use it.
685             set_value('bind', bind, 'transport-tls', pjsip, nmapped,
686                       'transport')
687             return
688         else:
689             # Need to strip the port from the UDP address
690             index = bind.rfind(':')
691             bind = bind[:index]
692
693     # Reaching this point means either there was no port provided or we
694     # stripped the port off. We need to add on the default 5061 port
695
696     bind += ':5061'
697
698     set_value('bind', bind, 'transport-tls', pjsip, nmapped, 'transport')
699
700
701 def set_tls_cert_file(val, pjsip, section, nmapped):
702     """Sets cert_file based on sip.conf tlscertfile"""
703     set_value('cert_file', val, section, pjsip, nmapped,
704               'transport')
705
706
707 def set_tls_private_key(val, pjsip, nmapped):
708     """Sets privkey_file based on sip.conf tlsprivatekey or sslprivatekey"""
709     set_value('priv_key_file', val, 'transport-tls', pjsip, nmapped,
710               'transport')
711
712
713 def set_tls_cipher(val, pjsip, nmapped):
714     """Sets cipher based on sip.conf tlscipher or sslcipher"""
715     set_value('cipher', val, 'transport-tls', pjsip, nmapped, 'transport')
716
717
718 def set_tls_cafile(val, pjsip, nmapped):
719     """Sets ca_list_file based on sip.conf tlscafile"""
720     set_value('ca_list_file', val, 'transport-tls', pjsip, nmapped,
721               'transport')
722
723
724 def set_tls_capath(val, pjsip, nmapped):
725     """Sets ca_list_path based on sip.conf tlscapath"""
726     set_value('ca_list_path', val, 'transport-tls', pjsip, nmapped,
727               'transport')
728
729
730 def set_tls_verifyclient(val, pjsip, nmapped):
731     """Sets verify_client based on sip.conf tlsverifyclient"""
732     set_value('verify_client', val, 'transport-tls', pjsip, nmapped,
733               'transport')
734
735
736 def set_tls_verifyserver(val, pjsip, nmapped):
737     """Sets verify_server based on sip.conf tlsdontverifyserver"""
738
739     if val == 'no':
740         set_value('verify_server', 'yes', 'transport-tls', pjsip, nmapped,
741                   'transport')
742     else:
743         set_value('verify_server', 'no', 'transport-tls', pjsip, nmapped,
744                   'transport')
745
746
747 def set_tls_method(val, pjsip, nmapped):
748     """Sets method based on sip.conf tlsclientmethod or sslclientmethod"""
749     set_value('method', val, 'transport-tls', pjsip, nmapped, 'transport')
750
751
752 def create_tls(sip, pjsip, nmapped):
753     """
754     Creates a 'transport-tls' section in pjsip.conf based on the following
755     settings from sip.conf:
756
757     tlsenable (or sslenable)
758     tlsbindaddr (or sslbindaddr)
759     tlsprivatekey (or sslprivatekey)
760     tlscipher (or sslcipher)
761     tlscafile
762     tlscapath (or tlscadir)
763     tlscertfile (or sslcert or tlscert)
764     tlsverifyclient
765     tlsdontverifyserver
766     tlsclientmethod (or sslclientmethod)
767     """
768
769     tls_map = [
770         (['tlsbindaddr', 'sslbindaddr'], set_tls_bindaddr),
771         (['tlscertfile', 'sslcert', 'tlscert'], set_tls_cert_file),
772         (['tlsprivatekey', 'sslprivatekey'], set_tls_private_key),
773         (['tlscipher', 'sslcipher'], set_tls_cipher),
774         (['tlscafile'], set_tls_cafile),
775         (['tlscapath', 'tlscadir'], set_tls_capath),
776         (['tlsverifyclient'], set_tls_verifyclient),
777         (['tlsdontverifyserver'], set_tls_verifyserver),
778         (['tlsclientmethod', 'sslclientmethod'], set_tls_method)
779     ]
780
781     try:
782         enabled = sip.multi_get('general', ['tlsenable', 'sslenable'])[0]
783     except LookupError:
784         # Not enabled. Don't create a transport
785         return
786
787     if enabled == 'no':
788         return
789
790     set_value('protocol', 'tls', 'transport-tls', pjsip, nmapped, 'transport')
791
792     for i in tls_map:
793         try:
794             i[1](sip.multi_get('general', i[0])[0], pjsip, nmapped)
795         except LookupError:
796             pass
797
798     set_transport_common('transport-tls', pjsip, nmapped)
799     try:
800         extern_addr = sip.multi_get('general', ['externaddr', 'externip',
801                                     'externhost'])[0]
802         host, port = split_hostport(extern_addr)
803         try:
804             tlsport = sip.get('general', 'externtlsport')[0]
805         except:
806             tlsport = port
807         set_value('external_signaling_address', host, 'transport-tls', pjsip,
808                   nmapped, 'transport')
809         if tlsport:
810             set_value('external_signaling_port', tlsport, 'transport-tls',
811                       pjsip, nmapped, 'transport')
812     except LookupError:
813         pass
814
815
816 def map_transports(sip, pjsip, nmapped):
817     """
818     Finds options in sip.conf general section pertaining to
819     transport configuration and creates appropriate transport
820     configuration sections in pjsip.conf.
821
822     sip.conf only allows a single UDP transport, TCP transport,
823     and TLS transport. As such, the mapping into PJSIP can be made
824     consistent by defining three sections:
825
826     transport-udp
827     transport-tcp
828     transport-tls
829
830     To accommodate the default behaviors in sip.conf, we'll need to
831     create the UDP transport first, followed by the TCP and TLS transports.
832     """
833
834     # First create a UDP transport. Even if no bind parameters were provided
835     # in sip.conf, chan_sip would always bind to UDP 0.0.0.0:5060
836     create_udp(sip, pjsip, nmapped)
837
838     # TCP settings may be dependent on UDP settings, so do it second.
839     create_tcp(sip, pjsip, nmapped)
840     create_tls(sip, pjsip, nmapped)
841
842
843 def map_auth(sip, pjsip, nmapped):
844     """
845     Creates auth sections based on entries in the authentication section of
846     sip.conf. pjsip.conf section names consist of "auth_" followed by the name
847     of the realm.
848     """
849     try:
850         auths = sip.get('authentication', 'auth')
851     except LookupError:
852         return
853
854     for i in auths:
855         creds, at, realm = i.partition('@')
856         if not at and not realm:
857             # Invalid. Move on
858             continue
859         user, colon, secret = creds.partition(':')
860         if not secret:
861             user, sharp, md5 = creds.partition('#')
862             if not md5:
863                 #Invalid. move on
864                 continue
865         section = "auth_" + realm
866
867         set_value('realm', realm, section, pjsip, nmapped, 'auth')
868         set_value('username', user, section, pjsip, nmapped, 'auth')
869         if secret:
870             set_value('password', secret, section, pjsip, nmapped, 'auth')
871         else:
872             set_value('md5_cred', md5, section, pjsip, nmapped, 'auth')
873             set_value('auth_type', 'md5', section, pjsip, nmapped, 'auth')
874
875
876 class Registration:
877     """
878     Class for parsing and storing information in a register line in sip.conf.
879     """
880     def __init__(self, line, retry_interval, max_attempts, outbound_proxy):
881         self.retry_interval = retry_interval
882         self.max_attempts = max_attempts
883         self.outbound_proxy = outbound_proxy
884         self.parse(line)
885
886     def parse(self, line):
887         """
888         Initial parsing routine for register lines in sip.conf.
889
890         This splits the line into the part before the host, and the part
891         after the '@' symbol. These two parts are then passed to their
892         own parsing routines
893         """
894
895         # register =>
896         # [peer?][transport://]user[@domain][:secret[:authuser]]@host[:port][/extension][~expiry]
897
898         prehost, at, host_part = line.rpartition('@')
899         if not prehost:
900             raise
901
902         self.parse_host_part(host_part)
903         self.parse_user_part(prehost)
904
905     def parse_host_part(self, host_part):
906         """
907         Parsing routine for the part after the final '@' in a register line.
908         The strategy is to use partition calls to peel away the data starting
909         from the right and working to the left.
910         """
911         pre_expiry, sep, expiry = host_part.partition('~')
912         pre_extension, sep, self.extension = pre_expiry.partition('/')
913         self.host, sep, self.port = pre_extension.partition(':')
914
915         self.expiry = expiry if expiry else '120'
916
917     def parse_user_part(self, user_part):
918         """
919         Parsing routine for the part before the final '@' in a register line.
920         The only mandatory part of this line is the user portion. The strategy
921         here is to start by using partition calls to remove everything to
922         the right of the user, then finish by using rpartition calls to remove
923         everything to the left of the user.
924         """
925         colons = user_part.count(':')
926         if (colons == 3):
927             # :domainport:secret:authuser
928             pre_auth, sep, port_auth = user_part.partition(':')
929             self.domainport, sep, auth = port_auth.partition(':')
930             self.secret, sep, self.authuser = auth.partition(':')
931         elif (colons == 2):
932             # :secret:authuser
933             pre_auth, sep, auth = user_part.partition(':')
934             self.secret, sep, self.authuser = auth.partition(':')
935         elif (colons == 1):
936             # :secret
937             pre_auth, sep, self.secret = user_part.partition(':')
938         elif (colons == 0):
939             # No port, secret, or authuser
940             pre_auth = user_part
941         else:
942             # Invalid setting
943             raise
944
945         pre_domain, sep, self.domain = pre_auth.partition('@')
946         self.peer, sep, post_peer = pre_domain.rpartition('?')
947         transport, sep, self.user = post_peer.rpartition('://')
948
949         self.protocol = transport if transport else 'udp'
950
951     def write(self, pjsip, nmapped):
952         """
953         Write parsed registration data into a section in pjsip.conf
954
955         Most of the data in self will get written to a registration section.
956         However, there will also need to be an auth section created if a
957         secret or authuser is present.
958
959         General mapping of values:
960         A combination of self.host and self.port is server_uri
961         A combination of self.user, self.domain, and self.domainport is
962           client_uri
963         self.expiry is expiration
964         self.extension is contact_user
965         self.protocol will map to one of the mapped transports
966         self.secret and self.authuser will result in a new auth section, and
967           outbound_auth will point to that section.
968         XXX self.peer really doesn't map to anything :(
969         """
970
971         section = 'reg_' + self.host
972
973         set_value('retry_interval', self.retry_interval, section, pjsip,
974                   nmapped, 'registration')
975         set_value('max_retries', self.max_attempts, section, pjsip, nmapped,
976                   'registration')
977         if self.extension:
978             set_value('contact_user', self.extension, section, pjsip, nmapped,
979                       'registration')
980
981         set_value('expiration', self.expiry, section, pjsip, nmapped,
982                   'registration')
983
984         if self.protocol == 'udp':
985             set_value('transport', 'transport-udp', section, pjsip, nmapped,
986                       'registration')
987         elif self.protocol == 'tcp':
988             set_value('transport', 'transport-tcp', section, pjsip, nmapped,
989                       'registration')
990         elif self.protocol == 'tls':
991             set_value('transport', 'transport-tls', section, pjsip, nmapped,
992                       'registration')
993
994         auth_section = 'auth_reg_' + self.host
995
996         if hasattr(self, 'secret') and self.secret:
997             set_value('password', self.secret, auth_section, pjsip, nmapped,
998                       'auth')
999             if hasattr(self, 'authuser'):
1000                 set_value('username', self.authuser or self.user, auth_section,
1001                           pjsip, nmapped, 'auth')
1002             set_value('outbound_auth', auth_section, section, pjsip, nmapped,
1003                       'registration')
1004
1005         client_uri = "sip:%s@" % self.user
1006         if self.domain:
1007             client_uri += self.domain
1008         else:
1009             client_uri += self.host
1010
1011         if hasattr(self, 'domainport') and self.domainport:
1012             client_uri += ":" + self.domainport
1013         elif self.port:
1014             client_uri += ":" + self.port
1015
1016         set_value('client_uri', client_uri, section, pjsip, nmapped,
1017                   'registration')
1018
1019         server_uri = "sip:%s" % self.host
1020         if self.port:
1021             server_uri += ":" + self.port
1022
1023         set_value('server_uri', server_uri, section, pjsip, nmapped,
1024                   'registration')
1025
1026         if self.outbound_proxy:
1027             set_value('outboundproxy', self.outbound_proxy, section, pjsip,
1028                       nmapped, 'registartion')
1029
1030
1031 def map_registrations(sip, pjsip, nmapped):
1032     """
1033     Gathers all necessary outbound registration data in sip.conf and creates
1034     corresponding registration sections in pjsip.conf
1035     """
1036     try:
1037         regs = sip.get('general', 'register')
1038     except LookupError:
1039         return
1040
1041     try:
1042         retry_interval = sip.get('general', 'registertimeout')[0]
1043     except LookupError:
1044         retry_interval = '20'
1045
1046     try:
1047         max_attempts = sip.get('general', 'registerattempts')[0]
1048     except LookupError:
1049         max_attempts = '10'
1050
1051     try:
1052         outbound_proxy = sip.get('general', 'outboundproxy')[0]
1053     except LookupError:
1054         outbound_proxy = ''
1055
1056     for i in regs:
1057         reg = Registration(i, retry_interval, max_attempts, outbound_proxy)
1058         reg.write(pjsip, nmapped)
1059
1060
1061 def map_peer(sip, section, pjsip, nmapped):
1062     """
1063     Map the options from a peer section in sip.conf into the appropriate
1064     sections in pjsip.conf
1065     """
1066     for i in peer_map:
1067         try:
1068             # coming from sip.conf the values should mostly be a list with a
1069             # single value.  In the few cases that they are not a specialized
1070             # function (see merge_value) is used to retrieve the values.
1071             i[1](i[0], sip.get(section, i[0])[0], section, pjsip, nmapped)
1072         except LookupError:
1073             pass  # key not found in sip.conf
1074
1075
1076 def find_non_mapped(sections, nmapped):
1077     """
1078     Determine sip.conf options that were not properly mapped to pjsip.conf
1079     options.
1080     """
1081     for section, sect in sections.iteritems():
1082         try:
1083             # since we are pulling from sip.conf this should always
1084             # be a single value list
1085             sect = sect[0]
1086             # loop through the section and store any values that were not
1087             # mapped
1088             for key in sect.keys(True):
1089                 for i in peer_map:
1090                     if i[0] == key:
1091                         break
1092                 else:
1093                     nmapped(section, key, sect[key])
1094         except LookupError:
1095             pass
1096
1097
1098 def convert(sip, filename, non_mappings, include):
1099     """
1100     Entry point for configuration file conversion. This
1101     function will create a pjsip.conf object and begin to
1102     map specific sections from sip.conf into it.
1103     Returns the new pjsip.conf object once completed
1104     """
1105     pjsip = astconfigparser.MultiOrderedConfigParser()
1106     non_mappings[filename] = astdicts.MultiOrderedDict()
1107     nmapped = non_mapped(non_mappings[filename])
1108     if not include:
1109         # Don't duplicate transport and registration configs
1110         map_transports(sip, pjsip, nmapped)
1111         map_registrations(sip, pjsip, nmapped)
1112     map_auth(sip, pjsip, nmapped)
1113     for section in sip.sections():
1114         if section == 'authentication':
1115             pass
1116         else:
1117             map_peer(sip, section, pjsip, nmapped)
1118
1119     find_non_mapped(sip.defaults(), nmapped)
1120     find_non_mapped(sip.sections(), nmapped)
1121
1122     for key, val in sip.includes().iteritems():
1123         pjsip.add_include(PREFIX + key, convert(val, PREFIX + key,
1124                           non_mappings, True)[0])
1125     return pjsip, non_mappings
1126
1127
1128 def write_pjsip(filename, pjsip, non_mappings):
1129     """
1130     Write pjsip.conf file to disk
1131     """
1132     try:
1133         with open(filename, 'wt') as fp:
1134             fp.write(';--\n')
1135             fp.write(';;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;\n')
1136             fp.write('Non mapped elements start\n')
1137             fp.write(';;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;\n\n')
1138             astconfigparser.write_dicts(fp, non_mappings[filename])
1139             fp.write(';;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;\n')
1140             fp.write('Non mapped elements end\n')
1141             fp.write(';;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;\n')
1142             fp.write('--;\n\n')
1143             # write out include file(s)
1144             pjsip.write(fp)
1145
1146     except IOError:
1147         print "Could not open file ", filename, " for writing"
1148
1149 ###############################################################################
1150
1151
1152 def cli_options():
1153     """
1154     Parse command line options and apply them. If invalid input is given,
1155     print usage information
1156     """
1157     global PREFIX
1158     usage = "usage: %prog [options] [input-file [output-file]]\n\n" \
1159                 "Converts the chan_sip configuration input-file to the chan_pjsip output-file.\n"\
1160         "The input-file defaults to 'sip.conf'.\n" \
1161         "The output-file defaults to 'pjsip.conf'."
1162     parser = optparse.OptionParser(usage=usage)
1163     parser.add_option('-p', '--prefix', dest='prefix', default=PREFIX,
1164                       help='output prefix for include files')
1165
1166     options, args = parser.parse_args()
1167     PREFIX = options.prefix
1168
1169     sip_filename = args[0] if len(args) else 'sip.conf'
1170     pjsip_filename = args[1] if len(args) == 2 else 'pjsip.conf'
1171
1172     return sip_filename, pjsip_filename
1173
1174 if __name__ == "__main__":
1175     sip_filename, pjsip_filename = cli_options()
1176     # configuration parser for sip.conf
1177     sip = astconfigparser.MultiOrderedConfigParser()
1178     print 'Reading', sip_filename
1179     sip.read(sip_filename)
1180     print 'Converting to PJSIP...'
1181     pjsip, non_mappings = convert(sip, pjsip_filename, dict(), False)
1182     print 'Writing', pjsip_filename
1183     write_pjsip(pjsip_filename, pjsip, non_mappings)