diff --git a/sippy/B2BTransforms.py b/sippy/B2BTransforms.py new file mode 100644 index 0000000..dbaad71 --- /dev/null +++ b/sippy/B2BTransforms.py @@ -0,0 +1,54 @@ +# Copyright (c) 2024 Sippy Software, Inc. All rights reserved. +# +# All rights reserved. +# +# Redistribution and use in source and binary forms, with or without modification, +# are permitted provided that the following conditions are met: +# +# 1. Redistributions of source code must retain the above copyright notice, this +# list of conditions and the following disclaimer. +# +# 2. Redistributions in binary form must reproduce the above copyright notice, +# this list of conditions and the following disclaimer in the documentation and/or +# other materials provided with the distribution. +# +# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND +# ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED +# WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE +# DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR +# ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES +# (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; +# LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON +# ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT +# (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS +# SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + +from sippy.SipRequest import SipRequest + +class HDR2Xattrs(): + # Source: https://github.com/sippy/b2bua/pull/38 + # Author: @twmobius + hdr_name:str + def __init__(self, hdr_name:str): + self.hdr_name = hdr_name + + def __call__(req:SipRequest, cc:'CallController'): + hfs = req.getHFs(self.hdr_name) + + if len(hfs) == 0: + return + + extra_attributes = [] + + for header in hfs: + kvPairs = header.body.body.split(';') + for pair in kvPairs: + [key, _, value] = pair.partition("=") + if value != '': + extra_attributes.append((key, value)) + if len(extra_attributes) == 0: + return + if cc.extra_attributes is None: + cc.extra_attributes = extra_attributes + else: + cc.extra_attributes.extend(extra_attributes) diff --git a/sippy/MyConfigParser.py b/sippy/MyConfigParser.py index 3869c4a..9b42338 100644 --- a/sippy/MyConfigParser.py +++ b/sippy/MyConfigParser.py @@ -33,6 +33,7 @@ from configparser import RawConfigParser _boolean_states = RawConfigParser.BOOLEAN_STATES from sippy.SipConf import SipConf +import sippy.B2BTransforms as bts SUPPORTED_OPTIONS = { \ 'acct_enable': ('B', 'enable or disable Radius accounting'), \ @@ -103,6 +104,8 @@ '"host:port:cert_file:key_file", where "cert_file" ' \ '/ "key_file" are paths to the TLS certificate ' \ 'and key file respectively in the X.509 PEM format'), + 'pre_auth_proc': ('S', 'internal routine to be executed before authentication '\ + 'is being processed. E.g. "HDR2Xattrs[X-foo-hdr]."'), \ 'xmpp_b2bua_id': ('I', 'ID passed to the XMPP socket server')} class MyConfigParser(RawConfigParser): @@ -216,6 +219,14 @@ def check_and_set(self, key, value, compat = True): if _value <= 0 or _value > 65535: raise ValueError('sip_port should be in the range 1-65535') self['_sip_port'] = _value + elif key == 'pre_auth_proc': + rparts = value.split('[', 1) + if not len(rparts) == 2 or not value.endswith(']'): + raise ValueError('pre_auth_proc should be in the format `function(argument)`') + fname = rparts[0] + fclass = getattr(bts, fname) + farg = rparts[1][:-1] + self['_pre_auth_proc'] = fclass(farg) self[key] = value def options_help(self): diff --git a/sippy/b2bua_radius.py b/sippy/b2bua_radius.py index aab4c3c..66b7681 100755 --- a/sippy/b2bua_radius.py +++ b/sippy/b2bua_radius.py @@ -132,6 +132,7 @@ class CallController(object): challenge = None req_source: str req_target: SipURL + extra_attributes = None def __init__(self, remote_ip, source, req_source, req_target, global_config, pass_headers): self.id = CallController.id @@ -213,11 +214,12 @@ def recvEvent(self, event, ua): elif auth == None or auth.username == None or len(auth.username) == 0: self.username = self.remote_ip self.auth_proc = self.global_config['_radius_client'].do_auth(self.remote_ip, self.cli, self.cld, \ - self.cId, self.remote_ip, self.rDone) + self.cId, self.remote_ip, self.rDone, extra_attributes=self.extra_attributes) else: self.username = auth.username self.auth_proc = self.global_config['_radius_client'].do_auth(auth.username, self.cli, self.cld, \ - self.cId, self.remote_ip, self.rDone, auth.realm, auth.nonce, auth.uri, auth.response) + self.cId, self.remote_ip, self.rDone, auth.realm, auth.nonce, auth.uri, auth.response, \ + extra_attributes=self.extra_attributes) return if self.state not in (CCStateARComplete, CCStateConnected, CCStateDisconnecting) or self.uaO == None: return @@ -511,6 +513,10 @@ def recvRequest(self, req, sip_t): pass_headers.extend(hfs) req_target = req.getRURI() cc = CallController(remote_ip, source, req_source, req_target, self.global_config, pass_headers) + + if '_pre_auth_proc' in self.global_config: + self.global_config['_pre_auth_proc'](req, cc) + cc.challenge = challenge rval = cc.uaA.recvRequest(req, sip_t) self.ccmap.append(cc) @@ -715,7 +721,7 @@ def main_func(): global_config['_orig_argv'] = sys.argv[:] global_config['_orig_cwd'] = os.getcwd() try: - opts, args = getopt.getopt(sys.argv[1:], 'fDl:p:d:P:L:s:a:t:T:k:m:A:ur:F:R:h:c:M:HC:W:', + opts, args = getopt.getopt(sys.argv[1:], 'fDl:p:d:P:L:s:a:t:T:k:m:A:ur:F:R:h:c:M:HC:W:x:', global_config.get_longopts()) except getopt.GetoptError: usage(global_config) @@ -807,6 +813,9 @@ def main_func(): for a in a.split(','): global_config.check_and_set('pass_header', a) continue + if o == '-x': + global_config.check_and_set('pre_auth_proc', a) + continue if o == '-c': global_config.check_and_set('b2bua_socket', a) continue