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