class UA(object):
    global_config = None
    state = None
    event_cb = None
    uasReq = None
    uacResp = None
    username = None
    password = None
    auth_enalgs = None
    equeue = None
    dId = None
    credit_time = None
    credit_times = None
    credit_timer = None
    conn_cbs = None
    disc_cbs = None
    fail_cbs = None
    ring_cbs = None
    dead_cbs = None
    rCSeq = None
    lTag = None
    lUri = None
    rUri = None
    cId = None
    lCSeq = None
    lContact = None
    rAddr = None
    rAddr0 = None
    routes = None
    rTarget = None
    uasResp = None
    lSDP = None
    rSDP = None
    kaInterval = 0
    branch = None
    reqs = None
    extra_headers = None
    useRefer = True
    expire_time = None
    expire_timer = None
    no_progress_time = None
    no_progress_timer = None
    no_reply_time = None
    no_reply_timer = None
    on_local_sdp_change = None
    on_remote_sdp_change = None
    last_scode = 100
    setup_ts = None
    p100_ts = None
    p1xx_ts = None
    connect_ts = None
    disconnect_ts = None
    local_ua = None
    remote_ua = None
    elast_seq = None
    origin = None
    source_address = None
    outbound_proxy = None
    pass_auth = False
    pending_tr = None
    late_media = False
    godead_timeout = 32.0
    compact_sip = False
    uas_lossemul = 0

    def __init__(self, global_config, event_cb = None, username = None, password = None, nh_address = None, credit_time = None, \
      conn_cbs = None, disc_cbs = None, fail_cbs = None, ring_cbs = None, dead_cbs = None, ltag = None, extra_headers = None, \
      expire_time = None, no_progress_time = None):
        self.global_config = global_config
        self.event_cb = event_cb
        self.equeue = []
        self.username = username
        self.password = password
        self.rAddr = nh_address
        self.rAddr0 = self.rAddr
        self.credit_time = credit_time
        self.credit_times = {}
        if conn_cbs != None:
            self.conn_cbs = conn_cbs
        else:
            self.conn_cbs = ()
        if disc_cbs != None:
            self.disc_cbs = disc_cbs
        else:
            self.disc_cbs = ()
        if fail_cbs != None:
            self.fail_cbs = fail_cbs
        else:
            self.fail_cbs = ()
        if ring_cbs != None:
            self.ring_cbs = ring_cbs
        else:
            self.ring_cbs = ()
        if dead_cbs != None:
            self.dead_cbs = dead_cbs
        else:
            self.dead_cbs = ()
        if ltag != None:
            self.lTag = ltag
        else:
            salt = str((random() * 1000000000) + time())
            self.lTag = md5(salt.encode()).hexdigest()
        self.reqs = {}
        self.extra_headers = extra_headers
        self.expire_time = expire_time
        self.no_progress_time = no_progress_time
        #print self.username, self.password

    def recvRequest(self, req, sip_t):
        #print 'Received request %s in state %s instance %s' % (req.getMethod(), self.state, self)
        #print self.rCSeq, req.getHFBody('cseq').getCSeqNum()
        sip_t.compact = self.compact_sip
        if self.remote_ua == None:
            self.update_ua(req)
        if self.rCSeq != None and self.rCSeq >= req.getHFBody('cseq').getCSeqNum():
            return (req.genResponse(500, 'Server Internal Error', server = self.local_ua), None, None)
        self.rCSeq = req.getHFBody('cseq').getCSeqNum()
        if self.state == None:
            if req.getMethod() == 'INVITE':
                self.changeState((UasStateIdle,))
            else:
                return None
        newstate = self.state.recvRequest(req)
        if newstate != None:
            self.changeState(newstate)
        self.emitPendingEvents()
        if newstate != None and req.getMethod() == 'INVITE':
            return (None, self.state.cancel, self.disconnect)
        else:
            return None

    def processChallenge(self, resp, cseq, ch_hfname, auth_hfname):
        if self.username == None or self.password == None or \
          self.reqs[cseq].countHFs(auth_hfname) != 0:
            return False
        for challenge in resp.getHFBodys(ch_hfname):
            #print(self.processChallenge, cseq, challenge, challenge.algorithm)
            if self.auth_enalgs != None and challenge.algorithm not in self.auth_enalgs:
                continue
            supported, qop = challenge.supportedAlgorithm()
            if supported:
                break
        else:
            return False
        req = self.genRequest('INVITE', self.lSDP, (challenge, qop))
        self.lCSeq += 1
        self.tr = self.global_config['_sip_tm'].newTransaction(req, self.recvResponse, \
          laddress = self.source_address, cb_ifver = 2, compact = self.compact_sip)
        del self.reqs[cseq]
        return True

    def recvResponse(self, resp, tr):
        if self.state == None:
            return
        self.update_ua(resp)
        code, reason = resp.getSCode()
        cseq, method = resp.getHFBody('cseq').getCSeq()
        if method == 'INVITE' and not self.pass_auth and cseq in self.reqs:
            if code == 401 and self.processChallenge(resp, cseq, \
              'www-authenticate', 'authorization'):
                return None
            if code == 407 and self.processChallenge(resp, cseq, \
              'proxy-authenticate', 'proxy-authorization'):
                return None
        if code >= 200 and cseq in self.reqs:
            del self.reqs[cseq]
        newstate = self.state.recvResponse(resp, tr)
        if newstate != None:
            self.changeState(newstate)
        self.emitPendingEvents()

    def recvEvent(self, event):
        #print self, event
        if self.state == None:
            if isinstance(event, CCEventTry) or isinstance(event, CCEventFail) or isinstance(event, CCEventDisconnect):
                self.changeState((UacStateIdle,))
            else:
                return
        newstate = self.state.recvEvent(event)
        if newstate != None:
            self.changeState(newstate)
        self.emitPendingEvents()

    def disconnect(self, rtime = None):
        if rtime == None:
            rtime = MonoTime()
        self.equeue.append(CCEventDisconnect(rtime = rtime))
        self.recvEvent(CCEventDisconnect(rtime = rtime))

    def expires(self):
        self.expire_timer = None
        self.disconnect()

    def no_progress_expires(self):
        self.no_progress_timer = None
        self.disconnect()

    def no_reply_expires(self):
        self.no_reply_timer = None
        self.disconnect()

    def credit_expires(self, rtime):
        self.credit_timer = None
        self.disconnect(rtime)

    def changeState(self, newstate):
        if self.state != None:
            self.state.onStateChange(newstate[0])
        self.state = newstate[0](self)
        if len(newstate) > 1:
            for callback in newstate[1]:
                callback(self, *newstate[2:])

    def emitEvent(self, event):
        if self.event_cb != None:
            if self.elast_seq != None and self.elast_seq >= event.seq:
                #print 'ignoring out-of-order event', event, event.seq, self.elast_seq, self.cId
                return
            self.elast_seq = event.seq
            self.event_cb(event, self)

    def emitPendingEvents(self):
        while len(self.equeue) != 0 and self.event_cb != None:
            event = self.equeue.pop(0)
            if self.elast_seq != None and self.elast_seq >= event.seq:
                #print 'ignoring out-of-order event', event, event.seq, self.elast_seq, self.cId
                continue
            self.elast_seq = event.seq
            self.event_cb(event, self)

    def genRequest(self, method, body = None, cqop = None, \
      reason = None, max_forwards = None):
        if self.outbound_proxy != None:
            target = self.outbound_proxy
        else:
            target = self.rAddr
        if max_forwards != None:
            max_forwards_hf = SipMaxForwards(number = max_forwards)
        else:
            max_forwards_hf = None
        req = SipRequest(method = method, ruri = self.rTarget, to = self.rUri, fr0m = self.lUri,
                         cseq = self.lCSeq, callid = self.cId, contact = self.lContact,
                         routes = self.routes, target = target,
                         user_agent = self.local_ua, maxforwards = max_forwards_hf)
        if cqop != None:
            challenge, qop = cqop
            if body != None and qop == 'auth-int':
                sbody = str(body)
                #print(len(sbody), sbody)
            else:
                sbody = None
            auth = challenge.genAuthHF(self.username, self.password, method, \
              str(self.rTarget), sbody, qop)
            req.appendHeader(SipHeader(body = auth))
        if body != None:
            req.setBody(body)
        if self.extra_headers != None:
            req.appendHeaders(self.extra_headers)
        if reason != None:
            req.appendHeader(SipHeader(body = reason))
        self.reqs[self.lCSeq] = req
        return req

    def sendUasResponse(self, scode, reason, body = None, contacts = None, \
      reason_rfc3326 = None, extra_headers = None, ack_wait = False):
        uasResp = self.uasResp.getCopy()
        uasResp.setSCode(scode, reason)
        uasResp.setBody(body)
        if contacts != None:
            for contact in contacts:
                uasResp.appendHeader(SipHeader(name = 'contact', body = contact))
        if reason_rfc3326 != None:
            uasResp.appendHeader(SipHeader(body = reason_rfc3326))
        if extra_headers != None:
            uasResp.appendHeaders(extra_headers)
        if ack_wait:
            ack_cb = self.recvACK
        else:
            ack_cb = None
        self.global_config['_sip_tm'].sendResponse(uasResp, ack_cb = ack_cb, \
          lossemul = self.uas_lossemul)

    def recvACK(self, req):
        if not self.isConnected():
            return
        #print 'UA::recvACK', req
        newstate = self.state.recvACK(req)
        if newstate != None:
            self.changeState(newstate)
        self.emitPendingEvents()

    def isYours(self, req = None, call_id = None, from_tag = None, to_tag = None):
        #print self.branch, req.getHFBody('via').getBranch()
        if req != None:
            if req.getMethod() != 'BYE' and self.branch != None and \
              self.branch != req.getHFBody('via').getBranch():
                return None
            call_id = str(req.getHFBody('call-id'))
            from_tag = req.getHFBody('from').getTag()
            to_tag = req.getHFBody('to').getTag()
        #print str(self.cId), call_id
        if call_id != str(self.cId):
            return None
        #print self.rUri.getTag(), from_tag
        if self.rUri != None and self.rUri.getTag() != from_tag:
            return None
        #print self.lUri.getTag(), to_tag
        if self.lUri != None and self.lUri.getTag() != to_tag:
            return None
        return self

    def isDead(self):
        if self.state != None:
            return self.state.dead
        return False

    def isConnected(self):
        if self.state != None:
            return self.state.connected
        return False

    def getCLD(self):
        if self.rUri == None:
            return None
        return self.rUri.getUrl().username

    def getCLI(self):
        if self.lUri == None:
            return None
        return self.lUri.getUrl().username

    def getCallingName(self):
        if self.lUri == None:
            return None
        return self.lUri.getUri().name

    def getRAddr0(self):
        return self.rAddr0

    def getCID(self):
        # Return tuple containing call-id, local tag and remote tag
        rval = [str(self.cId), None, None]
        if self.lUri != None:
            rval[1] = self.lUri.getTag()
        if self.rUri != None:
            rval[2] = self.rUri.getTag()
        return tuple(rval)

    def delayed_remote_sdp_update(self, event, remote_sdp_body):
        self.rSDP = remote_sdp_body.getCopy()
        self.equeue.append(event)
        self.emitPendingEvents()

    def getAcct(self):
        if self.disconnect_ts != None:
            disconnect_ts = self.disconnect_ts
            disconnected = True
        else:
            disconnect_ts = MonoTime()
            disconnected = False
        if self.connect_ts != None:
            return (disconnect_ts - self.connect_ts, self.connect_ts - self.setup_ts, True, disconnected)
        return (0, disconnect_ts - self.setup_ts, False, disconnected)

    def update_ua(self, msg):
        if msg.countHFs('user-agent') > 0:
            self.remote_ua = msg.getHFBody('user-agent').name
        elif msg.countHFs('server') > 0:
            self.remote_ua = msg.getHFBody('server').name
        return

    def cancelCreditTimer(self):
        if self.credit_timer != None:
            self.credit_timer.cancel()
            self.credit_timer = None

    def startCreditTimer(self, rtime):
        if self.credit_time != None:
            self.credit_times[0] = rtime.getOffsetCopy(self.credit_time)
            self.credit_time = None
        try:
            credit_time = min([x for x in self.credit_times.values() if x != None])
        except ValueError:
            return
        self.credit_timer = TimeoutAbsMono(self.credit_expires, credit_time, credit_time)

    def resetCreditTime(self, rtime, new_credit_times):
        self.credit_times.update(new_credit_times)
        if self.state.connected:
            self.cancelCreditTimer()
            self.startCreditTimer(rtime)

    def cleanup(self):
        pass class CCEventDisconnect(CCEventGeneric):
    name = 'CCEventDisconnect'
    pass class CCEventTry(CCEventGeneric):
    name = 'CCEventTry'
    max_forwards = None

    def getCopy(self):
        cself = CCEventGeneric.getCopy(self)
        cself.max_forwards = self.max_forwards
        return cself

    # Speacial method allowing tweaking internal parameters of the UAC
    # after everything has been setup but before INVITE goes out.
    def onUacSetupComplete(self, uac):
        pass class CCEventRing(CCEventGeneric):
    name = 'CCEventRing'
    pass class CCEventConnect(CCEventGeneric):
    name = 'CCEventConnect'
    pass class CCEventPreConnect(CCEventGeneric):
    name = 'CCEventPreConnect'
    pass class SipTransactionManager(object):
    global_config = None
    l4r = None
    tclient = None
    tserver = None
    req_cb = None
    l1rcache = None
    l2rcache = None
    nat_traversal = False
    req_consumers = None
    provisional_retr = 0
    ploss_out_rate = 0.0
    pdelay_out_max = 0.0
    nworkers_udp = None

    def __init__(self, global_config, req_cb = None):
        self.global_config = global_config
        self.l4r = local4remote(global_config, self.handleIncoming, self.nworkers_udp)
        self.l4r.ploss_out_rate = self.ploss_out_rate
        self.l4r.pdelay_out_max = self.pdelay_out_max
        self.tclient = {}
        self.tserver = {}
        self.req_cb = req_cb
        self.l1rcache = {}
        self.l2rcache = {}
        self.req_consumers = {}
        Timeout(self.rCachePurge, 32, -1)

    def handleIncoming(self, data_in, address, server, rtime):
        if len(data_in) < 32:
            return
        self.global_config['_sip_logger'].write('RECEIVED message from %s:%d:\n' % address, \
          data_in.decode(errors = 'backslashreplace'), ltime = rtime.realt)
        data = data_in.decode()
        checksum = md5(data_in).digest()
        retrans = self.l1rcache.get(checksum, None)
        if retrans == None:
            retrans = self.l2rcache.get(checksum, None)
        if retrans != None:
            if retrans.data == None:
                return
            self.transmitData(retrans.userv, retrans.data, retrans.address, \
              lossemul = retrans.lossemul)
            return
        if data.startswith('SIP/2.0 '):
            try:
                resp = SipResponse(data)
                tid = resp.getTId(True, True)
            except Exception as exception:
                dump_exception('can\'t parse SIP response from %s:%d' % (address[0], address[1]), extra = data)
                self.l1rcache[checksum] = SipTMRetransmitO()
                return
            if resp.getSCode()[0] < 100 or resp.getSCode()[0] > 999:
                print(datetime.now(), 'invalid status code in SIP response from %s:%d:' % address)
                print(data)
                sys.stdout.flush()
                self.l1rcache[checksum] = SipTMRetransmitO()
                return
            resp.rtime = rtime
            if not tid in self.tclient:
                #print 'no transaction with tid of %s in progress' % str(tid)
                self.l1rcache[checksum] = SipTMRetransmitO()
                return
            t = self.tclient[tid]
            if self.nat_traversal and resp.countHFs('contact') > 0 and not check1918(t.address[0]):
                cbody = resp.getHFBody('contact')
                if not cbody.asterisk:
                    curl = cbody.getUrl()
                    if check1918(curl.host):
                        curl.host, curl.port = address
            resp.setSource(address)
            self.incomingResponse(resp, t, checksum)
        else:
            try:
                req = SipRequest(data)
                tids = req.getTIds()
            except Exception as exception:
                if isinstance(exception, ESipParseException) and exception.sip_response != None:
                    self.transmitMsg(server, exception.sip_response, address, checksum)
                dump_exception('can\'t parse SIP request from %s:%d' % (address[0], address[1]), extra = data)
                self.l1rcache[checksum] = SipTMRetransmitO()
                return
            req.rtime = rtime
            via0 = req.getHFBody('via')
            ahost, aport = via0.getAddr()
            rhost, rport = address
            if self.nat_traversal and rport != aport and check1918(ahost):
                req.nated = True
            if ahost != rhost:
                via0.params['received'] = rhost
            if 'rport' in via0.params or req.nated:
                via0.params['rport'] = str(rport)
            if self.nat_traversal and req.countHFs('contact') > 0 and req.countHFs('via') == 1:
                try:
                    cbody = req.getHFBody('contact')
                except Exception as exception:
                    dump_exception('can\'t parse SIP request from %s:%d: %s:' % (address[0], address[1]), extra = data)
                    self.l1rcache[checksum] = SipTMRetransmitO()
                    return
                if not cbody.asterisk:
                    curl = cbody.getUrl()
                    if check1918(curl.host) or curl.port == 0 or curl.host == '255.255.255.255':
                        curl.host, curl.port = address
                        req.nated = True
            req.setSource(address)
            self.incomingRequest(req, checksum, tids, server)

    # 1. Client transaction methods
    def newTransaction(self, msg, resp_cb = None, laddress = None, userv = None, \
      cb_ifver = 1, compact = False, t = None):
        if t == None:
            t = SipTransaction()
        t.rtime = MonoTime()
        t.compact = compact
        t.method = msg.getMethod()
        t.cb_ifver = cb_ifver
        t.tid = msg.getTId(True, True)
        if t.tid in self.tclient:
            raise ValueError('BUG: Attempt to initiate transaction with the same TID as existing one!!!')
        t.tout = 0.5
        t.fcode = None
        t.address = msg.getTarget()
        if userv == None:
            if laddress == None:
                t.userv = self.l4r.getServer(t.address)
            else:
                t.userv = self.l4r.getServer(laddress, is_local = True)
        else:
            t.userv = userv
        t.data = msg.localStr(*t.userv.uopts.getSIPaddr(), compact = t.compact)
        if t.method == 'INVITE':
            try:
                t.expires = msg.getHFBody('expires').getNum()
                if t.expires <= 0:
                    t.expires = 300
            except IndexError:
                t.expires = 300
            t.needack = True
            t.ack = msg.genACK()
            t.cancel = msg.genCANCEL()
        else:
            t.expires = 32
            t.needack = False
            t.ack = None
            t.cancel = None
        t.cancelPending = False
        t.resp_cb = resp_cb
        t.teA = Timeout(self.timerA, t.tout, 1, t)
        if resp_cb != None:
            t.r408 = msg.genResponse(408, 'Request Timeout')
        t.teB = Timeout(self.timerB, 32.0, 1, t)
        t.teC = None
        t.state = TRYING
        self.tclient[t.tid] = t
        self.transmitData(t.userv, t.data, t.address)
        if t.req_out_cb != None:
            t.req_out_cb(msg)
        return t

    def cancelTransaction(self, t, reason = None):
        # If we got at least one provisional reply then (state == RINGING)
        # then start CANCEL transaction, otherwise deffer it
        if t.state != RINGING:
            t.cancelPending = True
        else:
            if reason != None:
                t.cancel.appendHeader(SipHeader(body = reason))
            self.newTransaction(t.cancel, userv = t.userv)

    def incomingResponse(self, msg, t, checksum):
        # In those two states upper level already notified, only do ACK retransmit
        # if needed
        if t.state == TERMINATED:
            return

        if t.state == TRYING:
            # Stop timers
            if t.teA != None:
                t.teA.cancel()
                t.teA = None

        if t.state in (TRYING, RINGING):
            if t.teB != None:
                t.teB.cancel()
                t.teB = None

            if msg.getSCode()[0] < 200:
                # Privisional response - leave everything as is, except that
                # change state and reload timeout timer
                if t.state == TRYING:
                    t.state = RINGING
                    if t.cancelPending:
                        self.newTransaction(t.cancel, userv = t.userv)
                        t.cancelPending = False
                t.teB = Timeout(self.timerB, t.expires, 1, t)
                self.l1rcache[checksum] = SipTMRetransmitO()
                if t.resp_cb != None:
                    if t.cb_ifver == 1:
                        t.resp_cb(msg)
                    else:
                        t.resp_cb(msg, t)
            else:
                # Final response - notify upper layer and remove transaction
                if t.needack:
                    # Prepare and send ACK if necessary
                    fcode = msg.getSCode()[0]
                    tag = msg.getHFBody('to').getTag()
                    if tag != None:
                        t.ack.getHFBody('to').setTag(tag)
                    rAddr = None
                    if msg.getSCode()[0] >= 200 and msg.getSCode()[0] < 300:
                        # Some hairy code ahead
                        if msg.countHFs('contact') > 0:
                            rTarget = msg.getHFBody('contact').getUrl().getCopy()
                        else:
                            rTarget = None
                        routes = [x.getCopy() for x in msg.getHFBodys('record-route')]
                        routes.reverse()
                        if len(routes) > 0:
                            if not routes[0].getUrl().lr:
                                if rTarget != None:
                                    routes.append(SipRoute(address = SipAddress(url = rTarget)))
                                rTarget = routes.pop(0).getUrl()
                                rAddr = rTarget.getAddr()
                            else:
                                rAddr = routes[0].getAddr()
                        elif rTarget != None:
                            rAddr = rTarget.getAddr()
                        if rTarget != None:
                            t.ack.setRURI(rTarget)
                        if rAddr != None:
                            t.ack.setTarget(rAddr)
                        t.ack.delHFs('route')
                        t.ack.appendHeaders([SipHeader(name = 'route', body = x) for x in routes])
                    if fcode >= 200 and fcode < 300:
                        t.ack.getHFBody('via').genBranch()
                    if rAddr == None:
                        rAddr = t.address
                    if not t.uack:
                        self.transmitMsg(t.userv, t.ack, rAddr, checksum, t.compact)
                        if t.req_out_cb != None:
                            t.req_out_cb(t.ack)
                    else:
                        t.state = UACK
                        t.ack_rAddr = rAddr
                        t.ack_checksum = checksum
                        self.l1rcache[checksum] = SipTMRetransmitO()
                        t.teG = Timeout(self.timerG, 64, 1, t)
                else:
                    self.l1rcache[checksum] = SipTMRetransmitO()
                if t.resp_cb != None:
                    if t.cb_ifver == 1:
                        t.resp_cb(msg)
                    else:
                        t.resp_cb(msg, t)
                if t.state == UACK:
                    return
                del self.tclient[t.tid]
                t.cleanup()

    def timerA(self, t):
        #print 'timerA', t
        self.transmitData(t.userv, t.data, t.address)
        t.tout *= 2
        t.teA = Timeout(self.timerA, t.tout, 1, t)

    def timerB(self, t):
        #print 'timerB', t
        t.teB = None
        if t.teA != None:
            t.teA.cancel()
            t.teA = None
        t.state = TERMINATED
        #print '2: Timeout(self.timerC, 32.0, 1, t)', t
        t.teC = Timeout(self.timerC, 32.0, 1, t)
        if t.resp_cb == None:
            return
        t.r408.rtime = MonoTime()
        if t.cb_ifver == 1:
            t.resp_cb(t.r408)
        else:
            t.resp_cb(t.r408, t)
        #try:
        #    t.resp_cb(SipRequest(t.data).genResponse(408, 'Request Timeout'))
        #except:
        #    print 'SipTransactionManager: unhandled exception when processing response!'

    def timerC(self, t):
        #print 'timerC', t
        #print self.tclient
        t.teC = None
        del self.tclient[t.tid]
        t.cleanup()

    # 2. Server transaction methods
    def incomingRequest(self, msg, checksum, tids, server):
        for tid in tids:
            if tid in self.tclient:
                t = self.tclient[tid]
                resp = msg.genResponse(482, 'Loop Detected')
                self.transmitMsg(server, resp, resp.getHFBody('via').getTAddr(), checksum, \
                  t.compact)
                return
        if  msg.getMethod() != 'ACK':
            tid = msg.getTId(wBRN = True)
        else:
            tid = msg.getTId(wTTG = True)
        t = self.tserver.get(tid, None)
        if t != None:
            #print 'existing transaction'
            if msg.getMethod() == t.method:
                # Duplicate received, check that we have sent any response on this
                # request already
                if t.data != None:
                    self.transmitData(t.userv, t.data, t.address, checksum)
                return
            elif msg.getMethod() == 'CANCEL':
                # RFC3261 says that we have to reply 200 OK in all cases if
                # there is such transaction
                resp = msg.genResponse(200, 'OK')
                self.transmitMsg(t.userv, resp, resp.getHFBody('via').getTAddr(), checksum, \
                  t.compact)
                if t.state in (TRYING, RINGING):
                    self.doCancel(t, msg.rtime, msg)
            elif msg.getMethod() == 'ACK' and t.state == COMPLETED:
                t.state = CONFIRMED
                if t.teA != None:
                    t.teA.cancel()
                    t.teA = None
                t.teD.cancel()
                # We have done with the transaction, no need to wait for timeout
                del self.tserver[t.tid]
                if t.ack_cb != None:
                    t.ack_cb(msg)
                t.cleanup()
                self.l1rcache[checksum] = SipTMRetransmitO()
        elif msg.getMethod() == 'ACK':
            # Some ACK that doesn't match any existing transaction.
            # Drop and forget it - upper layer is unlikely to be interested
            # to seeing this anyway.
            print(datetime.now(), 'unmatched ACK transaction - ignoring')
            sys.stdout.flush()
            self.l1rcache[checksum] = SipTMRetransmitO()
        elif msg.getMethod() == 'CANCEL':
            resp = msg.genResponse(481, 'Call Leg/Transaction Does Not Exist')
            self.transmitMsg(server, resp, resp.getHFBody('via').getTAddr(), checksum)
        else:
            #print 'new transaction', msg.getMethod()
            t = SipTransaction()
            t.tid = tid
            t.state = TRYING
            t.teA = None
            t.teD = None
            t.teE = None
            t.teF = None
            t.teG = None
            t.method = msg.getMethod()
            t.rtime = msg.rtime
            t.data = None
            t.address = None
            t.noack_cb = None
            t.ack_cb = None
            t.cancel_cb = None
            t.checksum = checksum
            if not server.uopts.isWildCard():
                t.userv = server
            else:
                # For messages received on the wildcard interface find
                # or create more specific server.
                t.userv = self.l4r.getServer(msg.getSource())
            if msg.getMethod() == 'INVITE':
                t.r487 = msg.genResponse(487, 'Request Terminated')
                t.needack = True
                t.branch = msg.getHFBody('via').getBranch()
                try:
                    e = msg.getHFBody('expires').getNum()
                    if e <= 0:
                        e = 300
                except IndexError:
                    e = 300
                t.teE = Timeout(self.timerE, e, 1, t)
            else:
                t.r487 = None
                t.needack = False
                t.branch = None
            self.tserver[t.tid] = t
            for consumer in self.req_consumers.get(t.tid[0], ()):
                cobj = consumer.cobj.isYours(msg)
                if cobj != None:
                    t.compact = consumer.compact
                    rval = cobj.recvRequest(msg, t)
                    break
            else:
                if self.req_cb == None:
                    self.l1rcache[checksum] = SipTMRetransmitO()
                    return
                rval = self.req_cb(msg, t)
            if rval == None:
                if t.teA != None or t.teD != None or t.teE != None or t.teF != None:
                    return
                if t.tid in self.tserver:
                    del self.tserver[t.tid]
                t.cleanup()
                return
            resp, t.cancel_cb, t.noack_cb = rval
            if resp != None:
                self.sendResponse(resp, t, lossemul = resp.lossemul)

    def regConsumer(self, consumer, call_id, compact = False):
        cons = SipTransactionConsumer(consumer, compact)
        self.req_consumers.setdefault(call_id, []).append(cons)

    def unregConsumer(self, consumer, call_id):
        # Usually there will be only one consumer per call_id, so that
        # optimize management for this case
        consumers = self.req_consumers.pop(call_id)
        for cons in consumers:
            if cons.cobj != consumer:
                continue
            consumers.remove(cons)
            cons.cleanup()
            if len(consumers) > 0:
                self.req_consumers[call_id] = consumers
            break
        else:
            self.req_consumers[call_id] = consumers
            raise IndexError('unregConsumer: consumer %s for call-id %s is not registered' % \
              (str(consumer), call_id))

    def sendResponse(self, resp, t = None, retrans = False, ack_cb = None,
      lossemul = 0):
        #print self.tserver
        if t == None:
            tid = resp.getTId(wBRN = True)
            t = self.tserver[tid]
        if t.state not in (TRYING, RINGING) and not retrans:
            raise ValueError('BUG: attempt to send reply on already finished transaction!!!')
        scode = resp.getSCode()[0]
        toHF = resp.getHFBody('to')
        if scode > 100 and toHF.getTag() == None:
            toHF.genTag()
        t.data = resp.localStr(*t.userv.uopts.getSIPaddr(), compact = t.compact)
        t.address = resp.getHFBody('via').getTAddr()
        self.transmitData(t.userv, t.data, t.address, t.checksum, lossemul)
        if t.res_out_cb != None:
            t.res_out_cb(resp)
        if scode < 200:
            t.state = RINGING
            if self.provisional_retr > 0 and scode > 100:
                if t.teF != None:
                    t.teF.cancel()
                t.teF = Timeout(self.timerF, self.provisional_retr, 1, t)
        else:
            t.state = COMPLETED
            if t.teE != None:
                t.teE.cancel()
                t.teE = None
            if t.teF != None:
                t.teF.cancel()
                t.teF = None
            if t.needack:
                # Schedule removal of the transaction
                t.ack_cb = ack_cb
                t.teD = Timeout(self.timerD, 32.0, 1, t)
                if scode >= 200:
                    # Black magick to allow proxy send us another INVITE
                    # same branch and From tag. Use To tag to match
                    # ACK transaction after this point. Branch tag in ACK
                    # could differ as well.
                    del self.tserver[t.tid]
                    t.tid = list(t.tid[:-1])
                    t.tid.append(resp.getHFBody('to').getTag())
                    t.tid = tuple(t.tid)
                    self.tserver[t.tid] = t
                # Install retransmit timer if necessary
                t.tout = 0.5
                t.teA = Timeout(self.timerA, t.tout, 1, t)
            else:
                # We have done with the transaction
                del self.tserver[t.tid]
                t.cleanup()

    def doCancel(self, t, rtime = None, req = None):
        if rtime == None:
            rtime = MonoTime()
        if t.r487 != None:
            self.sendResponse(t.r487, t, True)
        if t.cancel_cb != None:
            t.cancel_cb(rtime, req)

    def timerD(self, t):
        #print 'timerD'
        t.teD = None
        if t.teA != None:
            t.teA.cancel()
            t.teA = None
        if t.noack_cb != None and t.state != CONFIRMED:
            t.noack_cb()
        del self.tserver[t.tid]
        t.cleanup()

    def timerE(self, t):
        #print 'timerE'
        t.teE = None
        if t.teF != None:
            t.teF.cancel()
            t.teF = None
        if t.state in (TRYING, RINGING):
            if t.r487 != None:
                t.r487.reason = 'Request Expired'
            self.doCancel(t)

    # Timer to retransmit the last provisional reply every
    # 2 seconds
    def timerF(self, t):
        #print 'timerF', t.state
        t.teF = None
        if t.state == RINGING and self.provisional_retr > 0:
            self.transmitData(t.userv, t.data, t.address)
            t.teF = Timeout(self.timerF, self.provisional_retr, 1, t)

    def timerG(self, t):
        #print 'timerG', t.state
        t.teG = None
        if t.state == UACK:
            print(datetime.now(), 'INVITE transaction stuck in the UACK state, possible UAC bug')

    def rCachePurge(self):
        self.l2rcache = self.l1rcache
        self.l1rcache = {}
        self.l4r.rotateCache()

    def transmitMsg(self, userv, msg, address, cachesum, compact = False):
        data = msg.localStr(*userv.uopts.getSIPaddr(), compact = compact)
        self.transmitData(userv, data, address, cachesum)

    def transmitData(self, userv, data, address, cachesum = None, \
      lossemul = 0):
        if lossemul == 0:
            userv.send_to(data, address)
            logop = 'SENDING'
        else:
            logop = 'DISCARDING'
        self.global_config['_sip_logger'].write('%s message to %s:%d:\n' % \
          (logop, address[0], address[1]), data)
        if cachesum != None:
            if lossemul > 0:
                lossemul -= 1
            self.l1rcache[cachesum] = SipTMRetransmitO(userv, data, address, \
              None, lossemul)

    def sendACK(self, t):
        #print 'sendACK', t.state
        if t.teG != None:
            t.teG.cancel()
            t.teG = None
        self.transmitMsg(t.userv, t.ack, t.ack_rAddr, t.ack_checksum, t.compact)
        if t.req_out_cb != None:
            t.req_out_cb(t.ack)
        del self.tclient[t.tid]
        t.cleanup() class SipCiscoGUID(SipGenericHF):
    hf_names = ('cisco-guid', 'h323-conf-id')
    ciscoGUID = None

    def __init__(self, body = None, ciscoGUID = None):
        SipGenericHF.__init__(self, body)
        if body != None:
            return
        self.parsed = True
        if ciscoGUID != None:
            self.ciscoGUID = ciscoGUID
        else:
            salt = str((random() * 1000000000) + time())
            s = md5(salt.encode()).hexdigest()
            self.ciscoGUID = (int(s[0:8], 16), int(s[8:16], 16), int(s[16:24], 16), int(s[24:32], 16))

    def parse(self):
        self.ciscoGUID = tuple([int(x) for x in  self.body.split('-', 3)])
        self.parsed = True

    def __str__(self):
        if not self.parsed:
            return self.body
        return '%d-%d-%d-%d' % self.ciscoGUID

    def getCiscoGUID(self):
        return self.ciscoGUID

    def hexForm(self):
        return '%.8X %.8X %.8X %.8X' % self.ciscoGUID

    def getCanName(self, name, compact = False):
        if name.lower() == 'h323-conf-id':
            return 'h323-conf-id'
        else:
            return 'cisco-GUID'

    def getCopy(self):
        if not self.parsed:
            return SipCiscoGUID(self.body)
        return SipCiscoGUID(ciscoGUID = self.ciscoGUID) class SipCallId(SipGenericHF):
    hf_names = ('call-id', 'i')
    body = None

    def __init__(self, body = None):
        SipGenericHF.__init__(self, body)
        self.parsed = True
        if body == None:
            salt = str((random() * 1000000000) + time())
            self.body = md5(salt.encode()).hexdigest() + '@' + str(SipConf.my_address)

    def __add__(self, other):
        return SipCallId(self.body + str(other))

    def genCallId(self):
        salt = str((random() * 1000000000) + time())
        self.body = md5(salt.encode()).hexdigest() + '@' + str(SipConf.my_address)

    def getCanName(self, name, compact = False):
        if compact:
            return 'i'
        return 'Call-ID' class MsgBody(object):
    content = None
    mtype = None
    needs_update = True
    parsed = False

    def __init__(self, content = None, mtype = 'application/sdp', cself = None):
        if content != None:
            self.mtype = mtype
            self.content = content
        else:
            if type(cself.content) in str_types:
                self.content = cself.content
            else:
                self.content = cself.content.getCopy()
            self.mtype = cself.mtype
            self.parsed = True

    def parse(self):
        if not self.parsed:
            if self.mtype in b_types:
                self.content = b_types[self.mtype](self.content)
            self.parsed = True

    def __str__(self):
        return str(self.content)

    def localStr(self, local_addr = None, local_port = None):
        if type(self.content) in str_types:
            return self.content
        return self.content.localStr(local_addr, local_port)

    def getType(self):
        return self.mtype

    def getCopy(self):
        if not self.parsed:
            return MsgBody(self.content)
        return MsgBody(cself = self) class SdpOrigin(object):
    _session_id = int(random() * time() * 1000.0)
    username = None
    session_id = None
    version = None
    network_type = None
    address_type = None
    address = None
    session_id = None

    def __init__(self, body = None, cself = None):
        if body != None:
            self.username, self.session_id, self.version, self.network_type, self.address_type, self.address = body.split()
        elif cself == None:
            self.username = '-'
            self.session_id = SdpOrigin._session_id
            SdpOrigin._session_id += 1
            self.version = self.session_id
            self.network_type = 'IN'
            self.address_type = 'IP4'
            self.address = SipConf.my_address
        else:
            self.username = cself.username
            self.session_id = cself.session_id
            self.version = cself.version
            self.network_type = cself.network_type
            self.address_type = cself.address_type
            self.address = cself.address

    def __str__(self):
        return '%s %s %s %s %s %s' % (self.username, self.session_id, self.version, self.network_type, self.address_type, self.address)

    def localStr(self, local_addr = None, local_port = None):
        if local_addr != None and 'my' in dir(self.address):
            if local_addr.startswith('['):
                address_type = 'IP6'
                local_addr = local_addr[1:-1]
            else:
                address_type = 'IP4'
            return '%s %s %s %s %s %s' % (self.username, self.session_id, self.version, self.network_type, address_type, local_addr)
        return '%s %s %s %s %s %s' % (self.username, self.session_id, self.version, self.network_type, self.address_type, self.address)

    def getCopy(self):
        return self.__class__(cself = self) class Udp_server(object):
    skt = None
    uopts = None
    sendqueue = None
    stats = None
    wi_available = None
    wi = None
    asenders = None
    areceivers = None

    def __init__(self, global_config, uopts):
        self.uopts = uopts.getCopy()
        self.skt = socket.socket(self.uopts.family, socket.SOCK_DGRAM)
        if self.uopts.laddress != None:
            ai = socket.getaddrinfo(self.uopts.laddress[0], None, self.uopts.family)
            if self.uopts.family == socket.AF_INET:
                address = (ai[0][4][0], self.uopts.laddress[1])
            else:
                address = (ai[0][4][0], self.uopts.laddress[1], ai[0][4][2], ai[0][4][3])
            if (self.uopts.flags & socket.SO_REUSEADDR) != 0:
                self.skt.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1)
            if hasattr(socket, 'SO_REUSEPORT') and \
              (self.uopts.flags & socket.SO_REUSEPORT) != 0:
                self.skt.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEPORT, 1)
            self.skt.bind(address)
            if self.uopts.laddress[1] == 0:
                self.uopts.laddress = self.skt.getsockname()
        self.sendqueue = []
        self.stats = [0, 0, 0]
        self.wi_available = Condition()
        self.wi = []
        self.asenders = []
        self.areceivers = []
        if self.uopts.nworkers == None:
            nworkers = _DEFAULT_NWORKERS
        else:
            nworkers = self.uopts.nworkers
        for i in range(0, nworkers):
            self.asenders.append(AsyncSender(self))
            self.areceivers.append(AsyncReceiver(self))

    def send_to(self, data, address, delayed = False):
        if not isinstance(address, tuple):
            raise Exception('Invalid address, not a tuple: %s' % str(address))
        if not isinstance(data, bytes):
            data = data.encode('utf-8')
        if self.uopts.ploss_out_rate > 0.0 and not delayed:
            if random() < self.uopts.ploss_out_rate:
                return
        if self.uopts.pdelay_out_max > 0.0 and not delayed:
            pdelay = self.uopts.pdelay_out_max * random()
            Timeout(self.send_to, pdelay, 1, data, address, True)
            return
        addr, port = address
        if self.uopts.family == socket.AF_INET6:
            if not addr.startswith('['):
                raise Exception('Invalid IPv6 address: %s' % addr)
            address = (addr[1:-1], port)
        self.wi_available.acquire()
        self.wi.append((data, address))
        self.wi_available.notify()
        self.wi_available.release()
 
    def handle_read(self, data, address, rtime, delayed = False):
        if len(data) > 0 and self.uopts.data_callback != None:
            self.stats[2] += 1
            if self.uopts.ploss_in_rate > 0.0 and not delayed:
                if random() < self.uopts.ploss_in_rate:
                    return
            if self.uopts.pdelay_in_max > 0.0 and not delayed:
                pdelay = self.uopts.pdelay_in_max * random()
                Timeout(self.handle_read, pdelay, 1, data, address, rtime.getOffsetCopy(pdelay), True)
                return
            try:
                self.uopts.data_callback(data, address, self, rtime)
            except Exception as ex:
                if isinstance(ex, SystemExit):
                    raise 
                dump_exception('Udp_server: unhandled exception when processing incoming data')

    def shutdown(self):
        try:
            self.skt.shutdown(socket.SHUT_RDWR)
        except:
            pass
        self.wi_available.acquire()
        self.wi.append(None)
        self.wi_available.notify()
        self.wi_available.release()
        self.uopts.data_callback = None
        for worker in self.asenders + self.areceivers:
            worker.join()
        self.asenders = None
        self.areceivers = None class Udp_server_opts(object):
    laddress = None
    data_callback = None
    family = None
    flags = _DEFAULT_FLAGS
    nworkers = None
    ploss_out_rate = 0.0
    pdelay_out_max = 0.0
    ploss_in_rate = 0.0
    pdelay_in_max = 0.0

    def __init__(self, laddress, data_callback, family = None, o = None):
        if o == None:
            if family == None:
                if laddress != None and laddress[0].startswith('['):
                    family = socket.AF_INET6
                    laddress = (laddress[0][1:-1], laddress[1])
                else:
                    family = socket.AF_INET
            self.family = family
            self.laddress = laddress
            self.data_callback = data_callback
        else:
            self.laddress, self.data_callback, self.family, self.nworkers, self.flags, \
              self.ploss_out_rate, self.pdelay_out_max, self.ploss_in_rate, \
              self.pdelay_in_max = o.laddress, o.data_callback, o.family, \
              o.nworkers, o.flags, o.ploss_out_rate, o.pdelay_out_max, o.ploss_in_rate, \
              o.pdelay_in_max

    def getCopy(self):
        return self.__class__(None, None, o = self)

    def getSIPaddr(self):
        if self.family == socket.AF_INET:
            return self.laddress
        return (('[%s]' % self.laddress[0], self.laddress[1]))

    def isWildCard(self):
        if (self.family, self.laddress[0]) in ((socket.AF_INET, '0.0.0.0'), \
          (socket.AF_INET6, '::')):
            return True
        return False
from sippy.UA import UA
from sippy.CCEvents import CCEventDisconnect, CCEventTry
from sippy.CCEvents import CCEventRing, CCEventConnect, CCEventPreConnect
from sippy.SipTransactionManager import SipTransactionManager
from sippy.SipCiscoGUID import SipCiscoGUID
from sippy.SipCallId import SipCallId
from sippy.MsgBody import MsgBody
from sippy.SdpOrigin import SdpOrigin
from sippy.Udp_server import Udp_server, Udp_server_opts
from sippy.misc import local4remote
from RTPGen import RTPGen
# Copyright (c) 2018 Sippy Software, Inc. All rights reserved.
#
# All rights reserved.
#
# Redistribution and use in source and binary forms, with or without modification,
# are permitted provided that the following conditions are met:
#
# 1. Redistributions of source code must retain the above copyright notice, this
# list of conditions and the following disclaimer.
#
# 2. Redistributions in binary form must reproduce the above copyright notice,
# this list of conditions and the following disclaimer in the documentation and/or
# other materials provided with the distribution.
#
# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND
# ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
# WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
# DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR
# ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES
# (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;
# LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON
# ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
# (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
# SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.




body_txt = 'v=0\r\n' + \
  'o=- 380960 380960 IN IP4 192.168.22.95\r\n' + \
  's=-\r\n' + \
  'c=IN IP4 192.168.22.95\r\n' + \
  't=0 0\r\n' + \
  'm=audio 16474 RTP/AVP 0\r\n' + \
  'a=rtpmap:0 PCMU/8000\r\n' + \
  'a=ptime:30\r\n' + \
  'a=sendrecv\r\n' + \
  '\r\n'

class IoTUAC(object):
    global_config = None
    ua = None
    cli = 'pel150_uac'
    cld = 'pel150_uas'
    authname = None
    authpass = None
    body = None
    rgen = None
    rserv = None

    def __init__(self, global_config):
        self.global_config = global_config
        SipTransactionManager.nworkers_udp = 1
        global_config['_sip_tm'] = SipTransactionManager(global_config)
        self.body = MsgBody(body_txt)
        self.body.parse()

    def sess_started(self):
        print('started')
        self.ua = UA(self.global_config, event_cb = self.recvEvent, \
          nh_address = tuple(self.global_config['nh_addr']), disc_cbs = (self.sess_term,))
        self.ua.username = self.authname
        self.ua.password = self.authpass
        rtp_laddr = local4remote(self.global_config['nh_addr'][0])
        rserv_opts = Udp_server_opts((rtp_laddr, 0), self.rtp_received)
        rserv_opts.nworkers = 1
        self.rserv = Udp_server({}, rserv_opts)
        sect = self.body.content.sections[0]
        sect.c_header.addr = self.rserv.uopts.laddress[0]
        sect.m_header.port = self.rserv.uopts.laddress[1]
        self.body.content.o_header = SdpOrigin()
        event = CCEventTry((SipCallId(), SipCiscoGUID(), self.cli, self.cld, self.body, \
          None, 'PEL 150-2'))
        self.rgen = RTPGen()
        self.ua.recvEvent(event)
        return (self.rgen.enqueue)

    def sess_term(self, ua, rtime, origin, result = 0):
        print('disconnected', origin)
        if origin == 'IoTUAC':
            return
        self.rgen.suspend()
        self.ua = UA(self.global_config, event_cb = self.recvEvent, \
          nh_address = tuple(self.global_config['nh_addr']), disc_cbs = (self.sess_term,))
        self.ua.username = self.authname
        self.ua.password = self.authpass
        self.body.content.o_header = SdpOrigin()

        event = CCEventTry((SipCallId(), SipCiscoGUID(), self.cli, self.cld, self.body, \