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