From 3906b27e0a8ce9bc1e6d54ee52fc65590c58c4f2 Mon Sep 17 00:00:00 2001 From: pluralcafe-docker Date: Wed, 22 Aug 2018 07:10:06 +0000 Subject: Use insecure mode for docker --- node-mastodon/lib/helpers.js | 65 ++++++++ node-mastodon/lib/mastodon.js | 340 ++++++++++++++++++++++++++++++++++++++++++ node-mastodon/lib/settings.js | 2 + 3 files changed, 407 insertions(+) create mode 100644 node-mastodon/lib/helpers.js create mode 100644 node-mastodon/lib/mastodon.js create mode 100644 node-mastodon/lib/settings.js (limited to 'node-mastodon/lib') diff --git a/node-mastodon/lib/helpers.js b/node-mastodon/lib/helpers.js new file mode 100644 index 0000000..8833968 --- /dev/null +++ b/node-mastodon/lib/helpers.js @@ -0,0 +1,65 @@ +var querystring = require('querystring'); +var request = require('request'); + +/** + * For each `/:param` fragment in path, move the value in params + * at that key to path. If the key is not found in params, throw. + * Modifies both params and path values. + * + * @param {Objet} params Object used to build path. + * @param {String} path String to transform. + * @return {Undefined} + * + */ +exports.moveParamsIntoPath = function (params, path) { + var rgxParam = /\/:(\w+)/g + var missingParamErr = null + + path = path.replace(rgxParam, function (hit) { + var paramName = hit.slice(2) + var suppliedVal = params[paramName] + if (!suppliedVal) { + throw new Error('Mastodon: Params object is missing a required parameter for this request: `'+paramName+'`') + } + var retVal = '/' + suppliedVal + delete params[paramName] + return retVal + }) + return path +} + +/** + * When Mastodon returns a response that looks like an error response, + * use this function to attach the error info in the response body to `err`. + * + * @param {Error} err Error instance to which body info will be attached + * @param {Object} body JSON object that is the deserialized HTTP response body received from Mastodon + * @return {Undefined} + */ +exports.attachBodyInfoToError = function (err, body) { + err.mastodonReply = body; + if (!body) { + return + } + if (body.error) { + // the body itself is an error object + err.message = body.error + err.allErrors = err.allErrors.concat([body]) + } else if (body.errors && body.errors.length) { + // body contains multiple error objects + err.message = body.errors[0].message; + err.code = body.errors[0].code; + err.allErrors = err.allErrors.concat(body.errors) + } +} + +exports.makeMastodonError = function (message) { + var err = new Error() + if (message) { + err.message = message + } + err.code = null + err.allErrors = [] + err.mastodonReply = null + return err +} diff --git a/node-mastodon/lib/mastodon.js b/node-mastodon/lib/mastodon.js new file mode 100644 index 0000000..7f5c8c1 --- /dev/null +++ b/node-mastodon/lib/mastodon.js @@ -0,0 +1,340 @@ +// +// Mastodon API Wrapper +// +var assert = require('assert'); +var Promise = require('bluebird'); +var request = require('request'); +var util = require('util'); +var helpers = require('./helpers'); +var STATUS_CODES_TO_ABORT_ON = require('./settings').STATUS_CODES_TO_ABORT_ON; + +var DEFAULT_REST_ROOT = 'https://mastodon.social/api/v1/'; + +var required_for_user_auth = [ + 'access_token', +]; + +// +// Mastodon +// +var Mastodon = function (config) { + if (!(this instanceof Mastodon)) { + return new Mastodon(config); + } + + var self = this + var credentials = { + access_token : config.access_token + } + + this.apiUrl = config.api_url || DEFAULT_REST_ROOT; + + this._validateConfigOrThrow(config); + this.config = config; + this._mastodon_time_minus_local_time_ms = 0; +} + +Mastodon.prototype.get = function (path, params, callback) { + return this.request('GET', path, params, callback) +} + +Mastodon.prototype.post = function (path, params, callback) { + return this.request('POST', path, params, callback) +} + +Mastodon.prototype.patch = function (path, params, callback) { + return this.request('PATCH', path, params, callback) +} + +Mastodon.prototype.delete = function (path, params, callback) { + return this.request('DELETE', path, params, callback) +} + +Mastodon.prototype.request = function (method, path, params, callback) { + var self = this; + assert(method == 'GET' || method == 'POST' || method == 'PATCH' || method == 'DELETE'); + // if no `params` is specified but a callback is, use default params + if (typeof params === 'function') { + callback = params + params = {} + } + + return new Promise(function (resolve, reject) { + var _returnErrorToUser = function (err) { + if (callback && typeof callback === 'function') { + callback(err, null, null); + } + reject(err); + } + + self._buildReqOpts(method, path, params, function (err, reqOpts) { + if (err) { + _returnErrorToUser(err); + return + } + + var mastoOptions = (params && params.masto_options) || {}; + + process.nextTick(function () { + // ensure all HTTP i/o occurs after the user has a chance to bind their event handlers + self._doRestApiRequest(reqOpts, mastoOptions, method, function (err, parsedBody, resp) { + if (err) { + _returnErrorToUser(err); + return + } + self._updateClockOffsetFromResponse(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.makeMastodonError('The peer certificate was not signed; ' + authErrMsg); + _returnErrorToUser(err); + 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); + _returnErrorToUser(err); + return; + } + } + + if (callback && typeof callback === 'function') { + callback(err, parsedBody, resp); + } + + resolve({ data: parsedBody, resp: resp }); + return; + }) + }) + }); + }); +} + +Mastodon.prototype._updateClockOffsetFromResponse = function (resp) { + var self = this; + if (resp && resp.headers && resp.headers.date && + new Date(resp.headers.date).toString() !== 'Invalid Date' + ) { + var mastodonTimeMs = new Date(resp.headers.date).getTime() + self._mastodon_time_minus_local_time_ms = mastodonTimeMs - Date.now(); + } +} + +/** + * Builds and returns an options object ready to pass to `request()` + * @param {String} method "GET", "POST", or "DELETE" + * @param {String} path REST API resource uri (eg. "statuses/destroy/:id") + * @param {Object} params user's params object + * @returns {Undefined} + * + * Calls `callback` with Error, Object where Object is an options object ready to pass to `request()`. + * + * Returns error raised (if any) by `helpers.moveParamsIntoPath()` + */ +Mastodon.prototype._buildReqOpts = function (method, path, params, callback) { + var self = this + if (!params) { + params = {} + } + var finalParams = params; + delete finalParams.masto_options + + // the options object passed to `request` used to perform the HTTP request + var reqOpts = { + headers: { + 'Accept': '*/*', + 'User-Agent': 'node-mastodon-client', + 'Authorization': 'Bearer ' + self.config.access_token + }, + gzip: true, + encoding: null, + rejectUnauthorized: false, + insecure: true, + } + + if (typeof self.config.timeout_ms !== 'undefined') { + reqOpts.timeout = self.config.timeout_ms; + } + + try { + // finalize the `path` value by building it using user-supplied params + path = helpers.moveParamsIntoPath(finalParams, path) + } catch (e) { + callback(e, null, null) + return + } + + if (path.match(/^https?:\/\//i)) { + // This is a full url request + reqOpts.url = path + } else { + // This is a REST API request. + reqOpts.url = this.apiUrl + path; + } + + if (finalParams.file) { + // If we're sending a file + reqOpts.headers['Content-type'] = 'multipart/form-data'; + reqOpts.formData = finalParams; + } else { + // Non-file-upload params should be url-encoded + if (Object.keys(finalParams).length > 0) { + reqOpts.url += this.formEncodeParams(finalParams); + } + } + + callback(null, reqOpts); + return; +} + +/** + * Make HTTP request to Mastodon REST API. + * @param {Object} reqOpts options object passed to `request()` + * @param {Object} mastoOptions + * @param {String} method "GET", "POST", or "DELETE" + * @param {Function} callback user's callback + * @return {Undefined} + */ +Mastodon.prototype._doRestApiRequest = function (reqOpts, mastoOptions, method, callback) { + var request_method = request[method.toLowerCase()]; + var req = request_method(reqOpts); + + var body = ''; + var response = null; + + var onRequestComplete = function () { + if (body !== '') { + try { + body = JSON.parse(body) + } catch (jsonDecodeError) { + // there was no transport-level error, but a JSON object could not be decoded from the request body + // surface this to the caller + var err = helpers.makeMastodonError('JSON decode error: Mastodon HTTP response body was not valid JSON') + err.statusCode = response ? response.statusCode: null; + err.allErrors.concat({error: jsonDecodeError.toString()}) + callback(err, body, response); + return + } + } + + if (typeof body === 'object' && (body.error || body.errors)) { + // we got a Mastodon API-level error response + // place the errors in the HTTP response body into the Error object and pass control to caller + var err = helpers.makeMastodonError('Mastodon API Error') + err.statusCode = response ? response.statusCode: null; + helpers.attachBodyInfoToError(err, body); + callback(err, body, response); + return + } + + // success case - no errors in HTTP response body + callback(err, body, response) + } + + req.on('response', function (res) { + response = res + // 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) { + body += chunk.toString('utf8') + }) + // we're done reading the response + req.on('end', function () { + onRequestComplete() + }) + }) + + req.on('error', function (err) { + // transport-level error occurred - likely a socket error + if (mastoOptions.retry && + STATUS_CODES_TO_ABORT_ON.indexOf(err.statusCode) !== -1 + ) { + // retry the request since retries were specified and we got a status code we should retry on + self.request(method, path, params, callback); + return; + } else { + // pass the transport-level error to the caller + err.statusCode = null + err.code = null + err.allErrors = []; + helpers.attachBodyInfoToError(err, body) + callback(err, body, response); + return; + } + }) +} + +Mastodon.prototype.formEncodeParams = function (params, noQuestionMark) { + var encoded = ''; + for (var key in params) { + var value = params[key]; + if (encoded === '') { + if (!noQuestionMark) { + encoded = '?'; + } + } else { + encoded += '&'; + } + + if (Array.isArray(value)) { + value.forEach(function(v) { + encoded += encodeURIComponent(key) + '[]=' + encodeURIComponent(v) + '&'; + }); + } else { + encoded += encodeURIComponent(key) + '=' + encodeURIComponent(value); + } + } + + return (encoded); +} + +Mastodon.prototype.setAuth = function (auth) { + var self = this + var configKeys = [ + 'access_token' + ]; + + // update config + configKeys.forEach(function (k) { + if (auth[k]) { + self.config[k] = auth[k] + } + }) + this._validateConfigOrThrow(self.config); +} + +Mastodon.prototype.getAuth = function () { + return this.config +} + +// +// Check that the required auth credentials are present in `config`. +// @param {Object} config Object containing credentials for REST API auth +// +Mastodon.prototype._validateConfigOrThrow = function (config) { + //check config for proper format + if (typeof config !== 'object') { + throw new TypeError('config must be object, got ' + typeof config) + } + + if (typeof config.timeout_ms !== 'undefined' && isNaN(Number(config.timeout_ms))) { + throw new TypeError('Mastodon config `timeout_ms` must be a Number. Got: ' + config.timeout_ms + '.'); + } + + var auth_type = 'user auth' + var required_keys = required_for_user_auth + + required_keys.forEach(function (req_key) { + if (!config[req_key]) { + var err_msg = util.format('Mastodon config must include `%s` when using %s.', req_key, auth_type) + throw new Error(err_msg) + } + }) +} + +module.exports = Mastodon diff --git a/node-mastodon/lib/settings.js b/node-mastodon/lib/settings.js new file mode 100644 index 0000000..f6d5411 --- /dev/null +++ b/node-mastodon/lib/settings.js @@ -0,0 +1,2 @@ +// set of status codes where we don't attempt reconnecting to Mastodon +exports.STATUS_CODES_TO_ABORT_ON = [ 400, 401, 403, 404, 406, 410, 422 ]; -- cgit