зеркало из https://github.com/mozilla/pjs.git
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:
Родитель
ebed002936
Коммит
a7ee9d6de3
|
@ -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
|
||||
|
Загрузка…
Ссылка в новой задаче