First commit of my XMPP client, as demonstrated at the meeting today, to weave/modules. This does not include the test-synchronization stuff. xmppClient.js is the main client class; transportLayer.js and sasl.js (which does authentication) are the helper classes.

This commit is contained in:
jonathandicarlo@jonathan-dicarlos-macbook-pro.local 2008-04-30 16:27:32 -07:00
Родитель ebed002936
Коммит a7ee9d6de3
3 изменённых файлов: 1110 добавлений и 0 удалений

Просмотреть файл

@ -0,0 +1,370 @@
if(typeof(atob) == 'undefined') {
// This code was written by Tyler Akins and has been placed in the
// public domain. It would be nice if you left this header intact.
// Base64 code from Tyler Akins -- http://rumkin.com
var keyStr = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/=";
function btoa(input) {
var output = "";
var chr1, chr2, chr3;
var enc1, enc2, enc3, enc4;
var i = 0;
do {
chr1 = input.charCodeAt(i++);
chr2 = input.charCodeAt(i++);
chr3 = input.charCodeAt(i++);
enc1 = chr1 >> 2;
enc2 = ((chr1 & 3) << 4) | (chr2 >> 4);
enc3 = ((chr2 & 15) << 2) | (chr3 >> 6);
enc4 = chr3 & 63;
if (isNaN(chr2)) {
enc3 = enc4 = 64;
} else if (isNaN(chr3)) {
enc4 = 64;
}
output = output + keyStr.charAt(enc1) + keyStr.charAt(enc2) +
keyStr.charAt(enc3) + keyStr.charAt(enc4);
} while (i < input.length);
return output;
}
function atob(input) {
var output = "";
var chr1, chr2, chr3;
var enc1, enc2, enc3, enc4;
var i = 0;
// remove all characters that are not A-Z, a-z, 0-9, +, /, or =
input = input.replace(/[^A-Za-z0-9\+\/\=]/g, "");
do {
enc1 = keyStr.indexOf(input.charAt(i++));
enc2 = keyStr.indexOf(input.charAt(i++));
enc3 = keyStr.indexOf(input.charAt(i++));
enc4 = keyStr.indexOf(input.charAt(i++));
chr1 = (enc1 << 2) | (enc2 >> 4);
chr2 = ((enc2 & 15) << 4) | (enc3 >> 2);
chr3 = ((enc3 & 3) << 6) | enc4;
output = output + String.fromCharCode(chr1);
if (enc3 != 64) {
output = output + String.fromCharCode(chr2);
}
if (enc4 != 64) {
output = output + String.fromCharCode(chr3);
}
} while (i < input.length);
return output;
}
}
/* Two implementations of SASL authentication:
one using MD5-DIGEST, the other using PLAIN.
Here's the interface that each implementation must obey:
initialize( clientName, clientRealm, clientPassword );
generateResponse( rootElem );
getError();
returns text of error message
*/
function BaseAuthenticator() {
}
BaseAuthenticator.prototype = {
COMPLETION_CODE: "success!",
initialize: function( userName, realm, password ) {
this._name = userName;
this._realm = realm;
this._password = password;
this._stepNumber = 0;
this._errorMsg = "";
},
getError: function () {
/* Returns text of most recent error message.
Client code should call this if generateResponse() returns false
to see what the problem was. */
return this._errorMsg;
},
generateResponse: function( rootElem ) {
/* Subclasses must override this. rootElem is a DOM node which is
the root element of the XML the server has sent to us as part
of the authentication protocol. return value: the string that
should be sent back to the server in response. 'false' if
there's a failure, or COMPLETION_CODE if nothing else needs to
be sent because authentication is a success. */
this._errorMsg = "generateResponse() should be overridden by subclass.";
return false;
},
verifyProtocolSupport: function( rootElem, protocolName ) {
/* Parses the incoming stream from the server to check whether the
server supports the type of authentication we want to do
(specified in the protocolName argument).
Returns false if there is any problem.
*/
if ( rootElem.nodeName != "stream:stream" ) {
this._errorMsg = "Expected stream:stream but got " + rootElem.nodeName;
return false;
}
dump( "Got response from server...\n" );
dump( "ID is " + rootElem.getAttribute( "id" ) + "\n" );
// TODO: Do I need to do anything with this ID value???
dump( "From: " + rootElem.getAttribute( "from" ) + "\n" );
if (rootElem.childNodes.length == 0) {
// No child nodes is unexpected, but allowed by the protocol.
// this shouldn't be an error.
this._errorMsg = "Expected child nodes but got none.";
return false;
}
var child = rootElem.childNodes[0];
if (child.nodeName == "stream:error" ) {
this._errorMsg = this.parseError( child );
return false;
}
if ( child.nodeName != "stream:features" ) {
this._errorMsg = "Expected stream:features but got " + child.nodeName;
return false;
}
var protocolSupported = false;
var mechanisms = child.getElementsByTagName( "mechanism" );
for ( var x = 0; x < mechanisms.length; x++ ) {
if ( mechanisms[x].firstChild.nodeValue == protocolName ) {
protocolSupported = true;
}
}
if ( !protocolSupported ) {
this._errorMsg = protocolName + " not supported by server!";
return false;
}
return true;
}
};
function Md5DigestAuthenticator( ) {
/* SASL using DIGEST-MD5 authentication.
Uses complicated hash of password
with nonce and cnonce to obscure password while preventing replay
attacks.
See http://www.faqs.org/rfcs/rfc2831.html
"Using Digest Authentication as a SASL mechanism"
TODO: currently, this is getting rejected by my server.
What's wrong?
*/
}
Md5DigestAuthenticator.prototype = {
_makeCNonce: function( ) {
return "\"" + Math.floor( 10000000000 * Math.random() ) + "\"";
},
generateResponse: function Md5__generateResponse( rootElem ) {
if ( this._stepNumber == 0 ) {
if ( this.verifyProtocolSupport( rootElem, "DIGEST-MD5" ) == false ) {
return false;
}
// SASL step 1: request that we use DIGEST-MD5 authentication.
this._stepNumber = 1;
return "<auth xmlns='urn:ietf:params:xml:ns:xmpp-sasl' mechanism='DIGEST-MD5'/>";
} else if ( this._stepNumber == 1 ) {
// proceed to SASL step 2: are you asking for a CHALLENGE?!?
var challenge = this._unpackChallenge( rootElem.firstChild.nodeValue );
dump( "Nonce is " + challenge.nonce + "\n" );
// eg:
// nonce="3816627940",qop="auth",charset=utf-8,algorithm=md5-sess
// Now i have the nonce: make a digest-response out of
/* username: required
realm: only needed if realm is in challenge
nonce: required, just as recieved
cnonce: required, opaque quoted string, 64 bits entropy
nonce-count: optional
qop: (quality of protection) optional
serv-type: optional?
host: optional?
serv-name: optional?
digest-uri: "service/host/serv-name" (replaces those three?)
response: required (32 lowercase hex),
maxbuf: optional,
charset,
LHEX (32 hex digits = ??),
cipher: required if auth-conf is negotiatedd??
authzid: optional
*/
// TODO: Are these acceptable values for realm, nonceCount, and
// digestUri??
var nonceCount = "00000001";
var digestUri = "xmpp/" + this.realm;
var cNonce = this._makeCNonce();
// Section 2.1.2.1 of RFC2831
var A1 = str_md5( this.name + ":" + this.realm + ":" + this.password ) + ":" + challenge.nonce + ":" + cNonce;
var A2 = "AUTHENTICATE:" + digestUri;
var myResponse = hex_md5( hex_md5( A1 ) + ":" + challenge.nonce + ":" + nonceCount + ":" + cNonce + ":auth" + hex_md5( A2 ) );
var responseDict = {
username: "\"" + this.name + "\"",
nonce: challenge.nonce,
nc: nonceCount,
cnonce: cNonce,
qop: "\"auth\"",
algorithm: "md5-sess",
charset: "utf-8",
response: myResponse
};
responseDict[ "digest-uri" ] = "\"" + digestUri + "\"";
var responseStr = this._packChallengeResponse( responseDict );
this._stepNumber = 2;
return "<response xmlns='urn:ietf:params:xml:ns:xmpp-sasl'>" + responseStr + "</response>";
} else if ( this._stepNumber = 2 ) {
dump( "Got to step 3!" );
// At this point the server might reject us with a
// <failure><not-authorized/></failure>
if ( rootElem.nodeName == "failure" ) {
this._errorMsg = rootElem.firstChild.nodeName;
return false;
}
//this._connectionStatus = this.REQUESTED_SASL_3;
}
this._errorMsg = "Can't happen.";
return false;
},
_unpackChallenge: function( challengeString ) {
var challenge = atob( challengeString );
dump( "After b64 decoding: " + challenge + "\n" );
var challengeItemStrings = challenge.split( "," );
var challengeItems = {};
for ( var x in challengeItemStrings ) {
var stuff = challengeItemStrings[x].split( "=" );
challengeItems[ stuff[0] ] = stuff[1];
}
return challengeItems;
},
_packChallengeResponse: function( responseDict ) {
var responseArray = []
for( var x in responseDict ) {
responseArray.push( x + "=" + responseDict[x] );
}
var responseString = responseArray.join( "," );
dump( "Here's my response string: \n" );
dump( responseString + "\n" );
return btoa( responseString );
}
};
Md5DigestAuthenticator.prototype.__proto__ = new BaseAuthenticator();
function PlainAuthenticator( ) {
/* SASL using PLAIN authentication, which sends password in the clear. */
}
PlainAuthenticator.prototype = {
generateResponse: function( rootElem ) {
if ( this._stepNumber == 0 ) {
if ( this.verifyProtocolSupport( rootElem, "PLAIN" ) == false ) {
return false;
}
var authString = btoa( this._realm + '\0' + this._name + '\0' + this._password );
this._stepNumber = 1;
return "<auth xmlns='urn:ietf:params:xml:ns:xmpp-sasl' mechanism='PLAIN'>" + authString + "</auth>";
} else if ( this._stepNumber == 1 ) {
if ( rootElem.nodeName == "failure" ) {
// Authentication rejected: username or password may be wrong.
this._errorMsg = rootElem.firstChild.nodeName;
return false;
} else if ( rootElem.nodeName == "success" ) {
// Authentication accepted: now we start a new stream for
// resource binding.
/* RFC3920 part 7 says: upon receiving a success indication within the
SASL negotiation, the client MUST send a new stream header to the
server, to which the serer MUST respond with a stream header
as well as a list of available stream features. */
// TODO: resource binding happens in any authentication mechanism
// so should be moved to base class.
this._stepNumber = 2;
return "<?xml version='1.0'?><stream:stream to='jonathan-dicarlos-macbook-pro.local' xmlns='jabber:client' xmlns:stream='http://etherx.jabber.org/streams' version='1.0'>";
}
} else if ( this._stepNumber == 2 ) {
// See if the server is asking us to bind a resource, and if it's
// asking us to start a session:
var bindNodes = rootElem.getElementsByTagName( "bind" );
if ( bindNodes.length > 0 ) {
this._needBinding = true;
}
var sessionNodes = rootElem.getElementsByTagName( "session" );
if ( sessionNodes.length > 0 ) {
this._needSession = true;
}
if ( !this._needBinding && !this._needSession ) {
// Server hasn't requested either: we're done.
return this.COMPLETION_CODE;
}
if ( this._needBinding ) {
// Do resource binding:
// Tell the server to generate the resource ID for us.
this._stepNumber = 3;
return "<iq type='set' id='bind_1'><bind xmlns='urn:ietf:params:xml:ns:xmpp-bind'/></iq>";
}
this._errorMsg = "Server requested session not binding: can't happen?";
return false;
} else if ( this._stepNumber == 3 ) {
// Pull the JID out of the stuff the server sends us.
var jidNodes = rootElem.getElementsByTagName( "jid" );
if ( jidNodes.length == 0 ) {
this._errorMsg = "Expected JID node from server, got none.";
return false;
}
this._jid = jidNodes[0].firstChild.nodeValue;
// TODO: Does the client need to do anything special with its new
// "client@host.com/resourceID" full JID?
dump( "JID set to " + this._jid );
// If we still need to do session, then we're not done yet:
if ( this._needSession ) {
this._stepNumber = 4;
return "<iq to='" + this._realm + "' type='set' id='sess_1'><session xmlns='urn:ietf:params:xml:ns:xmpp-session'/></iq>";
} else {
return this.COMPLETION_CODE;
}
} else if ( this._stepNumber == 4 ) {
// OK, now we're done.
return this.COMPLETION_CODE;
}
}
};
PlainAuthenticator.prototype.__proto__ = new BaseAuthenticator();

Просмотреть файл

@ -0,0 +1,336 @@
var Cc = Components.classes;
var Ci = Components.interfaces;
function InputStreamBuffer() {
}
InputStreamBuffer.prototype = {
_data: "",
append: function( stuff ) {
this._data = this._data + stuff;
},
clear: function() {
this._data = "";
},
getData: function() {
return this._data;
}
}
function SocketClient( host, port ) {
this._init( host, port );
}
SocketClient.prototype = {
__threadManager: null,
get _threadManager() {
if (!this.__threadManager)
this.__threadManager = Cc["@mozilla.org/thread-manager;1"].getService();
return this.__threadManager;
},
__transport: null,
get _transport() {
if (!this.__transport) {
var transportService = Cc["@mozilla.org/network/socket-transport-service;1"].getService(Ci.nsISocketTransportService);
this.__transport = transportService.createTransport(['starttls'],
1, // ssl only
this._host,
this._port,
null); // proxy
}
return this.__transport;
},
_init: function( host, port ) {
this._host = host;
this._port = port;
this._contentRead = "";
this._buffer = null;
this.connect();
},
connect: function() {
var outstream = this._transport.openOutputStream( 0, // flags
0, // buffer size
0 ); // number of buffers
this._outstream = outstream;
var buffer = new InputStreamBuffer;
this._buffer = buffer;
// Wrap input stream is C only, nonscriptable, so wrap it in scriptable
// stream:
var rawInputStream = this._transport.openInputStream( 0, 0, 0 );
var scriptablestream = Cc["@mozilla.org/scriptableinputstream;1"].createInstance(Ci.nsIScriptableInputStream);
scriptablestream.init(rawInputStream);
// input stream pump for asynchronous reading
var pump = Cc["@mozilla.org/network/input-stream-pump;1"].createInstance(Ci.nsIInputStreamPump);
pump.init(rawInputStream, -1, -1, 0, 0,
false); //automatically close once all data read?
// create dataListener class for callback:
var dataListener = {
data : "",
onStartRequest: function(request, context){
},
onStopRequest: function(request, context, status){
rawInputStream.close();
outstream.close();
},
onDataAvailable: function(request, context, inputStream, offset, count){
// use scriptable stream wrapper, not "real" stream.
// count is number of bytes available, offset is position in stream.
// Do stuff with data here!
buffer.append( scriptablestream.read( count ));
}
};
// register it:
pump.asyncRead(dataListener, null); // second argument is a context
//which gets passed in as the context argument to methods of dataListener
//Should be done connecting now. TODO: Catch and report errors.
},
send: function( messageText ) {
this._outstream.write( messageText, messageText.length );
},
getBinaryOutStream: function() {
var binaryOutStream = Cc["@mozilla.org/binaryoutputstream;1"].createInstance(Ci.nsIBinaryOutputStream);
binaryOutStream.setOutputStream( this._outstream ); // is this right?
return binaryOutStream;
},
disconnect: function() {
var thread = this._threadManager.currentThread;
while( thread.hasPendingEvents() )
{
thread.processNextEvent( true );
}
},
checkResponse: function() {
return this._getData();
},
waitForResponse: function() {
var thread = this._threadManager.currentThread;
while( this._buffer.getData().length == 0 )
{
thread.processNextEvent( true );
}
var output = this._buffer.getData();
this._buffer.clear();
return output;
},
startTLS: function() {
this._transport.securityInfo.QueryInterface(Ci.nsISSLSocketControl);
this._transport.securityInfo.StartTLS();
},
// TODO have a variant of waitForResponse that gets binary data
// binaryInStream = Cc["@mozilla.org/binaryinputstream;1].createInstance( Ci.nsIBinaryInputStream );
// binaryInStream.setInputStream( this._rawInputStream );
};
/* The interface that should be implemented by any Transport object:
send( messageXml );
setCallbackObject( object with .onIncomingData and .onTransportError );
connect();
disconnect();
*/
function HTTPPollingTransport( serverUrl, useKeys, interval ) {
/* Send HTTP requests periodically to the server using a timer.
HTTP POST requests with content-type application/x-www-form-urlencoded.
responses from the server have content-type text/xml
request and response are UTF-8 encoded (ignore what HTTP header says)
identify session by always using set-cookie header with cookie named ID
first request sets this to 0 to indicate new session. */
this._init( serverUrl, useKeys, interval );
}
HTTPPollingTransport.prototype = {
_init: function( serverUrl, useKeys, interval ) {
this._serverUrl = serverUrl
this._n = 0;
this._key = this._makeSeed();
this._latestCookie = "";
this._connectionId = 0;
this._callbackObject = null;
this._useKeys = useKeys;
this._interval = interval;
},
__request: null,
get _request() {
if (!this.__request)
this.__request = Cc["@mozilla.org/xmlextras/xmlhttprequest;1"].createInstance( Ci.nsIXMLHttpRequest );
return this.__request;
},
__hasher: null,
get _hasher() {
if (!this.__hasher)
this.__hasher = Cc["@mozilla.org/security/hash;1"].createInstance( Ci.nsICryptoHash );
return this.__hasher;
},
__timer: null,
get _timer() {
if (!this.__timer)
this.__timer = Cc["@mozilla.org/timer;1"].createInstance( Ci.nsITimer );
return this.__timer;
},
_makeSeed: function() {
return "foo";//"MyKeyOfHorrors";
},
_advanceKey: function() {
var converter = Cc["@mozilla.org/intl/scriptableunicodeconverter"].createInstance(Ci.nsIScriptableUnicodeConverter);
// we use UTF-8 here, you can choose other encodings.
converter.charset = "UTF-8";
// result is an out parameter,
// result.value will contain the array length
var result = {};
// data is an array of bytes
var data = converter.convertToByteArray( this._key, result);
this._n += 1;
this._hasher.initWithString( "SHA1" );
this._hasher.update( data, data.length );
this._key = this._hasher.finish( true ); // true means B64encode
},
_setIdFromCookie: function( self, cookie ) {
// parse connection ID out of the cookie:
// dump( "Cookie is " + cookie + "\n" );
var cookieSegments = cookie.split( ";" );
cookieSegments = cookieSegments[0].split( "=" );
var newConnectionId = cookieSegments[1];
switch( newConnectionId) {
case "0:0":
self._onError( "Unknown error!\n" );
break;
case "-1:0":
self._onError( "Server error!\n" );
break;
case "-2:0":
self._onError( "Bad request!\n" );
break;
case "-3:0":
self._onError( "Key sequence error!\n" );
break;
default :
self._connectionId = cookieSegments[1];
// dump( "Connection ID set to " + self._connectionId + "\n" );
break;
}
},
_onError: function( errorText ) {
dump( "Transport error: " + errorText + "\n" );
if ( this._callbackObject != null ) {
this._callbackObject.onTransportError( errorText );
}
this.disconnect();
},
_doPost: function( requestXml ) {
var request = this._request;
var callbackObj = this._callbackObject;
var self = this;
var contents = "";
if ( this._useKey ) {
this._advanceKey();
contents = this._connectionId + ";" + this._key + "," + requestXml;
} else {
contents = this._connectionId + "," + requestXml;
/* TODO:
Currently I get a "-3:0" error (key sequence error) from the 2nd
exchange if using the keys is enabled. */
}
_processReqChange = function( ) {
//Callback for XMLHTTPRequest object state change messages
if ( request.readyState == 4 ) {
if ( request.status == 200) {
dump( "Server says: " + request.responseText + "\n" );
// Look for a set-cookie header:
var latestCookie = request.getResponseHeader( "Set-Cookie" );
if ( latestCookie.length > 0 ) {
self._setIdFromCookie( self, latestCookie );
}
if ( callbackObj != null && request.responseText.length > 0 ) {
callbackObj.onIncomingData( request.responseText );
}
} else {
dump ( "Error! Got HTTP status code " + request.status + "\n" );
}
}
};
request.open( "POST", this._serverUrl, true ); //async = true
request.setRequestHeader( "Content-type",
"application/x-www-form-urlencoded;charset=UTF-8" );
request.setRequestHeader( "Content-length", contents.length );
request.setRequestHeader( "Connection", "close" );
request.onreadystatechange = _processReqChange;
dump( "Sending: " + contents + "\n" );
request.send( contents );
},
send: function( messageXml ) {
this._doPost( messageXml );
},
setCallbackObject: function( callbackObject ) {
this._callbackObject = callbackObject;
},
notify: function( timer ) {
/* having a notify method makes this object satisfy the nsITimerCallback
interface, so the object can be passed to timer.initWithCallback. */
/* Periodically, if we don't have anything else to post, we should
post an empty message just to see if the server has any queued
data it's waiting to give us in return. */
this._doPost( "" );
},
connect: function() {
/* Set up a timer to poll the server periodically. */
// TODO doPost isn't reentrant; don't try to doPost if there's
//already a post in progress... or can that never happen?
this._timer.initWithCallback( this,
this._interval,
this._timer.TYPE_REPEATING_SLACK );
},
disconnect: function () {
this._timer.cancel();
},
testKeys: function () {
this._key = "foo";
dump( this._key + "\n" );
for ( var x = 1; x < 7; x++ ) {
this._advanceKey();
dump( this._key + "\n" );
}
},
};
//transport = new HTTPPollingTransport( "http://127.0.0.1:5280/http-poll" );
//transport.testKeys();

Просмотреть файл

@ -0,0 +1,404 @@
// See www.xulplanet.com/tutorials/mozsdk/sockets.php
// http://www.xmpp.org/specs/rfc3920.html
// http://www.process-one.net/docs/ejabberd/guide_en.html
// http://www.xulplanet.com/tutorials/mozsdk/xmlparse.php
// http://developer.mozilla.org/en/docs/xpcshell
// http://developer.mozilla.org/en/docs/Writing_xpcshell-based_unit_tests
var Cc = Components.classes;
var Ci = Components.interfaces;
function JabberClient( clientName, realm, clientPassword, transport, authenticator ) {
this._init( clientName, realm, clientPassword, transport, authenticator );
}
JabberClient.prototype = {
//connection status codes:
NOT_CONNECTED: 0,
CALLED_SERVER: 1,
AUTHENTICATING: 2,
CONNECTED: 3,
FAILED: -1,
// IQ stanza status codes:
IQ_WAIT: 0,
IQ_OK: 1,
IQ_ERROR: -1,
_init: function( clientName, realm, clientPassword, transport, authenticator ) {
this._myName = clientName;
this._realm = realm;
this._fullName = clientName + "@" + realm;
this._myPassword = clientPassword;
this._connectionStatus = this.NOT_CONNECTED;
this._error = null;
this._streamOpen = false;
this._transportLayer = transport;
this._authenticationLayer = authenticator;
this._authenticationLayer.initialize( clientName, realm, clientPassword );
this._messageHandlers = [];
this._iqResponders = [];
this._nextIqId = 0;
this._pendingIqs = {};
},
__parser: null,
get _parser() {
if (!this.__parser)
this.__parser = Cc["@mozilla.org/xmlextras/domparser;1"].createInstance( Ci.nsIDOMParser );
return this.__parser;
},
__threadManager: null,
get _threadManager() {
if (!this.__threadManager)
this.__threadManager = Cc["@mozilla.org/thread-manager;1"].getService();
return this.__threadManager;
},
parseError: function( streamErrorNode ) {
dump( "Uh-oh, there was an error!\n" );
var error = streamErrorNode.childNodes[0];
dump( "Name: " + error.nodeName + " Value: " + error.nodeValue + "\n" );
this._error = error.nodeName;
this.disconnect();
/* Note there can be an optional <text>bla bla </text> node inside
stream: error giving additional info; there can also optionally
be an app-specific condition element qualified by an app-defined
namespace */
},
setError: function( errorText ) {
dump( "Error: " + errorText + "\n" );
this._error = errorText;
this._connectionStatus = this.FAILED;
},
onIncomingData: function( messageText ) {
var responseDOM = this._parser.parseFromString( messageText, "text/xml" );
if (responseDOM.documentElement.nodeName == "parsererror" ) {
/* Before giving up, remember that XMPP doesn't close the top-level
<stream:stream> element until the communication is done; this means
that what we get from the server is often technically only an
xml fragment. Try manually appending the closing tag to simulate
a complete xml document and then parsing that. */
var response = messageText + this._makeClosingXml();
responseDOM = this._parser.parseFromString( response, "text/xml" );
}
if ( responseDOM.documentElement.nodeName == "parsererror" ) {
/* If that still doesn't work, it might be that we're getting a fragment
with multiple top-level tags, which is a no-no. Try wrapping it
all inside one proper top-level stream element and parsing. */
response = this._makeHeaderXml( this._fullName ) + messageText + this._makeClosingXml();
responseDOM = this._parser.parseFromString( response, "text/xml" );
}
if ( responseDOM.documentElement.nodeName == "parsererror" ) {
/* Still can't parse it, give up. */
this.setError( "Can't parse incoming XML." );
return;
}
var rootElem = responseDOM.documentElement;
if ( this._connectionStatus == this.CALLED_SERVER ) {
// skip TLS, go straight to SALS. (encryption should be negotiated
// at the HTTP layer, i.e. use HTTPS)
//dispatch whatever the next stage of the connection protocol is.
response = this._authenticationLayer.generateResponse( rootElem );
if ( response == false ) {
this.setError( this._authenticationLayer.getError() );
} else if ( response == this._authenticationLayer.COMPLETION_CODE ){
this._connectionStatus = this.CONNECTED;
dump( "We be connected!!\n" );
} else {
this._transportLayer.send( response );
}
return;
}
if ( this._connectionStatus == this.CONNECTED ) {
/* Check incoming xml to see if it contains errors, presence info,
or a message: */
var errors = rootElem.getElementsByTagName( "stream:error" );
if ( errors.length > 0 ) {
this.setError( errors[0].firstChild.nodeName );
return;
}
var presences = rootElem.getElementsByTagName( "presence" );
if (presences.length > 0 ) {
var from = presences[0].getAttribute( "from" );
if ( from != undefined ) {
dump( "I see that " + from + " is online.\n" );
}
}
if ( rootElem.nodeName == "message" ) {
this.processIncomingMessage( rootElem );
} else {
var messages = rootElem.getElementsByTagName( "message" );
if (messages.length > 0 ) {
for ( var message in messages ) {
this.processIncomingMessage( messages[ message ] );
}
}
}
if ( rootElem.nodeName == "iq" ) {
this.processIncomingIq( rootElem );
} else {
var iqs = rootElem.getElementsByTagName( "iq" );
if ( iqs.length > 0 ) {
for ( var iq in iqs ) {
this.processIncomingIq( iqs[ iq ] );
}
}
}
}
},
processIncomingMessage: function( messageElem ) {
dump( "in processIncomingMessage: messageElem is a " + messageElem + "\n" );
var from = messageElem.getAttribute( "from" );
var contentElem = messageElem.firstChild;
// Go down till we find the element with nodeType = 3 (TEXT_NODE)
while ( contentElem.nodeType != 3 ) {
contentElem = contentElem.firstChild;
}
dump( "Incoming message to you from " + from + ":\n" );
dump( contentElem.nodeValue );
for ( var x in this._messageHandlers ) {
// TODO do messages have standard place for metadata?
// will want to have handlers that trigger only on certain metadata.
this._messageHandlers[x].handle( contentElem.nodeValue, from );
}
},
processIncomingIq: function( iqElem ) {
/* This processes both kinds of incoming IQ stanzas --
ones that are new (initated by another jabber client) and those that
are responses to ones we sent out previously. We can tell the
difference by the type attribute. */
var buddy = iqElem.getAttribute( "from " );
var id = iqElem.getAttribute( id );
switch( iqElem.getAttribute( "type" ) ) {
case "get":
/* Someone is asking us for the value of a variable.
Delegate this to the registered iqResponder; package the answer
up in an IQ stanza of the same ID and send it back to the asker. */
var variable = iqElem.firstChild.firstChild.getAttribute( "var" );
// TODO what to do here if there's more than one registered
// iqResponder?
var value = this._iqResponders[0].get( variable );
var query = "<query><getresult value='" + value + "'/></query>";
var xml = _makeIqXml( this._fullName, buddy, "result", id, query );
this._transportLayer.send( xml );
break;
case "set":
/* Someone is telling us to set the value of a variable.
Delegate this to the registered iqResponder; we can reply
either with an empty iq type="result" stanza, or else an
iq type="error" stanza */
var variable = iqElem.firstChild.firstChild.getAttribute( "var" );
var newValue = iqElem.firstChild.firstChildgetAttribute( "value" );
// TODO what happens when there's more than one reigistered
// responder?
// TODO give the responder a chance to say "no" and give an error.
this._iqResponders[0].set( variable, value );
var xml = _makeIqXml( this._fullName, buddy, "result", id, "<query/>" );
this._transportLayer.send( xml );
break;
case "result":
/* If all is right with the universe, then the id of this iq stanza
corresponds to a set or get stanza that we sent out, so it should
be in our pending dictionary.
*/
if ( this._pendingIqs[ id ] == undefined ) {
this.setError( "Unexpected IQ reply id" + id );
return;
}
/* The result stanza may have a query with a value in it, in
which case this is the value of the variable we requested.
If there's no value, it was probably a set query, and should
just be considred a success. */
var newValue = iqElem.firstChild.firstChild.getAttribute( "value" );
if ( newValue != undefined ) {
this._pendingIqs[ id ].value = newValue;
} else {
this._pendingIqs[ id ].value = true;
}
this._pendingIqs[ id ].status = this.IQ_OK;
break;
case "error":
/* Dig down through the element tree till we find the one with
the error text... */
var elems = iqElem.getElementsByTagName( "error" );
var errorNode = elems[0].firstChild;
if ( errorNode.nodeValue != null ) {
this.setError( errorNode.nodeValue );
} else {
this.setError( errorNode.nodeName );
}
if ( this._pendingIqs[ id ] != undefined ) {
this._pendingIqs[ id ].status = this.IQ_ERROR;
}
break;
}
},
registerMessageHandler: function( handlerObject ) {
/* messageHandler object must have
handle( messageText, from ) method.
*/
this._messageHandlers.push( handlerObject );
},
registerIQResponder: function( handlerObject ) {
/* IQResponder object must have
.get( variable ) and
.set( variable, newvalue ) methods. */
this._iqResponders.push( handlerObject );
},
onTransportError: function( errorText ) {
this.setError( errorText );
},
connect: function( host ) {
// Do the handshake to connect with the server and authenticate.
this._transportLayer.connect();
this._transportLayer.setCallbackObject( this );
this._transportLayer.send( this._makeHeaderXml( host ) );
this._connectionStatus = this.CALLED_SERVER;
// Now we wait... the rest of the protocol will be driven by
// onIncomingData.
},
_makeHeaderXml: function( recipient ) {
return "<?xml version='1.0'?><stream:stream to='" + recipient + "' xmlns='jabber:client' xmlns:stream='http://etherx.jabber.org/streams' version='1.0'>";
},
_makeMessageXml: function( messageText, fullName, recipient ) {
/* a "message stanza". Note the message element must have the
full namespace info or it will be rejected. */
msgXml = "<message xmlns='jabber:client' xmlns:stream='http://etherx.jabber.org/streams' from='" + fullName + "' to='" + recipient + "' xml:lang='en'><body>" + messageText + "</body></message>";
dump( "Message xml: \n" );
dump( msgXml );
return msgXml;
},
_makePresenceXml: function( fullName ) {
// a "presence stanza", sent to announce my presence to the server;
// the server is supposed to multiplex this to anyone subscribed to
// presence notifications.
return "<presence from ='" + fullName + "'><show/></presence>";
},
_makeIqXml: function( fullName, recipient, type, id, query ) {
/* an "iq (info/query) stanza". This can be used for structured data
exchange: I send an <iq type='get' id='1'> containing a query,
and get back an <iq type='result' id='1'> containing the answer to my
query. I can also send an <iq type='set' id='2'> to set a value
remotely. The recipient answers with either <iq type='result'> or
<iq type='error'>, with an id matching the id of my set or get. */
//Useful!!
return "<iq xmlns='jabber:client' xmlns:stream='http://etherx.jabber.org/streams' from='" + fullName + "' to='" + recipient + "' type='" + type + "' id='" + id + "'>" + query + "</iq>";
},
_makeClosingXml: function () {
return "</stream:stream>";
},
_generateIqId: function() {
// Each time this is called, it returns an ID that has not
// previously been used this session.
var id = "client_" + this._nextIqId;
this._nextIqId = this._nextIqId + 1;
return id;
},
_sendIq: function( recipient, query, type ) {
var id = this._generateIqId();
this._pendingIqs[ id ] = { status: this.IQ_WAIT };
this._transportLayer.send( this._makeIqXml( this._fullName,
recipient,
type,
id,
query ) );
/* And then wait for a response with the same ID to come back...
When we get a reply, the pendingIq dictionary entry will have
its status set to IQ_OK or IQ_ERROR and, if it's IQ_OK and
this was a query that's supposed to return a value, the value
will be in the value field of the entry. */
var thread = this._threadManager.currentThread;
while( this._pendingIqs[ id ].status == this.IQ_WAIT ) {
thread.processNextEvent( true );
}
if ( this._pendingIqs[ id ].status == this.IQ_OK ) {
return this._pendingIqs[ id ].value;
} else if ( this._pendingIqs[ id ].status == this.IQ_ERROR ) {
return false;
}
// Can't happen?
},
iqGet: function( recipient, variable ) {
var query = "<query><getvar var='" + variable + "'/></query>";
return this._sendIq( recipient, query, "get" );
},
iqSet: function( recipient, variable, value ) {
var query = "<query><setvar var='" + variable + "' value='" + value + "'/></query>";
return this._sendIq( recipient, query, "set" );
},
sendMessage: function( recipient, messageText ) {
// OK so now I'm doing that part, but what am I supposed to do with the
// new JID that I'm bound to??
var body = this._makeMessageXml( messageText, this._fullName, recipient );
this._transportLayer.send( body );
},
announcePresence: function() {
this._transportLayer.send( "<presence/>" );
},
subscribeForPresence: function( buddyId ) {
// OK, there are 'subscriptions' and also 'rosters'...?
//this._transportLayer.send( "<presence to='" + buddyId + "' type='subscribe'/>" );
// TODO
// other side must then approve this by sending back a presence to
// me with type ='subscribed'.
},
disconnect: function() {
// todo: only send closing xml if the stream has not already been
// closed (if there was an error, the server will have closed the stream.)
this._transportLayer.send( this._makeClosingXml() );
this._transportLayer.disconnect();
},
waitForConnection: function( ) {
var thread = this._threadManager.currentThread;
while ( this._connectionStatus != this.CONNECTED &&
this._connectionStatus != this.FAILED ) {
thread.processNextEvent( true );
}
},
waitForDisconnect: function() {
var thread = this._threadManager.currentThread;
while ( this._connectionStatus == this.CONNECTED ) {
thread.processNextEvent( true );
}
}
};
// IM level protocol stuff: presence announcements, conversations, etc.
// ftp://ftp.isi.edu/in-notes/rfc3921.txt