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