diff --git a/lib/helpers.js b/lib/helpers.js index 394d277b..757d1e3a 100644 --- a/lib/helpers.js +++ b/lib/helpers.js @@ -1,5 +1,4 @@ var querystring = require('querystring'); -var request = require('request'); var endpoints = require('./endpoints'); @@ -80,49 +79,3 @@ exports.makeTwitError = function (message) { err.twitterReply = null return err } - -/** - * Get a bearer token for OAuth2 - * @param {String} consumer_key - * @param {String} consumer_secret - * @param {Function} cb - * - * Calls `cb` with Error, String - * - * Error (if it exists) is guaranteed to be Twit error-formatted. - * String (if it exists) is the bearer token received from Twitter. - */ -exports.getBearerToken = function (consumer_key, consumer_secret, cb) { - // use OAuth 2 for app-only auth (Twitter requires this) - // get a bearer token using our app's credentials - var b64Credentials = new Buffer(consumer_key + ':' + consumer_secret).toString('base64'); - request.post({ - url: endpoints.API_HOST + 'oauth2/token', - headers: { - 'Authorization': 'Basic ' + b64Credentials, - 'Content-Type': 'application/x-www-form-urlencoded;charset=UTF-8' - }, - body: 'grant_type=client_credentials', - json: true, - }, function (err, res, body) { - if (err) { - var error = exports.makeTwitError(err.toString()); - exports.attachBodyInfoToError(error, body); - return cb(error, body, res); - } - - if ( !body ) { - var error = exports.makeTwitError('Not valid reply from Twitter upon obtaining bearer token'); - exports.attachBodyInfoToError(error, body); - return cb(error, body, res); - } - - if (body.token_type !== 'bearer') { - var error = exports.makeTwitError('Unexpected reply from Twitter upon obtaining bearer token'); - exports.attachBodyInfoToError(error, body); - return cb(error, body, res); - } - - return cb(err, body.access_token); - }) -} diff --git a/lib/request.js b/lib/request.js new file mode 100644 index 00000000..bfe4556a --- /dev/null +++ b/lib/request.js @@ -0,0 +1,23 @@ +const request = require('request'); +const socks = require('socksv5'); + +module.exports = function (config) { + var proxyConfig = config.proxy; + + if (proxyConfig) { + return request.defaults({ + agentClass: socks.HttpsAgent, + agentOptions: { + proxyHost: proxyConfig.host, + proxyPort: proxyConfig.port, + auths: [ + proxyConfig.auth ? + socks.auth.UserPassword(proxyConfig.auth.username, proxyConfig.auth.password) : + socks.auth.None() + ] + } + }); + } + + return request; +}; diff --git a/lib/streaming-api-connection.js b/lib/streaming-api-connection.js index e05bff86..1faf3ca2 100644 --- a/lib/streaming-api-connection.js +++ b/lib/streaming-api-connection.js @@ -4,14 +4,13 @@ var util = require('util'); var helpers = require('./helpers') var Parser = require('./parser'); -var request = require('request'); +var request = require('./request'); var STATUS_CODES_TO_ABORT_ON = require('./settings').STATUS_CODES_TO_ABORT_ON -var StreamingAPIConnection = function (reqOpts, twitOptions) { - this.reqOpts = reqOpts - this.twitOptions = twitOptions +var StreamingAPIConnection = function (config) { this._twitter_time_minus_local_time_ms = 0 + this._request = request(config) EventEmitter.call(this) } @@ -66,7 +65,7 @@ StreamingAPIConnection.prototype._startPersistentConnection = function () { self._resetStallAbortTimeout(); self._setOauthTimestamp(); this.reqOpts.encoding = 'utf8' - self.request = request.post(this.reqOpts); + this.request = self._request.post(this.reqOpts); self.emit('connect', self.request); self.request.on('response', function (response) { self._updateOauthTimestampOffsetFromResponse(response) diff --git a/lib/twitter.js b/lib/twitter.js index 9540857e..9dd79ac0 100644 --- a/lib/twitter.js +++ b/lib/twitter.js @@ -3,7 +3,7 @@ // var assert = require('assert'); var Promise = require('bluebird'); -var request = require('request'); +var request = require('./request'); var util = require('util'); var endpoints = require('./endpoints'); var FileUploader = require('./file_uploader'); @@ -57,6 +57,7 @@ var Twitter = function (config) { this._validateConfigOrThrow(config); this.config = config; + this._request = request(config); this._twitter_time_minus_local_time_ms = 0; } @@ -104,23 +105,9 @@ Twitter.prototype.request = function (method, path, params, callback) { self._updateClockOffsetFromResponse(resp); var peerCertificate = resp && resp.socket && resp.socket.getPeerCertificate(); - if (self.config.trusted_cert_fingerprints && peerCertificate) { - if (!resp.socket.authorized) { - // The peer certificate was not signed by one of the authorized CA's. - var authErrMsg = resp.socket.authorizationError.toString(); - var err = helpers.makeTwitError('The peer certificate was not signed; ' + authErrMsg); - _returnErrorToUser(err); - return; - } - var fingerprint = peerCertificate.fingerprint; - var trustedFingerprints = self.config.trusted_cert_fingerprints; - if (trustedFingerprints.indexOf(fingerprint) === -1) { - var errMsg = util.format('Certificate untrusted. Trusted fingerprints are: %s. Got fingerprint: %s.', - trustedFingerprints.join(','), fingerprint); - var err = new Error(errMsg); - _returnErrorToUser(err); - return; - } + if (err) { + _returnErrorToUser(err); + return; } if (callback && typeof callback === 'function') { @@ -317,7 +304,8 @@ Twitter.prototype._buildReqOpts = function (method, path, params, isStreaming, c * @return {Undefined} */ Twitter.prototype._doRestApiRequest = function (reqOpts, twitOptions, method, callback) { - var request_method = request[method.toLowerCase()]; + var self = this; + var request_method = this._request[method.toLowerCase()]; var req = request_method(reqOpts); var body = ''; @@ -352,8 +340,28 @@ Twitter.prototype._doRestApiRequest = function (reqOpts, twitOptions, method, ca callback(err, body, response) } - req.on('response', function (res) { - response = res + req.on('response', function (resp) { + response = resp + + if (self.config.trusted_cert_fingerprints) { + if (!resp.socket.authorized) { + // The peer certificate was not signed by one of the authorized CA's. + var authErrMsg = resp.socket.authorizationError.toString(); + var err = helpers.makeTwitError('The peer certificate was not signed; ' + authErrMsg); + callback(err, body, response); + return; + } + var fingerprint = resp.socket.getPeerCertificate().fingerprint; + var trustedFingerprints = self.config.trusted_cert_fingerprints; + if (trustedFingerprints.indexOf(fingerprint) === -1) { + var errMsg = util.format('Certificate untrusted. Trusted fingerprints are: %s. Got fingerprint: %s.', + trustedFingerprints.join(','), fingerprint); + var err = new Error(errMsg); + callback(err, body, response); + return; + } + } + // read data from `request` object which contains the decompressed HTTP response body, // `response` is the unmodified http.IncomingMessage object which may contain compressed data req.on('data', function (chunk) { @@ -397,7 +405,7 @@ Twitter.prototype.stream = function (path, params) { var self = this; var twitOptions = (params && params.twit_options) || {}; - var streamingConnection = new StreamingAPIConnection() + var streamingConnection = new StreamingAPIConnection(this.config) self._buildReqOpts('POST', path, params, true, function (err, reqOpts) { if (err) { // we can get an error if we fail to obtain a bearer token or construct reqOpts @@ -429,7 +437,53 @@ Twitter.prototype._getBearerToken = function (callback) { return callback(null, self._bearerToken) } - helpers.getBearerToken(self.config.consumer_key, self.config.consumer_secret, + /** + * Get a bearer token for OAuth2 + * @param {String} consumer_key + * @param {String} consumer_secret + * @param {Function} cb + * + * Calls `cb` with Error, String + * + * Error (if it exists) is guaranteed to be Twit error-formatted. + * String (if it exists) is the bearer token received from Twitter. + */ + var getBearerToken = function (consumer_key, consumer_secret, cb) { + // use OAuth 2 for app-only auth (Twitter requires this) + // get a bearer token using our app's credentials + var b64Credentials = new Buffer(consumer_key + ':' + consumer_secret).toString('base64'); + request.post({ + url: endpoints.API_HOST + 'oauth2/token', + headers: { + 'Authorization': 'Basic ' + b64Credentials, + 'Content-Type': 'application/x-www-form-urlencoded;charset=UTF-8' + }, + body: 'grant_type=client_credentials', + json: true, + }, function (err, res, body) { + if (err) { + var error = exports.makeTwitError(err.toString()); + exports.attachBodyInfoToError(error, body); + return cb(error, body, res); + } + + if ( !body ) { + var error = exports.makeTwitError('Not valid reply from Twitter upon obtaining bearer token'); + exports.attachBodyInfoToError(error, body); + return cb(error, body, res); + } + + if (body.token_type !== 'bearer') { + var error = exports.makeTwitError('Unexpected reply from Twitter upon obtaining bearer token'); + exports.attachBodyInfoToError(error, body); + return cb(error, body, res); + } + + return cb(err, body.access_token); + }) + } + + getBearerToken(self.config.consumer_key, self.config.consumer_secret, function (err, bearerToken) { if (err) { // return the fully-qualified Twit Error object to caller diff --git a/package.json b/package.json index 4c430f43..1374b31c 100644 --- a/package.json +++ b/package.json @@ -14,7 +14,8 @@ "dependencies": { "bluebird": "^3.1.5", "mime": "^1.3.4", - "request": "^2.68.0" + "request": "^2.68.0", + "socksv5": "0.0.6" }, "devDependencies": { "async": "0.2.9", diff --git a/tests/rest.js b/tests/rest.js index 63fcbdbf..9ede5b80 100644 --- a/tests/rest.js +++ b/tests/rest.js @@ -640,9 +640,6 @@ describe('REST API', function () { assert(err.message.match(/token/)) assert(err.twitterReply) assert(err.allErrors) - assert(res) - assert(res.headers) - assert.equal(res.statusCode, 401) done() }) }) @@ -680,8 +677,7 @@ describe('REST API', function () { return fakeRequest } - var request = require('request') - var stubGet = sinon.stub(request, 'get', stubGet) + var stubGet = sinon.stub(twit._request, 'get', stubGet) twit.get('account/verify_credentials', function (err, reply, res) { assert(err === fakeError)