sip.py :  » Web-Services » Shtoom » shtoom-0.2 » shtoom » Python Open Source

Home
Python Open Source
1.3.1.2 Python
2.Ajax
3.Aspect Oriented
4.Blog
5.Build
6.Business Application
7.Chart Report
8.Content Management Systems
9.Cryptographic
10.Database
11.Development
12.Editor
13.Email
14.ERP
15.Game 2D 3D
16.GIS
17.GUI
18.IDE
19.Installer
20.IRC
21.Issue Tracker
22.Language Interface
23.Log
24.Math
25.Media Sound Audio
26.Mobile
27.Network
28.Parser
29.PDF
30.Project Management
31.RSS
32.Search
33.Security
34.Template Engines
35.Test
36.UML
37.USB Serial
38.Web Frameworks
39.Web Server
40.Web Services
41.Web Unit
42.Wiki
43.Windows
44.XML
Python Open Source » Web Services » Shtoom 
Shtoom » shtoom 0.2 » shtoom » sip.py
# Copyright (C) 2004 Anthony Baxter

'''SIP client code.'''

from interfaces import SIP

from twisted.internet.protocol import DatagramProtocol,ConnectedDatagramProtocol
from twisted.internet import defer
from twisted.internet import reactor
from twisted.protocols import sip
from rtp import RTPProtocol

import shtoom

_CACHED_LOCAL_IP = None

from twisted.python import log
import random, sys, socket

def errhandler(*args):
    print "BANG", args

def genCallId():
    return 400000000 + random.randint(0,2000000)

def genStandardDate(t=None):
    import time
    if t is None:
        t = time.gmtime()
    return time.strftime("%a, %d %b %Y %H:%M:%S GMT", t)

class Call(object):
    '''State machine for a phone call (inbound or outbound).'''

    cookie = _callID = uri = None
    nonce_count = 1
    sip = None
    _invite = None
    def __init__(self, phone, deferred, uri=None, callid=None):
        self.sip = phone
        self.compDef = deferred
        self.call_attempts = 0
        self._callID = None
        self.uri = None
        self.state = 'NEW'
        self._needSTUN = False
        self.cancel_trigger = None
        if uri:
            self.uri = uri
        if callid:
            self.setCallID(callid)
        self.cseq = random.randint(1000,5000)

    def setupLocalSIP(self, uri=None, via=None):
        ''' Setup SIP stuff at this end. Call with either a t.p.sip.URL
            (for outbound calls) or a t.p.sip.Via object (for inbound calls).

            returns a Deferred that will be triggered when the local SIP
            end is setup
        '''
        if not via:
            self.remote = uri
        else:
            self.remote = tpsip.URL(host=via.host, port=via.port)
        self.setupDeferred = defer.Deferred()
        self.setLocalIP(dest=(self.remote.host, self.remote.port or 5060))
        return self.setupDeferred

    def abortCall(self):
        # Bail out.
        self.state = 'ABORTED'

    def setCallID(self, callid=None):
        if callid:
            self._callID = callid
        else:
            self._callID = '%s@%s'%(genCallId(), self.getLocalSIPAddress()[0])

    def setState(self, state):
        self.state = state

    def getState(self):
        return self.state

    def getSTUNState(self):
        return self._needSTUN

    def getTag(self):
        if not hasattr(self, '_tag'):
            self._tag = ('%04x'%(random.randint(0, 2**10)))[:4]
            self._tag += ('%04x'%(random.randint(0, 2**10)))[:4]
        return self._tag

    def setLocalIP(self, dest):
        """ Try and determine the local IP address to use. We use a
            ConnectedDatagramProtocol in the (faint?) hope that on a machine
            with multiple interfaces, we'll get the right one
        """
        # XXX Allow over-riding
        from shtoom.stun import StunHook,getPolicy
        global _CACHED_LOCAL_IP
        host, port = dest
        getPref = self.sip.app.getPref
        if getPref('localip') is not None or _CACHED_LOCAL_IP is not None:
            ip = getPref('localip') or _CACHED_LOCAL_IP
            locAddress = (ip, getPref('listenport') or 5060)
            remAddress = ( host, port )
            # Argh. Do a DNS lookup on remAddress
        else:
            # it is a hack!
            protocol = ConnectedDatagramProtocol()
            port = reactor.connectUDP(host, port, protocol)
            if protocol.transport:
                locAddress = (protocol.transport.getHost()[1], getPref('listenport') or 5060)
                remAddress = protocol.transport.getPeer()[1:3]
                port.stopListening()
                log.msg("discovered local address %r, remote %r"%(locAddress,
                                                                  remAddress))
                _CACHED_LOCAL_IP = locAddress[0]
            else:
                self.compDef.errback(ValueError("couldn't connect to %s"%(
                                            host)))
                self.abortCall()
                return None
        useStun = getPolicy().checkStun(locAddress[0], remAddress[0])
        if useStun is True:
            self._needSTUN = True
            log.msg("stun policy says yes, use STUN")
            deferred = defer.Deferred()
            deferred.addCallback(self.setStunnedLocalIP).addErrback(log.err)
            SH = StunHook(self.sip)
            deferred = SH.discoverStun(deferred)
        elif useStun is False:
            self._localIP, self._localPort = locAddress
            if not self._callID:
                self.setCallID()
            self.sip.updateCallObject(self, self.getCallID())
            self.setupDeferred.callback((host,port))
        else:
            # None. STUN stuff failed. Abort.
            from shtoom.exceptions import HostNotKnown
            d = self.compDef
            del self.compDef
            d.errback(HostNotKnown)
            self.setState('ABORTED')

    def setStunnedLocalIP(self, (host, port)):
        log.msg("according to STUN, local address is %s:%s"%(host, port))
        self._localIP = host
        self._localPort = port
        # XXX Check for multiple firings!
        if not self._callID:
            self.setCallID()
            self.sip.updateCallObject(self, self.getCallID())
        self.setupDeferred.callback((host,port))

    def getCSeq(self, incr=0):
        if incr:
            self.cseq += 1
        return self.cseq

    def getCallID(self):
        return self._callID

    def getLocalSIPAddress(self):
        return (self._localIP, self._localPort or 5060)

    def getRemoteSIPAddress(self):
        return (self.remote.host, (self.remote.port or 5060))

    def acceptedCall(self, junk):
        self.sendResponse(self._invite, 200)
        self.setState('INVITE_OK')

    def rejectCall(self, message=None):
        ''' Accept currently pending call.
        '''
        from shtoom.exceptions import CallRejected
  print "rejecting because", message
        self.sendResponse(self._invite, 603)
        self.setState('ABORTED')
        self.compDef.errback(CallRejected)

    def sendResponse(self, message, code):
        ''' Send a response to a message. message is the response body,
            code is the response code (e.g.  200 for OK)
        '''
        from shtoom.multicast.SDP import SDP
        if message.method == 'INVITE' and code == 200:
            sdp = self.sip.app.getSDP(self.cookie)
            othersdp = SDP(message.body)
            sdp.intersect(othersdp)
            if not sdp.rtpmap:
                self.sendResponse(message, 406)
                self.setState('ABORTED')
                return
            self.sip.app.selectFormat(self.cookie, sdp.rtpmap)
        resp = tpsip.Response(code)
        #via = tpsip.parseViaHeader(message.headers['via'][0])
        #branch = via.branch
        #if branch:
        #    branch = ';branch=%s'%branch
        #else:
        #    branch = ''
        vias = message.headers['via']
        #resp.addHeader('via', 'SIP/2.0/UDP %s:%s%s'%(
        #                            self.getLocalSIPAddress()+(branch,)))
        for via in vias:
            resp.addHeader('via', via)
        resp.addHeader('from', message.headers['from'][0])
        toaddr = message.headers['to'][0]
        toname,touri,toparams = tpsip.parseAddress(toaddr)
        if not toparams.has_key('tag'):
            toaddr = "%s;tag=%s"%(toaddr, self.getTag())
        resp.addHeader('to', toaddr)
        resp.addHeader('date', genStandardDate())
        resp.addHeader('call-id', message.headers['call-id'][0])
        resp.addHeader('server', 'Shtoom/%s'%shtoom.Version)
        resp.addHeader('cseq', message.headers['cseq'][0])
        if message.method == 'INVITE' and code == 200:
            lhost, lport = self.getLocalSIPAddress()
            username = self.sip.app.getPref('username')
            resp.addHeader('contact', '<sip:%s@%s:%s>'%(
                                        username, lhost, lport))
            # We include SDP here
            resp.addHeader('content-type', 'application/sdp')
            sdp = sdp.show()
            resp.addHeader('content-length', len(sdp))
            resp.bodyDataReceived(sdp)
        else:
            resp.addHeader('content-length', 0)
        resp.creationFinished()
        try:
            self.sip.transport.write(resp.toString(), self.getRemoteSIPAddress())
            self.sip.app.debugMessage("Response sent\n"+resp.toString())
        except (socket.error, socket.gaierror):
            e,v,t = sys.exc_info()
            #self.compDef.errback(e(v))
            self.setState('ABORTED')

    def startOutboundCall(self, toAddr):
        # XXX Set up a callLater in case we don't get a response, for a 
        # retransmit
        # XXX Set up a timeout for the call completion
        uri = tpsip.parseURL(toAddr)
        d = self.setupLocalSIP(uri=uri)
        d.addCallback(lambda x:self.startSendInvite(toAddr, init=1)).addErrback(log.err)


    def startInboundCall(self, invite):
        via = tpsip.parseViaHeader(invite.headers['via'][0])
        d = self.setupLocalSIP(via=via)
        d.addCallback(lambda x:self.recvInvite(invite)).addErrback(log.err)
        if invite.headers.has_key('subject'):
            desc = invite.headers['subject'][0]
        else:
            name,uri,params =  tpsip.parseAddress(invite.headers['from'][0])
            desc = "From: %s %s" %(name,uri)
        self.cookie, defaccept = \
                    self.sip.app.acceptCall(self,
                                            calltype='inbound',
                                            desc=desc,
                                            fromIP=self.getRemoteSIPAddress()[0],
                                            withSTUN=self.getSTUNState() ,
                                            toAddr=invite.headers.get('to'),
                                            fromAddr=invite.headers.get('from'),
                                           )
        defaccept.addCallbacks(self.acceptedCall, self.rejectCall).addErrback(
                                                                        log.err)
        #self.app.incomingCall(subj, call, defaccept, defsetup)

    def recvInvite(self, invite):
        ''' Received an INVITE from another UA. If we're already on a
            call, it's an attempt to modify - send a 488 in this case.
        '''
        # XXX if self.getState() is not 'NEW', send a 488
        if self.getState() == 'SENT_RINGING':
            # Nag, nag, nag. Shut the fuck up, I'm answering...
            return
        self._invite = invite
        contact = invite.headers['contact']
        if type(contact) is list:
            contact = contact[0]
        self.contact = contact
        self.uri = invite.headers['from'][0]
        self.sendResponse(invite, 180)
        if self.getState() != 'ABORTED':
            self.setState('SENT_RINGING')
        self.installTeardownTrigger()

    def startSendInvite(self, toAddr, init=0):
        print "startSendInvite", init
        if init:
            print "ok"
            cookie, d = self.sip.app.acceptCall(self,
                                            calltype='outbound', 
                                            fromIP=self.getLocalSIPAddress()[0],
                                            withSTUN=self.getSTUNState(),
                                                )
            self.cookie = cookie
            d.addCallback(lambda x:self.sendInvite(toAddr, init=init)).addErrback(log.err)
        else:
            self.sendInvite(toAddr, init=0)

    def sendInvite(self, toAddr, auth=None, authhdr=None, init=0):
        if self.getState() == "ABORTED":
            d = self.compDef
            del self.compDef
            d.callback('call aborted')
            return
        self._toAddr = toAddr
        if toAddr is None:
            toAddr = self._toAddr
        username = self.sip.app.getPref('username')
        email_address = self.sip.app.getPref('email_address')
        invite = tpsip.Request('INVITE', str(self.remote))
        # XXX refactor all the common headers and the like
        invite.addHeader('via', 'SIP/2.0/UDP %s:%s;rport'%
                                                self.getLocalSIPAddress())
        invite.addHeader('cseq', '%s INVITE'%self.getCSeq(incr=1))
        invite.addHeader('to', str(self.uri))
        invite.addHeader('content-type', 'application/sdp')
        invite.addHeader('from', '"%s" <sip:%s>;tag=%s'%(
                            username, email_address, 
                            self.getTag()))
        invite.addHeader('call-id', self.getCallID())
        invite.addHeader('subject', 'sip: %s'%(email_address))
        invite.addHeader('user-agent', 'Shtoom/%s'%shtoom.Version)
        if auth is not None:
            print auth, authhdr
            invite.addHeader(authhdr, auth)
        lhost, lport = self.getLocalSIPAddress()
        invite.addHeader('contact', '"%s" <sip:%s:%s;transport=udp>'%(
                                username, lhost, lport))
        s = self.sip.app.getSDP(self.cookie)
        sdp = s.show()
        invite.addHeader('content-length', len(sdp))
        invite.bodyDataReceived(sdp)
        invite.creationFinished()
        try:
            self.sip.transport.write(invite.toString(), self.getRemoteSIPAddress())
            print "Invite sent", invite.toString()
        except (socket.error, socket.gaierror):
            e,v,t = sys.exc_info()
            self.compDef.errback(e(v))
            self.setState('ABORTED')
        else:
            self.setState('SENT_INVITE')
            self.installTeardownTrigger()

    def extractURI(self, val):
        name,uri,params = tpsip.parseAddress(val)
        return uri.toString()

    def sendAck(self, okmessage, startRTP=0):
        from shtoom.multicast.SDP import SDP
        print "sending ACK"
        username = self.sip.app.getPref('username')
        email_address = self.sip.app.getPref('email_address')
        oksdp = SDP(okmessage.body)
        sdp = self.sip.app.getSDP(self.cookie)
        sdp.intersect(oksdp)
        if not sdp.rtpmap:
            self.sendResponse(message, 406)
            self.setState('ABORTED')
            return
        self.sip.app.selectFormat(self.cookie, sdp.rtpmap)
        contact = okmessage.headers['contact']
        if type(contact) is list:
            contact = contact[0]
        self.contact = contact
        to = okmessage.headers['to']
        if type(to) is list:
            to = to[0]
        self.uri = self.extractURI(contact)
        uri = tpsip.parseURL(self.uri)

        ack = tpsip.Request('ACK', self.uri)
        # XXX refactor all the common headers and the like
        ack.addHeader('via', 'SIP/2.0/UDP %s:%s;rport'%self.getLocalSIPAddress())
        ack.addHeader('cseq', '%s ACK'%self.getCSeq())
        ack.addHeader('to', to)
        ack.addHeader('from', '"%s" <sip:%s>;tag=%s'%(
                            username, email_address, self.getTag()))
        ack.addHeader('call-id', self.getCallID())
        ack.addHeader('user-agent', 'Shtoom/%s'%shtoom.Version)
        ack.addHeader('content-length', 0)
        ack.creationFinished()
        if hasattr(self, 'compDef'):
            cb = self.compDef.callback
            del self.compDef
        else:
            cb = lambda *args: None
        log.msg("sending ACK to %s %s"%(uri.host, uri.port or 5060))
        self.sip.transport.write(ack.toString(), (uri.host, (uri.port or 5060)))
        self.setState('CONNECTED')
        if startRTP:
            self.sip.app.startCall(self.cookie, (oksdp.ipaddr,oksdp.port), cb)
        self.sip.app.statusMessage("Call Connected")

    def sendBye(self):
        username = self.sip.app.getPref('username')
        email_address = self.sip.app.getPref('email_address')
        uri = self.remote
        dest = uri.host, (uri.port or 5060)
        bye = tpsip.Request('BYE', self.remote)
        # XXX refactor all the common headers and the like
        bye.addHeader('via', 'SIP/2.0/UDP %s:%s;rport'%self.getLocalSIPAddress())
        bye.addHeader('cseq', '%s BYE'%self.getCSeq(incr=1))
        bye.addHeader('to', self.uri)
        bye.addHeader('from', '"%s" <sip:%s>;tag=%s'%(
                            username, email_address, self.getTag()))
        bye.addHeader('call-id', self.getCallID())
        bye.addHeader('user-agent', 'Shtoom/%s'%shtoom.Version)
        bye.addHeader('content-length', 0)
        bye.creationFinished()
        bye = bye.toString()
        print "sending BYE to %s %s"%dest
        self.sip.transport.write(bye, dest)
        self.setState('SENT_BYE')

    def sendCancel(self):
        """ Sends a CANCEL message to kill a call that's in the process of
            being established
        """
        username = self.sip.app.getPref('username')
        email_address = self.sip.app.getPref('email_address')
        uri = self.remote
        dest = uri.host, (uri.port or 5060)
        cancel = tpsip.Request('CANCEL', self.remote)
        # XXX refactor all the common headers and the like
        cancel.addHeader('via', 'SIP/2.0/UDP %s:%s;rport'%self.getLocalSIPAddress())
        cancel.addHeader('cseq', '%s BYE'%self.getCSeq(incr=1))
        cancel.addHeader('to', self.uri)
        cancel.addHeader('from', '"%s" <sip:%s>;tag=%s'%(
                            username, email_address, self.getTag()))
        cancel.addHeader('call-id', self.getCallID())
        cancel.addHeader('user-agent', 'Shtoom/%s'%shtoom.Version)
        cancel.addHeader('content-length', 0)
        cancel.creationFinished()
        cancel = cancel.toString()
        print "sending CANCEL to %s %s"%dest
        self.sip.transport.write(cancel, dest)
        self.setState('SENT_CANCEL')

    def recvBye(self, message):
        ''' An in-progress call got a BYE from theotherend.Hangup import 
            call, send a 200.
        '''
        self.sendResponse(message, 200)

    def recvCancel(self, message):
        """ The remote UAC changed it's mind about the new call and
            gave up.
        """
        self.sendResponse(message, 487)
        self.setState('ABORTED')
        self.sip.app.endCall(self.cookie)

    def recvAck(self, message):
        ''' The remote UAC has ACKed our response to their INVITE.
            Start sending and receiving audio.
        '''
        from shtoom.multicast.SDP import SDP
        sdp = SDP(self._invite.body)
        self.setState('CONNECTED')
        if hasattr(self, 'compDef'):
            d, self.compDef = self.compDef, None
            self.sip.app.startCall(self.cookie, (sdp.ipaddr,sdp.port), 
                                   d.callback)

    def recvOptions(self, message):
        """ Received an OPTIONS request from a remote UA.
            Put together a 200 response if we're ok with the Require headers,
            otherwise an error (XXXXX)
        """

    def installTeardownTrigger(self):
        if 0 and self.cancel_trigger is None:
            t = reactor.addSystemEventTrigger('before', 
                                              'shutdown', 
                                              self.dropCall, 
                                              appTeardown=True)
            self.cancel_trigger = t

    def dropCall(self, appTeardown=False):
        '''Drop call '''
        # XXX return a deferred, and handle responses properly
        if not appTeardown and self.cancel_trigger is not None:
                reactor.removeSystemEventTrigger(self.cancel_trigger)
        state = self.getState()
        print "dropcall in state", state
        if state == 'NONE':
            self.sip.app.debugMessage("no call to drop")
        elif state in ( 'CONNECTED', ):
            self.sendBye()
            self.setState('SENT_BYE')
            # XXX callLater to give up...
        elif state in ( 'SENT_INVITE', ):
            self.sendCancel()
            self.setState('SENT_CANCEL')
            # XXX callLater to give up...
        elif state in ( 'NEW', ):
            self.setState('ABORTED')

    def _getHashingImplementation(self, algorithm):
        # lambdas assume digest modules are imported at the top level
        import md5, sha
        if algorithm.lower() == 'md5':
            H = lambda x: md5.new(x).hexdigest()
        elif algorithm.lower() == 'sha':
            H = lambda x: sha.new(x).hexdigest()
        # XXX MD5-sess
        KD = lambda s, d, H=H: H("%s:%s" % (s, d))
        return H, KD

    def calcAuth(self, method, uri, authchal, cred):
        (user, passwd) = cred
        from urllib2 import parse_http_list
        import digestauth
        authmethod, auth = authchal.split(' ', 1)
        if authmethod.lower() != 'digest':
            raise ValueError, "Unknown auth method %s"%(authmethod)
        chal = digestauth.parse_keqv_list(parse_http_list(auth))
        qop = chal.get('qop', None)
        if qop and qop.lower() != 'auth':
            raise ValueError, "can't handle qop '%s'"%(qop)
        realm = chal.get('realm')
        algorithm = chal.get('algorithm', 'md5')
        nonce = chal.get('nonce')
        opaque = chal.get('opaque')
        H, KD = self._getHashingImplementation(algorithm)
        if user is None or passwd is None:
            raise RuntimeError, "Auth required, %s %s"%(user,passwd)
        A1 = '%s:%s:%s'%(user, chal['realm'], passwd)
        A2 = '%s:%s'%(method, uri)
        if qop is not None:
            self.nonce_count += 1
            ncvalue = '%08x'%(self.nonce_count)
            cnonce = digestauth.generate_nonce(bits=16,
                                           randomness=
                                              str(nonce)+str(self.nonce_count))
            # XXX nonce isn't there for proxy-auth. :-(
            noncebit =  "%s:%s:%s:%s:%s" % (nonce,ncvalue,cnonce,qop,H(A2))
            respdig = KD(H(A1), noncebit)
        else:
            noncebit =  "%s:%s" % (nonce,H(A2))
            respdig = KD(H(A1), noncebit)
        base = '%s username="%s", realm="%s", nonce="%s", uri="%s", ' \
               'response="%s"' % (authmethod, user, realm, nonce, 
                                  uri, respdig)
        if opaque:
            base = base + ', opaque="%s"' % opaque
        if algorithm.lower() != 'md5':
            base = base + ', algorithm="%s"' % algorithm
        if qop:
            base = base + ', qop=auth, nc=%s, cnonce="%s"'%(ncvalue, cnonce)
        return base

    def recvResponse(self, message):
        state = self.getState()
        print "Handling %s while in state %s"%(message.code, state)
        if message.code in ( 100, 180, 181, 182 ):
            return
        elif message.code == 200:
            if state == 'SENT_INVITE':
                self.sip.app.debugMessage("Got Response 200\n")
                self.sendAck(message, startRTP=1)
            elif state == 'CONNECTED':
                self.sip.app.debugMessage('Got duplicate OK to our ACK')
                self.sendAck(message)
            elif state == 'SENT_BYE':
                self.sip.app.endCall(self.cookie)
                self.sip._delCallObject(self.getCallID())
                self.sip.app.debugMessage("Hung up on call %s"%self.getCallID())
                self.sip.app.statusMessage("Call Disconnected")
            else:
                self.sip.app.debugMessage('Got OK in unexpected state %s'%state)
        elif message.code - (message.code%100) == 400:
            if state == 'SENT_CANCEL' and message.code == 487:
                print "cancelled"
                self.sip.app.endCall(self.cookie)
                self.sip._delCallObject(self.getCallID())
                self.sip.app.debugMessage("Hung up on call %s"%self.getCallID())
                self.sip.app.statusMessage("Call Disconnected")
                return
            self.call_attempts += 1
            if self.call_attempts > 5:
                print "TOO MANY AUTH FAILURES"
                self.setState('ABORTED')
                return
            if message.code in ( 401, 407 ):
                if state in ( 'SENT_INVITE', ):
                    if message.code == 401:
                        inH, outH = 'www-authenticate', 'authorization'
                    else:
                        inH, outH = 'proxy-authenticate', 'proxy-authorization'
                    a = message.headers.get(inH)
                    if a:
                        uri = str(self.remote)

                        credDef = self.sip.app.authCred('INVITE', uri, 
                                        retry=(self.call_attempts > 1)).addErrback(log.err)
                        credDef.addCallback(lambda c, uri=uri, chal=a[0]:
                                        self.calcAuth('INVITE', 
                                                      uri=uri, 
                                                      authchal=chal, 
                                                      cred=c)
                            ).addCallback(lambda a, h=outH: 
                                        self.sendInvite(toAddr=None, 
                                                        auth=a, 
                                                        authhdr=h)
                            ).addErrback(log.err)
                    else:
                        # * is retarded. If you send in an incorrect digest auth,
                        # you just get back a 401/407, with no auth challenge.
                        # In this case, retry without an auth header to get another
                        # challenge.
                        if self.call_attempts > 1:
                            self.sendInvite()
                        else: 
                            print "401/407 and no auth header"
                else:
                    print "Unknown state '%s' for a 401/407"%(state)
            else:
                self.sip.app.debugMessage(message.toString())
                self.sip.app.endCall(self.cookie, 'Other end sent %s'%message.toString())
                self.sip._delCallObject(self.getCallID())
                self.sip.app.statusMessage("Call Failed: %s %s"%(message.code,
                                                             message.phrase))
        elif message.code - (message.code%100) == 500:
            self.sip.app.debugMessage(message.toString())
            self.sip.app.endCall(self.cookie, 'Other end sent %s'%message.toString())
            self.sip._delCallObject(self.getCallID())
            self.sip.app.statusMessage("Call Failed: %s %s"%(message.code,
                                                             message.phrase))
        elif message.code - (message.code%100) == 600:
            self.sip.app.debugMessage(message.toString())
            self.sip.app.endCall(self.cookie, 'Other end sent %s'%message.toString())
            self.sip._delCallObject(self.getCallID())
            self.sip.app.statusMessage("Call Failed: %s %s"%(message.code,
                                                             message.phrase))
        else:
            self.sip.app.debugMessage(message.toString())

class Registration(Call):
    "State machine for registering with a server."

    def __init__(self, phone, deferred):
        self.sip = phone
        self.compDef = deferred
        self.regServer = None
        self.regAOR = None
        self.state = 'NEW'
        self._needSTUN = False
        self.cseq = random.randint(1000,5000)
        self.nonce_count = 0
        self.cancel_trigger = None
        self.register_attempts = 0

    def startRegistration(self):
        import copy
        self.regServer = tpsip.parseURL(self.sip.app.getPref('register_uri'))
        self.regAOR = copy.copy(self.regServer)
        self.regAOR.username = self.sip.app.getPref('username')
        self.regURI = copy.copy(self.regServer)
        #self.regURI.port = None
        d = self.setupLocalSIP(self.regServer)
        d.addCallback(self.sendRegistration).addErrback(log.err)

    def sendRegistration(self, cb=None, auth=None, authhdr=None):
        username = self.sip.app.getPref('username')
        invite = tpsip.Request('REGISTER', str(self.regURI))
        # XXX refactor all the common headers and the like
        invite.addHeader('via', 'SIP/2.0/UDP %s:%s;rport'%
                                                self.getLocalSIPAddress())
        invite.addHeader('cseq', '%s REGISTER'%self.getCSeq(incr=1))
        invite.addHeader('to', '"%s" <%s>'%(username,str(self.regAOR)))
        invite.addHeader('from', '"%s" <%s>'%(username,str(self.regAOR)))
        state =  self.getState() 
        if state in ( 'NEW', 'SENT_REGISTER', 'REGISTERED' ):
            invite.addHeader('expires', 900)
        elif state in ( 'CANCEL_REGISTER' ):
            invite.addHeader('expires', 0)
        invite.addHeader('call-id', self.getCallID())
        if auth is not None:
            invite.addHeader(authhdr, auth)
        invite.addHeader('user-agent', 'Shtoom/%s'%shtoom.Version)
        lhost, lport = self.getLocalSIPAddress()
        invite.addHeader('contact', '<sip:%s@%s:%s>'%(
                                username, lhost, lport))
        invite.addHeader('content-length', '0')
        invite.creationFinished()
        try:
            self.sip.transport.write(invite.toString(), self.getRemoteSIPAddress())
        except (socket.error, socket.gaierror):
            e,v,t = sys.exc_info()
            self.compDef.errback(e(v))
            self.setState('ABORTED')
        else:
            if self.getState() in ( 'NEW', 'REGISTERED' ):
                self.setState('SENT_REGISTER')
        self.sip.app.debugMessage("register sent\n"+invite.toString())

    def sendAuthResponse(self, authhdr, auth):
        self.sendRegistration(auth=auth, authhdr=authhdr)

    def recvResponse(self, message):
        state = self.getState()
        if message.code in ( 401, 407 ):
            self.register_attempts += 1
            if self.register_attempts > 5:
                print "REGISTRATION FAILED"
                self.setState('FAILED')
                return
            if state in ( 'SENT_REGISTER', 'CANCEL_REGISTER' ):
                if message.code == 401:
                    inH, outH = 'www-authenticate', 'authorization'
                else:
                    inH, outH = 'proxy-authenticate', 'proxy-authorization'
                a = message.headers.get(inH)
                if a:
                    uri = str(self.regURI)
                    credDef = self.sip.app.authCred('REGISTER', uri, 
                                        retry=(self.register_attempts > 1)).addErrback(log.err)
                    credDef.addCallback(lambda c, uri=uri, chal=a[0]:
                                        self.calcAuth('REGISTER', 
                                                      uri=uri, 
                                                      authchal=chal, 
                                                      cred=c)
                            ).addCallback(lambda a, h=outH: 
                                        self.sendRegistration(auth=a, authhdr=h)
                            ).addErrback(log.err)

                else:
                    # * is retarded. If you send in an incorrect digest auth,
                    # you just get back a 401/407, with no auth challenge.
                    # In this case, retry without an auth header to get another
                    # challenge.
                    if self.register_attempts > 1:
                        self.sendRegistration()
                    elif state == 'CANCEL_REGISTER':
                        self.setState('UNREGISTERED')
                        d = self._cancelDef
                        del self._cancelDef
                        d.callback(self)
                    else:
                        self.sip.app.statusMessage("Registration: auth failed")
            else:
                print "Unknown registration state '%s' for a 401/407"%(state)
        elif message.code in ( 200, ):
            self.sip.app.statusMessage("Registration: OK")
            # Woo. registration succeeded.
            self.register_attempts = 0
            if state == 'SENT_REGISTER':
                self.setState('REGISTERED')
                reactor.callLater(840, self.sendRegistration)
                if 0 and self.cancel_trigger is None:
                    t = reactor.addSystemEventTrigger('before', 
                                                      'shutdown', 
                                                      self.cancelRegistration)
                    self.cancel_trigger = t
            elif state == 'CANCEL_REGISTER':
                self.setState('UNREGISTERED')
                d = self._cancelDef
                del self._cancelDef
                d.callback(self)
            else:
                print "Unknown state '%s' for a 200"%(state)
        elif message.code in ( 100, ):
            # Trying?!?
            pass
        else:
            if state == 'CANCEL_REGISTER':
                self.setState('UNREGISTERED')
                d = self._cancelDef
                del self._cancelDef
                d.callback(self)
            else:
                log.err("don't know about %s for registration"%(message.code))

    def cancelRegistration(self):
        # Cancel this outstanding registration. Should return a deferred 
        # to pause the shutdown until we're done.
        # Send a registration with expires:0
        d = defer.Deferred()
        self.setState('CANCEL_REGISTER')
        self.sendRegistration()
        self._cancelDef = d
        return d





class SipPhone(DatagramProtocol, object):
    '''A SIP phone.'''

    __implements__ = ISip,

    def __init__(self, app, *args, **kwargs):
        self.app = app
        self._calls = {}
        super(SipPhone, self, *args, **kwargs)

    def getCalls(self):
        return [c for c in self._calls.values() if not isinstance(c, Registration)]

    def getRegistrations(self):
        return [c for c in self._calls.values() if isinstance(c, Registration)]

    def register(self, removed=None):
        if removed:
            print "cancelled", removed
            self._delCallObject(removed.getCallID())
        if self.app.getPref('register_uri') is not None:
            existing = self.getRegistrations()
            if existing:
                for reg in existing:
                    print "removing", reg, existing
                    d = reg.cancelRegistration()
                    d.addCallbacks(self.register, log.err)
            else:
                print "no outstanding registrations, registering"
                d = defer.Deferred()
                r = Registration(self,d)
                r.startRegistration()
                return d

    def _newCallObject(self, deferred, to=None, callid=None):
        call = Call(self, deferred, uri=to, callid=callid)
        print "XXX", call, call.getState(), call.getCallID()
        if call.getState() != 'ABORTED':
            if call.getCallID():
                self._calls[call.getCallID()] = call
            return call

    def updateCallObject(self, call, callid):
        "Used when Call setup returns a deferred result (e.g. STUN)"
        self._calls[call.getCallID()] = call

    def _getCallObject(self, callid):
        if type(callid) is list:
            callid = callid[0]
        if callid.startswith('<') and callid.endswith('>'):
            callid = callid[1:-1]
        return self._calls.get(callid)

    def _delCallObject(self, callid):
        if type(callid) is list:
            callid = callid[0]
        if callid.startswith('<') and callid.endswith('>'):
            callid = callid[1:-1]
        del self._calls[callid]

    def placeCall(self, uri):
        """Place a call.

        uri should be a string, an address of the person we are calling,
        e.g. 'sip:foo@example.com'.

        Returns a Call object and a Deferred
        """
        self.app.debugMessage("placeCall starting")
        _d = defer.Deferred()
        try:
            call = self._newCallObject(_d, to=uri)
        except:
            e,v,t = sys.exc_info()
            print "call creation failed", v
            return defer.fail(v)
        print "call is", call
        if call is None:
            from shtoom.exceptions import CallFailed
            _d.errback(CallFailed)
            return _d
        invite = call.startOutboundCall(uri)
        return _d


    def startDTMF(self, digit):
        """Start sending DTMF digit 'digit'
        """
        self.app.debugMessage("startDTMF not implemented yet!")

    def stopDTMF(self):
        """Stop sending DTMF digit 'digit'
        """
        self.app.debugMessage("stopDTMF not implemented yet!")

    def datagramReceived(self, datagram, addr):
        self.app.debugMessage("Got a SIP packet from %s:%s"%(addr))
        mp = tpsip.MessagesParser(self.sipMessageReceived)
        self.app.debugMessage("SIP PACKET\n%s"%(datagram))
        mp.dataReceived(datagram)
        mp.dataDone()

    def sipMessageReceived(self, message):
        if hasattr(message, 'code'):
            message.response, message.request = True, False
        elif hasattr(message, 'method'):
            message.response, message.request = False, True
        else:
            raise ValueError("message is neither fish nor fowl %s"%(message))
        if message.response:
            self.app.debugMessage("got SIP response %s: %s"%(
                                        message.code, message.phrase))
            self.app.statusMessage("%s: %s"%(message.code,message.phrase))
            self.app.debugMessage("got SIP response\n %s"%( message.toString()))
        else:
            self.app.debugMessage("got SIP request %s: %s"%(
                                        message.method, message.uri))
            self.app.debugMessage("got SIP request\n %s"%( message.toString()))
        callid = message.headers['call-id']
        call = self._getCallObject(callid)
        if message.response and not call:
            self.app.debugMessage("SIP response refers to unknown call %s %r"%(
                                                    callid, self._calls.keys()))
            print "unknown", message.toString()
            return
        if message.request and message.method.lower() != 'invite' and not call:
            self.app.debugMessage("SIP request refers to unknown call %s %r"%(
                                                    callid, self._calls.keys()))
            # XXX In this case, send a 481!
            return
        if message.request:
            print "handling request", message.method
            if not call:
                # We must have received an INVITE here. Handle it, reply with
                # a 180 Ringing.
                callid = message.headers['call-id'][0]
                defsetup = defer.Deferred()
                call = self._newCallObject(defsetup, callid=callid)
                call.startInboundCall(message)
            else:
                if message.method == 'BYE':
                    # Aw. Other end goes away :-(
                    self.app.statusMessage("received BYE")
                    # Drop the call, send a 200.
                    call.recvBye(message)
                    self.app.endCall(call.cookie)
                    self._delCallObject(callid)
                elif message.method == 'INVITE':
                    # modify dialog
                    call.recvInvite(message)
                elif message.method == 'ACK':
                    call.recvAck(message)
                elif message.method == 'CANCEL':
                    call.recvCancel(message)

        elif message.response:
            print "handling response", message.code
            call.recvResponse(message)

www.java2java.com | Contact Us
Copyright 2009 - 12 Demo Source and Support. All rights reserved.
All other trademarks are property of their respective owners.