diff --git a/NOTES b/NOTES new file mode 100644 index 0000000..543a662 --- /dev/null +++ b/NOTES @@ -0,0 +1,12 @@ +New in v1.0.0: +Originate Calls +Added blocking feature to readAudio, this makes readAudio not return until there is data to be returned. If blocking is off and data is not available, bytes(length) will be returned. +Now properly generating SIP tags to comply with the RFC. +Other bug fixes + +Currently Known Issues: +BYE request on originated calls causes a 500 Error on Asterisk 13 (other versions not tested). Unsure what causes this, reach out if you have a fix. +Currently does not work with PJSIP (Only tested with Asterisk 18) + +Upcoming patches/changes: +Adjust code to be compatible with Asterisk PJSIP. diff --git a/docs/RTP.rst b/docs/RTP.rst index 3e4aa58..e113e90 100644 --- a/docs/RTP.rst +++ b/docs/RTP.rst @@ -194,8 +194,11 @@ The RTPClient is used to send and receive RTP packets and encode/decode the audi **stop**\ () This method is called by :ref:`VoIPCall`.hangup() and :ref:`VoIPCall`.bye(). It stops the recv() and trans() threads. It will also close the bound port. **This should not be called by the** :term:`user`. - **read**\ (length=160) - This method is called by :ref:`VoIPCall`.readAudio(). It reads linear/raw audio data from the received buffer. Returns *length* amount of bytes. Default length is 160 as that is the amount of bytes sent per PCMU/PCMA packet. + **read**\ (length=160, blocking=True) + This method is called by :ref:`VoIPCall`.readAudio(). It reads linear/raw audio data from the received buffer. Returns *length* amount of bytes. Default length is 160 as that is the amount of bytes sent per PCMU/PCMA packet. When *blocking* is set to true, this function will not return until data is available. When *blocking* is set to false and data is not available, this function will return bytes(length). + + **write**\ (data) + This method is called by :ref:`VoIPCall`.writeAudio(). It queues the data written to be sent to the :term:`client`. **recv**\ () This method is called by RTPClient.start() and is responsible for receiving and parsing through RTP packets. **This should not be called by the** :term:`user`. diff --git a/docs/SIP.rst b/docs/SIP.rst index 0ae9e31..cc4effc 100644 --- a/docs/SIP.rst +++ b/docs/SIP.rst @@ -86,6 +86,9 @@ The SIPClient class is used to communicate with the PBX/VoIP server. It is resp **recv**\ () This method is called by SIPClient.start() and is responsible for receiving and parsing through SIP requests. **This should not be called by the** :term:`user`. + **parseMessage**\ (message) + This method is called by SIPClient.recv() and is responsible for parsing through SIP responses. **This should not be called by the** :term:`user`. + **start**\ () This method is called by :ref:`VoIPPhone`.start(). It starts the REGISTER and recv() threads. It is also what initiates the bound port. **This should not be called by the** :term:`user`. @@ -95,6 +98,9 @@ The SIPClient class is used to communicate with the PBX/VoIP server. It is resp **genCallID**\ () This method is called by other 'gen' methods when a new Call-ID header is needed. See `RFC 3261 Section 20.8 `_. **This should not be called by the** :term:`user`. + **genTag**\ () + This method is called by other 'gen' methods when a new tag is needed. See `RFC 3261 Section 8.2.6.2 `_. **This should not be called by the** :term:`user`. + **getSIPVersoinNotSupported**\ () This method is called by the recv() thread when it has received a SIP message that is not SIP version 2.0. @@ -107,6 +113,12 @@ The SIPClient class is used to communicate with the PBX/VoIP server. It is resp **genBusy**\ (request) This method generates a SIP 486 'Busy Here' response. The *request* argument should be a SIP INVITE request. + **genOk**\ (request) + This method generates a SIP 200 'Ok' response. The *request* argument should be a SIP BYE request. + + **genInvite**\ (number, sess_id, ms, sendtype, branch, call_id) + This method generates a SIP INVITE request. This is called by SIPClient.invite(). The *number* argument must be the number being called as a string. The *sess_id* argument must be a unique number. The *ms* argument is a dictionary of the media types to be used. Currently only PCMU and telephone-event is supported. The *sendtype* argument must be an instance of :ref:`TransmitType`. The *branch* argument must be a unique string starting with "z9hG4bK". See `RFC 3261 Section 8.1.1.7 `_. The *call_id* argument must be a unique string. See `RFC 3261 Section 8.1.1.4 `_. + **genRinging**\ (request) This method generates a SIP 180 'Ringing' response. The *request* argument should be a SIP INVITE request. @@ -124,6 +136,12 @@ The SIPClient class is used to communicate with the PBX/VoIP server. It is resp **genBye**\ (request) This method generates a SIP BYE request. This is used to end a call. The *request* argument should be a SIP INVITE request. **This should not be called by the** :term:`user`. + **genAck**\ (request) + This method generates a SIP ACK response. The *request* argument should be a SIP 401 response. + + **invite**\ (number, ms, sendtype) + This method generates a SIP INVITE request. This method is called by :ref:`VoIPPhone`.call(). The *number* argument must be the number being called as a string. The *ms* argument is a dictionary of the media types to be used. Currently only PCMU and telephone-event is supported. The *sendtype* argument must be an instance of :ref:`TransmitType`. + **bye**\ (request) This method is called by :ref:`VoIPCall`.hangup(). It calls genBye(), and then transmits the generated request. **This should not be called by the** :term:`user`. diff --git a/docs/VoIP.rst b/docs/VoIP.rst index 244a1c9..eed09c6 100644 --- a/docs/VoIP.rst +++ b/docs/VoIP.rst @@ -22,7 +22,10 @@ Enums .. _callstate: VoIP.\ **CallState** - CallState is an Enum with three attributes. + CallState is an Enum with four attributes. + + CallState.\ **DIALING*** + This CallState is used to describe when a :term:`user` has originated a call to a :term:`client`, but it has yet to be answered. CallState.\ **RINGING** This CallState is used to describe when a :term:`client` is calling, but the call has yet to be answered. @@ -80,6 +83,9 @@ The VoIPCall class is used to represent a single VoIP Session, which may be to m **answer**\ () Answers the call if the phone's state is CallState.RINGING. + **answered**\ (request) + This function is called by :ref:`SIPClient` when a call originated by the :term:`user` has been answered by the :term:`client`. + **deny**\ () Denies the call if the phone's state is CallState.RINGING. @@ -92,8 +98,8 @@ The VoIPCall class is used to represent a single VoIP Session, which may be to m **writeAudio**\ (data) Writes linear/raw audio data to the transmit buffer before being encoded and sent. The *data* argument MUST be bytes. **This audio must be linear/not encoded,** :ref:`RTPClient` **will encode it before transmitting.** - **readAudio**\ (length=160) - Reads linear/raw audio data from the received buffer. Returns *length* amount of bytes. Default length is 160 as that is the amount of bytes sent per PCMU/PCMA packet. + **readAudio**\ (length=160, blocking=True) + Reads linear/raw audio data from the received buffer. Returns *length* amount of bytes. Default length is 160 as that is the amount of bytes sent per PCMU/PCMA packet. When *blocking* is set to true, this function will not return until data is available. When *blocking* is set to false and data is not available, this function will return bytes(length). .. _VoIPPhone: @@ -128,5 +134,6 @@ The VoIPPhone class is used to manage the :ref:`SIPClient` class and create :ref **stop**\ () This method ends all currently ongoing calls, then stops the :ref:`SIPClient` class - + **call**\ (number) + Originates a call using PCMU and telephone-event. The *number* argument must be a string, and it returns a :ref:`VoIPCall` class in CallState.DIALING. You should use a while loop to wait until the CallState is ANSWRED. **NOTE:** In testing with Asterisk 13, calls made this way could not hangup. This issue may exist on other PBXs as well. \ No newline at end of file diff --git a/docs/index.rst b/docs/index.rst index daedae1..be8c0b4 100644 --- a/docs/index.rst +++ b/docs/index.rst @@ -8,7 +8,7 @@ Welcome to pyVoIP's documentation! PyVoIP is a pure python VoIP/SIP/RTP library. Currently, it supports PCMA, PCMU, and telephone-event. -Please note this is a beta version and is currently only able to recieve calls, and transmit PCMU. In future, it will be able to initiate calls in PCMA as well. +Please note this is is still in development and can only originate calls with PCMU. In future, it will be able to initiate calls in PCMA as well. This library does not depend on a sound library, i.e. you can use any sound library that can handle linear sound data such as pyaudio or even wave. Keep in mind PCMU only supports 8000Hz, 1 channel, 8 bit audio. diff --git a/pyVoIP/RTP.py b/pyVoIP/RTP.py index 6e5bab7..2b1c1dc 100644 --- a/pyVoIP/RTP.py +++ b/pyVoIP/RTP.py @@ -94,14 +94,7 @@ def __str__(self): #Non-codec EVENT = "telephone-event", 8000, 0, "telephone-event" - -''' -Note to self. -Probably should set the packet manager back to ByteIO to prevent out of order packets. -I'm thinking before write, log the cursor position, then go to the timestamp position, -write, then go to the logged possition. Again this will stop out of sync packet issues. -''' class RTPPacketManager(): def __init__(self): self.offset = 4294967296 #The largest number storable in 4 bytes + 1. This will ensure the offset adjustment in self.write(offset, data) works. @@ -249,7 +242,7 @@ def stop(self): self.sin.close() self.sout.close() - def read(self, length=160, blocking=False): + def read(self, length=160, blocking=True): if not blocking: return self.pmin.read(length) packet = self.pmin.read(length) diff --git a/pyVoIP/SIP.py b/pyVoIP/SIP.py index 5090bb2..28c7cfe 100644 --- a/pyVoIP/SIP.py +++ b/pyVoIP/SIP.py @@ -164,7 +164,6 @@ def __new__(cls, value): obj = int.__new__(cls, value) obj._value_ = value return obj - MESSAGE = 1 RESPONSE = 0 @@ -208,7 +207,7 @@ def parse(self, data): if check in self.SIPCompatibleVersions: self.type = SIPMessageType.RESPONSE - self.parseSIPResponce(data) + self.parseSIPResponse(data) elif check in self.SIPCompatibleMethods: self.type = SIPMessageType.MESSAGE self.parseSIPMessage(data) @@ -218,7 +217,12 @@ def parse(self, data): def parseHeader(self, header, data): if header=="Via": info = re.split(" |;", data) - self.headers['Via'] = {'type': info[0], 'address':(info[1].split(':')[0], info[1].split(':')[1]), 'branch': info[2].split('=')[1]} + self.headers['Via'] = {'type': info[0], 'address':(info[1].split(':')[0], info[1].split(':')[1])} + for x in info[2:]: #Sets branch, maddr, ttl, received, and rport if defined as per RFC 3261 20.7 + if '=' in x: + self.headers['Via'][x.split('=')[0]] = x.split('=')[1] + else: + self.headers['Via'][x] = None elif header=="From" or header=="To": info = data.split(';tag=') tag = '' @@ -410,7 +414,7 @@ def parseBody(self, header, data): else: self.body[header] = data - def parseSIPResponce(self, data): + def parseSIPResponse(self, data): headers = data.split(b'\r\n\r\n')[0] body = data.split(b'\r\n\r\n')[1] @@ -430,7 +434,15 @@ def parseSIPResponce(self, data): for x in headers: self.parseHeader(x, headers[x]) - + + if len(body)>0: + body_raw = body.split(b'\r\n') + body_tags={} + for x in body_raw: + i = str(x, 'utf8').split('=') + if i != ['']: + self.parseBody(i[0], i[1]) + def parseSIPMessage(self, data): headers = data.split(b'\r\n\r\n')[0] body = data.split(b'\r\n\r\n')[1] @@ -474,7 +486,8 @@ def __init__(self, server, port, username, password, myIP=None, myPort=5060, cal self.callCallback=callCallback - self.tag=hashlib.md5(str(random.randint(1, 10000)).encode('utf8')).hexdigest()[0:8] + self.tags = [] + self.tagLibrary = {} self.myPort = myPort @@ -482,6 +495,7 @@ def __init__(self, server, port, username, password, myIP=None, myPort=5060, cal self.registerCounter = Counter() self.byeCounter = Counter() self.callID = Counter() + self.sessID = Counter() self.registerThread = None self.recvLock = Lock() @@ -494,30 +508,12 @@ def recv(self): try: message = SIPMessage(self.s.recv(8192)) #print(message.summary()) - if message.type != SIPMessageType.MESSAGE: - if message.status == SIPStatus.OK: - pass - else: - print("TODO: Add 500 Error on Receiving SIP Response") - self.s.setblocking(True) - self.recvLock.release() - continue - if message.method == "INVITE": - if self.callCallback == None: - request = self.genBusy(message) - self.out.sendto(request.encode('utf8'), (self.server, self.port)) - else: - request = self.genRinging(message) - self.out.sendto(request.encode('utf8'), (self.server, self.port)) - self.callCallback(message) - elif message.method == "BYE": - self.callCallback(message) - elif message.method == "ACK": - pass - else: - print("TODO: Add 400 Error on non processable request") + self.parseMessage(message) except BlockingIOError: + self.s.setblocking(True) + self.recvLock.release() time.sleep(0.01) + continue except SIPParseError as e: if "SIP Version" in str(e): request = self.genSIPVersionNotSupported(message) @@ -529,6 +525,32 @@ def recv(self): self.s.setblocking(True) self.recvLock.release() + def parseMessage(self, message): + if message.type != SIPMessageType.MESSAGE: + if message.status == SIPStatus.OK: + if self.callCallback != None: + self.callCallback(message) + else: + print("TODO: Add 500 Error on Receiving SIP Response")#:\r\n"+message.summary()) + self.s.setblocking(True) + return + elif message.method == "INVITE": + if self.callCallback == None: + request = self.genBusy(message) + self.out.sendto(request.encode('utf8'), (self.server, self.port)) + else: + request = self.genRinging(message) + self.out.sendto(request.encode('utf8'), (self.server, self.port)) + self.callCallback(message) + elif message.method == "BYE": + self.callCallback(message) + response = self.genOk(message) + self.out.sendto(response.encode('utf8'), (self.server, self.port)) + elif message.method == "ACK": + return + else: + print("TODO: Add 400 Error on non processable request") + def start(self): self.s = socket.socket(socket.AF_INET, socket.SOCK_DGRAM) self.out = socket.socket(socket.AF_INET, socket.SOCK_DGRAM) @@ -546,13 +568,20 @@ def stop(self): self.out.close() def genCallID(self): - return hashlib.sha256(str(self.callID.next()).encode('utf8')).hexdigest() + return hashlib.sha256(str(self.callID.next()).encode('utf8')).hexdigest()[0:32]+"@"+self.myIP+":"+str(self.myPort) + + def genTag(self): + while True: + tag = hashlib.md5(str(random.randint(1, 4294967296)).encode('utf8')).hexdigest()[0:8] + if tag not in self.tags: + self.tags.append(tag) + return tag def genSIPVersionNotSupported(self, request): regRequest = "SIP/2.0 505 SIP Version Not Supported\r\n" regRequest += "Via: SIP/2.0/UDP "+request.headers['Via']['address'][0]+":"+request.headers['Via']['address'][1]+";branch="+request.headers['Via']['branch']+"\r\n" regRequest += "From: "+request.headers['From']['raw']+";tag="+request.headers['From']['tag']+"\r\n" - regRequest += "To: "+request.headers['To']['raw']+";tag="+self.tag+"\r\n" + regRequest += "To: "+request.headers['To']['raw']+";tag="+self.genTag()+"\r\n" regRequest += "Call-ID: "+request.headers['Call-ID']+"\r\n" regRequest += "CSeq: "+request.headers['CSeq']['check']+" "+request.headers['CSeq']['method']+"\r\n" regRequest += "Contact: "+request.headers['Contact']+"\r\n" #TODO: Add Supported @@ -563,7 +592,7 @@ def genSIPVersionNotSupported(self, request): def genAuthorization(self, request): HA1 = hashlib.md5(self.username.encode('utf8')+b':'+request.authentication['realm'].encode('utf8')+b':'+self.password.encode('utf8')).hexdigest().encode('utf8') - HA2 = hashlib.md5(b'REGISTER:sip:'+self.server.encode('utf8')+b';transport=UDP').hexdigest().encode('utf8') + HA2 = hashlib.md5(request.headers['CSeq']['method'].encode('utf8')+b':sip:'+self.server.encode('utf8')+b';transport=UDP').hexdigest().encode('utf8') nonce = request.authentication['nonce'].encode('utf8') response = hashlib.md5(HA1+b':'+nonce+b':'+HA2).hexdigest().encode('utf8') @@ -579,7 +608,7 @@ def genRegister(self, request): regRequest += self.username+"@"+self.myIP+":"+str(self.myPort) regRequest += ";transport=UDP>\r\nTo: \r\nFrom: \r\n" #TODO: Add Supported @@ -655,12 +702,57 @@ def genAnswer(self, request, sess_id, ms, sendtype): regRequest += body return regRequest - + + def genInvite(self, number, sess_id, ms, sendtype, branch, call_id): + #Generate body first for content length + body = "v=0\r\n" + body += "o=pyVoIP "+sess_id+" "+sess_id+" IN IP4 "+self.myIP+"\r\n" #TODO: Check IPv4/IPv6 + body += "s=pyVoIP """+pyVoIP.__version__+"\r\n" + body += "c=IN IP4 "+self.myIP+"\r\n" #TODO: Check IPv4/IPv6 + body += "t=0 0\r\n" + for x in ms: + body += "m=audio "+str(x)+" RTP/AVP" #TODO: Check AVP mode from request + for m in ms[x]: + body += " "+str(m) + body += "\r\n" #m=audio RTP/AVP \r\n + for x in ms: + for m in ms[x]: + body += "a=rtpmap:"+str(m)+" "+str(ms[x][m])+"/"+str(ms[x][m].rate)+"\r\n" + if str(ms[x][m]) == "telephone-event": + body += "a=fmtp:"+str(m)+" 0-15\r\n" + body += "a=ptime:20\r\n" + body += "a=maxptime:150\r\n" + body += "a="+str(sendtype)+"\r\n" + + tag = self.genTag() + self.tagLibrary[call_id] = tag + + invRequest = "INVITE sip:"+number+"@"+self.server+" SIP/2.0\r\n" + invRequest += "Via: SIP/2.0/UDP "+self.myIP+":"+str(self.myPort)+";branch="+branch+"\r\n" + invRequest += "Max-Forwards: 70\r\n" + invRequest += "Contact: \r\n" + invRequest += "To: \r\n" + invRequest += "From: ;tag="+tag+"\r\n" + invRequest += "Call-ID: "+call_id+"\r\n" + invRequest += "CSeq: "+str(self.inviteCounter.next())+" INVITE\r\n" + invRequest += "Allow: "+(", ".join(pyVoIP.SIPCompatibleMethods))+"\r\n" + invRequest += "Content-Type: application/sdp\r\n" + invRequest += "User-Agent: pyVoIP """+pyVoIP.__version__+"\r\n" + invRequest += "Content-Length: "+str(len(body))+"\r\n\r\n" + invRequest += body + + return invRequest + def genBye(self, request): + tag = self.tagLibrary[request.headers['Call-ID']] byeRequest = "BYE "+request.headers['Contact'].strip('<').strip('>')+" SIP/2.0\r\n" byeRequest += "Via: SIP/2.0/UDP "+self.myIP+":"+str(self.myPort)+";branch="+request.headers['Via']['branch']+"\r\n" - byeRequest += "To: "+request.headers['From']['raw']+";tag="+request.headers['From']['tag']+"\r\n" - byeRequest += "From: "+request.headers['To']['raw']+";tag="+self.tag+"\r\n" + if request.headers['From']['tag'] == tag: + byeRequest += "From: "+request.headers['From']['raw']+";tag="+tag+"\r\n" + byeRequest += "To: "+request.headers['To']['raw']+(";tag="+request.headers['To']['tag'] if request.headers['To']['tag'] != '' else '')+"\r\n" + else: + byeRequest += "To: "+request.headers['From']['raw']+";tag="+request.headers['From']['tag']+"\r\n" + byeRequest += "From: "+request.headers['To']['raw']+";tag="+tag+"\r\n" byeRequest += "Call-ID: "+request.headers['Call-ID']+"\r\n" byeRequest += "CSeq: "+str(self.byeCounter.next())+" BYE\r\n" byeRequest += "Contact: \r\n" @@ -669,6 +761,61 @@ def genBye(self, request): byeRequest += "Content-Length: 0\r\n\r\n" return byeRequest + + def genAck(self, request): + tag = self.tagLibrary[request.headers['Call-ID']] + ackMessage = "ACK "+request.headers['To']['raw'].strip('<').strip('>')+" SIP/2.0\r\n" + ackMessage += "Via: SIP/2.0/UDP "+self.myIP+":"+str(self.myPort)+";branch="+request.headers['Via']['branch']+"\r\n" + ackMessage += "Max-Forwards: 70\r\n" + ackMessage += "To: "+request.headers['To']['raw']+";tag="+request.headers['To']['tag']+"\r\n" + ackMessage += "From: "+request.headers['From']['raw']+";tag="+tag+"\r\n" + ackMessage += "Call-ID: "+request.headers['Call-ID']+"\r\n" + ackMessage += "CSeq: "+str(request.headers['CSeq']['check'])+" ACK\r\n" + ackMessage += "User-Agent: pyVoIP """+pyVoIP.__version__+"\r\n" + ackMessage += "Content-Length: 0\r\n\r\n" + + return ackMessage + + def invite(self, number, ms, sendtype): + branch = "z9hG4bK"+self.genCallID()[0:25] + call_id = self.genCallID() + sess_id = self.sessID.next() + invite = self.genInvite(number, str(sess_id), ms, sendtype, branch, call_id) + #print("Acquiring") + self.recvLock.acquire() + #print("Locked") + self.out.sendto(invite.encode('utf8'), (self.server, self.port)) + #print('Invited') + response = SIPMessage(self.s.recv(8192)) + + while response.status != SIPStatus(401) and response.status != SIPStatus(100) and response.status != SIPStatus(180): + if not self.NSD: + break + if response.status == SIPStatus(100) or response.status == SIPStatus(180): + return SIPMessage(invite.encode('utf8')), call_id, sess_id + self.parseMessage(response) + response = SIPMessage(self.s.recv(8192)) + + #print("Received Response: "+response.summary()) + + ack = self.genAck(response) + self.out.sendto(ack.encode('utf8'), (self.server, self.port)) + #print("Acknowledged") + authhash = self.genAuthorization(response) + nonce = response.authentication['nonce'] + auth = 'Authorization: Digest username="'+self.username + auth += '",realm="asterisk",nonce="'+nonce + auth += '",uri="sip:'+self.server + auth += ';transport=UDP",response="'+str(authhash, 'utf8') + auth += '",algorithm=MD5\r\n' + + invite = self.genInvite(number, str(sess_id), ms, sendtype, branch, call_id) + invite = invite.replace('\r\nContent-Length', '\r\n'+auth+'Content-Length') + + self.out.sendto(invite.encode('utf8'), (self.server, self.port)) + + self.recvLock.release() + return SIPMessage(invite.encode('utf8')), call_id, sess_id def bye(self, request): message = self.genBye(request) @@ -690,6 +837,8 @@ def deregister(self): self.recvLock.release() time.sleep(5) return self.deregister() + else: + self.parseMessage(response) response = SIPMessage(self.s.recv(8192)) regRequest = self.genRegister(response).replace('Expires: 300', 'Expires: 0') @@ -697,7 +846,6 @@ def deregister(self): self.out.sendto(regRequest.encode('utf8'), (self.server, self.port)) response = SIPMessage(self.s.recv(8192)) - self.recvLock.release() if response.status==SIPStatus.OK: return True self.recvLock.release() @@ -712,12 +860,14 @@ def register(self): self.out.sendto(regRequest.encode('utf8'), (self.server, self.port)) response = SIPMessage(self.s.recv(8192)) - + if response.status != SIPStatus(401): if response.status == SIPStatus(500): self.recvLock.release() time.sleep(5) return self.register() + else: + self.parseMessage(response) regRequest = self.genRegister(response) @@ -725,9 +875,10 @@ def register(self): response = SIPMessage(self.s.recv(8192)) self.recvLock.release() - if response.status==SIPStatus.OK: + if response.status == SIPStatus.OK: if self.NSD: - self.registerThread=Timer(295, self.register) + self.registerThread = Timer(295, self.register) + self.registerThread.name = "SIP Register" self.registerThread.start() return True else: diff --git a/pyVoIP/VoIP.py b/pyVoIP/VoIP.py index 481de67..cb06510 100644 --- a/pyVoIP/VoIP.py +++ b/pyVoIP/VoIP.py @@ -15,21 +15,25 @@ class InvalidStateError(Exception): pass class CallState(Enum): - RINGING = 0 - ANSWERED = 1 - ENDED = 2 + DIALING = "DIALING" + RINGING = "RINGING" + ANSWERED = "ANSWERED" + ENDED = "ENDED" +''' +For initiating a phone call, try sending the packet and the recieved OK packet will be sent to the VoIPCall request header. +''' class VoIPCall(): - def __init__(self, phone, request, session_id, myIP, rtpPortLow, rtpPortHigh): - self.state = CallState.RINGING + def __init__(self, phone, callstate, request, session_id, myIP, portRange=(10000, 20000), ms = None): + self.state = callstate self.phone = phone self.sip = self.phone.sip self.request = request self.call_id = request.headers['Call-ID'] self.session_id = str(session_id) self.myIP = myIP - self.rtpPortHigh = rtpPortHigh - self.rtpPortLow = rtpPortLow + self.rtpPortHigh = portRange[1] + self.rtpPortLow = portRange[0] self.dtmfLock = Lock() self.dtmf = io.StringIO() @@ -42,63 +46,68 @@ def __init__(self, phone, request, session_id, myIP, rtpPortLow, rtpPortHigh): self.assignedPorts = {} - audio = [] - video = [] - for x in self.request.body['c']: - self.connections += x['address_count'] - for x in self.request.body['m']: - if x['type'] == "audio": - self.audioPorts += x['port_count'] - audio.append(x) - elif x['type'] == "video": - self.videoPorts += x['port_count'] - video.append(x) - else: - print("Unknown media description: "+x['type']) - - #Ports Adjusted is used in case of multiple m=audio or m=video tags. - if len(audio) > 0: - audioPortsAdj = self.audioPorts/len(audio) - else: - audioPortsAdj = 0 - if len(video) > 0: - videoPortsAdj = self.videoPorts/len(video) - else: - videoPortsAdj = 0 - - if not ((audioPortsAdj == self.connections or self.audioPorts == 0) and (videoPortsAdj == self.connections or self.videoPorts == 0)): - print("Unable to assign ports for RTP.") - return - - - for i in request.body['m']: - assoc = {} - e = False - for x in i['methods']: - try: - p = RTP.PayloadType(int(x)) - assoc[int(x)] = p - except ValueError: + if callstate == CallState.RINGING: + audio = [] + video = [] + for x in self.request.body['c']: + self.connections += x['address_count'] + for x in self.request.body['m']: + if x['type'] == "audio": + self.audioPorts += x['port_count'] + audio.append(x) + elif x['type'] == "video": + self.videoPorts += x['port_count'] + video.append(x) + else: + print("Unknown media description: "+x['type']) + + #Ports Adjusted is used in case of multiple m=audio or m=video tags. + if len(audio) > 0: + audioPortsAdj = self.audioPorts/len(audio) + else: + audioPortsAdj = 0 + if len(video) > 0: + videoPortsAdj = self.videoPorts/len(video) + else: + videoPortsAdj = 0 + + if not ((audioPortsAdj == self.connections or self.audioPorts == 0) and (videoPortsAdj == self.connections or self.videoPorts == 0)): + print("Unable to assign ports for RTP.") + return + + for i in request.body['m']: + assoc = {} + e = False + for x in i['methods']: try: - p = RTP.PayloadType(i['attributes'][x]['rtpmap']['name']) + p = RTP.PayloadType(int(x)) assoc[int(x)] = p except ValueError: - e = True - - if e: - raise RTP.ParseError("RTP Payload type {} not found.".format(str(pt))) - - port = None - while port == None: - proposed = random.randint(rtpPortLow, rtpPortHigh) - if not proposed in self.phone.assignedPorts: - self.phone.assignedPorts.append(proposed) - self.assignedPorts[proposed] = assoc - port = proposed - for ii in range(len(request.body['c'])): - offset = ii * 2 - self.RTPClients.append(RTP.RTPClient(assoc, self.myIP, port, request.body['c'][ii]['address'], i['port']+ii, request.body['a']['transmit_type'], dtmf=self.dtmfCallback)) #TODO: Check IPv4/IPv6 + try: + p = RTP.PayloadType(i['attributes'][x]['rtpmap']['name']) + assoc[int(x)] = p + except ValueError: + e = True + + if e: + raise RTP.ParseError("RTP Payload type {} not found.".format(str(pt))) + + port = None + while port == None: + proposed = random.randint(self.rtpPortLow, self.rtpPortHigh) + if not proposed in self.phone.assignedPorts: + self.phone.assignedPorts.append(proposed) + self.assignedPorts[proposed] = assoc + port = proposed + for ii in range(len(request.body['c'])): + offset = ii * 2 + self.RTPClients.append(RTP.RTPClient(assoc, self.myIP, port, request.body['c'][ii]['address'], i['port']+ii, request.body['a']['transmit_type'], dtmf=self.dtmfCallback)) #TODO: Check IPv4/IPv6 + elif callstate == CallState.DIALING: + self.ms = ms + for m in self.ms: + self.port = m + self.assignedPorts[m] = self.ms[m] def dtmfCallback(self, code): self.dtmfLock.acquire() @@ -124,6 +133,38 @@ def answer(self): message = self.sip.genAnswer(self.request, self.session_id, m, self.request.body['a']['transmit_type']) self.sip.out.sendto(message.encode('utf8'), (self.phone.server, self.phone.port)) self.state = CallState.ANSWERED + + def answered(self, request): + if self.state != CallState.DIALING: + return + + for i in request.body['m']: + assoc = {} + e = False + for x in i['methods']: + try: + p = RTP.PayloadType(int(x)) + assoc[int(x)] = p + except ValueError: + try: + p = RTP.PayloadType(i['attributes'][x]['rtpmap']['name']) + assoc[int(x)] = p + except ValueError: + e = True + + if e: + raise RTP.ParseError("RTP Payload type {} not found.".format(str(pt))) + + + for ii in range(len(request.body['c'])): + offset = ii * 2 + self.RTPClients.append(RTP.RTPClient(assoc, self.myIP, self.port, request.body['c'][ii]['address'], i['port']+ii, request.body['a']['transmit_type'], dtmf=self.dtmfCallback)) #TODO: Check IPv4/IPv6 + + for x in self.RTPClients: + x.start() + self.request.headers['Contact'] = request.headers['Contact'] + self.request.headers['To']['tag'] = request.headers['To']['tag'] + self.state = CallState.ANSWERED def deny(self): if self.state != CallState.RINGING: @@ -153,7 +194,7 @@ def writeAudio(self, data): for x in self.RTPClients: x.write(data) - def readAudio(self, length=160, blocking=False): + def readAudio(self, length=160, blocking=True): if len(self.RTPClients) == 1: return self.RTPClients[0].read(length, blocking) data = [] @@ -191,32 +232,45 @@ def __init__(self, server, port, username, password, callCallback=None, myIP=Non def callback(self, request): call_id = request.headers['Call-ID'] - if request.method == "INVITE": - if call_id in self.calls: - return #Raise Error - if self.callCallback == None: - message = self.sip.genBusy(request) - self.sip.out.sendto(message.encode('utf8'), (self.server, self.port)) - else: - sess_id = None - while sess_id == None: - proposed = random.randint(1, 100000) - if not proposed in self.session_ids: - self.session_ids.append(proposed) - sess_id = proposed - self.calls[call_id] = VoIPCall(self, request, sess_id, self.myIP, self.rtpPortLow, self.rtpPortHigh) - try: - t = Timer(1, self.callCallback, [self.calls[call_id]]) - t.name = "Phone Call: "+call_id - t.start() - except Exception as e: + #print("Callback: "+request.summary()) + if request.type == pyVoIP.SIP.SIPMessageType.MESSAGE: + #print("This is a message") + if request.method == "INVITE": + if call_id in self.calls: + return #Raise Error + if self.callCallback == None: message = self.sip.genBusy(request) self.sip.out.sendto(message.encode('utf8'), (self.server, self.port)) - raise e - elif request.method == "BYE": - if not call_id in self.calls: - return - self.calls[call_id].bye() + else: + sess_id = None + while sess_id == None: + proposed = random.randint(1, 100000) + if not proposed in self.session_ids: + self.session_ids.append(proposed) + sess_id = proposed + self.calls[call_id] = VoIPCall(self, CallState.RINGING, request, sess_id, self.myIP, portRange=(self.rtpPortLow, self.rtpPortHigh)) + try: + t = Timer(1, self.callCallback, [self.calls[call_id]]) + t.name = "Phone Call: "+call_id + t.start() + except Exception as e: + message = self.sip.genBusy(request) + self.sip.out.sendto(message.encode('utf8'), (self.server, self.port)) + raise e + elif request.method == "BYE": + if not call_id in self.calls: + return + self.calls[call_id].bye() + else: + if request.status == SIP.SIPStatus.OK: + #print("OK recieved") + if not call_id in self.calls: + #print("Unknown call") + return + self.calls[call_id].answered(request) + #print("Answered") + ack = self.sip.genAck(request) + self.sip.out.sendto(ack.encode('utf8'), (self.server, self.port)) def start(self): self.sip.start() @@ -227,4 +281,18 @@ def stop(self): self.calls[x].hangup() except InvalidStateError: pass - self.sip.stop() \ No newline at end of file + self.sip.stop() + + def call(self, number): + port = None + while port == None: + proposed = random.randint(self.rtpPortLow, self.rtpPortHigh) + if not proposed in self.assignedPorts: + self.assignedPorts.append(proposed) + port = proposed + medias = {} + medias[port] = {0: pyVoIP.RTP.PayloadType.PCMU, 101: pyVoIP.RTP.PayloadType.EVENT} + request, call_id, sess_id = self.sip.invite(number, medias, pyVoIP.RTP.TransmitType.SENDRECV) + self.calls[call_id] = VoIPCall(self, CallState.DIALING, request, sess_id, self.myIP, ms = medias) + + return self.calls[call_id] \ No newline at end of file diff --git a/pyVoIP/__init__.py b/pyVoIP/__init__.py index 4735b30..d85c1bc 100644 --- a/pyVoIP/__init__.py +++ b/pyVoIP/__init__.py @@ -1,6 +1,6 @@ __all__ = ['SIP', 'RTP', 'VoIP'] -version_info = (0, 5, 2, '') +version_info = (1, 0, 0) __version__ = ".".join([str(x) for x in version_info])