Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Binary file added audio.wav
Binary file not shown.
4 changes: 3 additions & 1 deletion docs/SIP.rst
Original file line number Diff line number Diff line change
Expand Up @@ -68,7 +68,7 @@ SIPClient

The SIPClient class is used to communicate with the PBX/VoIP server. It is responsible for registering with the server, and receiving phone calls.

*class* SIP.\ **SIPClient**\ (server: str, port: int, username: str, password: str, myIP="0.0.0.0", myPort=5060, callCallback: Optional[Callable[[SIPMessage], None]] = None)
*class* SIP.\ **SIPClient**\ (server: str, port: int, username: str, password: str, myIP="0.0.0.0", myPort=5060, callCallback: Optional[Callable[[SIPMessage], None]] = None, auth_username: str)
The *server* argument is your PBX/VoIP server's IP.

The *port* argument is your PBX/VoIP server's port.
Expand All @@ -83,6 +83,8 @@ The SIPClient class is used to communicate with the PBX/VoIP server. It is resp

The *callCallback* argument is the callback function for :ref:`VoIPPhone`. VoIPPhone will process the SIP request, and perform the appropriate actions.

The *auth_username* argument is the optional username for proxy-authentication, represented as a string.

**recv**\ () -> None
This method is called by SIPClient.start() and is responsible for receiving and parsing through SIP requests. **This should not be called by the** :term:`user`.

Expand Down
4 changes: 3 additions & 1 deletion docs/VoIP.rst
Original file line number Diff line number Diff line change
Expand Up @@ -146,7 +146,7 @@ VoIPPhone

The VoIPPhone class is used to manage the :ref:`SIPClient` class and create :ref:`VoIPCall`'s when there is an incoming call. It then passes the VoIPCall as the argument in the callback.

*class* VoIP.\ **VoIPPhone**\ (server: str, port: int, username: str, password: str, callCallback: Optional[Callable] = None, myIP: Optional[str] = None, sipPort=5060, rtpPortLow=10000, rtpPortHigh=20000)
*class* VoIP.\ **VoIPPhone**\ (server: str, port: int, username: str, password: str, callCallback: Optional[Callable] = None, myIP: Optional[str] = None, sipPort=5060, rtpPortLow=10000, rtpPortHigh=20000, auth_username: str)
The *server* argument is your PBX/VoIP server's IP, represented as a string.

The *port* argument is your PBX/VoIP server's port, represented as an integer.
Expand All @@ -162,6 +162,8 @@ The VoIPPhone class is used to manage the :ref:`SIPClient` class and create :ref
The *sipPort* argument is the port SIP will bind to to receive SIP requests. The default for this protocol is port 5060, but any port can be used.

The *rtpPortLow* and *rtpPortHigh* arguments are used to generate random ports to use for audio transfer. Per RFC 4566 Sections `5.7 <https://tools.ietf.org/html/rfc4566#section-5.7>`_ and `5.14 <https://tools.ietf.org/html/rfc4566#section-5.14>`_, it can take multiple ports to fully communicate with other :term:`clients<client>`, as such a large range is recommended. If an invalid range is given, a :ref:`InvalidStateError<invalidstateerror>` will be thrown.

The *auth_username* argument is the optional username for proxy-authentication, represented as a string.

**callback**\ (request: :ref:`SIPMessage`) -> None
This method is called by the :ref:`SIPClient` when an INVITE or BYE request is received. This function then creates a :ref:`VoIPCall` or terminates it respectively. When a VoIPCall is created, it will then pass it to the *callCallback* function as an argument. If *callCallback* is set to None, this function replies as BUSY. **This function should not be called by the** :term:`user`.
Expand Down
86 changes: 64 additions & 22 deletions pyVoIP/SIP.py
Original file line number Diff line number Diff line change
Expand Up @@ -347,6 +347,7 @@ def __init__(self, data: bytes):
self.headers: Dict[str, Any] = {"Via": []}
self.body: Dict[str, Any] = {}
self.authentication: Dict[str, str] = {}
self.proxy_authentication: Dict[str, str] = {}
self.raw = data
self.auth_match = re.compile(r'(\w+)=("[^",]+"|[^ \t,]+)')
self.parse(data)
Expand Down Expand Up @@ -460,14 +461,21 @@ def parse_header(self, header: str, data: str) -> None:
self.headers[header] = data.split(", ")
elif header == "Content-Length":
self.headers[header] = int(data)
elif header == "WWW-Authenticate" or header == "Authorization":
elif (
header == "WWW-Authenticate"
or header == "Authorization"
or header == "Proxy-Authenticate"
):
data = data.replace("Digest ", "")
row_data = self.auth_match.findall(data)
header_data = {}
for var, data in row_data:
header_data[var] = data.strip('"')
self.headers[header] = header_data
self.authentication = header_data
if header == "Proxy-Authenticate":
self.proxy_authentication = header_data
else:
self.authentication = header_data
else:
self.headers[header] = data

Expand Down Expand Up @@ -813,12 +821,14 @@ def __init__(
myPort=5060,
callCallback: Optional[Callable[[SIPMessage], None]] = None,
fatalCallback: Optional[Callable[..., None]] = None,
auth_username: Optional[str] = None,
):
self.NSD = False
self.server = server
self.port = port
self.myIP = myIP
self.username = username
self.auth_username = auth_username
self.password = password

self.phone = phone
Expand Down Expand Up @@ -1075,8 +1085,16 @@ def genAuthorization(self, request: SIPMessage) -> bytes:
return self.gen_authorization(request)

def gen_authorization(self, request: SIPMessage) -> bytes:
realm = request.authentication["realm"]
HA1 = self.username + ":" + realm + ":" + self.password
if request.status == SIPStatus(407):
nonce = request.proxy_authentication["nonce"]
realm = request.proxy_authentication["realm"]
user = self.auth_username
else:
nonce = request.authentication["nonce"]
realm = request.authentication["realm"]
user = self.username

HA1 = user + ":" + realm + ":" + self.password
HA1 = hashlib.md5(HA1.encode("utf8")).hexdigest()
HA2 = (
""
Expand All @@ -1086,7 +1104,6 @@ def gen_authorization(self, request: SIPMessage) -> bytes:
+ ";transport=UDP"
)
HA2 = hashlib.md5(HA2.encode("utf8")).hexdigest()
nonce = request.authentication["nonce"]
response = (HA1 + ":" + nonce + ":" + HA2).encode("utf8")
response = hashlib.md5(response).hexdigest().encode("utf8")

Expand Down Expand Up @@ -1217,8 +1234,14 @@ def genRegister(self, request: SIPMessage, deregister=False) -> str:

def gen_register(self, request: SIPMessage, deregister=False) -> str:
response = str(self.gen_authorization(request), "utf8")
nonce = request.authentication["nonce"]
realm = request.authentication["realm"]
if request.status == SIPStatus(407):
nonce = request.proxy_authentication["nonce"]
realm = request.proxy_authentication["realm"]
user = self.auth_username
else:
nonce = request.authentication["nonce"]
realm = request.authentication["realm"]
user = self.username

regRequest = f"REGISTER sip:{self.server} SIP/2.0\r\n"
regRequest += (
Expand Down Expand Up @@ -1251,8 +1274,10 @@ def gen_register(self, request: SIPMessage, deregister=False) -> str:
"Expires: "
+ f"{self.default_expires if not deregister else 0}\r\n"
)
if request.status == SIPStatus(407):
regRequest += "Proxy-"
regRequest += (
f'Authorization: Digest username="{self.username}",'
f'Authorization: Digest username="{user}",'
+ f'realm="{realm}",nonce="{nonce}",'
+ f'uri="sip:{self.server};transport=UDP",'
+ f'response="{response}",algorithm=MD5\r\n'
Expand Down Expand Up @@ -1613,6 +1638,7 @@ def invite(

while (
response.status != SIPStatus(401)
and response.status != SIPStatus(407)
and response.status != SIPStatus(100)
and response.status != SIPStatus(180)
) or response.headers["Call-ID"] != call_id:
Expand All @@ -1629,11 +1655,22 @@ def invite(
ack = self.gen_ack(response)
self.out.sendto(ack.encode("utf8"), (self.server, self.port))
debug("Acknowledged")

authhash = self.gen_authorization(response)
nonce = response.authentication["nonce"]
realm = response.authentication["realm"]
auth = (
f'Authorization: Digest username="{self.username}",realm='
auth = ""

if response.status == SIPStatus(407):
nonce = response.proxy_authentication["nonce"]
realm = response.proxy_authentication["realm"]
user = self.auth_username
auth += "Proxy-"
else:
nonce = response.authentication["nonce"]
realm = response.authentication["realm"]
user = self.username

auth += (
f'Authorization: Digest username="{user}",realm='
+ f'"{realm}",nonce="{nonce}",uri="sip:{self.server};'
+ f'transport=UDP",response="{str(authhash, "utf8")}",'
+ "algorithm=MD5\r\n"
Expand Down Expand Up @@ -1693,17 +1730,23 @@ def __deregister(self) -> bool:
response = SIPMessage(resp)
response = self.trying_timeout_check(response)

if response.status == SIPStatus(401):
# Unauthorized, likely due to being password protected.
if response.status == SIPStatus(401) or response.status == SIPStatus(
407
):
# 401 Unauthorized, likely due to being password protected.
# 407 Proxy Authentication Required
regRequest = self.gen_register(response, deregister=True)

self.out.sendto(
regRequest.encode("utf8"), (self.server, self.port)
)
ready = select.select([self.s], [], [], self.register_timeout)
if ready[0]:
resp = self.s.recv(8192)
response = SIPMessage(resp)
if response.status == SIPStatus(401):
if response.status == SIPStatus(
401
) or response.status == SIPStatus(407):
# At this point, it's reasonable to assume that
# this is caused by invalid credentials.
debug("Unauthorized")
Expand Down Expand Up @@ -1797,7 +1840,9 @@ def __register(self) -> bool:
# with new urn:uuid or reply with expire 0
self._handle_bad_request()

if response.status == SIPStatus(401):
if response.status == SIPStatus(401) or response.status == SIPStatus(
407
):
# Unauthorized, likely due to being password protected.
regRequest = self.gen_register(response)
self.out.sendto(
Expand All @@ -1808,7 +1853,9 @@ def __register(self) -> bool:
resp = self.s.recv(8192)
response = SIPMessage(resp)
response = self.trying_timeout_check(response)
if response.status == SIPStatus(401):
if response.status == SIPStatus(
401
) or response.status == SIPStatus(407):
# At this point, it's reasonable to assume that
# this is caused by invalid credentials.
debug("=" * 50)
Expand Down Expand Up @@ -1837,11 +1884,6 @@ def __register(self) -> bool:
else:
raise TimeoutError("Registering on SIP Server timed out")

if response.status == SIPStatus(407):
# Proxy Authentication Required
# TODO: implement
debug("Proxy auth required")

# TODO: This must be done more reliable
if response.status not in [
SIPStatus(400),
Expand Down
3 changes: 3 additions & 0 deletions pyVoIP/VoIP/VoIP.py
Original file line number Diff line number Diff line change
Expand Up @@ -479,6 +479,7 @@ def __init__(
sipPort=5060,
rtpPortLow=10000,
rtpPortHigh=20000,
auth_username: str = None,
):
if rtpPortLow > rtpPortHigh:
raise InvalidRangeError("'rtpPortHigh' must be >= 'rtpPortLow'")
Expand All @@ -495,6 +496,7 @@ def __init__(
self.port = port
self.myIP = myIP
self.username = username
self.auth_username = auth_username
self.password = password
self.callCallback = callCallback
self._status = PhoneStatus.INACTIVE
Expand All @@ -517,6 +519,7 @@ def __init__(
myPort=sipPort,
callCallback=self.callback,
fatalCallback=self.fatal,
auth_username=self.auth_username,
)

def callback(self, request: SIP.SIPMessage) -> None:
Expand Down
Binary file added pyVoIP/audio.wav
Binary file not shown.
Loading