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