pax_global_header00006660000000000000000000000064117244045300014512gustar00rootroot0000000000000052 comment=43cc25cc60daec8f6ffee68c4084f9dadc1cd870 astro-node-xmpp-43cc25c/000077500000000000000000000000001172440453000151575ustar00rootroot00000000000000astro-node-xmpp-43cc25c/.gitignore000066400000000000000000000000261172440453000171450ustar00rootroot00000000000000.DS_Store node_modulesastro-node-xmpp-43cc25c/.travis.yml000066400000000000000000000001411172440453000172640ustar00rootroot00000000000000language: node_js node_js: - 0.4 - 0.6 before_install: - "sudo apt-get install libicu-dev" astro-node-xmpp-43cc25c/LICENSE000066400000000000000000000020411172440453000161610ustar00rootroot00000000000000Copyright (c) 2010 Stephan Maka Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. astro-node-xmpp-43cc25c/README.markdown000066400000000000000000000107541172440453000176670ustar00rootroot00000000000000# node-xmpp idiomatic XMPP library for [node.js](http://nodejs.org/) ## Installation With package manager [npm](http://npmjs.org/): npm install node-xmpp ## Objectives of *node-xmpp:* * Use node.js conventions, especially `EventEmitter`, ie. for write buffer control * Fast parsing, `node-expat` was written for this library * Client support for both XMPP clients and components * Optional server infrastructure with `Router` * After authentication, leave trivial protocol bits to the user, that is XML handling according to any [XEP](http://xmpp.org/xmpp-protocols/xmpp-extensions/) ## Features * Client authentication with SASL DIGEST-MD5, PLAIN, ANONYMOUS, X-FACEBOOK-PLATFORM * `_xmpp-client._tcp` SRV record support * Simple JID parsing with Stringprep normalization * Optional now, you won't need ICU for just node-xmpp * Please be aware if you identify users by JIDs * `npm install node-stringprep` * Uses [ltx](http://github.com/astro/ltx) * Much easier to handle than a standard DOM * xmlns-aware * Easy XML builder like Strophe.js (see down) * Non-buffering serialization * Was split out of node-xmpp for modularization and resuability * [Component](http://xmpp.org/extensions/xep-0114.html) connections * Run your own server/talk to other servers with `xmpp.Router` ## Dependencies * [node-expat](http://github.com/astro/node-expat) * [ltx](http://github.com/astro/ltx) Optional * [node-stringprep](http://github.com/astro/node-stringprep): for [icu](http://icu-project.org/)-based string normalization. ## Related Libraries * [node-xmpp-bosh](http://code.google.com/p/node-xmpp-bosh/): BOSH & websocket server (connection manager) * [node-xmpp-via-bosh](https://github.com/anoopc/node-xmpp-via-bosh/): BOSH client connections from node.js * [node-simple-xmpp](https://github.com/arunoda/node-simple-xmpp/): Simpler high-level client layer * [xmpp-server](https://github.com/superfeedr/xmpp-server/): Reusable XMPP server on top of node-xmpp ## Design Inheritance tree and associations: ┌────────────┐1 1┌────────────┐ │ net.Stream ├───────┤ Connection │ └────────────┘ └────────────┘ ↑ ┌────────────┬───────┴───┬────────────┐ │ │ │ │ ┏━━━━━┷━━━━┓ ┏━━━━━┷━━━━━┓ ┌───┴────┐ ┌─────┴─────┐ ┃ Client ┃ ┃ Component ┃ │ Server │ │ C2SStream │ ┗━━━━━━━━━━┛ ┗━━━━━━━━━━━┛ └────────┘ └───────────┘ ↑ ↑0..* ┌─────────────────────┤ │accepts │ │ │1 ┌────────┴───────┐ ┌───────────┴────┐ ┏━━━━━┷━━━━━┓ │ OutgoingServer │ │ IncomingServer │ ┃ C2SServer ┃ └─────────────┬──┘ └───┬────────────┘ ┗━━━━━┯━━━━━┛ 0..* │ │ 0..* │ creates │ │ accepts │ ┏┷━━━━━━━━┷┓ │ ┃ Router ┃←──────────────────┘ ┗━━━━━━━━━━┛ 1 This foundation is complemented by two basic data structures: * *JID:* a Jabber-Id, represented as a triple of `user`, `domain`, `resource` * *Element:* any XML Element ### Building XML Elements Strophe.js' XML Builder is very convenient for producing XMPP stanzas. ltx includes it in a much more primitive way: the `c()`, `cnode()` and `t()` methods can be called on any *Element* object, returning the new child element. This can be confusing: in the end, you will hold the last-added child until you use `up()`, a getter for the parent. `Connection.send()` first invokes `tree()` to retrieve the uppermost parent, the XMPP stanza, before sending it out the wire. ## TODO * More documentation * More tests (Using [Vows](http://vowsjs.org/)) astro-node-xmpp-43cc25c/examples/000077500000000000000000000000001172440453000167755ustar00rootroot00000000000000astro-node-xmpp-43cc25c/examples/c2s.js000066400000000000000000000023441172440453000200250ustar00rootroot00000000000000var xmpp = require('../lib/node-xmpp'); /* This is a very basic C2S server example. One of the key design decisions of node-xmpp is to keep it very lightweight */ /* If you need a full blown server check out https://github.com/superfeedr/xmpp-server */ // Sets up the server. var c2s = new xmpp.C2SServer({ port: 5222, domain: 'localhost'//, // tls: { // keyPath: './examples/localhost-key.pem', // certPath: './examples/localhost-cert.pem' // } }); // On Connect event. When a client connects. c2s.on("connect", function(client) { // That's the way you add mods to a given server. // Allows the developer to register the jid against anything they want c2s.on("register", function(opts, cb) { cb(true); }); // Allows the developer to authenticate users against anything they want. client.on("authenticate", function(opts, cb) { cb(null); // cb(false); }); client.on("online", function() { client.send(new xmpp.Message({ type: 'chat' }).c('body').t("Hello there, little client.")); }); // Stanza handling client.on("stanza", function(stanza) { }); // On Disconnect event. When a client disconnects client.on("disconnect", function(client) { }); }); astro-node-xmpp-43cc25c/examples/debug_c2s.js000066400000000000000000000002731172440453000211720ustar00rootroot00000000000000var xmpp = require('../lib/node-xmpp'); var cl = new xmpp.Client({ jid: "julien@localhost", password: "password"}); cl.on('online', function() { console.log("ONLINE!!! YIHAA"); }); astro-node-xmpp-43cc25c/examples/echo_bot.js000066400000000000000000000015741172440453000211240ustar00rootroot00000000000000/** * Echo Bot - the XMPP Hello World **/ var sys = require('sys'); var xmpp = require('../lib/node-xmpp'); var argv = process.argv; if (argv.length != 4) { sys.puts('Usage: node echo_bot.js '); process.exit(1); } var cl = new xmpp.Client({ jid: argv[2], password: argv[3] }); cl.on('online', function() { cl.send(new xmpp.Element('presence', { }). c('show').t('chat').up(). c('status').t('Happily echoing your stanzas') ); }); cl.on('stanza', function(stanza) { if (stanza.is('message') && // Important: never reply to errors! stanza.attrs.type !== 'error') { // Swap addresses... stanza.attrs.to = stanza.attrs.from; delete stanza.attrs.from; // and send back. cl.send(stanza); } }); cl.on('error', function(e) { sys.puts(e); }); astro-node-xmpp-43cc25c/examples/echo_component.js000066400000000000000000000017701172440453000223400ustar00rootroot00000000000000/** * Echo Component - the XMPP Hello World **/ var sys = require('sys'); var xmpp = require('../lib/node-xmpp'); var argv = process.argv; if (argv.length != 6) { sys.puts('Usage: node echo_bot.js '); process.exit(1); } var cl = new xmpp.Component({ jid: argv[2], password: argv[3], host: argv[4], port: argv[5] }); cl.on('online', function() { cl.send(new xmpp.Element('presence', { type: 'chat'}). c('show').t('chat').up(). c('status').t('Happily echoing your stanzas') ); }); cl.on('stanza', function(stanza) { if (stanza.is('message') && // Important: never reply to errors! stanza.attrs.type !== 'error') { // Swap addresses... var me = stanza.attrs.to; stanza.attrs.to = stanza.attrs.from; stanza.attrs.from = me; // and send back. cl.send(stanza); } }); cl.on('error', function(e) { sys.puts(e); }); astro-node-xmpp-43cc25c/examples/echo_mixin.js000066400000000000000000000025771172440453000214700ustar00rootroot00000000000000/** * Demonstrates how the echo behavior can be abstracted into a decorator, * working equally well on clients, components or S2S. */ var sys = require('sys'); var xmpp = require('../lib/node-xmpp'); var argv = process.argv; if (argv.length < 5 && argv) { sys.puts('Usage: node echo_mixin.js client '); sys.puts('Or: node echo_mixin.js component '); process.exit(1); } function echoMixin(connection) { connection.on('stanza', function(stanza) { if (stanza.is('message') && // Important: never reply to errors! stanza.attrs.type !== 'error') { // Swap addresses... stanza.attrs.to = stanza.attrs.from; delete stanza.attrs.from; // and send back. connection.send(stanza); } }); } function errorMixin(connection) { connection.on('error', function(e) { sys.puts(e); }); } var cl = null; if(argv[2] == "client") cl = new xmpp.Client({ jid: argv[3], password: argv[4]}); else cl = new xmpp.Component({ jid: argv[3], password: argv[4], host: argv[5], port: argv[6] }); cl.on('online', function() { cl.send(new xmpp.Element('presence', { type: 'chat'}). c('show').t('chat').up(). c('status').t('Happily echoing your stanzas') ); }); cl.addMixin(echoMixin); cl.addMixin(errorMixin); astro-node-xmpp-43cc25c/examples/echo_server.js000066400000000000000000000004671172440453000216460ustar00rootroot00000000000000var xmpp = require('../lib/node-xmpp'); var r = new xmpp.Router(); r.register('codetu.be', function(stanza) { console.log("<< "+stanza.toString()); if (stanza.attrs.type !== 'error') { var me = stanza.attrs.to; stanza.attrs.to = stanza.attrs.from; stanza.attrs.from = me; r.send(stanza); } }); astro-node-xmpp-43cc25c/examples/probe_server.js000066400000000000000000000031151172440453000220300ustar00rootroot00000000000000/** * This example establishes some s2s connections over time. It is a * router test. You must modify it! */ var xmpp = require('../lib/node-xmpp'); var MY_JID = 'codetu.be'; var r = new xmpp.Router(); r.loadCredentials('codetu.be', 'codetube.key', 'codetube.crt'); r.register(MY_JID, function(stanza) { var time = Date.now(); var query; if (stanza.is('iq') && stanza.attrs.type == 'result' && (query = stanza.getChild('query', 'jabber:iq:version'))) { var name = query.getChildText('name'); var version = query.getChildText('version'); var os = query.getChildText('os'); console.log(time + " >> " + stanza.attrs.from + " " + [name, version, os].join('/')); } else if (stanza.is('iq') && stanza.attrs.type == 'error') { console.log(time + " !! " + stanza.attrs.from); } else { console.log(time + " ?? " + stanza.toString()); } }); process.on('SIGINT', function() { r.unregister(MY_JID); process.nextTick(function() { process.exit(0); }); }); var PROBE_DOMAINS = ["spaceboyz.net", "jabber.ccc.de", "gmail.com", "jabber.org", "jabbim.cz", "jabber.ru", "process-one.net", "gtalk2voip.com", "swissjabber.ch", "aspsms.swissjabber.ch", "icq.hq.c3d2.de", "codetu.be", "webkeks.org"]; function probe() { setTimeout(probe, Math.floor((Math.random() * 15 + 5) * 1000)); var to = PROBE_DOMAINS[Math.floor(Math.random() * PROBE_DOMAINS.length)]; r.send(new xmpp.Element('iq', { type: 'get', to: to, from: MY_JID }). c('query', { xmlns: 'jabber:iq:version' }) ); } probe(); astro-node-xmpp-43cc25c/examples/send_message.js000066400000000000000000000020141172440453000217650ustar00rootroot00000000000000var sys = require('sys'); var xmpp = require('../lib/node-xmpp'); var argv = process.argv; if (argv.length < 6) { sys.puts('Usage: node send_message.js [jid2] ... [jidN]'); process.exit(1); } var cl = new xmpp.Client({ jid: argv[2], password: argv[3] }); cl.addListener('online', function() { argv.slice(5).forEach( function(to) { cl.send(new xmpp.Element('message', { to: to, type: 'chat'}). c('body'). t(argv[4])); }); // nodejs has nothing left to do and will exit cl.end(); }); cl.addListener('error', function(e) { sys.puts(e); process.exit(1); }); astro-node-xmpp-43cc25c/examples/send_message_component.js000066400000000000000000000014701172440453000240540ustar00rootroot00000000000000var sys = require('sys'); var xmpp = require('../lib/node-xmpp'); var argv = process.argv; if (argv.length < 6) { sys.puts('Usage: node send_message.js [jid2] ... [jidN]'); process.exit(1); } var c = new xmpp.Component({ jid: argv[2], password: argv[3], host: argv[4], port: Number(argv[5]) }); c.addListener('online', function() { argv.slice(7).forEach( function(to) { c.send(new xmpp.Element('message', { to: to, from: c.jid, type: 'chat'}). c('body'). t(argv[6])); }); // nodejs has nothing left to do and will exit c.end(); }); c.addListener('error', function(e) { sys.puts(e); process.exit(1); }); astro-node-xmpp-43cc25c/lib/000077500000000000000000000000001172440453000157255ustar00rootroot00000000000000astro-node-xmpp-43cc25c/lib/idle_timeout.js000066400000000000000000000016601172440453000207510ustar00rootroot00000000000000/** * Emulates stream.setTimeout() behaviour, but respects outgoing data * too. * * @param {Number} timeout Milliseconds */ exports.attach = function(stream, timeout) { var timer; var emitTimeout = function() { timer = undefined; stream.emit('timeout'); }; var updateTimer = function() { if (timer) clearTimeout(timer); timer = setTimeout(emitTimeout, timeout); }; var oldWrite = stream.write; stream.write = function() { updateTimer(); oldWrite.apply(this, arguments); }; var clear = function() { if (timer) clearTimeout(timer); if (stream.write !== oldWrite) stream.write = oldWrite; delete stream.clearTimer; }; stream.clearTimer = clear; stream.addListener('data', updateTimer); stream.addListener('error', clear); stream.addListener('close', clear); stream.addListener('end', clear); }; astro-node-xmpp-43cc25c/lib/node-xmpp.js000066400000000000000000000012331172440453000201710ustar00rootroot00000000000000var Connection = require('./xmpp/connection'); var Client = require('./xmpp/client').Client; var Component = require('./xmpp/component').Component; var C2SServer = require('./xmpp/c2s').C2SServer; var JID = require('./xmpp/jid').JID; var Router = require('./xmpp/router'); var ltx = require('ltx'); var Stanza = require('./xmpp/stanza'); exports.Connection = Connection; exports.Client = Client; exports.Component = Component; exports.C2SServer = C2SServer; exports.JID = JID; exports.Element = ltx.Element; exports.Stanza = Stanza.Stanza; exports.Message = Stanza.Message; exports.Presence = Stanza.Presence; exports.Iq = Stanza.Iq; exports.Router = Router.Router; astro-node-xmpp-43cc25c/lib/starttls.js000066400000000000000000000027061172440453000201500ustar00rootroot00000000000000// Target API: // // var s = require('net').createStream(25, 'smtp.example.com'); // s.on('connect', function() { // require('starttls')(s, creds, false, function() { // if (!s.authorized) { // s.destroy(); // return; // } // // s.end("hello world\n"); // }); // }); var crypto = require('crypto'); var tls = require('tls'); module.exports = function starttls(socket, credentials, isServer, cb) { var pair = tls.createSecurePair(credentials, isServer, false, !isServer); var cleartext = pipe(pair, socket); pair.on('secure', function() { var ssl = pair._ssl || pair.ssl; var verifyError = ssl.verifyError(); if (verifyError) { cleartext.authorized = false; cleartext.authorizationError = verifyError; } else { cleartext.authorized = true; } if (cb) cb(); }); cleartext._controlReleased = true; return cleartext; }; function pipe(pair, socket) { pair.encrypted.pipe(socket); socket.pipe(pair.encrypted); pair.fd = socket.fd; var cleartext = pair.cleartext; cleartext.socket = socket; cleartext.encrypted = pair.encrypted; cleartext.authorized = false; function onerror(e) { if (cleartext._controlReleased) { cleartext.emit('error', e); } } function onclose() { socket.removeListener('error', onerror); socket.removeListener('close', onclose); } socket.on('error', onerror); socket.on('close', onclose); return cleartext; } astro-node-xmpp-43cc25c/lib/stream_shaper.js000066400000000000000000000015721172440453000211250ustar00rootroot00000000000000/** * This is extremely simple and unprecise. * * @param {Number} rateLimit B/ms or KB/s */ exports.attach = function(stream, rateLimit) { var timer; stream.rateLimit = rateLimit; // makes it readjustable after attachment stream.addListener('data', function(data) { if (timer) clearTimeout(timer); stream.pause(); var sleep = Math.floor(data.length / stream.rateLimit); timer = setTimeout(function() { timer = undefined; stream.resume(); }, sleep); }); stream.addListener('close', function() { // don't let the last timeout inhibit node shutdown if (timer) clearTimeout(timer); }); }; astro-node-xmpp-43cc25c/lib/xmpp.js000066400000000000000000000000511172440453000172430ustar00rootroot00000000000000module.exports = require('./node-xmpp'); astro-node-xmpp-43cc25c/lib/xmpp/000077500000000000000000000000001172440453000167115ustar00rootroot00000000000000astro-node-xmpp-43cc25c/lib/xmpp/c2s.js000066400000000000000000000202671172440453000177450ustar00rootroot00000000000000var EventEmitter = require('events').EventEmitter; var Connection = require('./connection'); var JID = require('./jid').JID; var ltx = require('ltx'); var util = require('util'); var crypto = require('crypto'); var SRV = require('./srv'); var net = require('net'); var sasl = require('./sasl'); var fs = require('fs'); var NS_CLIENT = 'jabber:client'; var NS_XMPP_SASL = 'urn:ietf:params:xml:ns:xmpp-sasl'; var NS_XMPP_TLS = 'urn:ietf:params:xml:ns:xmpp-tls'; var NS_REGISTER = 'jabber:iq:register'; var NS_SESSION = 'urn:ietf:params:xml:ns:xmpp-session'; var NS_BIND = 'urn:ietf:params:xml:ns:xmpp-bind'; /** * params: * options : port on which to listen to C2S connections */ function C2SServer(options) { var self = this; this.options = options; // And now start listening to connections on the port provided as an option. net.createServer(function (inStream) { self.acceptConnection(inStream); }).listen(options.port || 5222, options.domain || '::'); // Load TLS key material if (options.tls) { var key = fs.readFileSync(options.tls.keyPath, 'ascii'); var cert = fs.readFileSync(options.tls.certPath, 'ascii'); this.credentials = crypto.createCredentials({ key: key, cert: cert }); } } util.inherits(C2SServer, EventEmitter); exports.C2SServer = C2SServer; /** * Called upon TCP connection from client. */ C2SServer.prototype.acceptConnection = function(socket) { var client = new C2SStream(socket, this); this.emit("connect", client); socket.addListener('error', function() { }); client.addListener('error', function() { }); }; function C2SStream(socket, server) { var self = this; this.authenticated = false; this.server = server; Connection.Connection.call(this, socket); this.xmlns[''] = NS_CLIENT; this.xmppVersion = '1.0'; this.startParser(); this.addListener('streamStart', function(streamAttrs) { this.domain = streamAttrs.to; self.startStream(streamAttrs); }); this.addListener('rawStanza', function(stanza) { self.onRawStanza(stanza); }); this.socket.addListener('end', function() { self.emit('end'); }); return self; }; util.inherits(C2SStream, Connection.Connection); C2SStream.prototype.startStream = function(streamAttrs) { var attrs = {}; for(var k in this.xmlns) { if (this.xmlns.hasOwnProperty(k)) { if (!k) attrs.xmlns = this.xmlns[k]; else attrs['xmlns:' + k] = this.xmlns[k]; } } if (this.xmppVersion) attrs.version = this.xmppVersion; if (this.streamTo) attrs.to = this.streamTo; this.streamId = generateId(); attrs.id = this.streamId; attrs.from = this.server.options.domain; var el = new ltx.Element('stream:stream', attrs); // make it non-empty to cut the closing tag el.t(' '); var s = el.toString(); this.send(s.substr(0, s.indexOf(' '))); this.sendFeatures(); }; C2SStream.prototype.sendFeatures = function() { var features = new ltx.Element('stream:features'); if (!this.authenticated) { // TLS if (this.server.options.tls && !this.socket.encrypted) { features.c("starttls", {xmlns: NS_XMPP_TLS}).c("required"); } this.mechanisms = sasl.availableMechanisms(); var mechanismsEl = features.c("mechanisms", { xmlns: NS_XMPP_SASL}); this.mechanisms.forEach(function(mech) { mechanismsEl.c("mechanism").t(mech.name); }); } else { features.c("bind", {xmlns: NS_BIND}); features.c("session", {xmlns: NS_SESSION}); } this.send(features); }; C2SStream.prototype.onRawStanza = function(stanza) { var bind, session; if (this.jid) { stanza.attrs.from = this.jid.toString(); } if (stanza.is('starttls', NS_XMPP_TLS)) { this.send(new ltx.Element('proceed', { xmlns: Connection.NS_XMPP_TLS })); this.setSecure(this.server.credentials, true); } else if (stanza.is('auth', NS_XMPP_SASL)) { this.onAuth(stanza); } else if (stanza.is('iq') && stanza.getChild('query', NS_REGISTER)) { this.onRegistration(stanza); } else if (this.authenticated) { if (stanza.is('iq') && stanza.attrs.type == 'set' && (bind = stanza.getChild('bind', NS_BIND))) { this.onBind(stanza); } else if (stanza.is('iq') && stanza.attrs.type == 'set' && stanza.getChild('session', NS_SESSION)) { this.onSession(stanza); } else this.emit('stanza', stanza); } }; C2SStream.prototype.authenticate = function(username, password) { var self = this; var jid = new JID(username, this.domain ? this.domain.toString() : ""); this.emit('authenticate', { jid: jid, user: username, password: password, client: this }, function(error) { if (!error) { self.emit('auth-success', jid); self.jid = jid; self.authenticated = true; self.stopParser(); self.send(new ltx.Element("success", { xmlns: NS_XMPP_SASL })); self.startParser(); } else { self.emit('auth-failure', jid); self.send(new ltx.Element("failure", { xmlns: NS_XMPP_SASL }). c('text').t(error.message)); } }); }; C2SStream.prototype.onAuth = function(stanza) { var matchingMechs = this.mechanisms.filter(function(mech) { return mech.name === stanza.attrs.mechanism; }); if (matchingMechs[0]) { this.mechanism = matchingMechs[0]; this.mechanism.authServer(decode64(stanza.getText()), this); } else { this.send(new ltx.Element("failure", { xmlns: NS_XMPP_SASL })); // We're doomed. Not right auth mechanism offered. } }; C2SStream.prototype.onRegistration = function(stanza) { var self = this; var register = stanza.getChild('query', NS_REGISTER); var reply = new ltx.Element('iq', { type: 'result' }); if (stanza.attrs.id) reply.attrs.id = stanza.attrs.id; if (stanza.attrs.type === 'get') { reply.c('query', { xmlns: NS_REGISTER }). c("instructions").t("Choose a username and password for use with this service. ").up(). c("username").up(). c("password"); } else if (stanza.attrs.type === 'set') { var jid = new JID(register.getChildText('username'), this.server.options.domain) this.emit('register', { jid: jid, username: register.getChildText('username'), password: register.getChildText('password'), client: self }, function(error) { if (!error) { self.emit('registration-success', self.jid); } else { self.emit('registration-failure', jid); reply.attrs.type = "error"; reply.c("error", { code: '' + error.code, type: error.type }).c('text', { xmlns: "urn:ietf:params:xml:ns:xmpp-stanzas" }).t(error.message); } }); } self.send(reply); }; C2SStream.prototype.onBind = function(stanza) { var bind = stanza.getChild('bind', NS_BIND); var resource; if ((resource = bind.getChild("resource", NS_BIND))) { this.jid.setResource(resource.getText()); } else { this.jid.setResource(generateId()); } this.send(new ltx.Element("iq", { type:"result", id: stanza.attrs.id }). c("bind", { xmlns: NS_BIND }). c("jid").t(this.jid.toString()) ); }; C2SStream.prototype.onSession = function(stanza) { this.send(new ltx.Element("iq", { type:"result", id: stanza.attrs.id }). c("session", { xmlns: NS_SESSION}) ); this.emit('online'); }; function generateId() { var r = new Buffer(16); for(var i = 0; i < r.length; i++) { r[i] = 48 + Math.floor(Math.random() * 10); // '0'..'9' } return r.toString(); }; function decode64(encoded) { return (new Buffer(encoded, 'base64')).toString('utf8'); } function encode64(decoded) { return (new Buffer(decoded, 'utf8')).toString('base64'); } astro-node-xmpp-43cc25c/lib/xmpp/client.js000066400000000000000000000213031172440453000205240ustar00rootroot00000000000000var Connection = require('./connection'); var JID = require('./jid').JID; var ltx = require('ltx'); var sasl = require('./sasl'); var util = require('util'); var Buffer = require('buffer').Buffer; var SRV = require('./srv'); var NS_CLIENT = 'jabber:client'; var NS_REGISTER = 'jabber:iq:register'; var NS_XMPP_SASL = 'urn:ietf:params:xml:ns:xmpp-sasl'; var NS_XMPP_BIND = 'urn:ietf:params:xml:ns:xmpp-bind'; var NS_XMPP_SESSION = 'urn:ietf:params:xml:ns:xmpp-session'; var STATE_PREAUTH = 0, STATE_AUTH = 1, STATE_AUTHED = 2, STATE_BIND = 3, STATE_SESSION = 4, STATE_ONLINE = 5; var IQID_SESSION = 'sess', IQID_BIND = 'bind'; /** * params object: * jid: String (required) * password: String (required) * host: String (optional) * port: Number (optional) * reconnect: Boolean (optional) * register: Boolean (option) - register account before authentication * * Example: * var cl = new xmpp.Client({ * jid: "me@example.com", * password: "secret" * }); * var aceboo = new xmpp.Client({ * jid: '-' + fbUID + '@chat.facebook.com', * api_key: '54321', // api key of your facebook app * access_token: 'abcdefg', // user access token * host: 'chat.facebook.com', * }); */ function Client(params) { var self = this; Connection.Connection.call(this); if (typeof params.jid == 'string') this.jid = new JID(params.jid); else this.jid = params.jid; this.password = params.password; this.preferredSaslMechanism = params.preferredSaslMechanism; this.api_key = params.api_key; this.access_token = params.access_token; this.register = params.register; this.xmlns[''] = NS_CLIENT; this.xmppVersion = "1.0"; this.streamTo = this.jid.domain; this.addListener('rawStanza', this.onRawStanza); var connect = function() { self.state = STATE_PREAUTH; delete self.did_bind; delete self.did_session; if (params.host) { self.socket.connect(params.port || 5222, params.host); self.socket.on('connect', function() { self.startParser(); self.startStream(); }); } else { var attempt = SRV.connect(self.socket, ['_xmpp-client._tcp'], self.jid.domain, 5222); attempt.addListener('connect', function() { self.startParser(); self.startStream(); }); attempt.addListener('error', function(e) { self.emit('error', e); }); } }; if (params.reconnect) this.reconnect = connect; connect(); // it's us who must restart after starttls this.socket.addListener('secure', function() { self.startStream(); }); this.socket.addListener('end', function() { self.state = STATE_PREAUTH; self.emit('offline'); }); } util.inherits(Client, Connection.Connection); exports.Client = Client; Client.prototype.onRawStanza = function(stanza) { /* Actually, we shouldn't wait for if this.streamAttrs.version is missing, but who uses pre-XMPP-1.0 these days anyway? */ if (this.state != STATE_ONLINE && stanza.is('features', Connection.NS_STREAM)) { this.streamFeatures = stanza; this.useFeatures(); } else if (this.state == STATE_AUTH) { if (stanza.is('challenge', NS_XMPP_SASL)) { var challengeMsg = decode64(stanza.getText()); var responseMsg = encode64(this.mech.challenge(challengeMsg)); this.send(new ltx.Element('response', { xmlns: NS_XMPP_SASL }).t(responseMsg)); } else if (stanza.is('success', NS_XMPP_SASL)) { this.mech = null; this.state = STATE_AUTHED; this.startParser(); this.startStream(); } else { this.emit('error', 'XMPP authentication failure'); } } else if (this.state == STATE_BIND && stanza.is('iq') && stanza.attrs.id == IQID_BIND) { if (stanza.attrs.type == 'result') { this.state = STATE_AUTHED; this.did_bind = true; var bindEl = stanza.getChild('bind', NS_XMPP_BIND); if (bindEl && bindEl.getChild('jid')) { this.jid = new JID(bindEl.getChild('jid').getText()); } /* no stream restart, but next feature */ this.useFeatures(); } else { this.emit('error', 'Cannot bind resource'); } } else if (this.state == STATE_SESSION && stanza.is('iq') && stanza.attrs.id == IQID_SESSION) { if (stanza.attrs.type == 'result') { this.state = STATE_AUTHED; this.did_session = true; /* no stream restart, but next feature (most probably we'll go online next) */ this.useFeatures(); } else { this.emit('error', 'Cannot bind resource'); } } else if (stanza.name == 'stream:error') { this.emit('error', stanza); } else if (this.state == STATE_ONLINE) { this.emit('stanza', stanza); } }; /** * Either we just received , or we just enabled a * feature and are looking for the next. */ Client.prototype.useFeatures = function() { if (this.state == STATE_PREAUTH && this.register) { delete this.register; this.doRegister(); } else if (this.state == STATE_PREAUTH && this.streamFeatures.getChild('mechanisms', NS_XMPP_SASL)) { this.state = STATE_AUTH; var offeredMechs = this.streamFeatures. getChild('mechanisms', NS_XMPP_SASL). getChildren('mechanism', NS_XMPP_SASL). map(function(el) { return el.getText(); }); this.mech = sasl.selectMechanism(offeredMechs, this.preferredSaslMechanism); if (this.mech) { this.mech.authzid = this.jid.bare().toString(); this.mech.authcid = this.jid.user; this.mech.password = this.password; this.mech.api_key = this.api_key; this.mech.access_token = this.access_token; this.mech.realm = this.jid.domain; // anything? this.mech.digest_uri = "xmpp/" + this.jid.domain; var authMsg = encode64(this.mech.auth()); this.send(new ltx.Element('auth', { xmlns: NS_XMPP_SASL, mechanism: this.mech.name }).t(authMsg)); } else { this.emit('error', 'No usable SASL mechanism'); } } else if (this.state == STATE_AUTHED && !this.did_bind && this.streamFeatures.getChild('bind', NS_XMPP_BIND)) { this.state = STATE_BIND; var bindEl = new ltx.Element('iq', { type: 'set', id: IQID_BIND }).c('bind', { xmlns: NS_XMPP_BIND }); if (this.jid.resource) bindEl.c('resource').t(this.jid.resource); this.send(bindEl); } else if (this.state == STATE_AUTHED && !this.did_session && this.streamFeatures.getChild('session', NS_XMPP_SESSION)) { this.state = STATE_SESSION; this.send(new ltx.Element('iq', { type: 'set', to: this.jid.domain, id: IQID_SESSION }).c('session', { xmlns: NS_XMPP_SESSION })); } else if (this.state == STATE_AUTHED) { /* Ok, we're authenticated and all features have been processed */ this.state = STATE_ONLINE; this.emit('online'); } }; Client.prototype.doRegister = function() { var id = "register" + Math.ceil(Math.random() * 99999); var iq = new ltx.Element('iq', { type: 'set', id: id, to: this.jid.domain }). c('query', { xmlns: NS_REGISTER }). c('username').t(this.jid.user).up(). c('password').t(this.password); this.send(iq); var that = this; var onReply = function(reply) { if (reply.is('iq') && reply.attrs.id === id) { that.removeListener('rawStanza', onReply); if (reply.attrs.type === 'result') { /* Registration successful, proceed to auth */ that.useFeatures(); } else { that.emit('error', new Error("Registration error")); } } }; this.on('rawStanza', onReply); }; function decode64(encoded) { return (new Buffer(encoded, 'base64')).toString('utf8'); } function encode64(decoded) { return (new Buffer(decoded, 'utf8')).toString('base64'); } astro-node-xmpp-43cc25c/lib/xmpp/component.js000066400000000000000000000034621172440453000212560ustar00rootroot00000000000000var Connection = require('./connection'); var JID = require('./jid').JID; var ltx = require('ltx'); var util = require('util'); var crypto = require('crypto'); var SRV = require('./srv'); var NS_COMPONENT = 'jabber:component:accept'; /** * params: * jid: String (required) * password: String (required) * host: String (required) * port: Number (required) * reconnect: Boolean (optional) */ function Component(params) { var self = this; Connection.Connection.call(this); if (typeof params.jid == 'string') this.jid = new JID(params.jid); else this.jid = params.jid; this.password = params.password; this.xmlns[''] = NS_COMPONENT; this.streamTo = this.jid.domain; this.addListener('streamStart', function(streamAttrs) { self.onStreamStart(streamAttrs); }); this.addListener('rawStanza', function(stanza) { self.onRawStanza(stanza); }); var connect = function() { var attempt = SRV.connect(self.socket, [], params.host, params.port); attempt.addListener('connect', function() { self.startParser(); self.startStream(); }); attempt.addListener('error', function(e) { self.emit('error', e); }); }; if (params.reconnect) this.reconnect = connect; connect(); } util.inherits(Component, Connection.Connection); exports.Component = Component; Component.prototype.onStreamStart = function(streamAttrs) { var digest = sha1_hex(streamAttrs.id + this.password); this.send(new ltx.Element('handshake').t(digest)); }; Component.prototype.onRawStanza = function(stanza) { if (stanza.is('handshake')) { this.emit('online'); } else { this.emit('stanza', stanza); } }; function sha1_hex(s) { var hash = crypto.createHash('sha1'); hash.update(s); return hash.digest('hex'); } astro-node-xmpp-43cc25c/lib/xmpp/connection.js000066400000000000000000000235221172440453000214120ustar00rootroot00000000000000var net = require('net'); var EventEmitter = require('events').EventEmitter; var util = require('util'); var ltx = require('ltx'); var StreamParser = require('./stream_parser'); var starttls = require('../starttls'); var NS_XMPP_TLS = exports.NS_XMPP_TLS = 'urn:ietf:params:xml:ns:xmpp-tls'; var NS_STREAM = exports.NS_STREAM = 'http://etherx.jabber.org/streams'; var NS_XMPP_STREAMS = 'urn:ietf:params:xml:ns:xmpp-streams'; /** Base class for connection-based streams. The socket parameter is optional for incoming connections. A note on events: this base class will emit 'rawStanza' and leaves 'stanza' to Client & Component. Therefore we won't confuse the user with stanzas before authentication has finished. */ var MAX_RECONNECT_DELAY = 30 * 1000; function Connection(socket) { EventEmitter.call(this); this.charset = 'UTF-8'; this.xmlns = { stream: NS_STREAM }; this.socket = socket || new net.Socket(); this.reconnectDelay = 0; this.setupStream(); this.mixins = []; } util.inherits(Connection, EventEmitter); exports.Connection = Connection; // Defaults Connection.prototype.charset = 'UTF-8'; Connection.prototype.allowTLS = true; /** Used by both the constructor and by reinitialization in setSecure(). */ Connection.prototype.setupStream = function() { var self = this; this.reconnectDelay = 0; this.socket.addListener('data', function(data) { self.onData(data); }); this.socket.addListener('end', function() { self.onEnd(); }); this.socket.addListener('error', function() { /* unhandled errors may throw up in node, preventing a reconnect */ self.onEnd(); }); this.socket.addListener('close', function() { self.onClose(); }); var proxyEvent = function(event) { self.socket.addListener(event, function() { var args = Array.prototype.slice.call(arguments); args.unshift(event); self.emit.apply(self, args); }); }; proxyEvent('data'); // let them sniff unparsed XML proxyEvent('drain'); proxyEvent('close'); /** * This is optimized for continuous TCP streams. If your "socket" * actually transports frames (WebSockets) and you can't have * stanzas split across those, use: * cb(el.toString()); */ if (!this.socket.serializeStanza) { this.socket.serializeStanza = function(el, cb) { // Continuously write out el.write(function(s) { cb(s); }); }; } }; /** Climbs the stanza up if a child was passed, but you can send strings and buffers too. Returns whether the socket flushed data. */ Connection.prototype.send = function(stanza) { var self = this; var flushed = true; if (!this.socket.writable) { this.socket.end(); return; } if (stanza.root) { var el = this.rmStreamNs(stanza.root()); this.socket.serializeStanza(el, function(s) { flushed = self.socket.write(s); }); } else { flushed = this.socket.write(stanza); } return flushed; }; Connection.prototype.startParser = function() { var self = this; this.parser = new StreamParser.StreamParser(this.charset, this.maxStanzaSize); this.parser.addListener('start', function(attrs) { self.streamAttrs = attrs; /* We need those xmlns often, store them extra */ self.streamNsAttrs = {}; for(var k in attrs) { if (k == 'xmlns' || k.substr(0, 6) == 'xmlns:') self.streamNsAttrs[k] = attrs[k]; } /* Notify in case we don't wait for (Component or non-1.0 streams) */ self.emit('streamStart', attrs); }); this.parser.addListener('stanza', function(stanza) { self.onStanza(self.addStreamNs(stanza)); }); this.parser.addListener('error', function(e) { self.error(e.condition || 'internal-server-error', e.message); }); this.parser.addListener('end', function() { self.stopParser(); self.end(); }); }; Connection.prototype.stopParser = function() { /* No more events, please (may happen however) */ if(this.parser) { this.parser.stop(); /* Get GC'ed */ delete this.parser; } }; Connection.prototype.startStream = function() { var attrs = {}; for(var k in this.xmlns) { if (this.xmlns.hasOwnProperty(k)) { if (!k) attrs.xmlns = this.xmlns[k]; else attrs['xmlns:' + k] = this.xmlns[k]; } } if (this.xmppVersion) attrs.version = this.xmppVersion; if (this.streamTo) attrs.to = this.streamTo; if (this.streamId) attrs.id = this.streamId; var el = new ltx.Element('stream:stream', attrs); // make it non-empty to cut the closing tag el.t(' '); var s = el.toString(); this.send(s.substr(0, s.indexOf(' '))); this.streamOpened = true; }; Connection.prototype.onData = function(data) { if (this.parser) this.parser.write(data); }; Connection.prototype.setSecure = function(credentials, isServer) { var self = this; this.stopParser(); // Remove old event listeners this.socket.removeAllListeners('data'); // retain socket 'end' listeners because ssl layer doesn't support it this.socket.removeAllListeners('drain'); this.socket.removeAllListeners('close'); // remove idle_timeout if (this.socket.clearTimer) this.socket.clearTimer(); var ct = starttls(this.socket, credentials || this.credentials, isServer, function() { self.isSecure = true; if (!isServer) // Clients start , servers reply self.startStream(); self.startParser(); }); ct.on('close', function() { self.onClose(); }); // The socket is now the cleartext stream this.socket = ct; // Attach new listeners on the cleartext stream this.setupStream(); }; /** * This is not an event listener, but takes care of the TLS handshake * before 'rawStanza' events are emitted to the derived classes. */ Connection.prototype.onStanza = function(stanza) { if (stanza.is('error', NS_STREAM)) { /* TODO: extract error text */ this.emit('error', stanza); } else if (stanza.is('features', NS_STREAM) && this.allowTLS && !this.isSecure && stanza.getChild('starttls', NS_XMPP_TLS)) { /* Signal willingness to perform TLS handshake */ this.send(new ltx.Element('starttls', { xmlns: NS_XMPP_TLS })); } else if (this.allowTLS && stanza.is('proceed', NS_XMPP_TLS)) { /* Server is waiting for TLS handshake */ this.setSecure(); } else { this.emit('rawStanza', stanza); } }; /** * Add stream xmlns to a stanza * * Does not add our default xmlns as it is different for * C2S/S2S/Component connections. */ Connection.prototype.addStreamNs = function(stanza) { for(var attr in this.streamNsAttrs) { if (!stanza.attrs[attr] && !(attr === 'xmlns' && this.streamNsAttrs[attr] === this.xmlns[''])) stanza.attrs[attr] = this.streamNsAttrs[attr]; } return stanza; }; /** * Remove superfluous xmlns that were aleady declared in * our */ Connection.prototype.rmStreamNs = function(stanza) { for(var prefix in this.xmlns) { var attr = prefix ? 'xmlns:'+prefix : 'xmlns'; if (stanza.attrs[attr] == this.xmlns[prefix]) delete stanza.attrs[attr]; } return stanza; }; /** * Connection has been ended by remote, we will not get any incoming * 'data' events. Alternatively, used for 'error' event. */ Connection.prototype.onEnd = function() { this.stopParser(); this.socket.end(); }; /** * XMPP-style end connection for user */ Connection.prototype.end = function() { if (this.socket.writable) { if (this.streamOpened) { this.socket.write(''); delete this.streamOpened; /* wait for being called again upon 'end' from other side */ } else { this.socket.end(); } } }; Connection.prototype.onClose = function() { if (!this.socket) /* A reconnect may have already been scheduled */ return; delete this.socket; if (this.reconnect) { var self = this; setTimeout(function() { self.socket = new net.Stream(); self.setupStream(); self.reconnect(); }, this.reconnectDelay); console.log("Reconnect in", this.reconnectDelay); this.reconnectDelay += Math.ceil(Math.random() * 2000); if (this.reconnectDelay > MAX_RECONNECT_DELAY) this.reconnectDelay = MAX_RECONNECT_DELAY; } }; /** * End connection with stream error. * Emits 'error' event too. * * @param {String} condition XMPP error condition, see RFC3920 4.7.3. Defined Conditions * @param {String} text Optional error message */ Connection.prototype.error = function(condition, message) { this.emit('error', new Error(message)); if (!this.socket.writable) return; if(!this.streamOpened) this.startStream(); /* RFC 3920, 4.7.1 stream-level errors rules */ var e = new ltx.Element('stream:error'); e.c(condition, { xmlns: NS_XMPP_STREAMS }); if (message) e.c('text', { xmlns: NS_XMPP_STREAMS, 'xml:lang': 'en' }). t(message); this.send(e); this.end(); }; /** * Adds a mixin to this Connection for implementing higher-level functionality. * * A mixin is a module that exports a single function, taking the * Connection as its value. The module can perform its own stanza-handling, * event emission and other functionality. Note that mixins can only be * added once. */ Connection.prototype.addMixin = function(mixin, mixinArgs) { if(this.mixins.indexOf(mixin) == -1) { mixin(this, mixinArgs); this.mixins = this.mixins.concat(mixin); } }; astro-node-xmpp-43cc25c/lib/xmpp/jid.js000066400000000000000000000043761172440453000200270ustar00rootroot00000000000000try { var StringPrep = require('node-stringprep').StringPrep; var toUnicode = require('node-stringprep').toUnicode; var c = function(n) { var p = new StringPrep(n); return function(s) { return p.prepare(s); }; }; var nameprep = c('nameprep'); var nodeprep = c('nodeprep'); var resourceprep = c('resourceprep'); } catch(ex) { console.warn("Cannot load StringPrep-0.1.0 bindings. You may need to `npm install node-stringprep'"); var identity = function(a) { return a; }; var toUnicode = identity; var nameprep = identity; var nodeprep = identity; var resourceprep = identity; } function JID(a, b, c) { if (a && b == null && c == null) { this.parseJID(a); } else if (b) { this.setUser(a); this.setDomain(b); this.setResource(c); } else throw new Error('Argument error'); } JID.prototype.parseJID = function(s) { if (s.indexOf('@') >= 0) { this.setUser(s.substr(0, s.indexOf('@'))); s = s.substr(s.indexOf('@') + 1); } if (s.indexOf('/') >= 0) { this.setResource(s.substr(s.indexOf('/') + 1)); s = s.substr(0, s.indexOf('/')); } this.setDomain(s); }; JID.prototype.toString = function() { var s = this.domain; if (this.user) s = this.user + '@' + s; if (this.resource) s = s + '/' + this.resource; return s; }; /** * Convenience method to distinguish users **/ JID.prototype.bare = function() { if (this.resource) return new JID(this.user, this.domain, null); else return this; }; /** * Comparison function **/ JID.prototype.equals = function(other) { return this.user == other.user && this.domain == other.domain && this.resource == other.resource; }; /** * Setters that do stringprep normalization. **/ JID.prototype.setUser = function(user) { this.user = user && nodeprep(user); }; /** * http://xmpp.org/rfcs/rfc6122.html#addressing-domain */ JID.prototype.setDomain = function(domain) { this.domain = domain && nameprep(domain.split("."). map(toUnicode). join(".")); }; JID.prototype.setResource = function(resource) { this.resource = resource && resourceprep(resource); }; exports.JID = JID; astro-node-xmpp-43cc25c/lib/xmpp/router.js000066400000000000000000000406451172440453000206000ustar00rootroot00000000000000var net = require('net'); var Server = require('./server'); var JID = require('./jid'); var ltx = require('ltx'); var StreamShaper = require('./../stream_shaper'); var IdleTimeout = require('./../idle_timeout'); try { var StringPrep = require('node-stringprep').StringPrep; var c = function(n) { var p = new StringPrep(n); return function(s) { return p.prepare(s); }; }; var nameprep = c('nameprep'); } catch (ex) { var nameprep = function(a) { return a; }; } var NS_XMPP_SASL = 'urn:ietf:params:xml:ns:xmpp-sasl'; var NS_XMPP_STANZAS = 'urn:ietf:params:xml:ns:xmpp-stanzas'; /** * Represents a domain we host with connections to federated servers */ function DomainContext(router, domain) { this.router = router; this.domain = domain; this.s2sIn = {}; this.s2sOut = {}; } /** * Buffers until stream has been verified via Dialback */ DomainContext.prototype.send = function(stanza) { if (stanza.root) stanza = stanza.root(); // no destination? return to ourself if (!stanza.attrs.to) { // do not provoke ping-pong effects if (stanza.attrs.type === 'error') return; stanza.attrs.to = stanza.attrs.from; delete stanza.attrs.from; stanza.attrs.type = 'error'; stanza.c('error', { type: 'modify' }). c('jid-malformed', { xmlns: NS_XMPP_STANZAS }); this.receive(stanza); return; } var destDomain = new JID.JID(stanza.attrs.to).domain; var outStream = this.getOutStream(destDomain); if (outStream.isAuthed) outStream.send(stanza); else { // TODO: queues per domain in domaincontext outStream.queue = outStream.queue || []; outStream.queue.push(stanza); } }; /** * Does only buffer until stream is established, used for Dialback * communication itself. * * returns the stream */ DomainContext.prototype.sendRaw = function(stanza, destDomain) { if (stanza.root) stanza = stanza.root(); var outStream = this.getOutStream(destDomain); var send = function() { outStream.send(stanza); }; if (outStream.isConnected) send(); else outStream.addListener('online', send); return outStream; }; /** * Establish outgoing stream on demand */ DomainContext.prototype.getOutStream = function(destDomain) { var self = this; // unfortunately we cannot use the incoming streams if (!destDomain) { throw new Error('Trying to reach empty domain'); } else if (this.s2sOut.hasOwnProperty(destDomain)) { // There's one already return this.s2sOut[destDomain]; } else { var credentials = this.router.credentials[this.domain]; // Setup a new outgoing connection var outStream = new Server.OutgoingServer(this.domain, destDomain, credentials); this.s2sOut[destDomain] = outStream; this.router.setupStream(outStream); this.setupStream(destDomain, outStream); var closeCb = function() { // purge queue if (outStream.queue) { outStream.queue.forEach(function(stanza) { // do not provoke ping-pong effects if (stanza.attrs.type === 'error') return; var dest = stanza.attrs.to; stanza.attrs.to = stanza.attrs.from; stanza.attrs.from = dest; stanza.attrs.type = 'error'; stanza.c('error', { type: 'cancel' }). c('remote-server-not-found', { xmlns: NS_XMPP_STANZAS }); self.receive(stanza); }); } delete outStream.queue; // remove from DomainContext delete self.s2sOut[destDomain]; }; outStream.addListener('close', closeCb); outStream.addListener('error', closeCb); var onAuth = function(method) { outStream.isConnected = true; switch(method) { case 'dialback': self.startDialback(destDomain, outStream); break; case 'external': outStream.send(new ltx.Element('auth', { xmlns: NS_XMPP_SASL, mechanism: 'EXTERNAL' }). t(new Buffer(self.domain).toString('base64')) ); var onStanza; onStanza = function(stanza) { if (stanza.is('success', NS_XMPP_SASL)) { outStream.startParser(); outStream.startStream(); outStream.removeListener('stanza', onStanza); var onStream; onStream = function() { outStream.emit('online'); outStream.removeListener('streamStart', onStream); }; outStream.addListener('streamStart', onStream); } else if (stanza.is('failure', NS_XMPP_SASL)) outStream.end(); }; outStream.addListener('stanza', onStanza); break; default: outStream.error('undefined-condition', 'Cannot authenticate via ' + method); } outStream.removeListener('auth', onAuth); }; outStream.addListener('auth', onAuth); outStream.addListener('online', function() { outStream.isAuthed = true; if (outStream.queue) { outStream.queue.forEach(function(stanza) { outStream.send(stanza); }); delete outStream.queue; } }); return outStream; } }; /** * Called by router when verification is done */ DomainContext.prototype.addInStream = function(srcDomain, stream) { var self = this; if (this.s2sIn.hasOwnProperty(srcDomain)) { // Replace old var oldStream = this.s2sIn[srcDomain]; oldStream.error('conflict', 'Connection replaced'); delete self.s2sIn[srcDomain]; } this.setupStream(srcDomain, stream); stream.isConnected = true; stream.isAuthed = true; var closeCb = function() { if (self.s2sIn[srcDomain] == stream) delete self.s2sIn[srcDomain]; }; stream.addListener('close', closeCb); stream.addListener('error', closeCb); this.s2sIn[srcDomain] = stream; }; DomainContext.prototype.setupStream = function(domain, stream) { var self = this; stream.addListener('stanza', function(stanza) { // Before verified they can send whatever they want if (!stream.isAuthed) return; if (stanza.name !== 'message' && stanza.name !== 'presence' && stanza.name !== 'iq') // no normal stanza return; if (!(typeof stanza.attrs.from === 'string' && typeof stanza.attrs.to === 'string')) { stream.error('improper-addressing'); return; } // Only accept 'from' attribute JIDs that have the same domain // that we validated the stream for var fromDomain = (new JID.JID(stanza.attrs.from)).domain; if (fromDomain !== domain) { stream.error('invalid-from'); return; } // Only accept 'to' attribute JIDs to this DomainContext var toDomain = (new JID.JID(stanza.attrs.to)).domain; if (toDomain !== self.domain) { stream.error('improper-addressing'); return; } self.receive(stanza); }); }; // we want to get our outgoing connection verified, sends DomainContext.prototype.startDialback = function(destDomain, outStream) { outStream.dbKey = generateKey(); outStream.send(Server.dialbackKey(this.domain, destDomain, outStream.dbKey)); var self = this; var onResult = function(from, to, isValid) { if (from != destDomain || to != self.domain) // not for us return; outStream.removeListener('dialbackResult', onResult); if (isValid) { outStream.emit('online'); } else { // we cannot do anything else with this stream that // failed dialback outStream.end(); } }; outStream.addListener('dialbackResult', onResult); }; // incoming verification request for our outgoing connection that came // in via an inbound server connection DomainContext.prototype.verifyDialback = function(domain, id, key, cb) { var self = this; var outStream; if (this.s2sOut.hasOwnProperty(domain) && (outStream = this.s2sOut[domain])) { if (outStream.isConnected) { var isValid = outStream.streamAttrs.id === id && outStream.dbKey === key; cb(isValid); } else { // Not online, wait for outStream.streamAttrs // (they may have our stream header & dialback key, but // our slow connection hasn't received their stream // header) outStream.addListener('online', function() { // recurse self.verifyDialback(domain, id, key, cb); }); outStream.addListener('close', function() { cb(false); }); } } else cb(false); }; DomainContext.prototype.verifyIncoming = function(fromDomain, inStream, dbKey) { var self = this; var outStream = this.sendRaw(Server.dialbackVerify(this.domain, fromDomain, inStream.streamId, dbKey), fromDomain); // these are needed before for removeListener() var onVerified = function(from, to, id, isValid) { from = nameprep(from); to = nameprep(to); if (from !== fromDomain || to !== self.domain || id != inStream.streamId) // not for us return; // tell them about it inStream.send(Server.dialbackResult(to, from, isValid)); if (isValid) { // finally validated them! self.addInStream(from, inStream); } else { // the connection isn't used for another domain, so // closing is safe inStream.send(''); inStream.end(); } rmCbs(); }; var onClose = function() { // outgoing connection didn't work out, tell the incoming // connection inStream.send(Server.dialbackResult(self.domain, fromDomain, false)); rmCbs(); }; var onCloseIn = function() { // t'was the incoming stream that wanted to get // verified, nothing to do remains rmCbs(); }; var rmCbs = function() { outStream.removeListener('dialbackVerified', onVerified); outStream.removeListener('close', onClose); inStream.removeListener('close', onCloseIn); }; outStream.addListener('dialbackVerified', onVerified); outStream.addListener('close', onClose); inStream.addListener('close', onCloseIn); }; DomainContext.prototype.receive = function(stanza) { if (this.stanzaListener) this.stanzaListener(stanza); }; DomainContext.prototype.end = function() { var shutdown = function(conns) { for(var domain in conns) if (conns.hasOwnProperty(domain)) conns[domain].end(); }; shutdown(this.s2sOut); shutdown(this.s2sIn); }; /** * Accepts incoming S2S connections. Handles routing of outgoing * stanzas, and allows you to register a handler for your own domain. * * TODO: * * Incoming SASL EXTERNAL with certificate validation */ function Router(s2sPort, bindAddress) { var self = this; this.ctxs = {}; net.createServer(function(inStream) { self.acceptConnection(inStream); }).listen(s2sPort || 5269, bindAddress || '::'); } exports.Router = Router; // Defaults Router.prototype.rateLimit = 100; // 100 KB/s, it's S2S after all Router.prototype.maxStanzaSize = 65536; // 64 KB, by convention Router.prototype.keepAlive = 30 * 1000; // 30s Router.prototype.streamTimeout = 5 * 60 * 1000; // 5min Router.prototype.credentials = {}; // TLS credentials per domain // little helper, because dealing with crypto & fs gets unwieldy Router.prototype.loadCredentials = function(domain, keyPath, certPath) { var crypto = require('crypto'); var fs = require('fs'); var key = fs.readFileSync(keyPath, 'ascii'); var cert = fs.readFileSync(certPath, 'ascii'); var creds = crypto.createCredentials({ key: key, cert: cert }); this.credentials[domain] = creds; }; Router.prototype.acceptConnection = function(socket) { var self = this; var inStream = new Server.IncomingServer(socket, this.credentials); this.setupStream(inStream); // Unhandled 'error' events will trigger exceptions, don't let // that happen: socket.addListener('error', function() { }); inStream.addListener('error', function() { }); // incoming server wants to verify an outgoing connection of ours inStream.addListener('dialbackVerify', function(from, to, id, key) { from = nameprep(from); to = nameprep(to); if (self.hasContext(to)) { self.getContext(to).verifyDialback(from, id, key, function(isValid) { // look if this was a connection of ours inStream.send(Server.dialbackVerified(to, from, id, isValid)); }); } else // we don't host the 'to' domain inStream.send(Server.dialbackVerified(to, from, id, false)); }); // incoming connection wants to get verified inStream.addListener('dialbackKey', function(from, to, key) { from = nameprep(from); to = nameprep(to); if (self.hasContext(to)) { // trigger verification via outgoing connection self.getContext(to).verifyIncoming(from, inStream, key); } else { inStream.error('host-unknown', to + ' is not served here'); } }); }; Router.prototype.setupStream = function(stream) { stream.maxStanzaSize = this.maxStanzaSize; StreamShaper.attach(stream.socket, this.rateLimit); stream.socket.setKeepAlive(true, this.keepAlive); IdleTimeout.attach(stream.socket, this.streamTimeout); stream.socket.addListener('timeout', function() { stream.error('connection-timeout'); }); }; /** * Create domain context & register a stanza listener callback */ Router.prototype.register = function(domain, listener) { domain = nameprep(domain); this.getContext(domain).stanzaListener = listener; }; /** * Unregister a context and stop its connections */ Router.prototype.unregister = function(domain) { if (this.hasContext(domain)) { this.ctxs[domain].end(); delete this.ctxs[domain]; } }; Router.prototype.send = function(stanza) { if (stanza.root) stanza = stanza.root(); var to = stanza.attrs && stanza.attrs.to; var toDomain = to && (new JID.JID(to)).domain; if (toDomain && this.hasContext(toDomain)) { // inner routing this.getContext(toDomain).receive(stanza); } else if (stanza.attrs && stanza.attrs.from) { // route to domain context for s2s var domain = (new JID.JID(stanza.attrs.from)).domain; this.getContext(domain).send(stanza); } else throw new Error('Sending stanza from a domain we do not host'); }; Router.prototype.hasContext = function(domain) { return this.ctxs.hasOwnProperty(domain); }; Router.prototype.getContext = function(domain) { if (this.ctxs.hasOwnProperty(domain)) return this.ctxs[domain]; else return (this.ctxs[domain] = new DomainContext(this, domain)); }; /** * TODO: According to XEP-0185 we should hash from, to & streamId */ function generateKey() { var r = new Buffer(16); for(var i = 0; i < r.length; i++) { r[i] = 48 + Math.floor(Math.random() * 10); // '0'..'9' } return r.toString(); } astro-node-xmpp-43cc25c/lib/xmpp/sasl.js000066400000000000000000000145451172440453000202220ustar00rootroot00000000000000var crypto = require('crypto'); var querystring = require('querystring'); var util = require('util'); var EventEmitter = require('events').EventEmitter; /** * What's available for client-side authentication (Client) * * @param {Array} mechs Server-offered SASL mechanism names */ function selectMechanism(offeredMechs, preferredMech) { var mechClasses = [XFacebookPlatform, DigestMD5, Plain, Anonymous]; var byName = {}; var mech; mechClasses.forEach(function(mechClass) { byName[mechClass.prototype.name] = mechClass; }); /* Any preferred? */ if (byName[preferredMech]) { mech = byName[preferredMech]; } /* By priority */ mechClasses.forEach(function(mechClass) { if (!mech && offeredMechs.indexOf(mechClass.prototype.name) >= 0) mech = mechClass; }); return mech ? new mech() : null; } exports.selectMechanism = selectMechanism; /** * What's available for server-side authentication (C2S) */ function availableMechanisms() { return [new Plain()]; } exports.availableMechanisms = availableMechanisms; // Mechanisms function Mechanism() { EventEmitter.call(this); } util.inherits(Mechanism, EventEmitter); function Plain() { } util.inherits(Plain, Mechanism); Plain.prototype.name = "PLAIN"; Plain.prototype.auth = function() { return this.authzid + "\0" + this.authcid + "\0" + this.password; }; Plain.prototype.authServer = function(auth, client) { var params = auth.split("\x00"); this.username = params[1]; client.authenticate(this.username, params[2]); }; function XFacebookPlatform() { } util.inherits(XFacebookPlatform, Mechanism); XFacebookPlatform.prototype.name = "X-FACEBOOK-PLATFORM"; XFacebookPlatform.prototype.auth = function() { return ""; }; XFacebookPlatform.prototype.challenge = function(s) { var dict = querystring.parse(s); var response = { api_key: this.api_key, call_id: new Date().getTime(), method: dict.method, nonce: dict.nonce, access_token: this.access_token, v: "1.0" }; return querystring.stringify(response); }; function Anonymous() { } util.inherits(Anonymous, Mechanism); Anonymous.prototype.name = "ANONYMOUS"; Anonymous.prototype.auth = function() { return this.authzid; }; function DigestMD5() { this.nonce_count = 0; this.cnonce = generateNonce(); } util.inherits(DigestMD5, Mechanism); DigestMD5.prototype.name = "DIGEST-MD5"; DigestMD5.prototype.auth = function() { return ""; }; DigestMD5.prototype.getNC = function() { return rjust(this.nonce_count.toString(), 8, '0'); }; DigestMD5.prototype.responseValue = function(s) { var dict = parseDict(s); if (dict.realm) this.realm = dict.realm; var value; if (dict.nonce && dict.qop) { this.nonce_count++; var a1 = md5(this.authcid + ':' + this.realm + ':' + this.password) + ':' + dict.nonce + ':' + this.cnonce + ':' + this.authzid || ""; var a2 = "AUTHENTICATE:" + this.digest_uri; if (dict.qop == 'auth-int' || dict.qop == 'auth-conf') a2 += ":00000000000000000000000000000000"; value = md5_hex(md5_hex(a1) + ':' + dict.nonce + ':' + this.getNC() + ':' + this.cnonce + ':' + dict.qop + ':' + md5_hex(a2)); } return value; }; DigestMD5.prototype.challenge = function(s) { var dict = parseDict(s); if (dict.realm) this.realm = dict.realm; var response; if (dict.nonce && dict.qop) { var responseValue = this.responseValue(s); response = { username: this.authcid, realm: this.realm, nonce: dict.nonce, cnonce: this.cnonce, nc: this.getNC(), qop: dict.qop, 'digest-uri': this.digest_uri, response: responseValue, authzid: this.authzid || "", charset: 'utf-8' }; } else if (dict.rspauth) { return ""; } return encodeDict(response); }; DigestMD5.prototype.serverChallenge = function() { var dict = {}; dict.realm = ""; this.nonce = dict.nonce = generateNonce(); dict.qop = "auth"; this.charset = dict.charset = "utf-8"; dict.algorithm = "md5-sess"; return encodeDict(dict); }; // Used on the server to check for auth! DigestMD5.prototype.response = function(s) { var dict = parseDict(s); this.authcid = dict.username; if(dict.nonce != this.nonce) { return false; } if(!dict.cnonce) { return false; } this.cnonce = dict.cnonce; if(this.charset != dict.charset) { return false; } this.response = dict.response; return true; }; /** * Parse SASL serialization */ function parseDict(s) { var result = {}; while (s) { var m; if((m = /^(.+?)=(.*?[^\\]),(.*)/.exec(s))) { result[m[1]] = m[2].replace(/\"/g, ''); s = m[3]; } else if ((m = /^(.+?)=(.+?),(.*)/.exec(s))) { result[m[1]] = m[2]; s = m[3]; } else if ((m = /^(.+?)="(.*?[^\\])"$/.exec(s))) { result[m[1]] = m[2]; s = m[3]; } else if ((m = /^(.+?)=(.+?)$/.exec(s))) { result[m[1]] = m[2]; s = m[3]; } else { s = null; } } return result; } /** * SASL serialization */ function encodeDict(dict) { var s = ""; for(k in dict) { var v = dict[k]; if (v) s += ',' + k + '="' + v + '"'; } return s.substr(1); // without first ',' } /** * Right-justify a string, * eg. pad with 0s */ function rjust(s, targetLen, padding) { while(s.length < targetLen) s = padding + s; return s; } /** * Hash a string */ function md5(s, encoding) { var hash = crypto.createHash('md5'); hash.update(s); return hash.digest(encoding || 'binary'); } /** * Hash a string hexadecimally */ function md5_hex(s) { return md5(s, 'hex'); } /** * Generate a string of 8 digits * (number used once) */ function generateNonce() { var result = ""; for(var i = 0; i < 8; i++) result += String.fromCharCode(48 + Math.ceil(Math.random() * 10)); return result; } astro-node-xmpp-43cc25c/lib/xmpp/server.js000066400000000000000000000144051172440453000205610ustar00rootroot00000000000000var dns = require('dns'); var Connection = require('./connection'); var ltx = require('ltx'); var util = require('util'); var SRV = require('./srv'); var NS_SERVER = 'jabber:server'; var NS_DIALBACK = 'jabber:server:dialback'; var NS_XMPP_SASL = 'urn:ietf:params:xml:ns:xmpp-sasl'; /** * Dialback-specific events: * (1) dialbackKey(from, to, key) * (2) dialbackVerify(from, to, id, key) * (3) dialbackVerified(from, to, id, isValid) * (4) dialbackResult(from, to, isValid) */ function Server(socket) { var self = this; Connection.Connection.call(this, socket); this.xmlns[''] = NS_SERVER; this.xmlns['db'] = NS_DIALBACK; this.xmppVersion = '1.0'; this.addListener('rawStanza', function(stanza) { var key = stanza.getText(); if (stanza.is('result', NS_DIALBACK)) { if (stanza.attrs.from && stanza.attrs.to && stanza.attrs.type) { self.emit('dialbackResult', stanza.attrs.from, stanza.attrs.to, stanza.attrs.type == 'valid'); } else if (stanza.attrs.from && stanza.attrs.to) { self.emit('dialbackKey', stanza.attrs.from, stanza.attrs.to, key); } } else if (stanza.is('verify', NS_DIALBACK)) { if (stanza.attrs.from && stanza.attrs.to && stanza.attrs.id && stanza.attrs.type) { self.emit('dialbackVerified', stanza.attrs.from, stanza.attrs.to, stanza.attrs.id, stanza.attrs.type == 'valid'); } else if (stanza.attrs.from && stanza.attrs.to && stanza.attrs.id) { self.emit('dialbackVerify', stanza.attrs.from, stanza.attrs.to, stanza.attrs.id, key); } } else self.emit('stanza', stanza); }); } util.inherits(Server, Connection.Connection); exports.dialbackKey = function(from, to, key) { return new ltx.Element('db:result', { to: to, from: from }). t(key); }; exports.dialbackVerify = function(from, to, id, key) { return new ltx.Element('db:verify', { to: to, from: from, id: id }). t(key); }; exports.dialbackVerified = function(from, to, id, isValid) { return new ltx.Element('db:verify', { to: to, from: from, id: id, type: isValid ? 'valid' : 'invalid' }); }; exports.dialbackResult = function(from, to, isValid) { return new ltx.Element('db:result', { to: to, from: from, type: isValid ? 'valid' : 'invalid' }); }; exports.IncomingServer = function(stream, credentials) { var self = this; Server.call(this, stream); this.startParser(); this.streamId = generateId(); this.addListener('streamStart', function(streamAttrs) { if (streamAttrs.to && credentials && credentials.hasOwnProperty(streamAttrs.to)) // TLS cert & key for this domain self.credentials = credentials[streamAttrs.to]; // No credentials means we cannot on the server // side. Unfortunately this is required for XMPP 1.0. if (!self.credentials) delete self.xmppVersion; self.startStream(); }); this.addListener('rawStanza', function(stanza) { if (stanza.is('starttls', Connection.NS_XMPP_TLS)) { self.send(new ltx.Element('proceed', { xmlns: Connection.NS_XMPP_TLS })); self.setSecure(self.credentials, true); } }); return self; }; util.inherits(exports.IncomingServer, Server); exports.IncomingServer.prototype.startStream = function() { Server.prototype.startStream.call(this); if (this.xmppVersion == '1.0') { this.send(""); if (this.credentials && !this.isSecure) this.send(""); this.send(""); } }; exports.OutgoingServer = function(srcDomain, destDomain, credentials) { var self = this; Server.call(this); this.streamTo = destDomain; // For outgoing, we only need our own cert & key this.credentials = credentials; // No credentials means we cannot on the server // side. Unfortunately this is required for XMPP 1.0. if (!this.credentials) delete this.xmppVersion; this.socket.addListener('secure', function() { self.startStream(); }); this.addListener('streamStart', function(attrs) { if (attrs.version !== "1.0") // Don't wait for self.emit('auth', 'dialback'); }); self.addListener('rawStanza', function(stanza) { if (stanza.is('features', Connection.NS_STREAM)) { var mechsEl; if ((mechsEl = stanza.getChild('mechanisms', NS_XMPP_SASL))) { var mechs = mechsEl.getChildren('mechanism', NS_XMPP_SASL). map(function(el) { return el.getText(); }); if (mechs.indexOf('EXTERNAL') >= 0) self.emit('auth', 'external'); else self.emit('auth', 'dialback'); } else { // No SASL mechanisms self.emit('auth', 'dialback'); } } }); var attempt = SRV.connect(this.socket, ['_xmpp-server._tcp', '_jabber._tcp'], destDomain, 5269); attempt.addListener('connect', function() { self.startParser(); self.startStream(); }); attempt.addListener('error', function(e) { self.emit('error', e); }); }; util.inherits(exports.OutgoingServer, Server); function generateId() { var r = new Buffer(16); for(var i = 0; i < r.length; i++) { r[i] = 48 + Math.floor(Math.random() * 10); // '0'..'9' } return r.toString(); }; astro-node-xmpp-43cc25c/lib/xmpp/srv.js000066400000000000000000000105141172440453000200620ustar00rootroot00000000000000var dns = require('dns'); var EventEmitter = require('events').EventEmitter; function compareNumbers(a, b) { a = parseInt(a, 10); b = parseInt(b, 10); if (a < b) return -1; if (a > b) return 1; return 0; } function groupSrvRecords(addrs) { var groups = {}; // by priority addrs.forEach(function(addr) { if (!groups.hasOwnProperty(addr.priority)) groups[addr.priority] = []; groups[addr.priority].push(addr); }); var result = []; Object.keys(groups).sort(compareNumbers).forEach(function(priority) { var group = groups[priority]; var totalWeight = 0; group.forEach(function(addr) { totalWeight += addr.weight; }); var w = Math.floor(Math.random() * totalWeight); totalWeight = 0; var candidate = group[0]; group.forEach(function(addr) { totalWeight += addr.weight; if (w < totalWeight) candidate = addr; }); if (candidate) result.push(candidate); }); return result; } function resolveSrv(name, cb) { dns.resolveSrv(name, function(err, addrs) { if (err) { /* no SRV record, try domain as A */ cb(err); } else { var pending = 0, error, results = []; var cb1 = function(e, addrs1) { error = error || e; results = results.concat(addrs1); pending--; if (pending < 1) { cb(results ? null : error, results); } }; var gSRV = groupSrvRecords(addrs); pending = gSRV.length; gSRV.forEach(function(addr) { resolveHost(addr.name, function(e, a) { if (a) a = a.map(function(a1) { return { name: a1, port: addr.port }; }); cb1(e, a); }); }); } }); } // one of both A & AAAA, in case of broken tunnels function resolveHost(name, cb) { var error, results = []; var cb1 = function(e, addr) { error = error || e; if (addr) results.push(addr); cb((results.length > 0) ? null : error, results); }; dns.lookup(name, cb1); } // connection attempts to multiple addresses in a row function tryConnect(socket, addrs, listener) { var onConnect = function() { socket.removeListener('connect', onConnect); socket.removeListener('error', onError); // done! listener.emit('connect'); }; var error; var onError = function(e) { error = e; connectNext(); }; var connectNext = function() { var addr = addrs.shift(); if (addr) socket.connect(addr.port, addr.name); else { socket.removeListener('connect', onConnect); socket.removeListener('error', onError); listener.emit('error', error || new Error('No addresses to connect to')); } }; socket.addListener('connect', onConnect); socket.addListener('error', onError); connectNext(); } // returns EventEmitter with 'connect' & 'error' exports.connect = function(socket, services, domain, defaultPort) { var listener = new EventEmitter(); var tryServices = function() { var service = services.shift(); if (service) { resolveSrv(service + '.' + domain, function(error, addrs) { if (addrs) tryConnect(socket, addrs, listener); else tryServices(); }); } else { resolveHost(domain, function(error, addrs) { if (addrs && addrs.length > 0) { addrs = addrs.map(function(addr) { return { name: addr, port: defaultPort }; }); tryConnect(socket, addrs, listener); } else { listener.emit('error', error || new Error('No addresses resolved for ' + domain)); } }); } }; tryServices(); return listener; }; astro-node-xmpp-43cc25c/lib/xmpp/stanza.js000066400000000000000000000024711172440453000205530ustar00rootroot00000000000000var util = require('util'); var ltx = require('ltx'); function Stanza(name, attrs) { ltx.Element.call(this, name, attrs); } util.inherits(Stanza, ltx.Element); /** * Common attribute getters/setters for all stanzas */ Stanza.prototype.__defineGetter__('from', function() { return this.attrs.from; }); Stanza.prototype.__defineSetter__('from', function(from) { this.attrs.from = from; }); Stanza.prototype.__defineGetter__('to', function() { return this.attrs.to; }); Stanza.prototype.__defineSetter__('to', function(to) { this.attrs.to = to; }); Stanza.prototype.__defineGetter__('id', function() { return this.attrs.id; }); Stanza.prototype.__defineSetter__('id', function(id) { this.attrs.id = id; }); Stanza.prototype.__defineGetter__('type', function() { return this.attrs.type; }); Stanza.prototype.__defineSetter__('type', function(type) { this.attrs.type = type; }); /** * Stanza kinds */ function Message(attrs) { Stanza.call(this, 'message', attrs); } util.inherits(Message, Stanza); function Presence(attrs) { Stanza.call(this, 'presence', attrs); } util.inherits(Presence, Stanza); function Iq(attrs) { Stanza.call(this, 'iq', attrs); } util.inherits(Iq, Stanza); exports.Stanza = Stanza; exports.Message = Message; exports.Presence = Presence; exports.Iq = Iq; astro-node-xmpp-43cc25c/lib/xmpp/stream_parser.js000066400000000000000000000061731172440453000221250ustar00rootroot00000000000000var util = require('util'); var EventEmitter = require('events').EventEmitter; var expat = require('node-expat'); var ltx = require('ltx'); var Stanza = require('./stanza').Stanza; function StreamParser(charset, maxStanzaSize) { EventEmitter.call(this); var self = this; this.parser = new expat.Parser(charset); this.maxStanzaSize = maxStanzaSize; this.bytesParsedOnStanzaBegin = 0; this.parser.addListener('startElement', function(name, attrs) { // TODO: refuse anything but if (!self.element && name == 'stream:stream') { self.emit('start', attrs); } else { var child; if (!self.element) { /* A new stanza */ child = new Stanza(name, attrs); self.element = child; self.bytesParsedOnStanzaBegin = self.bytesParsed; } else { /* A child element of a stanza */ child = new ltx.Element(name, attrs); self.element = self.element.cnode(child); } } }); this.parser.addListener('endElement', function(name, attrs) { if (!self.element && name == 'stream:stream') { self.end(); } else if (self.element && name == self.element.name) { if (self.element.parent) self.element = self.element.parent; else { /* Stanza complete */ self.emit('stanza', self.element); delete self.element; delete self.bytesParsedOnStanzaBegin; } } else { self.error('xml-not-well-formed', 'XML parse error'); } }); this.parser.addListener('text', function(str) { if (self.element) self.element.t(str); }); this.parser.addListener('entityDecl', function() { /* Entity declarations are forbidden in XMPP. We must abort to * avoid a billion laughs. */ self.parser.stop(); self.error('xml-not-well-formed', 'No entity declarations allowed'); self.end(); }); } util.inherits(StreamParser, EventEmitter); exports.StreamParser = StreamParser; StreamParser.prototype.write = function(data) { if (this.parser) { if (this.bytesParsedOnStanzaBegin && this.maxStanzaSize && this.bytesParsed > this.bytesParsedOnStanzaBegin + this.maxStanzaSize) { this.error('policy-violation', 'Maximum stanza size exceeded'); return; } this.bytesParsed += data.length; if (!this.parser.parse(data, this.final ? true : false)) { this.error('xml-not-well-formed', 'XML parse error'); } } }; /* In case of connection restarts, we want no events from this parser anymore */ StreamParser.prototype.stop = function(data) { if(this.parser) { this.parser.stop(); } }; StreamParser.prototype.end = function(data) { if (data) { this.final = true; this.write(data); } delete this.parser; this.emit('end'); }; StreamParser.prototype.error = function(condition, message) { var e = new Error(message); e.condition = condition; this.emit('error', e); }; astro-node-xmpp-43cc25c/package.json000066400000000000000000000017721172440453000174540ustar00rootroot00000000000000{ "name": "node-xmpp" ,"version": "0.3.2" ,"main": "./lib/node-xmpp" ,"description": "Idiomatic XMPP client, component & server library for node.js" ,"author": "Stephan Maka" ,"dependencies": {"node-expat": ">=1.4.1" ,"ltx": ">= 0.1.1" } ,"devDependencies": {"node-stringprep": ">=0.1.0" ,"vows": ">=0.5.12" } ,"repositories": [{"type": "git" ,"path": "git://github.com/astro/node-xmpp.git" }] ,"homepage": "http://github.com/astro/node-xmpp" ,"bugs": "http://github.com/astro/node-xmpp/issues" ,"maintainers": [{"name": "Astro" ,"email": "astro@spaceboyz.net" ,"web": "http://spaceboyz.net/~astro/" }] ,"contributors": ["Stephan Maka", "Derek Hammer", "Daniel Zelisko", "Michael Geers", "Nolan Darilek", "Nathan Rajlich", "Dhruv Matani", "Camilo Aguilar", "Henry Chan", "Justin Campbell", "Trent Mick", "Alexey Shamrin", "Sonny Piers", "Chaitanya Gupta", "Иван", "Julien Genestoux"] ,"licenses": [{"type": "MIT"}] ,"engine": "node >=0.4.0" ,"scripts": {"test": "vows --spec"} } astro-node-xmpp-43cc25c/test/000077500000000000000000000000001172440453000161365ustar00rootroot00000000000000astro-node-xmpp-43cc25c/test/jid-test.js000066400000000000000000000060541172440453000202240ustar00rootroot00000000000000var vows = require('vows'), assert = require('assert'), xmpp = require('./../lib/xmpp'); vows.describe('JID').addBatch({ 'parsing': { 'parse a "domain" JID': function() { var j = new xmpp.JID('d'); assert.equal(j.user, null); assert.equal(j.domain, 'd'); assert.equal(j.resource, null); }, 'parse a "user@domain" JID': function() { var j = new xmpp.JID('u@d'); assert.equal(j.user, 'u'); assert.equal(j.domain, 'd'); assert.equal(j.resource, null); }, 'parse a "domain/resource" JID': function() { var j = new xmpp.JID('d/r'); assert.equal(j.user, null); assert.equal(j.domain, 'd'); assert.equal(j.resource, 'r'); }, 'parse a "user@domain/resource" JID': function() { var j = new xmpp.JID('u@d/r'); assert.equal(j.user, 'u'); assert.equal(j.domain, 'd'); assert.equal(j.resource, 'r'); }, 'parse an internationalized domain name as unicode': function() { var j = new xmpp.JID('öko.de'); assert.equal(j.domain, 'öko.de'); }, 'parse an internationalized domain name as ascii/punycode': function() { var j = new xmpp.JID('xn--ko-eka.de'); assert.equal(j.domain, 'öko.de'); }, 'parse a JID with punycode': function() { var j = new xmpp.JID('Сергей@xn--lsa92diaqnge.xn--p1ai'); assert.equal(j.user, 'сергей'); assert.equal(j.domain, 'приме́р.рф'); } }, 'serialization': { 'serialize a "domain" JID': function() { var j = new xmpp.JID(null, 'd'); assert.equal(j.toString(), 'd'); }, 'serialize a "user@domain" JID': function() { var j = new xmpp.JID('u', 'd'); assert.equal(j.toString(), 'u@d'); }, 'serialize a "domain/resource" JID': function() { var j = new xmpp.JID(null, 'd', 'r'); assert.equal(j.toString(), 'd/r'); }, 'serialize a "user@domain/resource" JID': function() { var j = new xmpp.JID('u', 'd', 'r'); assert.equal(j.toString(), 'u@d/r'); } }, 'equality': { 'parsed JIDs should be equal': function() { var j1 = new xmpp.JID('foo@bar/baz'); var j2 = new xmpp.JID('foo@bar/baz'); assert.equal(j1.equals(j2), true); }, 'parsed JIDs should be not equal': function() { var j1 = new xmpp.JID('foo@bar/baz'); var j2 = new xmpp.JID('quux@bar/baz'); assert.equal(j1.equals(j2), false); }, 'should ignore case in user': function() { var j1 = new xmpp.JID('foo@bar/baz'); var j2 = new xmpp.JID('FOO@bar/baz'); assert.equal(j1.equals(j2), true); }, 'should ignore case in domain': function() { var j1 = new xmpp.JID('foo@bar/baz'); var j2 = new xmpp.JID('foo@BAR/baz'); assert.equal(j1.equals(j2), true); }, 'should not ignore case in resource': function() { var j1 = new xmpp.JID('foo@bar/baz'); var j2 = new xmpp.JID('foo@bar/Baz'); assert.equal(j1.equals(j2), false); }, 'should ignore international caseness': function() { var j1 = new xmpp.JID('föö@bär/baß'); var j2 = new xmpp.JID('fÖö@BÄR/baß'); assert.equal(j1.equals(j2), true); } } }).export(module);