6a5ea30647efcb1eef73e2e46184b50a11b0fe29
[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             if hasattr(self, 'authuser'):
999                 set_value('username', self.authuser or self.user, auth_section,
1000                           pjsip, nmapped, 'auth')
1001             set_value('outbound_auth', auth_section, section, pjsip, nmapped,
1002                       'registration')
1003
1004         client_uri = "sip:%s@" % self.user
1005         if self.domain:
1006             client_uri += self.domain
1007         else:
1008             client_uri += self.host
1009
1010         if hasattr(self, 'domainport') and self.domainport:
1011             client_uri += ":" + self.domainport
1012         elif self.port:
1013             client_uri += ":" + self.port
1014
1015         set_value('client_uri', client_uri, section, pjsip, nmapped,
1016                   'registration')
1017
1018         server_uri = "sip:%s" % self.host
1019         if self.port:
1020             server_uri += ":" + self.port
1021
1022         set_value('server_uri', server_uri, section, pjsip, nmapped,
1023                   'registration')
1024
1025         if self.outbound_proxy:
1026             set_value('outboundproxy', self.outbound_proxy, section, pjsip,
1027                       nmapped, 'registartion')
1028
1029
1030 def map_registrations(sip, pjsip, nmapped):
1031     """
1032     Gathers all necessary outbound registration data in sip.conf and creates
1033     corresponding registration sections in pjsip.conf
1034     """
1035     try:
1036         regs = sip.get('general', 'register')
1037     except LookupError:
1038         return
1039
1040     try:
1041         retry_interval = sip.get('general', 'registertimeout')[0]
1042     except LookupError:
1043         retry_interval = '20'
1044
1045     try:
1046         max_attempts = sip.get('general', 'registerattempts')[0]
1047     except LookupError:
1048         max_attempts = '10'
1049
1050     try:
1051         outbound_proxy = sip.get('general', 'outboundproxy')[0]
1052     except LookupError:
1053         outbound_proxy = ''
1054
1055     for i in regs:
1056         reg = Registration(i, retry_interval, max_attempts, outbound_proxy)
1057         reg.write(pjsip, nmapped)
1058
1059
1060 def map_peer(sip, section, pjsip, nmapped):
1061     """
1062     Map the options from a peer section in sip.conf into the appropriate
1063     sections in pjsip.conf
1064     """
1065     for i in peer_map:
1066         try:
1067             # coming from sip.conf the values should mostly be a list with a
1068             # single value.  In the few cases that they are not a specialized
1069             # function (see merge_value) is used to retrieve the values.
1070             i[1](i[0], sip.get(section, i[0])[0], section, pjsip, nmapped)
1071         except LookupError:
1072             pass  # key not found in sip.conf
1073
1074
1075 def find_non_mapped(sections, nmapped):
1076     """
1077     Determine sip.conf options that were not properly mapped to pjsip.conf
1078     options.
1079     """
1080     for section, sect in sections.iteritems():
1081         try:
1082             # since we are pulling from sip.conf this should always
1083             # be a single value list
1084             sect = sect[0]
1085             # loop through the section and store any values that were not
1086             # mapped
1087             for key in sect.keys(True):
1088                 for i in peer_map:
1089                     if i[0] == key:
1090                         break
1091                 else:
1092                     nmapped(section, key, sect[key])
1093         except LookupError:
1094             pass
1095
1096
1097 def map_system(sip, pjsip, nmapped):
1098     section = 'system' # Just a label; you as user can change that
1099     type = 'system' # Not a label, therefore not the same as section
1100
1101     try:
1102         user_agent = sip.get('general', 'useragent')[0]
1103         set_value('user_agent', user_agent, 'global', pjsip, nmapped, 'global')
1104     except LookupError:
1105         pass
1106
1107     try:
1108         timer_t1 = sip.get('general', 'timert1')[0]
1109         set_value('timer_t1', timer_t1, section, pjsip, nmapped, type)
1110     except LookupError:
1111         pass
1112
1113     try:
1114         timer_b = sip.get('general', 'timerb')[0]
1115         set_value('timer_b', timer_b, section, pjsip, nmapped, type)
1116     except LookupError:
1117         pass
1118
1119     try:
1120         compact_headers = sip.get('general', 'compactheaders')[0]
1121         set_value('compact_headers', compact_headers, section, pjsip, nmapped, type)
1122     except LookupError:
1123         pass
1124
1125
1126 def convert(sip, filename, non_mappings, include):
1127     """
1128     Entry point for configuration file conversion. This
1129     function will create a pjsip.conf object and begin to
1130     map specific sections from sip.conf into it.
1131     Returns the new pjsip.conf object once completed
1132     """
1133     pjsip = astconfigparser.MultiOrderedConfigParser()
1134     non_mappings[filename] = astdicts.MultiOrderedDict()
1135     nmapped = non_mapped(non_mappings[filename])
1136     if not include:
1137         # Don't duplicate transport and registration configs
1138         map_system(sip, pjsip, nmapped)
1139         map_transports(sip, pjsip, nmapped)
1140         map_registrations(sip, pjsip, nmapped)
1141     map_auth(sip, pjsip, nmapped)
1142     for section in sip.sections():
1143         if section == 'authentication':
1144             pass
1145         else:
1146             map_peer(sip, section, pjsip, nmapped)
1147
1148     find_non_mapped(sip.defaults(), nmapped)
1149     find_non_mapped(sip.sections(), nmapped)
1150
1151     for key, val in sip.includes().iteritems():
1152         pjsip.add_include(PREFIX + key, convert(val, PREFIX + key,
1153                           non_mappings, True)[0])
1154     return pjsip, non_mappings
1155
1156
1157 def write_pjsip(filename, pjsip, non_mappings):
1158     """
1159     Write pjsip.conf file to disk
1160     """
1161     try:
1162         with open(filename, 'wt') as fp:
1163             fp.write(';--\n')
1164             fp.write(';;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;\n')
1165             fp.write('Non mapped elements start\n')
1166             fp.write(';;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;\n\n')
1167             astconfigparser.write_dicts(fp, non_mappings[filename])
1168             fp.write(';;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;\n')
1169             fp.write('Non mapped elements end\n')
1170             fp.write(';;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;\n')
1171             fp.write('--;\n\n')
1172             # write out include file(s)
1173             pjsip.write(fp)
1174
1175     except IOError:
1176         print "Could not open file ", filename, " for writing"
1177
1178 ###############################################################################
1179
1180
1181 def cli_options():
1182     """
1183     Parse command line options and apply them. If invalid input is given,
1184     print usage information
1185     """
1186     global PREFIX
1187     usage = "usage: %prog [options] [input-file [output-file]]\n\n" \
1188                 "Converts the chan_sip configuration input-file to the chan_pjsip output-file.\n"\
1189         "The input-file defaults to 'sip.conf'.\n" \
1190         "The output-file defaults to 'pjsip.conf'."
1191     parser = optparse.OptionParser(usage=usage)
1192     parser.add_option('-p', '--prefix', dest='prefix', default=PREFIX,
1193                       help='output prefix for include files')
1194
1195     options, args = parser.parse_args()
1196     PREFIX = options.prefix
1197
1198     sip_filename = args[0] if len(args) else 'sip.conf'
1199     pjsip_filename = args[1] if len(args) == 2 else 'pjsip.conf'
1200
1201     return sip_filename, pjsip_filename
1202
1203 if __name__ == "__main__":
1204     sip_filename, pjsip_filename = cli_options()
1205     # configuration parser for sip.conf
1206     sip = astconfigparser.MultiOrderedConfigParser()
1207     print 'Reading', sip_filename
1208     sip.read(sip_filename)
1209     print 'Converting to PJSIP...'
1210     pjsip, non_mappings = convert(sip, pjsip_filename, dict(), False)
1211     print 'Writing', pjsip_filename
1212     write_pjsip(pjsip_filename, pjsip, non_mappings)