UNPKG

oscar

Version:

An OSCAR protocol module for node.js

1,399 lines (1,301 loc) 156 kB
var net = require('net'), util = require('util'), EventEmitter = require('events').EventEmitter, crypto = require('crypto'); var fnEmpty = function() {}; var debug = fnEmpty, hexy, inspectMutated = false, hexyFormat = { caps: 'upper', format: 'twos', numbering: 'none', groupSpacing: 2 }; function OscarConnection(options) { if (!(this instanceof OscarConnection)) return new OscarConnection(options); EventEmitter.call(this); this._options = { connection: { username: '', password: '', host: SERVER_AOL, port: 5190, connTimeout: 10000, // connection timeout in msecs allowMultiLogin: true, debug: false }, other: { initialStatus: USER_STATUSES.ONLINE, initialFlags: USER_FLAGS.DCDISABLED } }; this._options = extend(true, this._options, options); if (typeof this._options.connection.debug === 'function') { debug = this._options.connection.debug; if (!inspectMutated) { inspectMutated = true; hexy = require('./hexy').hexy; Buffer.prototype.inspect = function () { return hexy(this, hexyFormat); }; } } this._state = { connections: {}, serviceMap: {}, reqID: 0, // 32-bit number that identifies a single SNAC request status: this._options.other.initialStatus, flags: this._options.other.initialFlags, requests: {}, isAOL: (this._options.connection.host.substr(this._options.connection.host.length-7).toUpperCase() === 'AOL.COM'), rateLimitGroups: {}, rateLimits: {}, svcInfo: {}, svcPaused: {}, SSI: {}, iconQueue: {}, rndvCookies: { out: {}, in: {} }, chatrooms: {}, p2p: {} }; this.icon = { datetime: undefined, data: undefined }; // my 'buddy' icon this.me = undefined; this.contacts = { lastModified: undefined, list: undefined, permit: undefined, deny: undefined, prefs: undefined, _totalSSICount: 0, _usedIDs: {} }; } util.inherits(OscarConnection, EventEmitter); // setIdle only needs to be called once when you are idle and again when you are no longer idle. // The server will automatically increment the idle time for you, so don't call setIdle every second // or the evil AOL wizards will come for you! // Just kidding about the wizards, but it will make OSCAR do funny things. OscarConnection.prototype.setIdle = function(amount) { // amount is in seconds if an integer is supplied, false disables the idle state if (typeof amount === 'boolean' && !amount) amount = 0; else if (typeof amount !== 'number' || amount <= 0 || amount === true) throw new Error('Amount must be boolean false or a positive number > 0'); this._send(this._createFLAP(this._state.connections.main, FLAP_CHANNELS.SNAC, this._createSNAC(SNAC_SERVICES.GENERIC, 0x11, NO_FLAGS, [(amount >> 24 & 0xFF), (amount >> 16 & 0xFF), (amount >> 8 & 0xFF), (amount & 0xFF)] ) )); }; OscarConnection.prototype.sendIM = function(who, message, flags, cb) { var msgData, cookie, msgLen, self = this, features = (self._state.isAOL ? [0x01, 0x01, 0x01, 0x02] : [0x01]), featLen = features.length, charset = ICBM_MSG_CHARSETS.ASCII, isSMS; cb = arguments[arguments.length-1]; if (typeof flags !== 'number') flags = 0x00000000; if (typeof who === 'object') who = who.name; isSMS = /\+[\d]+/.test(who); if (isSMS && !self._state.isAOL) { var uin = parseInt(this._options.connection.username), len, data, req = str2bytes('<icq_sms_message><destination>' + who + '</destination><text>' + message + '</text>' + '<codepage>1252</codepage><senders_UIN>' + uin + '</senders_UIN>' + '<senders_name>' + this.me.fullname + '</senders_name>' + '<delivery_receipt>' + (typeof cb === 'function' ? 'Yes' : 'No') + '</delivery_receipt>' + '<time>' + (new Date).toUTCString() + '</time>' + '</icq_sms_message>'); data = splitNum(uin, 4).reverse().concat([ 0xD0, 0x07, (this._state.reqID & 0xFF) << 8, (this._state.reqID >> 8 & 0xFF), 0x82, 0x14, 0x00, 0x01, 0x00, 0x16, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, (req.length >> 8 & 0xFF), (req.length & 0xFF) ]).concat(req); data.push(0x00); len = data.length; data.unshift(len >> 8 & 0xFF); data.unshift(len & 0xFF); self._send(self._createFLAP(self._state.serviceMap[SNAC_SERVICES.ICQ_EXT], FLAP_CHANNELS.SNAC, self._createSNAC(SNAC_SERVICES.ICQ_EXT, 0x02, NO_FLAGS, self._createTLV(0x01, data) ) ), cb); return; } else if (isSMS && (flags & ICBM_MSG_FLAGS.OFFLINE)) flags -= ICBM_MSG_FLAGS.OFFLINE; cookie = splitNum(Date.now(), 4).concat(splitNum(Date.now()+1, 4)); who = str2bytes(who); if (who.length > MAX_SN_LEN) { var err = new Error('Screen names cannot be longer than ' + MAX_SN_LEN + ' characters'); if (typeof cb === 'function') cb(err); else throw err; return; } message = str2bytes(''+message); if (message.length > MAX_MSG_LEN) { // TODO: try stripping message of any HTML to see if it then fits within the length limit var err = new Error('IM messages cannot be longer than ' + MAX_MSG_LEN + ' characters'); if (typeof cb === 'function') cb(err); else throw err; return; } msgLen = message.length + 4; msgData = [0x05, 0x01, (featLen >> 8 & 0xFF), (featLen & 0xFF)] .concat(features) .concat([0x01, 0x01, (msgLen >> 8 & 0xFF), (msgLen & 0xFF), (charset >> 8 & 0xFF), (charset & 0xFF), 0x00, 0x00]); var content = cookie.concat([0x00, 0x01, who.length]) .concat(who) .concat(self._createTLV(0x02, msgData.concat(message))); if (flags & ICBM_MSG_FLAGS.AWAY) content = content.concat(self._createTLV(0x04)); else { // request that the server send us an ACK that the message was sent ok, // but not necessarily immediately received by the destination user (i.e. they are offline) if (typeof cb === 'function') content = content.concat(self._createTLV(0x03)); if (flags & ICBM_MSG_FLAGS.OFFLINE) content = content.concat(self._createTLV(0x06)); } if (flags & ICBM_MSG_FLAGS.REQ_ICON) content = content.concat(self._createTLV(0x09)); self._send(self._createFLAP(self._state.connections.main, FLAP_CHANNELS.SNAC, self._createSNAC(SNAC_SERVICES.ICBM, 0x06, NO_FLAGS, content ) ), cb); }; OscarConnection.prototype.setProfile = function(text) { var self = this, maxProfileLen = self._state.svcInfo[SNAC_SERVICES.LOCATION].maxProfileLen; if (text.length > maxProfileLen) throw new Error('Profile text cannot exceed max profile length of ' + maxProfileLen + ' characters'); self._send(self._createFLAP(self._state.connections.main, FLAP_CHANNELS.SNAC, self._createSNAC(SNAC_SERVICES.LOCATION, 0x04, NO_FLAGS, self._createTLV(0x01, str2bytes('text/aolrtf; charset="us-ascii"')) .concat(self._createTLV(0x02, str2bytes(text))) ) )); }; OscarConnection.prototype.warn = function(who, isAnonymous, cb) { isAnonymous = (typeof isAnonymous !== 'boolean' ? true : isAnonymous); cb = arguments[arguments.length-1]; if (self._state.isAOL) { var msgLen, self = this; who = str2bytes(''+who); if (who.length > MAX_SN_LEN) { var err = new Error('Screen names cannot be longer than ' + MAX_SN_LEN + ' characters'); if (typeof cb === 'function') cb(err); else throw err; return; } self._send(self._createFLAP(self._state.connections.main, FLAP_CHANNELS.SNAC, self._createSNAC(SNAC_SERVICES.ICBM, 0x08, NO_FLAGS, [0x00, (isAnonymous ? 0x01 : 0x00), who.length].concat(who) ) ), function(e, gain, newLevel) { if (typeof cb === 'function') cb(e, gain, newLevel); }); } else { var err = new Error('Warn feature only available on AOL'); if (typeof cb === 'function') cb(err); else throw err; } }; OscarConnection.prototype.notifyTyping = function(who, which) { var self = this; who = str2bytes(''+who); if (who.length > MAX_SN_LEN) throw new Error('Screen names cannot be longer than ' + MAX_SN_LEN + ' characters'); var notifyType = [0x00]; if (typeof which !== 'undefined') notifyType.push((which ? 0x02 : 0x00)); else notifyType.push(0x01); self._send(self._createFLAP(self._state.connections.main, FLAP_CHANNELS.SNAC, self._createSNAC(SNAC_SERVICES.ICBM, 0x14, NO_FLAGS, [0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x01, who.length] .concat(who) .concat(notifyType) ) )); }; OscarConnection.prototype.addContact = function(who, group, cb) { var self = this, check, err, record, skipTrans = (typeof arguments[arguments.length-1] === 'boolean'), contactCount = 0; cb = (typeof group === 'function' ? group : cb); for (var i=0,groups=Object.keys(self.contacts.list),len=groups.length; i<len; i++) contactCount += Object.keys(self.contacts.list[groups[i]].contacts).length; if (contactCount < self._state.svcInfo[SNAC_SERVICES.SSI].maxContacts) { record = (typeof who !== 'object' ? { name: '' } : who); if (typeof group !== 'function') record.group = group; if (typeof who === 'string') record.name = who; check = self._SSIFindContact(record.group, record.name); if (check[1] > -1) err = 'That contact already exists'; else if (check[0] === -1) err = 'Group not found'; } else err = 'Maximum number of contacts reached'; if (err) { err = new Error(err); if (typeof cb === 'function') cb(err); else throw err; return; } record.type = 0x00; record.group = check[0]; record.item = -1; if (!skipTrans) self._SSIStartTrans(); self._SSIModify(record, 0, function(e) { if (e) { if (!skipTrans) self._SSIEndTrans(); if (typeof cb === 'function') cb(e); return; } self.contacts.list[record.group].contacts[record.item] = record; self._SSIModify(self.contacts.list[record.group], 2, function(e) { if (!skipTrans) self._SSIEndTrans(); if (!e) self.contacts._usedIDs[record.group][record.item] = true; if (typeof cb === 'function') cb(e); }); }); }; OscarConnection.prototype.delContact = function(who, group, cb) { var self = this, check, record, skipTrans = (typeof arguments[arguments.length-1] === 'boolean'); cb = (typeof group === 'function' ? group : cb); record = (typeof who !== 'object' ? { name: '' } : who); record.group = (typeof group !== 'function' ? group : undefined); if (typeof who === 'string') record.name = who; check = self._SSIFindContact(record.group, record.name); if (check[1] === -1) { var err = new Error('That contact doesn\'t exists'); if (typeof cb === 'function') cb(err); else throw err; return; } record.type = 0x00; record.group = check[0]; record.item = check[1]; if (!skipTrans) self._SSIStartTrans(); self._SSIModify(record, 1, function(e) { if (e) { if (!skipTrans) self._SSIEndTrans(); if (typeof cb === 'function') cb(e); return; } delete self.contacts.list[record.group].contacts[record.item]; self._SSIModify(self.contacts.list[record.group], 2, function(e) { if (!skipTrans) self._SSIEndTrans(); if (!e) delete self.contacts._usedIDs[record.group][record.item]; if (typeof cb === 'function') cb(e); }); }); }; OscarConnection.prototype.moveContact = function(who, newGroup, cb) { var self = this, check, err, record; cb = arguments[arguments.length-1]; if (typeof who !== 'object') record = { name: '' }; if (typeof who === 'string') record.name = who; check = self._SSIFindContact(record.group, record.name); newGroup = self._SSIFindGroup(newGroup); if (check[1] === -1) err = 'That contact doesn\'t exist'; else if (newGroup === -1) err = 'Destination group not found'; if (err) { err = new Error(err); if (typeof cb === 'function') cb(err); else throw err; return; } var contact = self.contacts.list[check[0]].contacts[check[1]]; self.delContact(contact, function(e) { if (e) { self._SSIEndTrans(); if (typeof cb === 'function') cb(e); return; } self.addContact(contact, newGroup, function(e) { self._SSIEndTrans(); cb(e); }, true); }, true); }; OscarConnection.prototype.addGroup = function(group, cb) { var self = this, check, record, err; if (Object.keys(self.contacts.list).length < self._state.svcInfo[SNAC_SERVICES.SSI].maxGroups) { record = (typeof group !== 'object' ? { name: '' } : group); record.item = 0x00; record.type = 0x01; if (typeof group === 'string') record.name = group; check = self._SSIFindGroup(record.name); if (check > -1) err = 'That group already exists'; } else err = 'Maximum number of groups reached'; if (err) { err = new Error(err); if (typeof cb === 'function') cb(err); else throw err; return; } record.group = -1; self._SSIStartTrans(); self._SSIModify(record, 0, function(e) { if (e) { self._SSIEndTrans(); if (typeof cb === 'function') cb(e); return; } record.contacts = {}; self.contacts.list[record.group] = record; self._SSIModify(SSI_ROOT_GROUP, 2, function(e) { self._SSIEndTrans(); if (!e) self.contacts._usedIDs[record.group] = { 0: true }; if (typeof cb === 'function') cb(e); }); }); }; OscarConnection.prototype.delGroup = function(group, force, cb) { var self = this, check, err, record; cb = arguments[arguments.length-1]; if (typeof force !== 'boolean') force = false; record = (typeof group !== 'object' ? { name: '' } : group); record.item = 0x00; record.type = 0x01; if (typeof group === 'string') record.name = group; check = self._SSIFindGroup(record.name); if (check === -1) err = 'That group doesn\'t exist'; else if (!force && Object.keys(self.contacts.list[check].contacts).length > 0) err = 'That group isn\'t empty'; if (err) { err = new Error(err); if (typeof cb === 'function') cb(err); else throw err; return; } record.group = check; self._SSIStartTrans(); var fnContinue = function() { self._SSIModify(record, 1, function(e) { if (e) { self._SSIEndTrans(); if (typeof cb === 'function') cb(e); return; } delete self.contacts.list[record.group]; self._SSIModify(SSI_ROOT_GROUP, 2, function(e) { self._SSIEndTrans(); if (!e) delete self.contacts._usedIDs[record.group]; if (typeof cb === 'function') cb(e); }); }); }; var children = Object.keys(self.contacts.list[record.group].contacts), chlen = children.length; if (force && chlen > 0) { var contacts = []; for (var i=0; i<chlen; i++) contacts.push(self.contacts.list[record.group].contacts[children[i]]); self._SSIModify(contacts, 1, function(e) { if (e) { self._SSIEndTrans(); if (typeof cb === 'function') cb(e); return; } fnContinue(); }); } else fnContinue(); }; OscarConnection.prototype.renameGroup = function(group, newName, cb) { var self = this, check, err, record; record = (typeof group !== 'object' ? { name: '' } : group); record.item = 0x00; record.type = 0x01; if (typeof group === 'string') record.name = group; check = self._SSIFindGroup(record.name); if (check === -1) err = 'The source group doesn\'t exist'; else if (self._SSIFindGroup(newName) > -1) err = 'The destination group already exists'; if (err) { err = new Error(err); if (typeof cb === 'function') cb(err); else throw err; return; } record.group = check; record.name = newName; self._SSIStartTrans(); self._SSIModify(record, 2, function(e) { self._SSIEndTrans(); if (!e) self.contacts.list[record.group].name = newName; if (typeof cb === 'function') cb(e); }); }; OscarConnection.prototype.getInfo = function(who, cb) { // requests profile, away msg, capabilities this._send(this._createFLAP(this._state.connections.main, FLAP_CHANNELS.SNAC, this._createSNAC(SNAC_SERVICES.LOCATION, 0x15, NO_FLAGS, [0x00, 0x00, 0x00, 0x07, who.length] .concat(str2bytes(who)) ) ), cb); }; OscarConnection.prototype.addDeny = function(who, cb) { // TODO }; OscarConnection.prototype.delDeny = function(who) { // TODO }; OscarConnection.prototype.addPermit = function(who, cb) { // TODO }; OscarConnection.prototype.delPermit = function(who) { // TODO }; OscarConnection.prototype.getIcon = function(who, metaData, cb) { var icons, self = this, fnGetBest = function(list) { var idx; for (var i=0,best=0; i<list.length; i++) { if (list[i].type > best) { best = list[i].type; idx = i; } else if (typeof idx === 'undefined') idx = i; } process.nextTick(function(){ self._downloadIcons((typeof who === 'string' ? who : who.name), list[idx], cb); }); }; cb = arguments[arguments.length-1]; if (typeof metaData !== 'function') icons = metaData; if (typeof who === 'string') { var where = self._SSIFindContact(who); if (where[1] === -1 && !metaData) { self.getInfo(who, function(e, info) { if (e || !info.icons) { if (typeof cb === 'function') cb(e); return; } fnGetBest(info.icons); }); return; } else if (where[1] > -1) icons = self.contacts.list[where[0]].contacts[where[1]].icons; } else if (typeof who === 'object') icons = who.icons; if (!icons) { if (typeof cb === 'function') cb(); // user has no icon set } else fnGetBest(icons); }; OscarConnection.prototype.getOfflineMsgs = function() { this._send(this._createFLAP(this._state.connections.main, FLAP_CHANNELS.SNAC, this._createSNAC(SNAC_SERVICES.ICBM, 0x10, NO_FLAGS ) )); }; OscarConnection.prototype.joinChat = function(name, cb) { var self = this; if (!self._state.chatrooms[name]) { var exchange = 4; this._send(this._createFLAP(self._state.serviceMap[SNAC_SERVICES.CHAT_NAV], FLAP_CHANNELS.SNAC, this._createSNAC(SNAC_SERVICES.CHAT_NAV, 0x08, NO_FLAGS, [(exchange >> 8) & 0xFF, exchange & 0xFF, 0x06] .concat(str2bytes('create')).concat([0xFF, 0xFF, 0x01, 0x00, 0x03]) .concat(this._createTLV(0xD3, name)) .concat(this._createTLV(0xD6, 'us-ascii')) .concat(this._createTLV(0xD7, 'en')) ) ), function(err, roomInfo) { // CHAT_NAV 0x09 activates this callback if (err) { cb(err); return; } this._addService(SNAC_SERVICES.CHAT, roomInfo, function(err, conn) { if (err) { cb(err); return; } cb(); }); }); } else cb(new Error('You are already in that chat room')); }; OscarConnection.prototype.inviteChat = function(name, msg, who) { if (this._state.chatrooms[name]) { if (typeof who === 'undefined') { who = msg; msg = 'Please join me in this chat'; } return this._chatInvite(who, msg, this._state.chatrooms[name].roomInfo); } else return false; }; OscarConnection.prototype.sendChatMsg = function(name, text) { if (this._state.chatrooms[name]) { var conn = this._state.chatrooms[name], cookie = splitNum(Date.now(), 4).concat(splitNum(Date.now()+1, 4)); if (text.length > conn.roomInfo.maxMsgLen) return false; this._send(conn, this._createFLAP(conn, FLAP_CHANNELS.SNAC, this._createSNAC(SNAC_SERVICES.CHAT, 0x05, NO_FLAGS, cookie.concat([0x00, 0x03]) // channel .concat(this._createTLV(0x01)) // send to entire chat room .concat(this._createTLV(0x06)) // send us our own message .concat(this._createTLV(0x05, this._createTLV(0x02, 'us-ascii') .concat(this._createTLV(0x03, 'en')) .concat(this._createTLV(0x01, str2bytes(text))) )) ) )); return true; } else return false; }; OscarConnection.prototype.leaveChat = function(name) { if (this._state.chatrooms[name]) { this._state.chatrooms[name].destroy(); delete this._state.chatrooms[name]; return true; } else return false; }; OscarConnection.prototype.connect = function(cb) { var self = this; self._addConnection('login', null, self._options.connection.host, self._options.connection.port, function(e) { if (self._state.connections.main) { self._state.connections.main.authCookie = undefined; self._state.connections.main.rateLimitGroups = undefined; } if (e) { if (typeof cb === 'function') cb(e); return; } self._addService(SNAC_SERVICES.BART, function(e) { if (e) { if (typeof cb === 'function') cb(e); return; } self._addService(SNAC_SERVICES.CHAT_NAV, cb); }); }); }; OscarConnection.prototype.end = function() { var ids = Object.keys(this._state.connections); for (var i=0,len=ids.length; i<len; ++i) this._state.connections[ids[i]].end(); this._resetState(); }; // Connection handlers ------------------------------------------------------------------------------- function connect_handler(oscar) { var self = oscar, conn = this; conn.availServices = {}; conn.isConnected = true; clearTimeout(conn.tmrConn); conn.restartKeepAlive(); if (conn.isTransferring) { if (conn.id === 'login') { self._state.connections.main = self._state.connections.login; delete self._state.connections.login; conn.id = 'main'; } conn.serverType = 'BOS'; conn.isTransferring = false; } debug('(' + conn.remoteAddress + ') Connected to ' + conn.serverType + ' server'); conn.write(''); } function data_handler(oscar, data, cb) { var self = oscar, conn = this; //debug('(' + conn.remoteAddress + ') RECEIVED: \n' + util.inspect(data)) + '\n'; if (conn.curData) conn.curData = bufferAppend(conn.curData, data); else conn.curData = data; data = conn.curData; if (data[0] === 0x2A) { switch (data[1]) { case FLAP_CHANNELS.CONN_NEW: // new connection negotiation debug('(' + conn.remoteAddress + ') RECEIVED FLAP type: New connection negotiation'); if (conn.serverType === 'login') { self._send(conn, self._createFLAP(conn, FLAP_CHANNELS.CONN_NEW, [0x00, 0x00, 0x00, 0x01])); // send FLAP protocol version self._login(undefined, conn, cb); } else self._login(undefined, conn, cb, 2); conn.curData = undefined; break; case FLAP_CHANNELS.SNAC: // SNAC response var payloadLen = (data[4] << 8) + data[5]; if (6+payloadLen > data.length) return; else conn.curData = undefined; //debug('(' + conn.remoteAddress + ') RECEIVED FLAP type: SNAC response'); self._parseSNAC(conn, data.slice(6, 6+payloadLen), cb); if (data.length > 6 + payloadLen) { // extra bytes -- start of another FLAP message? var extra = new Buffer(data.length - (6 + payloadLen)); data.copy(extra, 0, 6 + payloadLen); process.nextTick(function() { conn.emit('data', extra); }); } break; case FLAP_CHANNELS.ERROR: // FLAP-level error debug('(' + conn.remoteAddress + ') RECEIVED FLAP type: FLAP error'); conn.curData = undefined; break; case FLAP_CHANNELS.CONN_CLOSE: // close connection negotiation debug('(' + conn.remoteAddress + ') RECEIVED FLAP type: Close connection negotiation'); if (conn.serverType === 'BOS') { var tlvs = extractTLVs(data, 6); if (tlvs[TLV_TYPES.ERROR]) { var code = (tlvs[TLV_TYPES.ERROR][0] << 8) + tlvs[TLV_TYPES.ERROR][1]; var err = new Error(AUTH_ERRORS_TEXT[code]); err.code = code; if (typeof cb === 'function') cb(err); else throw err; return; } else if (tlvs[TLV_TYPES.BOS_SERVER]) self._login(undefined, conn, cb, 1, tlvs[TLV_TYPES.BOS_SERVER].toString(), tlvs[TLV_TYPES.AUTH_COOKIE]); } conn.curData = undefined; break; case FLAP_CHANNELS.KEEPALIVE: // keep alive debug('(' + conn.remoteAddress + ') RECEIVED FLAP type: Keep-alive'); conn.curData = undefined; break; default: debug('(' + conn.remoteAddress + ') RECEIVED FLAP type: UNKNOWN (0x' + data[1].toString(16) + ')'); conn.curData = undefined; } } else debug('(' + conn.remoteAddress + ') RECEIVED Non-FLAP message'); } function end_handler(oscar) { var self = oscar, conn = this; conn.isConnected = false; if (!conn.isTransferring) { if (conn === self._state.connections.main) { self._resetState(); self.emit('end'); } debug('(' + conn.remoteAddress + ') [' + getConnSvcNames(conn) + '] FIN packet received. Disconnecting...'); } } function error_handler(oscar, err, cb) { var self = oscar, conn = this; clearTimeout(conn.tmrConn); if (!conn.isConnected) cb(new Error('(' + conn.remoteAddress + ') Unable to connect to ' + conn.serverType + ' server.\n ' + err.stack)); if (conn.readyState === 'closed') conn.isConnected = false; debug('(' + conn.remoteAddress + ') ' + err); self.emit('error', err); } function close_handler(oscar, had_error) { var self = oscar, conn = this; conn.isConnected = false; if (!conn.isTransferring || had_error) { if (conn === self._state.connections.main) { self._resetState(); self.emit('close', had_error); } debug('(' + conn.remoteAddress + ') [' + getConnSvcNames(conn) + '] Connection forcefully closed.'); } } // Private methods ----------------------------------------------------------------------------------- OscarConnection.prototype._resetState = function() { this._state = { connections: {}, serviceMap: {}, reqID: 0, // 32-bit number that identifies a single SNAC request status: this._options.other.initialStatus, flags: this._options.other.initialFlags, requests: {}, isAOL: (this._options.connection.host.substr(this._options.connection.host.length-7).toUpperCase() === 'AOL.COM'), rateLimitGroups: {}, rateLimits: {}, svcInfo: {}, svcPaused: {}, SSI: {}, iconQueue: {}, rndvCookies: { out: {}, in: {} }, chatrooms: {}, p2p: {} }; }; OscarConnection.prototype._addConnection = function(id, services, host, port, cb) { var self = this; self._state.connections[id] = new net.Socket(); self._state.connections[id].id = id; self._state.connections[id].neededServices = services; self._state.connections[id].serverType = (id === 'login' ? 'login' : 'BOS'); self._state.connections[id].isTransferring = false; self._state.connections[id].isReady = false; self._state.connections[id].isConnected = false; self._state.connections[id].seqNum = 0; // 0x0000 to 0x7FFF, wraps to 0x0000 past 0x7FFF self._state.connections[id].fnTmrConn = function(cbConn) { cbConn(new Error('Connection timed out while connecting to ' + self._state.connections[id].serverType + ' server')); self._state.connections[id].destroy(); }; self._state.connections[id].tmrConn = setTimeout(self._state.connections[id].fnTmrConn, self._options.connection.connTimeout, cb); self._state.connections[id].setTimeout(0); self._state.connections[id].setKeepAlive(true); self._state.connections[id].restartKeepAlive = function() { var conn = this; if (conn.keepAliveTimer) clearTimeout(conn.keepAliveTimer); conn.keepAliveTimer = setInterval(function() { self._sendKeepAlive(conn); }, KEEPALIVE_INTERVAL); }; self._state.connections[id].on('connect', function() { connect_handler.call(this, self); }); self._state.connections[id].on('data', function(data) { data_handler.call(this, self, data, cb); }); self._state.connections[id].on('end', function() { end_handler.call(this, self); }); self._state.connections[id].on('error', function(err) { error_handler.call(this, self, err, cb); }); self._state.connections[id].on('close', function(had_error) { close_handler.call(this, self, had_error); }); self._state.connections[id].connect(port, host); } OscarConnection.prototype._addService = function(svc, roomInfo, cb) { var self = this; if (typeof cb === 'undefined') { cb = roomInfo; roomInfo = undefined; } if (svc === SNAC_SERVICES.CHAT || !self._state.serviceMap[svc]) { var content = [svc >> 8 & 0xFF, svc & 0xFF]; if (roomInfo) { content = content.concat(self._createTLV(0x01, [(roomInfo.exchange >> 8) & 0xFF, roomInfo.exchange & 0xFF, roomInfo.cookie.length].concat(roomInfo.cookie).concat([0x00, 0x00]) )); } self._send(self._createFLAP(self._state.connections.main, FLAP_CHANNELS.SNAC, self._createSNAC(SNAC_SERVICES.GENERIC, 0x04, NO_FLAGS, content ) ), function(e, address, cookie) { if (e) { if (typeof cb === 'function') cb(e); return; } var services = {}, server, port, id = Date.now(); if (address.indexOf(':') > -1) { server = address.substring(0, address.indexOf(':')); port = parseInt(address.substr(address.indexOf(':')+1)); } else { server = address; port = self._state.connections.main.remotePort; } services[svc] = true; self._addConnection(id, services, server, port, function(e) { self._state.connections[id].authCookie = undefined; self._state.connections[id].rateLimitGroups = undefined; if (!e && roomInfo) { self._state.chatrooms[roomInfo.name] = self._state.connections[id]; self._state.chatrooms[roomInfo.name].roomInfo = roomInfo; } if (typeof cb === 'function') process.nextTick(function(){ cb(e, self._state.connections[id]); }); }); self._state.connections[id].authCookie = cookie; }); } else if (typeof cb === 'function') process.nextTick(function(){ cb(); }); }; // action === 0 (add), 1 (delete), 2 (modify) OscarConnection.prototype._SSIModify = function(items, action, cb) { // TODO: check max (and min) name length, etc before hitting the server cb = arguments[arguments.length-1]; var err = '', exists, bytes = [], self = this; items = (Array.isArray(items) ? items : [items]); for (var i=0,ilen=items.length; i<ilen; i++) { bytes.push(items[i].name.length >> 8 & 0xFF); bytes.push(items[i].name.length & 0xFF); for (var j=0; j<items[i].name.length; j++) bytes.push(items[i].name.charCodeAt(j) & 0xFF); if (action === 0) { if (items[i].type === 0x01) { for (var j=0; j<=0x7FFF; j++) { if (typeof self.contacts._usedIDs[j] === 'undefined') { items[i].group = j; break; } } } else if (items[i].group === 0) { var groups = Object.keys(self.contacts._usedIDs), groupsLen = groups.length; for (var j=0,found; j<=0x7FFF; j++) { if (typeof self.contacts._usedIDs[j] === 'undefined') { found = false; for (var k=0; k<groupsLen; k++) { if (typeof self.contacts._usedIDs[groups[k]][j] !== 'undefined') found = true; } if (!found) { items[i].item = j; break; } } } } else { for (var j=0; j<=0x7FFF; j++) { if (typeof self.contacts._usedIDs[items[i].group][j] === 'undefined') { items[i].item = j; break; } } } } bytes.push(items[i].group >> 8 & 0xFF); bytes.push(items[i].group & 0xFF); bytes.push(items[i].item >> 8 & 0xFF); bytes.push(items[i].item & 0xFF); bytes.push(items[i].type >> 8 & 0xFF); bytes.push(items[i].type & 0xFF); var itemBytes = []; if (action !== 1) { if (items[i].type === 0x00) { if (items[i].localInfo) { if (items[i].localInfo.alias) itemBytes = itemBytes.concat(self._createTLV(0x0131, str2bytes(items[i].localInfo.alias))); if (items[i].localInfo.emailAddress) itemBytes = itemBytes.concat(self._createTLV(0x0137, str2bytes(items[i].localInfo.emailAddress))); if (items[i].localInfo.homePhoneNum) itemBytes = itemBytes.concat(self._createTLV(0x0138, str2bytes(items[i].localInfo.homePhoneNum))); if (items[i].localInfo.cellPhoneNum) itemBytes = itemBytes.concat(self._createTLV(0x0139, str2bytes(items[i].localInfo.cellPhoneNum))); if (items[i].localInfo.smsPhoneNum) itemBytes = itemBytes.concat(self._createTLV(0x013A, str2bytes(items[i].localInfo.smsPhoneNum))); if (items[i].localInfo.workPhoneNum) itemBytes = itemBytes.concat(self._createTLV(0x0158, str2bytes(items[i].localInfo.workPhoneNum))); if (items[i].localInfo.otherPhoneNum) itemBytes = itemBytes.concat(self._createTLV(0x0159, str2bytes(items[i].localInfo.otherPhoneNum))); if (items[i].localInfo.notes) itemBytes = itemBytes.concat(self._createTLV(0x013C, str2bytes(items[i].localInfo.notes))); } // TODO: support adding/modifying the alerts for this contact and check maxWatchers first } else if (items[i].type === 0x01) { if (action === 2) { var ids = [], children = (Object.keys(items[i].group > 0 ? this.contacts.list[items[i].group].contacts : this.contacts._usedIDs),len2 = children.length); for (var j=0; j<len2; j++) { if (items[i].group === 0 && children[j] === 0) continue; ids.push(children[j] >> 8 & 0xFF); ids.push(children[j] & 0xFF); } if (ids.length > 0) itemBytes = itemBytes.concat(self._createTLV(0x00C8, ids)); } } else if (items[i].tlvs) { for (var j=0,types=Object.keys(items[i].tlvs),len2=types.length; j<len2; j++) itemBytes = itemBytes.concat(self._createTLV(parseInt(types[j]), items[i].tlvs[types[j]])); } } bytes.push(itemBytes.length >> 8 & 0xFF); bytes.push(itemBytes.length & 0xFF); if (itemBytes.length > 0) bytes = bytes.concat(itemBytes); } var subtype = 0x08; if (action === 1) subtype = 0x0A; else if (action === 2) subtype = 0x09; self._send(self._createFLAP(self._state.connections.main, FLAP_CHANNELS.SNAC, self._createSNAC(SNAC_SERVICES.SSI, subtype, NO_FLAGS, bytes ) ), function(e) { if (typeof cb === 'function') cb(e); }); }; OscarConnection.prototype._SSIStartTrans = function() { var self = this; self._send(self._createFLAP(self._state.connections.main, FLAP_CHANNELS.SNAC, self._createSNAC(SNAC_SERVICES.SSI, 0x11, NO_FLAGS ) )); }; OscarConnection.prototype._SSIEndTrans = function() { var self = this; self._send(self._createFLAP(self._state.connections.main, FLAP_CHANNELS.SNAC, self._createSNAC(SNAC_SERVICES.SSI, 0x12, NO_FLAGS ) )); }; OscarConnection.prototype._SSIFindGroup = function(group) { var retVal = -1; if (typeof group === 'undefined' || group === null) retVal = -1; else if (typeof group === 'number') { if (typeof this.contacts.list[group] === 'undefined') retVal = -1; } else { group = (''+group).toUpperCase(); for (var i=0,groups=Object.keys(this.contacts.list),len=groups.length; i<len; i++) { if (this.contacts.list[groups[i]].name.toUpperCase() === group) { retVal = groups[i]; break; } } } return retVal; }; OscarConnection.prototype._SSIFindContact = function(group, contact) { var retVal = [-1, -1]; if (arguments.length === 1) { contact = arguments[0]; group = undefined; } group = this._SSIFindGroup(group); if (group > -1) retVal[0] = group; if (typeof contact === 'number') { if (typeof this.contacts.list[group].contacts[contact] !== 'undefined') retVal[1] = contact; } else { contact = (''+contact).toUpperCase().replace(/ /g, ''); for (var i=0,groups=Object.keys(this.contacts.list),glen=groups.length; i<glen; i++) { for (var j=0,contacts=Object.keys(this.contacts.list[groups[i]].contacts),len=contacts.length; j<len; j++) { if (this.contacts.list[groups[i]].contacts[contacts[j]].name.toUpperCase().replace(/ /g, '') === contact) { retVal[0] = groups[i]; retVal[1] = contacts[j]; break; } } } } return retVal; }; OscarConnection.prototype._send = function(conn, payload, cb) { var self = this, isSNAC, svc; if (!(conn instanceof net.Stream)) { cb = payload; payload = conn; } isSNAC = (payload[1] === FLAP_CHANNELS.SNAC); svc = (isSNAC ? (payload[6] << 8) + payload[7] : undefined); if (!(conn instanceof net.Stream) && svc !== 'undefined') conn = self._state.serviceMap[svc]; if (isSNAC) { if (Object.keys(conn.availServices).length > 0 && typeof conn.availServices[svc] === 'undefined') { var err = new Error('No available server supports the requested SNAC: 0x' + svc.toString(16)); if (typeof cb === 'function') cb(err); else throw err; return; } if (typeof cb === 'function') { // some requests don't expect a response var reqID = (payload[12] << 24) + (payload[13] << 16) + (payload[14] << 8) + payload[15]; // SNAC request ID this._state.requests[reqID] = cb; } } payload = new Buffer(payload); debug('(' + conn.remoteAddress + ') SENDING: \n' + util.inspect(payload) + '\n'); conn.write(payload); conn.restartKeepAlive(); }; OscarConnection.prototype._dispatch = function(reqID) { var cb = this._state.requests[reqID]; if (typeof cb === 'function') { this._state.requests[reqID] = undefined; cb.apply(this, Array.prototype.slice.call(arguments).slice(1)); } }; OscarConnection.prototype._incomingFile = function(data) { var fileInfo = {}, i = 0, len; fileInfo.subtype = (data[i++] << 8) + data[i++]; // 0x0001 === 'one file', 0x0002 === 'more than one file' fileInfo.numFiles = (data[i++] << 8) + data[i++]; fileInfo.totalSize = (data[i++] << 24) + (data[i++] << 16) + (data[i++] << 8) + data[i++]; fileInfo.filename = data.toString(i, i+(data.length-1)); // string is null-terminated return fileInfo; }; OscarConnection.prototype._incomingIcon = function(data) { var i = 0, hash = (data[i++] << 24) + (data[i++] << 16) + (data[i++] << 8) + data[i++], len = (data[i++] << 24) + (data[i++] << 16) + (data[i++] << 8) + data[i++], datetime = (data[i++] << 24) + (data[i++] << 16) + (data[i++] << 8) + data[i++]; return { hash: hash, datetime: datetime, data: data.slice(i, i+len) }; }; OscarConnection.prototype._incomingChat = function(data) { var roomInfo = {}, i = 0, len; roomInfo.exchange = (data[i++] << 8) + data[i++]; len = data[i++]; roomInfo.name = data.toString('utf8', i, i+len); i += len; roomInfo.instance = (data[i++] << 8) + data[i++]; return roomInfo; }; OscarConnection.prototype._incomingList = function(data) { var list = {}; for (var i=0,len=data.length,group,namelen,numContacts; i<len;) { namelen = (data[i++] << 8) + data[i++]; group = data.toString('utf8', i, i+namelen); list[group] = []; i += namelen; numContacts = (data[i++] << 8) + data[i++]; for (var j=0; j<numContacts; j++) { namelen = (data[i++] << 8) + data[i++]; list[group].push(data.toString('utf8', i, i+namelen)); i += namelen; } } return list; }; OscarConnection.prototype._calcIconSum = function(icon) { var sum = 0; // 16-bit if (Buffer.isBuffer(icon) || Array.isArray(icon)) { var iconLen = icon.length, i; for (i=0; i+1<iconLen; i+=2) sum += (icon[i+1] << 8) + icon[i]; if (i < iconLen) sum += icon[i]; sum = ((sum & 0xFFFF0000) >> 16) + (sum & 0x0000FFFF); } return sum; }; OscarConnection.prototype._sendIcon = function(who) { if (Buffer.isBuffer(this.icon.data) || Array.isArray(this.icon.data)) { if (this.icon.data.length <= MAX_ICON_LEN) { var content = [], cookie; who = str2bytes(who); cookie = splitNum(Date.now(), 4).concat(splitNum(Date.now()+1, 4)); content = content.concat(cookie).concat[0x00, 0x02, who.length].concat(who) .concat(this._createTLV(0x05, [0x00, 0x00].concat(cookie).concat(CAPABILITIES.BUDDY_ICON))); content = content.concat(this._createTLV(0x0A, [0x00, 0x01])); content = content.concat(this._createTLV(0x0F)); content = content.concat(this._createTLV(0x2711, [0x00].concat(splitNum(this._calcIconSum(this.icon.data), 2)) .concat(splitNum(this.icon.data.length, 4)) .concat(splitNum(this.icon.datetime, 4)) )); for (var i=0,len=this.icon.data.length; i<len; i++) content.push(this.icon.data[i]); content = content.concat(str2bytes('AVT1picture.id')); this._send(this._createFLAP(this._state.connections.main, FLAP_CHANNELS.SNAC, this._createSNAC(SNAC_SERVICES.ICBM, 0x06, NO_FLAGS, content ) )); } else debug('Uh oh, my icon exceeds the maximum icon size of ' + MAX_ICON_LEN + ' bytes. Cannot send it to: ' + who); } }; OscarConnection.prototype.sendFile = function(who, file, ip, port, cb) { }; OscarConnection.prototype._cancelRendezvous = function(inout, cookie, cb) { cookie = ''+cookie; var info = this._state.rndvCookies[inout][cookie]; if (typeof info !== 'undefined') { var content, type; cookie = info.cookie; if (info.type === 'chat') type = CAPABILITIES.CHAT; else if (info.type === 'file') type = CAPABILITIES.SEND_FILE; if (inout === 'out') { content = cookie.concat[0x00, 0x02, who.length].concat(who).concat(this._createTLV(0x03)) .concat(this._createTLV(0x05, [0x00, 0x01].concat(cookie).concat(type).concat(this._createTLV(0x0B)))); this._send(this._createFLAP(this._state.connections.main, FLAP_CHANNELS.SNAC, this._createSNAC(SNAC_SERVICES.ICBM, 0x06, NO_FLAGS, content ) ), cb); } else if (inout === 'in') { if (info.type === 'file') { } } delete this._state.rndvCookies[inout][cookie]; } }; OscarConnection.prototype._chatInvite = function(who, msg, roomInfo) { var content, cookie; who = str2bytes(who); msg = str2bytes(msg); name = str2bytes(roomInfo.name); exchange = splitNum(roomInfo.exchange, 2); instance = splitNum(roomInfo.instance, 2); cookie = splitNum(Date.now(), 4).concat(splitNum(Date.now()+1, 4)); this._state.rndvCookies.out[cookie.join('')] = { cookie: cookie, type: 'chat', user: who, name: name, exchange: exchange, instance: instance }; content = cookie.concat([0x00, 0x02, who.length]).concat(who) .concat(this._createTLV(0x05, [0x00, 0x00].concat(cookie).concat(CAPABILITIES.CHAT) .concat(this._createTLV(0x0A, [0x00, 0x01])).concat(this._createTLV(0x0F)) .concat(this._createTLV(0x0C, msg)) .concat(this._createTLV(0x2711, exchange.concat(name).concat(instance))))); this._send(this._createFLAP(this._state.connections.main, FLAP_CHANNELS.SNAC, this._createSNAC(SNAC_SERVICES.ICBM, 0x06, NO_FLAGS, content ) )); return cookie.join(''); }; OscarConnection.prototype._parseSNAC = function(conn, snac, cb) { //debug('(' + conn.remoteAddress + ') SNAC response follows:\n' + util.inspect(snac)); var serviceID, subtypeID, flags, reqID, tlvs, idx, isServerOrig, moreFollows, debugtext, self = this; serviceID = (snac[0] << 8) + snac[1]; subtypeID = (snac[2] << 8) + snac[3]; flags = (snac[4] << 8) + snac[5]; reqID = (snac[6] << 24) + (snac[7] << 16) + (snac[8] << 8) + snac[9]; isServerOrig = (flags < 0); // MSB === 1 indicates the SNAC is not the result of a client request, // (i.e. the server is asking/telling us something) moreFollows = (flags & 0x1); // At least one more packet of the same SNAC service and subtype (?) will come after this packet idx = 10; if (flags & 0x8000) { // apparently this means AOL has decided to prepend service version information for the fun of it, // so skip it to get to the real data idx += 2+((snac[idx] << 8) + snac[idx+1]); } debugtext = '(' + conn.remoteAddress + ') RECEIVED SNAC: '; switch (serviceID) { case SNAC_SERVICES.AUTH: debugtext += 'AUTH > '; switch (subtypeID) { case 0x03: debugtext += 'MD5 login reply'; tlvs = extractTLVs(snac); if (tlvs[TLV_TYPES.ERROR]) { var err = new Error(AUTH_ERRORS_TEXT[(tlvs[TLV_TYPES.ERROR][0] << 8) + tlvs[TLV_TYPES.ERROR][1]]); debugtext += ' (error: ' + err + ')'; if (typeof cb === 'function') cb(err); else throw err; } else { self._dispatch(reqID, undefined, tlvs[TLV_TYPES.BOS_SERVER].toString(), tlvs[TLV_TYPES.AUTH_COOKIE].toArray()); debugtext += ' (no error)'; } break; case 0x07: // md5 salt debugtext += 'MD5 key/salt'; var saltLen, salt; saltLen = (snac[idx++] << 8) + snac[idx++]; salt = snac.toString('utf8', idx, idx+saltLen); self._dispatch(reqID, undefined, salt); break; default: debugtext += 'Unknown (0x' + subtypeID.toString(16) + ')'; } break; case SNAC_SERVICES.GENERIC: debugtext += 'GENERIC > '; switch (subtypeID) { case 0x01: // error debugtext += 'Error'; var code = (snac[idx++] << 8) + snac[idx++], msg = GLOBAL_ERRORS_TEXT[code] || 'Unknown error code received: ' + code, err = new Error(msg); err.code = code; debugtext += ': ' + msg; self._dispatch(reqID, err); break; case 0x03: debugtext += 'Supported services: '; var debugAvail = [], services = flip(SNAC_SERVICES); for (var len=snac.length,svc; idx<len;) { svc = (snac[idx++] << 8) + snac[idx++]; conn.availServices[svc] = true; if (typeof services[svc] !== 'undefined') debugAvail.push(services[svc]); } debugtext += debugAvail.join(', '); self._login(undefined, conn, cb, 3); break; case 0x05: // redirect info for requested service debugtext += 'Service request info'; var services = flip(SNAC_SERVICES), useSSL; tlvs = extractTLVs(snac, idx); useSSL = (tlvs[0x8E] && tlvs[0x8E][0] === 0x01); if (typeof services[(tlvs[0x0D][0] << 8) + tlvs[0x0D][1]] !== 'undefined') debugtext += ' (' + services[(tlvs[0x0D][0] << 8) + tlvs[0x0D][1]] + ')'; else debugtext += ' (Unknown: ' + ((tlvs[0x0D][0] << 8) + tlvs[0x0D][1]).toString(16) + ')'; debugtext += '. Host: ' + tlvs[0x05].toString(); self._dispatch(reqID, undefined, tlvs[0x05].toString(), tlvs[0x06].toArray()); break; case 0x07: debugtext += 'Rate limit classes and groups'; var numGroups = 0, classId; if (typeof snac[idx] !== 'undefined') { numGroups = (snac[idx++] << 8) + snac[idx++]; for (var i=0, group; i<numGroups; i++) { classId = (snac[idx++] << 8) + snac[idx++]; group = { windowSize: (snac[idx++] << 24) + (snac[idx++] << 16) + (snac[idx++] << 8) + snac[idx++], levels: { clear: (snac[idx++] << 24) + (snac[idx++] << 16) + (snac[idx++] << 8) + snac[idx++], alert: (snac[idx++] << 24) + (snac[idx++] << 16) + (snac[idx++] << 8) + snac[idx++], limit: (snac[idx++] << 24) + (snac[idx++] << 16) + (snac[idx++] << 8) + snac[idx++], disconnect: (snac[idx++] << 24) + (snac[idx++] << 16) + (snac[idx++] << 8) + snac[idx++], current: (snac[idx++] << 24) + (snac[idx++] << 16) + (snac[idx++] << 8) + snac[idx++], max: (snac[idx++] << 24) + (snac[idx++] << 16) + (snac[idx++] << 8) + snac[idx++], }, delta: 0, droppingSNACs: false }; if (conn.availServices[serviceID] >= 3) { group.delta = (snac[idx++] << 24) + (snac[idx++] << 16) + (snac[idx++] << 8) + snac[idx++]; group.droppingSNACs = (snac[idx++] === 0x01 ? true : false); } if (!self._state.rateLimitGroups[classId]) self._state.rateLimitGroups[classId] = group; if (!conn.rateLimitGroups) conn.rateLimitGroups = []; if (conn.rateLimitGroups.indexOf(classId) === -1) conn.rateL