url_parser.js 14 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405
  1. "use strict";
  2. var ReadPreference = require('./read_preference'),
  3. parser = require('url'),
  4. f = require('util').format;
  5. module.exports = function(url) {
  6. // Variables
  7. var connection_part = '';
  8. var auth_part = '';
  9. var query_string_part = '';
  10. var dbName = 'admin';
  11. // Url parser result
  12. var result = parser.parse(url, true);
  13. if(result.protocol != 'mongodb:') {
  14. throw new Error('invalid schema, expected mongodb');
  15. }
  16. if((result.hostname == null || result.hostname == '') && url.indexOf('.sock') == -1) {
  17. throw new Error('no hostname or hostnames provided in connection string');
  18. }
  19. if(result.port == '0') {
  20. throw new Error('invalid port (zero) with hostname');
  21. }
  22. if(!isNaN(parseInt(result.port, 10)) && parseInt(result.port, 10) > 65535) {
  23. throw new Error('invalid port (larger than 65535) with hostname');
  24. }
  25. if(result.path
  26. && result.path.length > 0
  27. && result.path[0] != '/'
  28. && url.indexOf('.sock') == -1) {
  29. throw new Error('missing delimiting slash between hosts and options');
  30. }
  31. if(result.query) {
  32. for(var name in result.query) {
  33. if(name.indexOf('::') != -1) {
  34. throw new Error('double colon in host identifier');
  35. }
  36. if(result.query[name] == '') {
  37. throw new Error('query parameter ' + name + ' is an incomplete value pair');
  38. }
  39. }
  40. }
  41. if(result.auth) {
  42. var parts = result.auth.split(':');
  43. if(url.indexOf(result.auth) != -1 && parts.length > 2) {
  44. throw new Error('Username with password containing an unescaped colon');
  45. }
  46. if(url.indexOf(result.auth) != -1 && result.auth.indexOf('@') != -1) {
  47. throw new Error('Username containing an unescaped at-sign');
  48. }
  49. }
  50. // Remove query
  51. var clean = url.split('?').shift();
  52. // Extract the list of hosts
  53. var strings = clean.split(',');
  54. var hosts = [];
  55. for(var i = 0; i < strings.length; i++) {
  56. var hostString = strings[i];
  57. if(hostString.indexOf('mongodb') != -1) {
  58. if(hostString.indexOf('@') != -1) {
  59. hosts.push(hostString.split('@').pop())
  60. } else {
  61. hosts.push(hostString.substr('mongodb://'.length));
  62. }
  63. } else if(hostString.indexOf('/') != -1) {
  64. hosts.push(hostString.split('/').shift());
  65. } else if(hostString.indexOf('/') == -1) {
  66. hosts.push(hostString.trim());
  67. }
  68. }
  69. for(i = 0; i < hosts.length; i++) {
  70. var r = parser.parse(f('mongodb://%s', hosts[i].trim()));
  71. if(r.path && r.path.indexOf(':') != -1) {
  72. throw new Error('double colon in host identifier');
  73. }
  74. }
  75. // If we have a ? mark cut the query elements off
  76. if(url.indexOf("?") != -1) {
  77. query_string_part = url.substr(url.indexOf("?") + 1);
  78. connection_part = url.substring("mongodb://".length, url.indexOf("?"))
  79. } else {
  80. connection_part = url.substring("mongodb://".length);
  81. }
  82. // Check if we have auth params
  83. if(connection_part.indexOf("@") != -1) {
  84. auth_part = connection_part.split("@")[0];
  85. connection_part = connection_part.split("@")[1];
  86. }
  87. // Check if the connection string has a db
  88. if(connection_part.indexOf(".sock") != -1) {
  89. if(connection_part.indexOf(".sock/") != -1) {
  90. dbName = connection_part.split(".sock/")[1];
  91. // Check if multiple database names provided, or just an illegal trailing backslash
  92. if (dbName.indexOf("/") != -1) {
  93. if (dbName.split("/").length == 2 && dbName.split("/")[1].length == 0) {
  94. throw new Error('Illegal trailing backslash after database name');
  95. }
  96. throw new Error('More than 1 database name in URL');
  97. }
  98. connection_part = connection_part.split("/", connection_part.indexOf(".sock") + ".sock".length);
  99. }
  100. } else if(connection_part.indexOf("/") != -1) {
  101. // Check if multiple database names provided, or just an illegal trailing backslash
  102. if (connection_part.split("/").length > 2) {
  103. if (connection_part.split("/")[2].length == 0) {
  104. throw new Error('Illegal trailing backslash after database name');
  105. }
  106. throw new Error('More than 1 database name in URL');
  107. }
  108. dbName = connection_part.split("/")[1];
  109. connection_part = connection_part.split("/")[0];
  110. }
  111. // Result object
  112. var object = {};
  113. // Pick apart the authentication part of the string
  114. var authPart = auth_part || '';
  115. var auth = authPart.split(':', 2);
  116. // Decode the URI components
  117. auth[0] = decodeURIComponent(auth[0]);
  118. if(auth[1]){
  119. auth[1] = decodeURIComponent(auth[1]);
  120. }
  121. // Add auth to final object if we have 2 elements
  122. if(auth.length == 2) object.auth = {user: auth[0], password: auth[1]};
  123. // Variables used for temporary storage
  124. var hostPart;
  125. var urlOptions;
  126. var servers;
  127. var serverOptions = {socketOptions: {}};
  128. var dbOptions = {read_preference_tags: []};
  129. var replSetServersOptions = {socketOptions: {}};
  130. var mongosOptions = {socketOptions: {}};
  131. // Add server options to final object
  132. object.server_options = serverOptions;
  133. object.db_options = dbOptions;
  134. object.rs_options = replSetServersOptions;
  135. object.mongos_options = mongosOptions;
  136. // Let's check if we are using a domain socket
  137. if(url.match(/\.sock/)) {
  138. // Split out the socket part
  139. var domainSocket = url.substring(
  140. url.indexOf("mongodb://") + "mongodb://".length
  141. , url.lastIndexOf(".sock") + ".sock".length);
  142. // Clean out any auth stuff if any
  143. if(domainSocket.indexOf("@") != -1) domainSocket = domainSocket.split("@")[1];
  144. servers = [{domain_socket: domainSocket}];
  145. } else {
  146. // Split up the db
  147. hostPart = connection_part;
  148. // Deduplicate servers
  149. var deduplicatedServers = {};
  150. // Parse all server results
  151. servers = hostPart.split(',').map(function(h) {
  152. var _host, _port, ipv6match;
  153. //check if it matches [IPv6]:port, where the port number is optional
  154. if ((ipv6match = /\[([^\]]+)\](?:\:(.+))?/.exec(h))) {
  155. _host = ipv6match[1];
  156. _port = parseInt(ipv6match[2], 10) || 27017;
  157. } else {
  158. //otherwise assume it's IPv4, or plain hostname
  159. var hostPort = h.split(':', 2);
  160. _host = hostPort[0] || 'localhost';
  161. _port = hostPort[1] != null ? parseInt(hostPort[1], 10) : 27017;
  162. // Check for localhost?safe=true style case
  163. if(_host.indexOf("?") != -1) _host = _host.split(/\?/)[0];
  164. }
  165. // No entry returned for duplicate servr
  166. if(deduplicatedServers[_host + "_" + _port]) return null;
  167. deduplicatedServers[_host + "_" + _port] = 1;
  168. // Return the mapped object
  169. return {host: _host, port: _port};
  170. }).filter(function(x) {
  171. return x != null;
  172. });
  173. }
  174. // Get the db name
  175. object.dbName = dbName || 'admin';
  176. // Split up all the options
  177. urlOptions = (query_string_part || '').split(/[&;]/);
  178. // Ugh, we have to figure out which options go to which constructor manually.
  179. urlOptions.forEach(function(opt) {
  180. if(!opt) return;
  181. var splitOpt = opt.split('='), name = splitOpt[0], value = splitOpt[1];
  182. // Options implementations
  183. switch(name) {
  184. case 'slaveOk':
  185. case 'slave_ok':
  186. serverOptions.slave_ok = (value == 'true');
  187. dbOptions.slaveOk = (value == 'true');
  188. break;
  189. case 'maxPoolSize':
  190. case 'poolSize':
  191. serverOptions.poolSize = parseInt(value, 10);
  192. replSetServersOptions.poolSize = parseInt(value, 10);
  193. break;
  194. case 'appname':
  195. object.appname = decodeURIComponent(value);
  196. break;
  197. case 'autoReconnect':
  198. case 'auto_reconnect':
  199. serverOptions.auto_reconnect = (value == 'true');
  200. break;
  201. case 'minPoolSize':
  202. throw new Error("minPoolSize not supported");
  203. case 'maxIdleTimeMS':
  204. throw new Error("maxIdleTimeMS not supported");
  205. case 'waitQueueMultiple':
  206. throw new Error("waitQueueMultiple not supported");
  207. case 'waitQueueTimeoutMS':
  208. throw new Error("waitQueueTimeoutMS not supported");
  209. case 'uuidRepresentation':
  210. throw new Error("uuidRepresentation not supported");
  211. case 'ssl':
  212. if(value == 'prefer') {
  213. serverOptions.ssl = value;
  214. replSetServersOptions.ssl = value;
  215. mongosOptions.ssl = value;
  216. break;
  217. }
  218. serverOptions.ssl = (value == 'true');
  219. replSetServersOptions.ssl = (value == 'true');
  220. mongosOptions.ssl = (value == 'true');
  221. break;
  222. case 'sslValidate':
  223. serverOptions.sslValidate = (value == 'true');
  224. replSetServersOptions.sslValidate = (value == 'true');
  225. mongosOptions.sslValidate = (value == 'true');
  226. break;
  227. case 'replicaSet':
  228. case 'rs_name':
  229. replSetServersOptions.rs_name = value;
  230. break;
  231. case 'reconnectWait':
  232. replSetServersOptions.reconnectWait = parseInt(value, 10);
  233. break;
  234. case 'retries':
  235. replSetServersOptions.retries = parseInt(value, 10);
  236. break;
  237. case 'readSecondary':
  238. case 'read_secondary':
  239. replSetServersOptions.read_secondary = (value == 'true');
  240. break;
  241. case 'fsync':
  242. dbOptions.fsync = (value == 'true');
  243. break;
  244. case 'journal':
  245. dbOptions.j = (value == 'true');
  246. break;
  247. case 'safe':
  248. dbOptions.safe = (value == 'true');
  249. break;
  250. case 'nativeParser':
  251. case 'native_parser':
  252. dbOptions.native_parser = (value == 'true');
  253. break;
  254. case 'readConcernLevel':
  255. dbOptions.readConcern = {level: value};
  256. break;
  257. case 'connectTimeoutMS':
  258. serverOptions.socketOptions.connectTimeoutMS = parseInt(value, 10);
  259. replSetServersOptions.socketOptions.connectTimeoutMS = parseInt(value, 10);
  260. mongosOptions.socketOptions.connectTimeoutMS = parseInt(value, 10);
  261. break;
  262. case 'socketTimeoutMS':
  263. serverOptions.socketOptions.socketTimeoutMS = parseInt(value, 10);
  264. replSetServersOptions.socketOptions.socketTimeoutMS = parseInt(value, 10);
  265. mongosOptions.socketOptions.socketTimeoutMS = parseInt(value, 10);
  266. break;
  267. case 'w':
  268. dbOptions.w = parseInt(value, 10);
  269. if(isNaN(dbOptions.w)) dbOptions.w = value;
  270. break;
  271. case 'authSource':
  272. dbOptions.authSource = value;
  273. break;
  274. case 'gssapiServiceName':
  275. dbOptions.gssapiServiceName = value;
  276. break;
  277. case 'authMechanism':
  278. if(value == 'GSSAPI') {
  279. // If no password provided decode only the principal
  280. if(object.auth == null) {
  281. var urlDecodeAuthPart = decodeURIComponent(authPart);
  282. if(urlDecodeAuthPart.indexOf("@") == -1) throw new Error("GSSAPI requires a provided principal");
  283. object.auth = {user: urlDecodeAuthPart, password: null};
  284. } else {
  285. object.auth.user = decodeURIComponent(object.auth.user);
  286. }
  287. } else if(value == 'MONGODB-X509') {
  288. object.auth = {user: decodeURIComponent(authPart)};
  289. }
  290. // Only support GSSAPI or MONGODB-CR for now
  291. if(value != 'GSSAPI'
  292. && value != 'MONGODB-X509'
  293. && value != 'MONGODB-CR'
  294. && value != 'DEFAULT'
  295. && value != 'SCRAM-SHA-1'
  296. && value != 'PLAIN')
  297. throw new Error("only DEFAULT, GSSAPI, PLAIN, MONGODB-X509, SCRAM-SHA-1 or MONGODB-CR is supported by authMechanism");
  298. // Authentication mechanism
  299. dbOptions.authMechanism = value;
  300. break;
  301. case 'authMechanismProperties':
  302. // Split up into key, value pairs
  303. var values = value.split(',');
  304. var o = {};
  305. // For each value split into key, value
  306. values.forEach(function(x) {
  307. var v = x.split(':');
  308. o[v[0]] = v[1];
  309. });
  310. // Set all authMechanismProperties
  311. dbOptions.authMechanismProperties = o;
  312. // Set the service name value
  313. if(typeof o.SERVICE_NAME == 'string') dbOptions.gssapiServiceName = o.SERVICE_NAME;
  314. if(typeof o.SERVICE_REALM == 'string') dbOptions.gssapiServiceRealm = o.SERVICE_REALM;
  315. if(typeof o.CANONICALIZE_HOST_NAME == 'string') dbOptions.gssapiCanonicalizeHostName = o.CANONICALIZE_HOST_NAME == 'true' ? true : false;
  316. break;
  317. case 'wtimeoutMS':
  318. dbOptions.wtimeout = parseInt(value, 10);
  319. break;
  320. case 'readPreference':
  321. if(!ReadPreference.isValid(value)) throw new Error("readPreference must be either primary/primaryPreferred/secondary/secondaryPreferred/nearest");
  322. dbOptions.readPreference = value;
  323. break;
  324. case 'maxStalenessSeconds':
  325. dbOptions.maxStalenessSeconds = parseInt(value, 10);
  326. break;
  327. case 'readPreferenceTags':
  328. // Decode the value
  329. value = decodeURIComponent(value);
  330. // Contains the tag object
  331. var tagObject = {};
  332. if(value == null || value == '') {
  333. dbOptions.read_preference_tags.push(tagObject);
  334. break;
  335. }
  336. // Split up the tags
  337. var tags = value.split(/\,/);
  338. for(var i = 0; i < tags.length; i++) {
  339. var parts = tags[i].trim().split(/\:/);
  340. tagObject[parts[0]] = parts[1];
  341. }
  342. // Set the preferences tags
  343. dbOptions.read_preference_tags.push(tagObject);
  344. break;
  345. default:
  346. break;
  347. }
  348. });
  349. // No tags: should be null (not [])
  350. if(dbOptions.read_preference_tags.length === 0) {
  351. dbOptions.read_preference_tags = null;
  352. }
  353. // Validate if there are an invalid write concern combinations
  354. if((dbOptions.w == -1 || dbOptions.w == 0) && (
  355. dbOptions.journal == true
  356. || dbOptions.fsync == true
  357. || dbOptions.safe == true)) throw new Error("w set to -1 or 0 cannot be combined with safe/w/journal/fsync")
  358. // If no read preference set it to primary
  359. if(!dbOptions.readPreference) {
  360. dbOptions.readPreference = 'primary';
  361. }
  362. // Add servers to result
  363. object.servers = servers;
  364. // Returned parsed object
  365. return object;
  366. }