getSocketUrlParts.js 3.3 KB

12345678910111213141516171819202122232425262728293031323334353637383940414243444546474849505152535455565758596061626364656667686970717273747576777879808182838485868788899091929394
  1. const url = require('native-url');
  2. const getCurrentScriptSource = require('./getCurrentScriptSource');
  3. const parseQuery = require('./parseQuery');
  4. /**
  5. * @typedef {Object} SocketUrlParts
  6. * @property {string} [auth]
  7. * @property {string} [hostname]
  8. * @property {string} [protocol]
  9. * @property {string} [pathname]
  10. * @property {string} [port]
  11. */
  12. /**
  13. * Parse current location and Webpack's `__resourceQuery` into parts that can create a valid socket URL.
  14. * @param {string} [resourceQuery] The Webpack `__resourceQuery` string.
  15. * @returns {SocketUrlParts} The parsed URL parts.
  16. * @see https://webpack.js.org/api/module-variables/#__resourcequery-webpack-specific
  17. */
  18. function getSocketUrlParts(resourceQuery) {
  19. const scriptSource = getCurrentScriptSource();
  20. const urlParts = url.parse(scriptSource);
  21. /** @type {string | undefined} */
  22. let auth;
  23. let hostname = urlParts.hostname;
  24. let protocol = urlParts.protocol;
  25. let pathname = '/sockjs-node'; // This is hard-coded in WDS
  26. let port = urlParts.port;
  27. // FIXME:
  28. // This is a hack to work-around `native-url`'s parse method,
  29. // which filters out falsy values when concatenating the `auth` string.
  30. // In reality, we need to check for both values to correctly inject them.
  31. // Ref: GoogleChromeLabs/native-url#32
  32. // The placeholder `baseURL` is to allow parsing of relative paths,
  33. // and will have no effect if `scriptSource` is a proper URL.
  34. const authUrlParts = new URL(scriptSource, 'http://foo.bar');
  35. // Parse authentication credentials in case we need them
  36. if (authUrlParts.username) {
  37. auth = authUrlParts.username;
  38. // Since HTTP basic authentication does not allow empty username,
  39. // we only include password if the username is not empty.
  40. if (authUrlParts.password) {
  41. // Result: <username>:<password>
  42. auth = auth.concat(':', authUrlParts.password);
  43. }
  44. }
  45. // Check for IPv4 and IPv6 host addresses that corresponds to `any`/`empty`.
  46. // This is important because `hostname` can be empty for some hosts,
  47. // such as `about:blank` or `file://` URLs.
  48. const isEmptyHostname =
  49. urlParts.hostname === '0.0.0.0' || urlParts.hostname === '::' || urlParts.hostname === null;
  50. // We only re-assign the hostname if we are using HTTP/HTTPS protocols
  51. if (
  52. isEmptyHostname &&
  53. window.location.hostname &&
  54. window.location.protocol.indexOf('http') !== -1
  55. ) {
  56. hostname = window.location.hostname;
  57. }
  58. // We only re-assign `protocol` when `hostname` is available and is empty,
  59. // since otherwise we risk creating an invalid URL.
  60. // We also do this when `https` is used as it mandates the use of secure sockets.
  61. if (hostname && (isEmptyHostname || window.location.protocol === 'https:')) {
  62. protocol = window.location.protocol;
  63. }
  64. // We only re-assign port when it is not available or `empty`
  65. if (!port || port === '0') {
  66. port = window.location.port;
  67. }
  68. // If the resource query is available,
  69. // parse it and overwrite everything we received from the script host.
  70. const parsedQuery = parseQuery(resourceQuery || '');
  71. hostname = parsedQuery.sockHost || hostname;
  72. pathname = parsedQuery.sockPath || pathname;
  73. port = parsedQuery.sockPort || port;
  74. return {
  75. auth: auth,
  76. hostname: hostname,
  77. pathname: pathname,
  78. protocol: protocol,
  79. port: port,
  80. };
  81. }
  82. module.exports = getSocketUrlParts;