sip_to_pjsip: Add defaultexpiry, maxexpiry, and minexpiry.
[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     ['encrpytion',         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 set_tls_method(val, pjsip, nmapped):
738     """Sets method based on sip.conf tlsclientmethod or sslclientmethod"""
739     set_value('method', val, 'transport-tls', pjsip, nmapped, 'transport')
740
741
742 def create_tls(sip, pjsip, nmapped):
743     """
744     Creates a 'transport-tls' section in pjsip.conf based on the following
745     settings from sip.conf:
746
747     tlsenable (or sslenable)
748     tlsbindaddr (or sslbindaddr)
749     tlsprivatekey (or sslprivatekey)
750     tlscipher (or sslcipher)
751     tlscafile
752     tlscapath (or tlscadir)
753     tlscertfile (or sslcert or tlscert)
754     tlsverifyclient
755     tlsdontverifyserver
756     tlsclientmethod (or sslclientmethod)
757     """
758
759     tls_map = [
760         (['tlsbindaddr', 'sslbindaddr'], set_tls_bindaddr),
761         (['tlsprivatekey', 'sslprivatekey'], set_tls_private_key),
762         (['tlscipher', 'sslcipher'], set_tls_cipher),
763         (['tlscafile'], set_tls_cafile),
764         (['tlsverifyclient'], set_tls_verifyclient),
765         (['tlsdontverifyserver'], set_tls_verifyserver),
766         (['tlsclientmethod', 'sslclientmethod'], set_tls_method)
767     ]
768
769     try:
770         enabled = sip.multi_get('general', ['tlsenable', 'sslenable'])[0]
771     except LookupError:
772         # Not enabled. Don't create a transport
773         return
774
775     if enabled == 'no':
776         return
777
778     set_value('protocol', 'tls', 'transport-tls', pjsip, nmapped, 'transport')
779
780     for i in tls_map:
781         try:
782             i[1](sip.multi_get('general', i[0])[0], pjsip, nmapped)
783         except LookupError:
784             pass
785
786     set_transport_common('transport-tls', pjsip, nmapped)
787     try:
788         extern_addr = sip.multi_get('general', ['externaddr', 'externip',
789                                     'externhost'])[0]
790         host, port = split_hostport(extern_addr)
791         try:
792             tlsport = sip.get('general', 'externtlsport')[0]
793         except:
794             tlsport = port
795         set_value('external_signaling_address', host, 'transport-tls', pjsip,
796                   nmapped, 'transport')
797         if tlsport:
798             set_value('external_signaling_port', tlsport, 'transport-tls',
799                       pjsip, nmapped, 'transport')
800     except LookupError:
801         pass
802
803
804 def map_transports(sip, pjsip, nmapped):
805     """
806     Finds options in sip.conf general section pertaining to
807     transport configuration and creates appropriate transport
808     configuration sections in pjsip.conf.
809
810     sip.conf only allows a single UDP transport, TCP transport,
811     and TLS transport. As such, the mapping into PJSIP can be made
812     consistent by defining three sections:
813
814     transport-udp
815     transport-tcp
816     transport-tls
817
818     To accommodate the default behaviors in sip.conf, we'll need to
819     create the UDP transport first, followed by the TCP and TLS transports.
820     """
821
822     # First create a UDP transport. Even if no bind parameters were provided
823     # in sip.conf, chan_sip would always bind to UDP 0.0.0.0:5060
824     create_udp(sip, pjsip, nmapped)
825
826     # TCP settings may be dependent on UDP settings, so do it second.
827     create_tcp(sip, pjsip, nmapped)
828     create_tls(sip, pjsip, nmapped)
829
830
831 def map_auth(sip, pjsip, nmapped):
832     """
833     Creates auth sections based on entries in the authentication section of
834     sip.conf. pjsip.conf section names consist of "auth_" followed by the name
835     of the realm.
836     """
837     try:
838         auths = sip.get('authentication', 'auth')
839     except LookupError:
840         return
841
842     for i in auths:
843         creds, at, realm = i.partition('@')
844         if not at and not realm:
845             # Invalid. Move on
846             continue
847         user, colon, secret = creds.partition(':')
848         if not secret:
849             user, sharp, md5 = creds.partition('#')
850             if not md5:
851                 #Invalid. move on
852                 continue
853         section = "auth_" + realm
854
855         set_value('realm', realm, section, pjsip, nmapped, 'auth')
856         set_value('username', user, section, pjsip, nmapped, 'auth')
857         if secret:
858             set_value('password', secret, section, pjsip, nmapped, 'auth')
859         else:
860             set_value('md5_cred', md5, section, pjsip, nmapped, 'auth')
861             set_value('auth_type', 'md5', section, pjsip, nmapped, 'auth')
862
863
864 class Registration:
865     """
866     Class for parsing and storing information in a register line in sip.conf.
867     """
868     def __init__(self, line, retry_interval, max_attempts, outbound_proxy):
869         self.retry_interval = retry_interval
870         self.max_attempts = max_attempts
871         self.outbound_proxy = outbound_proxy
872         self.parse(line)
873
874     def parse(self, line):
875         """
876         Initial parsing routine for register lines in sip.conf.
877
878         This splits the line into the part before the host, and the part
879         after the '@' symbol. These two parts are then passed to their
880         own parsing routines
881         """
882
883         # register =>
884         # [peer?][transport://]user[@domain][:secret[:authuser]]@host[:port][/extension][~expiry]
885
886         prehost, at, host_part = line.rpartition('@')
887         if not prehost:
888             raise
889
890         self.parse_host_part(host_part)
891         self.parse_user_part(prehost)
892
893     def parse_host_part(self, host_part):
894         """
895         Parsing routine for the part after the final '@' in a register line.
896         The strategy is to use partition calls to peel away the data starting
897         from the right and working to the left.
898         """
899         pre_expiry, sep, expiry = host_part.partition('~')
900         pre_extension, sep, self.extension = pre_expiry.partition('/')
901         self.host, sep, self.port = pre_extension.partition(':')
902
903         self.expiry = expiry if expiry else '120'
904
905     def parse_user_part(self, user_part):
906         """
907         Parsing routine for the part before the final '@' in a register line.
908         The only mandatory part of this line is the user portion. The strategy
909         here is to start by using partition calls to remove everything to
910         the right of the user, then finish by using rpartition calls to remove
911         everything to the left of the user.
912         """
913         colons = user_part.count(':')
914         if (colons == 3):
915             # :domainport:secret:authuser
916             pre_auth, sep, port_auth = user_part.partition(':')
917             self.domainport, sep, auth = port_auth.partition(':')
918             self.secret, sep, self.authuser = auth.partition(':')
919         elif (colons == 2):
920             # :secret:authuser
921             pre_auth, sep, auth = user_part.partition(':')
922             self.secret, sep, self.authuser = auth.partition(':')
923         elif (colons == 1):
924             # :secret
925             pre_auth, sep, self.secret = user_part.partition(':')
926         elif (colons == 0):
927             # No port, secret, or authuser
928             pre_auth = user_part
929         else:
930             # Invalid setting
931             raise
932
933         pre_domain, sep, self.domain = pre_auth.partition('@')
934         self.peer, sep, post_peer = pre_domain.rpartition('?')
935         transport, sep, self.user = post_peer.rpartition('://')
936
937         self.protocol = transport if transport else 'udp'
938
939     def write(self, pjsip, nmapped):
940         """
941         Write parsed registration data into a section in pjsip.conf
942
943         Most of the data in self will get written to a registration section.
944         However, there will also need to be an auth section created if a
945         secret or authuser is present.
946
947         General mapping of values:
948         A combination of self.host and self.port is server_uri
949         A combination of self.user, self.domain, and self.domainport is
950           client_uri
951         self.expiry is expiration
952         self.extension is contact_user
953         self.protocol will map to one of the mapped transports
954         self.secret and self.authuser will result in a new auth section, and
955           outbound_auth will point to that section.
956         XXX self.peer really doesn't map to anything :(
957         """
958
959         section = 'reg_' + self.host
960
961         set_value('retry_interval', self.retry_interval, section, pjsip,
962                   nmapped, 'registration')
963         set_value('max_retries', self.max_attempts, section, pjsip, nmapped,
964                   'registration')
965         if self.extension:
966             set_value('contact_user', self.extension, section, pjsip, nmapped,
967                       'registration')
968
969         set_value('expiration', self.expiry, section, pjsip, nmapped,
970                   'registration')
971
972         if self.protocol == 'udp':
973             set_value('transport', 'transport-udp', section, pjsip, nmapped,
974                       'registration')
975         elif self.protocol == 'tcp':
976             set_value('transport', 'transport-tcp', section, pjsip, nmapped,
977                       'registration')
978         elif self.protocol == 'tls':
979             set_value('transport', 'transport-tls', section, pjsip, nmapped,
980                       'registration')
981
982         auth_section = 'auth_reg_' + self.host
983
984         if hasattr(self, 'secret') and self.secret:
985             set_value('password', self.secret, auth_section, pjsip, nmapped,
986                       'auth')
987             if hasattr(self, 'authuser'):
988                 set_value('username', self.authuser or self.user, auth_section,
989                           pjsip, nmapped, 'auth')
990             set_value('outbound_auth', auth_section, section, pjsip, nmapped,
991                       'registration')
992
993         client_uri = "sip:%s@" % self.user
994         if self.domain:
995             client_uri += self.domain
996         else:
997             client_uri += self.host
998
999         if hasattr(self, 'domainport') and self.domainport:
1000             client_uri += ":" + self.domainport
1001         elif self.port:
1002             client_uri += ":" + self.port
1003
1004         set_value('client_uri', client_uri, section, pjsip, nmapped,
1005                   'registration')
1006
1007         server_uri = "sip:%s" % self.host
1008         if self.port:
1009             server_uri += ":" + self.port
1010
1011         set_value('server_uri', server_uri, section, pjsip, nmapped,
1012                   'registration')
1013
1014         if self.outbound_proxy:
1015             set_value('outboundproxy', self.outbound_proxy, section, pjsip,
1016                       nmapped, 'registartion')
1017
1018
1019 def map_registrations(sip, pjsip, nmapped):
1020     """
1021     Gathers all necessary outbound registration data in sip.conf and creates
1022     corresponding registration sections in pjsip.conf
1023     """
1024     try:
1025         regs = sip.get('general', 'register')
1026     except LookupError:
1027         return
1028
1029     try:
1030         retry_interval = sip.get('general', 'registertimeout')[0]
1031     except LookupError:
1032         retry_interval = '20'
1033
1034     try:
1035         max_attempts = sip.get('general', 'registerattempts')[0]
1036     except LookupError:
1037         max_attempts = '10'
1038
1039     try:
1040         outbound_proxy = sip.get('general', 'outboundproxy')[0]
1041     except LookupError:
1042         outbound_proxy = ''
1043
1044     for i in regs:
1045         reg = Registration(i, retry_interval, max_attempts, outbound_proxy)
1046         reg.write(pjsip, nmapped)
1047
1048
1049 def map_peer(sip, section, pjsip, nmapped):
1050     """
1051     Map the options from a peer section in sip.conf into the appropriate
1052     sections in pjsip.conf
1053     """
1054     for i in peer_map:
1055         try:
1056             # coming from sip.conf the values should mostly be a list with a
1057             # single value.  In the few cases that they are not a specialized
1058             # function (see merge_value) is used to retrieve the values.
1059             i[1](i[0], sip.get(section, i[0])[0], section, pjsip, nmapped)
1060         except LookupError:
1061             pass  # key not found in sip.conf
1062
1063
1064 def find_non_mapped(sections, nmapped):
1065     """
1066     Determine sip.conf options that were not properly mapped to pjsip.conf
1067     options.
1068     """
1069     for section, sect in sections.iteritems():
1070         try:
1071             # since we are pulling from sip.conf this should always
1072             # be a single value list
1073             sect = sect[0]
1074             # loop through the section and store any values that were not
1075             # mapped
1076             for key in sect.keys(True):
1077                 for i in peer_map:
1078                     if i[0] == key:
1079                         break
1080                 else:
1081                     nmapped(section, key, sect[key])
1082         except LookupError:
1083             pass
1084
1085
1086 def convert(sip, filename, non_mappings, include):
1087     """
1088     Entry point for configuration file conversion. This
1089     function will create a pjsip.conf object and begin to
1090     map specific sections from sip.conf into it.
1091     Returns the new pjsip.conf object once completed
1092     """
1093     pjsip = astconfigparser.MultiOrderedConfigParser()
1094     non_mappings[filename] = astdicts.MultiOrderedDict()
1095     nmapped = non_mapped(non_mappings[filename])
1096     if not include:
1097         # Don't duplicate transport and registration configs
1098         map_transports(sip, pjsip, nmapped)
1099         map_registrations(sip, pjsip, nmapped)
1100     map_auth(sip, pjsip, nmapped)
1101     for section in sip.sections():
1102         if section == 'authentication':
1103             pass
1104         else:
1105             map_peer(sip, section, pjsip, nmapped)
1106
1107     find_non_mapped(sip.defaults(), nmapped)
1108     find_non_mapped(sip.sections(), nmapped)
1109
1110     for key, val in sip.includes().iteritems():
1111         pjsip.add_include(PREFIX + key, convert(val, PREFIX + key,
1112                           non_mappings, True)[0])
1113     return pjsip, non_mappings
1114
1115
1116 def write_pjsip(filename, pjsip, non_mappings):
1117     """
1118     Write pjsip.conf file to disk
1119     """
1120     try:
1121         with open(filename, 'wt') as fp:
1122             fp.write(';--\n')
1123             fp.write(';;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;\n')
1124             fp.write('Non mapped elements start\n')
1125             fp.write(';;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;\n\n')
1126             astconfigparser.write_dicts(fp, non_mappings[filename])
1127             fp.write(';;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;\n')
1128             fp.write('Non mapped elements end\n')
1129             fp.write(';;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;\n')
1130             fp.write('--;\n\n')
1131             # write out include file(s)
1132             pjsip.write(fp)
1133
1134     except IOError:
1135         print "Could not open file ", filename, " for writing"
1136
1137 ###############################################################################
1138
1139
1140 def cli_options():
1141     """
1142     Parse command line options and apply them. If invalid input is given,
1143     print usage information
1144     """
1145     global PREFIX
1146     usage = "usage: %prog [options] [input-file [output-file]]\n\n" \
1147                 "Converts the chan_sip configuration input-file to the chan_pjsip output-file.\n"\
1148         "The input-file defaults to 'sip.conf'.\n" \
1149         "The output-file defaults to 'pjsip.conf'."
1150     parser = optparse.OptionParser(usage=usage)
1151     parser.add_option('-p', '--prefix', dest='prefix', default=PREFIX,
1152                       help='output prefix for include files')
1153
1154     options, args = parser.parse_args()
1155     PREFIX = options.prefix
1156
1157     sip_filename = args[0] if len(args) else 'sip.conf'
1158     pjsip_filename = args[1] if len(args) == 2 else 'pjsip.conf'
1159
1160     return sip_filename, pjsip_filename
1161
1162 if __name__ == "__main__":
1163     sip_filename, pjsip_filename = cli_options()
1164     # configuration parser for sip.conf
1165     sip = astconfigparser.MultiOrderedConfigParser()
1166     print 'Reading', sip_filename
1167     sip.read(sip_filename)
1168     print 'Converting to PJSIP...'
1169     pjsip, non_mappings = convert(sip, pjsip_filename, dict(), False)
1170     print 'Writing', pjsip_filename
1171     write_pjsip(pjsip_filename, pjsip, non_mappings)