智慧水务管理系统 - 精河县供水工程综合管理平台

shouldBypassProxy.js 6.1KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210
  1. const LOOPBACK_HOSTNAMES = new Set(['localhost', '0.0.0.0']);
  2. const isIPv4Loopback = (host) => {
  3. const parts = host.split('.');
  4. if (parts.length !== 4) return false;
  5. if (parts[0] !== '127') return false;
  6. return parts.every((p) => /^\d+$/.test(p) && Number(p) >= 0 && Number(p) <= 255);
  7. };
  8. const isIPv6ZeroGroup = (group) => /^0{1,4}$/.test(group);
  9. // The unspecified address (IPv4 0.0.0.0 / IPv6 ::) resolves to the local host
  10. // for outbound connections, so treat it as loopback-equivalent for NO_PROXY
  11. // matching. 0.0.0.0 is covered by LOOPBACK_HOSTNAMES; this handles compressed
  12. // and full IPv6 all-zero forms so both families bypass symmetrically.
  13. const isIPv6Unspecified = (host) => {
  14. if (host === '::') return true;
  15. const compressionIndex = host.indexOf('::');
  16. if (compressionIndex !== -1) {
  17. if (compressionIndex !== host.lastIndexOf('::')) return false;
  18. const left = host.slice(0, compressionIndex);
  19. const right = host.slice(compressionIndex + 2);
  20. const leftGroups = left ? left.split(':') : [];
  21. const rightGroups = right ? right.split(':') : [];
  22. const explicitGroups = leftGroups.length + rightGroups.length;
  23. return (
  24. explicitGroups < 8 &&
  25. leftGroups.every(isIPv6ZeroGroup) &&
  26. rightGroups.every(isIPv6ZeroGroup)
  27. );
  28. }
  29. const groups = host.split(':');
  30. return groups.length === 8 && groups.every(isIPv6ZeroGroup);
  31. };
  32. const isIPv6Loopback = (host) => {
  33. // Collapse all-zero groups: any form of ::1 / 0:0:...:0:1
  34. // First, strip any leading "::" by normalising with Set lookup of common forms,
  35. // then fall back to structural check.
  36. if (host === '::1') return true;
  37. // Check IPv4-mapped IPv6 loopback: ::ffff:<v4-loopback> or ::ffff:<hex-v4-loopback>
  38. // Node's URL parser normalises ::ffff:127.0.0.1 → ::ffff:7f00:1
  39. const v4MappedDotted = host.match(/^::ffff:(\d+\.\d+\.\d+\.\d+)$/i);
  40. if (v4MappedDotted) return isIPv4Loopback(v4MappedDotted[1]);
  41. const v4MappedHex = host.match(/^::ffff:([0-9a-f]{1,4}):([0-9a-f]{1,4})$/i);
  42. if (v4MappedHex) {
  43. const high = parseInt(v4MappedHex[1], 16);
  44. // High 16 bits must start with 127 (0x7f) — i.e. 0x7f00..0x7fff
  45. return high >= 0x7f00 && high <= 0x7fff;
  46. }
  47. // Full-form ::1 variants: any number of zero groups followed by trailing 1
  48. // e.g. 0:0:0:0:0:0:0:1, 0000:...:0001
  49. const groups = host.split(':');
  50. if (groups.length === 8) {
  51. for (let i = 0; i < 7; i++) {
  52. if (!/^0+$/.test(groups[i])) return false;
  53. }
  54. return /^0*1$/.test(groups[7]);
  55. }
  56. return false;
  57. };
  58. const isLoopback = (host) => {
  59. if (!host) return false;
  60. if (LOOPBACK_HOSTNAMES.has(host)) return true;
  61. if (isIPv4Loopback(host)) return true;
  62. if (isIPv6Unspecified(host)) return true;
  63. return isIPv6Loopback(host);
  64. };
  65. const DEFAULT_PORTS = {
  66. http: 80,
  67. https: 443,
  68. ws: 80,
  69. wss: 443,
  70. ftp: 21,
  71. };
  72. const parseNoProxyEntry = (entry) => {
  73. let entryHost = entry;
  74. let entryPort = 0;
  75. if (entryHost.charAt(0) === '[') {
  76. const bracketIndex = entryHost.indexOf(']');
  77. if (bracketIndex !== -1) {
  78. const host = entryHost.slice(1, bracketIndex);
  79. const rest = entryHost.slice(bracketIndex + 1);
  80. if (rest.charAt(0) === ':' && /^\d+$/.test(rest.slice(1))) {
  81. entryPort = Number.parseInt(rest.slice(1), 10);
  82. }
  83. return [host, entryPort];
  84. }
  85. }
  86. const firstColon = entryHost.indexOf(':');
  87. const lastColon = entryHost.lastIndexOf(':');
  88. if (
  89. firstColon !== -1 &&
  90. firstColon === lastColon &&
  91. /^\d+$/.test(entryHost.slice(lastColon + 1))
  92. ) {
  93. entryPort = Number.parseInt(entryHost.slice(lastColon + 1), 10);
  94. entryHost = entryHost.slice(0, lastColon);
  95. }
  96. return [entryHost, entryPort];
  97. };
  98. // Convert IPv4-mapped IPv6 (::ffff:0:0/96 prefix) to IPv4 dotted form so both
  99. // sides of a NO_PROXY comparison see the same canonical address. Without this,
  100. // `NO_PROXY=192.168.1.5` would not match a request to `http://[::ffff:192.168.1.5]/`
  101. // (Node's URL parser normalises that to `[::ffff:c0a8:105]`), and vice-versa,
  102. // allowing the proxy-bypass policy to be circumvented by using the alternate
  103. // representation. Returns the input unchanged when not IPv4-mapped.
  104. const IPV4_MAPPED_DOTTED_RE = /^(?:::|(?:0{1,4}:){1,4}:|(?:0{1,4}:){5})ffff:(\d+\.\d+\.\d+\.\d+)$/i;
  105. const IPV4_MAPPED_HEX_RE = /^(?:::|(?:0{1,4}:){1,4}:|(?:0{1,4}:){5})ffff:([0-9a-f]{1,4}):([0-9a-f]{1,4})$/i;
  106. const unmapIPv4MappedIPv6 = (host) => {
  107. if (typeof host !== 'string' || host.indexOf(':') === -1) return host;
  108. const dotted = host.match(IPV4_MAPPED_DOTTED_RE);
  109. if (dotted) return dotted[1];
  110. const hex = host.match(IPV4_MAPPED_HEX_RE);
  111. if (hex) {
  112. const high = parseInt(hex[1], 16);
  113. const low = parseInt(hex[2], 16);
  114. return `${high >> 8}.${high & 0xff}.${low >> 8}.${low & 0xff}`;
  115. }
  116. return host;
  117. };
  118. const normalizeNoProxyHost = (hostname) => {
  119. if (!hostname) {
  120. return hostname;
  121. }
  122. if (hostname.charAt(0) === '[' && hostname.charAt(hostname.length - 1) === ']') {
  123. hostname = hostname.slice(1, -1);
  124. }
  125. return unmapIPv4MappedIPv6(hostname.replace(/\.+$/, ''));
  126. };
  127. export default function shouldBypassProxy(location) {
  128. let parsed;
  129. try {
  130. parsed = new URL(location);
  131. } catch (_err) {
  132. return false;
  133. }
  134. const noProxy = (process.env.no_proxy || process.env.NO_PROXY || '').toLowerCase();
  135. if (!noProxy) {
  136. return false;
  137. }
  138. if (noProxy === '*') {
  139. return true;
  140. }
  141. const port =
  142. Number.parseInt(parsed.port, 10) || DEFAULT_PORTS[parsed.protocol.split(':', 1)[0]] || 0;
  143. const hostname = normalizeNoProxyHost(parsed.hostname.toLowerCase());
  144. return noProxy.split(/[\s,]+/).some((entry) => {
  145. if (!entry) {
  146. return false;
  147. }
  148. let [entryHost, entryPort] = parseNoProxyEntry(entry);
  149. entryHost = normalizeNoProxyHost(entryHost);
  150. if (!entryHost) {
  151. return false;
  152. }
  153. if (entryPort && entryPort !== port) {
  154. return false;
  155. }
  156. if (entryHost.charAt(0) === '*') {
  157. entryHost = entryHost.slice(1);
  158. }
  159. if (entryHost.charAt(0) === '.') {
  160. return hostname.endsWith(entryHost);
  161. }
  162. return hostname === entryHost || (isLoopback(hostname) && isLoopback(entryHost));
  163. });
  164. }