From 20672fca2fa5e267e51bb46b48bf10a40acb7baa Mon Sep 17 00:00:00 2001 From: "alex%croczilla.com" Date: Mon, 19 Dec 2005 16:35:29 +0000 Subject: [PATCH] Fix registrations, calls, ringer. git-svn-id: svn://10.0.0.236/branches/ZAP_20050610_BRANCH@186268 18797224-902f-48f8-a5cc-f745e15eee43 --- mozilla/zap/client/content/zap.js | 377 +++++++++++++++++++++++------- 1 file changed, 289 insertions(+), 88 deletions(-) diff --git a/mozilla/zap/client/content/zap.js b/mozilla/zap/client/content/zap.js index 1b07579cd45..8842e274605 100644 --- a/mozilla/zap/client/content/zap.js +++ b/mozilla/zap/client/content/zap.js @@ -47,7 +47,7 @@ Components.utils.importModule("rel:AsyncUtils.js"); //////////////////////////////////////////////////////////////////////// // globals -var wUserAgent = "zap/0.1.5"; // String to be sent in User-Agent +var wUserAgent = "zap/0.1.7"; // String to be sent in User-Agent // header for SIP requests var wNetUtils = Components.classes["@mozilla.org/zap/netutils;1"].getService(Components.interfaces.zapINetUtils); var wPromptService = Components.classes["@mozilla.org/embedcomp/prompt-service;1"].getService(Components.interfaces.nsIPromptService); @@ -235,6 +235,9 @@ Service.rdfLiteralAttrib("urn:mozilla:zap:use_stun_contact_address", "false"); // whether or not we should send OPTIONS requests to keep alive the connection: Service.rdfLiteralAttrib("urn:mozilla:zap:options_keep_alive", "false"); Service.rdfLiteralAttrib("urn:mozilla:zap:suggested_registration_interval", ""); +// whether or not the top (loose) route header should be removed from +// (non-dialog) requests: +Service.rdfLiteralAttrib("urn:mozilla:zap:elide_destination_route_header", "false"); Service.fun( function getStunServer() { @@ -251,9 +254,16 @@ Service.fun( Service.spec( function createFromDocument(doc) { + // Temporarily defer flushing until later, so that the container + // changes get flushed as well: + this.autoflush = false; this._Service_createFromDocument(doc); - // append to services container: + this.autoflush = true; + + // Append to services container: wServicesContainer.AppendElement(this.resource); + + this.flush(); }); //////////////////////////////////////////////////////////////////////// @@ -272,6 +282,14 @@ function getIdentity(resourceID) { return identity; } +function getIdentityByAOR(uri) { + for (var id in wIdentities) { + if (wIdentities[id].getAOR().equals(uri)) + return wIdentities[id]; + } + return null; +} + function initIdentities() { wPasswordManager = Components.classes["@mozilla.org/passwordmanager;1"]. createInstance(Components.interfaces.nsIPasswordManager); @@ -396,9 +414,16 @@ Identity.fun( Identity.spec( function createFromDocument(doc) { + // Temporarily defer flushing until later, so that the container + // changes get flushed as well: + this.autoflush = false; this._Identity_createFromDocument(doc); - // append to identities: + this.autoflush = true; + + // Append to identities: wIdentitiesContainer.AppendElement(this.resource); + + this.flush(); }); // zapISipCredentialsProvider methods: @@ -419,7 +444,7 @@ Identity.fun( // fill in username from identity database username.value = this["urn:mozilla:zap:username"]; if (!username.value) - username.value = this.getUserinfo(); + username.value = this.getUser(); } try { @@ -454,30 +479,35 @@ Identity.fun( if (saveCredentials) { // store credentials for future sessions: if (is_our_realm) { - if (username.value != this.getUserinfo()) { + if (username.value != this.getUser()) { this["urn:mozilla:zap:username"] = username.value; } } + try { + // addUser doesn't update when there already is an existing entry. + // -> try removing first + wPasswordManager.removeUser(realm, username.value); + } catch(e) {} wPasswordManager.addUser(realm, username.value, password.value); } return true; }); -// Return userinfo part of the AOR (or empty string if the AOR can't +// Return user part of the AOR (or empty string if the AOR can't // be parsed) Identity.fun( - function getUserinfo() { + function getUser() { // XXX these try/catch blocks will be redundant once we hook up // syntax checking in PersistentRDFObject try { - var userinfo = this.getAOR().QueryInterface(Components.interfaces.zapISipSIPURI).userinfo; + var user = this.getAOR().QueryInterface(Components.interfaces.zapISipSIPURI).user; } catch(e) { - this._dump("Can't determine userinfo from "+this["http://home.netscape.com/NC-rdf#Name"]); + this._dump("Can't determine user from "+this["http://home.netscape.com/NC-rdf#Name"]); return ""; } - return userinfo; + return user; }); // Return host part of the AOR (or empty string if the AOR can't be @@ -517,15 +547,6 @@ Identity.fun( return wSipStack.syntaxFactory.createAddress(this["urn:mozilla:zap:display_name"], this.getAOR()); }); -// Returns the userinfo part of our register contact address: -Identity.fun( - function getRegisterContactUserinfo() { - // we use our AOR (with '@' replaced by '_') to distinguish - // incoming calls: - var aor = this.getAOR().QueryInterface(Components.interfaces.zapISipSIPURI); - return aor.userinfo + "_" + aor.host; - }); - // this will be initially be set to our internal address and // asynchronously resolved to our public STUN address if our // associated service has urn:mozilla:zap:use_stun_contact_address set @@ -535,9 +556,26 @@ Identity.obj("hostAddress", null); // contact address used for registration Identity.fun( function getRegisterContactAddress() { - var addr = ""; - return wSipStack.syntaxFactory.deserializeAddress(addr); + // Synthesize from the 'From Address', adjusting host and port. + // XXX We DO want to keep the From address's display name, but what about + // uri parameters??? + var addr = this.getFromAddress(); + var uri = addr.uri.QueryInterface(Components.interfaces.zapISipSIPURI); + uri.host = this.hostAddress; + if (wSipStack.listeningPort == 5060) + uri.port = ""; + else + uri.port = wSipStack.listeningPort; + + // The zid will allow us to identify the identity for incoming + // calls. The hex encoding (which will be encoded again for + // transmission!) is necessary since uri parameters are not case + // sensitive, but rdf resource ids are... + // XXX this is only needed when there are several identities/aor. + // investigate whether this is a feasible use-case + // uri.setURIParameter("zid", octetToHex(this.resource.Value)); + + return addr; }); // Returns a structure {contact, routeset} with routing information @@ -556,6 +594,15 @@ Identity.fun( return routingInfo; }); +// Constructs request client flags based on this identity's service: +Identity.fun( + function getRCFlags() { + var flags = 0; + if (this.service["urn:mozilla:zap:elide_destination_route_header"]=="true") + flags |= Components.interfaces.zapISipUAStack.ELIDE_DESTINATION_ROUTE_HEADER; + return flags; + }); + Identity.fun( function _constructFallbackRoutingInfo() { return { @@ -631,9 +678,9 @@ function unregisterIdentity(resource) { function notifyServiceUpdated(service_resource) { var service_id = service_resource.Value; // reregister any identities that use this service: - for (var r in wRegistrations) { - if (wRegistrations[r].service.resource.Value == service_id) - registerIdentity(wRDF.GetResource(r)); + for (var identity in wIdentities) { + if (wIdentities[identity].service.resource.Value == service_id) + registerIdentity(wIdentities[identity].resource); } } @@ -641,6 +688,8 @@ function notifyServiceUpdated(service_resource) { var RegistrationGroup = makeClass("RegistrationGroup", ErrorReporter); +RegistrationGroup.obj("registrations", null); + // true if we are currently unregistering: RegistrationGroup.obj("unregistering", false); @@ -726,12 +775,13 @@ RegistrationGroup.fun( RegistrationGroup.fun( function getRoutingInfo() { + if (!this.registrations) return null; // find first active flow: for (var i=0,l=this.registrations.length; i"'); - contact.setParameter("flow-id", flowid); - - this.registrationRC.listener = this; - this.registrationRC.sendRequest(); - }); + contact.setParameter("flow-id", this.flowid); + }); Registration.fun( function unregister() { @@ -867,10 +955,7 @@ Registration.fun( this.refreshTimer.cancel(); delete this.refreshTimer; } - if (this.flow) { - this.flow.removeFlowMonitor(this); - delete this.flow; - } + this._unmonitorFlow(); if (this.registrationRC.listener) { // We are currently waiting for a registration to complete. @@ -886,6 +971,62 @@ Registration.fun( delete this.registrationRC; }); +// Helper to compare the first hop in the new route to the remote ip +// address. Returns 'true' if the first hop has changed. +function firstHopChanged(newRoute, oldRoute, currentFlow) { + if (!newRoute.length) return (oldRoute.length>0); + if (!oldRoute.length) return true; + //XXX this needs some more thought + // See http://www.croczilla.com/zap/codedocs/notes A. + // var hop1 = newRoute[0].uri.QueryInterface(Components.interfaces.zapISipSIPURI); + // if (hop1.host == currentFlow.remoteAddress && + // (hop1.port == "" || hop1.port == currentFlow.remotePort)) + // return false; // endpoint addresses are equal + var uri1 = newRoute[0].uri.QueryInterface(Components.interfaces.zapISipSIPURI); + var uri2 = oldRoute[0].uri.QueryInterface(Components.interfaces.zapISipSIPURI); + return !uri1.equals(uri2); +} + +// Update the current request route from the service route on the +// given registration response (which can be null - in which case the +// preloaded route will be restored). Returns 'true' if the first hop +// in the new route is different to the remote end of the flow over +// which the response was received. See http://www.croczilla.com/zap/codedocs/notes A. +Registration.fun( + function _updateRequestRoute(response, flow) { + var newRoute = null; + var serviceRouteHeaders = response.getHeaders("Service-Route", {}); + if (serviceRouteHeaders.length) { + newRoute = []; + serviceRouteHeaders.forEach( + function(h) { + newRoute.push(h.QueryInterface(Components.interfaces.zapISipServiceRouteHeader).address); + }); + } + else + newRoute = this.defaultOutboundRoute; + + var oldRoute = this.outboundRoute; + if (oldRoute != newRoute) { + this.outboundRoute = newRoute; + // Use the service route as a new route for *all* outbound + // request over this flow, including future REGISTERs + // (RFC3608, draft-rosenberg-sip-route-construct-00). + + // We need to create a new request client with the updated route: + var oldRequest = this.registrationRC.request; + this._initRC(); + var newRequest = this.registrationRC.request; + // copy credentials: + oldRequest.getHeaders("Authorization", {}).forEach( + function(h) {newRequest.appendHeader(h);}); + oldRequest.getHeaders("Proxy-Authorization", {}).forEach( + function(h) {newRequest.appendHeader(h);}); + } + + return firstHopChanged(newRoute, oldRoute, flow); + }); + // helper to extract a gruu from a contact header function extractGRUU(contact) { @@ -983,6 +1124,18 @@ Registration.fun( // update our state: this.contact = contact; this.gruu = extractGRUU(contact); + + var hopChanged = this._updateRequestRoute(response, flow); + + if (hopChanged) { + // The new service route has a first hop that is different + // to the one we were using. + // Send the request again using the updated route: + this.registrationRC.listener = this; + this.registrationRC.sendRequest(); + return; + } + if (!this.registered) { this.failureCount = 0; this.registered = true; @@ -991,10 +1144,7 @@ Registration.fun( } // set up a refresh timer: this.scheduleRefresh(expires*1000*0.9); - if (this.monitorType && !this.flow) { - this.flow = flow; - this.flow.addFlowMonitor(this, this.monitorType); - } + this._maybeMonitorFlow(flow); return; } } @@ -1004,10 +1154,8 @@ Registration.fun( this.contact = null; this.gruu = null; ++this.failureCount; - if (this.flow) { - this.flow.removeFlowMonitor(this); - delete this.flow; - } + this._unmonitorFlow(); + // let our registration group handle recovery: this.group.notifyRegistrationFailure(this); }); @@ -1043,8 +1191,7 @@ Registration.fun( function flowChanged(flow, changeFlags) { // stop monitoring the flow; recover as described in // draft-ietf-sip-outbound-01.txt - flow.removeFlowMonitor(this); - delete this.flow; + this._unmonitorFlow(); this.registered = false; // we leave recovery up to our group: ++this.failureCount; @@ -1085,28 +1232,40 @@ Contact.rdfLiteralAttrib("urn:mozilla:zap:isfriend", "true"); Contact.spec( function createFromDocument(doc) { + // Temporarily defer flushing until later, so that the container + // changes get flushed as well: + this.autoflush = false; this._Contact_createFromDocument(doc); - // append to contacts: + this.autoflush = true; + + // Append to contacts: wContactsContainer.AppendElement(this.resource); // ... and if it's a friend, to the friends list as well: if (this["urn:mozilla:zap:isfriend"] == "true") wFriendsContainer.AppendElement(this.resource); + + this.flush(); }); - Contact.spec( - function updateFromDocument(doc) { - this._Contact_updateFromDocument(doc); - // add/remove from friends container: - var index = wFriendsContainer.IndexOf(this.resource); - if (this["urn:mozilla:zap:isfriend"] == "true") { - if (index == -1) - wFriendsContainer.AppendElement(this.resource); - } - else { - if (index != -1) - wFriendsContainer.RemoveElementAt(index, true); - } - }); +Contact.spec( + function updateFromDocument(doc) { + // Temporarily defer flushing until later, so that the container + // changes get flushed as well: + this.autoflush = false; + this._Contact_updateFromDocument(doc); + this.autoflush = true; + // add/remove from friends container: + var index = wFriendsContainer.IndexOf(this.resource); + if (this["urn:mozilla:zap:isfriend"] == "true") { + if (index == -1) + wFriendsContainer.AppendElement(this.resource); + } + else { + if (index != -1) + wFriendsContainer.RemoveElementAt(index, true); + } + this.flush(); + }); //////////////////////////////////////////////////////////////////////// // Sidebar: @@ -1302,7 +1461,7 @@ var wSipTrafficLogger = { entry += remoteAddress+":" + remotePort + " " + protocol + " "; entry += data.length + "bytes " + (new Date()).toUTCString() + "\n"; if (wNetUtils.snoopStunPacket(data) != -2) { - entry += bin2hex(data, 16); + entry += octetToHex(data, " ", 16); // XXX log or not? return; } @@ -1435,7 +1594,6 @@ Call.rdfPointerAttrib("urn:mozilla:zap:root", "urn:mozilla:zap:current-call", "ephemeral"); - // Add this call to the recent calls container (so that it displays // in the call list). Maybe remove old items if the list is getting too large: Call.fun( @@ -1487,20 +1645,31 @@ ActiveCall.obj("dialog", null); ActiveCall.spec( function createFromDocument(doc) { + // Temporarily defer flushing until later, so that the container + // changes get flushed as well: + this.autoflush = false; this._ActiveCall_createFromDocument(doc); - // append to calls container: - wCallsContainer.AppendElement(this.resource); + this.autoflush = true; + // append to calls container: + wCallsContainer.AppendElement(this.resource); // ... and to recent calls as well: this.addToRecentCalls(); // enter into active calls hash: wActiveCalls[this.resource.Value] = this; + + this.flush(); }); ActiveCall.spec( function createNew(addAssertions) { + // Temporarily defer flushing until later, so that the container + // changes get flushed as well: + this.autoflush = false; this._ActiveCall_createNew(addAssertions); + this.autoflush = true; + // append to calls container: wCallsContainer.AppendElement(this.resource); @@ -1509,6 +1678,8 @@ ActiveCall.spec( // enter into active calls hash: wActiveCalls[this.resource.Value] = this; + + this.flush(); }); // clean up after call has terminated @@ -1548,7 +1719,8 @@ OutboundCall.fun( identity.getFromAddress(), this.routingInfo.contact, this.routingInfo.routeset, - this.routingInfo.routeset.length); + this.routingInfo.routeset.length, + identity.getRCFlags()); rc.listener = this.callHandler; this.callHandler.rc = rc; @@ -1582,6 +1754,11 @@ OutboundCallHandler.addInterfaces(Components.interfaces.zapISipInviteResponseHan // zapISipInviteResponseHandler methods: OutboundCallHandler.fun( function handle2XXResponse(rc, dialog, response, ackTemplate) { + // we have a positive answer for this request, so we won't + // resend. make sure that notifyTerminated really terminates from + // now on: + this.changeState("2XX_RECEIVED"); + if (this.call.dialog) { this._dump("received additional dialog for outbound call"); // we already have a dialog. this must be a subsequent dialog @@ -1597,7 +1774,7 @@ OutboundCallHandler.fun( sd = wSdpService.deserializeSessionDescription(response.body); } catch(e) { - this["urn:mozilla:zap:status"] = "Can't parse other party's session description"; + this.call["urn:mozilla:zap:status"] = "Can't parse other party's session description"; // send a BYE: // XXX need a way to send ACK first dialog.createNonInviteRequestClient("BYE").sendRequest(); @@ -1610,7 +1787,7 @@ OutboundCallHandler.fun( } catch(e) { // XXX add verbose error - this["urn:mozilla:zap:status"] = "Session negotiation failed"; + this.call["urn:mozilla:zap:status"] = "Session negotiation failed"; // send a BYE: // XXX need a way to send ACK first @@ -1624,6 +1801,7 @@ OutboundCallHandler.fun( this.call.dialog = dialog; this.call.dialog.listener = this.call; this.call["urn:mozilla:zap:session-running"] = "true"; + this.call["urn:mozilla:zap:status"] = "Connected"; return ackTemplate; }); @@ -1635,9 +1813,7 @@ OutboundCallHandler.fun( this.call["urn:mozilla:zap:status"] = response.statusCode + " " + response.reasonPhrase; else if (response.statusCode[0] == "2") { - this.call["urn:mozilla:zap:status"] = "Connected"; - this.changeState("2XX_RECEIVED"); - // we'll terminate from notifyTerminated now + // we handle 2XX codes in handle2XXResponse } else if (response.statusCode == "401" || response.statusCode == "407") { @@ -1714,8 +1890,17 @@ InboundCallHandler.fun( this.rs = rs; rs.listener = this; - // XXX work out identity based on To-address - this.identity = wCurrentIdentity; + // Work out identity based on To-address + this.identity = null; + try { + var uri = rs.request.getToHeader().address.uri.QueryInterface(Components.interfaces.zapISipSIPURI); + this.identity = getIdentityByAOR(uri); + } + catch(e) { /* To address probably wasn't a SIP URI -> fall through */ } + + if (!this.identity) + this.identity = wCurrentIdentity; // XXX maybe need a separate config for this? + this.contact = this.identity.getRoutingInfo().contact; // reject based on busy settings: @@ -1748,6 +1933,19 @@ InboundCallHandler.fun( // the offer is acceptable; we have an answer at hand. ring the user: this._respond("180"); + this.playRinger(); + }); + +InboundCallHandler.fun( + function playRinger() { + this.ringer = wMediaPipeline.mediagraph.addNode("rtttl-player", null); + wMediaPipeline.mediagraph.connect(this.ringer, null, + "aout", null); + }); + +InboundCallHandler.fun( + function stopRinger() { + if (this.ringer) wMediaPipeline.mediagraph.removeNode(this.ringer); }); InboundCallHandler.fun( @@ -1757,7 +1955,7 @@ InboundCallHandler.fun( // append session description: resp.setContent("application", "sdp", this.answer.serialize()); this.call["urn:mozilla:zap:status"] = resp.statusCode+" "+resp.reasonPhrase; - + this.stopRinger(); this.call.mediasession.startSession(); this.call.dialog = this.rs.sendResponse(resp); this.call.dialog.listener = this.call; @@ -1785,17 +1983,20 @@ InboundCallHandler.fun( } this.call["urn:mozilla:zap:status"] = resp.statusCode+" "+resp.reasonPhrase; + this.stopRinger(); return this.rs.sendResponse(resp); }); // zapISipInviteRSListener methods: InboundCallHandler.fun( function notifyCancelled(rs) { + this.stopRinger(); this.call["urn:mozilla:zap:status"] = "Missed"; }); InboundCallHandler.fun( function notifyACKReceived(rs, ack) { + this.stopRinger(); this.call["urn:mozilla:zap:status"] = "Confirmed"; });