mirror of
https://github.com/bvanroll/yahoo-thing.git
synced 2025-08-30 04:22:42 +00:00
euh
This commit is contained in:
536
node_modules/mongodb-core/lib/uri_parser.js
generated
vendored
Normal file
536
node_modules/mongodb-core/lib/uri_parser.js
generated
vendored
Normal file
@@ -0,0 +1,536 @@
|
||||
'use strict';
|
||||
const URL = require('url');
|
||||
const qs = require('querystring');
|
||||
const dns = require('dns');
|
||||
const MongoParseError = require('./error').MongoParseError;
|
||||
const ReadPreference = require('./topologies/read_preference');
|
||||
|
||||
/**
|
||||
* 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 a `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`));
|
||||
}
|
||||
|
||||
// Resolve the SRV record and use the result as the list of hosts to connect to.
|
||||
const lookupAddress = result.host;
|
||||
dns.resolveSrv(`_mongodb._tcp.${lookupAddress}`, (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')
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
// Convert the original URL to a non-SRV URL.
|
||||
result.protocol = 'mongodb';
|
||||
result.host = addresses.map(address => `${address.name}:${address.port}`).join(',');
|
||||
|
||||
// Default to SSL true if it's not specified.
|
||||
if (
|
||||
!('ssl' in options) &&
|
||||
(!result.search || !('ssl' in result.query) || result.query.ssl === null)
|
||||
) {
|
||||
result.query.ssl = true;
|
||||
}
|
||||
|
||||
// Resolve TXT record and add options from there if they exist.
|
||||
dns.resolveTxt(lookupAddress, (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 = qs.parse(record[0].join(''));
|
||||
if (Object.keys(record).some(key => key !== 'authSource' && key !== 'replicaSet')) {
|
||||
return callback(
|
||||
new MongoParseError('Text record must only set `authSource` or `replicaSet`')
|
||||
);
|
||||
}
|
||||
|
||||
Object.assign(result.query, record);
|
||||
}
|
||||
|
||||
// Set completed options back into the URL object.
|
||||
result.search = qs.stringify(result.query);
|
||||
|
||||
const finalString = URL.format(result);
|
||||
parseConnectionString(finalString, options, callback);
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Parses a query string item according to the connection string spec
|
||||
*
|
||||
* @param {string} key The key for the parsed value
|
||||
* @param {Array|String} value The value to parse
|
||||
* @return {Array|Object|String} The parsed value
|
||||
*/
|
||||
function parseQueryStringItemValue(key, value) {
|
||||
if (Array.isArray(value)) {
|
||||
// deduplicate and simplify arrays
|
||||
value = value.filter((v, idx) => value.indexOf(v) === 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(key, parts[1]);
|
||||
return result;
|
||||
}, {});
|
||||
} else if (value.indexOf(',') > 0) {
|
||||
value = value.split(',').map(v => {
|
||||
return parseQueryStringItemValue(key, v);
|
||||
});
|
||||
} else if (value.toLowerCase() === 'true' || value.toLowerCase() === 'false') {
|
||||
value = value.toLowerCase() === 'true';
|
||||
} else if (!Number.isNaN(value) && !STRING_OPTIONS.has(key)) {
|
||||
const numericValue = parseFloat(value);
|
||||
if (!Number.isNaN(numericValue)) {
|
||||
value = parseFloat(value);
|
||||
}
|
||||
}
|
||||
|
||||
return value;
|
||||
}
|
||||
|
||||
// Options that are known boolean types
|
||||
const BOOLEAN_OPTIONS = new Set([
|
||||
'slaveok',
|
||||
'slave_ok',
|
||||
'sslvalidate',
|
||||
'fsync',
|
||||
'safe',
|
||||
'retrywrites',
|
||||
'j'
|
||||
]);
|
||||
|
||||
// Known string options, only used to bypass Number coercion in `parseQueryStringItemValue`
|
||||
const STRING_OPTIONS = new Set(['authsource', 'replicaset']);
|
||||
|
||||
// Supported text representations of auth mechanisms
|
||||
// NOTE: this list exists in native already, if it is merged here we should deduplicate
|
||||
const AUTH_MECHANISMS = new Set([
|
||||
'GSSAPI',
|
||||
'MONGODB-X509',
|
||||
'MONGODB-CR',
|
||||
'DEFAULT',
|
||||
'SCRAM-SHA-1',
|
||||
'SCRAM-SHA-256',
|
||||
'PLAIN'
|
||||
]);
|
||||
|
||||
// Lookup table used to translate normalized (lower-cased) forms of connection string
|
||||
// options to their expected camelCase version
|
||||
const CASE_TRANSLATION = {
|
||||
replicaset: 'replicaSet',
|
||||
connecttimeoutms: 'connectTimeoutMS',
|
||||
sockettimeoutms: 'socketTimeoutMS',
|
||||
maxpoolsize: 'maxPoolSize',
|
||||
minpoolsize: 'minPoolSize',
|
||||
maxidletimems: 'maxIdleTimeMS',
|
||||
waitqueuemultiple: 'waitQueueMultiple',
|
||||
waitqueuetimeoutms: 'waitQueueTimeoutMS',
|
||||
wtimeoutms: 'wtimeoutMS',
|
||||
readconcern: 'readConcern',
|
||||
readconcernlevel: 'readConcernLevel',
|
||||
readpreference: 'readPreference',
|
||||
maxstalenessseconds: 'maxStalenessSeconds',
|
||||
readpreferencetags: 'readPreferenceTags',
|
||||
authsource: 'authSource',
|
||||
authmechanism: 'authMechanism',
|
||||
authmechanismproperties: 'authMechanismProperties',
|
||||
gssapiservicename: 'gssapiServiceName',
|
||||
localthresholdms: 'localThresholdMS',
|
||||
serverselectiontimeoutms: 'serverSelectionTimeoutMS',
|
||||
serverselectiontryonce: 'serverSelectionTryOnce',
|
||||
heartbeatfrequencyms: 'heartbeatFrequencyMS',
|
||||
appname: 'appName',
|
||||
retrywrites: 'retryWrites',
|
||||
uuidrepresentation: 'uuidRepresentation',
|
||||
zlibcompressionlevel: 'zlibCompressionLevel'
|
||||
};
|
||||
|
||||
/**
|
||||
* Sets the value for `key`, allowing for any required translation
|
||||
*
|
||||
* @param {object} obj The object to set the key on
|
||||
* @param {string} key The key to set the value for
|
||||
* @param {*} value The value to set
|
||||
* @param {object} options The options used for option parsing
|
||||
*/
|
||||
function applyConnectionStringOption(obj, key, value, options) {
|
||||
// simple key translation
|
||||
if (key === 'journal') {
|
||||
key = 'j';
|
||||
} else if (key === 'wtimeoutms') {
|
||||
key = 'wtimeout';
|
||||
}
|
||||
|
||||
// more complicated translation
|
||||
if (BOOLEAN_OPTIONS.has(key)) {
|
||||
value = value === 'true' || value === true;
|
||||
} else if (key === 'appname') {
|
||||
value = decodeURIComponent(value);
|
||||
} else if (key === 'readconcernlevel') {
|
||||
key = 'readconcern';
|
||||
value = { level: value };
|
||||
}
|
||||
|
||||
// simple validation
|
||||
if (key === 'compressors') {
|
||||
value = Array.isArray(value) ? value : [value];
|
||||
|
||||
if (!value.every(c => c === 'snappy' || c === 'zlib')) {
|
||||
throw new MongoParseError(
|
||||
'Value for `compressors` must be at least one of: `snappy`, `zlib`'
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
if (key === 'authmechanism' && !AUTH_MECHANISMS.has(value)) {
|
||||
throw new MongoParseError(
|
||||
'Value for `authMechanism` must be one of: `DEFAULT`, `GSSAPI`, `PLAIN`, `MONGODB-X509`, `SCRAM-SHA-1`, `SCRAM-SHA-256`'
|
||||
);
|
||||
}
|
||||
|
||||
if (key === 'readpreference' && !ReadPreference.isValid(value)) {
|
||||
throw new MongoParseError(
|
||||
'Value for `readPreference` must be one of: `primary`, `primaryPreferred`, `secondary`, `secondaryPreferred`, `nearest`'
|
||||
);
|
||||
}
|
||||
|
||||
if (key === 'zlibcompressionlevel' && (value < -1 || value > 9)) {
|
||||
throw new MongoParseError('zlibCompressionLevel must be an integer between -1 and 9');
|
||||
}
|
||||
|
||||
// special cases
|
||||
if (key === 'compressors' || key === 'zlibcompressionlevel') {
|
||||
obj.compression = obj.compression || {};
|
||||
obj = obj.compression;
|
||||
}
|
||||
|
||||
if (key === 'authmechanismproperties') {
|
||||
if (typeof value.SERVICE_NAME === 'string') obj.gssapiServiceName = value.SERVICE_NAME;
|
||||
if (typeof value.SERVICE_REALM === 'string') obj.gssapiServiceRealm = value.SERVICE_REALM;
|
||||
if (typeof value.CANONICALIZE_HOST_NAME !== 'undefined') {
|
||||
obj.gssapiCanonicalizeHostName = value.CANONICALIZE_HOST_NAME;
|
||||
}
|
||||
}
|
||||
|
||||
// set the actual value
|
||||
if (options.caseTranslate && CASE_TRANSLATION[key]) {
|
||||
obj[CASE_TRANSLATION[key]] = value;
|
||||
return;
|
||||
}
|
||||
|
||||
obj[key] = value;
|
||||
}
|
||||
|
||||
const USERNAME_REQUIRED_MECHANISMS = new Set([
|
||||
'GSSAPI',
|
||||
'MONGODB-CR',
|
||||
'PLAIN',
|
||||
'SCRAM-SHA-1',
|
||||
'SCRAM-SHA-256'
|
||||
]);
|
||||
|
||||
/**
|
||||
* Modifies the parsed connection string object taking into account expectations we
|
||||
* have for authentication-related options.
|
||||
*
|
||||
* @param {object} parsed The parsed connection string result
|
||||
* @return The parsed connection string result possibly modified for auth expectations
|
||||
*/
|
||||
function applyAuthExpectations(parsed) {
|
||||
if (parsed.options == null) {
|
||||
return;
|
||||
}
|
||||
|
||||
const options = parsed.options;
|
||||
const authSource = options.authsource || options.authSource;
|
||||
if (authSource != null) {
|
||||
parsed.auth = Object.assign({}, parsed.auth, { db: authSource });
|
||||
}
|
||||
|
||||
const authMechanism = options.authmechanism || options.authMechanism;
|
||||
if (authMechanism != null) {
|
||||
if (
|
||||
USERNAME_REQUIRED_MECHANISMS.has(authMechanism) &&
|
||||
(!parsed.auth || parsed.auth.username == null)
|
||||
) {
|
||||
throw new MongoParseError(`Username required for mechanism \`${authMechanism}\``);
|
||||
}
|
||||
|
||||
if (authMechanism === 'GSSAPI') {
|
||||
if (authSource != null && authSource !== '$external') {
|
||||
throw new MongoParseError(
|
||||
`Invalid source \`${authSource}\` for mechanism \`${authMechanism}\` specified.`
|
||||
);
|
||||
}
|
||||
|
||||
parsed.auth = Object.assign({}, parsed.auth, { db: '$external' });
|
||||
}
|
||||
|
||||
if (authMechanism === 'MONGODB-X509') {
|
||||
if (parsed.auth && parsed.auth.password != null) {
|
||||
throw new MongoParseError(`Password not allowed for mechanism \`${authMechanism}\``);
|
||||
}
|
||||
|
||||
if (authSource != null && authSource !== '$external') {
|
||||
throw new MongoParseError(
|
||||
`Invalid source \`${authSource}\` for mechanism \`${authMechanism}\` specified.`
|
||||
);
|
||||
}
|
||||
|
||||
parsed.auth = Object.assign({}, parsed.auth, { db: '$external' });
|
||||
}
|
||||
|
||||
if (authMechanism === 'PLAIN') {
|
||||
if (parsed.auth && parsed.auth.db == null) {
|
||||
parsed.auth = Object.assign({}, parsed.auth, { db: '$external' });
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// default to `admin` if nothing else was resolved
|
||||
if (parsed.auth && parsed.auth.db == null) {
|
||||
parsed.auth = Object.assign({}, parsed.auth, { db: 'admin' });
|
||||
}
|
||||
|
||||
return parsed;
|
||||
}
|
||||
|
||||
/**
|
||||
* Parses a query string according the connection string spec.
|
||||
*
|
||||
* @param {String} query The query string to parse
|
||||
* @param {object} [options] The options used for options parsing
|
||||
* @return {Object|Error} The parsed query string as an object, or an error if one was encountered
|
||||
*/
|
||||
function parseQueryString(query, options) {
|
||||
const result = {};
|
||||
let parsedQueryString = qs.parse(query);
|
||||
|
||||
for (const key in parsedQueryString) {
|
||||
const value = parsedQueryString[key];
|
||||
if (value === '' || value == null) {
|
||||
throw new MongoParseError('Incomplete key value pair for option');
|
||||
}
|
||||
|
||||
const normalizedKey = key.toLowerCase();
|
||||
const parsedValue = parseQueryStringItemValue(normalizedKey, value);
|
||||
applyConnectionStringOption(result, normalizedKey, parsedValue, options);
|
||||
}
|
||||
|
||||
// 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 {boolean} [options.caseTranslate] Whether the parser should translate options back into camelCase after normalization
|
||||
* @param {parseCallback} callback
|
||||
*/
|
||||
function parseConnectionString(uri, options, callback) {
|
||||
if (typeof options === 'function') (callback = options), (options = {});
|
||||
options = Object.assign({}, { caseTranslate: true }, 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;
|
||||
try {
|
||||
parsedOptions = parseQueryString(query, options);
|
||||
} catch (parseError) {
|
||||
return callback(parseError);
|
||||
}
|
||||
|
||||
parsedOptions = Object.assign({}, parsedOptions, options);
|
||||
const auth = { username: null, password: null, db: db && db !== '' ? qs.unescape(db) : null };
|
||||
if (parsedOptions.auth) {
|
||||
// maintain support for legacy options passed into `MongoClient`
|
||||
if (parsedOptions.auth.username) auth.username = parsedOptions.auth.username;
|
||||
if (parsedOptions.auth.user) auth.username = parsedOptions.auth.user;
|
||||
if (parsedOptions.auth.password) auth.password = parsedOptions.auth.password;
|
||||
}
|
||||
|
||||
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) : 27017
|
||||
};
|
||||
|
||||
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'));
|
||||
}
|
||||
|
||||
const result = {
|
||||
hosts: hosts,
|
||||
auth: auth.db || auth.username ? auth : null,
|
||||
options: Object.keys(parsedOptions).length ? parsedOptions : null
|
||||
};
|
||||
|
||||
if (result.auth && result.auth.db) {
|
||||
result.defaultDatabase = result.auth.db;
|
||||
}
|
||||
|
||||
try {
|
||||
applyAuthExpectations(result);
|
||||
} catch (authError) {
|
||||
return callback(authError);
|
||||
}
|
||||
|
||||
callback(null, result);
|
||||
}
|
||||
|
||||
module.exports = parseConnectionString;
|
Reference in New Issue
Block a user