[ https://issues.apache.org/jira/browse/NETBEANS-96?page=com.atlassian.jira.plugin.system.issuetabpanels:comment-tabpanel&focusedCommentId=16243716#comment-16243716 ]
ASF GitHub Bot commented on NETBEANS-96: ---------------------------------------- lbruun commented on a change in pull request #161: [NETBEANS-96] New PAC Script evaluation environment URL: https://github.com/apache/incubator-netbeans/pull/161#discussion_r149635987 ########## File path: core.network/src/org/netbeans/core/network/utils/IpAddressUtils.java ########## @@ -0,0 +1,501 @@ +/** + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +package org.netbeans.core.network.utils; + +import java.net.Inet4Address; +import java.net.Inet6Address; +import java.net.InetAddress; +import java.net.UnknownHostException; +import java.util.Arrays; +import java.util.Collections; +import java.util.Comparator; +import java.util.List; +import java.util.concurrent.Callable; +import java.util.concurrent.ExecutionException; +import java.util.concurrent.Future; +import java.util.concurrent.TimeUnit; +import java.util.concurrent.TimeoutException; +import java.util.regex.Pattern; +import org.netbeans.api.annotations.common.NonNull; +import org.openide.util.Exceptions; +import org.openide.util.RequestProcessor; + +/** + * IP address utilities. Mainly providing functionality + * for doing name resolve with explicit timeout. + * + * <p> + * TODO: Support for reverse lookup with timeout isn't implemented. Hasn't been + * any need for it. + * + * @author lbruun + */ +public class IpAddressUtils { + + private static final Pattern IPV4_PATTERN = Pattern.compile("^\\d{1,3}\\.\\d{1,3}\\.\\d{1,3}\\.\\d{1,3}$"); + private static final RequestProcessor RP = new RequestProcessor("DNSBackgroundResolvers", 10); + + private IpAddressUtils() {} + + /** + * Filters the result of a method according to IP protocol preference. + */ + public enum IpTypePreference { + /** + * Only IPv4 address(es) in the returned value. + */ + IPV4_ONLY, + /** + * Only IPv6 address(es) in the returned value. + */ + IPV6_ONLY, + /** + * Any of IPv4 or IPv6 addresses are acceptable in the returned value, + * but IPv4 address is preferred over IPv6. If the method returns + * an array then IPv4 addresses will come before IPv6 addresses. + */ + ANY_IPV4_PREF, + /** + * Any of IPv4 or IPv6 addresses are acceptable in the returned value, + * but IPv6 address is preferred over IPv4. If the method returns + * an array then IPv6 addresses will come before IPv4 addresses. + */ + ANY_IPV6_PREF, + /** + * Any of IPv4 or IPv6 addresses are acceptable in the returned value, + * but their internal preference is determined by the setting in the + * JDK, namely the {@code java.net.preferIPv6Addresses} system property. + * If this property is {@code true} then using this preference will be + * exactly as {@link #ANY_IPV6_PREF}, if {@code false} it will be + * exactly as {@link #ANY_IPV4_PREF}. + */ + ANY_JDK_PREF + } + + /** + * Performs a name service lookup with a timeout. + * + * <p>This method can be used when the JRE's default DNS timeout is not + * acceptable. The method is essentially a wrapper around + * {@link java.net.InetAddress#getAllByName(java.lang.String) InetAddress.getAllByName()}. + * + * <p>A reasonable timeout value is 4 seconds (4000 ms) as this value + * will - with the JRE's default settings - allow each DNS server in a + * list of 4 servers to be queried once. + * + * <p>If the {@code host} is a literal representation + * of an IPv4 address (using dot notation, for example {@code "192.168.1.44"}) + * or an IPv6 address (any form accepted by {@link java.net.Inet6Address}), + * then the method will convert the text into {@code InetAddress} and + * return immediately. The timeout does not apply to this case. + * + * <br> + * <br> + * <p> + * <u>Java's default DNS timeout:</u> + * <p> + * The default timeout DNS lookup is described + * <a href="http://docs.oracle.com/javase/8/docs/technotes/guides/jndi/jndi-dns.html#PROP"> + * in the documentation for JNDI</a> in properties: + * <p> + * {@code com.example.jndi.dns.timeout.initial} (defaults to 1 sec in Java 8)<br> + * {@code com.example.jndi.dns.timeout.retries} (defaults to 4 in Java 8) + * <p> + * The defaults mean that if the host OS has two servers defined in its DNS + * server search string (usually there's <i>at least</i> two) then - if both + * servers do not respond - the call to + * {@link InetAddress#getByName(java.lang.String)} will block for + * <b>30 seconds!</b> before it returns. With three servers it will be 45 seconds + * and so forth. This wait may be unacceptable in some scenarios and this is + * where this method is then a better alternative. + * <br> + * <br> + * @see #nameResolve(String, int, IpTypePreference) + * @param host either a host name or a literal IPv4 + * address in dot notation. + * @param timeoutMs Milliseconds to wait for the result from the name service + * query before aborting. A value of 0 means not to use any timeout for + * the background resolver task. In that case only the standard timeouts + * as defined in the JRE will apply. + * @param ipTypePref IP protocol filter + * @return the result of the name lookup (may be more than one address) + * @throws InterruptedException if the background task was interrupted + * @throws TimeoutException if the timeout ({@code timeoutMs}) expired + * before a result was obtained. + * @throws UnknownHostException if no IP address for the host could be + * found. + */ + public static @NonNull InetAddress[] nameResolveArr(String host, int timeoutMs, IpTypePreference ipTypePref) + throws InterruptedException, UnknownHostException, TimeoutException { + + if (looksLikeIpv6Literal(host) || looksLikeIpv4Literal(host)) { + // No DNS lookup is needed in this case so we can simply + // call directly. It won't block. + InetAddress addr = InetAddress.getByName(host); + if (ipTypePref == IpTypePreference.IPV4_ONLY && addr instanceof Inet6Address) { + throw new UnknownHostException("Mismatch between supplied literal IP address \"" + host + "\" (which is IPv6) and value of ipTypePref : " + ipTypePref); + } + if (ipTypePref == IpTypePreference.IPV6_ONLY && addr instanceof Inet4Address) { + throw new UnknownHostException("Mismatch between supplied literal IP address \"" + host + "\" (which is IPv6) and value of ipTypePref : " + ipTypePref); + } + return new InetAddress[]{addr}; + } + + Callable<InetAddress[]> lookupTask = new DnsTimeoutTask(host); + Future<InetAddress[]> future = RP.submit(lookupTask); + try { + InetAddress[] ipAddresses; + if (timeoutMs == 0) { + ipAddresses = future.get(); + } else { + ipAddresses = future.get(timeoutMs, TimeUnit.MILLISECONDS); + } + List<InetAddress> resultList = IpAddressUtilsFilter.filterInetAddresses(Arrays.asList(ipAddresses), ipTypePref); + if (resultList.isEmpty()) { + throw new UnknownHostException("A positive result was returned from name lookup for \"" + host + "\" but none that matched a filter of " + ipTypePref); + } + return resultList.toArray(new InetAddress[resultList.size()]); + + } catch (ExecutionException ex) { + Throwable cause = ex.getCause(); + if (cause instanceof UnknownHostException) { + throw (UnknownHostException) cause; + } + // Unlikely + Exceptions.printStackTrace(cause); + return new InetAddress[]{}; + } catch (TimeoutException ex) { + // If the wait times out then cancel the background job too. + // We're no longer interested in the result. + // The downside of not letting the task finish is that the + // JRE's DNS cache will then not be populated with the result + // - if a result is indeed obtained later than the timeout. + future.cancel(true); + throw new TimeoutException("No answer from name service within " + timeoutMs + " milliseconds when resolving \"" + host+ "\""); + } + } + + /** + * Performs a name service lookup with a timeout. Same as + * {@link #nameResolveArr(java.lang.String, int, org.netbeans.network.IpAddressUtils.IpTypePreference) nameResolveArr()} + * but only returns a single address. + * + * @see #nameResolveArr(String, int, IpTypePreference) + * @param host either a host name or a literal IPv4 + * address in dot notation. + * @param timeoutMs Milliseconds to wait for the result from the name service + * query before aborting. A value of 0 means not to use any timeout for + * the background resolver task. In that case only the standard timeouts + * as defined in the JRE will apply. + * @param ipTypePref IP protocol filter + * @return IP address + * @throws InterruptedException if the background task was interrupted + * @throws TimeoutException if the timeout ({@code timeoutMs}) expired + * before a result was obtained. + * @throws UnknownHostException if no IP address for the host could be + * found. + */ + public static @NonNull InetAddress nameResolve(String host, int timeoutMs, IpTypePreference ipTypePref) + throws InterruptedException, UnknownHostException, TimeoutException { + InetAddress[] ipAddresses = nameResolveArr(host, timeoutMs, ipTypePref); + // We're guaranteed the array will have length > 0 and never null, + // so the following is safe. + return ipAddresses[0]; + } + + /** + * Performs a name service lookup with a timeout. Same as + * {@link #nameResolveArr(java.lang.String, int, org.netbeans.network.IpAddressUtils.IpTypePreference) nameResolveArr()} + * but only returns a single address and uses + * {@link IpTypePreference#ANY_JDK_PREF IpTypePreference.ANY_JDK_PREF}. + * + * <p> + * There are several overloaded forms of this method. If you don't need + * any specific filtering on the type of IP address returned then use + * this method! + * + * @see #nameResolveArr(String, int, IpTypePreference) + * @param host either a host name or a literal IPv4 + * address in dot notation. + * @param timeoutMs Milliseconds to wait for the result from the name service + * query before aborting. A value of 0 means not to use any timeout for + * the background resolver task. In that case only the standard timeouts + * as defined in the JRE will apply. + * @return IP address + * @throws InterruptedException if the background task was interrupted + * @throws TimeoutException if the timeout ({@code timeoutMs}) expired + * before a result was obtained. + * @throws UnknownHostException if no IP address for the host could be + * found. + */ + public static @NonNull InetAddress nameResolve(String host, int timeoutMs) + throws InterruptedException, UnknownHostException, TimeoutException { + return nameResolve(host, timeoutMs, IpTypePreference.ANY_JDK_PREF); + } + + /** + * Validates if a string can be parsed as an IPv4 address. + * + * <p> + * The standard way to do this in Java is + * {@link java.net.InetAddress#getByName(java.lang.String)} but this method + * will block if the string is <i>not</i> an IP address literal, because it + * will query the name service. In contrast, this method relies solely on + * pattern matching techniques and will never block. + * + * @param ipAddressStr input string to be evaluated + * @return true if the string is a valid IPv4 literal. + */ + public static boolean isValidIpv4Address(String ipAddressStr) { + if (IPV4_PATTERN.matcher(ipAddressStr).matches()) { + + String[] segments = ipAddressStr.split("\\."); + + if (segments.length != 4) { + return false; + } + + for (String segment : segments) { + if (segment == null || segment.length() == 0) { + return false; + } + + // leading zeroes are not allowed within the segment. + if (segment.length() > 1 && segment.startsWith("0")) { + return false; + } + + try { + int value = Integer.parseInt(segment); + if (value > 255) { + return false; + } + } catch (NumberFormatException e) { + return false; // Unlikely. Already matched by regexp + } + } + return true; + } + return false; + } + + /** + * Does a shallow check if the argument looks like an IPv6 address as + * opposed to a host name. Note, that a return value of {@code true} doesn't + * guarantee that the argument is a <i>valid</i> IPv6 literal, but a return + * value of {@code false} is a guarantee that it is not. + * + * @param ipAddressStr + * @return true if argument looks like an IPv6 literal. + */ + public static boolean looksLikeIpv6Literal(String ipAddressStr) { + if (ipAddressStr == null) { + return false; + } + // ::d is the shortest possible form of an IPv6 address. + if (ipAddressStr.length() < 3) { + return false; + } + if (ipAddressStr.startsWith(":") || ipAddressStr.endsWith(":")) { + return true; + } + // Matches A80::8:800:200C:417A or 0A80::8:800:200C:417A + if (ipAddressStr.length() >= 5) { + if ((ipAddressStr.charAt(3) == ':') || (ipAddressStr.charAt(4) == ':')) { + return true; + } + } + return false; + } + + /** + * Does a shallow check if the argument looks like an IPv4 address (on the + * form {@code #.#.#.#}) as opposed to a host name. Note, that a return + * value of {@code true} doesn't guarantee that the argument is a + * <i>valid</i> IPv4 literal, but a return value of {@code false} is a + * guarantee that it is not. + * + * @param ipAddressStr + * @return true if argument looks like an IPv4 literal + * + */ + public static boolean looksLikeIpv4Literal(String ipAddressStr) { + if (ipAddressStr == null || ipAddressStr.isEmpty()) { + return false; + } + if (!(isAsciiDigit(ipAddressStr.charAt(0)))) { + return false; + } + int dotPos = ipAddressStr.indexOf('.'); + if (dotPos > 0) { + for(int i = 0; i < dotPos; i++) { + if (!(isAsciiDigit(ipAddressStr.charAt(0)))) { Review comment: The bug fixed. Thx. Re use of regexp. The IPV4_PATTERN is used to check for full validity. And it requires a full IPv4 pattern, e.g. `#.#.#.#` as opposed to say `#.#` which in Java world is also a valid IPv4 address. The two `looksLike` methods have a very narrow use case. I've tried to explain that in the Javadoc (improved). They are not validity tests. If the user actually wants full validity check he should use `isValidIpv4Address()` as opposed to `looksLikeIpv4Literal()`. The former is actually now unused in the module but I've kept it for completeness and because it may have value for those implementing their own version of PAC evaluator. This is also the reason why the two `looksLike` methods are not private, even if they have a very narrow use case. Finding out if a string is a host name or an IP literal is somewhat difficult. The problem is that an all digit string is actually a valid hostname (what wise guy came up with that RFC??), but a digit is not allowed as the first character in a domain name. Example: "123.45" is a valid IPv4 address, but it is not a valid hostname, because of the '4'. However, the corner case value of "123" is indeterminate. This is explained in the Javadoc. ---------------------------------------------------------------- This is an automated message from the Apache Git Service. To respond to the message, please log on GitHub and use the URL above to go to the specific comment. For queries about this service, please contact Infrastructure at: us...@infra.apache.org > New PAC Script evaluator > ------------------------ > > Key: NETBEANS-96 > URL: https://issues.apache.org/jira/browse/NETBEANS-96 > Project: NetBeans > Issue Type: Improvement > Reporter: lbruun > Labels: pull-request-available > > The current [PAC script|https://en.wikipedia.org/wiki/Proxy_auto-config] > evaluator (in {{core.network}}) was developed pre-Nashorn and has a few > problems: > * It simply fails with Nashorn - but not with Rhino - if the downloaded > script uses {{isInNet()}}. This was reported in [Bug > 245116|https://netbeans.org/bugzilla/show_bug.cgi?id=245116]. It fails > silently in this case and defaults to no proxy. The user will never know the > reason - not even by looking in the message log - that there was an error. > * It doesn't implement two mandatory JavaScript helper methods, > {{dnsResolve()}} and {{myIpAddress()}}. This is a known issue. This causes > many PAC scripts to silently fail. > * It doesn't implement Microsoft's IPv6-aware additions to the PAC standard. > This is a problem in MS shops because they will have designed their PAC > script to be compatible with MS IE and MS Edge (which unsurprisingly support > these functions .. as do Chrome). > * It uses a small JavaScript helper, {{nsProxyAutoConfig.js}}, which uses a > license which is not compatible with Apache. This is described in NETBEANS-4. > * Isn't executing the downloaded PAC script in a sandboxed environment. (The > PAC script should be treated as hostile because the download may have been > spoofed. Browsers indeed treat the PAC script as hostile and so should > NetBeans). > Pull Request with a new implementation is on its way. -- This message was sent by Atlassian JIRA (v6.4.14#64029)