001/*
002 * Licensed to the Apache Software Foundation (ASF) under one or more
003 * contributor license agreements.  See the NOTICE file distributed with
004 * this work for additional information regarding copyright ownership.
005 * The ASF licenses this file to You under the Apache License, Version 2.0
006 * (the "License"); you may not use this file except in compliance with
007 * the License.  You may obtain a copy of the License at
008 *
009 *      http://www.apache.org/licenses/LICENSE-2.0
010 *
011 * Unless required by applicable law or agreed to in writing, software
012 * distributed under the License is distributed on an "AS IS" BASIS,
013 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
014 * See the License for the specific language governing permissions and
015 * limitations under the License.
016 */
017package org.apache.juneau.http.remote;
018
019import static org.apache.juneau.commons.utils.Shorts.*;
020
021import java.net.*;
022import java.util.*;
023import java.util.concurrent.atomic.*;
024import java.util.regex.*;
025import java.util.stream.*;
026
027/**
028 * Shared SSRF-hardening policy for the {@link Remote @Remote}/{@link RemoteOp @RemoteOp} proxy engines.
029 *
030 * <p>
031 * Replaces the earlier scheme-only guard (<c>requireHttpScheme</c>) with a <b>deny-private</b> default policy:
032 * an absolute <c>http</c>/<c>https</c> {@code @Remote} URL (an explicit {@code @Url} value, a {@code baseUrl()}
033 * override, or an already-absolute default-path {@code fullPath}) is additionally checked against a deny-list of
034 * loopback, RFC1918/link-local, and cloud-metadata targets &mdash; both lexically (the literal host string) and,
035 * separately, against the <b>resolved</b> address actually connected to ("pin-on-connect", see
036 * {@link #selectAllowedAddress(String, boolean, AddressResolver)}).
037 *
038 * <p>
039 * Scheme-less (relative) values are unaffected &mdash; they resolve against the operator-configured client root,
040 * which is not re-checked by this policy.
041 *
042 * <p>
043 * Both engines (next-generation {@code RestClient.remote(...)} and classic {@code RestClient.getRemote(...)}) call
044 * into this single shared class so the deny-list and pin-selection logic are defined exactly once.
045 *
046 * @since 10.0.0
047 */
048@SuppressWarnings({
049   "java:S125", // Comments document URI-template blanking; they are not commented-out code.
050   "java:S3516" // requireAllowedUrl is a validation passthrough: success returns the same URL that was supplied.
051})
052public final class RemoteUrlPolicy {
053
054   private RemoteUrlPolicy() {}
055
056   /** The maximum number of redirect hops a policy-covered {@code @Remote} call will follow before failing. */
057   public static final int MAX_REDIRECT_HOPS = 5;
058
059   /** The {@code RestClient} builder/settings key for the {@code allowPrivateUrls} opt-in (system property fallback). */
060   public static final String ALLOW_PRIVATE_URLS_PROPERTY = "RestClient.allowPrivateUrls";
061
062   /**
063    * The system property holding the narrow lab allow-list: a comma- or whitespace-separated list of CIDR ranges
064    * (e.g. {@code 198.18.0.0/15,192.0.0.8}) that re-permit addresses denied only because they fall in one of the
065    * special-purpose ranges <c>192.0.0.0/24</c>, <c>198.18.0.0/15</c>, <c>224.0.0.0/4</c>, <c>240.0.0.0/4</c> or
066    * <c>ff00::/8</c>.  Unset or blank (the default) allows nothing.
067    *
068    * <p>
069    * The allow is narrow by construction: every entry must lie entirely inside one of those five ranges (anything
070    * else, including <c>0.0.0.0/0</c> and <c>::/0</c>, is rejected with an {@link IllegalArgumentException}), and it
071    * never re-permits loopback, link-local/metadata, private, CGNAT, this-network, Teredo, local-use NAT64 or any
072    * other denied address, unlike the all-or-nothing {@link #ALLOW_PRIVATE_URLS_PROPERTY} opt-in.  An IPv4 entry
073    * matches an IPv4 address or the IPv4 address embedded in an IPv6 form; an IPv6 entry matches only a native
074    * <c>ff00::/8</c> address.
075    */
076   public static final String ALLOW_ADDRESS_RANGES_PROPERTY = "RestClient.allowAddressRanges";
077
078   // IPv4 literal: 4 dot-separated groups of 1-3 digits (over-permissive on range; InetAddress.getByName validates).
079   private static final Pattern IPV4_LITERAL = Pattern.compile("^\\d{1,3}\\.\\d{1,3}\\.\\d{1,3}\\.\\d{1,3}$");
080
081   // Host names (case-insensitive) that are always denied by default, independent of DNS resolution.
082   private static final Set<String> DENIED_HOST_NAMES = Set.of("localhost", "localhost.localdomain");
083
084   private static final String METADATA_HOST = "metadata.google.internal";
085   private static final String METADATA_HOST_SUFFIX = "." + METADATA_HOST;
086
087   /**
088    * Resolves the {@code InetAddress} candidates for a hostname.  Injectable so pin-on-connect tests can simulate
089    * DNS rebinding / mixed A+AAAA / blocked-address resolution without depending on live (and flaky) DNS.
090    */
091   @FunctionalInterface
092   public interface AddressResolver {
093
094      /**
095       * Resolves {@code host} to its candidate addresses.
096       *
097       * @param host The hostname (or IP literal) to resolve.
098       * @return The resolved candidate addresses. Never <jk>null</jk> or empty on success.
099       * @throws UnknownHostException If the host cannot be resolved.
100       */
101      InetAddress[] resolve(String host) throws UnknownHostException;
102
103      /** The production resolver: real DNS resolution via {@link InetAddress#getAllByName(String)}. */
104      AddressResolver DEFAULT = InetAddress::getAllByName;
105   }
106
107   /**
108    * Enforces the SSRF guardrail on an effective {@code @Remote} URL: when {@code url} carries a URI scheme, it
109    * must be a canonical absolute {@code http}/{@code https} URL with a nonempty host, and &mdash; unless
110    * {@code allowPrivateUrls} is set &mdash; the literal host must not match the deny-list (see class javadoc).
111    * Scheme-less (relative) values pass through unchanged (they resolve against the client root, which is not
112    * re-checked here).
113    *
114    * <p>
115    * This is the <b>pre-check</b> stage only.  It does not resolve DNS; a hostname that is not itself denied but
116    * resolves to a denied address is instead caught by {@link #selectAllowedAddress(String, boolean, AddressResolver)}
117    * ("pin-on-connect") at connect time, including on every followed redirect hop.
118    *
119    * @param url The effective URL/path to validate.
120    * @param allowPrivateUrls When <jk>true</jk>, the deny-list check is skipped (the http/https scheme requirement
121    *    and canonical-URI requirement still apply).
122    * @return The unchanged {@code url}.
123    * @throws IllegalArgumentException If {@code url} is absolute and violates the policy.
124    */
125   public static String requireAllowedUrl(String url, boolean allowPrivateUrls) {
126      return requireAllowedUrl(url, allowPrivateUrls, AllowedRanges.NONE);
127   }
128
129   /**
130    * Same as {@link #requireAllowedUrl(String, boolean)}, additionally re-permitting literal hosts that fall inside
131    * the per-client {@code allowedRanges} (unioned with the {@value #ALLOW_ADDRESS_RANGES_PROPERTY} system property).
132    *
133    * @param url The effective URL/path to validate.
134    * @param allowPrivateUrls When <jk>true</jk>, the deny-list check is skipped.
135    * @param allowedRanges The per-client narrow allow-list. Must not be <jk>null</jk>.
136    * @return The unchanged {@code url}.
137    * @throws IllegalArgumentException If {@code url} is absolute and violates the policy.
138    */
139   public static String requireAllowedUrl(String url, boolean allowPrivateUrls, AllowedRanges allowedRanges) {
140      var scheme = schemeOf(url);
141      if (scheme == null)
142         return url; // Scheme-less relative value; resolved against the client root, not re-checked here.
143
144      if (! (eqic(scheme, "http") || eqic(scheme, "https")))
145         throw iaex("Unsupported URL scheme '%s' in @Remote URL override; only http/https are allowed: %s", scheme, url);
146
147      URI uri;
148      try {
149         // SVL/@Path {var} tokens (e.g. "http://host/u/{id}") are resolved later, at request-build time -- {}
150         // is not valid URI syntax, so blank out any such token before parsing (the sanitized copy is used only
151         // to classify scheme/host here; the original url, template tokens intact, is what is returned/used).
152         uri = new URI(blankOutTemplateTokens(url));
153      } catch (URISyntaxException e) {
154         throw iaex(e, "Invalid @Remote URL override: %s", url);
155      }
156      var host = uri.getHost();
157      if (ie(host))
158         throw iaex("@Remote URL override has no host (non-canonical or opaque form is not allowed): %s", url);
159
160      if (! allowPrivateUrls && isDeniedHost(host, allowedRanges))
161         throw iaex("@Remote URL override targets a private/loopback/link-local/metadata/reserved host (including CGNAT, this-network, special-purpose/multicast/reserved, Teredo and local-use NAT64 ranges), which is denied by default (set allowPrivateUrls to allow local-dev targets, or list a special-purpose range such as 198.18.0.0/15 in the RestClient.allowAddressRanges system property or the client's allowAddressRanges setting): %s", url);
162
163      return url;
164   }
165
166   // Matches {name} SVL/@Path template tokens (no nested braces) so requireAllowedUrl can parse an unsubstituted
167   // @Url/@RemoteOp(path) value (tokens are filled in later, at request-build time) as a well-formed java.net.URI.
168   private static final Pattern TEMPLATE_TOKEN = Pattern.compile("\\{[^{}]*\\}");
169
170   private static String blankOutTemplateTokens(String url) {
171      return url.indexOf('{') == -1 ? url : TEMPLATE_TOKEN.matcher(url).replaceAll("x");
172   }
173
174   /**
175    * Returns <jk>true</jk> if the literal {@code host} string is denied by the default policy: a dotted-decimal
176    * IPv4/bracketed-IPv6 literal classified as denied by {@link #isDeniedAddress(InetAddress)} (an IPv6 zone id is
177    * ignored, and an unparseable literal containing a {@code :} is denied),
178    * or a case-insensitive match on {@code localhost}/{@code localhost.localdomain}/{@code metadata.google.internal}
179    * (and its subdomains).
180    *
181    * <p>
182    * A bare decimal/octal/hex IP literal (e.g. {@code http://2130706433}) does <b>not</b> look like a dotted IPv4
183    * literal and is intentionally left to {@link #selectAllowedAddress(String, boolean, AddressResolver)} (resolved
184    * at pin-on-connect time), not this lexical pre-check.
185    *
186    * @param host The literal host string (from {@link URI#getHost()}).
187    * @return <jk>true</jk> if the host is denied by the default policy.
188    */
189   public static boolean isDeniedHost(String host) {
190      return isDeniedHost(host, AllowedRanges.NONE);
191   }
192
193   /**
194    * Same as {@link #isDeniedHost(String)}, taking the per-client {@code allowedRanges} into account.
195    *
196    * @param host The literal host string (from {@link URI#getHost()}).
197    * @param allowedRanges The per-client narrow allow-list. Must not be <jk>null</jk>.
198    * @return <jk>true</jk> if the host is denied.
199    */
200   public static boolean isDeniedHost(String host, AllowedRanges allowedRanges) {
201      var h = host.toLowerCase(Locale.ROOT);
202      if (DENIED_HOST_NAMES.contains(h) || h.equals(METADATA_HOST) || h.endsWith(METADATA_HOST_SUFFIX))
203         return true;
204      if (looksLikeIpLiteral(host)) {
205         var literal = stripZoneId(host);
206         try {
207            return isDeniedAddress(InetAddress.getByName(literal), allowedRanges);
208         } catch (UnknownHostException e) {
209            // A colon-containing literal that cannot be parsed fails closed; a dotted-decimal form that is not valid
210            // (e.g. an octet > 255) is treated as an ordinary (non-denied) name and left to pin-on-connect.
211            return literal.indexOf(':') != -1;
212         }
213      }
214      return false;
215   }
216
217   // Removes an IPv6 zone/scope id (the '%...' suffix, or '%25...' as percent-encoded in a URL) from a possibly
218   // bracketed literal: "[fe80::1%25en0]" -> "[fe80::1]".  The zone does not affect the address classification.
219   private static String stripZoneId(String host) {
220      var i = host.indexOf('%');
221      if (i == -1)
222         return host;
223      return host.endsWith("]") ? host.substring(0, i) + "]" : host.substring(0, i);
224   }
225
226   /**
227    * Returns <jk>true</jk> if {@code host} is textually shaped like an IP literal (a dotted-decimal IPv4 form, or
228    * any form containing a {@code :} as in bracket-stripped IPv6) -- i.e. a form worth parsing locally (no DNS
229    * lookup) to classify without deferring to pin-on-connect.
230    */
231   private static boolean looksLikeIpLiteral(String host) {
232      return IPV4_LITERAL.matcher(host).matches() || host.indexOf(':') != -1;
233   }
234
235   /**
236    * Returns <jk>true</jk> if {@code addr} is denied by the default policy -- the deny-list applied both to
237    * literal-IP hosts (pre-check) and to resolved addresses (pin-on-connect).
238    *
239    * <p>
240    * Denied addresses:
241    * <ul>
242    *    <li>Loopback, unspecified, link-local (including the <c>169.254.169.254</c> metadata address) and RFC1918
243    *       site-local addresses.
244    *    <li>IPv4 <c>0.0.0.0/8</c> ("this network") and <c>100.64.0.0/10</c> (carrier-grade NAT shared space).
245    *    <li>IPv4 special-purpose ranges that are never legitimate remote targets: <c>192.0.0.0/24</c> (IETF protocol
246    *       assignments), <c>198.18.0.0/15</c> (benchmarking; often lab or appliance networks &mdash; see
247    *       {@link #ALLOW_ADDRESS_RANGES_PROPERTY}), <c>224.0.0.0/4</c> (multicast) and <c>240.0.0.0/4</c> (reserved,
248    *       including the limited broadcast address <c>255.255.255.255</c>).
249    *    <li>IPv6 unique-local <c>fc00::/7</c> and multicast <c>ff00::/8</c>.
250    *    <li>IPv6 Teredo <c>2001::/32</c>, denied outright because the embedded client IPv4 address is obfuscated.
251    *    <li>IPv6 local-use NAT64 <c>64:ff9b:1::/48</c> (RFC 8215), denied outright because the embedding length
252    *       (/48, /56, /64 or /96) is operator-defined and cannot be determined from the address.
253    * </ul>
254    *
255    * <p>
256    * IPv6 forms that embed an IPv4 address are unwrapped and classified by the embedded IPv4 address:
257    * IPv4-mapped (<c>::ffff:a.b.c.d</c>), IPv4-compatible (<c>::a.b.c.d</c>), IPv4-translated
258    * (<c>::ffff:0:a.b.c.d</c>), NAT64 (<c>64:ff9b::/96</c>) and 6to4 (<c>2002::/16</c>, IPv4 in bytes 2-5).
259    * A public embedded address (e.g. <c>64:ff9b::808:808</c>) remains allowed.
260    *
261    * <p>
262    * Addresses inside a range listed in the {@value #ALLOW_ADDRESS_RANGES_PROPERTY} system property (empty by
263    * default) are re-permitted if they are denied only as a special-purpose range; see
264    * {@link #ALLOW_ADDRESS_RANGES_PROPERTY}.
265    *
266    * @param addr The address to classify. Must not be <jk>null</jk>.
267    * @return <jk>true</jk> if the address is denied, taking the {@value #ALLOW_ADDRESS_RANGES_PROPERTY} allow-list
268    *    into account.
269    * @throws IllegalArgumentException If the {@value #ALLOW_ADDRESS_RANGES_PROPERTY} property is malformed.
270    */
271   public static boolean isDeniedAddress(InetAddress addr) {
272      return isDeniedAddress(addr, AllowedRanges.NONE);
273   }
274
275   /**
276    * Same as {@link #isDeniedAddress(InetAddress)}, additionally re-permitting special-purpose addresses inside the
277    * per-client {@code allowedRanges}.  The effective allow-list is the <b>union</b> of those ranges and the
278    * {@value #ALLOW_ADDRESS_RANGES_PROPERTY} system property; both are bounded to the same five special-purpose
279    * ranges, so the union can never re-open any other deny.
280    *
281    * @param addr The address to classify. Must not be <jk>null</jk>.
282    * @param allowedRanges The per-client narrow allow-list. Must not be <jk>null</jk>.
283    * @return <jk>true</jk> if the address is denied.
284    * @throws IllegalArgumentException If the {@value #ALLOW_ADDRESS_RANGES_PROPERTY} property is malformed.
285    */
286   public static boolean isDeniedAddress(InetAddress addr, AllowedRanges allowedRanges) {
287      var ranges = parseAllowedRanges(System.getProperty(ALLOW_ADDRESS_RANGES_PROPERTY));
288      if (! allowedRanges.isEmpty()) {
289         var union = new ArrayList<>(ranges);
290         union.addAll(allowedRanges.ranges);
291         ranges = union;
292      }
293      return isDenied(addr, ranges);
294   }
295
296   // Same as isDeniedAddress(InetAddress) but with an explicit allow-list spec instead of the system property.
297   static boolean isDeniedAddress(InetAddress addr, String allowedRanges) {
298      return isDenied(addr, parseAllowedRanges(allowedRanges));
299   }
300
301   private static boolean isDenied(InetAddress addr, List<Cidr> ranges) {
302      if (isDeniedOther(addr))
303         return true; // Never re-permitted: only special-purpose-range denials can be allowed.
304      if (! isSpecialPurpose(unwrapEmbeddedIpv4(addr)))
305         return false;
306      if (ranges.isEmpty())
307         return true;
308      var raw = addr.getAddress();
309      var embedded = unwrapEmbeddedIpv4(addr).getAddress();
310      for (var r : ranges)
311         if (r.contains(raw) || r.contains(embedded))
312            return false;
313      return true;
314   }
315
316   /** A parsed CIDR range. */
317   private record Cidr(byte[] prefix, int bits) {
318      @Override /* Object */
319      public boolean equals(Object o) {
320         return o instanceof Cidr other && bits == other.bits && Arrays.equals(prefix, other.prefix);
321      }
322
323      @Override /* Object */
324      public int hashCode() {
325         return 31 * Arrays.hashCode(prefix) + bits;
326      }
327
328      @Override /* Object */
329      public String toString() {
330         return "Cidr[prefix=" + Arrays.toString(prefix) + ", bits=" + bits + "]";
331      }
332
333      boolean contains(byte[] b) {
334         if (b.length != prefix.length)
335            return false;
336         for (var i = 0; i < bits; i++)
337            if (((b[i >> 3] ^ prefix[i >> 3]) & (0x80 >> (i & 7))) != 0)
338               return false;
339         return true;
340      }
341   }
342
343   /**
344    * A validated, parsed per-client allow-list of special-purpose address ranges, the per-client counterpart of the
345    * {@value RemoteUrlPolicy#ALLOW_ADDRESS_RANGES_PROPERTY} system property.
346    *
347    * <p>
348    * It has exactly the property's narrow semantics: every entry must lie entirely inside <c>192.0.0.0/24</c>,
349    * <c>198.18.0.0/15</c>, <c>224.0.0.0/4</c>, <c>240.0.0.0/4</c> or <c>ff00::/8</c> (anything else is rejected with an
350    * {@link IllegalArgumentException}), entries are parsed as literals without any DNS lookup, and the ranges never
351    * re-permit loopback, link-local/metadata, private, CGNAT or any other denied address.  Instances are immutable;
352    * two instances holding the same set of ranges are equal.
353    *
354    * @since 10.0.0
355    */
356   public static final class AllowedRanges {
357
358      /** The empty allow-list, which re-permits nothing. */
359      public static final AllowedRanges NONE = new AllowedRanges(List.of());
360
361      private final List<Cidr> ranges;
362      private final Set<String> key;
363
364      private AllowedRanges(List<Cidr> ranges) {
365         this.ranges = ranges;
366         this.key = ranges.stream().map(r -> Arrays.toString(r.prefix()) + "/" + r.bits()).collect(Collectors.toCollection(TreeSet::new));
367      }
368
369      /**
370       * Parses and validates allow-list entries.
371       *
372       * @param specs The entries; each element may itself hold several comma- or whitespace-separated CIDR ranges or
373       *    single addresses.  <jk>null</jk> or empty yields {@link #NONE}.
374       * @return The parsed allow-list. Never <jk>null</jk>.
375       * @throws IllegalArgumentException If any entry is malformed or not entirely inside one of the five
376       *    special-purpose ranges.
377       */
378      public static AllowedRanges of(String...specs) {
379         if (specs == null || specs.length == 0)
380            return NONE;
381         var list = new ArrayList<Cidr>();
382         for (var spec : specs) {
383            if (spec == null)
384               throw iaex("Invalid allowed address range: null entry");
385            list.addAll(parseAllowedRangesStrict(spec));
386         }
387         return list.isEmpty() ? NONE : new AllowedRanges(List.copyOf(list));
388      }
389
390      /**
391       * Returns <jk>true</jk> if this allow-list re-permits nothing.
392       *
393       * @return <jk>true</jk> if empty.
394       */
395      public boolean isEmpty() {
396         return ranges.isEmpty();
397      }
398
399      @Override /* Object */
400      public boolean equals(Object o) {
401         return o instanceof AllowedRanges other && key.equals(other.key);
402      }
403
404      @Override /* Object */
405      public int hashCode() {
406         return key.hashCode();
407      }
408
409      @Override /* Object */
410      public String toString() {
411         return "AllowedRanges" + key;
412      }
413   }
414
415   // Parses a spec without touching the single-entry property cache; an invalid entry throws.
416   private static List<Cidr> parseAllowedRangesStrict(String spec) {
417      return Arrays.stream(spec.trim().split("[,\\s]+")).filter(token -> ! token.isEmpty()).map(RemoteUrlPolicy::parseAllowEntry).toList();
418   }
419
420   // The only ranges the allow-list may re-permit (and that every allow entry must lie within).
421   private static final List<Cidr> SPECIAL_PURPOSE = List.of(
422      new Cidr(new byte[]{(byte)192, 0, 0, 0}, 24),
423      new Cidr(new byte[]{(byte)198, 18, 0, 0}, 15),
424      new Cidr(new byte[]{(byte)224, 0, 0, 0}, 4),
425      new Cidr(new byte[]{(byte)240, 0, 0, 0}, 4),
426      new Cidr(new byte[]{(byte)0xFF, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0}, 8)
427   );
428
429   private static final Pattern ALLOW_ENTRY = Pattern.compile("^([\\d.]+|[\\dA-Fa-f.]*:[\\dA-Fa-f:.]*)(?:/(0|[1-9]\\d{0,2}))?$");
430
431   // Single-entry cache (successes and failures): the spec is normally a constant system property, so parse once per
432   // distinct value.  A failed parse is remembered by message so the same string throws again without re-parsing.
433   private record ParsedAllow(String spec, List<Cidr> ranges, String error) {}
434   private static final AtomicReference<ParsedAllow> PARSED_ALLOW = new AtomicReference<>(new ParsedAllow("", List.of(), null));
435
436   private static List<Cidr> parseAllowedRanges(String spec) {
437      if (ib(spec))
438         return List.of();
439      var cached = PARSED_ALLOW.get();
440      if (! cached.spec().equals(spec)) {
441         cached = parseAllowSpec(spec);
442         PARSED_ALLOW.set(cached);
443      }
444      if (cached.error() != null)
445         throw iaex("%s", cached.error());
446      return cached.ranges();
447   }
448
449   private static ParsedAllow parseAllowSpec(String spec) {
450      try {
451         return new ParsedAllow(spec, List.copyOf(parseAllowedRangesStrict(spec)), null);
452      } catch (IllegalArgumentException e) {
453         return new ParsedAllow(spec, List.of(), e.getMessage());
454      }
455   }
456
457   // Parses a dotted-quad IPv4 literal purely syntactically (octets 0-255, no leading zeros); returns null if invalid.
458   @SuppressWarnings({
459      "java:S1168" // null signals an invalid IPv4 literal (see comment); callers test for it
460   })
461   private static byte[] parseIpv4(String s) {
462      var parts = s.split("\\.", -1);
463      if (parts.length != 4)
464         return null;
465      var out = new byte[4];
466      for (var i = 0; i < 4; i++) {
467         var p = parts[i];
468         if (! (p.equals("0") || p.matches("[1-9]\\d{0,2}")))
469            return null;
470         var v = Integer.parseInt(p);
471         if (v > 255)
472            return null;
473         out[i] = (byte) v;
474      }
475      return out;
476   }
477
478   private static Cidr parseAllowEntry(String token) {
479      var m = ALLOW_ENTRY.matcher(token);
480      if (! m.matches())
481         throw iaex("Invalid allowed address range '%s': expected an IPv4 or IPv6 literal with an optional /prefix", token);
482      var literal = m.group(1);
483      var isIpv6 = literal.indexOf(':') != -1;
484      InetAddress a;
485      try {
486         if (isIpv6) {
487            a = InetAddress.getByName(literal); // Contains ':' and only hex/colon/dot characters, so it is parsed as a literal (never resolved via DNS).
488         } else {
489            var v4 = parseIpv4(literal);
490            if (v4 == null)
491               throw iaex("Invalid allowed address range '%s': IPv4 addresses must be dotted-quad with octets 0-255 and no leading zeros", token);
492            a = InetAddress.getByAddress(v4);
493         }
494      } catch (UnknownHostException e) {
495         throw iaex(e, "Invalid allowed address range '%s': unparseable address", token);
496      }
497      // An IPv6-syntax entry must be a native IPv6 address (not collapsed to IPv4, e.g. ::ffff:a.b.c.d).
498      if (isIpv6 != (a instanceof Inet6Address))
499         throw iaex("Invalid allowed address range '%s': IPv4-embedded IPv6 forms are not allowed here", token);
500      var prefix = a.getAddress();
501      var bits = prefix.length * 8;
502      if (m.group(2) != null) {
503         bits = Integer.parseInt(m.group(2));
504         if (bits > prefix.length * 8)
505            throw iaex("Invalid allowed address range '%s': prefix length must be 0-%s", token, prefix.length * 8);
506      }
507      for (var r : SPECIAL_PURPOSE)
508         if (r.prefix().length == prefix.length && bits >= r.bits() && r.contains(prefix))
509            return new Cidr(prefix, bits);
510      throw iaex("Invalid allowed address range '%s': must lie entirely within 192.0.0.0/24, 198.18.0.0/15, 224.0.0.0/4, 240.0.0.0/4 or ff00::/8", token);
511   }
512
513   // True if the (already unwrapped) address is in one of the special-purpose ranges.
514   private static boolean isSpecialPurpose(InetAddress a) {
515      var bytes = a.getAddress();
516      if (a instanceof Inet6Address)
517         return (bytes[0] & 0xFF) == 0xFF;
518      var b0 = bytes[0] & 0xFF;
519      var b1 = bytes[1] & 0xFF;
520      return (b0 == 192 && b1 == 0 && bytes[2] == 0)
521         || (b0 == 198 && (b1 & 0xFE) == 18)
522         || (b0 & 0xE0) == 0xE0; // 224.0.0.0/4 multicast + 240.0.0.0/4 reserved
523   }
524
525   // Every deny other than the special-purpose ranges (never subject to the allow-list).
526   private static boolean isDeniedOther(InetAddress addr) {
527      if (isTeredo(addr) || isNat64LocalUse(addr))
528         return true;
529      var a = unwrapEmbeddedIpv4(addr);
530      if (a.isLoopbackAddress() || a.isAnyLocalAddress() || a.isLinkLocalAddress() || a.isSiteLocalAddress())
531         return true;
532      var bytes = a.getAddress();
533      if (a instanceof Inet6Address)
534         return (bytes[0] & 0xFE) == 0xFC; // Unique-local fc00::/7 (top 7 bits are 1111 110).
535      // 0.0.0.0/8 ("this network") and 100.64.0.0/10 (CGNAT shared space).
536      return bytes[0] == 0 || ((bytes[0] & 0xFF) == 100 && (bytes[1] & 0xC0) == 0x40);
537   }
538
539   /** Returns <jk>true</jk> for a Teredo (<c>2001:0000::/32</c>) address. */
540   private static boolean isTeredo(InetAddress addr) {
541      if (! (addr instanceof Inet6Address))
542         return false;
543      var b = addr.getAddress();
544      return b[0] == 0x20 && b[1] == 0x01 && b[2] == 0 && b[3] == 0;
545   }
546
547   /** Returns <jk>true</jk> for a local-use NAT64 (<c>64:ff9b:1::/48</c>, RFC 8215) address. */
548   private static boolean isNat64LocalUse(InetAddress addr) {
549      if (! (addr instanceof Inet6Address))
550         return false;
551      var b = addr.getAddress();
552      return b[0] == 0 && b[1] == 0x64 && (b[2] & 0xFF) == 0xFF && (b[3] & 0xFF) == 0x9B && b[4] == 0 && b[5] == 1;
553   }
554
555   /**
556    * Unwraps an IPv6 address that embeds an IPv4 address (IPv4-mapped, IPv4-compatible, IPv4-translated, NAT64
557    * or 6to4) to the embedded {@link Inet4Address}; any other address is returned unchanged.
558    */
559   @SuppressWarnings({
560      "java:S3776" // One flat decision table over the IPv6 embedding prefixes; splitting it would scatter the byte-layout rules.
561   })
562   private static InetAddress unwrapEmbeddedIpv4(InetAddress addr) {
563      if (! (addr instanceof Inet6Address))
564         return addr;
565      var b = addr.getAddress();
566      byte[] v4 = null;
567      if (isZero(b, 0, 10)) {
568         // IPv4-mapped ::ffff:a.b.c.d, or IPv4-compatible ::a.b.c.d (but '::' and '::1' keep their IPv6 classification).
569         var mapped = (b[10] & 0xFF) == 0xFF && (b[11] & 0xFF) == 0xFF;
570         var compatible = b[10] == 0 && b[11] == 0 && (b[12] != 0 || b[13] != 0 || b[14] != 0 || (b[15] & 0xFF) > 1);
571         if (mapped || compatible)
572            v4 = new byte[]{b[12], b[13], b[14], b[15]};
573      } else if (isZero(b, 0, 8) && (b[8] & 0xFF) == 0xFF && (b[9] & 0xFF) == 0xFF && b[10] == 0 && b[11] == 0) {
574         // IPv4-translated (RFC 2765) ::ffff:0:a.b.c.d
575         v4 = new byte[]{b[12], b[13], b[14], b[15]};
576      } else if (b[0] == 0 && b[1] == 0x64 && (b[2] & 0xFF) == 0xFF && (b[3] & 0xFF) == 0x9B) {
577         // NAT64 well-known prefix 64:ff9b::/96 (RFC 6052). The local-use 64:ff9b:1::/48 is denied before unwrapping.
578         if (isZero(b, 4, 12))
579            v4 = new byte[]{b[12], b[13], b[14], b[15]};
580      } else if (b[0] == 0x20 && b[1] == 0x02) {
581         // 6to4 2002::/16: the IPv4 address is in bytes 2-5.
582         v4 = new byte[]{b[2], b[3], b[4], b[5]};
583      }
584      if (v4 == null)
585         return addr;
586      try {
587         return InetAddress.getByAddress(v4);
588      } catch (UnknownHostException e) {
589         return addr; // HTT: getByAddress with a 4-byte array never throws
590      }
591   }
592
593   // Returns true if bytes [from, to) are all zero.
594   private static boolean isZero(byte[] b, int from, int to) {
595      for (var i = from; i < to; i++)
596         if (b[i] != 0)
597            return false;
598      return true;
599   }
600
601   /**
602    * Resolves {@code host} and returns the first candidate address that is allowed by the default policy
603    * ("pin-on-connect") -- the address the caller should actually connect to.  When multiple candidates are
604    * returned (e.g. mixed A/AAAA), the first allowed one wins; a candidate that is itself denied is skipped rather
605    * than rejecting the whole resolution, so a name with one public and one loopback address may still be used.
606    *
607    * @param host The hostname (or IP literal) to resolve and pin.
608    * @param allowPrivateUrls When <jk>true</jk>, the deny-list check is skipped and the first resolved candidate is
609    *    returned unconditionally.
610    * @param resolver The address resolver to use. Pass {@link AddressResolver#DEFAULT} for real DNS, or an
611    *    injectable stub in tests to simulate rebinding/mixed-family resolution without live DNS.
612    * @return The selected address to connect to. Never <jk>null</jk>.
613    * @throws UnknownHostException If {@code host} cannot be resolved.
614    * @throws IllegalArgumentException If every resolved candidate is denied (and {@code allowPrivateUrls} is
615    *    <jk>false</jk>).
616    */
617   public static InetAddress selectAllowedAddress(String host, boolean allowPrivateUrls, AddressResolver resolver) throws UnknownHostException {
618      return selectAllowedAddress(host, allowPrivateUrls, resolver, AllowedRanges.NONE);
619   }
620
621   /**
622    * Same as {@link #selectAllowedAddress(String, boolean, AddressResolver)}, additionally re-permitting candidates
623    * inside the per-client {@code allowedRanges} (unioned with the {@value #ALLOW_ADDRESS_RANGES_PROPERTY} system property).
624    *
625    * @param host The hostname (or IP literal) to resolve and pin.
626    * @param allowPrivateUrls When <jk>true</jk>, the deny-list check is skipped.
627    * @param resolver The address resolver to use.
628    * @param allowedRanges The per-client narrow allow-list. Must not be <jk>null</jk>.
629    * @return The selected address to connect to. Never <jk>null</jk>.
630    * @throws UnknownHostException If {@code host} cannot be resolved.
631    * @throws IllegalArgumentException If every resolved candidate is denied.
632    */
633   public static InetAddress selectAllowedAddress(String host, boolean allowPrivateUrls, AddressResolver resolver, AllowedRanges allowedRanges) throws UnknownHostException {
634      var candidates = resolver.resolve(host);
635      if (allowPrivateUrls)
636         return candidates[0];
637      for (var c : candidates)
638         if (! isDeniedAddress(c, allowedRanges))
639            return c;
640      throw iaex("No allowed address for host '%s': every resolved candidate is private/loopback/link-local/reserved (set allowPrivateUrls to allow local-dev targets, or list a special-purpose range such as 198.18.0.0/15 in the RestClient.allowAddressRanges system property or the client's allowAddressRanges setting)", host);
641   }
642
643   /**
644    * Resolves a redirect {@code Location} header value against the current request URI, per RFC 7231 ยง7.1.2
645    * (relative locations are resolved against the request URI).
646    *
647    * @param from The current (pre-redirect) request URI. Must be absolute.
648    * @param location The {@code Location} header value. Must not be <jk>null</jk> or blank.
649    * @return The resolved absolute redirect-target URI.
650    * @throws IllegalArgumentException If {@code location} is blank or does not resolve to an absolute URI.
651    */
652   public static URI resolveRedirectLocation(URI from, String location) {
653      if (ib(location))
654         throw iaex("Redirect response is missing a Location header");
655      URI target;
656      try {
657         target = from.resolve(location.trim());
658      } catch (IllegalArgumentException e) {
659         throw iaex(e, "Redirect response has an invalid Location header: %s", location);
660      }
661      if (! target.isAbsolute())
662         throw iaex("Redirect Location did not resolve to an absolute URI: %s", location);
663      return target;
664   }
665
666   /**
667    * Returns the URI scheme of a URL (the token before the first {@code :} when it precedes any {@code /},
668    * {@code ?} or {@code #}), or <jk>null</jk> if the value has no scheme (e.g. a relative path or a value
669    * beginning with a {@code {var}} token).
670    *
671    * @param url The URL to inspect.
672    * @return The scheme, or <jk>null</jk> if none.
673    */
674   public static String schemeOf(String url) {
675      if (url.isEmpty() || ! Character.isLetter(url.charAt(0)))
676         return null;
677      for (var i = 0; i < url.length(); i++) {
678         var c = url.charAt(i);
679         if (c == ':')
680            return url.substring(0, i);
681         if (c == '/' || c == '?' || c == '#')
682            return null;
683         if (! (Character.isLetterOrDigit(c) || c == '+' || c == '-' || c == '.'))
684            return null;
685      }
686      return null;
687   }
688}