url_parser.js 12 KB

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