Skip to content

Commit

Permalink
[ADD] Added container and volume remove to the Docker start script.
Browse files Browse the repository at this point in the history
[ADD] Added basic Network Address Translation (NAT) feature.
[ADD] Added __gen_via in SIP.client.
[ADD] Added __gen_contact in SIP.client.
[ADD] Added some IPv6 handling.
[CHANGE] Changed Docker start script to PowerShell.
[FIX] Fixed not receiving replies due to network routing.
[FIX] Fixed registration tests failing due to max contacts.
[FIX] Fixed uri's not denoting sips when using TLS
[FIX] Fixed Via headers from other hosts being incorrectly changed.
[FIX] Fixed timeout errors being raised incorrectly.
[FIX] Fixed deregister_connection for TCP and TLS sockets.
[FIX] Fixed all registration tests.
  • Loading branch information
tayler6000 committed Oct 15, 2023
1 parent 322757b commit f4b6d27
Show file tree
Hide file tree
Showing 10 changed files with 170 additions and 101 deletions.
2 changes: 1 addition & 1 deletion .github/workflows/pytest.yml
Original file line number Diff line number Diff line change
Expand Up @@ -33,7 +33,7 @@ jobs:
- name: Start Docker
run: |
docker build docker -t pyvoip/tests
docker run -d -p 5060:5060/udp -p 5061-5062:5061-5062/tcp pyvoip/tests
docker run --add-host host.docker.internal:host-gateway -d -p 5060:5060/udp -p 5061-5062:5061-5062/tcp pyvoip/tests
- name: pytest
# run: pytest --check-func
run: pytest
4 changes: 2 additions & 2 deletions docker/settings/pjsip.conf
Original file line number Diff line number Diff line change
Expand Up @@ -29,7 +29,7 @@ aors=nopass

[nopass]
type=aor
max_contacts=1
max_contacts=999

[pass]
type=endpoint
Expand All @@ -48,4 +48,4 @@ password=Testing123!

[pass]
type=aor
max_contacts=1
max_contacts=999
4 changes: 0 additions & 4 deletions docker/start.bat

This file was deleted.

5 changes: 5 additions & 0 deletions docker/start.ps1
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
docker stop $(docker ps -a -q)
docker rm --force --volumes $(docker ps -a -q)
docker rmi pyvoip/tests
docker build . -t pyvoip/tests
docker run --add-host host.docker.internal:host-gateway -d -p 5060:5060/udp -p 5061-5062:5061-5062/tcp pyvoip/tests
146 changes: 81 additions & 65 deletions pyVoIP/SIP/client.py
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@
InvalidAccountInfoError,
)
from pyVoIP.helpers import Counter
from pyVoIP.networking.nat import NAT
from pyVoIP.SIP.message import (
SIPMessage,
SIPStatus,
Expand Down Expand Up @@ -37,6 +38,9 @@ def __init__(
user: str,
credentials_manager: CredentialsManager,
bind_ip="0.0.0.0",
bind_network="0.0.0.0/0",
hostname: Optional[str] = None,
remote_hostname: Optional[str] = None,
bind_port=5060,
call_callback: Optional[Callable[[SIPMessage], Optional[str]]] = None,
transport_mode: TransportMode = TransportMode.UDP,
Expand All @@ -49,6 +53,7 @@ def __init__(
self.port = port
self.bind_ip = bind_ip
self.bind_port = bind_port
self.nat = NAT(bind_ip, bind_network, hostname, remote_hostname)
self.user = user
self.credentials_manager = credentials_manager
self.transport_mode = transport_mode
Expand Down Expand Up @@ -285,6 +290,31 @@ def __gen_uri(
params = params if params else ""
return f"{method}:{user}{password}@{host}{port_str}{params}"

def __gen_via(self, to: str, branch: str) -> str:
# SIP/2.0/ should still be the prefix even if using TLS per RFC 3261
# 8.1.1.7, as shown in RFC 5630 6.1
return (
"Via: "
+ f"SIP/2.0/{str(self.transport_mode)}"
+ f" {self.nat.get_host(to)}:{self.bind_port};branch={branch}\r\n"
)

def __gen_contact(
self,
method: str,
user: str,
host: str,
password: Optional[str] = None,
port=5060,
uriparams: Optional[str] = None,
params: list[str] = [],
) -> str:
uri = self.__gen_uri(method, user, host, password, port, uriparams)
uri = f"<{uri}>"
if params:
uri += ";" + (";".join(params))
return f"Contact: {uri}\r\n"

def __gen_user_agent(self) -> str:
return f"User-Agent: pyVoIP {pyVoIP.__version__}\r\n"

Expand Down Expand Up @@ -484,12 +514,8 @@ def gen_urn_uuid(self) -> str:

def gen_first_request(self, deregister=False) -> str:
regRequest = f"REGISTER sip:{self.server}:{self.port} SIP/2.0\r\n"
regRequest += (
"Via: SIP/2.0/"
+ str(self.transport_mode)
+ f" {self.bind_ip}:{self.bind_port};"
+ f"branch={self.gen_branch()};rport\r\n"
)
regRequest += self.__gen_via(self.server, self.gen_branch())

regRequest += (
f'From: "{self.user}" '
+ f"<sip:{self.user}@{self.bind_ip}:{self.bind_port}>;tag="
Expand All @@ -501,13 +527,14 @@ def gen_first_request(self, deregister=False) -> str:
)
regRequest += f"Call-ID: {self.gen_call_id()}\r\n"
regRequest += f"CSeq: {self.registerCounter.next()} REGISTER\r\n"
regRequest += (
"Contact: "
+ f"<sip:{self.user}@{self.bind_ip}:{self.bind_port};"
+ "transport="
+ str(self.transport_mode)
+ ">;+sip.instance="
+ f'"<urn:uuid:{self.urnUUID}>"\r\n'
method = "sips" if self.transport_mode is TransportMode.TLS else "sip"
trans_mode = str(self.transport_mode)
regRequest += self.__gen_contact(
method,
self.user,
self.nat.get_host(self.server),
uriparams=f";transport={trans_mode}",
params=[f'+sip.instance="<urn:uuid:{self.urnUUID}>"'],
)
regRequest += f'Allow: {(", ".join(pyVoIP.SIPCompatibleMethods))}\r\n'
regRequest += "Max-Forwards: 70\r\n"
Expand All @@ -525,12 +552,7 @@ def gen_first_request(self, deregister=False) -> str:

def gen_subscribe(self, response: SIPMessage) -> str:
subRequest = f"SUBSCRIBE sip:{self.user}@{self.server} SIP/2.0\r\n"
subRequest += (
"Via: SIP/2.0/"
+ str(self.transport_mode)
+ f" {self.bind_ip}:{self.bind_port};"
+ f"branch={self.gen_branch()};rport\r\n"
)
subRequest += self.__gen_via(self.server, self.gen_branch())
subRequest += (
f'From: "{self.user}" '
+ f"<sip:{self.user}@{self.server}>;tag="
Expand All @@ -540,13 +562,14 @@ def gen_subscribe(self, response: SIPMessage) -> str:
subRequest += f'Call-ID: {response.headers["Call-ID"]}\r\n'
subRequest += f"CSeq: {self.subscribeCounter.next()} SUBSCRIBE\r\n"
# TODO: check if transport is needed
subRequest += (
"Contact: "
+ f"<sip:{self.user}@{self.bind_ip}:{self.bind_port};"
+ "transport="
+ str(self.transport_mode)
+ ">;+sip.instance="
+ f'"<urn:uuid:{self.urnUUID}>"\r\n'
method = "sips" if self.transport_mode is TransportMode.TLS else "sip"
trans_mode = str(self.transport_mode)
subRequest += self.__gen_contact(
method,
self.user,
self.nat.get_host(self.server),
uriparams=f";transport={trans_mode}",
params=[f'+sip.instance="<urn:uuid:{self.urnUUID}>"'],
)
subRequest += "Max-Forwards: 70\r\n"
subRequest += self.__gen_user_agent()
Expand All @@ -560,12 +583,7 @@ def gen_subscribe(self, response: SIPMessage) -> str:

def gen_register(self, request: SIPMessage, deregister=False) -> str:
regRequest = f"REGISTER sip:{self.server}:{self.port} SIP/2.0\r\n"
regRequest += (
"Via: SIP/2.0/"
+ str(self.transport_mode)
+ f" {self.bind_ip}:{self.bind_port};branch="
+ f"{self.gen_branch()};rport\r\n"
)
regRequest += self.__gen_via(self.server, self.gen_branch())
regRequest += (
f'From: "{self.user}" '
+ f"<sip:{self.user}@{self.bind_ip}:{self.bind_port}>;tag="
Expand All @@ -578,13 +596,14 @@ def gen_register(self, request: SIPMessage, deregister=False) -> str:
call_id = request.headers.get("Call-ID", self.gen_call_id())
regRequest += f"Call-ID: {call_id}\r\n"
regRequest += f"CSeq: {self.registerCounter.next()} REGISTER\r\n"
regRequest += (
"Contact: "
+ f"<sip:{self.user}@{self.bind_ip}:{self.bind_port};"
+ "transport="
+ str(self.transport_mode)
+ ">;+sip.instance="
+ f'"<urn:uuid:{self.urnUUID}>"\r\n'
method = "sips" if self.transport_mode is TransportMode.TLS else "sip"
trans_mode = str(self.transport_mode)
regRequest += self.__gen_contact(
method,
self.user,
self.nat.get_host(self.server),
uriparams=f";transport={trans_mode}",
params=[f'+sip.instance="<urn:uuid:{self.urnUUID}>"'],
)
regRequest += f'Allow: {(", ".join(pyVoIP.SIPCompatibleMethods))}\r\n'
regRequest += "Max-Forwards: 70\r\n"
Expand Down Expand Up @@ -703,9 +722,12 @@ def gen_answer(
f"CSeq: {request.headers['CSeq']['check']} "
+ f"{request.headers['CSeq']['method']}\r\n"
)
regRequest += (
"Contact: "
+ f"<sip:{self.user}@{self.bind_ip}:{self.bind_port}>\r\n"
method = "sips" if self.transport_mode is TransportMode.TLS else "sip"
trans_mode = str(self.transport_mode)
regRequest += self.__gen_contact(
method,
self.user,
self.nat.get_host(self.server),
)
# TODO: Add Supported
regRequest += self.__gen_user_agent()
Expand All @@ -727,12 +749,12 @@ def gen_invite(
) -> str:
# Generate body first for content length
body = "v=0\r\n"
# TODO: Check IPv4/IPv6
body += (
f"o=pyVoIP {sess_id} {int(sess_id)+2} IN IP4 {self.bind_ip}\r\n"
f"o=pyVoIP {sess_id} {int(sess_id)+2} IN IP"
+ f"{self.nat.bind_ip.version} {self.bind_ip}\r\n"
)
body += f"s=pyVoIP {pyVoIP.__version__}\r\n"
body += f"c=IN IP4 {self.bind_ip}\r\n" # TODO: Check IPv4/IPv6
body += f"c=IN IP{self.nat.bind_ip.version} {self.bind_ip}\r\n"
body += "t=0 0\r\n"
for x in ms:
# TODO: Check AVP mode from request
Expand All @@ -759,24 +781,21 @@ def gen_invite(
uri_method, number, self.server, port=self.port
)
invRequest = f"INVITE {to_uri} SIP/2.0\r\n"
invRequest += (
"Via: SIP/2.0/"
+ str(self.transport_mode)
+ f" {self.bind_ip}:{self.bind_port};branch="
+ f"{branch}\r\n"
)
invRequest += self.__gen_via(self.server, branch)
invRequest += "Max-Forwards: 70\r\n"
uri = self.__gen_uri(
uri_method, self.user, self.bind_ip, port=self.bind_port
method = "sips" if self.transport_mode is TransportMode.TLS else "sip"
invRequest += self.__gen_contact(
method,
self.user,
self.nat.get_host(self.server),
)
invRequest += f"Contact: <{uri}>\r\n"
invRequest += self.__gen_from_to(
"To", number, self.server, port=self.port
)
invRequest += self.__gen_from_to(
"From",
self.user,
self.bind_ip,
self.nat.get_host(self.server),
port=self.bind_port,
header_parms=f";tag={tag}",
)
Expand Down Expand Up @@ -808,9 +827,11 @@ def _gen_bye_cancel(self, request: SIPMessage, cmd: str) -> str:
byeRequest += f"Call-ID: {request.headers['Call-ID']}\r\n"
cseq = request.headers["CSeq"]["check"]
byeRequest += f"CSeq: {cseq} {cmd}\r\n"
byeRequest += (
"Contact: "
+ f"<sip:{self.user}@{self.bind_ip}:{self.bind_port}>\r\n"
method = "sips" if self.transport_mode is TransportMode.TLS else "sip"
byeRequest += self.__gen_contact(
method,
self.user,
self.nat.get_host(self.server),
)
byeRequest += self.__gen_user_agent()
byeRequest += f"Allow: {(', '.join(pyVoIP.SIPCompatibleMethods))}\r\n"
Expand Down Expand Up @@ -852,9 +873,7 @@ def _gen_response_via_header(self, request: SIPMessage) -> str:
via = ""
for h_via in request.headers["Via"]:
v_line = (
"Via: SIP/2.0/"
+ str(self.transport_mode)
+ " "
f"Via: {h_via['type']} "
+ f'{h_via["address"][0]}:{h_via["address"][1]}'
)
if "branch" in h_via.keys():
Expand Down Expand Up @@ -925,10 +944,7 @@ def gen_message(
self, number: str, body: str, ctype: str, branch: str, call_id: str
) -> str:
msg = f"MESSAGE sip:{number}@{self.server} SIP/2.0\r\n"
msg += (
f"Via: SIP/2.0/{self.transport_mode} "
+ f"{self.bind_ip}:{self.bind_port};branch={branch}\r\n"
)
msg += self.__gen_via(self.server, branch)
msg += "Max-Forwards: 70\r\n"
msg += f"To: <sip:{number}@{self.server}>\r\n"
msg += (
Expand Down
6 changes: 6 additions & 0 deletions pyVoIP/VoIP/phone.py
Original file line number Diff line number Diff line change
Expand Up @@ -40,6 +40,9 @@ def __init__(
user: str,
credentials_manager: CredentialsManager,
bind_ip="0.0.0.0",
bind_network="0.0.0.0/0",
hostname: Optional[str] = None,
remote_hostname: Optional[str] = None,
bind_port=5060,
transport_mode=TransportMode.UDP,
cert_file: Optional[str] = None,
Expand Down Expand Up @@ -90,6 +93,9 @@ def __init__(
user,
credentials_manager,
bind_ip=self.bind_ip,
bind_network=bind_network,
hostname=hostname,
remote_hostname=remote_hostname,
bind_port=bind_port,
call_callback=self.callback,
transport_mode=self.transport_mode,
Expand Down
Empty file added pyVoIP/networking/__init__.py
Empty file.
41 changes: 41 additions & 0 deletions pyVoIP/networking/nat.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,41 @@
from typing import Optional
import ipaddress
import socket


class NATError(Exception):
pass


class NAT:
def __init__(
self,
bind_ip: str,
network: str,
hostname: Optional[str] = None,
remote_hostname: Optional[str] = None,
):
self.bind_ip = ipaddress.ip_address(bind_ip)
self.network = ipaddress.ip_network(network)
self.hostname = bind_ip if hostname is None else hostname
self.remote_hostname = remote_hostname

def get_host(self, host: str):
"""Return the hostname another client needs to connect to us."""
try:
ip = ipaddress.ip_address(host)
except ValueError:
try:
ip = socket.gethostbyname(host)
except socket.gaierror:
raise NATError(f"Unable to resolve hostname {host}")

if ip in self.network:
return self.hostname
else:
if self.remote_hostname is not None:
return self.remote_hostname
raise NATError(
"No remote hostname specified, "
+ "cannot provide a return path for remote hosts."
)
Loading

0 comments on commit f4b6d27

Please sign in to comment.