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