You can not select more than 25 topics
Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
401 lines
14 KiB
401 lines
14 KiB
"use strict"; |
|
|
|
var ReadPreference = require('./read_preference'), |
|
parser = require('url'), |
|
f = require('util').format; |
|
|
|
module.exports = function(url, options) { |
|
// Ensure we have a default options object if none set |
|
options = options || {}; |
|
// Variables |
|
var connection_part = ''; |
|
var auth_part = ''; |
|
var query_string_part = ''; |
|
var dbName = 'admin'; |
|
|
|
// Url parser result |
|
var result = parser.parse(url, true); |
|
|
|
if(result.protocol != 'mongodb:') { |
|
throw new Error('invalid schema, expected mongodb'); |
|
} |
|
|
|
if((result.hostname == null || result.hostname == '') && url.indexOf('.sock') == -1) { |
|
throw new Error('no hostname or hostnames provided in connection string'); |
|
} |
|
|
|
if(result.port == '0') { |
|
throw new Error('invalid port (zero) with hostname'); |
|
} |
|
|
|
if(!isNaN(parseInt(result.port, 10)) && parseInt(result.port, 10) > 65535) { |
|
throw new Error('invalid port (larger than 65535) with hostname'); |
|
} |
|
|
|
if(result.path |
|
&& result.path.length > 0 |
|
&& result.path[0] != '/' |
|
&& url.indexOf('.sock') == -1) { |
|
throw new Error('missing delimiting slash between hosts and options'); |
|
} |
|
|
|
if(result.query) { |
|
for(var name in result.query) { |
|
if(name.indexOf('::') != -1) { |
|
throw new Error('double colon in host identifier'); |
|
} |
|
|
|
if(result.query[name] == '') { |
|
throw new Error('query parameter ' + name + ' is an incomplete value pair'); |
|
} |
|
} |
|
} |
|
|
|
if(result.auth) { |
|
var parts = result.auth.split(':'); |
|
if(url.indexOf(result.auth) != -1 && parts.length > 2) { |
|
throw new Error('Username with password containing an unescaped colon'); |
|
} |
|
|
|
if(url.indexOf(result.auth) != -1 && result.auth.indexOf('@') != -1) { |
|
throw new Error('Username containing an unescaped at-sign'); |
|
} |
|
} |
|
|
|
// Remove query |
|
var clean = url.split('?').shift(); |
|
|
|
// Extract the list of hosts |
|
var strings = clean.split(','); |
|
var hosts = []; |
|
|
|
for(var i = 0; i < strings.length; i++) { |
|
var hostString = strings[i]; |
|
|
|
if(hostString.indexOf('mongodb') != -1) { |
|
if(hostString.indexOf('@') != -1) { |
|
hosts.push(hostString.split('@').pop()) |
|
} else { |
|
hosts.push(hostString.substr('mongodb://'.length)); |
|
} |
|
} else if(hostString.indexOf('/') != -1) { |
|
hosts.push(hostString.split('/').shift()); |
|
} else if(hostString.indexOf('/') == -1) { |
|
hosts.push(hostString.trim()); |
|
} |
|
} |
|
|
|
for(var i = 0; i < hosts.length; i++) { |
|
var r = parser.parse(f('mongodb://%s', hosts[i].trim())); |
|
if(r.path && r.path.indexOf(':') != -1) { |
|
throw new Error('double colon in host identifier'); |
|
} |
|
} |
|
|
|
// If we have a ? mark cut the query elements off |
|
if(url.indexOf("?") != -1) { |
|
query_string_part = url.substr(url.indexOf("?") + 1); |
|
connection_part = url.substring("mongodb://".length, url.indexOf("?")) |
|
} else { |
|
connection_part = url.substring("mongodb://".length); |
|
} |
|
|
|
// Check if we have auth params |
|
if(connection_part.indexOf("@") != -1) { |
|
auth_part = connection_part.split("@")[0]; |
|
connection_part = connection_part.split("@")[1]; |
|
} |
|
|
|
// Check if the connection string has a db |
|
if(connection_part.indexOf(".sock") != -1) { |
|
if(connection_part.indexOf(".sock/") != -1) { |
|
dbName = connection_part.split(".sock/")[1]; |
|
// Check if multiple database names provided, or just an illegal trailing backslash |
|
if (dbName.indexOf("/") != -1) { |
|
if (dbName.split("/").length == 2 && dbName.split("/")[1].length == 0) { |
|
throw new Error('Illegal trailing backslash after database name'); |
|
} |
|
throw new Error('More than 1 database name in URL'); |
|
} |
|
connection_part = connection_part.split("/", connection_part.indexOf(".sock") + ".sock".length); |
|
} |
|
} else if(connection_part.indexOf("/") != -1) { |
|
// Check if multiple database names provided, or just an illegal trailing backslash |
|
if (connection_part.split("/").length > 2) { |
|
if (connection_part.split("/")[2].length == 0) { |
|
throw new Error('Illegal trailing backslash after database name'); |
|
} |
|
throw new Error('More than 1 database name in URL'); |
|
} |
|
dbName = connection_part.split("/")[1]; |
|
connection_part = connection_part.split("/")[0]; |
|
} |
|
|
|
// Result object |
|
var object = {}; |
|
|
|
// Pick apart the authentication part of the string |
|
var authPart = auth_part || ''; |
|
var auth = authPart.split(':', 2); |
|
|
|
// Decode the URI components |
|
auth[0] = decodeURIComponent(auth[0]); |
|
if(auth[1]){ |
|
auth[1] = decodeURIComponent(auth[1]); |
|
} |
|
|
|
// Add auth to final object if we have 2 elements |
|
if(auth.length == 2) object.auth = {user: auth[0], password: auth[1]}; |
|
|
|
// Variables used for temporary storage |
|
var hostPart; |
|
var urlOptions; |
|
var servers; |
|
var serverOptions = {socketOptions: {}}; |
|
var dbOptions = {read_preference_tags: []}; |
|
var replSetServersOptions = {socketOptions: {}}; |
|
var mongosOptions = {socketOptions: {}}; |
|
// Add server options to final object |
|
object.server_options = serverOptions; |
|
object.db_options = dbOptions; |
|
object.rs_options = replSetServersOptions; |
|
object.mongos_options = mongosOptions; |
|
|
|
// Let's check if we are using a domain socket |
|
if(url.match(/\.sock/)) { |
|
// Split out the socket part |
|
var domainSocket = url.substring( |
|
url.indexOf("mongodb://") + "mongodb://".length |
|
, url.lastIndexOf(".sock") + ".sock".length); |
|
// Clean out any auth stuff if any |
|
if(domainSocket.indexOf("@") != -1) domainSocket = domainSocket.split("@")[1]; |
|
servers = [{domain_socket: domainSocket}]; |
|
} else { |
|
// Split up the db |
|
hostPart = connection_part; |
|
// Deduplicate servers |
|
var deduplicatedServers = {}; |
|
|
|
// Parse all server results |
|
servers = hostPart.split(',').map(function(h) { |
|
var _host, _port, ipv6match; |
|
//check if it matches [IPv6]:port, where the port number is optional |
|
if ((ipv6match = /\[([^\]]+)\](?:\:(.+))?/.exec(h))) { |
|
_host = ipv6match[1]; |
|
_port = parseInt(ipv6match[2], 10) || 27017; |
|
} else { |
|
//otherwise assume it's IPv4, or plain hostname |
|
var hostPort = h.split(':', 2); |
|
_host = hostPort[0] || 'localhost'; |
|
_port = hostPort[1] != null ? parseInt(hostPort[1], 10) : 27017; |
|
// Check for localhost?safe=true style case |
|
if(_host.indexOf("?") != -1) _host = _host.split(/\?/)[0]; |
|
} |
|
|
|
// No entry returned for duplicate servr |
|
if(deduplicatedServers[_host + "_" + _port]) return null; |
|
deduplicatedServers[_host + "_" + _port] = 1; |
|
|
|
// Return the mapped object |
|
return {host: _host, port: _port}; |
|
}).filter(function(x) { |
|
return x != null; |
|
}); |
|
} |
|
|
|
// Get the db name |
|
object.dbName = dbName || 'admin'; |
|
// Split up all the options |
|
urlOptions = (query_string_part || '').split(/[&;]/); |
|
// Ugh, we have to figure out which options go to which constructor manually. |
|
urlOptions.forEach(function(opt) { |
|
if(!opt) return; |
|
var splitOpt = opt.split('='), name = splitOpt[0], value = splitOpt[1]; |
|
// Options implementations |
|
switch(name) { |
|
case 'slaveOk': |
|
case 'slave_ok': |
|
serverOptions.slave_ok = (value == 'true'); |
|
dbOptions.slaveOk = (value == 'true'); |
|
break; |
|
case 'maxPoolSize': |
|
case 'poolSize': |
|
serverOptions.poolSize = parseInt(value, 10); |
|
replSetServersOptions.poolSize = parseInt(value, 10); |
|
break; |
|
case 'autoReconnect': |
|
case 'auto_reconnect': |
|
serverOptions.auto_reconnect = (value == 'true'); |
|
break; |
|
case 'minPoolSize': |
|
throw new Error("minPoolSize not supported"); |
|
case 'maxIdleTimeMS': |
|
throw new Error("maxIdleTimeMS not supported"); |
|
case 'waitQueueMultiple': |
|
throw new Error("waitQueueMultiple not supported"); |
|
case 'waitQueueTimeoutMS': |
|
throw new Error("waitQueueTimeoutMS not supported"); |
|
case 'uuidRepresentation': |
|
throw new Error("uuidRepresentation not supported"); |
|
case 'ssl': |
|
if(value == 'prefer') { |
|
serverOptions.ssl = value; |
|
replSetServersOptions.ssl = value; |
|
mongosOptions.ssl = value; |
|
break; |
|
} |
|
serverOptions.ssl = (value == 'true'); |
|
replSetServersOptions.ssl = (value == 'true'); |
|
mongosOptions.ssl = (value == 'true'); |
|
break; |
|
case 'sslValidate': |
|
serverOptions.sslValidate = (value == 'true'); |
|
replSetServersOptions.sslValidate = (value == 'true'); |
|
mongosOptions.sslValidate = (value == 'true'); |
|
break; |
|
case 'replicaSet': |
|
case 'rs_name': |
|
replSetServersOptions.rs_name = value; |
|
break; |
|
case 'reconnectWait': |
|
replSetServersOptions.reconnectWait = parseInt(value, 10); |
|
break; |
|
case 'retries': |
|
replSetServersOptions.retries = parseInt(value, 10); |
|
break; |
|
case 'readSecondary': |
|
case 'read_secondary': |
|
replSetServersOptions.read_secondary = (value == 'true'); |
|
break; |
|
case 'fsync': |
|
dbOptions.fsync = (value == 'true'); |
|
break; |
|
case 'journal': |
|
dbOptions.j = (value == 'true'); |
|
break; |
|
case 'safe': |
|
dbOptions.safe = (value == 'true'); |
|
break; |
|
case 'nativeParser': |
|
case 'native_parser': |
|
dbOptions.native_parser = (value == 'true'); |
|
break; |
|
case 'readConcernLevel': |
|
dbOptions.readConcern = {level: value}; |
|
break; |
|
case 'connectTimeoutMS': |
|
serverOptions.socketOptions.connectTimeoutMS = parseInt(value, 10); |
|
replSetServersOptions.socketOptions.connectTimeoutMS = parseInt(value, 10); |
|
mongosOptions.socketOptions.connectTimeoutMS = parseInt(value, 10); |
|
break; |
|
case 'socketTimeoutMS': |
|
serverOptions.socketOptions.socketTimeoutMS = parseInt(value, 10); |
|
replSetServersOptions.socketOptions.socketTimeoutMS = parseInt(value, 10); |
|
mongosOptions.socketOptions.socketTimeoutMS = parseInt(value, 10); |
|
break; |
|
case 'w': |
|
dbOptions.w = parseInt(value, 10); |
|
if(isNaN(dbOptions.w)) dbOptions.w = value; |
|
break; |
|
case 'authSource': |
|
dbOptions.authSource = value; |
|
break; |
|
case 'gssapiServiceName': |
|
dbOptions.gssapiServiceName = value; |
|
break; |
|
case 'authMechanism': |
|
if(value == 'GSSAPI') { |
|
// If no password provided decode only the principal |
|
if(object.auth == null) { |
|
var urlDecodeAuthPart = decodeURIComponent(authPart); |
|
if(urlDecodeAuthPart.indexOf("@") == -1) throw new Error("GSSAPI requires a provided principal"); |
|
object.auth = {user: urlDecodeAuthPart, password: null}; |
|
} else { |
|
object.auth.user = decodeURIComponent(object.auth.user); |
|
} |
|
} else if(value == 'MONGODB-X509') { |
|
object.auth = {user: decodeURIComponent(authPart)}; |
|
} |
|
|
|
// Only support GSSAPI or MONGODB-CR for now |
|
if(value != 'GSSAPI' |
|
&& value != 'MONGODB-X509' |
|
&& value != 'MONGODB-CR' |
|
&& value != 'DEFAULT' |
|
&& value != 'SCRAM-SHA-1' |
|
&& value != 'PLAIN') |
|
throw new Error("only DEFAULT, GSSAPI, PLAIN, MONGODB-X509, SCRAM-SHA-1 or MONGODB-CR is supported by authMechanism"); |
|
|
|
// Authentication mechanism |
|
dbOptions.authMechanism = value; |
|
break; |
|
case 'authMechanismProperties': |
|
// Split up into key, value pairs |
|
var values = value.split(','); |
|
var o = {}; |
|
// For each value split into key, value |
|
values.forEach(function(x) { |
|
var v = x.split(':'); |
|
o[v[0]] = v[1]; |
|
}); |
|
|
|
// Set all authMechanismProperties |
|
dbOptions.authMechanismProperties = o; |
|
// Set the service name value |
|
if(typeof o.SERVICE_NAME == 'string') dbOptions.gssapiServiceName = o.SERVICE_NAME; |
|
if(typeof o.SERVICE_REALM == 'string') dbOptions.gssapiServiceRealm = o.SERVICE_REALM; |
|
if(typeof o.CANONICALIZE_HOST_NAME == 'string') dbOptions.gssapiCanonicalizeHostName = o.CANONICALIZE_HOST_NAME == 'true' ? true : false; |
|
break; |
|
case 'wtimeoutMS': |
|
dbOptions.wtimeout = parseInt(value, 10); |
|
break; |
|
case 'readPreference': |
|
if(!ReadPreference.isValid(value)) throw new Error("readPreference must be either primary/primaryPreferred/secondary/secondaryPreferred/nearest"); |
|
dbOptions.readPreference = value; |
|
break; |
|
case 'readPreferenceTags': |
|
// Decode the value |
|
value = decodeURIComponent(value); |
|
// Contains the tag object |
|
var tagObject = {}; |
|
if(value == null || value == '') { |
|
dbOptions.read_preference_tags.push(tagObject); |
|
break; |
|
} |
|
|
|
// Split up the tags |
|
var tags = value.split(/\,/); |
|
for(var i = 0; i < tags.length; i++) { |
|
var parts = tags[i].trim().split(/\:/); |
|
tagObject[parts[0]] = parts[1]; |
|
} |
|
|
|
// Set the preferences tags |
|
dbOptions.read_preference_tags.push(tagObject); |
|
break; |
|
default: |
|
break; |
|
} |
|
}); |
|
|
|
// No tags: should be null (not []) |
|
if(dbOptions.read_preference_tags.length === 0) { |
|
dbOptions.read_preference_tags = null; |
|
} |
|
|
|
// Validate if there are an invalid write concern combinations |
|
if((dbOptions.w == -1 || dbOptions.w == 0) && ( |
|
dbOptions.journal == true |
|
|| dbOptions.fsync == true |
|
|| dbOptions.safe == true)) throw new Error("w set to -1 or 0 cannot be combined with safe/w/journal/fsync") |
|
|
|
// If no read preference set it to primary |
|
if(!dbOptions.readPreference) { |
|
dbOptions.readPreference = 'primary'; |
|
} |
|
|
|
// Add servers to result |
|
object.servers = servers; |
|
// Returned parsed object |
|
return object; |
|
}
|
|
|