module.exports = Request var debug = require('debug')('snapchat:request') var https = require('https') var request = require('request') var urljoin = require('url-join') var extend = require('xtend') var zlib = require('zlib') var BufferUtils = require('./buffer-utils') var StringUtils = require('./string-utils') var constants = require('./constants') /** * Snapchat wrapper for HTTP requests * * @class * @param {Object} opts */ function Request (opts) { var self = this if (!(self instanceof Request)) return new Request(opts) if (!opts) opts = {} self.HTTPMethod = opts.method self.HTTPHeaders = {} self.opts = opts if (opts.method === 'POST') { if (opts.endpoint) { self._initPOST(opts) } else if (opts.url) { self._initPOSTURL(opts) } else { throw new Error('invalid request') } } else if (opts.method === 'GET') { self._initGET(opts) } else { throw new Error('invalid request') } } Request.prototype._initHeaderFields = function (headers) { var self = this self.HTTPHeaders[constants.headers.contentType] = 'application/x-www-form-urlencoded' self.HTTPHeaders[constants.headers.userAgent] = constants.core.userAgent self.HTTPHeaders[constants.headers.acceptLanguage] = constants.headers.values.language self.HTTPHeaders[constants.headers.acceptLocale] = constants.headers.values.locale if (headers) { self.HTTPHeaders = extend(self.HTTPHeaders, headers) } } /** * Automatically adds query parameters: * - timestamp * - req_token * Automatically adds HTTP header fields: * - User-Agent * - Accept-Language * - Accept-Locale * - Content-Type */ Request.prototype._initPOST = function (opts) { var self = this Request._applyHeaderOverrides(opts) opts.token = opts.token || constants.core.staticToken self._initHeaderFields(opts.headers) Request._applyOverrides(opts) self.URL = urljoin(constants.core.baseURL, opts.endpoint) if (!opts.timestamp) { opts.timestamp = StringUtils.timestamp() } var reqToken = 'req_token' if (!opts.params[reqToken]) { opts.params[reqToken] = StringUtils.hashSCString(opts.token, opts.timestamp) } if (!opts.params.timestamp) { opts.params.timestamp = +opts.timestamp } // special-case for uploading snaps if (opts.endpoint === constants.endpoints.snaps.upload) { var body = [ ] for (var key in opts.params) { if (key === 'data') { body.push(BufferUtils.boundaryForBuffer(key, opts.params[key])) } else { body.push(BufferUtils.boundaryForString(key, opts.params[key])) } } // final boundary body.push(new Buffer('\r\n--' + constants.core.boundary + '--\r\n')) // concat all buffers together to form the body self.HTTPBody = Buffer.concat(body) self.multipart = true // debugging... // var fs = require('fs') // fs.writeFileSync('out.txt', self.HTTPBody.toString('utf-8')) // console.log(self.HTTPBody.toString('utf-8')) } else { self.HTTPBody = opts.params } if (opts.endpoint === constants.endpoints.snaps.loadBlob || opts.endpoint === constants.endpoints.chat.media) { self.HTTPHeaders[constants.headers.timestamp] = opts.timestamp } } Request.prototype._initPOSTURL = function (opts) { var self = this self.URL = opts.url self.HTTPBody = opts.eventData } /** * Automatically adds HTTP header fields: * - User-Agent * - Accept-Language * - Accept-Locale * - Content-Type */ Request.prototype._initGET = function (opts) { var self = this Request._applyHeaderOverrides(opts) self._initHeaderFields(opts.headers) self.URL = urljoin(constants.core.baseURL, opts.endpoint) } /** * Initiates the underlying HTTP request Request.httpRequest. * * @param {function} cb */ Request.prototype.start = function (cb) { var self = this function wrapcb (err, response, body) { var contentType = response && response.headers && response.headers['content-type'] if (contentType && contentType !== 'application/octet-stream') { response.body = response.body.toString('utf8') } if (err) { return cb(err, body) } else if (!err && response && (response.statusCode < 200 || response.statusCode >= 300)) { // catch snapchat API error codes debug('Snapchat Request Error: %d (%s) \nendpoint: %s \nheaders: %j \nrequest: %s', response.statusCode, response.statusMessage, self.opts.endpoint, response.request.headers, self.multipart ? '' : response.request.body.toString('utf-8').substr(0, 80)) // console.log(response) // console.log(response.request.body.toString('base64')) // console.log(response.request.body.toString('utf-8')) return cb('Snapchat API error ' + response.statusCode + ' (' + response.statusMessage + ')', body) } else { var result = body // attempt to parse the body as JSON if appropriate if (contentType && contentType.indexOf('application/json') >= 0) { result = StringUtils.tryParseJSON(body) if (!result) { debug('Snapchat Request JSON Parse Error: \nresponse: "%s"', body) // return cb('JSON parse error', body) } } return cb(null, result) } } if (self.HTTPMethod === 'POST') { if (self.multipart) { self.HTTPHeaders['content-length'] = Buffer.byteLength(self.HTTPBody) // self.HTTPHeaders['X-Timestamp'] = 0 // TODO: // neither objc or php versions send Content-Length header // don't think we can use request because its multipart/form-data uses random boundary var req = https.request({ host: 'feelinsonice-hrd.appspot.com', // constants.core.baseURL, path: self.endpoint, method: 'POST', headers: self.HTTPHeaders, agent: false, rejectUnauthorized: false //, secureOptions: require('constants').SSL_OP_NO_TLSv1 }, function (response) { response.request = req response.request.headers = req._headers console.log('status', response.statusCode) console.log('headers', JSON.stringify(response.headers)) response.setEncoding('utf8') var result = '' response.on('data', function (chunk) { console.log('RESPONSE DATA', chunk) result += chunk }) response.on('end', function () { console.log('RESPONSE END') wrapcb(null, response, result) }) }) // req.removeHeader('content-length') // req.removeHeader('transfer-encoding') // req.removeHeader('connection') req.on('error', function (err) { console.error('ERROR', err) wrapcb(err, null, null) }) req.on('abort', function () { console.error('ABORT') wrapcb('abort', null, null) }) req.write(self.HTTPBody) req.end() console.log('REQUEST END', req._headers) } else { self.httpRequest = request.post({ url: self.URL, headers: self.HTTPHeaders, form: self.HTTPBody }, wrapcb) // debug('%s Request %s (headers %j) (body \'%s\')', self.HTTPMethod, self.URL, self.HTTPHeaders, // self.httpRequest.body.toString('utf-8')) } } else { self.httpRequest = request({ url: self.URL, headers: self.HTTPHeaders, encoding: null }, wrapcb) debug('%s Request %s (headers %j)', self.HTTPMethod, self.URL, self.HTTPHeaders) } } // ----------------------------------------------------------------------------- // static utility methods // ----------------------------------------------------------------------------- /** * @static * @param {string=} endpoint Optional endpoint of the request relative to the base URL. * @param {Object=} params Optional parameters for the request. * @param {string=} gauth Optional parameter set to the X-Snapchat-Client-Auth-Token header field. * @param {string=} token Optional snapchat auth token returned from logging in. * @param {Object=} opts Optional custom options. * @param {function} cb * @return {Request} new Request wrapping this HTTP POST request. */ Request.post = function (endpoint, params, gauth, token, opts, cb) { // handle optional parameters if (typeof params === 'function') { cb = params params = { } gauth = token = opts = null } else if (typeof gauth === 'function') { cb = gauth gauth = token = opts = null } else if (typeof token === 'function') { cb = token token = opts = null } else if (typeof opts === 'function') { cb = opts opts = null } var headers = { } headers[constants.headers.clientAuthToken] = 'Bearer ' + (gauth || '') return Request.postCustom(endpoint, params, headers, token, opts, cb) } /** * @static * @param {string} endpoint endpoint of the request relative to the base URL. * @param {Object} params parameters for the request. * @param {Object} headers Custom HTTP headers for this request. * @param {string} token snapchat auth token returned from logging in. * @param {Object=} opts Optional custom options. * @param {function} cb * @return {Request} new Request wrapping this HTTP POST request. */ Request.postCustom = function (endpoint, params, headers, token, opts, cb) { // handle optional parameters if (typeof opts === 'function') { cb = opts opts = null } opts = opts || { } var request = new Request(extend({ method: 'POST', endpoint: endpoint, token: token, params: params, headers: headers }, opts)) request.start(cb) return request } /** * @static * @param {string} endpoint The endpoint of the request relative to the base URL. * @param {function} cb * @return {Request} new Request wrapping this HTTP GET request */ Request.get = function (endpoint, cb) { var request = new Request({ method: 'GET', endpoint: endpoint }) request.start(cb) return request } /** * @static * @param {Object} eventData * @param {function} cb * @return {Request} new Request wrapping this events HTTP POST request */ Request.sendEvents = function (eventData, cb) { var request = new Request({ method: 'POST', url: constants.core.eventsURL, eventData: eventData }) request.start(cb) return request } Request.postRaw = function (params, cb) { request.post(extend(params, { encoding: null }), function (err, response, body) { if (err) { return cb(err, response, body) } var contentType = response.headers['content-type'] var encoding = response.headers['content-encoding'] function contentTypeWrapper (err, body) { if (err) { return cb(err, response, body) } else if (contentType.indexOf('application/json') >= 0) { return cb(err, response, StringUtils.tryParseJSON(body.toString())) } else if (contentType.indexOf('text/plain') >= 0) { return cb(err, response, body.toString()) } else { return cb(err, response, body) } } if (encoding === 'gzip') { zlib.gunzip(body, function (err, dezipped) { contentTypeWrapper(err, dezipped && dezipped.toString()) }) } else if (encoding === 'deflate') { zlib.inflate(body, function (err, decoded) { contentTypeWrapper(err, decoded && decoded.toString()) }) } else { contentTypeWrapper(err, body) } }) } // ----------------------------------------------------------------------------- // override handling // ----------------------------------------------------------------------------- Request.overrideHeaderValuesGlobally = function (headers) { globalHeaderOverrides = headers } Request.overrideHeaderValues = function (headers, endpoint) { scopeHeaderOverrides[endpoint] = headers } Request.overrideValues = function (params, endpoint) { scopeParamOverrides[endpoint] = params } Request.overrideValuesGlobally = function (params) { globalParamOverrides = params } Request.overrideEndpoints = function (endpoints) { endpointOverrides = endpoints } Request._applyOverrides = function (opts) { if (opts.params) { opts.params = extend(opts.params, globalParamOverrides) opts.params = extend(opts.params, scopeParamOverrides[opts.endpoint] || { }) } if (opts.endpoint) { opts.endpoint = endpointOverrides[opts.endpoint] || opts.endpoint } } Request._applyHeaderOverrides = function (opts) { if (!opts.headers) return opts.headers = extend(opts.headers, globalHeaderOverrides) opts.headers = extend(opts.headers, scopeHeaderOverrides[opts.endpoint] || { }) } var globalHeaderOverrides = { } var globalParamOverrides = { } var scopeParamOverrides = { } var scopeHeaderOverrides = { } var endpointOverrides = { }