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