diff --git a/pyVoIP/SIP/client.py b/pyVoIP/SIP/client.py index e7ebf60..1d92f90 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: @@ -549,9 +556,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 +638,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( @@ -1109,25 +1116,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) @@ -1144,7 +1158,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 @@ -1186,14 +1200,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 @@ -1204,7 +1220,7 @@ def message( ) conn.send(msg) continue - if response.status == SIPStatus.OK: + if response.status == ResponseCode.OK: break if self.NSD: break @@ -1220,7 +1236,7 @@ def bye(self, request: SIPMessage) -> None: request.headers["Contact"]["port"], ), ) - response = SIPMessage(conn.recv(8192)) + response = SIPMessage.from_bytes(conn.recv(8192)) if response.status == SIPStatus(401) or response.status == SIPStatus( 407 ): @@ -1278,25 +1294,25 @@ def __deregister(self) -> bool: 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) or response.status == SIPStatus( @@ -1383,23 +1399,23 @@ def __register(self) -> bool: 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) or response.status == SIPStatus( @@ -1438,25 +1454,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.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..52a2010 --- /dev/null +++ b/pyVoIP/SIP/message/message.py @@ -0,0 +1,198 @@ +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.SIP.message.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" + 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: List[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) -> Union["SIPRequest", "SIPResponse"]: + parsed_headers: Dict[str, Any] = {"Via": []} + parsed_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 map(lambda x: str(x), list(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 + + @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: List[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: List[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..6173e10 --- /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(): + 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 new file mode 100644 index 0000000..7f42387 --- /dev/null +++ b/pyVoIP/SIP/message/response_codes.py @@ -0,0 +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" diff --git a/pyVoIP/VoIP/call.py b/pyVoIP/VoIP/call.py index 89ba0dc..8d39890 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 SIPResponse: + 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 03a9fa7..11a23bb 100644 --- a/pyVoIP/__init__.py +++ b/pyVoIP/__init__.py @@ -91,9 +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 # noqa: E402 -SIPCompatibleMethods = ["INVITE", "ACK", "BYE", "CANCEL", "OPTIONS", "NOTIFY"] 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 cfa7dc2..b552403 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, 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:") @@ -471,11 +471,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/pyVoIP/regex.py b/pyVoIP/regex.py index 7251529..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 @@ -19,3 +20,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.]+") 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