bot18
Version:
A high-frequency cryptocurrency trading bot by Zenbot creator @carlos8f
432 lines (393 loc) • 12.6 kB
JavaScript
var Session = require('./session')
var core = require('node-xmpp-core')
var JID = core.JID
var Stanza = core.Stanza
var Element = core.Element
var inherits = core.inherits
var sasl = require('./sasl')
var Anonymous = require('./authentication/anonymous')
var Plain = require('./authentication/plain')
var DigestMD5 = require('./authentication/digestmd5')
var XOAuth2 = require('./authentication/xoauth2')
var External = require('./authentication/external')
var exec = require('child_process').exec
var debug = require('debug')('xmpp:client')
var path = require('path')
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
var STATE_AUTH = 1
var STATE_AUTHED = 2
var STATE_BIND = 3
var STATE_SESSION = 4
var STATE_ONLINE = 5
var IQID_SESSION = 'sess'
var IQID_BIND = 'bind'
var decode64, encode64, Buffer
if (typeof btoa === 'undefined') {
var btoa = null
var atob = null
}
if (typeof btoa === 'function') {
decode64 = function (encoded) {
return atob(encoded)
}
} else {
Buffer = require('buffer').Buffer
decode64 = function (encoded) {
return (new Buffer(encoded, 'base64')).toString('utf8')
}
}
if (typeof atob === 'function') {
encode64 = function (decoded) {
return btoa(decoded)
}
} else {
Buffer = require('buffer').Buffer
encode64 = function (decoded) {
return (new Buffer(decoded, 'utf8')).toString('base64')
}
}
/**
* params object:
* jid: String (required)
* password: String (required)
* host: String (optional)
* port: Number (optional)
* reconnect: Boolean (optional)
* autostart: Boolean (optional) - if we start connecting to a given port
* register: Boolean (option) - register account before authentication
* legacySSL: Boolean (optional) - connect to the legacy SSL port, requires at least the host to be specified
* credentials: Dictionary (optional) - TLS or SSL key and certificate credentials
* actAs: String (optional) - if admin user act on behalf of another user (just user)
* disallowTLS: Boolean (optional) - prevent upgrading the connection to a secure one via TLS
* preferred: String (optional) - Preferred SASL mechanism to use
* bosh.url: String (optional) - BOSH endpoint to use
* bosh.prebind: Function(error, data) (optional) - Just prebind a new BOSH session for browser client use
* error String - Result of XMPP error. Ex : [Error: XMPP authentication failure]
* data Object - Result of XMPP BOSH connection.
*
* Examples:
* var cl = new xmpp.Client({
* jid: "me@example.com",
* password: "secret"
* })
* var gtalk = new xmpp.Client({
* jid: 'me@gmail.com',
* oauth2_token: 'xxxx.xxxxxxxxxxx', // from OAuth2
* oauth2_auth: 'http://www.google.com/talk/protocol/auth',
* host: 'talk.google.com'
* })
* var prebind = new xmpp.Client({
* jid: "me@example.com",
* password: "secret",
* bosh: {
* url: "http://example.com/http-bind",
* prebind: function(error, data) {
* if (error) {}
* res.send({ rid: data.rid, sid: data.sid })
* }
* }
* })
*
* Example SASL EXTERNAL:
*
* var myCredentials = {
* // These are necessary only if using the client certificate authentication
* key: fs.readFileSync('key.pem'),
* cert: fs.readFileSync('cert.pem'),
* // passphrase: 'optional'
* }
* var cl = new xmppClient({
* jid: "me@example.com",
* credentials: myCredentials
* preferred: 'EXTERNAL' // not really required, but possible
* })
*
*/
function Client (options) {
this.options = {}
if (options) this.options = options
this.availableSaslMechanisms = [
XOAuth2, External, DigestMD5, Plain, Anonymous
]
if (this.options.autostart !== false) this.connect()
}
inherits(Client, Session)
Client.NS_CLIENT = NS_CLIENT
Client.prototype.connect = function () {
if (this.options.bosh && this.options.bosh.prebind) {
return this._connectViaBosh()
}
this._useStandardConnect()
}
Client.prototype._useStandardConnect = function () {
this.options.xmlns = NS_CLIENT
delete this.did_bind
delete this.did_session
this.state = STATE_PREAUTH
this.on('end', function () {
this.state = STATE_PREAUTH
delete this.did_bind
delete this.did_session
})
Session.call(this, this.options)
this.options.jid = this.jid
this.connection.on('disconnect', function (error) {
this.state = STATE_PREAUTH
if (!this.connection.reconnect) {
if (error) this.emit('error', error)
this.emit('offline')
}
delete this.did_bind
delete this.did_session
}.bind(this))
/* If server and client have multiple possible auth mechanisms
* we try to select the preferred one
*/
if (this.options.preferred) {
this.preferredSaslMechanism = this.options.preferred
} else {
this.preferredSaslMechanism = 'DIGEST-MD5'
}
var mechs = sasl.detectMechanisms(this.options, this.availableSaslMechanisms)
this.availableSaslMechanisms = mechs
}
Client.prototype._connectViaBosh = function () {
debug('load bosh prebind')
var cb = this.options.bosh.prebind
delete this.options.bosh.prebind
var cmd = 'node ' + path.join(__dirname, 'prebind.js') + ' ' + encodeURI(JSON.stringify(this.options))
exec(
cmd,
function (error, stdout, stderr) {
if (error) {
cb(error, null)
} else {
var r = stdout.match(/rid:+[ 0-9]*/i)
var s = stdout.match(/sid:+[ a-z+'"-_A-Z+0-9]*/i)
if (!r || !s) {
return cb(stderr)
}
r = (r[0].split(':'))[1].trim()
s = (s[0].split(':'))[1]
.replace("'", '')
.replace("'", '')
.trim()
if (r && s) {
return cb(null, { rid: r, sid: s })
}
cb(stderr)
}
}
)
}
Client.prototype.onStanza = function (stanza) {
/* Actually, we shouldn't wait for <stream:features/> if
* this.streamAttrs.version is missing, but who uses pre-XMPP-1.0
* these days anyway?
*/
if (stanza.name === 'stream:error') {
return this._handleStreamError(stanza)
}
if ((this.state !== STATE_ONLINE) && stanza.is('features')) {
this.streamFeatures = stanza
return this.useFeatures()
}
this._handleStanza(stanza)
}
Client.prototype._handleStanza = function (stanza) {
switch (this.state) {
case STATE_ONLINE:
this.emit('stanza', stanza)
break
case STATE_PREAUTH:
this.emit('stanza:preauth', stanza)
break
case STATE_AUTH:
this._handleAuthState(stanza)
break
case STATE_BIND:
if (stanza.is('iq') && (stanza.attrs.id === IQID_BIND)) {
this._handleBindState(stanza)
}
break
case STATE_SESSION:
if ((stanza.is('iq') === true) && (stanza.attrs.id === IQID_SESSION)) {
this._handleSessionState(stanza)
}
break
}
}
Client.prototype._handleStreamError = function (stanza) {
if (!this.reconnect) {
this.emit('error', stanza)
}
}
Client.prototype._handleSessionState = function (stanza) {
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')
}
}
Client.prototype._handleBindState = function (stanza) {
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')
}
}
Client.prototype._handleAuthState = function (stanza) {
if (stanza.is('challenge', NS_XMPP_SASL)) {
var challengeMsg = decode64(stanza.getText())
var responseMsg = encode64(this.mech.challenge(challengeMsg))
var response = new Element('response', {xmlns: NS_XMPP_SASL}).t(responseMsg)
this.send(response)
} else if (stanza.is('success', NS_XMPP_SASL)) {
this.mech = null
this.state = STATE_AUTHED
this.emit('auth')
} else {
this.emit('error', 'XMPP authentication failure')
}
}
Client.prototype._handlePreAuthState = function () {
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,
this.availableSaslMechanisms
)
if (this.mech) {
this.mech.authzid = this.jid.bare().toString()
this.mech.authcid = this.jid.local
this.mech.password = this.password
this.mech.api_key = this.api_key
this.mech.access_token = this.access_token
this.mech.oauth2_token = this.oauth2_token
this.mech.oauth2_auth = this.oauth2_auth
this.mech.realm = this.jid.domain // anything?
if (this.actAs) this.mech.actAs = this.actAs.user
this.mech.digest_uri = 'xmpp/' + this.jid.domain
var authMsg = encode64(this.mech.auth())
var attrs = this.mech.authAttrs()
attrs.xmlns = NS_XMPP_SASL
attrs.mechanism = this.mech.name
this.send(new Element('auth', attrs).t(authMsg))
} else {
this.emit('error', new Error('No usable SASL mechanism'))
}
}
/**
* Either we just received <stream:features/>, 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._handlePreAuthState()
} else if ((this.state === STATE_AUTHED) &&
!this.did_bind &&
this.streamFeatures.getChild('bind', NS_XMPP_BIND)) {
this.state = STATE_BIND
var bindEl = new Stanza('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
var stanza = new Stanza('iq', {
type: 'set',
to: this.jid.domain,
id: IQID_SESSION
}).c('session', { xmlns: NS_XMPP_SESSION })
this.send(stanza)
} else if (this.state === STATE_AUTHED) {
/* Ok, we're authenticated and all features have been
processed */
this.state = STATE_ONLINE
this.emit('online', { jid: this.jid })
}
}
Client.prototype.doRegister = function () {
var id = 'register' + Math.ceil(Math.random() * 99999)
var iq = new Stanza('iq', {
type: 'set',
id: id,
to: this.jid.domain
}).c('query', {xmlns: NS_REGISTER})
.c('username').t(this.jid.local).up()
.c('password').t(this.password)
this.send(iq)
var self = this
var onReply = function (reply) {
if (reply.is('iq') && (reply.attrs.id === id)) {
self.removeListener('stanza', onReply)
if (reply.attrs.type === 'result') {
/* Registration successful, proceed to auth */
self.useFeatures()
} else {
self.emit('error', new Error('Registration error'))
}
}
}
this.on('stanza:preauth', onReply)
}
/**
* returns all registered sasl mechanisms
*/
Client.prototype.getSaslMechanisms = function () {
return this.availableSaslMechanisms
}
/**
* removes all registered sasl mechanisms
*/
Client.prototype.clearSaslMechanism = function () {
this.availableSaslMechanisms = []
}
/**
* register a new sasl mechanism
*/
Client.prototype.registerSaslMechanism = function (method) {
// check if method is registered
if (this.availableSaslMechanisms.indexOf(method) === -1) {
this.availableSaslMechanisms.push(method)
}
}
/**
* unregister an existing sasl mechanism
*/
Client.prototype.unregisterSaslMechanism = function (method) {
// check if method is registered
var index = this.availableSaslMechanisms.indexOf(method)
if (index >= 0) {
this.availableSaslMechanisms = this.availableSaslMechanisms.splice(index, 1)
}
}
module.exports = Client