From 41746656145f7f703dcf96f4809eef163e1f3566 Mon Sep 17 00:00:00 2001 From: TJ Porter Date: Mon, 15 Jan 2024 15:38:46 -0600 Subject: [PATCH 1/5] [ADD] Created SIPRequest and SIPResponse subclasses of SIPMessage [ADD] Created `SIPMessage.from_bytes` to create a SIPReqeust or SIPResponse [ADD] Added SIP version regex to better determine what is a request and what is a response if a new version of SIP is ever released [CHANGE] Split `pyVoIP.SIP.message` into `message.message`, `message.parse`, and `message.response_codes` [CHANGE] Changed `pyVoIP.SIPCompatibleMethods` to `message.SIPMethod` --- pyVoIP/SIP/message.py | 831 --------------------------- pyVoIP/SIP/message/__init__.py | 0 pyVoIP/SIP/message/message.py | 179 ++++++ pyVoIP/SIP/message/parse.py | 435 ++++++++++++++ pyVoIP/SIP/message/response_codes.py | 267 +++++++++ pyVoIP/__init__.py | 1 - pyVoIP/regex.py | 1 + 7 files changed, 882 insertions(+), 832 deletions(-) delete mode 100644 pyVoIP/SIP/message.py create mode 100644 pyVoIP/SIP/message/__init__.py create mode 100644 pyVoIP/SIP/message/message.py create mode 100644 pyVoIP/SIP/message/parse.py create mode 100644 pyVoIP/SIP/message/response_codes.py diff --git a/pyVoIP/SIP/message.py b/pyVoIP/SIP/message.py deleted file mode 100644 index 868abd1..0000000 --- a/pyVoIP/SIP/message.py +++ /dev/null @@ -1,831 +0,0 @@ -from enum import Enum, IntEnum -from pyVoIP import regex -from pyVoIP.SIP.error import SIPParseError -from pyVoIP.types import URI_HEADER -from typing import Any, Callable, Dict, List, Optional, Union -import pyVoIP - - -debug = pyVoIP.debug - - -class SIPStatus(Enum): - def __new__(cls, value: int, phrase: str = "", description: str = ""): - obj = object.__new__(cls) - obj._value_ = value - - obj.phrase = phrase - obj.description = description - return obj - - def __int__(self) -> int: - return self._value_ - - def __str__(self) -> str: - return f"{self._value_} {self.phrase}" - - @property - def phrase(self) -> str: - return self._phrase - - @phrase.setter - def phrase(self, value: str) -> None: - self._phrase = value - - @property - def description(self) -> str: - return self._description - - @description.setter - def description(self, value: str) -> None: - self._description = value - - # Informational - TRYING = ( - 100, - "Trying", - "Extended search being performed, may take a significant time", - ) - RINGING = ( - 180, - "Ringing", - "Destination user agent received INVITE, " - + "and is alerting user of call", - ) - FORWARDED = 181, "Call is Being Forwarded" - QUEUED = 182, "Queued" - SESSION_PROGRESS = 183, "Session Progress" - TERMINATED = 199, "Early Dialog Terminated" - - # Success - OK = 200, "OK", "Request successful" - ACCEPTED = ( - 202, - "Accepted", - "Request accepted, processing continues (Deprecated.)", - ) - NO_NOTIFICATION = ( - 204, - "No Notification", - "Request fulfilled, nothing follows", - ) - - # Redirection - MULTIPLE_CHOICES = ( - 300, - "Multiple Choices", - "Object has several resources -- see URI list", - ) - MOVED_PERMANENTLY = ( - 301, - "Moved Permanently", - "Object moved permanently -- see URI list", - ) - MOVED_TEMPORARILY = ( - 302, - "Moved Temporarily", - "Object moved temporarily -- see URI list", - ) - USE_PROXY = ( - 305, - "Use Proxy", - "You must use proxy specified in Location to " - + "access this resource", - ) - ALTERNATE_SERVICE = ( - 380, - "Alternate Service", - "The call failed, but alternatives are available -- see URI list", - ) - - # Client Error - BAD_REQUEST = ( - 400, - "Bad Request", - "Bad request syntax or unsupported method", - ) - UNAUTHORIZED = ( - 401, - "Unauthorized", - "No permission -- see authorization schemes", - ) - PAYMENT_REQUIRED = ( - 402, - "Payment Required", - "No payment -- see charging schemes", - ) - FORBIDDEN = ( - 403, - "Forbidden", - "Request forbidden -- authorization will not help", - ) - NOT_FOUND = (404, "Not Found", "Nothing matches the given URI") - METHOD_NOT_ALLOWED = ( - 405, - "Method Not Allowed", - "Specified method is invalid for this resource", - ) - NOT_ACCEPTABLE = ( - 406, - "Not Acceptable", - "URI not available in preferred format", - ) - PROXY_AUTHENTICATION_REQUIRED = ( - 407, - "Proxy Authentication Required", - "You must authenticate with this proxy before proceeding", - ) - REQUEST_TIMEOUT = ( - 408, - "Request Timeout", - "Request timed out; try again later", - ) - CONFLICT = 409, "Conflict", "Request conflict" - GONE = ( - 410, - "Gone", - "URI no longer exists and has been permanently removed", - ) - LENGTH_REQUIRED = ( - 411, - "Length Required", - "Client must specify Content-Length", - ) - CONDITIONAL_REQUEST_FAILED = 412, "Conditional Request Failed" - REQUEST_ENTITY_TOO_LARGE = ( - 413, - "Request Entity Too Large", - "Entity is too large", - ) - REQUEST_URI_TOO_LONG = 414, "Request-URI Too Long", "URI is too long" - UNSUPPORTED_MEDIA_TYPE = ( - 415, - "Unsupported Media Type", - "Entity body in unsupported format", - ) - UNSUPPORTED_URI_SCHEME = ( - 416, - "Unsupported URI Scheme", - "Cannot satisfy request", - ) - UNKOWN_RESOURCE_PRIORITY = ( - 417, - "Unkown Resource-Priority", - "There was a resource-priority option tag, " - + "but no Resource-Priority header", - ) - BAD_EXTENSION = ( - 420, - "Bad Extension", - "Bad SIP Protocol Extension used, not understood by the server.", - ) - EXTENSION_REQUIRED = ( - 421, - "Extension Required", - "Server requeires a specific extension to be " - + "listed in the Supported header.", - ) - SESSION_INTERVAL_TOO_SMALL = 422, "Session Interval Too Small" - SESSION_INTERVAL_TOO_BRIEF = 423, "Session Interval Too Breif" - BAD_LOCATION_INFORMATION = 424, "Bad Location Information" - USE_IDENTITY_HEADER = ( - 428, - "Use Identity Header", - "The server requires an Identity header, " - + "and one has not been provided.", - ) - PROVIDE_REFERRER_IDENTITY = 429, "Provide Referrer Identity" - """ - This response is intended for use between proxy devices, - and should not be seen by an endpoint. If it is seen by one, - it should be treated as a 400 Bad Request response. - """ - FLOW_FAILED = ( - 430, - "Flow Failed", - "A specific flow to a user agent has failed, " - + "although other flows may succeed.", - ) - ANONYMITY_DISALLOWED = 433, "Anonymity Disallowed" - BAD_IDENTITY_INFO = 436, "Bad Identity-Info" - UNSUPPORTED_CERTIFICATE = 437, "Unsupported Certificate" - INVALID_IDENTITY_HEADER = 438, "Invalid Identity Header" - FIRST_HOP_LACKS_OUTBOUND_SUPPORT = 439, "First Hop Lacks Outbound Support" - MAX_BREADTH_EXCEEDED = 440, "Max-Breadth Exceeded" - BAD_INFO_PACKAGE = 469, "Bad Info Package" - CONSENT_NEEDED = 470, "Consent Needed" - TEMPORARILY_UNAVAILABLE = 480, "Temporarily Unavailable" - CALL_OR_TRANSACTION_DOESNT_EXIST = 481, "Call/Transaction Does Not Exist" - LOOP_DETECTED = 482, "Loop Detected" - TOO_MANY_HOPS = 483, "Too Many Hops" - ADDRESS_INCOMPLETE = 484, "Address Incomplete" - AMBIGUOUS = 485, "Ambiguous" - BUSY_HERE = 486, "Busy Here", "Callee is busy" - REQUEST_TERMINATED = 487, "Request Terminated" - NOT_ACCEPTABLE_HERE = 488, "Not Acceptable Here" - BAD_EVENT = 489, "Bad Event" - REQUEST_PENDING = 491, "Request Pending" - UNDECIPHERABLE = 493, "Undecipherable" - SECURITY_AGREEMENT_REQUIRED = 494, "Security Agreement Required" - - # Server Errors - INTERNAL_SERVER_ERROR = ( - 500, - "Internal Server Error", - "Server got itself in trouble", - ) - NOT_IMPLEMENTED = ( - 501, - "Not Implemented", - "Server does not support this operation", - ) - BAD_GATEWAY = ( - 502, - "Bad Gateway", - "Invalid responses from another server/proxy", - ) - SERVICE_UNAVAILABLE = ( - 503, - "Service Unavailable", - "The server cannot process the request due to a high load", - ) - GATEWAY_TIMEOUT = ( - 504, - "Server Timeout", - "The server did not receive a timely response", - ) - SIP_VERSION_NOT_SUPPORTED = ( - 505, - "SIP Version Not Supported", - "Cannot fulfill request", - ) - MESSAGE_TOO_LONG = 513, "Message Too Long" - PUSH_NOTIFICATION_SERVICE_NOT_SUPPORTED = ( - 555, - "Push Notification Service Not Supported", - ) - PRECONDITION_FAILURE = 580, "Precondition Failure" - - # Global Failure Responses - BUSY_EVERYWHERE = 600, "Busy Everywhere" - DECLINE = 603, "Decline" - DOES_NOT_EXIST_ANYWHERE = 604, "Does Not Exist Anywhere" - GLOBAL_NOT_ACCEPTABLE = 606, "Not Acceptable" - UNWANTED = 607, "Unwanted" - REJECTED = 608, "Rejected" - - -class SIPMessageType(IntEnum): - def __new__(cls, value: int): - obj = int.__new__(cls, value) - obj._value_ = value - return obj - - REQUEST = 1 - RESPONSE = 0 - - -class SIPMessage: - def __init__(self, data: bytes): - self.SIPCompatibleVersions = pyVoIP.SIPCompatibleVersions - self.SIPCompatibleMethods = pyVoIP.SIPCompatibleMethods - self.heading: List[str] = [] - self.type: Optional[SIPMessageType] = None - self.status = SIPStatus(491) - self.headers: Dict[str, Any] = {"Via": []} - self.body: Dict[str, Any] = {} - self.authentication: Dict[str, Union[str, List[str]]] = {} - self.raw = data - self.auth_match = regex.AUTH_MATCH - - # Compacts defined in RFC 3261 Section 7.3.3 and 20 - self.compact_key = { - "i": "Call-ID", - "m": "Contact", - "e": "Content-Encoding", - "l": "Content-Length", - "c": "Content-Type", - "f": "From", - "s": "Subject", - "k": "Supported", - "t": "To", - "v": "Via", - } - - try: - self.parse(data) - except Exception as e: - if type(e) is not SIPParseError: - raise SIPParseError(e) from e - raise - - @property - def to(self) -> Optional[URI_HEADER]: - """ - The to property specifies the URI in the first line of a SIP request. - """ - return self._to - - @to.setter - def to(self, value: str) -> None: - self._to = value - - def summary(self) -> str: - data = "" - data += f"{' '.join(self.heading)}\n\n" - data += "Headers:\n" - for x in self.headers: - data += f"{x}: {self.headers[x]}\n" - data += "\n" - data += "Body:\n" - for x in self.body: - data += f"{x}: {self.body[x]}\n" - data += "\n" - data += "Raw:\n" - data += str(self.raw) - - return data - - def parse(self, data: bytes) -> None: - try: - headers, body = data.split(b"\r\n\r\n") - except ValueError as ve: - debug(f"Error unpacking data, only using header: {ve}") - headers = data.split(b"\r\n\r\n")[0] - body = b"" - - headers_raw = headers.split(b"\r\n") - self.heading = str(headers_raw.pop(0), "utf8").split(" ") - check = self.heading[0] - data = b"\r\n".join(headers_raw) + b"\r\n\r\n" + body - - if check in self.SIPCompatibleVersions: - self.type = SIPMessageType.RESPONSE - self.parse_sip_response(data) - else: # elif check in self.SIPCompatibleMethods: - self.type = SIPMessageType.REQUEST - self.parse_sip_request(data) - """ - else: - raise SIPParseError( - "Unable to decipher SIP request: " + str(heading, "utf8") - ) - """ - - def __get_uri_header(self, data: str) -> URI_HEADER: - info = data.split(";tag=") - tag = "" - if len(info) >= 2: - tag = info[1] - raw = data - reg = regex.TO_FROM_MATCH - direct = "@" not in data - if direct: - reg = regex.TO_FROM_DIRECT_MATCH - match = reg.match(data) - if match is None: - raise SIPParseError( - "Regex failed to match To/From.\n\n" - + "Please open a GitHub Issue at " - + "https://www.github.com/tayler6000/pyVoIP " - + "and include the following:\n\n" - + f"{data=} {type(match)=}" - ) - matches = match.groupdict() - if direct: - matches["user"] = "" - matches["password"] = "" - uri = f'{matches["uri_type"]}:{matches["user"]}@{matches["host"]}' - if direct: - uri = f'{matches["uri_type"]}:{matches["host"]}' - if matches["port"]: - uri += matches["port"] - uri_type = matches["uri_type"] - user = matches["user"] - password = ( - matches["password"].strip(":") if matches["password"] else "" - ) - display_name = ( - matches["display_name"].strip().strip('"') - if matches["display_name"] - else "" - ) - host = matches["host"] - port = int(matches["port"].strip(":")) if matches["port"] else 5060 - - return { - "raw": raw, - "tag": tag, - "uri": uri, - "uri-type": uri_type, - "user": user, - "password": password, - "display-name": display_name, - "host": host, - "port": port, - } - - def parse_header(self, header: str, data: str) -> None: - if header in self.compact_key.keys(): - header = self.compact_key[header] - - if header == "Via": - for d in data: - info = regex.VIA_SPLIT.split(d) - _type = info[0] # SIP Method - _address = info[1].split(":") # Tuple: address, port - _ip = _address[0] - - """ - If no port is provided in via header assume default port. - Needs to be str. Check response build for better str creation - """ - _port = ( - int(info[1].split(":")[1]) if len(_address) > 1 else 5060 - ) - _via = {"type": _type, "address": (_ip, _port)} - - """ - Sets branch, maddr, ttl, received, and rport if defined - as per RFC 3261 20.7 - """ - for x in info[2:]: - if "=" in x: - try: - _via[x.split("=")[0]] = int(x.split("=")[1]) - except ValueError: - _via[x.split("=")[0]] = x.split("=")[1] - else: - _via[x] = None - self.headers["Via"].append(_via) - elif header in ["To", "From", "Contact", "Refer-To"]: - self.headers[header] = self.__get_uri_header(data) - elif header == "CSeq": - self.headers[header] = { - "check": int(data.split(" ")[0]), - "method": data.split(" ")[1], - } - elif header in ["Allow", "Supported", "Require"]: - self.headers[header] = data.split(", ") - elif header == "Call-ID": - self.headers[header] = data - elif header in ( - "WWW-Authenticate", - "Authorization", - "Proxy-Authenticate", - ): - method = data.split(" ")[0] - data = data.replace(f"{method} ", "") - row_data = self.auth_match.findall(data) - header_data: Dict[str, Any] = {"header": header, "method": method} - for var, data in row_data: - if var == "userhash": - header_data[var] = ( - False if data.strip('"').lower() == "false" else True - ) - continue - if var == "qop": - authorized = data.strip('"').split(",") - for i, value in enumerate(authorized): - authorized[i] = value.strip() - header_data[var] = authorized - continue - header_data[var] = data.strip('"') - self.headers[header] = header_data - self.authentication = header_data - elif header == "Target-Dialog": - # Target-Dialog (tdialog) is specified in RFC 4538 - params = data.split(";") - header_data: Dict[str, Any] = { - "callid": params.pop(0) - } # key is callid to be consitenent with RFC 4538 Section 7 - for x in params: - y = x.split("=") - header_data[y[0]] = y[1] - self.headers[header] = header_data - elif header == "Refer-Sub": - # Refer-Sub (norefersub) is specified in RFC 4488 - params = data.split(";") - header_data: Dict[str, Any] = { - "value": True if params.pop(0) == "true" else False - } # BNF states extens are possible - for x in params: - y = x.split("=") - header_data[y[0]] = y[1] - self.headers[header] = header_data - else: - try: - self.headers[header] = int(data) - except ValueError: - self.headers[header] = data - - def parse_body(self, header: str, data: str) -> None: - if "Content-Encoding" in self.headers: - raise SIPParseError("Unable to parse encoded content.") - if self.headers["Content-Type"] == "application/sdp": - # Referenced RFC 4566 July 2006 - if header == "v": - # SDP 5.1 Version - self.body[header] = int(data) - elif header == "o": - # SDP 5.2 Origin - # o= # noqa: E501 - d = data.split(" ") - self.body[header] = { - "username": d[0], - "id": d[1], - "version": d[2], - "network_type": d[3], - "address_type": d[4], - "address": d[5], - } - elif header == "s": - # SDP 5.3 Session Name - # s= - self.body[header] = data - elif header == "i": - # SDP 5.4 Session Information - # i= - self.body[header] = data - elif header == "u": - # SDP 5.5 URI - # u= - self.body[header] = data - elif header == "e" or header == "p": - # SDP 5.6 Email Address and Phone Number of person - # responsible for the conference - # e= - # p= - self.body[header] = data - elif header == "c": - # SDP 5.7 Connection Data - # c= - if "c" not in self.body: - self.body["c"] = [] - d = data.split(" ") - # TTL Data and Multicast addresses may be specified. - # For IPv4 its listed as addr/ttl/number of addresses. - # c=IN IP4 224.2.1.1/127/3 means: - # c=IN IP4 224.2.1.1/127 - # c=IN IP4 224.2.1.2/127 - # c=IN IP4 224.2.1.3/127 - # With the TTL being 127. - # IPv6 does not support time to live so you will only see a '/' - # for multicast addresses. - if "/" in d[2]: - if d[1] == "IP6": - self.body[header].append( - { - "network_type": d[0], - "address_type": d[1], - "address": d[2].split("/")[0], - "ttl": None, - "address_count": int(d[2].split("/")[1]), - } - ) - else: - address_data = d[2].split("/") - if len(address_data) == 2: - self.body[header].append( - { - "network_type": d[0], - "address_type": d[1], - "address": address_data[0], - "ttl": int(address_data[1]), - "address_count": 1, - } - ) - else: - self.body[header].append( - { - "network_type": d[0], - "address_type": d[1], - "address": address_data[0], - "ttl": int(address_data[1]), - "address_count": int(address_data[2]), - } - ) - else: - self.body[header].append( - { - "network_type": d[0], - "address_type": d[1], - "address": d[2], - "ttl": None, - "address_count": 1, - } - ) - elif header == "b": - # SDP 5.8 Bandwidth - # b=: - # A bwtype of CT means Conference Total between all medias - # and all devices in the conference. - # A bwtype of AS means Applicaton Specific total for this - # media and this device. - # The bandwidth is given in kilobits per second. - # As this was written in 2006, this could be Kibibits. - # TODO: Implement Bandwidth restrictions - d = data.split(":") - self.body[header] = {"type": d[0], "bandwidth": d[1]} - elif header == "t": - # SDP 5.9 Timing - # t= - d = data.split(" ") - self.body[header] = {"start": d[0], "stop": d[1]} - elif header == "r": - # SDP 5.10 Repeat Times - # r= # noqa: E501 - d = data.split(" ") - self.body[header] = { - "repeat": d[0], - "duration": d[1], - "offset1": d[2], - "offset2": d[3], - } - elif header == "z": - # SDP 5.11 Time Zones - # z= .... - # Used for change in timezones such as day light savings time. - d = data.split() - amount = len(d) / 2 - self.body[header] = {} - for x in range(int(amount)): - self.body[header]["adjustment-time" + str(x)] = d[x * 2] - self.body[header]["offset" + str(x)] = d[x * 2 + 1] - elif header == "k": - # SDP 5.12 Encryption Keys - # k= - # k=: - if ":" in data: - d = data.split(":") - self.body[header] = {"method": d[0], "key": d[1]} - else: - self.body[header] = {"method": d} - elif header == "m": - # SDP 5.14 Media Descriptions - # m= / ... - # should be even, and +1 should be the RTCP port. - # should coinside with number of - # addresses in SDP 5.7 c= - if "m" not in self.body: - self.body["m"] = [] - d = data.split(" ") - - if "/" in d[1]: - ports_raw = d[1].split("/") - port = ports_raw[0] - count = int(ports_raw[1]) - else: - port = d[1] - count = 1 - methods = d[3:] - - self.body["m"].append( - { - "type": d[0], - "port": int(port), - "port_count": count, - "protocol": pyVoIP.RTP.RTPProtocol(d[2]), - "methods": methods, - "attributes": {}, - } - ) - for x in self.body["m"][-1]["methods"]: - self.body["m"][-1]["attributes"][x] = {} - elif header == "a": - # SDP 5.13 Attributes & 6.0 SDP Attributes - # a= - # a=: - - if "a" not in self.body: - self.body["a"] = {} - - if ":" in data: - d = data.split(":") - attribute = d[0] - value = d[1] - else: - attribute = data - value = None - - if value is not None: - if attribute == "rtpmap": - # a=rtpmap: / [/] # noqa: E501 - v = regex.SDP_A_SPLIT.split(value) - for t in self.body["m"]: - if v[0] in t["methods"]: - index = int(self.body["m"].index(t)) - break - if len(v) == 4: - encoding = v[3] - else: - encoding = None - - self.body["m"][index]["attributes"][v[0]]["rtpmap"] = { - "id": v[0], - "name": v[1], - "frequency": v[2], - "encoding": encoding, - } - - elif attribute == "fmtp": - # a=fmtp: - d = value.split(" ") - for t in self.body["m"]: - if d[0] in t["methods"]: - index = int(self.body["m"].index(t)) - break - - self.body["m"][index]["attributes"][d[0]]["fmtp"] = { - "id": d[0], - "settings": d[1:], - } - else: - self.body["a"][attribute] = value - else: - if ( - attribute == "recvonly" - or attribute == "sendrecv" - or attribute == "sendonly" - or attribute == "inactive" - ): - self.body["a"][ - "transmit_type" - ] = pyVoIP.RTP.TransmitType( - attribute - ) # noqa: E501 - else: - self.body[header] = data - - else: - self.body["content"] = data - - @staticmethod - def parse_raw_header( - headers_raw: List[bytes], handle: Callable[[str, str], None] - ) -> None: - headers: Dict[str, Any] = {"Via": []} - # Only use first occurance of VIA header field; - # got second VIA from Kamailio running in DOCKER - # According to RFC 3261 these messages should be - # discarded in a response - for x in headers_raw: - i = str(x, "utf8").split(": ") - if i[0] == "Via": - headers["Via"].append(i[1]) - if i[0] not in headers.keys(): - headers[i[0]] = i[1] - - for key, val in headers.items(): - handle(key, val) - - @staticmethod - def parse_raw_body( - body: bytes, ctype: str, handle: Callable[[str, str], None] - ) -> None: - if len(body) > 0: - if ctype == "application/sdp": - body_raw = body.split(b"\r\n") - for x in body_raw: - i = str(x, "utf8").split("=") - if i != [""]: - handle(i[0], i[1]) - else: - handle("", body) - - def parse_sip_response(self, data: bytes) -> None: - headers, body = data.split(b"\r\n\r\n") - - headers_raw = headers.split(b"\r\n") - self.version = self.heading[0] - if self.version not in self.SIPCompatibleVersions: - raise SIPParseError(f"SIP Version {self.version} not compatible.") - - self.status = SIPStatus(int(self.heading[1])) - - self.parse_raw_header(headers_raw, self.parse_header) - - self.parse_raw_body( - body, - self.headers.get("Content-Type", "text/plain"), - self.parse_body, - ) - - def parse_sip_request(self, data: bytes) -> None: - headers, body = data.split(b"\r\n\r\n") - - headers_raw = headers.split(b"\r\n") - self.version = self.heading[2] - if self.version not in self.SIPCompatibleVersions: - raise SIPParseError(f"SIP Version {self.version} not compatible.") - - self.method = self.heading[0] - self.to = self.__get_uri_header(self.heading[1]) - - self.parse_raw_header(headers_raw, self.parse_header) - - self.parse_raw_body( - body, - self.headers.get("Content-Type", "text/plain"), - self.parse_body, - ) diff --git a/pyVoIP/SIP/message/__init__.py b/pyVoIP/SIP/message/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/pyVoIP/SIP/message/message.py b/pyVoIP/SIP/message/message.py new file mode 100644 index 0000000..11e77f9 --- /dev/null +++ b/pyVoIP/SIP/message/message.py @@ -0,0 +1,179 @@ +from enum import Enum +from pyVoIP import regex +from pyVoIP.SIP.error import SIPParseError +from pyVoIP.SIP.message.parse import ( + parse_raw_headers, + parse_raw_body, + get_uri_header, +) +from pyVoIP.response_codes import ResponseCode +from pyVoIP.types import URI_HEADER +from typing import Any, Dict, List, Union +import pyVoIP + + +__all__ = ["SIPMethod", "SIPMessage", "SIPRequest", "SIPResponse"] + + +debug = pyVoIP.debug + + +class SIPMethod(Enum): + INVITE = "INVITE" + ACK = "ACK" + BYE = "BYE" + CANCEL = "CANCEL" + OPTIONS = "OPTIONS" + NOTIFY = "NOTIFY" + + +class SIPMessage: + def __init__( + self, + start_line: str, + headers: Dict[str, Any], + body: Dict[str, Any], + authentication: Dict[str, Union[str, List[str]]], + raw: bytes, + ): + self.start_line = start_line + self.headers = headers + self.body = body + self.authentication = authentication + self.raw = raw + + def summary(self) -> str: + data = "" + data += f"{' '.join(self.start_line)}\n\n" + data += "Headers:\n" + for x in self.headers: + data += f"{x}: {self.headers[x]}\n" + data += "\n" + data += "Body:\n" + for x in self.body: + data += f"{x}: {self.body[x]}\n" + data += "\n" + data += "Raw:\n" + data += str(self.raw) + + return data + + @staticmethod + def from_bytes(data: bytes) -> "SIPMessage": + parsed_headers: Dict[str, Any] = {"Via": []} + body: Dict[str, Any] = {} + authentication: Dict[str, Union[str, List[str]]] = {} + version_match = regex.SIP_VERSION_MATCH + + try: + try: + headers, body = data.split(b"\r\n\r\n") + except ValueError as ve: + debug(f"Error unpacking data, only using headers. ({ve})") + headers = data + body = b"" + + headers_raw = headers.split(b"\r\n") + start_line = str(headers_raw.pop(0), "utf8").split(" ") + check = start_line[0] + + response = False + + if version_match.match(check): + if check.upper() not in pyVoIP.SIPCompatibleVersions: + raise SIPParseError(f"SIP Version {check} not compatible.") + + response = True + status = ResponseCode(int(start_line[1])) + else: + if start_line[2].upper() not in pyVoIP.SIPCompatibleVersions: + raise SIPParseError( + f"SIP Version {start_line[2]} not compatible." + ) + if start_line[0] not in SIPMethod: + raise SIPParseError( + f"SIP Method `{start_line[0]}` not supported." + ) + + method = SIPMethod(start_line[0]) + destination = get_uri_header(start_line[1]) + + parsed_headers = parse_raw_headers(headers_raw) + + authentication = {} + if "WWW-Authenticate" in parsed_headers: + authentication = parsed_headers["WWW-Authenticate"] + elif "Authorization" in parsed_headers: + authentication = parsed_headers["Authorization"] + elif "Proxy-Authenticate" in parsed_headers: + authentication = parsed_headers["Proxy-Authenticate"] + + parsed_body = parse_raw_body( + body, parsed_headers.get("Content-Type", "text/plain") + ) + + if response: + return SIPResponse( + start_line, + parsed_headers, + parsed_body, + authentication, + data, + status, + ) + return SIPRequest( + start_line, + parsed_headers, + parsed_body, + authentication, + data, + method, + destination, + ) + + except Exception as e: + if type(e) is not SIPParseError: + raise SIPParseError(e) from e + raise + + +class SIPRequest(SIPMessage): + def __init__( + self, + start_line: str, + headers: Dict[str, Any], + body: Dict[str, Any], + authentication: Dict[str, Union[str, List[str]]], + raw: bytes, + method: SIPMethod, + destination: URI_HEADER, + ): + super().__init__(start_line, headers, body, authentication, raw) + self.method = method + self.destination = destination + + @property + def destination(self) -> URI_HEADER: + """ + The destination property specifies the Request-URI in the Request-Line + detailed in RFC 3261 Section 7.1 + """ + return self._destination + + @destination.setter + def destination(self, value: URI_HEADER) -> None: + self._destination = value + + +class SIPResponse(SIPMessage): + def __init__( + self, + start_line: str, + headers: Dict[str, Any], + body: Dict[str, Any], + authentication: Dict[str, Union[str, List[str]]], + raw: bytes, + status: ResponseCode, + ): + super().__init__(start_line, headers, body, authentication, raw) + self.status = status diff --git a/pyVoIP/SIP/message/parse.py b/pyVoIP/SIP/message/parse.py new file mode 100644 index 0000000..064f0de --- /dev/null +++ b/pyVoIP/SIP/message/parse.py @@ -0,0 +1,435 @@ +from typing import Any, Dict, List +from pyVoIP import regex +from pyVoIP.types import URI_HEADER +from pyVoIP.SIP.error import SIPParseError +import pyVoIP + + +# Compacts defined in RFC 3261 Section 7.3.3 and 20 +COMPACT_KEY = { + "i": "Call-ID", + "m": "Contact", + "e": "Content-Encoding", + "l": "Content-Length", + "c": "Content-Type", + "f": "From", + "s": "Subject", + "k": "Supported", + "t": "To", + "v": "Via", +} + + +def parse_raw_headers(raw_headers: List[bytes]) -> Dict[str, Any]: + headers: Dict[str, Any] = {"Via": []} + # Only use first occurance of VIA header field; + # got second VIA from Kamailio running in DOCKER + # According to RFC 3261 these messages should be + # discarded in a response + for x in raw_headers: + i = str(x, "utf8").split(": ") + if i[0] == "Via": + headers["Via"].append(i[1]) + if i[0] not in headers.keys(): + headers[i[0]] = i[1] + + parsed_headers: Dict[str, Any] = {} + for key, val in headers.items(): + parsed_headers[key] = parse_header(key, val) + return parsed_headers + + +def parse_raw_body(body: bytes, ctype: str) -> Dict[str, Any]: + if len(body) > 0: + if ctype == "application/sdp": + parsed_body: Dict[str, Any] = {} + body_raw = body.split(b"\r\n") + for x in body_raw: + i = str(x, "utf8").split("=") + if i != [""]: + parse_sdp_tag(parsed_body, i[0], i[1]) + return parsed_body + else: + return {"content": body} + return {"content": None} + + +def get_uri_header(data: str) -> URI_HEADER: + info = data.split(";tag=") + tag = "" + if len(info) >= 2: + tag = info[1] + raw = data + reg = regex.TO_FROM_MATCH + direct = "@" not in data + if direct: + reg = regex.TO_FROM_DIRECT_MATCH + match = reg.match(data) + if match is None: + raise SIPParseError( + "Regex failed to match To/From.\n\n" + + "Please open a GitHub Issue at " + + "https://www.github.com/tayler6000/pyVoIP " + + "and include the following:\n\n" + + f"{data=} {type(match)=}" + ) + matches = match.groupdict() + if direct: + matches["user"] = "" + matches["password"] = "" + uri = f'{matches["uri_type"]}:{matches["user"]}@{matches["host"]}' + if direct: + uri = f'{matches["uri_type"]}:{matches["host"]}' + if matches["port"]: + uri += matches["port"] + uri_type = matches["uri_type"] + user = matches["user"] + password = matches["password"].strip(":") if matches["password"] else "" + display_name = ( + matches["display_name"].strip().strip('"') + if matches["display_name"] + else "" + ) + host = matches["host"] + port = int(matches["port"].strip(":")) if matches["port"] else 5060 + + return { + "raw": raw, + "tag": tag, + "uri": uri, + "uri-type": uri_type, + "user": user, + "password": password, + "display-name": display_name, + "host": host, + "port": port, + } + + +def parse_header(header: str, data: str) -> Any: + if header in COMPACT_KEY.keys(): + header = COMPACT_KEY[header] + + if header == "Via": + vias = [] + for d in data: + info = regex.VIA_SPLIT.split(d) + _type = info[0] # SIP Method + _address = info[1].split(":") # Tuple: address, port + _ip = _address[0] + + """ + If no port is provided in via header assume default port. + Needs to be str. Check response build for better str creation + """ + _port = int(info[1].split(":")[1]) if len(_address) > 1 else 5060 + _via = {"type": _type, "address": (_ip, _port)} + + """ + Sets branch, maddr, ttl, received, and rport if defined + as per RFC 3261 20.7 + """ + for x in info[2:]: + if "=" in x: + try: + _via[x.split("=")[0]] = int(x.split("=")[1]) + except ValueError: + _via[x.split("=")[0]] = x.split("=")[1] + else: + _via[x] = None + vias.append(_via) + return vias + elif header in ["To", "From", "Contact", "Refer-To"]: + return get_uri_header(data) + elif header == "CSeq": + return { + "check": int(data.split(" ")[0]), + "method": data.split(" ")[1], + } + elif header in ["Allow", "Supported", "Require"]: + return data.split(", ") + elif header == "Call-ID": + return data + elif header in ( + "WWW-Authenticate", + "Authorization", + "Proxy-Authenticate", + ): + method = data.split(" ")[0] + data = data.replace(f"{method} ", "") + auth_match = regex.AUTH_MATCH + row_data = auth_match.findall(data) + header_data: Dict[str, Any] = {"header": header, "method": method} + for var, data in row_data: + if var == "userhash": + header_data[var] = ( + False if data.strip('"').lower() == "false" else True + ) + continue + if var == "qop": + authorized = data.strip('"').split(",") + for i, value in enumerate(authorized): + authorized[i] = value.strip() + header_data[var] = authorized + continue + header_data[var] = data.strip('"') + return header_data + elif header == "Target-Dialog": + # Target-Dialog (tdialog) is specified in RFC 4538 + params = data.split(";") + header_data: Dict[str, Any] = { + "callid": params.pop(0) + } # key is callid to be consitenent with RFC 4538 Section 7 + for x in params: + y = x.split("=") + header_data[y[0]] = y[1] + return header_data + elif header == "Refer-Sub": + # Refer-Sub (norefersub) is specified in RFC 4488 + params = data.split(";") + header_data: Dict[str, Any] = { + "value": True if params.pop(0) == "true" else False + } # BNF states extens are possible + for x in params: + y = x.split("=") + header_data[y[0]] = y[1] + return header_data + else: + try: + return int(data) + except ValueError: + return data + + +def parse_sdp_tag(parsed_body: Dict[str, Any], field: str, data: str) -> Any: + # Referenced RFC 4566 July 2006 + if field == "v": + # SDP 5.1 Version + parsed_body[field] = int(data) + elif field == "o": + # SDP 5.2 Origin + # o= # noqa: E501 + d = data.split(" ") + parsed_body[field] = { + "username": d[0], + "id": d[1], + "version": d[2], + "network_type": d[3], + "address_type": d[4], + "address": d[5], + } + elif field == "s": + # SDP 5.3 Session Name + # s= + parsed_body[field] = data + elif field == "i": + # SDP 5.4 Session Information + # i= + parsed_body[field] = data + elif field == "u": + # SDP 5.5 URI + # u= + parsed_body[field] = data + elif field == "e" or field == "p": + # SDP 5.6 Email Address and Phone Number of person + # responsible for the conference + # e= + # p= + parsed_body[field] = data + elif field == "c": + # SDP 5.7 Connection Data + # c= + if "c" not in parsed_body: + parsed_body["c"] = [] + d = data.split(" ") + # TTL Data and Multicast addresses may be specified. + # For IPv4 its listed as addr/ttl/number of addresses. + # c=IN IP4 224.2.1.1/127/3 means: + # c=IN IP4 224.2.1.1/127 + # c=IN IP4 224.2.1.2/127 + # c=IN IP4 224.2.1.3/127 + # With the TTL being 127. + # IPv6 does not support time to live so you will only see a '/' + # for multicast addresses. + if "/" in d[2]: + if d[1] == "IP6": + parsed_body[field].append( + { + "network_type": d[0], + "address_type": d[1], + "address": d[2].split("/")[0], + "ttl": None, + "address_count": int(d[2].split("/")[1]), + } + ) + else: + address_data = d[2].split("/") + if len(address_data) == 2: + parsed_body[field].append( + { + "network_type": d[0], + "address_type": d[1], + "address": address_data[0], + "ttl": int(address_data[1]), + "address_count": 1, + } + ) + else: + parsed_body[field].append( + { + "network_type": d[0], + "address_type": d[1], + "address": address_data[0], + "ttl": int(address_data[1]), + "address_count": int(address_data[2]), + } + ) + else: + parsed_body[field].append( + { + "network_type": d[0], + "address_type": d[1], + "address": d[2], + "ttl": None, + "address_count": 1, + } + ) + elif field == "b": + # SDP 5.8 Bandwidth + # b=: + # A bwtype of CT means Conference Total between all medias + # and all devices in the conference. + # A bwtype of AS means Applicaton Specific total for this + # media and this device. + # The bandwidth is given in kilobits per second. + # As this was written in 2006, this could be Kibibits. + # TODO: Implement Bandwidth restrictions + d = data.split(":") + parsed_body[field] = {"type": d[0], "bandwidth": d[1]} + elif field == "t": + # SDP 5.9 Timing + # t= + d = data.split(" ") + parsed_body[field] = {"start": d[0], "stop": d[1]} + elif field == "r": + # SDP 5.10 Repeat Times + # r= # noqa: E501 + d = data.split(" ") + parsed_body[field] = { + "repeat": d[0], + "duration": d[1], + "offset1": d[2], + "offset2": d[3], + } + elif field == "z": + # SDP 5.11 Time Zones + # z= .... + # Used for change in timezones such as day light savings time. + d = data.split() + amount = len(d) / 2 + parsed_body[field] = {} + for x in range(int(amount)): + parsed_body[field]["adjustment-time" + str(x)] = d[x * 2] + parsed_body[field]["offset" + str(x)] = d[x * 2 + 1] + elif field == "k": + # SDP 5.12 Encryption Keys + # k= + # k=: + if ":" in data: + d = data.split(":") + parsed_body[field] = {"method": d[0], "key": d[1]} + else: + parsed_body[field] = {"method": d} + elif field == "m": + # SDP 5.14 Media Descriptions + # m= / ... + # should be even, and +1 should be the RTCP port. + # should coinside with number of + # addresses in SDP 5.7 c= + if "m" not in parsed_body: + parsed_body["m"] = [] + d = data.split(" ") + + if "/" in d[1]: + ports_raw = d[1].split("/") + port = ports_raw[0] + count = int(ports_raw[1]) + else: + port = d[1] + count = 1 + methods = d[3:] + + parsed_body["m"].append( + { + "type": d[0], + "port": int(port), + "port_count": count, + "protocol": pyVoIP.RTP.RTPProtocol(d[2]), + "methods": methods, + "attributes": {}, + } + ) + for x in parsed_body["m"][-1]["methods"]: + parsed_body["m"][-1]["attributes"][x] = {} + elif field == "a": + # SDP 5.13 Attributes & 6.0 SDP Attributes + # a= + # a=: + + if "a" not in parsed_body: + parsed_body["a"] = {} + + if ":" in data: + d = data.split(":") + attribute = d[0] + value = d[1] + else: + attribute = data + value = None + + if value is not None: + if attribute == "rtpmap": + # a=rtpmap: / [/] # noqa: E501 + v = regex.SDP_A_SPLIT.split(value) + for t in parsed_body["m"]: + if v[0] in t["methods"]: + index = int(parsed_body["m"].index(t)) + break + if len(v) == 4: + encoding = v[3] + else: + encoding = None + + parsed_body["m"][index]["attributes"][v[0]]["rtpmap"] = { + "id": v[0], + "name": v[1], + "frequency": v[2], + "encoding": encoding, + } + + elif attribute == "fmtp": + # a=fmtp: + d = value.split(" ") + for t in parsed_body["m"]: + if d[0] in t["methods"]: + index = int(parsed_body["m"].index(t)) + break + + parsed_body["m"][index]["attributes"][d[0]]["fmtp"] = { + "id": d[0], + "settings": d[1:], + } + else: + parsed_body["a"][attribute] = value + else: + if ( + attribute == "recvonly" + or attribute == "sendrecv" + or attribute == "sendonly" + or attribute == "inactive" + ): + parsed_body["a"]["transmit_type"] = pyVoIP.RTP.TransmitType( + attribute + ) # noqa: E501 + else: + parsed_body[field] = data diff --git a/pyVoIP/SIP/message/response_codes.py b/pyVoIP/SIP/message/response_codes.py new file mode 100644 index 0000000..684d52a --- /dev/null +++ b/pyVoIP/SIP/message/response_codes.py @@ -0,0 +1,267 @@ +from enum import Enum + + +class ResponseCode(Enum): + def __new__(cls, value: int, phrase: str = "", description: str = ""): + obj = object.__new__(cls) + obj._value_ = value + + obj.phrase = phrase + obj.description = description + return obj + + def __int__(self) -> int: + return self._value_ + + def __str__(self) -> str: + return f"{self._value_} {self.phrase}" + + @property + def phrase(self) -> str: + return self._phrase + + @phrase.setter + def phrase(self, value: str) -> None: + self._phrase = value + + @property + def description(self) -> str: + return self._description + + @description.setter + def description(self, value: str) -> None: + self._description = value + + # Informational + TRYING = ( + 100, + "Trying", + "Extended search being performed, may take a significant time", + ) + RINGING = ( + 180, + "Ringing", + "Destination user agent received INVITE, " + + "and is alerting user of call", + ) + FORWARDED = 181, "Call is Being Forwarded" + QUEUED = 182, "Queued" + SESSION_PROGRESS = 183, "Session Progress" + TERMINATED = 199, "Early Dialog Terminated" + + # Success + OK = 200, "OK", "Request successful" + ACCEPTED = ( + 202, + "Accepted", + "Request accepted, processing continues (Deprecated.)", + ) + NO_NOTIFICATION = ( + 204, + "No Notification", + "Request fulfilled, nothing follows", + ) + + # Redirection + MULTIPLE_CHOICES = ( + 300, + "Multiple Choices", + "Object has several resources -- see URI list", + ) + MOVED_PERMANENTLY = ( + 301, + "Moved Permanently", + "Object moved permanently -- see URI list", + ) + MOVED_TEMPORARILY = ( + 302, + "Moved Temporarily", + "Object moved temporarily -- see URI list", + ) + USE_PROXY = ( + 305, + "Use Proxy", + "You must use proxy specified in Location to " + + "access this resource", + ) + ALTERNATE_SERVICE = ( + 380, + "Alternate Service", + "The call failed, but alternatives are available -- see URI list", + ) + + # Client Error + BAD_REQUEST = ( + 400, + "Bad Request", + "Bad request syntax or unsupported method", + ) + UNAUTHORIZED = ( + 401, + "Unauthorized", + "No permission -- see authorization schemes", + ) + PAYMENT_REQUIRED = ( + 402, + "Payment Required", + "No payment -- see charging schemes", + ) + FORBIDDEN = ( + 403, + "Forbidden", + "Request forbidden -- authorization will not help", + ) + NOT_FOUND = (404, "Not Found", "Nothing matches the given URI") + METHOD_NOT_ALLOWED = ( + 405, + "Method Not Allowed", + "Specified method is invalid for this resource", + ) + NOT_ACCEPTABLE = ( + 406, + "Not Acceptable", + "URI not available in preferred format", + ) + PROXY_AUTHENTICATION_REQUIRED = ( + 407, + "Proxy Authentication Required", + "You must authenticate with this proxy before proceeding", + ) + REQUEST_TIMEOUT = ( + 408, + "Request Timeout", + "Request timed out; try again later", + ) + CONFLICT = 409, "Conflict", "Request conflict" + GONE = ( + 410, + "Gone", + "URI no longer exists and has been permanently removed", + ) + LENGTH_REQUIRED = ( + 411, + "Length Required", + "Client must specify Content-Length", + ) + CONDITIONAL_REQUEST_FAILED = 412, "Conditional Request Failed" + REQUEST_ENTITY_TOO_LARGE = ( + 413, + "Request Entity Too Large", + "Entity is too large", + ) + REQUEST_URI_TOO_LONG = 414, "Request-URI Too Long", "URI is too long" + UNSUPPORTED_MEDIA_TYPE = ( + 415, + "Unsupported Media Type", + "Entity body in unsupported format", + ) + UNSUPPORTED_URI_SCHEME = ( + 416, + "Unsupported URI Scheme", + "Cannot satisfy request", + ) + UNKOWN_RESOURCE_PRIORITY = ( + 417, + "Unkown Resource-Priority", + "There was a resource-priority option tag, " + + "but no Resource-Priority header", + ) + BAD_EXTENSION = ( + 420, + "Bad Extension", + "Bad SIP Protocol Extension used, not understood by the server.", + ) + EXTENSION_REQUIRED = ( + 421, + "Extension Required", + "Server requeires a specific extension to be " + + "listed in the Supported header.", + ) + SESSION_INTERVAL_TOO_SMALL = 422, "Session Interval Too Small" + SESSION_INTERVAL_TOO_BRIEF = 423, "Session Interval Too Breif" + BAD_LOCATION_INFORMATION = 424, "Bad Location Information" + USE_IDENTITY_HEADER = ( + 428, + "Use Identity Header", + "The server requires an Identity header, " + + "and one has not been provided.", + ) + PROVIDE_REFERRER_IDENTITY = 429, "Provide Referrer Identity" + """ + This response is intended for use between proxy devices, + and should not be seen by an endpoint. If it is seen by one, + it should be treated as a 400 Bad Request response. + """ + FLOW_FAILED = ( + 430, + "Flow Failed", + "A specific flow to a user agent has failed, " + + "although other flows may succeed.", + ) + ANONYMITY_DISALLOWED = 433, "Anonymity Disallowed" + BAD_IDENTITY_INFO = 436, "Bad Identity-Info" + UNSUPPORTED_CERTIFICATE = 437, "Unsupported Certificate" + INVALID_IDENTITY_HEADER = 438, "Invalid Identity Header" + FIRST_HOP_LACKS_OUTBOUND_SUPPORT = 439, "First Hop Lacks Outbound Support" + MAX_BREADTH_EXCEEDED = 440, "Max-Breadth Exceeded" + BAD_INFO_PACKAGE = 469, "Bad Info Package" + CONSENT_NEEDED = 470, "Consent Needed" + TEMPORARILY_UNAVAILABLE = 480, "Temporarily Unavailable" + CALL_OR_TRANSACTION_DOESNT_EXIST = 481, "Call/Transaction Does Not Exist" + LOOP_DETECTED = 482, "Loop Detected" + TOO_MANY_HOPS = 483, "Too Many Hops" + ADDRESS_INCOMPLETE = 484, "Address Incomplete" + AMBIGUOUS = 485, "Ambiguous" + BUSY_HERE = 486, "Busy Here", "Callee is busy" + REQUEST_TERMINATED = 487, "Request Terminated" + NOT_ACCEPTABLE_HERE = 488, "Not Acceptable Here" + BAD_EVENT = 489, "Bad Event" + REQUEST_PENDING = 491, "Request Pending" + UNDECIPHERABLE = 493, "Undecipherable" + SECURITY_AGREEMENT_REQUIRED = 494, "Security Agreement Required" + + # Server Errors + INTERNAL_SERVER_ERROR = ( + 500, + "Internal Server Error", + "Server got itself in trouble", + ) + NOT_IMPLEMENTED = ( + 501, + "Not Implemented", + "Server does not support this operation", + ) + BAD_GATEWAY = ( + 502, + "Bad Gateway", + "Invalid responses from another server/proxy", + ) + SERVICE_UNAVAILABLE = ( + 503, + "Service Unavailable", + "The server cannot process the request due to a high load", + ) + GATEWAY_TIMEOUT = ( + 504, + "Server Timeout", + "The server did not receive a timely response", + ) + SIP_VERSION_NOT_SUPPORTED = ( + 505, + "SIP Version Not Supported", + "Cannot fulfill request", + ) + MESSAGE_TOO_LONG = 513, "Message Too Long" + PUSH_NOTIFICATION_SERVICE_NOT_SUPPORTED = ( + 555, + "Push Notification Service Not Supported", + ) + PRECONDITION_FAILURE = 580, "Precondition Failure" + + # Global Failure Responses + BUSY_EVERYWHERE = 600, "Busy Everywhere" + DECLINE = 603, "Decline" + DOES_NOT_EXIST_ANYWHERE = 604, "Does Not Exist Anywhere" + GLOBAL_NOT_ACCEPTABLE = 606, "Not Acceptable" + UNWANTED = 607, "Unwanted" + REJECTED = 608, "Rejected" diff --git a/pyVoIP/__init__.py b/pyVoIP/__init__.py index 6ceacf2..3959ba0 100644 --- a/pyVoIP/__init__.py +++ b/pyVoIP/__init__.py @@ -92,7 +92,6 @@ def debug(s, e=None): # noqa because import will fail if debug is not defined from pyVoIP.RTP import PayloadType # noqa: E402 -SIPCompatibleMethods = ["INVITE", "ACK", "BYE", "CANCEL", "OPTIONS", "NOTIFY"] SIPCompatibleVersions = ["SIP/2.0"] RTPCompatibleVersions = [2] diff --git a/pyVoIP/regex.py b/pyVoIP/regex.py index 7251529..573780f 100644 --- a/pyVoIP/regex.py +++ b/pyVoIP/regex.py @@ -19,3 +19,4 @@ r'(?P"?[\w ]*"? )?sips?):(?P[\w.]+)(?P:[0-9]+)?>?' ) SDP_A_SPLIT = re.compile(" |/") +SIP_VERSION_MATCH = re.compile(r"(?:SIP|sip)/[0-9.]+") From 50eed5e46d646e3a52a62415e603f2bf1114f1bf Mon Sep 17 00:00:00 2001 From: TJ Porter Date: Fri, 26 Jan 2024 17:31:01 -0600 Subject: [PATCH 2/5] [ADD] Added `SIPMessage.from_string` [ADD] Added repr for ResponseCode and SIPMethod enum [ADD] Added automatic map to populate SIPCompatibleMethods [CHANGE] Changed all existing code to use the new SIPMessage implementation [CHANGE] Changed all existing code to use the new ResponseCode enum [FIX] Fixed type annotation of `SIPMessage.from_bytes` [FIX] Fixed incorrect type annotations for SIPMessage and subclasses [FIX] Fixed some mypy errors --- pyVoIP/SIP/client.py | 197 +++++++++++++++------------ pyVoIP/SIP/message/message.py | 31 ++++- pyVoIP/SIP/message/parse.py | 30 ++-- pyVoIP/SIP/message/response_codes.py | 3 + pyVoIP/VoIP/call.py | 83 ++++++----- pyVoIP/VoIP/phone.py | 26 ++-- pyVoIP/__init__.py | 2 + pyVoIP/networking/sock.py | 24 ++-- tests/test_sip_requests.py | 10 +- tests/test_sip_responses.py | 11 +- 10 files changed, 248 insertions(+), 169 deletions(-) diff --git a/pyVoIP/SIP/client.py b/pyVoIP/SIP/client.py index 78b10c8..ccf32f0 100644 --- a/pyVoIP/SIP/client.py +++ b/pyVoIP/SIP/client.py @@ -11,11 +11,13 @@ InvalidAccountInfoError, RetryRequiredError, ) -from pyVoIP.SIP.message import ( +from pyVoIP.SIP.message.message import ( SIPMessage, - SIPStatus, - SIPMessageType, + SIPMethod, + SIPResponse, + SIPRequest, ) +from pyVoIP.SIP.message.response_codes import ResponseCode from pyVoIP.types import KEY_PASSWORD from pyVoIP.VoIP.status import PhoneStatus import pyVoIP @@ -35,10 +37,14 @@ UNAUTORIZED_RESPONSE_CODES = [ - SIPStatus.UNAUTHORIZED, - SIPStatus.PROXY_AUTHENTICATION_REQUIRED, + ResponseCode.UNAUTHORIZED, + ResponseCode.PROXY_AUTHENTICATION_REQUIRED, +] +INVITE_OK_RESPONSE_CODES = [ + ResponseCode.TRYING, + ResponseCode.RINGING, + ResponseCode.OK, ] -INVITE_OK_RESPONSE_CODES = [SIPStatus.TRYING, SIPStatus.RINGING, SIPStatus.OK] class SIPClient: @@ -107,7 +113,7 @@ def recv(self) -> None: raw = self.s.recv(8192) if raw != b"\x00\x00\x00\x00": try: - message = SIPMessage(raw) + message = SIPMessage.from_bytes(raw) debug(message.summary()) self.parse_message(message) except Exception as ex: @@ -133,13 +139,13 @@ def recv(self) -> None: raise def handle_new_connection(self, conn: "VoIPConnection") -> None: - message = SIPMessage(conn.peak()) - if message.type == SIPMessageType.REQUEST: - if message.method == "INVITE": + message = SIPMessage.from_bytes(conn.peak()) + if type(message) is SIPRequest: + if message.method == SIPMethod.INVITE: self._handle_invite(conn) def _handle_invite(self, conn: "VoIPConnection") -> None: - message = SIPMessage(conn.peak()) + message = SIPMessage.from_bytes(conn.peak()) if self.call_callback is None: request = self.gen_busy(message) conn.send(request) @@ -147,20 +153,20 @@ def _handle_invite(self, conn: "VoIPConnection") -> None: self.call_callback(conn, message) def parse_message(self, message: SIPMessage) -> None: - if message.type != SIPMessageType.REQUEST: + if type(message) is SIPResponse: if message.status in ( - SIPStatus.OK, - SIPStatus.NOT_FOUND, - SIPStatus.SERVICE_UNAVAILABLE, - SIPStatus.PROXY_AUTHENTICATION_REQUIRED, - SIPStatus.RINGING, - SIPStatus.BUSY_HERE, - SIPStatus.SESSION_PROGRESS, - SIPStatus.REQUEST_TERMINATED, + ResponseCode.OK, + ResponseCode.NOT_FOUND, + ResponseCode.SERVICE_UNAVAILABLE, + ResponseCode.PROXY_AUTHENTICATION_REQUIRED, + ResponseCode.RINGING, + ResponseCode.BUSY_HERE, + ResponseCode.SESSION_PROGRESS, + ResponseCode.REQUEST_TERMINATED, ): if self.call_callback is not None: self.call_callback(message) - elif message.status == SIPStatus.TRYING: + elif message.status == ResponseCode.TRYING: pass else: debug( @@ -169,38 +175,39 @@ def parse_message(self, message: SIPMessage) -> None: "TODO: Add 500 Error on Receiving SIP Response", ) return - elif message.method == "BYE": - # TODO: If callCallback is None, the call doesn't exist, 481 - if self.call_callback: - self.call_callback(message) - response = self.gen_ok(message) - try: - # BYE comes from client cause server only acts as mediator - (_sender_adress, _sender_port) = message.headers["Via"][0][ - "address" - ] - self.sendto( - response, - (_sender_adress, int(_sender_port)), - ) - except Exception: - debug("BYE Answer failed falling back to server as target") + elif type(message) is SIPRequest: + if message.method == "BYE": + # TODO: If callCallback is None, the call doesn't exist, 481 + if self.call_callback: + self.call_callback(message) + response = self.gen_ok(message) + try: + # BYE comes from client cause server only acts as mediator + (_sender_adress, _sender_port) = message.headers["Via"][0][ + "address" + ] + self.sendto( + response, + (_sender_adress, int(_sender_port)), + ) + except Exception: + debug("BYE Answer failed falling back to server as target") + self.sendto(response, message.headers["Via"]["address"]) + elif message.method == "ACK": + return + elif message.method == "CANCEL": + # TODO: If callCallback is None, the call doesn't exist, 481 + self.call_callback(message) # type: ignore + response = self.gen_ok(message) + self.sendto(response, message.headers["Via"]["address"]) + elif message.method == "OPTIONS": + if self.call_callback: + response = str(self.call_callback(message)) + else: + response = self._gen_options_response(message) self.sendto(response, message.headers["Via"]["address"]) - elif message.method == "ACK": - return - elif message.method == "CANCEL": - # TODO: If callCallback is None, the call doesn't exist, 481 - self.call_callback(message) # type: ignore - response = self.gen_ok(message) - self.sendto(response, message.headers["Via"]["address"]) - elif message.method == "OPTIONS": - if self.call_callback: - response = str(self.call_callback(message)) else: - response = self._gen_options_response(message) - self.sendto(response, message.headers["Via"]["address"]) - else: - debug("TODO: Add 400 Error on non processable request") + debug("TODO: Add 400 Error on non processable request") def start(self) -> None: if self.NSD: @@ -1089,25 +1096,32 @@ def invite( ) conn = self.sendto(invite) debug("Invited") - response = SIPMessage(conn.recv(8192)) + response = SIPMessage.from_bytes(conn.recv(8192)) while ( - response.status - not in UNAUTORIZED_RESPONSE_CODES + INVITE_OK_RESPONSE_CODES - ) or response.headers["Call-ID"] != call_id: + type(response) is SIPResponse + and ( + response.status + not in UNAUTORIZED_RESPONSE_CODES + INVITE_OK_RESPONSE_CODES + ) + or response.headers["Call-ID"] != call_id + ): if not self.NSD: break debug(f"Received Response: {response.summary()}") self.parse_message(response) - response = SIPMessage(conn.recv(8192)) + response = SIPMessage.from_bytes(conn.recv(8192)) debug(f"Received Response: {response.summary()}") - if response.status in INVITE_OK_RESPONSE_CODES: + if ( + type(response) is SIPResponse + and response.status in INVITE_OK_RESPONSE_CODES + ): debug("Invite Accepted") - if response.status is SIPStatus.OK: + if response.status is ResponseCode.OK: return response, call_id, sess_id, conn - return SIPMessage(invite.encode("utf8")), call_id, sess_id, conn + return SIPMessage.from_string(invite), call_id, sess_id, conn debug("Invite Requires Authorization") ack = self.gen_ack(response) conn.send(ack) @@ -1124,7 +1138,7 @@ def invite( conn = self.sendto(invite) - return SIPMessage(invite.encode("utf8")), call_id, sess_id, conn + return SIPMessage.from_string(invite), call_id, sess_id, conn def gen_message( self, number: str, body: str, ctype: str, branch: str, call_id: str @@ -1154,14 +1168,16 @@ def message( debug("Message") auth = False while True: - response = SIPMessage(conn.recv(8192)) + response = SIPMessage.from_bytes(conn.recv(8192)) debug(f"Received Response: {response.summary()}") self.parse_message(response) - if response.status == SIPStatus(100): + if type(response) is not SIPResponse: + continue + if response.status == ResponseCode(100): continue - if response.status == SIPStatus( + if response.status == ResponseCode( 401 - ) or response.status == SIPStatus(407): + ) or response.status == ResponseCode(407): if auth: debug("Auth Failure") break @@ -1172,7 +1188,7 @@ def message( ) conn.send(msg) continue - if response.status == SIPStatus.OK: + if response.status == ResponseCode.OK: break if self.NSD: break @@ -1188,8 +1204,8 @@ def bye(self, request: SIPMessage) -> None: request.headers["Contact"]["port"], ), ) - response = SIPMessage(conn.recv(8192)) - if response.status == SIPStatus(401): + response = SIPMessage.from_bytes(conn.recv(8192)) + if response.status == ResponseCode(401): # Requires password auth = self.gen_authorization(response) message = message.replace( @@ -1235,35 +1251,35 @@ def __deregister(self) -> bool: first_response = response conn.close() # Regardless of the response, the dialog is over - if response.status == SIPStatus(401): + if response.status == ResponseCode(401): # Unauthorized, likely due to being password protected. password_request = self.gen_register(response, deregister=True) conn = self.send(password_request) response = self.__receive(conn) conn.close() - if response.status == SIPStatus(400): + if response.status == ResponseCode(400): # Bad Request # TODO: implement # TODO: check if broken connection can be brought back # with new urn:uuid or reply with expire 0 self._handle_bad_request() - elif response.status == SIPStatus(407): + elif response.status == ResponseCode(407): # Proxy Authentication Required # TODO: implement debug("Proxy auth required") - elif response.status == SIPStatus(500): + elif response.status == ResponseCode(500): # We raise so the calling function can sleep and try again raise RetryRequiredError( "Received a 500 error when deregistering." ) - elif response.status == SIPStatus.OK: + elif response.status == ResponseCode.OK: return True - elif response.status == SIPStatus(401): + elif response.status == ResponseCode(401): # At this point, it's reasonable to assume that # this is caused by invalid credentials. debug("=" * 50) @@ -1336,33 +1352,33 @@ def __register(self) -> bool: first_response = response conn.close() # Regardless of the response, the dialog is over - if response.status == SIPStatus(401): + if response.status == ResponseCode(401): # Unauthorized, likely due to being password protected. password_request = self.gen_register(response) conn = self.send(password_request) response = self.__receive(conn) conn.close() - if response.status == SIPStatus(400): + if response.status == ResponseCode(400): # Bad Request # TODO: implement # TODO: check if broken connection can be brought back # with new urn:uuid or reply with expire 0 self._handle_bad_request() - elif response.status == SIPStatus(407): + elif response.status == ResponseCode(407): # Proxy Authentication Required # TODO: implement debug("Proxy auth required") - elif response.status == SIPStatus(500): + elif response.status == ResponseCode(500): # We raise so the calling function can sleep and try again raise RetryRequiredError("Received a 500 error when registering.") - elif response.status == SIPStatus.OK: + elif response.status == ResponseCode.OK: return True - elif response.status == SIPStatus(401): + elif response.status == ResponseCode(401): # At this point, it's reasonable to assume that # this is caused by invalid credentials. debug("=" * 50) @@ -1396,25 +1412,34 @@ def subscribe(self, lastresponse: SIPMessage) -> None: subRequest = self.gen_subscribe(lastresponse) conn = self.sendto(subRequest) - response = SIPMessage(conn.recv(8192)) + response = SIPMessage.from_bytes(conn.recv(8192)) - debug(f'Got response to subscribe: {str(response.heading, "utf8")}') + debug(f'Got response to subscribe: {str(response.start_line, "utf8")}') - def __receive(self, conn: "VoIPConnection") -> SIPMessage: + def __receive(self, conn: "VoIPConnection") -> SIPResponse: """ Some servers need time to process the response. When this happens, the first response you get from the server is - SIPStatus.TRYING. This while loop tries checks every second for an + ResponseCode.TRYING. This while loop tries checks every second for an updated response. It times out after 30 seconds with no response. """ try: - response = SIPMessage(conn.recv(8128, self.register_timeout)) - while response.status == SIPStatus.TRYING and self.NSD: - response = SIPMessage(conn.recv(8128, self.register_timeout)) + response = SIPMessage.from_bytes( + conn.recv(8128, self.register_timeout) + ) + while ( + type(response) is SIPResponse + and response.status == ResponseCode.TRYING + and self.NSD + ): + response = SIPMessage.from_bytes( + conn.recv(8128, self.register_timeout) + ) time.sleep(1) except TimeoutError: raise TimeoutError( f"Waited {self.register_timeout} seconds but the server is " + "still TRYING or has not responded." ) + assert type(response) is SIPResponse return response diff --git a/pyVoIP/SIP/message/message.py b/pyVoIP/SIP/message/message.py index 11e77f9..fa6ef72 100644 --- a/pyVoIP/SIP/message/message.py +++ b/pyVoIP/SIP/message/message.py @@ -6,7 +6,7 @@ parse_raw_body, get_uri_header, ) -from pyVoIP.response_codes import ResponseCode +from pyVoIP.SIP.message.response_codes import ResponseCode from pyVoIP.types import URI_HEADER from typing import Any, Dict, List, Union import pyVoIP @@ -25,12 +25,22 @@ class SIPMethod(Enum): CANCEL = "CANCEL" OPTIONS = "OPTIONS" NOTIFY = "NOTIFY" + REGISTER = "REGISTER" + MESSAGE = "MESSAGE" + SUBSCRIBE = "SUBSCRIBE" + REFER = "REFER" + + def __str__(self) -> str: + return self._value_ + + def __repr__(self) -> str: + return str(self) class SIPMessage: def __init__( self, - start_line: str, + start_line: List[str], headers: Dict[str, Any], body: Dict[str, Any], authentication: Dict[str, Union[str, List[str]]], @@ -59,9 +69,9 @@ def summary(self) -> str: return data @staticmethod - def from_bytes(data: bytes) -> "SIPMessage": + def from_bytes(data: bytes) -> Union["SIPRequest", "SIPResponse"]: parsed_headers: Dict[str, Any] = {"Via": []} - body: Dict[str, Any] = {} + parsed_body: Dict[str, Any] = {} authentication: Dict[str, Union[str, List[str]]] = {} version_match = regex.SIP_VERSION_MATCH @@ -136,11 +146,20 @@ def from_bytes(data: bytes) -> "SIPMessage": raise SIPParseError(e) from e raise + @staticmethod + def from_string(data: str) -> Union["SIPRequest", "SIPResponse"]: + try: + return SIPMessage.from_bytes(data.encode("utf8")) + except Exception as e: + if type(e) is not SIPParseError: + raise SIPParseError(e) from e + raise + class SIPRequest(SIPMessage): def __init__( self, - start_line: str, + start_line: List[str], headers: Dict[str, Any], body: Dict[str, Any], authentication: Dict[str, Union[str, List[str]]], @@ -168,7 +187,7 @@ def destination(self, value: URI_HEADER) -> None: class SIPResponse(SIPMessage): def __init__( self, - start_line: str, + start_line: List[str], headers: Dict[str, Any], body: Dict[str, Any], authentication: Dict[str, Union[str, List[str]]], diff --git a/pyVoIP/SIP/message/parse.py b/pyVoIP/SIP/message/parse.py index 064f0de..1fefc5e 100644 --- a/pyVoIP/SIP/message/parse.py +++ b/pyVoIP/SIP/message/parse.py @@ -35,6 +35,9 @@ def parse_raw_headers(raw_headers: List[bytes]) -> Dict[str, Any]: parsed_headers: Dict[str, Any] = {} for key, val in headers.items(): + if key in COMPACT_KEY.keys(): + key = COMPACT_KEY[key] + parsed_headers[key] = parse_header(key, val) return parsed_headers @@ -107,9 +110,6 @@ def get_uri_header(data: str) -> URI_HEADER: def parse_header(header: str, data: str) -> Any: - if header in COMPACT_KEY.keys(): - header = COMPACT_KEY[header] - if header == "Via": vias = [] for d in data: @@ -159,10 +159,10 @@ def parse_header(header: str, data: str) -> Any: data = data.replace(f"{method} ", "") auth_match = regex.AUTH_MATCH row_data = auth_match.findall(data) - header_data: Dict[str, Any] = {"header": header, "method": method} + auth_data: Dict[str, Any] = {"header": header, "method": method} for var, data in row_data: if var == "userhash": - header_data[var] = ( + auth_data[var] = ( False if data.strip('"').lower() == "false" else True ) continue @@ -170,30 +170,30 @@ def parse_header(header: str, data: str) -> Any: authorized = data.strip('"').split(",") for i, value in enumerate(authorized): authorized[i] = value.strip() - header_data[var] = authorized + auth_data[var] = authorized continue - header_data[var] = data.strip('"') - return header_data + auth_data[var] = data.strip('"') + return auth_data elif header == "Target-Dialog": # Target-Dialog (tdialog) is specified in RFC 4538 params = data.split(";") - header_data: Dict[str, Any] = { + td_data: Dict[str, Any] = { "callid": params.pop(0) } # key is callid to be consitenent with RFC 4538 Section 7 for x in params: y = x.split("=") - header_data[y[0]] = y[1] - return header_data + td_data[y[0]] = y[1] + return td_data elif header == "Refer-Sub": # Refer-Sub (norefersub) is specified in RFC 4488 params = data.split(";") - header_data: Dict[str, Any] = { + rs_data: Dict[str, Any] = { "value": True if params.pop(0) == "true" else False } # BNF states extens are possible for x in params: y = x.split("=") - header_data[y[0]] = y[1] - return header_data + rs_data[y[0]] = y[1] + return rs_data else: try: return int(data) @@ -339,7 +339,7 @@ def parse_sdp_tag(parsed_body: Dict[str, Any], field: str, data: str) -> Any: d = data.split(":") parsed_body[field] = {"method": d[0], "key": d[1]} else: - parsed_body[field] = {"method": d} + parsed_body[field] = {"method": data} elif field == "m": # SDP 5.14 Media Descriptions # m= / ... diff --git a/pyVoIP/SIP/message/response_codes.py b/pyVoIP/SIP/message/response_codes.py index 684d52a..4c389a6 100644 --- a/pyVoIP/SIP/message/response_codes.py +++ b/pyVoIP/SIP/message/response_codes.py @@ -16,6 +16,9 @@ def __int__(self) -> int: def __str__(self) -> str: return f"{self._value_} {self.phrase}" + def __repr__(self) -> str: + return str(self) + @property def phrase(self) -> str: return self._phrase diff --git a/pyVoIP/VoIP/call.py b/pyVoIP/VoIP/call.py index 89ba0dc..f16addd 100644 --- a/pyVoIP/VoIP/call.py +++ b/pyVoIP/VoIP/call.py @@ -1,7 +1,13 @@ from enum import Enum from pyVoIP import RTP from pyVoIP.SIP.error import SIPParseError -from pyVoIP.SIP.message import SIPMessage, SIPMessageType, SIPStatus +from pyVoIP.SIP.message.message import ( + SIPMessage, + SIPMethod, + SIPResponse, + SIPRequest, +) +from pyVoIP.SIP.message.response_codes import ResponseCode from pyVoIP.VoIP.error import InvalidStateError from threading import Lock, Timer from typing import Any, Dict, List, Optional, TYPE_CHECKING @@ -89,21 +95,21 @@ def receiver(self): if data is None: continue try: - message = SIPMessage(data) + message = SIPMessage.from_bytes(data) except SIPParseError: continue - if message.type is SIPMessageType.RESPONSE: - if message.status is SIPStatus.OK: + if type(message) is SIPResonse: + if message.status is ResponseCode.OK: if self.state in [ CallState.DIALING, CallState.RINGING, CallState.PROGRESS, ]: self.answered(message) - elif message.status == SIPStatus.NOT_FOUND: + elif message.status == ResponseCode.NOT_FOUND: pass else: - if message.method == "BYE": + if message.method == SIPMethod.BYE: self.bye(message) def init_outgoing_call(self, ms: Optional[Dict[int, RTP.PayloadType]]): @@ -279,16 +285,22 @@ def answer(self) -> None: if self.state != CallState.RINGING: raise InvalidStateError("Call is not ringing") m = self.gen_ms() - message = self.sip.gen_answer( + data = self.sip.gen_answer( self.request, self.session_id, m, self.sendmode ) - self.conn.send(message) - message = SIPMessage(self.conn.recv()) - if message.method != "ACK": + self.conn.send(data) + message = SIPMessage.from_bytes(self.conn.recv()) + if type(message) is SIPResponse: + debug( + f"Received Response to OK instead of ACK: {message.status}:\n\n" + + f"{message.summary()}", + f"Received Response to OK instead of ACK: {message.status}", + ) + elif type(message) is SIPRequest and message.method != SIPMethod.ACK: debug( - f"Received Message to OK other than ACK: {message.method}:\n\n" + f"Received Request to OK other than ACK: {message.method}:\n\n" + f"{message.summary()}", - f"Received Message to OK other than ACK: {message.method}", + f"Received Request to OK other than ACK: {message.method}", ) self.state = CallState.ANSWERED @@ -313,13 +325,15 @@ def transfer( new_dialog = True conn = self.sip.send(request) conn.send(request) - response = SIPMessage(conn.recv()) + response = SIPMessage.from_bytes(conn.recv()) + if type(response) is not SIPResponse: + return False if response.status not in [ - SIPStatus.OK, - SIPStatus.TRYING, - SIPStatus.ACCEPTED, - SIPStatus.BAD_EXTENSION, + ResponseCode.OK, + ResponseCode.TRYING, + ResponseCode.ACCEPTED, + ResponseCode.BAD_EXTENSION, ]: # If we've not received any of these responses, the client likely # does not accept out of dialog REFER requests. @@ -330,10 +344,12 @@ def transfer( self.request, user, uri, blind, new_dialog=False ) conn.send(request) - response = SIPMessage(conn.recv()) + response = SIPMessage.from_bytes(conn.recv()) + if type(response) is not SIPResponse: + return False norefersub = True - if blind and response.status == SIPStatus.BAD_EXTENSION: + if blind and response.status == ResponseCode.BAD_EXTENSION: # If the client does not support norefersub, resend without it. norefersub = False if new_dialog: @@ -343,9 +359,11 @@ def transfer( conn = self.sip.send(request) else: conn.send(request) - response = SIPMessage(conn.recv()) + response = SIPMessage.from_bytes(conn.recv()) + if type(response) is not SIPResponse: + return False - if response.status not in [SIPStatus.OK, SIPStatus.ACCEPTED]: + if response.status not in [ResponseCode.OK, ResponseCode.ACCEPTED]: return False if blind: if norefersub: @@ -353,17 +371,20 @@ def transfer( conn.close() self.hangup() return True - response = SIPMessage(conn.recv()) - while response.method == "NOTIFY" and response.body.get( - "content", b"" - ) in [ - b"", - b"SIP/2.0 100 Trying\r\n", - b"SIP/2.0 181 Ringing\r\n", - ]: + response = SIPMessage.from_bytes(conn.recv()) + while ( + type(response) is SIPRequest + and response.method == SIPMethod.NOTIFY + and response.body.get("content", b"") + in [ + b"", + b"SIP/2.0 100 Trying\r\n", + b"SIP/2.0 181 Ringing\r\n", + ] + ): reply = self.sip.gen_ok(response) conn.send(reply) - response = SIPMessage(conn.recv()) + response = SIPMessage.from_bytes(conn.recv()) if response.body.get("content", b"") == b"SIP/2.0 200 OK\r\n": reply = self.sip.gen_ok(response) conn.send(reply) @@ -478,7 +499,7 @@ def ringing(self, request: SIPMessage) -> None: self.request = request def busy(self, request: SIPMessage) -> None: - self.bye() + self.bye(request) def deny(self) -> None: if self.state != CallState.RINGING: diff --git a/pyVoIP/VoIP/phone.py b/pyVoIP/VoIP/phone.py index afc3fd5..56b82a5 100644 --- a/pyVoIP/VoIP/phone.py +++ b/pyVoIP/VoIP/phone.py @@ -1,7 +1,8 @@ from pyVoIP import RTP from pyVoIP.credentials import CredentialsManager from pyVoIP.SIP.client import SIPClient -from pyVoIP.SIP.message import SIPMessage, SIPMessageType, SIPStatus +from pyVoIP.SIP.message.message import SIPMessage, SIPRequest, SIPResponse +from pyVoIP.SIP.message.response_codes import ResponseCode from pyVoIP.networking.sock import VoIPConnection from pyVoIP.networking.transport import TransportMode from pyVoIP.types import KEY_PASSWORD @@ -105,7 +106,7 @@ def callback( self, conn: VoIPConnection, request: SIPMessage ) -> Optional[str]: # debug("Callback: "+request.summary()) - if request.type == SIPMessageType.REQUEST: + if type(request) is SIPRequest: # debug("This is a message") if request.method == "INVITE": self._callback_MSG_Invite(conn, request) @@ -113,20 +114,20 @@ def callback( self._callback_MSG_Bye(request) elif request.method == "OPTIONS": return self._callback_MSG_Options(request) - else: - if request.status == SIPStatus.OK: + elif type(request) is SIPResponse: + if request.status == ResponseCode.OK: self._callback_RESP_OK(request) - elif request.status == SIPStatus.NOT_FOUND: + elif request.status == ResponseCode.NOT_FOUND: self._callback_RESP_NotFound(request) - elif request.status == SIPStatus.SERVICE_UNAVAILABLE: + elif request.status == ResponseCode.SERVICE_UNAVAILABLE: self._callback_RESP_Unavailable(request) - elif request.status == SIPStatus.RINGING: + elif request.status == ResponseCode.RINGING: self._callback_RESP_Ringing(request) - elif request.status == SIPStatus.SESSION_PROGRESS: + elif request.status == ResponseCode.SESSION_PROGRESS: self._callback_RESP_Progress(request) - elif request.status == SIPStatus.BUSY_HERE: + elif request.status == ResponseCode.BUSY_HERE: self._callback_RESP_Busy(request) - elif request.status == SIPStatus.REQUEST_TERMINATED: + elif request.status == ResponseCode.REQUEST_TERMINATED: self._callback_RESP_Terminated(request) return None @@ -355,7 +356,10 @@ def message( self, number: str, body: str, ctype: str = "text/plain" ) -> bool: response = self.sip.message(number, body, ctype) - return response and response.status == SIPStatus.OK + return ( + type(response) is SIPResponse + and response.status == ResponseCode.OK + ) def request_port(self, blocking=True) -> int: ports_available = [ diff --git a/pyVoIP/__init__.py b/pyVoIP/__init__.py index 3959ba0..f4ec072 100644 --- a/pyVoIP/__init__.py +++ b/pyVoIP/__init__.py @@ -91,8 +91,10 @@ def debug(s, e=None): # noqa because import will fail if debug is not defined from pyVoIP.RTP import PayloadType # noqa: E402 +from pyVoIP.SIP.message.message import SIPMethod SIPCompatibleVersions = ["SIP/2.0"] +SIPCompatibleMethods = list(map(lambda x: str(x), list(SIPMethod))) RTPCompatibleVersions = [2] RTPCompatibleCodecs = [PayloadType.PCMU, PayloadType.PCMA, PayloadType.EVENT] diff --git a/pyVoIP/networking/sock.py b/pyVoIP/networking/sock.py index d390032..923160d 100644 --- a/pyVoIP/networking/sock.py +++ b/pyVoIP/networking/sock.py @@ -1,6 +1,6 @@ from typing import TYPE_CHECKING, List, Optional, Tuple, Union from pyVoIP.types import KEY_PASSWORD, SOCKETS -from pyVoIP.SIP.message import SIPMessage, SIPMessageType +from pyVoIP.SIP.message.message import SIPMessage, SIPResponse, SIPRequest from pyVoIP.SIP.error import SIPParseError from pyVoIP.networking.nat import NAT, AddressType from pyVoIP.networking.transport import TransportMode @@ -42,27 +42,27 @@ def __init__( self.message ) self._peak_buffer: Optional[bytes] = None - if conn and message.type == SIPMessageType.REQUEST: + if conn and type(message) is SIPRequest: if self.sock.mode.tls_mode: client_context = ssl.create_default_context() client_context.check_hostname = pyVoIP.TLS_CHECK_HOSTNAME client_context.verify_mode = pyVoIP.TLS_VERIFY_MODE self.conn = client_context.wrap_socket( - self.conn, server_hostname=message.to["host"] + self.conn, server_hostname=message.destination["host"] ) - addr = (message.to["host"], message.to["port"]) + addr = (message.destination["host"], message.destination["port"]) self.conn.connect(addr) def send(self, data: Union[bytes, str]) -> None: if type(data) is str: data = data.encode("utf8") try: - msg = SIPMessage(data) + msg = SIPMessage.from_bytes(data) except SIPParseError: return if not self.conn: # If UDP - if msg.type == SIPMessageType.REQUEST: - addr = (msg.to["host"], msg.to["port"]) + if type(msg) is SIPRequest: + addr = (msg.destination["host"], msg.destination["port"]) else: addr = msg.headers["Via"][0]["address"] self.sock.s.sendto(data, addr) @@ -95,7 +95,7 @@ def _tcp_tls_recv(self, nbytes: int, timeout=0, peak=False) -> bytes: while not msg and not self.sock.SD: data = self.conn.recv(nbytes) try: - msg = SIPMessage(data) + msg = SIPMessage.from_bytes(data) except SIPParseError as e: br = self.sock.gen_bad_request( connection=self, error=e, received=data @@ -402,7 +402,7 @@ def _tcp_tls_run(self) -> None: debug(f"Received new {self.mode} connection from {addr}.") data = conn.recv(8192) try: - message = SIPMessage(data) + message = SIPMessage.from_bytes(data) except SIPParseError: continue debug("\n\nReceived SIP Message:") @@ -416,7 +416,7 @@ def _udp_run(self) -> None: except OSError: continue try: - message = SIPMessage(data) + message = SIPMessage.from_bytes(data) except SIPParseError: continue debug("\n\nReceived UDP Message:") @@ -468,11 +468,11 @@ def send(self, data: bytes) -> VoIPConnection: Creates a new connection, sends the data, then returns the socket """ if self.mode == TransportMode.UDP: - conn = VoIPConnection(self, None, SIPMessage(data)) + conn = VoIPConnection(self, None, SIPMessage.from_bytes(data)) self.__register_connection(conn) conn.send(data) return conn s = socket.socket(socket.AF_INET, self.mode.socket_type) - conn = VoIPConnection(self, s, SIPMessage(data)) + conn = VoIPConnection(self, s, SIPMessage.from_bytes(data)) conn.send(data) return conn diff --git a/tests/test_sip_requests.py b/tests/test_sip_requests.py index 176b614..a2c7e6e 100644 --- a/tests/test_sip_requests.py +++ b/tests/test_sip_requests.py @@ -1,4 +1,4 @@ -from pyVoIP.SIP.message import SIPMessage +from pyVoIP.SIP.message.message import SIPMessage, SIPRequest import pytest @@ -459,7 +459,8 @@ ], ) def test_sip_headers(packet, expected): - message = SIPMessage(packet) + message = SIPMessage.from_bytes(packet) + assert type(message) is SIPRequest assert message.headers == expected @@ -567,5 +568,6 @@ def test_sip_headers(packet, expected): ], ) def test_sip_to(packet, expected): - message = SIPMessage(packet) - assert message.to == expected + message = SIPMessage.from_bytes(packet) + assert type(message) is SIPRequest + assert message.destination == expected diff --git a/tests/test_sip_responses.py b/tests/test_sip_responses.py index 53bde91..24a4fe2 100644 --- a/tests/test_sip_responses.py +++ b/tests/test_sip_responses.py @@ -1,4 +1,4 @@ -from pyVoIP.SIP.message import SIPMessage +from pyVoIP.SIP.message.message import SIPMessage, SIPResponse import pytest @@ -83,7 +83,8 @@ ], ) def test_sip_authentication(packet, expected): - message = SIPMessage(packet) + message = SIPMessage.from_bytes(packet) + assert type(message) is SIPResponse assert message.authentication == expected @@ -206,7 +207,8 @@ def test_sip_authentication(packet, expected): ], ) def test_sip_to_from(packet, expected): - message = SIPMessage(packet) + message = SIPMessage.from_bytes(packet) + assert type(message) is SIPResponse assert type(message.headers["To"]) == dict assert message.headers["To"] == expected @@ -293,5 +295,6 @@ def test_sip_to_from(packet, expected): ], ) def test_multi_via(packet, expected): - message = SIPMessage(packet) + message = SIPMessage.from_bytes(packet) + assert type(message) is SIPResponse assert message.headers["Via"] == expected From 9def073f8d60a0c84d972201c8738d9db4eb9383 Mon Sep 17 00:00:00 2001 From: TJ Porter Date: Fri, 26 Jan 2024 17:32:08 -0600 Subject: [PATCH 3/5] [FIX] Set file format to unit --- pyVoIP/SIP/message/parse.py | 870 +++++++++++++-------------- pyVoIP/SIP/message/response_codes.py | 540 ++++++++--------- 2 files changed, 705 insertions(+), 705 deletions(-) diff --git a/pyVoIP/SIP/message/parse.py b/pyVoIP/SIP/message/parse.py index 1fefc5e..6173e10 100644 --- a/pyVoIP/SIP/message/parse.py +++ b/pyVoIP/SIP/message/parse.py @@ -1,435 +1,435 @@ -from typing import Any, Dict, List -from pyVoIP import regex -from pyVoIP.types import URI_HEADER -from pyVoIP.SIP.error import SIPParseError -import pyVoIP - - -# Compacts defined in RFC 3261 Section 7.3.3 and 20 -COMPACT_KEY = { - "i": "Call-ID", - "m": "Contact", - "e": "Content-Encoding", - "l": "Content-Length", - "c": "Content-Type", - "f": "From", - "s": "Subject", - "k": "Supported", - "t": "To", - "v": "Via", -} - - -def parse_raw_headers(raw_headers: List[bytes]) -> Dict[str, Any]: - headers: Dict[str, Any] = {"Via": []} - # Only use first occurance of VIA header field; - # got second VIA from Kamailio running in DOCKER - # According to RFC 3261 these messages should be - # discarded in a response - for x in raw_headers: - i = str(x, "utf8").split(": ") - if i[0] == "Via": - headers["Via"].append(i[1]) - if i[0] not in headers.keys(): - headers[i[0]] = i[1] - - parsed_headers: Dict[str, Any] = {} - for key, val in headers.items(): - if key in COMPACT_KEY.keys(): - key = COMPACT_KEY[key] - - parsed_headers[key] = parse_header(key, val) - return parsed_headers - - -def parse_raw_body(body: bytes, ctype: str) -> Dict[str, Any]: - if len(body) > 0: - if ctype == "application/sdp": - parsed_body: Dict[str, Any] = {} - body_raw = body.split(b"\r\n") - for x in body_raw: - i = str(x, "utf8").split("=") - if i != [""]: - parse_sdp_tag(parsed_body, i[0], i[1]) - return parsed_body - else: - return {"content": body} - return {"content": None} - - -def get_uri_header(data: str) -> URI_HEADER: - info = data.split(";tag=") - tag = "" - if len(info) >= 2: - tag = info[1] - raw = data - reg = regex.TO_FROM_MATCH - direct = "@" not in data - if direct: - reg = regex.TO_FROM_DIRECT_MATCH - match = reg.match(data) - if match is None: - raise SIPParseError( - "Regex failed to match To/From.\n\n" - + "Please open a GitHub Issue at " - + "https://www.github.com/tayler6000/pyVoIP " - + "and include the following:\n\n" - + f"{data=} {type(match)=}" - ) - matches = match.groupdict() - if direct: - matches["user"] = "" - matches["password"] = "" - uri = f'{matches["uri_type"]}:{matches["user"]}@{matches["host"]}' - if direct: - uri = f'{matches["uri_type"]}:{matches["host"]}' - if matches["port"]: - uri += matches["port"] - uri_type = matches["uri_type"] - user = matches["user"] - password = matches["password"].strip(":") if matches["password"] else "" - display_name = ( - matches["display_name"].strip().strip('"') - if matches["display_name"] - else "" - ) - host = matches["host"] - port = int(matches["port"].strip(":")) if matches["port"] else 5060 - - return { - "raw": raw, - "tag": tag, - "uri": uri, - "uri-type": uri_type, - "user": user, - "password": password, - "display-name": display_name, - "host": host, - "port": port, - } - - -def parse_header(header: str, data: str) -> Any: - if header == "Via": - vias = [] - for d in data: - info = regex.VIA_SPLIT.split(d) - _type = info[0] # SIP Method - _address = info[1].split(":") # Tuple: address, port - _ip = _address[0] - - """ - If no port is provided in via header assume default port. - Needs to be str. Check response build for better str creation - """ - _port = int(info[1].split(":")[1]) if len(_address) > 1 else 5060 - _via = {"type": _type, "address": (_ip, _port)} - - """ - Sets branch, maddr, ttl, received, and rport if defined - as per RFC 3261 20.7 - """ - for x in info[2:]: - if "=" in x: - try: - _via[x.split("=")[0]] = int(x.split("=")[1]) - except ValueError: - _via[x.split("=")[0]] = x.split("=")[1] - else: - _via[x] = None - vias.append(_via) - return vias - elif header in ["To", "From", "Contact", "Refer-To"]: - return get_uri_header(data) - elif header == "CSeq": - return { - "check": int(data.split(" ")[0]), - "method": data.split(" ")[1], - } - elif header in ["Allow", "Supported", "Require"]: - return data.split(", ") - elif header == "Call-ID": - return data - elif header in ( - "WWW-Authenticate", - "Authorization", - "Proxy-Authenticate", - ): - method = data.split(" ")[0] - data = data.replace(f"{method} ", "") - auth_match = regex.AUTH_MATCH - row_data = auth_match.findall(data) - auth_data: Dict[str, Any] = {"header": header, "method": method} - for var, data in row_data: - if var == "userhash": - auth_data[var] = ( - False if data.strip('"').lower() == "false" else True - ) - continue - if var == "qop": - authorized = data.strip('"').split(",") - for i, value in enumerate(authorized): - authorized[i] = value.strip() - auth_data[var] = authorized - continue - auth_data[var] = data.strip('"') - return auth_data - elif header == "Target-Dialog": - # Target-Dialog (tdialog) is specified in RFC 4538 - params = data.split(";") - td_data: Dict[str, Any] = { - "callid": params.pop(0) - } # key is callid to be consitenent with RFC 4538 Section 7 - for x in params: - y = x.split("=") - td_data[y[0]] = y[1] - return td_data - elif header == "Refer-Sub": - # Refer-Sub (norefersub) is specified in RFC 4488 - params = data.split(";") - rs_data: Dict[str, Any] = { - "value": True if params.pop(0) == "true" else False - } # BNF states extens are possible - for x in params: - y = x.split("=") - rs_data[y[0]] = y[1] - return rs_data - else: - try: - return int(data) - except ValueError: - return data - - -def parse_sdp_tag(parsed_body: Dict[str, Any], field: str, data: str) -> Any: - # Referenced RFC 4566 July 2006 - if field == "v": - # SDP 5.1 Version - parsed_body[field] = int(data) - elif field == "o": - # SDP 5.2 Origin - # o= # noqa: E501 - d = data.split(" ") - parsed_body[field] = { - "username": d[0], - "id": d[1], - "version": d[2], - "network_type": d[3], - "address_type": d[4], - "address": d[5], - } - elif field == "s": - # SDP 5.3 Session Name - # s= - parsed_body[field] = data - elif field == "i": - # SDP 5.4 Session Information - # i= - parsed_body[field] = data - elif field == "u": - # SDP 5.5 URI - # u= - parsed_body[field] = data - elif field == "e" or field == "p": - # SDP 5.6 Email Address and Phone Number of person - # responsible for the conference - # e= - # p= - parsed_body[field] = data - elif field == "c": - # SDP 5.7 Connection Data - # c= - if "c" not in parsed_body: - parsed_body["c"] = [] - d = data.split(" ") - # TTL Data and Multicast addresses may be specified. - # For IPv4 its listed as addr/ttl/number of addresses. - # c=IN IP4 224.2.1.1/127/3 means: - # c=IN IP4 224.2.1.1/127 - # c=IN IP4 224.2.1.2/127 - # c=IN IP4 224.2.1.3/127 - # With the TTL being 127. - # IPv6 does not support time to live so you will only see a '/' - # for multicast addresses. - if "/" in d[2]: - if d[1] == "IP6": - parsed_body[field].append( - { - "network_type": d[0], - "address_type": d[1], - "address": d[2].split("/")[0], - "ttl": None, - "address_count": int(d[2].split("/")[1]), - } - ) - else: - address_data = d[2].split("/") - if len(address_data) == 2: - parsed_body[field].append( - { - "network_type": d[0], - "address_type": d[1], - "address": address_data[0], - "ttl": int(address_data[1]), - "address_count": 1, - } - ) - else: - parsed_body[field].append( - { - "network_type": d[0], - "address_type": d[1], - "address": address_data[0], - "ttl": int(address_data[1]), - "address_count": int(address_data[2]), - } - ) - else: - parsed_body[field].append( - { - "network_type": d[0], - "address_type": d[1], - "address": d[2], - "ttl": None, - "address_count": 1, - } - ) - elif field == "b": - # SDP 5.8 Bandwidth - # b=: - # A bwtype of CT means Conference Total between all medias - # and all devices in the conference. - # A bwtype of AS means Applicaton Specific total for this - # media and this device. - # The bandwidth is given in kilobits per second. - # As this was written in 2006, this could be Kibibits. - # TODO: Implement Bandwidth restrictions - d = data.split(":") - parsed_body[field] = {"type": d[0], "bandwidth": d[1]} - elif field == "t": - # SDP 5.9 Timing - # t= - d = data.split(" ") - parsed_body[field] = {"start": d[0], "stop": d[1]} - elif field == "r": - # SDP 5.10 Repeat Times - # r= # noqa: E501 - d = data.split(" ") - parsed_body[field] = { - "repeat": d[0], - "duration": d[1], - "offset1": d[2], - "offset2": d[3], - } - elif field == "z": - # SDP 5.11 Time Zones - # z= .... - # Used for change in timezones such as day light savings time. - d = data.split() - amount = len(d) / 2 - parsed_body[field] = {} - for x in range(int(amount)): - parsed_body[field]["adjustment-time" + str(x)] = d[x * 2] - parsed_body[field]["offset" + str(x)] = d[x * 2 + 1] - elif field == "k": - # SDP 5.12 Encryption Keys - # k= - # k=: - if ":" in data: - d = data.split(":") - parsed_body[field] = {"method": d[0], "key": d[1]} - else: - parsed_body[field] = {"method": data} - elif field == "m": - # SDP 5.14 Media Descriptions - # m= / ... - # should be even, and +1 should be the RTCP port. - # should coinside with number of - # addresses in SDP 5.7 c= - if "m" not in parsed_body: - parsed_body["m"] = [] - d = data.split(" ") - - if "/" in d[1]: - ports_raw = d[1].split("/") - port = ports_raw[0] - count = int(ports_raw[1]) - else: - port = d[1] - count = 1 - methods = d[3:] - - parsed_body["m"].append( - { - "type": d[0], - "port": int(port), - "port_count": count, - "protocol": pyVoIP.RTP.RTPProtocol(d[2]), - "methods": methods, - "attributes": {}, - } - ) - for x in parsed_body["m"][-1]["methods"]: - parsed_body["m"][-1]["attributes"][x] = {} - elif field == "a": - # SDP 5.13 Attributes & 6.0 SDP Attributes - # a= - # a=: - - if "a" not in parsed_body: - parsed_body["a"] = {} - - if ":" in data: - d = data.split(":") - attribute = d[0] - value = d[1] - else: - attribute = data - value = None - - if value is not None: - if attribute == "rtpmap": - # a=rtpmap: / [/] # noqa: E501 - v = regex.SDP_A_SPLIT.split(value) - for t in parsed_body["m"]: - if v[0] in t["methods"]: - index = int(parsed_body["m"].index(t)) - break - if len(v) == 4: - encoding = v[3] - else: - encoding = None - - parsed_body["m"][index]["attributes"][v[0]]["rtpmap"] = { - "id": v[0], - "name": v[1], - "frequency": v[2], - "encoding": encoding, - } - - elif attribute == "fmtp": - # a=fmtp: - d = value.split(" ") - for t in parsed_body["m"]: - if d[0] in t["methods"]: - index = int(parsed_body["m"].index(t)) - break - - parsed_body["m"][index]["attributes"][d[0]]["fmtp"] = { - "id": d[0], - "settings": d[1:], - } - else: - parsed_body["a"][attribute] = value - else: - if ( - attribute == "recvonly" - or attribute == "sendrecv" - or attribute == "sendonly" - or attribute == "inactive" - ): - parsed_body["a"]["transmit_type"] = pyVoIP.RTP.TransmitType( - attribute - ) # noqa: E501 - else: - parsed_body[field] = data +from typing import Any, Dict, List +from pyVoIP import regex +from pyVoIP.types import URI_HEADER +from pyVoIP.SIP.error import SIPParseError +import pyVoIP + + +# Compacts defined in RFC 3261 Section 7.3.3 and 20 +COMPACT_KEY = { + "i": "Call-ID", + "m": "Contact", + "e": "Content-Encoding", + "l": "Content-Length", + "c": "Content-Type", + "f": "From", + "s": "Subject", + "k": "Supported", + "t": "To", + "v": "Via", +} + + +def parse_raw_headers(raw_headers: List[bytes]) -> Dict[str, Any]: + headers: Dict[str, Any] = {"Via": []} + # Only use first occurance of VIA header field; + # got second VIA from Kamailio running in DOCKER + # According to RFC 3261 these messages should be + # discarded in a response + for x in raw_headers: + i = str(x, "utf8").split(": ") + if i[0] == "Via": + headers["Via"].append(i[1]) + if i[0] not in headers.keys(): + headers[i[0]] = i[1] + + parsed_headers: Dict[str, Any] = {} + for key, val in headers.items(): + if key in COMPACT_KEY.keys(): + key = COMPACT_KEY[key] + + parsed_headers[key] = parse_header(key, val) + return parsed_headers + + +def parse_raw_body(body: bytes, ctype: str) -> Dict[str, Any]: + if len(body) > 0: + if ctype == "application/sdp": + parsed_body: Dict[str, Any] = {} + body_raw = body.split(b"\r\n") + for x in body_raw: + i = str(x, "utf8").split("=") + if i != [""]: + parse_sdp_tag(parsed_body, i[0], i[1]) + return parsed_body + else: + return {"content": body} + return {"content": None} + + +def get_uri_header(data: str) -> URI_HEADER: + info = data.split(";tag=") + tag = "" + if len(info) >= 2: + tag = info[1] + raw = data + reg = regex.TO_FROM_MATCH + direct = "@" not in data + if direct: + reg = regex.TO_FROM_DIRECT_MATCH + match = reg.match(data) + if match is None: + raise SIPParseError( + "Regex failed to match To/From.\n\n" + + "Please open a GitHub Issue at " + + "https://www.github.com/tayler6000/pyVoIP " + + "and include the following:\n\n" + + f"{data=} {type(match)=}" + ) + matches = match.groupdict() + if direct: + matches["user"] = "" + matches["password"] = "" + uri = f'{matches["uri_type"]}:{matches["user"]}@{matches["host"]}' + if direct: + uri = f'{matches["uri_type"]}:{matches["host"]}' + if matches["port"]: + uri += matches["port"] + uri_type = matches["uri_type"] + user = matches["user"] + password = matches["password"].strip(":") if matches["password"] else "" + display_name = ( + matches["display_name"].strip().strip('"') + if matches["display_name"] + else "" + ) + host = matches["host"] + port = int(matches["port"].strip(":")) if matches["port"] else 5060 + + return { + "raw": raw, + "tag": tag, + "uri": uri, + "uri-type": uri_type, + "user": user, + "password": password, + "display-name": display_name, + "host": host, + "port": port, + } + + +def parse_header(header: str, data: str) -> Any: + if header == "Via": + vias = [] + for d in data: + info = regex.VIA_SPLIT.split(d) + _type = info[0] # SIP Method + _address = info[1].split(":") # Tuple: address, port + _ip = _address[0] + + """ + If no port is provided in via header assume default port. + Needs to be str. Check response build for better str creation + """ + _port = int(info[1].split(":")[1]) if len(_address) > 1 else 5060 + _via = {"type": _type, "address": (_ip, _port)} + + """ + Sets branch, maddr, ttl, received, and rport if defined + as per RFC 3261 20.7 + """ + for x in info[2:]: + if "=" in x: + try: + _via[x.split("=")[0]] = int(x.split("=")[1]) + except ValueError: + _via[x.split("=")[0]] = x.split("=")[1] + else: + _via[x] = None + vias.append(_via) + return vias + elif header in ["To", "From", "Contact", "Refer-To"]: + return get_uri_header(data) + elif header == "CSeq": + return { + "check": int(data.split(" ")[0]), + "method": data.split(" ")[1], + } + elif header in ["Allow", "Supported", "Require"]: + return data.split(", ") + elif header == "Call-ID": + return data + elif header in ( + "WWW-Authenticate", + "Authorization", + "Proxy-Authenticate", + ): + method = data.split(" ")[0] + data = data.replace(f"{method} ", "") + auth_match = regex.AUTH_MATCH + row_data = auth_match.findall(data) + auth_data: Dict[str, Any] = {"header": header, "method": method} + for var, data in row_data: + if var == "userhash": + auth_data[var] = ( + False if data.strip('"').lower() == "false" else True + ) + continue + if var == "qop": + authorized = data.strip('"').split(",") + for i, value in enumerate(authorized): + authorized[i] = value.strip() + auth_data[var] = authorized + continue + auth_data[var] = data.strip('"') + return auth_data + elif header == "Target-Dialog": + # Target-Dialog (tdialog) is specified in RFC 4538 + params = data.split(";") + td_data: Dict[str, Any] = { + "callid": params.pop(0) + } # key is callid to be consitenent with RFC 4538 Section 7 + for x in params: + y = x.split("=") + td_data[y[0]] = y[1] + return td_data + elif header == "Refer-Sub": + # Refer-Sub (norefersub) is specified in RFC 4488 + params = data.split(";") + rs_data: Dict[str, Any] = { + "value": True if params.pop(0) == "true" else False + } # BNF states extens are possible + for x in params: + y = x.split("=") + rs_data[y[0]] = y[1] + return rs_data + else: + try: + return int(data) + except ValueError: + return data + + +def parse_sdp_tag(parsed_body: Dict[str, Any], field: str, data: str) -> Any: + # Referenced RFC 4566 July 2006 + if field == "v": + # SDP 5.1 Version + parsed_body[field] = int(data) + elif field == "o": + # SDP 5.2 Origin + # o= # noqa: E501 + d = data.split(" ") + parsed_body[field] = { + "username": d[0], + "id": d[1], + "version": d[2], + "network_type": d[3], + "address_type": d[4], + "address": d[5], + } + elif field == "s": + # SDP 5.3 Session Name + # s= + parsed_body[field] = data + elif field == "i": + # SDP 5.4 Session Information + # i= + parsed_body[field] = data + elif field == "u": + # SDP 5.5 URI + # u= + parsed_body[field] = data + elif field == "e" or field == "p": + # SDP 5.6 Email Address and Phone Number of person + # responsible for the conference + # e= + # p= + parsed_body[field] = data + elif field == "c": + # SDP 5.7 Connection Data + # c= + if "c" not in parsed_body: + parsed_body["c"] = [] + d = data.split(" ") + # TTL Data and Multicast addresses may be specified. + # For IPv4 its listed as addr/ttl/number of addresses. + # c=IN IP4 224.2.1.1/127/3 means: + # c=IN IP4 224.2.1.1/127 + # c=IN IP4 224.2.1.2/127 + # c=IN IP4 224.2.1.3/127 + # With the TTL being 127. + # IPv6 does not support time to live so you will only see a '/' + # for multicast addresses. + if "/" in d[2]: + if d[1] == "IP6": + parsed_body[field].append( + { + "network_type": d[0], + "address_type": d[1], + "address": d[2].split("/")[0], + "ttl": None, + "address_count": int(d[2].split("/")[1]), + } + ) + else: + address_data = d[2].split("/") + if len(address_data) == 2: + parsed_body[field].append( + { + "network_type": d[0], + "address_type": d[1], + "address": address_data[0], + "ttl": int(address_data[1]), + "address_count": 1, + } + ) + else: + parsed_body[field].append( + { + "network_type": d[0], + "address_type": d[1], + "address": address_data[0], + "ttl": int(address_data[1]), + "address_count": int(address_data[2]), + } + ) + else: + parsed_body[field].append( + { + "network_type": d[0], + "address_type": d[1], + "address": d[2], + "ttl": None, + "address_count": 1, + } + ) + elif field == "b": + # SDP 5.8 Bandwidth + # b=: + # A bwtype of CT means Conference Total between all medias + # and all devices in the conference. + # A bwtype of AS means Applicaton Specific total for this + # media and this device. + # The bandwidth is given in kilobits per second. + # As this was written in 2006, this could be Kibibits. + # TODO: Implement Bandwidth restrictions + d = data.split(":") + parsed_body[field] = {"type": d[0], "bandwidth": d[1]} + elif field == "t": + # SDP 5.9 Timing + # t= + d = data.split(" ") + parsed_body[field] = {"start": d[0], "stop": d[1]} + elif field == "r": + # SDP 5.10 Repeat Times + # r= # noqa: E501 + d = data.split(" ") + parsed_body[field] = { + "repeat": d[0], + "duration": d[1], + "offset1": d[2], + "offset2": d[3], + } + elif field == "z": + # SDP 5.11 Time Zones + # z= .... + # Used for change in timezones such as day light savings time. + d = data.split() + amount = len(d) / 2 + parsed_body[field] = {} + for x in range(int(amount)): + parsed_body[field]["adjustment-time" + str(x)] = d[x * 2] + parsed_body[field]["offset" + str(x)] = d[x * 2 + 1] + elif field == "k": + # SDP 5.12 Encryption Keys + # k= + # k=: + if ":" in data: + d = data.split(":") + parsed_body[field] = {"method": d[0], "key": d[1]} + else: + parsed_body[field] = {"method": data} + elif field == "m": + # SDP 5.14 Media Descriptions + # m= / ... + # should be even, and +1 should be the RTCP port. + # should coinside with number of + # addresses in SDP 5.7 c= + if "m" not in parsed_body: + parsed_body["m"] = [] + d = data.split(" ") + + if "/" in d[1]: + ports_raw = d[1].split("/") + port = ports_raw[0] + count = int(ports_raw[1]) + else: + port = d[1] + count = 1 + methods = d[3:] + + parsed_body["m"].append( + { + "type": d[0], + "port": int(port), + "port_count": count, + "protocol": pyVoIP.RTP.RTPProtocol(d[2]), + "methods": methods, + "attributes": {}, + } + ) + for x in parsed_body["m"][-1]["methods"]: + parsed_body["m"][-1]["attributes"][x] = {} + elif field == "a": + # SDP 5.13 Attributes & 6.0 SDP Attributes + # a= + # a=: + + if "a" not in parsed_body: + parsed_body["a"] = {} + + if ":" in data: + d = data.split(":") + attribute = d[0] + value = d[1] + else: + attribute = data + value = None + + if value is not None: + if attribute == "rtpmap": + # a=rtpmap: / [/] # noqa: E501 + v = regex.SDP_A_SPLIT.split(value) + for t in parsed_body["m"]: + if v[0] in t["methods"]: + index = int(parsed_body["m"].index(t)) + break + if len(v) == 4: + encoding = v[3] + else: + encoding = None + + parsed_body["m"][index]["attributes"][v[0]]["rtpmap"] = { + "id": v[0], + "name": v[1], + "frequency": v[2], + "encoding": encoding, + } + + elif attribute == "fmtp": + # a=fmtp: + d = value.split(" ") + for t in parsed_body["m"]: + if d[0] in t["methods"]: + index = int(parsed_body["m"].index(t)) + break + + parsed_body["m"][index]["attributes"][d[0]]["fmtp"] = { + "id": d[0], + "settings": d[1:], + } + else: + parsed_body["a"][attribute] = value + else: + if ( + attribute == "recvonly" + or attribute == "sendrecv" + or attribute == "sendonly" + or attribute == "inactive" + ): + parsed_body["a"]["transmit_type"] = pyVoIP.RTP.TransmitType( + attribute + ) # noqa: E501 + else: + parsed_body[field] = data diff --git a/pyVoIP/SIP/message/response_codes.py b/pyVoIP/SIP/message/response_codes.py index 4c389a6..7f42387 100644 --- a/pyVoIP/SIP/message/response_codes.py +++ b/pyVoIP/SIP/message/response_codes.py @@ -1,270 +1,270 @@ -from enum import Enum - - -class ResponseCode(Enum): - def __new__(cls, value: int, phrase: str = "", description: str = ""): - obj = object.__new__(cls) - obj._value_ = value - - obj.phrase = phrase - obj.description = description - return obj - - def __int__(self) -> int: - return self._value_ - - def __str__(self) -> str: - return f"{self._value_} {self.phrase}" - - def __repr__(self) -> str: - return str(self) - - @property - def phrase(self) -> str: - return self._phrase - - @phrase.setter - def phrase(self, value: str) -> None: - self._phrase = value - - @property - def description(self) -> str: - return self._description - - @description.setter - def description(self, value: str) -> None: - self._description = value - - # Informational - TRYING = ( - 100, - "Trying", - "Extended search being performed, may take a significant time", - ) - RINGING = ( - 180, - "Ringing", - "Destination user agent received INVITE, " - + "and is alerting user of call", - ) - FORWARDED = 181, "Call is Being Forwarded" - QUEUED = 182, "Queued" - SESSION_PROGRESS = 183, "Session Progress" - TERMINATED = 199, "Early Dialog Terminated" - - # Success - OK = 200, "OK", "Request successful" - ACCEPTED = ( - 202, - "Accepted", - "Request accepted, processing continues (Deprecated.)", - ) - NO_NOTIFICATION = ( - 204, - "No Notification", - "Request fulfilled, nothing follows", - ) - - # Redirection - MULTIPLE_CHOICES = ( - 300, - "Multiple Choices", - "Object has several resources -- see URI list", - ) - MOVED_PERMANENTLY = ( - 301, - "Moved Permanently", - "Object moved permanently -- see URI list", - ) - MOVED_TEMPORARILY = ( - 302, - "Moved Temporarily", - "Object moved temporarily -- see URI list", - ) - USE_PROXY = ( - 305, - "Use Proxy", - "You must use proxy specified in Location to " - + "access this resource", - ) - ALTERNATE_SERVICE = ( - 380, - "Alternate Service", - "The call failed, but alternatives are available -- see URI list", - ) - - # Client Error - BAD_REQUEST = ( - 400, - "Bad Request", - "Bad request syntax or unsupported method", - ) - UNAUTHORIZED = ( - 401, - "Unauthorized", - "No permission -- see authorization schemes", - ) - PAYMENT_REQUIRED = ( - 402, - "Payment Required", - "No payment -- see charging schemes", - ) - FORBIDDEN = ( - 403, - "Forbidden", - "Request forbidden -- authorization will not help", - ) - NOT_FOUND = (404, "Not Found", "Nothing matches the given URI") - METHOD_NOT_ALLOWED = ( - 405, - "Method Not Allowed", - "Specified method is invalid for this resource", - ) - NOT_ACCEPTABLE = ( - 406, - "Not Acceptable", - "URI not available in preferred format", - ) - PROXY_AUTHENTICATION_REQUIRED = ( - 407, - "Proxy Authentication Required", - "You must authenticate with this proxy before proceeding", - ) - REQUEST_TIMEOUT = ( - 408, - "Request Timeout", - "Request timed out; try again later", - ) - CONFLICT = 409, "Conflict", "Request conflict" - GONE = ( - 410, - "Gone", - "URI no longer exists and has been permanently removed", - ) - LENGTH_REQUIRED = ( - 411, - "Length Required", - "Client must specify Content-Length", - ) - CONDITIONAL_REQUEST_FAILED = 412, "Conditional Request Failed" - REQUEST_ENTITY_TOO_LARGE = ( - 413, - "Request Entity Too Large", - "Entity is too large", - ) - REQUEST_URI_TOO_LONG = 414, "Request-URI Too Long", "URI is too long" - UNSUPPORTED_MEDIA_TYPE = ( - 415, - "Unsupported Media Type", - "Entity body in unsupported format", - ) - UNSUPPORTED_URI_SCHEME = ( - 416, - "Unsupported URI Scheme", - "Cannot satisfy request", - ) - UNKOWN_RESOURCE_PRIORITY = ( - 417, - "Unkown Resource-Priority", - "There was a resource-priority option tag, " - + "but no Resource-Priority header", - ) - BAD_EXTENSION = ( - 420, - "Bad Extension", - "Bad SIP Protocol Extension used, not understood by the server.", - ) - EXTENSION_REQUIRED = ( - 421, - "Extension Required", - "Server requeires a specific extension to be " - + "listed in the Supported header.", - ) - SESSION_INTERVAL_TOO_SMALL = 422, "Session Interval Too Small" - SESSION_INTERVAL_TOO_BRIEF = 423, "Session Interval Too Breif" - BAD_LOCATION_INFORMATION = 424, "Bad Location Information" - USE_IDENTITY_HEADER = ( - 428, - "Use Identity Header", - "The server requires an Identity header, " - + "and one has not been provided.", - ) - PROVIDE_REFERRER_IDENTITY = 429, "Provide Referrer Identity" - """ - This response is intended for use between proxy devices, - and should not be seen by an endpoint. If it is seen by one, - it should be treated as a 400 Bad Request response. - """ - FLOW_FAILED = ( - 430, - "Flow Failed", - "A specific flow to a user agent has failed, " - + "although other flows may succeed.", - ) - ANONYMITY_DISALLOWED = 433, "Anonymity Disallowed" - BAD_IDENTITY_INFO = 436, "Bad Identity-Info" - UNSUPPORTED_CERTIFICATE = 437, "Unsupported Certificate" - INVALID_IDENTITY_HEADER = 438, "Invalid Identity Header" - FIRST_HOP_LACKS_OUTBOUND_SUPPORT = 439, "First Hop Lacks Outbound Support" - MAX_BREADTH_EXCEEDED = 440, "Max-Breadth Exceeded" - BAD_INFO_PACKAGE = 469, "Bad Info Package" - CONSENT_NEEDED = 470, "Consent Needed" - TEMPORARILY_UNAVAILABLE = 480, "Temporarily Unavailable" - CALL_OR_TRANSACTION_DOESNT_EXIST = 481, "Call/Transaction Does Not Exist" - LOOP_DETECTED = 482, "Loop Detected" - TOO_MANY_HOPS = 483, "Too Many Hops" - ADDRESS_INCOMPLETE = 484, "Address Incomplete" - AMBIGUOUS = 485, "Ambiguous" - BUSY_HERE = 486, "Busy Here", "Callee is busy" - REQUEST_TERMINATED = 487, "Request Terminated" - NOT_ACCEPTABLE_HERE = 488, "Not Acceptable Here" - BAD_EVENT = 489, "Bad Event" - REQUEST_PENDING = 491, "Request Pending" - UNDECIPHERABLE = 493, "Undecipherable" - SECURITY_AGREEMENT_REQUIRED = 494, "Security Agreement Required" - - # Server Errors - INTERNAL_SERVER_ERROR = ( - 500, - "Internal Server Error", - "Server got itself in trouble", - ) - NOT_IMPLEMENTED = ( - 501, - "Not Implemented", - "Server does not support this operation", - ) - BAD_GATEWAY = ( - 502, - "Bad Gateway", - "Invalid responses from another server/proxy", - ) - SERVICE_UNAVAILABLE = ( - 503, - "Service Unavailable", - "The server cannot process the request due to a high load", - ) - GATEWAY_TIMEOUT = ( - 504, - "Server Timeout", - "The server did not receive a timely response", - ) - SIP_VERSION_NOT_SUPPORTED = ( - 505, - "SIP Version Not Supported", - "Cannot fulfill request", - ) - MESSAGE_TOO_LONG = 513, "Message Too Long" - PUSH_NOTIFICATION_SERVICE_NOT_SUPPORTED = ( - 555, - "Push Notification Service Not Supported", - ) - PRECONDITION_FAILURE = 580, "Precondition Failure" - - # Global Failure Responses - BUSY_EVERYWHERE = 600, "Busy Everywhere" - DECLINE = 603, "Decline" - DOES_NOT_EXIST_ANYWHERE = 604, "Does Not Exist Anywhere" - GLOBAL_NOT_ACCEPTABLE = 606, "Not Acceptable" - UNWANTED = 607, "Unwanted" - REJECTED = 608, "Rejected" +from enum import Enum + + +class ResponseCode(Enum): + def __new__(cls, value: int, phrase: str = "", description: str = ""): + obj = object.__new__(cls) + obj._value_ = value + + obj.phrase = phrase + obj.description = description + return obj + + def __int__(self) -> int: + return self._value_ + + def __str__(self) -> str: + return f"{self._value_} {self.phrase}" + + def __repr__(self) -> str: + return str(self) + + @property + def phrase(self) -> str: + return self._phrase + + @phrase.setter + def phrase(self, value: str) -> None: + self._phrase = value + + @property + def description(self) -> str: + return self._description + + @description.setter + def description(self, value: str) -> None: + self._description = value + + # Informational + TRYING = ( + 100, + "Trying", + "Extended search being performed, may take a significant time", + ) + RINGING = ( + 180, + "Ringing", + "Destination user agent received INVITE, " + + "and is alerting user of call", + ) + FORWARDED = 181, "Call is Being Forwarded" + QUEUED = 182, "Queued" + SESSION_PROGRESS = 183, "Session Progress" + TERMINATED = 199, "Early Dialog Terminated" + + # Success + OK = 200, "OK", "Request successful" + ACCEPTED = ( + 202, + "Accepted", + "Request accepted, processing continues (Deprecated.)", + ) + NO_NOTIFICATION = ( + 204, + "No Notification", + "Request fulfilled, nothing follows", + ) + + # Redirection + MULTIPLE_CHOICES = ( + 300, + "Multiple Choices", + "Object has several resources -- see URI list", + ) + MOVED_PERMANENTLY = ( + 301, + "Moved Permanently", + "Object moved permanently -- see URI list", + ) + MOVED_TEMPORARILY = ( + 302, + "Moved Temporarily", + "Object moved temporarily -- see URI list", + ) + USE_PROXY = ( + 305, + "Use Proxy", + "You must use proxy specified in Location to " + + "access this resource", + ) + ALTERNATE_SERVICE = ( + 380, + "Alternate Service", + "The call failed, but alternatives are available -- see URI list", + ) + + # Client Error + BAD_REQUEST = ( + 400, + "Bad Request", + "Bad request syntax or unsupported method", + ) + UNAUTHORIZED = ( + 401, + "Unauthorized", + "No permission -- see authorization schemes", + ) + PAYMENT_REQUIRED = ( + 402, + "Payment Required", + "No payment -- see charging schemes", + ) + FORBIDDEN = ( + 403, + "Forbidden", + "Request forbidden -- authorization will not help", + ) + NOT_FOUND = (404, "Not Found", "Nothing matches the given URI") + METHOD_NOT_ALLOWED = ( + 405, + "Method Not Allowed", + "Specified method is invalid for this resource", + ) + NOT_ACCEPTABLE = ( + 406, + "Not Acceptable", + "URI not available in preferred format", + ) + PROXY_AUTHENTICATION_REQUIRED = ( + 407, + "Proxy Authentication Required", + "You must authenticate with this proxy before proceeding", + ) + REQUEST_TIMEOUT = ( + 408, + "Request Timeout", + "Request timed out; try again later", + ) + CONFLICT = 409, "Conflict", "Request conflict" + GONE = ( + 410, + "Gone", + "URI no longer exists and has been permanently removed", + ) + LENGTH_REQUIRED = ( + 411, + "Length Required", + "Client must specify Content-Length", + ) + CONDITIONAL_REQUEST_FAILED = 412, "Conditional Request Failed" + REQUEST_ENTITY_TOO_LARGE = ( + 413, + "Request Entity Too Large", + "Entity is too large", + ) + REQUEST_URI_TOO_LONG = 414, "Request-URI Too Long", "URI is too long" + UNSUPPORTED_MEDIA_TYPE = ( + 415, + "Unsupported Media Type", + "Entity body in unsupported format", + ) + UNSUPPORTED_URI_SCHEME = ( + 416, + "Unsupported URI Scheme", + "Cannot satisfy request", + ) + UNKOWN_RESOURCE_PRIORITY = ( + 417, + "Unkown Resource-Priority", + "There was a resource-priority option tag, " + + "but no Resource-Priority header", + ) + BAD_EXTENSION = ( + 420, + "Bad Extension", + "Bad SIP Protocol Extension used, not understood by the server.", + ) + EXTENSION_REQUIRED = ( + 421, + "Extension Required", + "Server requeires a specific extension to be " + + "listed in the Supported header.", + ) + SESSION_INTERVAL_TOO_SMALL = 422, "Session Interval Too Small" + SESSION_INTERVAL_TOO_BRIEF = 423, "Session Interval Too Breif" + BAD_LOCATION_INFORMATION = 424, "Bad Location Information" + USE_IDENTITY_HEADER = ( + 428, + "Use Identity Header", + "The server requires an Identity header, " + + "and one has not been provided.", + ) + PROVIDE_REFERRER_IDENTITY = 429, "Provide Referrer Identity" + """ + This response is intended for use between proxy devices, + and should not be seen by an endpoint. If it is seen by one, + it should be treated as a 400 Bad Request response. + """ + FLOW_FAILED = ( + 430, + "Flow Failed", + "A specific flow to a user agent has failed, " + + "although other flows may succeed.", + ) + ANONYMITY_DISALLOWED = 433, "Anonymity Disallowed" + BAD_IDENTITY_INFO = 436, "Bad Identity-Info" + UNSUPPORTED_CERTIFICATE = 437, "Unsupported Certificate" + INVALID_IDENTITY_HEADER = 438, "Invalid Identity Header" + FIRST_HOP_LACKS_OUTBOUND_SUPPORT = 439, "First Hop Lacks Outbound Support" + MAX_BREADTH_EXCEEDED = 440, "Max-Breadth Exceeded" + BAD_INFO_PACKAGE = 469, "Bad Info Package" + CONSENT_NEEDED = 470, "Consent Needed" + TEMPORARILY_UNAVAILABLE = 480, "Temporarily Unavailable" + CALL_OR_TRANSACTION_DOESNT_EXIST = 481, "Call/Transaction Does Not Exist" + LOOP_DETECTED = 482, "Loop Detected" + TOO_MANY_HOPS = 483, "Too Many Hops" + ADDRESS_INCOMPLETE = 484, "Address Incomplete" + AMBIGUOUS = 485, "Ambiguous" + BUSY_HERE = 486, "Busy Here", "Callee is busy" + REQUEST_TERMINATED = 487, "Request Terminated" + NOT_ACCEPTABLE_HERE = 488, "Not Acceptable Here" + BAD_EVENT = 489, "Bad Event" + REQUEST_PENDING = 491, "Request Pending" + UNDECIPHERABLE = 493, "Undecipherable" + SECURITY_AGREEMENT_REQUIRED = 494, "Security Agreement Required" + + # Server Errors + INTERNAL_SERVER_ERROR = ( + 500, + "Internal Server Error", + "Server got itself in trouble", + ) + NOT_IMPLEMENTED = ( + 501, + "Not Implemented", + "Server does not support this operation", + ) + BAD_GATEWAY = ( + 502, + "Bad Gateway", + "Invalid responses from another server/proxy", + ) + SERVICE_UNAVAILABLE = ( + 503, + "Service Unavailable", + "The server cannot process the request due to a high load", + ) + GATEWAY_TIMEOUT = ( + 504, + "Server Timeout", + "The server did not receive a timely response", + ) + SIP_VERSION_NOT_SUPPORTED = ( + 505, + "SIP Version Not Supported", + "Cannot fulfill request", + ) + MESSAGE_TOO_LONG = 513, "Message Too Long" + PUSH_NOTIFICATION_SERVICE_NOT_SUPPORTED = ( + 555, + "Push Notification Service Not Supported", + ) + PRECONDITION_FAILURE = 580, "Precondition Failure" + + # Global Failure Responses + BUSY_EVERYWHERE = 600, "Busy Everywhere" + DECLINE = 603, "Decline" + DOES_NOT_EXIST_ANYWHERE = 604, "Does Not Exist Anywhere" + GLOBAL_NOT_ACCEPTABLE = 606, "Not Acceptable" + UNWANTED = 607, "Unwanted" + REJECTED = 608, "Rejected" From e5d1d166344fd1b955ce69a5f085bb19b38e5227 Mon Sep 17 00:00:00 2001 From: TJ Porter Date: Fri, 26 Jan 2024 18:37:52 -0600 Subject: [PATCH 4/5] [FIX] Fixed use of `in SIPMethod` to be compatible with Python <3.12 [FIX] Fixed flake8 and black issues --- pyVoIP/SIP/message/message.py | 2 +- pyVoIP/VoIP/call.py | 2 +- pyVoIP/__init__.py | 2 +- pyVoIP/networking/sock.py | 2 +- pyVoIP/regex.py | 1 + 5 files changed, 5 insertions(+), 4 deletions(-) diff --git a/pyVoIP/SIP/message/message.py b/pyVoIP/SIP/message/message.py index fa6ef72..52a2010 100644 --- a/pyVoIP/SIP/message/message.py +++ b/pyVoIP/SIP/message/message.py @@ -100,7 +100,7 @@ def from_bytes(data: bytes) -> Union["SIPRequest", "SIPResponse"]: raise SIPParseError( f"SIP Version {start_line[2]} not compatible." ) - if start_line[0] not in SIPMethod: + if start_line[0] not in map(lambda x: str(x), list(SIPMethod)): raise SIPParseError( f"SIP Method `{start_line[0]}` not supported." ) diff --git a/pyVoIP/VoIP/call.py b/pyVoIP/VoIP/call.py index f16addd..8d39890 100644 --- a/pyVoIP/VoIP/call.py +++ b/pyVoIP/VoIP/call.py @@ -98,7 +98,7 @@ def receiver(self): message = SIPMessage.from_bytes(data) except SIPParseError: continue - if type(message) is SIPResonse: + if type(message) is SIPResponse: if message.status is ResponseCode.OK: if self.state in [ CallState.DIALING, diff --git a/pyVoIP/__init__.py b/pyVoIP/__init__.py index f4ec072..734a61a 100644 --- a/pyVoIP/__init__.py +++ b/pyVoIP/__init__.py @@ -91,7 +91,7 @@ def debug(s, e=None): # noqa because import will fail if debug is not defined from pyVoIP.RTP import PayloadType # noqa: E402 -from pyVoIP.SIP.message.message import SIPMethod +from pyVoIP.SIP.message.message import SIPMethod # noqa: E402 SIPCompatibleVersions = ["SIP/2.0"] SIPCompatibleMethods = list(map(lambda x: str(x), list(SIPMethod))) diff --git a/pyVoIP/networking/sock.py b/pyVoIP/networking/sock.py index 923160d..309e6ba 100644 --- a/pyVoIP/networking/sock.py +++ b/pyVoIP/networking/sock.py @@ -1,6 +1,6 @@ from typing import TYPE_CHECKING, List, Optional, Tuple, Union from pyVoIP.types import KEY_PASSWORD, SOCKETS -from pyVoIP.SIP.message.message import SIPMessage, SIPResponse, SIPRequest +from pyVoIP.SIP.message.message import SIPMessage, SIPRequest from pyVoIP.SIP.error import SIPParseError from pyVoIP.networking.nat import NAT, AddressType from pyVoIP.networking.transport import TransportMode diff --git a/pyVoIP/regex.py b/pyVoIP/regex.py index 573780f..9b8125a 100644 --- a/pyVoIP/regex.py +++ b/pyVoIP/regex.py @@ -5,6 +5,7 @@ each search. This module holds all the compiled regex so it can be compiled once on startup, then used directly later by other modules. """ + import re From b623687bc2fb3610818f4ea2035340aac41d4b13 Mon Sep 17 00:00:00 2001 From: TJ Porter Date: Fri, 26 Jan 2024 20:38:56 -0600 Subject: [PATCH 5/5] [FIX] Fixed issue with registration [FIX] Ran black --- pyVoIP/SIP/client.py | 8 ++++---- pyVoIP/SIP/message.py | 6 ++---- pyVoIP/regex.py | 1 + 3 files changed, 7 insertions(+), 8 deletions(-) diff --git a/pyVoIP/SIP/client.py b/pyVoIP/SIP/client.py index 71d8ca6..bb4f65c 100644 --- a/pyVoIP/SIP/client.py +++ b/pyVoIP/SIP/client.py @@ -549,9 +549,9 @@ def gen_first_request(self, deregister=False) -> str: regRequest += self.__gen_from_to( "From", self.user, - self.nat.get_host(self.server), + self.server, method=method, - port=self.bind_port, + port=self.port, header_parms=f";tag={tag}", ) regRequest += self.__gen_from_to( @@ -631,9 +631,9 @@ def gen_register(self, request: SIPMessage, deregister=False) -> str: regRequest += self.__gen_from_to( "From", self.user, - self.nat.get_host(self.server), + self.server, method=method, - port=self.bind_port, + port=self.port, header_parms=f";tag={self.tagLibrary['register']}", ) regRequest += self.__gen_from_to( diff --git a/pyVoIP/SIP/message.py b/pyVoIP/SIP/message.py index 868abd1..84b4dc7 100644 --- a/pyVoIP/SIP/message.py +++ b/pyVoIP/SIP/message.py @@ -749,10 +749,8 @@ def parse_body(self, header: str, data: str) -> None: or attribute == "sendonly" or attribute == "inactive" ): - self.body["a"][ - "transmit_type" - ] = pyVoIP.RTP.TransmitType( - attribute + self.body["a"]["transmit_type"] = ( + pyVoIP.RTP.TransmitType(attribute) ) # noqa: E501 else: self.body[header] = data diff --git a/pyVoIP/regex.py b/pyVoIP/regex.py index 7251529..3525e1e 100644 --- a/pyVoIP/regex.py +++ b/pyVoIP/regex.py @@ -5,6 +5,7 @@ each search. This module holds all the compiled regex so it can be compiled once on startup, then used directly later by other modules. """ + import re