sippy 2.1.0__tar.gz → 2.2.0__tar.gz
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- {sippy-2.1.0/sippy.egg-info → sippy-2.2.0}/PKG-INFO +11 -3
- {sippy-2.1.0 → sippy-2.2.0}/README.md +9 -2
- sippy-2.2.0/pyproject.toml +3 -0
- {sippy-2.1.0 → sippy-2.2.0}/requirements.txt +1 -0
- {sippy-2.1.0 → sippy-2.2.0}/setup.py +1 -1
- {sippy-2.1.0 → sippy-2.2.0}/sippy/B2BRoute.py +15 -2
- sippy-2.2.0/sippy/B2BTransforms.py +99 -0
- {sippy-2.1.0 → sippy-2.2.0}/sippy/CLIManager.py +9 -8
- {sippy-2.1.0 → sippy-2.2.0}/sippy/Core/EventDispatcher.py +0 -5
- {sippy-2.1.0 → sippy-2.2.0}/sippy/Core/Exceptions.py +1 -1
- {sippy-2.1.0 → sippy-2.2.0}/sippy/Exceptions/RtpProxyError.py +3 -0
- {sippy-2.1.0 → sippy-2.2.0}/sippy/Exceptions/SipParseError.py +7 -3
- {sippy-2.1.0 → sippy-2.2.0}/sippy/External_command.py +1 -1
- {sippy-2.1.0 → sippy-2.2.0}/sippy/MsgBody.py +2 -2
- {sippy-2.1.0 → sippy-2.2.0}/sippy/MultipartMixBody.py +3 -3
- {sippy-2.1.0 → sippy-2.2.0}/sippy/MyConfigParser.py +12 -1
- sippy-2.2.0/sippy/Network_server.py +150 -0
- {sippy-2.1.0 → sippy-2.2.0}/sippy/RadiusAccounting.py +4 -0
- {sippy-2.1.0 → sippy-2.2.0}/sippy/SdpBody.py +14 -14
- {sippy-2.1.0 → sippy-2.2.0}/sippy/SdpConnecton.py +2 -2
- {sippy-2.1.0 → sippy-2.2.0}/sippy/SdpGeneric.py +1 -1
- {sippy-2.1.0 → sippy-2.2.0}/sippy/SdpMedia.py +2 -2
- {sippy-2.1.0 → sippy-2.2.0}/sippy/SdpMediaDescription.py +8 -2
- {sippy-2.1.0 → sippy-2.2.0}/sippy/SdpOrigin.py +3 -2
- {sippy-2.1.0 → sippy-2.2.0}/sippy/Security/SipNonce.py +2 -2
- {sippy-2.1.0 → sippy-2.2.0}/sippy/SipAddress.py +2 -2
- {sippy-2.1.0 → sippy-2.2.0}/sippy/SipAddressHF.py +3 -3
- {sippy-2.1.0 → sippy-2.2.0}/sippy/SipAllow.py +1 -1
- {sippy-2.1.0 → sippy-2.2.0}/sippy/SipAuthorization.py +1 -1
- {sippy-2.1.0 → sippy-2.2.0}/sippy/SipCSeq.py +1 -1
- {sippy-2.1.0 → sippy-2.2.0}/sippy/SipCallId.py +3 -4
- {sippy-2.1.0 → sippy-2.2.0}/sippy/SipCiscoGUID.py +1 -1
- {sippy-2.1.0 → sippy-2.2.0}/sippy/SipConf.py +19 -0
- {sippy-2.1.0 → sippy-2.2.0}/sippy/SipContact.py +4 -2
- {sippy-2.1.0 → sippy-2.2.0}/sippy/SipFrom.py +3 -2
- {sippy-2.1.0 → sippy-2.2.0}/sippy/SipGenericHF.py +1 -1
- {sippy-2.1.0 → sippy-2.2.0}/sippy/SipHeader.py +5 -4
- {sippy-2.1.0 → sippy-2.2.0}/sippy/SipLogger.py +1 -1
- {sippy-2.1.0 → sippy-2.2.0}/sippy/SipMsg.py +14 -11
- {sippy-2.1.0 → sippy-2.2.0}/sippy/SipNumericHF.py +3 -3
- sippy-2.2.0/sippy/SipPath.py +37 -0
- {sippy-2.1.0 → sippy-2.2.0}/sippy/SipRAck.py +1 -1
- {sippy-2.1.0 → sippy-2.2.0}/sippy/SipReason.py +21 -5
- {sippy-2.1.0 → sippy-2.2.0}/sippy/SipRecordRoute.py +3 -0
- {sippy-2.1.0 → sippy-2.2.0}/sippy/SipReplaces.py +1 -1
- {sippy-2.1.0 → sippy-2.2.0}/sippy/SipRequest.py +5 -7
- {sippy-2.1.0 → sippy-2.2.0}/sippy/SipResponse.py +2 -2
- {sippy-2.1.0 → sippy-2.2.0}/sippy/SipRoute.py +1 -0
- {sippy-2.1.0 → sippy-2.2.0}/sippy/SipServer.py +1 -1
- {sippy-2.1.0 → sippy-2.2.0}/sippy/SipSupported.py +1 -1
- {sippy-2.1.0 → sippy-2.2.0}/sippy/SipTransactionManager.py +69 -36
- {sippy-2.1.0 → sippy-2.2.0}/sippy/SipURL.py +29 -10
- {sippy-2.1.0 → sippy-2.2.0}/sippy/SipVia.py +37 -25
- {sippy-2.1.0 → sippy-2.2.0}/sippy/SipWWWAuthenticate.py +2 -2
- {sippy-2.1.0 → sippy-2.2.0}/sippy/SipWarning.py +1 -1
- {sippy-2.1.0 → sippy-2.2.0}/sippy/StatefulProxy.py +23 -1
- {sippy-2.1.0 → sippy-2.2.0}/sippy/Time/MonoTime.py +0 -11
- {sippy-2.1.0 → sippy-2.2.0}/sippy/UA.py +29 -12
- {sippy-2.1.0 → sippy-2.2.0}/sippy/UaStateConnected.py +19 -17
- {sippy-2.1.0 → sippy-2.2.0}/sippy/UacStateCancelling.py +3 -3
- {sippy-2.1.0 → sippy-2.2.0}/sippy/UacStateIdle.py +10 -7
- {sippy-2.1.0 → sippy-2.2.0}/sippy/UacStateRinging.py +16 -55
- {sippy-2.1.0 → sippy-2.2.0}/sippy/UacStateTrying.py +60 -55
- {sippy-2.1.0 → sippy-2.2.0}/sippy/UacStateUpdating.py +5 -5
- {sippy-2.1.0 → sippy-2.2.0}/sippy/UasStateIdle.py +9 -9
- {sippy-2.1.0 → sippy-2.2.0}/sippy/UasStateRinging.py +2 -2
- {sippy-2.1.0 → sippy-2.2.0}/sippy/UasStateTrying.py +2 -2
- {sippy-2.1.0 → sippy-2.2.0}/sippy/UasStateUpdating.py +2 -2
- {sippy-2.1.0 → sippy-2.2.0}/sippy/Udp_server.py +73 -116
- sippy-2.2.0/sippy/Wss_server.py +158 -0
- {sippy-2.1.0 → sippy-2.2.0}/sippy/XMPP_server.py +12 -12
- {sippy-2.1.0 → sippy-2.2.0}/sippy/b2bua_radius.py +132 -59
- {sippy-2.1.0 → sippy-2.2.0/sippy.egg-info}/PKG-INFO +11 -3
- {sippy-2.1.0 → sippy-2.2.0}/sippy.egg-info/SOURCES.txt +9 -8
- {sippy-2.1.0 → sippy-2.2.0}/sippy.egg-info/requires.txt +1 -0
- sippy-2.2.0/tests/test_B2BRoute.py +27 -0
- sippy-2.2.0/tests/test_B2BTransforms.py +48 -0
- {sippy-2.1.0 → sippy-2.2.0}/tests/test_SdbBody.py +31 -25
- sippy-2.2.0/tests/test_Udp_server.py +17 -0
- sippy-2.1.0/sippy/Rtp_proxy_client.py +0 -279
- sippy-2.1.0/sippy/Rtp_proxy_client_local.py +0 -53
- sippy-2.1.0/sippy/Rtp_proxy_client_net.py +0 -42
- sippy-2.1.0/sippy/Rtp_proxy_client_stream.py +0 -226
- sippy-2.1.0/sippy/Rtp_proxy_client_udp.py +0 -225
- sippy-2.1.0/sippy/Rtp_proxy_cmd.py +0 -224
- sippy-2.1.0/sippy/Rtp_proxy_session.py +0 -573
- {sippy-2.1.0 → sippy-2.2.0}/LICENSE +0 -0
- {sippy-2.1.0 → sippy-2.2.0}/MANIFEST.in +0 -0
- {sippy-2.1.0 → sippy-2.2.0}/setup.cfg +0 -0
- {sippy-2.1.0 → sippy-2.2.0}/sippy/CCEvents.py +0 -0
- {sippy-2.1.0 → sippy-2.2.0}/sippy/Core/__init__.py +0 -0
- {sippy-2.1.0 → sippy-2.2.0}/sippy/ESipHeaderCSV.py +0 -0
- {sippy-2.1.0 → sippy-2.2.0}/sippy/ESipHeaderIgnore.py +0 -0
- {sippy-2.1.0 → sippy-2.2.0}/sippy/Exceptions/__init__.py +0 -0
- {sippy-2.1.0 → sippy-2.2.0}/sippy/FakeAccounting.py +0 -0
- {sippy-2.1.0 → sippy-2.2.0}/sippy/Math/__init__.py +0 -0
- {sippy-2.1.0 → sippy-2.2.0}/sippy/Math/recfilter.py +0 -0
- {sippy-2.1.0 → sippy-2.2.0}/sippy/RadiusAuthorisation.py +0 -0
- {sippy-2.1.0 → sippy-2.2.0}/sippy/Radius_client.py +0 -0
- {sippy-2.1.0 → sippy-2.2.0}/sippy/Security/__init__.py +0 -0
- {sippy-2.1.0 → sippy-2.2.0}/sippy/Signal.py +0 -0
- {sippy-2.1.0 → sippy-2.2.0}/sippy/SipAlso.py +0 -0
- {sippy-2.1.0 → sippy-2.2.0}/sippy/SipCCDiversion.py +0 -0
- {sippy-2.1.0 → sippy-2.2.0}/sippy/SipContentLength.py +0 -0
- {sippy-2.1.0 → sippy-2.2.0}/sippy/SipContentType.py +0 -0
- {sippy-2.1.0 → sippy-2.2.0}/sippy/SipDiversion.py +0 -0
- {sippy-2.1.0 → sippy-2.2.0}/sippy/SipExpires.py +0 -0
- {sippy-2.1.0 → sippy-2.2.0}/sippy/SipMaxForwards.py +0 -0
- {sippy-2.1.0 → sippy-2.2.0}/sippy/SipPAssertedIdentity.py +0 -0
- {sippy-2.1.0 → sippy-2.2.0}/sippy/SipProxyAuthenticate.py +0 -0
- {sippy-2.1.0 → sippy-2.2.0}/sippy/SipProxyAuthorization.py +0 -0
- {sippy-2.1.0 → sippy-2.2.0}/sippy/SipRSeq.py +0 -0
- {sippy-2.1.0 → sippy-2.2.0}/sippy/SipReferTo.py +0 -0
- {sippy-2.1.0 → sippy-2.2.0}/sippy/SipReferredBy.py +0 -0
- {sippy-2.1.0 → sippy-2.2.0}/sippy/SipRegistrationAgent.py +0 -0
- {sippy-2.1.0 → sippy-2.2.0}/sippy/SipTo.py +0 -0
- {sippy-2.1.0 → sippy-2.2.0}/sippy/SipUserAgent.py +0 -0
- {sippy-2.1.0 → sippy-2.2.0}/sippy/Time/Timeout.py +0 -0
- {sippy-2.1.0 → sippy-2.2.0}/sippy/Time/__init__.py +0 -0
- {sippy-2.1.0 → sippy-2.2.0}/sippy/Time/clock_dtime.py +0 -0
- {sippy-2.1.0 → sippy-2.2.0}/sippy/UaStateDead.py +0 -0
- {sippy-2.1.0 → sippy-2.2.0}/sippy/UaStateDisconnected.py +0 -0
- {sippy-2.1.0 → sippy-2.2.0}/sippy/UaStateFailed.py +0 -0
- {sippy-2.1.0 → sippy-2.2.0}/sippy/UaStateGeneric.py +0 -0
- {sippy-2.1.0 → sippy-2.2.0}/sippy/__init__.py +0 -0
- {sippy-2.1.0 → sippy-2.2.0}/sippy/b2bua_simple.py +0 -0
- {sippy-2.1.0 → sippy-2.2.0}/sippy/dictionary +0 -0
- {sippy-2.1.0 → sippy-2.2.0}/sippy/misc.py +0 -0
- {sippy-2.1.0 → sippy-2.2.0}/sippy.egg-info/dependency_links.txt +0 -0
- {sippy-2.1.0 → sippy-2.2.0}/sippy.egg-info/entry_points.txt +0 -0
- {sippy-2.1.0 → sippy-2.2.0}/sippy.egg-info/top_level.txt +0 -0
- {sippy-2.1.0 → sippy-2.2.0}/tests/__init__.py +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.1
|
|
2
2
|
Name: sippy
|
|
3
|
-
Version: 2.
|
|
3
|
+
Version: 2.2.0
|
|
4
4
|
Summary: RFC3261 SIP Stack and Back-to-Back User Agent (B2BUA)
|
|
5
5
|
Home-page: http://www.b2bua.org/
|
|
6
6
|
Author: Sippy Software, Inc.
|
|
@@ -14,6 +14,7 @@ Description-Content-Type: text/markdown
|
|
|
14
14
|
License-File: LICENSE
|
|
15
15
|
Requires-Dist: ElPeriodic>=1.1
|
|
16
16
|
Requires-Dist: pycryptodome
|
|
17
|
+
Requires-Dist: websockets
|
|
17
18
|
|
|
18
19
|
[](https://github.com/sippy/b2bua/actions?query=branch%3Amaster++)
|
|
19
20
|
|
|
@@ -33,6 +34,13 @@ Sippy B2BUA doesn't perform any media relaying or processing, therefore it
|
|
|
33
34
|
doesn't introduce any additional packet loss, delay or jitter into the media
|
|
34
35
|
path.
|
|
35
36
|
|
|
37
|
+
## News
|
|
38
|
+
|
|
39
|
+
- Support for WebRTC clients has been added via RFC7118-compatible secure
|
|
40
|
+
websocket (`wss`) interface. Combined with the functionality provided
|
|
41
|
+
by the RTPProxy's `dtls_gw` and `ice_lite` modules allows building
|
|
42
|
+
high-capacity WebRTC<->SIP gateways.
|
|
43
|
+
|
|
36
44
|
## Features
|
|
37
45
|
|
|
38
46
|
- 5,000-10,000 simultaneous sessions per server
|
|
@@ -66,8 +74,8 @@ path.
|
|
|
66
74
|
To get started, you can use the b2bua_simple implementation. The following
|
|
67
75
|
example will cause the b2bua run in the foreground so you can see the SIP
|
|
68
76
|
messaging. If you make a call to the IP address of your host machine, the b2bua
|
|
69
|
-
will recieve the call on its
|
|
70
|
-
|
|
77
|
+
will recieve the call on its UAS side, and it will send a new call leg out its
|
|
78
|
+
UAC side to the IP address 192.168.1.1. It is expected that 192.168.1.1 is some
|
|
71
79
|
sort of SIP switch or termination gateway.
|
|
72
80
|
|
|
73
81
|
`b2bua_simple -f -n 192.168.1.1`
|
|
@@ -16,6 +16,13 @@ Sippy B2BUA doesn't perform any media relaying or processing, therefore it
|
|
|
16
16
|
doesn't introduce any additional packet loss, delay or jitter into the media
|
|
17
17
|
path.
|
|
18
18
|
|
|
19
|
+
## News
|
|
20
|
+
|
|
21
|
+
- Support for WebRTC clients has been added via RFC7118-compatible secure
|
|
22
|
+
websocket (`wss`) interface. Combined with the functionality provided
|
|
23
|
+
by the RTPProxy's `dtls_gw` and `ice_lite` modules allows building
|
|
24
|
+
high-capacity WebRTC<->SIP gateways.
|
|
25
|
+
|
|
19
26
|
## Features
|
|
20
27
|
|
|
21
28
|
- 5,000-10,000 simultaneous sessions per server
|
|
@@ -49,8 +56,8 @@ path.
|
|
|
49
56
|
To get started, you can use the b2bua_simple implementation. The following
|
|
50
57
|
example will cause the b2bua run in the foreground so you can see the SIP
|
|
51
58
|
messaging. If you make a call to the IP address of your host machine, the b2bua
|
|
52
|
-
will recieve the call on its
|
|
53
|
-
|
|
59
|
+
will recieve the call on its UAS side, and it will send a new call leg out its
|
|
60
|
+
UAC side to the IP address 192.168.1.1. It is expected that 192.168.1.1 is some
|
|
54
61
|
sort of SIP switch or termination gateway.
|
|
55
62
|
|
|
56
63
|
`b2bua_simple -f -n 192.168.1.1`
|
|
@@ -26,6 +26,7 @@
|
|
|
26
26
|
|
|
27
27
|
from sippy.SipHeader import SipHeader
|
|
28
28
|
from sippy.SipConf import SipConf
|
|
29
|
+
from sippy.B2BTransforms import getTransProc
|
|
29
30
|
|
|
30
31
|
try:
|
|
31
32
|
from urllib import unquote
|
|
@@ -33,6 +34,11 @@ except ImportError:
|
|
|
33
34
|
from urllib.parse import unquote
|
|
34
35
|
from socket import getaddrinfo, SOCK_STREAM, AF_INET, AF_INET6
|
|
35
36
|
|
|
37
|
+
SRC_WSS = '[[WSS]]'
|
|
38
|
+
SRC_PROXY = '[[PROXY]]'
|
|
39
|
+
DST_SIP_UA = 'sip-ua'
|
|
40
|
+
DST_WSS_UA = 'wss-ua'
|
|
41
|
+
|
|
36
42
|
class B2BRoute(object):
|
|
37
43
|
rnum = None
|
|
38
44
|
addrinfo = None
|
|
@@ -48,6 +54,7 @@ class B2BRoute(object):
|
|
|
48
54
|
forward_on_fail = False
|
|
49
55
|
user = None
|
|
50
56
|
passw = None
|
|
57
|
+
pass_auth = True
|
|
51
58
|
cli = None
|
|
52
59
|
cli_set = False
|
|
53
60
|
params = None
|
|
@@ -70,6 +77,7 @@ class B2BRoute(object):
|
|
|
70
77
|
self.forward_on_fail = cself.forward_on_fail
|
|
71
78
|
self.user = cself.user
|
|
72
79
|
self.passw = cself.passw
|
|
80
|
+
self.pass_auth = cself.pass_auth
|
|
73
81
|
self.cli = cself.cli
|
|
74
82
|
self.cli_set = cself.cli_set
|
|
75
83
|
self.params = dict(cself.params)
|
|
@@ -105,7 +113,8 @@ class B2BRoute(object):
|
|
|
105
113
|
port = SipConf.default_port
|
|
106
114
|
else:
|
|
107
115
|
port = int(hostport[1])
|
|
108
|
-
|
|
116
|
+
if not hostport[0] in (DST_SIP_UA, DST_WSS_UA):
|
|
117
|
+
self.ainfo = getaddrinfo(hostport[0], port, af, SOCK_STREAM)
|
|
109
118
|
self.params = {}
|
|
110
119
|
extra_headers = []
|
|
111
120
|
for a, v in [x.split('=', 1) for x in route[1:]]:
|
|
@@ -131,7 +140,9 @@ class B2BRoute(object):
|
|
|
131
140
|
elif a == 'forward_on_fail':
|
|
132
141
|
self.forward_on_fail = True
|
|
133
142
|
elif a == 'auth':
|
|
134
|
-
self.
|
|
143
|
+
self.pass_auth = False
|
|
144
|
+
if v != 'nopass':
|
|
145
|
+
self.user, self.passw = v.split(':', 1)
|
|
135
146
|
elif a == 'cli':
|
|
136
147
|
self.cli = v
|
|
137
148
|
if len(self.cli) == 0:
|
|
@@ -153,6 +164,8 @@ class B2BRoute(object):
|
|
|
153
164
|
self.params['outbound_proxy'] = (v, 5060)
|
|
154
165
|
else:
|
|
155
166
|
self.params['outbound_proxy'] = (host_port[0], int(host_port[1]))
|
|
167
|
+
elif a == 'po_proc':
|
|
168
|
+
self.params['po_proc'] = getTransProc(v)
|
|
156
169
|
else:
|
|
157
170
|
self.params[a] = v
|
|
158
171
|
if len(extra_headers) > 0:
|
|
@@ -0,0 +1,99 @@
|
|
|
1
|
+
# Copyright (c) 2024 Sippy Software, Inc. All rights reserved.
|
|
2
|
+
#
|
|
3
|
+
# All rights reserved.
|
|
4
|
+
#
|
|
5
|
+
# Redistribution and use in source and binary forms, with or without modification,
|
|
6
|
+
# are permitted provided that the following conditions are met:
|
|
7
|
+
#
|
|
8
|
+
# 1. Redistributions of source code must retain the above copyright notice, this
|
|
9
|
+
# list of conditions and the following disclaimer.
|
|
10
|
+
#
|
|
11
|
+
# 2. Redistributions in binary form must reproduce the above copyright notice,
|
|
12
|
+
# this list of conditions and the following disclaimer in the documentation and/or
|
|
13
|
+
# other materials provided with the distribution.
|
|
14
|
+
#
|
|
15
|
+
# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND
|
|
16
|
+
# ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
|
|
17
|
+
# WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
|
|
18
|
+
# DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR
|
|
19
|
+
# ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES
|
|
20
|
+
# (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;
|
|
21
|
+
# LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON
|
|
22
|
+
# ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
|
|
23
|
+
# (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
|
|
24
|
+
# SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
|
|
25
|
+
|
|
26
|
+
import sys
|
|
27
|
+
|
|
28
|
+
from sippy.SipRequest import SipRequest
|
|
29
|
+
from sippy.CCEvents import CCEventTry
|
|
30
|
+
|
|
31
|
+
class HDR2Xattrs():
|
|
32
|
+
# Source: https://github.com/sippy/b2bua/pull/38
|
|
33
|
+
# Author: @twmobius
|
|
34
|
+
hdr_name:str
|
|
35
|
+
def __init__(self, hdr_name:str):
|
|
36
|
+
self.hdr_name = hdr_name
|
|
37
|
+
|
|
38
|
+
def __call__(self, cc:'CallController', req:SipRequest):
|
|
39
|
+
hfs = req.getHFs(self.hdr_name)
|
|
40
|
+
|
|
41
|
+
if len(hfs) == 0:
|
|
42
|
+
return
|
|
43
|
+
|
|
44
|
+
extra_attributes = []
|
|
45
|
+
|
|
46
|
+
for header in hfs:
|
|
47
|
+
kvPairs = header.body.body.split(';')
|
|
48
|
+
for pair in kvPairs:
|
|
49
|
+
[key, _, value] = pair.partition("=")
|
|
50
|
+
if value != '':
|
|
51
|
+
extra_attributes.append((key, value))
|
|
52
|
+
if len(extra_attributes) == 0:
|
|
53
|
+
return
|
|
54
|
+
if cc.extra_attributes is None:
|
|
55
|
+
cc.extra_attributes = extra_attributes
|
|
56
|
+
else:
|
|
57
|
+
cc.extra_attributes.extend(extra_attributes)
|
|
58
|
+
|
|
59
|
+
class VAL2Xattrs():
|
|
60
|
+
# Source: https://github.com/sippy/b2bua/pull/39
|
|
61
|
+
# Author: @twmobius
|
|
62
|
+
doO: bool = True
|
|
63
|
+
doA: bool = True
|
|
64
|
+
radius_parameters: list
|
|
65
|
+
def __init__(self, v:str):
|
|
66
|
+
radius_parameters = []
|
|
67
|
+
pairs = v.split(',')
|
|
68
|
+
for pair in pairs:
|
|
69
|
+
[key, _, value] = pair.partition("=")
|
|
70
|
+
if value == '': raise ValueError(f'{v}: comma-separated list of key=value pairs is expected')
|
|
71
|
+
radius_parameters.append((key, value))
|
|
72
|
+
self.radius_parameters = radius_parameters
|
|
73
|
+
|
|
74
|
+
def __call__(self, cc:'CallController', _:CCEventTry):
|
|
75
|
+
if self.doO and cc.acctO is not None:
|
|
76
|
+
cc.acctO.addAttributes(self.radius_parameters)
|
|
77
|
+
if self.doA and cc.acctA is not None:
|
|
78
|
+
cc.acctA.addAttributes(self.radius_parameters)
|
|
79
|
+
|
|
80
|
+
class Nop():
|
|
81
|
+
def __init__(self, v:str): pass
|
|
82
|
+
def __call__(self, *a, **kwa): pass
|
|
83
|
+
|
|
84
|
+
class VAL2XattrsA(VAL2Xattrs): doO = False
|
|
85
|
+
class VAL2XattrsO(VAL2Xattrs): doA = False
|
|
86
|
+
|
|
87
|
+
def getTransProc(value:str):
|
|
88
|
+
rparts = value.split('[', 1)
|
|
89
|
+
if not len(rparts) == 2 or not value.endswith(']'):
|
|
90
|
+
raise ValueError(f'getTransProc: `{value}` should be in the format `function[argument]`')
|
|
91
|
+
fname = rparts[0]
|
|
92
|
+
bts = sys.modules[__name__]
|
|
93
|
+
fclass = getattr(bts, fname)
|
|
94
|
+
farg = rparts[1][:-1]
|
|
95
|
+
return fclass(farg)
|
|
96
|
+
|
|
97
|
+
if __name__ == '__main__':
|
|
98
|
+
for t in ('HDR2Xattrs[X-foo-hdr]', 'VAL2Xattrs[foo=bar,baz=xxx]', 'VAL2XattrsA[foo=bar,baz=xxx]', 'VAL2XattrsO[foo=bar,baz=xxx]'):
|
|
99
|
+
p = getTransProc(t)
|
|
@@ -43,6 +43,7 @@ except NameError:
|
|
|
43
43
|
_uobj = bytes
|
|
44
44
|
|
|
45
45
|
class _Acceptor(Thread):
|
|
46
|
+
daemon = True
|
|
46
47
|
clicm = None
|
|
47
48
|
pollobj = None
|
|
48
49
|
fileno = None
|
|
@@ -53,7 +54,6 @@ class _Acceptor(Thread):
|
|
|
53
54
|
self.pollobj = poll()
|
|
54
55
|
self.fileno = self.clicm.serversock.fileno()
|
|
55
56
|
self.pollobj.register(self.fileno, POLLIN)
|
|
56
|
-
self.setDaemon(True)
|
|
57
57
|
self.start()
|
|
58
58
|
|
|
59
59
|
def run(self):
|
|
@@ -85,7 +85,7 @@ class _Acceptor(Thread):
|
|
|
85
85
|
class CLIConnectionManager(object):
|
|
86
86
|
command_cb = None
|
|
87
87
|
tcp = False
|
|
88
|
-
accept_list = None
|
|
88
|
+
accept_list: list = None
|
|
89
89
|
serversock = None
|
|
90
90
|
atr = None
|
|
91
91
|
|
|
@@ -120,9 +120,10 @@ class CLIConnectionManager(object):
|
|
|
120
120
|
|
|
121
121
|
def handle_accept(self, conn, address):
|
|
122
122
|
#print(self.handle_accept)
|
|
123
|
-
if self.tcp and self.accept_list
|
|
124
|
-
|
|
125
|
-
|
|
123
|
+
if self.tcp and self.accept_list is not None:
|
|
124
|
+
if address[0] not in self.accept_list: # pylint: disable=unsupported-membership-test
|
|
125
|
+
conn.close()
|
|
126
|
+
return
|
|
126
127
|
try:
|
|
127
128
|
cm = CLIManager(conn, self.command_cb, address)
|
|
128
129
|
except Exception as e:
|
|
@@ -136,6 +137,7 @@ class CLIConnectionManager(object):
|
|
|
136
137
|
self.atr.join()
|
|
137
138
|
|
|
138
139
|
class _CLIManager_w(Thread):
|
|
140
|
+
daemon = True
|
|
139
141
|
clim = None
|
|
140
142
|
wbuffer = None
|
|
141
143
|
w_available = None
|
|
@@ -148,7 +150,6 @@ class _CLIManager_w(Thread):
|
|
|
148
150
|
self.clim = clim
|
|
149
151
|
self.w_available = Condition()
|
|
150
152
|
self.wbuffer = bytes()
|
|
151
|
-
self.setDaemon(True)
|
|
152
153
|
self.start()
|
|
153
154
|
|
|
154
155
|
def run(self):
|
|
@@ -195,6 +196,7 @@ class _CLIManager_w(Thread):
|
|
|
195
196
|
self.w_available.release()
|
|
196
197
|
|
|
197
198
|
class _CLIManager_r(Thread):
|
|
199
|
+
daemon = True
|
|
198
200
|
clim = None
|
|
199
201
|
|
|
200
202
|
def __init__(self, clientsock, clim):
|
|
@@ -202,7 +204,6 @@ class _CLIManager_r(Thread):
|
|
|
202
204
|
Thread.__init__(self)
|
|
203
205
|
self.clientsock = clientsock
|
|
204
206
|
self.clim = clim
|
|
205
|
-
self.setDaemon(True)
|
|
206
207
|
self.start()
|
|
207
208
|
|
|
208
209
|
def run(self):
|
|
@@ -274,7 +275,7 @@ class CLIManager(object):
|
|
|
274
275
|
try:
|
|
275
276
|
self.clientsock.shutdown(socket.SHUT_RDWR)
|
|
276
277
|
except Exception as e:
|
|
277
|
-
if not isinstance(e, socket.error) or e.errno != ENOTCONN:
|
|
278
|
+
if not isinstance(e, socket.error) or e.errno != ENOTCONN: # pylint: disable=no-member
|
|
278
279
|
dump_exception('self.clientsock.shutdown(socket.SHUT_RDWR)')
|
|
279
280
|
self.clientsock.close()
|
|
280
281
|
self.wthr = None
|
|
@@ -34,7 +34,7 @@ class StdException(Exception):
|
|
|
34
34
|
|
|
35
35
|
def __init__(self, *args):
|
|
36
36
|
pin_exception(self, 2)
|
|
37
|
-
super(
|
|
37
|
+
super(StdException, self).__init__(*args)
|
|
38
38
|
|
|
39
39
|
def dump_exception(msg, f = sys.stdout, extra = None):
|
|
40
40
|
exc_type, exc_value, exc_traceback = sys.exc_info()
|
|
@@ -48,9 +48,13 @@ class SdpParseError(SipParseError):
|
|
|
48
48
|
if self.sip_response is not None:
|
|
49
49
|
return self.sip_response
|
|
50
50
|
resp = req.genResponse(self.code, self.msg)
|
|
51
|
-
if self.
|
|
52
|
-
reason = SipReason(protocol='SIP', cause=self.code,
|
|
53
|
-
reason=self.arg)
|
|
51
|
+
if (reason:=self.getReason()) is not None:
|
|
54
52
|
rsh = SipHeader(body = reason)
|
|
55
53
|
resp.appendHeader(rsh)
|
|
56
54
|
return resp
|
|
55
|
+
|
|
56
|
+
def getReason(self):
|
|
57
|
+
if self.arg is not None and len(self.arg) > 0:
|
|
58
|
+
return SipReason(protocol='SIP', cause=self.code,
|
|
59
|
+
reason=self.arg)
|
|
60
|
+
return None
|
|
@@ -36,6 +36,7 @@ from sippy.Core.EventDispatcher import ED2
|
|
|
36
36
|
_MAX_WORKERS = 20
|
|
37
37
|
|
|
38
38
|
class _Worker(Thread):
|
|
39
|
+
daemon = True
|
|
39
40
|
command = None
|
|
40
41
|
master = None
|
|
41
42
|
|
|
@@ -43,7 +44,6 @@ class _Worker(Thread):
|
|
|
43
44
|
Thread.__init__(self)
|
|
44
45
|
self.command = command
|
|
45
46
|
self.master = master
|
|
46
|
-
self.setDaemon(True)
|
|
47
47
|
self.start()
|
|
48
48
|
|
|
49
49
|
def run(self):
|
|
@@ -68,10 +68,10 @@ class MsgBody(object):
|
|
|
68
68
|
def __str__(self):
|
|
69
69
|
return str(self.content)
|
|
70
70
|
|
|
71
|
-
def localStr(self, local_addr = None
|
|
71
|
+
def localStr(self, local_addr = None):
|
|
72
72
|
if type(self.content) in str_types:
|
|
73
73
|
return self.content
|
|
74
|
-
return self.content.localStr(local_addr
|
|
74
|
+
return self.content.localStr(local_addr)
|
|
75
75
|
|
|
76
76
|
def getType(self):
|
|
77
77
|
return self.mtype.name.lower()
|
|
@@ -30,7 +30,7 @@ class MultipartMixBody():
|
|
|
30
30
|
boundary = None
|
|
31
31
|
|
|
32
32
|
def __init__(self, body = None, ctype = None):
|
|
33
|
-
if body
|
|
33
|
+
if body is None:
|
|
34
34
|
return
|
|
35
35
|
sep = f'--{ctype.params["boundary"]}'
|
|
36
36
|
bparts = body.split(sep)
|
|
@@ -53,9 +53,9 @@ class MultipartMixBody():
|
|
|
53
53
|
s += f'{bnd[:-2]}--\r\n'
|
|
54
54
|
return s
|
|
55
55
|
|
|
56
|
-
def localStr(self, local_addr = None
|
|
56
|
+
def localStr(self, local_addr = None):
|
|
57
57
|
bnd = f'--{self.boundary}\r\n'
|
|
58
|
-
parts = [f'{bnd}Content-Type: {p.mtype}\r\n{p.localStr(local_addr
|
|
58
|
+
parts = [f'{bnd}Content-Type: {p.mtype}\r\n{p.localStr(local_addr)}'
|
|
59
59
|
for p in self.parts]
|
|
60
60
|
s = ''.join(parts)
|
|
61
61
|
s += f'{bnd[:-2]}--\r\n'
|
|
@@ -33,6 +33,7 @@ except ImportError:
|
|
|
33
33
|
from configparser import RawConfigParser
|
|
34
34
|
_boolean_states = RawConfigParser.BOOLEAN_STATES
|
|
35
35
|
from sippy.SipConf import SipConf
|
|
36
|
+
from sippy.B2BTransforms import getTransProc
|
|
36
37
|
|
|
37
38
|
SUPPORTED_OPTIONS = { \
|
|
38
39
|
'acct_enable': ('B', 'enable or disable Radius accounting'), \
|
|
@@ -98,6 +99,13 @@ SUPPORTED_OPTIONS = { \
|
|
|
98
99
|
'and "SUBSCRIBE" messages. Address in the format ' \
|
|
99
100
|
'"host[:port]"'),
|
|
100
101
|
'nat_traversal': ('B', 'enable NAT traversal for signalling'), \
|
|
102
|
+
'wss_socket': ('S', 'WSS (SIP via websockets, RFC7118) socket ' \
|
|
103
|
+
'configuration. Configuration in the format ' \
|
|
104
|
+
'"host:port:cert_file:key_file", where "cert_file" ' \
|
|
105
|
+
'/ "key_file" are paths to the TLS certificate ' \
|
|
106
|
+
'and key file respectively in the X.509 PEM format'),
|
|
107
|
+
'pre_auth_proc': ('S', 'internal routine to be executed before authentication '\
|
|
108
|
+
'is being processed. E.g. "HDR2Xattrs[X-foo-hdr]."'), \
|
|
101
109
|
'xmpp_b2bua_id': ('I', 'ID passed to the XMPP socket server')}
|
|
102
110
|
|
|
103
111
|
class MyConfigParser(RawConfigParser):
|
|
@@ -193,7 +201,8 @@ class MyConfigParser(RawConfigParser):
|
|
|
193
201
|
if _value <= 0:
|
|
194
202
|
raise ValueError('max_credit_time should be more than zero')
|
|
195
203
|
elif key == 'allowed_pts':
|
|
196
|
-
self['_allowed_pts'] = [int(x)
|
|
204
|
+
self['_allowed_pts'] = [int(x) if not (x.startswith('[') and x.endswith(']')) \
|
|
205
|
+
else x[1:-1] for x in value.split(',')]
|
|
197
206
|
elif key in ('accept_ips', 'rtp_proxy_clients'):
|
|
198
207
|
self['_' + key] = [x.strip() for x in value.split(',')]
|
|
199
208
|
elif key == 'pass_headers':
|
|
@@ -210,6 +219,8 @@ class MyConfigParser(RawConfigParser):
|
|
|
210
219
|
if _value <= 0 or _value > 65535:
|
|
211
220
|
raise ValueError('sip_port should be in the range 1-65535')
|
|
212
221
|
self['_sip_port'] = _value
|
|
222
|
+
elif key == 'pre_auth_proc':
|
|
223
|
+
self['_pre_auth_proc'] = getTransProc(value)
|
|
213
224
|
self[key] = value
|
|
214
225
|
|
|
215
226
|
def options_help(self):
|
|
@@ -0,0 +1,150 @@
|
|
|
1
|
+
# Copyright (c) 2003-2005 Maxim Sobolev. All rights reserved.
|
|
2
|
+
# Copyright (c) 2006-2024 Sippy Software, Inc. All rights reserved.
|
|
3
|
+
#
|
|
4
|
+
# All rights reserved.
|
|
5
|
+
#
|
|
6
|
+
# Redistribution and use in source and binary forms, with or without modification,
|
|
7
|
+
# are permitted provided that the following conditions are met:
|
|
8
|
+
#
|
|
9
|
+
# 1. Redistributions of source code must retain the above copyright notice, this
|
|
10
|
+
# list of conditions and the following disclaimer.
|
|
11
|
+
#
|
|
12
|
+
# 2. Redistributions in binary form must reproduce the above copyright notice,
|
|
13
|
+
# this list of conditions and the following disclaimer in the documentation and/or
|
|
14
|
+
# other materials provided with the distribution.
|
|
15
|
+
#
|
|
16
|
+
# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND
|
|
17
|
+
# ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
|
|
18
|
+
# WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
|
|
19
|
+
# DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR
|
|
20
|
+
# ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES
|
|
21
|
+
# (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;
|
|
22
|
+
# LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON
|
|
23
|
+
# ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
|
|
24
|
+
# (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
|
|
25
|
+
# SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
|
|
26
|
+
|
|
27
|
+
from typing import Optional, Tuple, List, Union
|
|
28
|
+
from queue import Queue
|
|
29
|
+
from abc import ABC, abstractmethod
|
|
30
|
+
from random import random, randint
|
|
31
|
+
|
|
32
|
+
from sippy.Core.Exceptions import dump_exception
|
|
33
|
+
from sippy.Time.MonoTime import MonoTime
|
|
34
|
+
from sippy.Time.Timeout import Timeout
|
|
35
|
+
from sippy.SipConf import MyPort
|
|
36
|
+
|
|
37
|
+
class Remote_address():
|
|
38
|
+
transport: str
|
|
39
|
+
address: Tuple[str, int]
|
|
40
|
+
received: str
|
|
41
|
+
|
|
42
|
+
def __init__(self, address:Tuple[str, int], transport:str):
|
|
43
|
+
self.transport = transport
|
|
44
|
+
self.address = address
|
|
45
|
+
self.received = address[0]
|
|
46
|
+
|
|
47
|
+
def __str__(self):
|
|
48
|
+
return f'{self.transport}:{self.address[0]}:{self.address[1]}'
|
|
49
|
+
|
|
50
|
+
class Network_server_opts():
|
|
51
|
+
laddress: Optional[Tuple[str, Union[int, callable, MyPort]]] = None
|
|
52
|
+
data_callback: Optional[callable] = None
|
|
53
|
+
direct_dispatch: bool = False
|
|
54
|
+
ploss_out_rate: float = 0.0
|
|
55
|
+
pdelay_out_max: float = 0.0
|
|
56
|
+
ploss_in_rate: float = 0.0
|
|
57
|
+
pdelay_in_max: float = 0.0
|
|
58
|
+
|
|
59
|
+
def __init__(self, *args:Optional[Tuple[str, int]], o:Optional['Network_server_opts'] = None):
|
|
60
|
+
if o != None:
|
|
61
|
+
self.laddress, self.data_callback, self.direct_dispatch, \
|
|
62
|
+
self.ploss_out_rate, self.pdelay_out_max, self.ploss_in_rate, \
|
|
63
|
+
self.pdelay_in_max = o.laddress, o.data_callback, o.direct_dispatch, \
|
|
64
|
+
o.ploss_out_rate, o.pdelay_out_max, o.ploss_in_rate, o.pdelay_in_max
|
|
65
|
+
return
|
|
66
|
+
self.laddress, self.data_callback = args
|
|
67
|
+
|
|
68
|
+
def getCopy(self) -> 'Network_server_opts':
|
|
69
|
+
return self.__class__(o = self)
|
|
70
|
+
|
|
71
|
+
def isWildCard(self) -> bool:
|
|
72
|
+
return False
|
|
73
|
+
|
|
74
|
+
class Network_server(ABC):
|
|
75
|
+
transport: str
|
|
76
|
+
uopts: Network_server_opts
|
|
77
|
+
sendqueue: Queue
|
|
78
|
+
stats: List[int]
|
|
79
|
+
|
|
80
|
+
def __init__(self, uopts:Network_server_opts):
|
|
81
|
+
self.uopts = uopts.getCopy()
|
|
82
|
+
self.sendqueue = Queue()
|
|
83
|
+
self.stats = [0, 0, 0]
|
|
84
|
+
|
|
85
|
+
def getSIPaddr(self) -> Tuple[Tuple[str, int], int]:
|
|
86
|
+
return (self.uopts.laddress, self.transport)
|
|
87
|
+
|
|
88
|
+
def addr2str(self, address):
|
|
89
|
+
return f'{self.transport}:{address[0]}:{address[1]}'
|
|
90
|
+
|
|
91
|
+
def send_to(self, data:Union[bytes, str], address:object, delayed:bool = False):
|
|
92
|
+
if not isinstance(data, bytes):
|
|
93
|
+
data = data.encode('utf-8')
|
|
94
|
+
if self.uopts.ploss_out_rate > 0.0 and not delayed:
|
|
95
|
+
if random() < self.uopts.ploss_out_rate:
|
|
96
|
+
return
|
|
97
|
+
if self.uopts.pdelay_out_max > 0.0 and not delayed:
|
|
98
|
+
pdelay = self.uopts.pdelay_out_max * random()
|
|
99
|
+
Timeout(self.send_to, pdelay, 1, data, address, True)
|
|
100
|
+
return
|
|
101
|
+
self.sendqueue.put((data, address))
|
|
102
|
+
|
|
103
|
+
def handle_read(self, data:bytes, address:Remote_address, rtime:MonoTime, delayed:bool = False):
|
|
104
|
+
if len(data) > 0 and self.uopts.data_callback != None:
|
|
105
|
+
self.stats[2] += 1
|
|
106
|
+
if self.uopts.ploss_in_rate > 0.0 and not delayed:
|
|
107
|
+
if random() < self.uopts.ploss_in_rate:
|
|
108
|
+
return
|
|
109
|
+
if self.uopts.pdelay_in_max > 0.0 and not delayed:
|
|
110
|
+
pdelay = self.uopts.pdelay_in_max * random()
|
|
111
|
+
Timeout(self.handle_read, pdelay, 1, data, address, rtime.getOffsetCopy(pdelay), True)
|
|
112
|
+
return
|
|
113
|
+
try:
|
|
114
|
+
self.uopts.data_callback(data, address, self, rtime)
|
|
115
|
+
except Exception as ex:
|
|
116
|
+
if isinstance(ex, SystemExit):
|
|
117
|
+
raise
|
|
118
|
+
dump_exception(f'{self.__class__}: unhandled exception when processing incoming data')
|
|
119
|
+
|
|
120
|
+
@abstractmethod
|
|
121
|
+
def join(self):
|
|
122
|
+
pass
|
|
123
|
+
|
|
124
|
+
def shutdown(self):
|
|
125
|
+
self.sendqueue.put(None)
|
|
126
|
+
self.join()
|
|
127
|
+
self.uopts.data_callback = None
|
|
128
|
+
|
|
129
|
+
class PortAllocationError(Exception): pass
|
|
130
|
+
|
|
131
|
+
class RTP_port_allocator():
|
|
132
|
+
min_port: int
|
|
133
|
+
max_port: int
|
|
134
|
+
|
|
135
|
+
def __init__(self, min_port:Optional[int]=None, max_port:Optional[int]=None):
|
|
136
|
+
if min_port is None: min_port = 1024
|
|
137
|
+
if max_port is None: max_port = 65535
|
|
138
|
+
if min_port % 2 != 0:
|
|
139
|
+
min_port += 1
|
|
140
|
+
assert min_port <= max_port, f'min_port={min_port} > max_port={max_port}'
|
|
141
|
+
self.min_port = min_port
|
|
142
|
+
self.max_port = max_port
|
|
143
|
+
|
|
144
|
+
def __call__(self, ntry: int) -> int:
|
|
145
|
+
rlen = self.max_port - self.min_port
|
|
146
|
+
if ntry > (rlen // 2):
|
|
147
|
+
raise PortAllocationError(f'No free ports available after {ntry} tries')
|
|
148
|
+
port = self.min_port + (randint(0, rlen // 2) * 2)
|
|
149
|
+
assert port <= self.max_port, f'port={port} > self.max_port={self.max_port}'
|
|
150
|
+
return port
|
|
@@ -188,3 +188,7 @@ class RadiusAccounting(object):
|
|
|
188
188
|
else:
|
|
189
189
|
message = 'Error sending Acct/%s request (delay is %.3f)\n' % (self.origin, delay)
|
|
190
190
|
self.global_config['_sip_logger'].write(message, call_id = sip_cid)
|
|
191
|
+
|
|
192
|
+
def addAttributes(self, attributes):
|
|
193
|
+
self._attributes.extend(tuple(x) for x in attributes \
|
|
194
|
+
if tuple(x) not in self._attributes)
|