@@ -85,7 +85,7 @@ class SipMessage:
8585 body : str
8686
8787 @staticmethod
88- def parse_sip (message : str ) -> SipMessage :
88+ def parse_sip (message : str , header_lowercase : bool = True ) -> SipMessage :
8989 """Parse a SIP message into a SipMessage object."""
9090 lines = message .splitlines ()
9191
@@ -115,7 +115,7 @@ def parse_sip(message: str) -> SipMessage:
115115 break
116116 else :
117117 key , value = line .split (":" , maxsplit = 1 )
118- headers [key .lower ()] = value .strip ()
118+ headers [key .lower () if header_lowercase else key ] = value .strip ()
119119
120120 body = message [offset :]
121121 return SipMessage (protocol , method , request_uri , code , reason , headers , body )
@@ -220,6 +220,11 @@ def get_rtp_info(body: str) -> RtpInfo:
220220 return RtpInfo (rtp_ip = rtp_ip , rtp_port = rtp_port , payload_type = opus_payload_type )
221221
222222
223+ def get_header (headers : dict [str , str ], name : str ) -> tuple [str , str ] | None :
224+ """Get a header entry using a case insensitive key comparison."""
225+ return next (((k , v ) for k , v in headers .items () if k .lower () == name .lower ()), None )
226+
227+
223228class SipDatagramProtocol (asyncio .DatagramProtocol , ABC ):
224229 """UDP server for the Session Initiation Protocol (SIP)."""
225230
@@ -289,25 +294,28 @@ def outgoing_call(
289294 (destination .host , destination .port ),
290295 )
291296
297+ invite_msg = SipMessage .parse_sip (invite_text , False )
298+
292299 return CallInfo (
293300 caller_endpoint = destination ,
294301 local_endpoint = source ,
295302 caller_rtp_port = rtp_port ,
296303 server_ip = source .host ,
297- headers = { "call-id" : call_id } ,
304+ headers = invite_msg . headers ,
298305 )
299306
300307 def hang_up (self , call_info : CallInfo ):
301308 """Hang up the call when finished"""
302309 if self .transport is None :
303310 raise RuntimeError ("No transport available for sending hangup." )
304311
312+ call_id = get_header (call_info .headers , "call-id" )[1 ]
305313 bye_lines = [
306314 f"BYE { call_info .caller_endpoint .uri } SIP/2.0" ,
307315 f"Via: SIP/2.0/UDP { call_info .local_endpoint .host } :{ call_info .local_endpoint .port } " ,
308316 f"From: { call_info .local_endpoint .sip_header } " ,
309317 f"To: { call_info .caller_endpoint .sip_header } " ,
310- f"Call-ID: { call_info . headers [ 'call-id' ] } " ,
318+ f"Call-ID: { call_id } " ,
311319 "CSeq: 51 BYE" ,
312320 f"User-Agent: { VOIP_UTILS_AGENT } 1.0" ,
313321 "Content-Length: 0" ,
@@ -328,17 +336,27 @@ def cancel_call(self, call_info: CallInfo):
328336 if self .transport is None :
329337 raise RuntimeError ("No transport available for sending cancel." )
330338
331- cancel_lines = [
332- f"CANCEL { call_info .caller_endpoint .uri } SIP/2.0" ,
333- f"Via: SIP/2.0/UDP { call_info .local_endpoint .host } :{ call_info .local_endpoint .port } " ,
334- f"From: { call_info .local_endpoint .sip_header } " ,
335- f"To: { call_info .caller_endpoint .sip_header } " ,
336- f"Call-ID: { call_info .headers ['call-id' ]} " ,
337- "CSeq: 51 CANCEL" ,
338- f"User-Agent: { VOIP_UTILS_AGENT } 1.0" ,
339- "Content-Length: 0" ,
340- "" ,
339+ required_headers = ("via" , "from" , "to" , "call-id" )
340+
341+ cancel_headers = [
342+ f"{ k } : { v } "
343+ for k , v in call_info .headers .items ()
344+ if k .lower () in required_headers
341345 ]
346+
347+ cseq_header , cseq_value = get_header (call_info .headers , "cseq" )
348+ cseq_num = cseq_value .split ()[0 ]
349+
350+ cancel_lines = (
351+ [f"CANCEL { call_info .caller_endpoint .uri } SIP/2.0" ]
352+ + cancel_headers
353+ + [
354+ f"{ cseq_header } : { cseq_num } CANCEL" ,
355+ f"User-Agent: { VOIP_UTILS_AGENT } 1.0" ,
356+ "Content-Length: 0" ,
357+ "" ,
358+ ]
359+ )
342360 _LOGGER .debug ("Canceling call..." )
343361 cancel_text = _CRLF .join (cancel_lines ) + _CRLF
344362 cancel_bytes = cancel_text .encode ("utf-8" )
@@ -347,7 +365,7 @@ def cancel_call(self, call_info: CallInfo):
347365 (call_info .caller_endpoint .host , call_info .caller_endpoint .port ),
348366 )
349367
350- self ._end_outgoing_call (call_info .headers [ "call-id" ])
368+ self ._end_outgoing_call (get_header ( call_info .headers , "call-id" )[ 1 ])
351369 self .on_hangup (call_info )
352370
353371 def _register_outgoing_call (self , call_id : str , rtp_port : int ):
@@ -482,6 +500,31 @@ def datagram_received(self, data: bytes, addr):
482500 # TODO: Verify that the call / sequence IDs match our outgoing INVITE
483501 _LOGGER .debug ("Received response [%s]" , message )
484502 is_ok = smsg .code == "200" and smsg .reason == "OK"
503+ if smsg .code == "487" :
504+ # A 487 Request Terminated will be sent in response to a Cancel message
505+ _LOGGER .debug ("Got 487 Request Terminated" )
506+ caller_endpoint = None
507+ if smsg .headers .get ("to" ) is not None :
508+ caller_endpoint = SipEndpoint (smsg .headers .get ("to" , "" ))
509+ else :
510+ caller_endpoint = get_sip_endpoint (
511+ caller_ip , port = caller_sip_port
512+ )
513+ cseq_num = get_header (smsg .headers , "cseq" )[1 ].split ()[0 ]
514+ ack_lines = [
515+ f"ACK { caller_endpoint .uri } SIP/2.0" ,
516+ f"Via: { smsg .headers ['via' ]} " ,
517+ f"From: { smsg .headers ['from' ]} " ,
518+ f"To: { smsg .headers ['to' ]} " ,
519+ f"Call-ID: { smsg .headers ['call-id' ]} " ,
520+ f"CSeq: { cseq_num } ACK" ,
521+ f"User-Agent: { VOIP_UTILS_AGENT } 1.0" ,
522+ "Content-Length: 0" ,
523+ ]
524+ ack_text = _CRLF .join (ack_lines ) + _CRLF
525+ ack_bytes = ack_text .encode ("utf-8" )
526+ self .transport .sendto (ack_bytes , (caller_ip , caller_sip_port ))
527+ return
485528 if not is_ok :
486529 _LOGGER .debug ("Received non-OK response [%s]" , message )
487530 return
0 commit comments