UNPKG

bot18

Version:

A high-frequency cryptocurrency trading bot by Zenbot creator @carlos8f

315 lines (263 loc) 9.96 kB
'use strict'; const URL = require('url'); const qs = require('querystring'); const dns = require('dns'); const MongoParseError = require('./error').MongoParseError; /** * The following regular expression validates a connection string and breaks the * provide string into the following capture groups: [protocol, username, password, hosts] */ const HOSTS_RX = /(mongodb(?:\+srv|)):\/\/(?: (?:[^:]*) (?: : ([^@]*) )? @ )?([^/?]*)(?:\/|)(.*)/; /** * Determines whether a provided address matches the provided parent domain in order * to avoid certain attack vectors. * * @param {String} srvAddress The address to check against a domain * @param {String} parentDomain The domain to check the provided address against * @return {Boolean} Whether the provided address matches the parent domain */ function matchesParentDomain(srvAddress, parentDomain) { const regex = /^.*?\./; const srv = `.${srvAddress.replace(regex, '')}`; const parent = `.${parentDomain.replace(regex, '')}`; return srv.endsWith(parent); } /** * Lookup an `mongodb+srv` connection string, combine the parts and reparse it as a normal * connection string. * * @param {string} uri The connection string to parse * @param {object} options Optional user provided connection string options * @param {function} callback */ function parseSrvConnectionString(uri, options, callback) { const result = URL.parse(uri, true); if (result.hostname.split('.').length < 3) { return callback(new MongoParseError('URI does not have hostname, domain name and tld')); } result.domainLength = result.hostname.split('.').length; if (result.pathname && result.pathname.match(',')) { return callback(new MongoParseError('Invalid URI, cannot contain multiple hostnames')); } if (result.port) { return callback(new MongoParseError(`Ports not accepted with '${PROTOCOL_MONGODB_SRV}' URIs`)); } let srvAddress = `_mongodb._tcp.${result.host}`; dns.resolveSrv(srvAddress, (err, addresses) => { if (err) return callback(err); if (addresses.length === 0) { return callback(new MongoParseError('No addresses found at host')); } for (let i = 0; i < addresses.length; i++) { if (!matchesParentDomain(addresses[i].name, result.hostname, result.domainLength)) { return callback( new MongoParseError('Server record does not share hostname with parent URI') ); } } let base = result.auth ? `mongodb://${result.auth}@` : `mongodb://`; let connectionStrings = addresses.map( (address, i) => i === 0 ? `${base}${address.name}:${address.port}` : `${address.name}:${address.port}` ); let connectionString = `${connectionStrings.join(',')}/`; let connectionStringOptions = []; // Add the default database if needed if (result.path) { let defaultDb = result.path.slice(1); if (defaultDb.indexOf('?') !== -1) { defaultDb = defaultDb.slice(0, defaultDb.indexOf('?')); } connectionString += defaultDb; } // Default to SSL true if (!options.ssl && (!result.search || result.query['ssl'] == null)) { connectionStringOptions.push('ssl=true'); } // Keep original uri options if (result.search) { connectionStringOptions.push(result.search.replace('?', '')); } dns.resolveTxt(result.host, (err, record) => { if (err) { if (err.code !== 'ENODATA') { return callback(err); } record = null; } if (record) { if (record.length > 1) { return callback(new MongoParseError('Multiple text records not allowed')); } record = record[0]; record = record.length > 1 ? record.join('') : record[0]; if (!record.includes('authSource') && !record.includes('replicaSet')) { return callback( new MongoParseError('Text record must only set `authSource` or `replicaSet`') ); } connectionStringOptions.push(record); } // Add any options to the connection string if (connectionStringOptions.length) { connectionString += `?${connectionStringOptions.join('&')}`; } parseConnectionString(connectionString, callback); }); }); } /** * Parses a query string item according to the connection string spec * * @param {Array|String} value The value to parse * @return {Array|Object|String} The parsed value */ function parseQueryStringItemValue(value) { if (Array.isArray(value)) { // deduplicate and simplify arrays value = value.filter((value, idx) => value.indexOf(value) === idx); if (value.length === 1) value = value[0]; } else if (value.indexOf(':') > 0) { value = value.split(',').reduce((result, pair) => { const parts = pair.split(':'); result[parts[0]] = parseQueryStringItemValue(parts[1]); return result; }, {}); } else if (value.toLowerCase() === 'true' || value.toLowerCase() === 'false') { value = value.toLowerCase() === 'true'; } else if (!Number.isNaN(value)) { const numericValue = parseFloat(value); if (!Number.isNaN(numericValue)) { value = parseFloat(value); } } return value; } /** * Parses a query string according the connection string spec. * * @param {String} query The query string to parse * @return {Object|Error} The parsed query string as an object, or an error if one was encountered */ function parseQueryString(query) { const result = {}; let parsedQueryString = qs.parse(query); for (const key in parsedQueryString) { const value = parsedQueryString[key]; if (value === '' || value == null) { return new MongoParseError('Incomplete key value pair for option'); } result[key.toLowerCase()] = parseQueryStringItemValue(value); } // special cases for known deprecated options if (result.wtimeout && result.wtimeoutms) { delete result.wtimeout; console.warn('Unsupported option `wtimeout` specified'); } return Object.keys(result).length ? result : null; } const PROTOCOL_MONGODB = 'mongodb'; const PROTOCOL_MONGODB_SRV = 'mongodb+srv'; const SUPPORTED_PROTOCOLS = [PROTOCOL_MONGODB, PROTOCOL_MONGODB_SRV]; /** * Parses a MongoDB connection string * * @param {*} uri the MongoDB connection string to parse * @param {object} [options] Optional settings. * @param {parseCallback} callback */ function parseConnectionString(uri, options, callback) { if (typeof options === 'function') (callback = options), (options = {}); options = options || {}; // Check for bad uris before we parse try { URL.parse(uri); } catch (e) { return callback(new MongoParseError('URI malformed, cannot be parsed')); } const cap = uri.match(HOSTS_RX); if (!cap) { return callback(new MongoParseError('Invalid connection string')); } const protocol = cap[1]; if (SUPPORTED_PROTOCOLS.indexOf(protocol) === -1) { return callback(new MongoParseError('Invalid protocol provided')); } if (protocol === PROTOCOL_MONGODB_SRV) { return parseSrvConnectionString(uri, options, callback); } const dbAndQuery = cap[4].split('?'); const db = dbAndQuery.length > 0 ? dbAndQuery[0] : null; const query = dbAndQuery.length > 1 ? dbAndQuery[1] : null; let parsedOptions = parseQueryString(query); if (parsedOptions instanceof MongoParseError) { return callback(parsedOptions); } parsedOptions = Object.assign({}, parsedOptions, options); const auth = { username: null, password: null, db: db && db !== '' ? qs.unescape(db) : null }; if (cap[4].split('?')[0].indexOf('@') !== -1) { return callback(new MongoParseError('Unescaped slash in userinfo section')); } const authorityParts = cap[3].split('@'); if (authorityParts.length > 2) { return callback(new MongoParseError('Unescaped at-sign in authority section')); } if (authorityParts.length > 1) { const authParts = authorityParts.shift().split(':'); if (authParts.length > 2) { return callback(new MongoParseError('Unescaped colon in authority section')); } auth.username = qs.unescape(authParts[0]); auth.password = authParts[1] ? qs.unescape(authParts[1]) : null; } let hostParsingError = null; const hosts = authorityParts .shift() .split(',') .map(host => { let parsedHost = URL.parse(`mongodb://${host}`); if (parsedHost.path === '/:') { hostParsingError = new MongoParseError('Double colon in host identifier'); return null; } // heuristically determine if we're working with a domain socket if (host.match(/\.sock/)) { parsedHost.hostname = qs.unescape(host); parsedHost.port = null; } if (Number.isNaN(parsedHost.port)) { hostParsingError = new MongoParseError('Invalid port (non-numeric string)'); return; } const result = { host: parsedHost.hostname, port: parsedHost.port ? parseInt(parsedHost.port) : null }; if (result.port === 0) { hostParsingError = new MongoParseError('Invalid port (zero) with hostname'); return; } if (result.port > 65535) { hostParsingError = new MongoParseError('Invalid port (larger than 65535) with hostname'); return; } if (result.port < 0) { hostParsingError = new MongoParseError('Invalid port (negative number)'); return; } return result; }) .filter(host => !!host); if (hostParsingError) { return callback(hostParsingError); } if (hosts.length === 0 || hosts[0].host === '' || hosts[0].host === null) { return callback(new MongoParseError('No hostname or hostnames provided in connection string')); } callback(null, { hosts: hosts, auth: auth.db || auth.username ? auth : null, options: Object.keys(parsedOptions).length ? parsedOptions : null }); } module.exports = parseConnectionString;