Add DefaultHostsFileEntriesResolver#addresses to provide all hosts file's entries for a hostname (#11246)

Motivation:
DefaultHostsFileEntriesResolver should provide all hosts file's entries for a hostname when
DnsNameResolver#resolveAll as opposed to the current implementation where only the first
entry is taken into consideration

Modification:
- Add DefaultHostsFileEntriesResolver#addresses to provide all hosts file's entries for a hostname
- Add HostsFileEntriesProvider to provide all hosts file's entries for a hostname and to keep
backwards compatibility for HostsFileEntries and HostsFileParser
- DnsNameResolver#resolveAll uses the new DefaultHostsFileEntriesResolver#addresses
- BlockHound configuration: replace HostsFileParser#parse with HostsFileEntriesProvider$ParserImpl#parse
as the latter does the parsing
- Add junit tests

Result:
Fixes #10834

* Address feedback
This commit is contained in:
Violeta Georgieva 2021-05-14 10:50:42 +03:00 committed by GitHub
parent 11e6a77fba
commit fa8f7a3510
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
8 changed files with 672 additions and 196 deletions

View File

@ -128,7 +128,7 @@ class Hidden {
"parseEtcResolverOptions"); "parseEtcResolverOptions");
builder.allowBlockingCallsInside( builder.allowBlockingCallsInside(
"io.netty.resolver.HostsFileParser", "io.netty.resolver.HostsFileEntriesProvider$ParserImpl",
"parse"); "parse");
builder.nonBlockingThreadPredicate(new Function<Predicate<Thread>, Predicate<Thread>>() { builder.nonBlockingThreadPredicate(new Function<Predicate<Thread>, Predicate<Thread>>() {

View File

@ -46,6 +46,7 @@ import io.netty.handler.codec.dns.DnsRecordType;
import io.netty.handler.codec.dns.DnsResponse; import io.netty.handler.codec.dns.DnsResponse;
import io.netty.handler.codec.dns.TcpDnsQueryEncoder; import io.netty.handler.codec.dns.TcpDnsQueryEncoder;
import io.netty.handler.codec.dns.TcpDnsResponseDecoder; import io.netty.handler.codec.dns.TcpDnsResponseDecoder;
import io.netty.resolver.DefaultHostsFileEntriesResolver;
import io.netty.resolver.HostsFileEntries; import io.netty.resolver.HostsFileEntries;
import io.netty.resolver.HostsFileEntriesResolver; import io.netty.resolver.HostsFileEntriesResolver;
import io.netty.resolver.InetNameResolver; import io.netty.resolver.InetNameResolver;
@ -693,20 +694,38 @@ public class DnsNameResolver extends InetNameResolver {
private InetAddress resolveHostsFileEntry(String hostname) { private InetAddress resolveHostsFileEntry(String hostname) {
if (hostsFileEntriesResolver == null) { if (hostsFileEntriesResolver == null) {
return null; return null;
}
InetAddress address = hostsFileEntriesResolver.address(hostname, resolvedAddressTypes);
return address == null && isLocalWindowsHost(hostname) ? LOCALHOST_ADDRESS : address;
}
private List<InetAddress> resolveHostsFileEntries(String hostname) {
if (hostsFileEntriesResolver == null) {
return null;
}
List<InetAddress> addresses;
if (hostsFileEntriesResolver instanceof DefaultHostsFileEntriesResolver) {
addresses = ((DefaultHostsFileEntriesResolver) hostsFileEntriesResolver)
.addresses(hostname, resolvedAddressTypes);
} else { } else {
InetAddress address = hostsFileEntriesResolver.address(hostname, resolvedAddressTypes); InetAddress address = hostsFileEntriesResolver.address(hostname, resolvedAddressTypes);
if (address == null && PlatformDependent.isWindows() && addresses = address != null ? Collections.singletonList(address) : null;
(LOCALHOST.equalsIgnoreCase(hostname) ||
(WINDOWS_HOST_NAME != null && WINDOWS_HOST_NAME.equalsIgnoreCase(hostname)))) {
// If we tried to resolve localhost we need workaround that windows removed localhost from its
// hostfile in later versions.
// See https://github.com/netty/netty/issues/5386
// Need a workaround for resolving the host (computer) name in case it cannot be resolved from hostfile
// See https://github.com/netty/netty/issues/11142
return LOCALHOST_ADDRESS;
}
return address;
} }
return addresses == null && isLocalWindowsHost(hostname) ?
Collections.singletonList(LOCALHOST_ADDRESS) : addresses;
}
/**
* Checks whether the given hostname is the localhost/host (computer) name on Windows OS.
* Windows OS removed the localhost/host (computer) name information from the hosts file in the later versions
* and such hostname cannot be resolved from hosts file.
* See https://github.com/netty/netty/issues/5386
* See https://github.com/netty/netty/issues/11142
*/
private static boolean isLocalWindowsHost(String hostname) {
return PlatformDependent.isWindows() &&
(LOCALHOST.equalsIgnoreCase(hostname) ||
(WINDOWS_HOST_NAME != null && WINDOWS_HOST_NAME.equalsIgnoreCase(hostname)));
} }
/** /**
@ -840,24 +859,29 @@ public class DnsNameResolver extends InetNameResolver {
final String hostname = question.name(); final String hostname = question.name();
if (type == DnsRecordType.A || type == DnsRecordType.AAAA) { if (type == DnsRecordType.A || type == DnsRecordType.AAAA) {
final InetAddress hostsFileEntry = resolveHostsFileEntry(hostname); final List<InetAddress> hostsFileEntries = resolveHostsFileEntries(hostname);
if (hostsFileEntry != null) { if (hostsFileEntries != null) {
ByteBuf content = null; List<DnsRecord> result = new ArrayList<DnsRecord>();
if (hostsFileEntry instanceof Inet4Address) { for (InetAddress hostsFileEntry : hostsFileEntries) {
if (type == DnsRecordType.A) { ByteBuf content = null;
content = Unpooled.wrappedBuffer(hostsFileEntry.getAddress()); if (hostsFileEntry instanceof Inet4Address) {
if (type == DnsRecordType.A) {
content = Unpooled.wrappedBuffer(hostsFileEntry.getAddress());
}
} else if (hostsFileEntry instanceof Inet6Address) {
if (type == DnsRecordType.AAAA) {
content = Unpooled.wrappedBuffer(hostsFileEntry.getAddress());
}
} }
} else if (hostsFileEntry instanceof Inet6Address) { if (content != null) {
if (type == DnsRecordType.AAAA) { // Our current implementation does not support reloading the hosts file,
content = Unpooled.wrappedBuffer(hostsFileEntry.getAddress()); // so use a fairly large TTL (1 day, i.e. 86400 seconds).
result.add(new DefaultDnsRawRecord(hostname, type, 86400, content));
} }
} }
if (content != null) { if (!result.isEmpty()) {
// Our current implementation does not support reloading the hosts file, trySuccess(promise, result);
// so use a fairly large TTL (1 day, i.e. 86400 seconds).
trySuccess(promise, Collections.<DnsRecord>singletonList(
new DefaultDnsRawRecord(hostname, type, 86400, content)));
return promise; return promise;
} }
} }
@ -1033,9 +1057,9 @@ public class DnsNameResolver extends InetNameResolver {
final String hostname = hostname(inetHost); final String hostname = hostname(inetHost);
InetAddress hostsFileEntry = resolveHostsFileEntry(hostname); List<InetAddress> hostsFileEntries = resolveHostsFileEntries(hostname);
if (hostsFileEntry != null) { if (hostsFileEntries != null) {
promise.setSuccess(Collections.singletonList(hostsFileEntry)); promise.setSuccess(hostsFileEntries);
return; return;
} }

View File

@ -18,10 +18,10 @@ package io.netty.resolver;
import io.netty.util.CharsetUtil; import io.netty.util.CharsetUtil;
import io.netty.util.internal.PlatformDependent; import io.netty.util.internal.PlatformDependent;
import java.net.Inet4Address;
import java.net.Inet6Address;
import java.net.InetAddress; import java.net.InetAddress;
import java.nio.charset.Charset; import java.nio.charset.Charset;
import java.util.ArrayList;
import java.util.List;
import java.util.Locale; import java.util.Locale;
import java.util.Map; import java.util.Map;
@ -30,21 +30,47 @@ import java.util.Map;
*/ */
public final class DefaultHostsFileEntriesResolver implements HostsFileEntriesResolver { public final class DefaultHostsFileEntriesResolver implements HostsFileEntriesResolver {
private final Map<String, Inet4Address> inet4Entries; private final Map<String, List<InetAddress>> inet4Entries;
private final Map<String, Inet6Address> inet6Entries; private final Map<String, List<InetAddress>> inet6Entries;
public DefaultHostsFileEntriesResolver() { public DefaultHostsFileEntriesResolver() {
this(parseEntries()); this(parseEntries());
} }
// for testing purpose only // for testing purpose only
DefaultHostsFileEntriesResolver(HostsFileEntries entries) { DefaultHostsFileEntriesResolver(HostsFileEntriesProvider entries) {
inet4Entries = entries.inet4Entries(); inet4Entries = entries.ipv4Entries();
inet6Entries = entries.inet6Entries(); inet6Entries = entries.ipv6Entries();
} }
@Override @Override
public InetAddress address(String inetHost, ResolvedAddressTypes resolvedAddressTypes) { public InetAddress address(String inetHost, ResolvedAddressTypes resolvedAddressTypes) {
String normalized = normalize(inetHost);
switch (resolvedAddressTypes) {
case IPV4_ONLY:
return firstAddress(inet4Entries.get(normalized));
case IPV6_ONLY:
return firstAddress(inet6Entries.get(normalized));
case IPV4_PREFERRED:
InetAddress inet4Address = firstAddress(inet4Entries.get(normalized));
return inet4Address != null ? inet4Address : firstAddress(inet6Entries.get(normalized));
case IPV6_PREFERRED:
InetAddress inet6Address = firstAddress(inet6Entries.get(normalized));
return inet6Address != null ? inet6Address : firstAddress(inet4Entries.get(normalized));
default:
throw new IllegalArgumentException("Unknown ResolvedAddressTypes " + resolvedAddressTypes);
}
}
/**
* Resolves all addresses of a hostname against the entries in a hosts file, depending on the specified
* {@link ResolvedAddressTypes}.
*
* @param inetHost the hostname to resolve
* @param resolvedAddressTypes the address types to resolve
* @return all matching addresses or {@code null} in case the hostname cannot be resolved
*/
public List<InetAddress> addresses(String inetHost, ResolvedAddressTypes resolvedAddressTypes) {
String normalized = normalize(inetHost); String normalized = normalize(inetHost);
switch (resolvedAddressTypes) { switch (resolvedAddressTypes) {
case IPV4_ONLY: case IPV4_ONLY:
@ -52,11 +78,13 @@ public final class DefaultHostsFileEntriesResolver implements HostsFileEntriesRe
case IPV6_ONLY: case IPV6_ONLY:
return inet6Entries.get(normalized); return inet6Entries.get(normalized);
case IPV4_PREFERRED: case IPV4_PREFERRED:
Inet4Address inet4Address = inet4Entries.get(normalized); List<InetAddress> allInet4Addresses = inet4Entries.get(normalized);
return inet4Address != null? inet4Address : inet6Entries.get(normalized); return allInet4Addresses != null ? allAddresses(allInet4Addresses, inet6Entries.get(normalized)) :
inet6Entries.get(normalized);
case IPV6_PREFERRED: case IPV6_PREFERRED:
Inet6Address inet6Address = inet6Entries.get(normalized); List<InetAddress> allInet6Addresses = inet6Entries.get(normalized);
return inet6Address != null? inet6Address : inet4Entries.get(normalized); return allInet6Addresses != null ? allAddresses(allInet6Addresses, inet4Entries.get(normalized)) :
inet4Entries.get(normalized);
default: default:
throw new IllegalArgumentException("Unknown ResolvedAddressTypes " + resolvedAddressTypes); throw new IllegalArgumentException("Unknown ResolvedAddressTypes " + resolvedAddressTypes);
} }
@ -67,13 +95,27 @@ public final class DefaultHostsFileEntriesResolver implements HostsFileEntriesRe
return inetHost.toLowerCase(Locale.ENGLISH); return inetHost.toLowerCase(Locale.ENGLISH);
} }
private static HostsFileEntries parseEntries() { private static List<InetAddress> allAddresses(List<InetAddress> a, List<InetAddress> b) {
List<InetAddress> result = new ArrayList<InetAddress>(a.size() + (b == null ? 0 : b.size()));
result.addAll(a);
if (b != null) {
result.addAll(b);
}
return result;
}
private static InetAddress firstAddress(List<InetAddress> addresses) {
return addresses != null && !addresses.isEmpty() ? addresses.get(0) : null;
}
private static HostsFileEntriesProvider parseEntries() {
if (PlatformDependent.isWindows()) { if (PlatformDependent.isWindows()) {
// Ony windows there seems to be no standard for the encoding used for the hosts file, so let us // Ony windows there seems to be no standard for the encoding used for the hosts file, so let us
// try multiple until we either were able to parse it or there is none left and so we return an // try multiple until we either were able to parse it or there is none left and so we return an
// empty intstance. // empty instance.
return HostsFileParser.parseSilently(Charset.defaultCharset(), CharsetUtil.UTF_16, CharsetUtil.UTF_8); return HostsFileEntriesProvider.parser()
.parseSilently(Charset.defaultCharset(), CharsetUtil.UTF_16, CharsetUtil.UTF_8);
} }
return HostsFileParser.parseSilently(); return HostsFileEntriesProvider.parser().parseSilently();
} }
} }

View File

@ -22,7 +22,9 @@ import java.util.HashMap;
import java.util.Map; import java.util.Map;
/** /**
* A container of hosts file entries * A container of hosts file entries.
* The mappings contain only the first entry per hostname.
* Consider using {@link HostsFileEntriesProvider} when mappings with all entries per hostname are needed.
*/ */
public final class HostsFileEntries { public final class HostsFileEntries {

View File

@ -0,0 +1,311 @@
/*
* Copyright 2021 The Netty Project
*
* The Netty Project 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:
*
* https://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 io.netty.resolver;
import io.netty.util.NetUtil;
import io.netty.util.internal.PlatformDependent;
import io.netty.util.internal.logging.InternalLogger;
import io.netty.util.internal.logging.InternalLoggerFactory;
import java.io.BufferedReader;
import java.io.File;
import java.io.FileInputStream;
import java.io.IOException;
import java.io.InputStreamReader;
import java.io.Reader;
import java.net.Inet4Address;
import java.net.InetAddress;
import java.nio.charset.Charset;
import java.util.ArrayList;
import java.util.Collections;
import java.util.HashMap;
import java.util.List;
import java.util.Locale;
import java.util.Map;
import java.util.regex.Pattern;
import static io.netty.util.internal.ObjectUtil.checkNotNull;
/**
* A container of hosts file entries
*/
public final class HostsFileEntriesProvider {
public interface Parser {
/**
* Parses the hosts file at standard OS location using the system default {@link Charset} for decoding.
*
* @return a new {@link HostsFileEntriesProvider}
* @throws IOException file could not be read
*/
HostsFileEntriesProvider parse() throws IOException;
/**
* Parses the hosts file at standard OS location using the given {@link Charset}s one after another until
* parse something or none is left.
*
* @param charsets the {@link Charset}s to try as file encodings when parsing
* @return a new {@link HostsFileEntriesProvider}
* @throws IOException file could not be read
*/
HostsFileEntriesProvider parse(Charset... charsets) throws IOException;
/**
* Parses the provided hosts file using the given {@link Charset}s one after another until
* parse something or none is left. In case {@link Charset}s are not provided,
* the system default {@link Charset} is used for decoding.
*
* @param file the file to be parsed
* @param charsets the {@link Charset}s to try as file encodings when parsing, in case {@link Charset}s
* are not provided, the system default {@link Charset} is used for decoding
* @return a new {@link HostsFileEntriesProvider}
* @throws IOException file could not be read
*/
HostsFileEntriesProvider parse(File file, Charset... charsets) throws IOException;
/**
* Performs the parsing operation using the provided reader of hosts file format.
*
* @param reader the reader of hosts file format
* @return a new {@link HostsFileEntriesProvider}
*/
HostsFileEntriesProvider parse(Reader reader) throws IOException;
/**
* Parses the hosts file at standard OS location using the system default {@link Charset} for decoding.
*
* @return a new {@link HostsFileEntriesProvider}
*/
HostsFileEntriesProvider parseSilently();
/**
* Parses the hosts file at standard OS location using the given {@link Charset}s one after another until
* parse something or none is left.
*
* @param charsets the {@link Charset}s to try as file encodings when parsing
* @return a new {@link HostsFileEntriesProvider}
*/
HostsFileEntriesProvider parseSilently(Charset... charsets);
/**
* Parses the provided hosts file using the given {@link Charset}s one after another until
* parse something or none is left. In case {@link Charset}s are not provided,
* the system default {@link Charset} is used for decoding.
*
* @param file the file to be parsed
* @param charsets the {@link Charset}s to try as file encodings when parsing, in case {@link Charset}s
* are not provided, the system default {@link Charset} is used for decoding
* @return a new {@link HostsFileEntriesProvider}
*/
HostsFileEntriesProvider parseSilently(File file, Charset... charsets);
}
/**
* Creates a parser for {@link HostsFileEntriesProvider}.
*
* @return a new {@link HostsFileEntriesProvider.Parser}
*/
public static Parser parser() {
return new ParserImpl();
}
static final HostsFileEntriesProvider EMPTY =
new HostsFileEntriesProvider(
Collections.<String, List<InetAddress>>emptyMap(),
Collections.<String, List<InetAddress>>emptyMap());
private final Map<String, List<InetAddress>> ipv4Entries;
private final Map<String, List<InetAddress>> ipv6Entries;
HostsFileEntriesProvider(Map<String, List<InetAddress>> ipv4Entries, Map<String, List<InetAddress>> ipv6Entries) {
this.ipv4Entries = Collections.unmodifiableMap(new HashMap<String, List<InetAddress>>(ipv4Entries));
this.ipv6Entries = Collections.unmodifiableMap(new HashMap<String, List<InetAddress>>(ipv6Entries));
}
/**
* The IPv4 entries.
*
* @return the IPv4 entries
*/
public Map<String, List<InetAddress>> ipv4Entries() {
return ipv4Entries;
}
/**
* The IPv6 entries.
*
* @return the IPv6 entries
*/
public Map<String, List<InetAddress>> ipv6Entries() {
return ipv6Entries;
}
private static final class ParserImpl implements Parser {
private static final String WINDOWS_DEFAULT_SYSTEM_ROOT = "C:\\Windows";
private static final String WINDOWS_HOSTS_FILE_RELATIVE_PATH = "\\system32\\drivers\\etc\\hosts";
private static final String X_PLATFORMS_HOSTS_FILE_PATH = "/etc/hosts";
private static final Pattern WHITESPACES = Pattern.compile("[ \t]+");
private static final InternalLogger logger = InternalLoggerFactory.getInstance(Parser.class);
@Override
public HostsFileEntriesProvider parse() throws IOException {
return parse(locateHostsFile(), Charset.defaultCharset());
}
@Override
public HostsFileEntriesProvider parse(Charset... charsets) throws IOException {
return parse(locateHostsFile(), charsets);
}
@Override
public HostsFileEntriesProvider parse(File file, Charset... charsets) throws IOException {
checkNotNull(file, "file");
checkNotNull(charsets, "charsets");
if (charsets.length == 0) {
charsets = new Charset[]{Charset.defaultCharset()};
}
if (file.exists() && file.isFile()) {
for (Charset charset : charsets) {
BufferedReader reader = new BufferedReader(
new InputStreamReader(new FileInputStream(file), charset));
try {
HostsFileEntriesProvider entries = parse(reader);
if (entries != HostsFileEntriesProvider.EMPTY) {
return entries;
}
} finally {
reader.close();
}
}
}
return HostsFileEntriesProvider.EMPTY;
}
@Override
public HostsFileEntriesProvider parse(Reader reader) throws IOException {
checkNotNull(reader, "reader");
BufferedReader buff = new BufferedReader(reader);
try {
Map<String, List<InetAddress>> ipv4Entries = new HashMap<String, List<InetAddress>>();
Map<String, List<InetAddress>> ipv6Entries = new HashMap<String, List<InetAddress>>();
String line;
while ((line = buff.readLine()) != null) {
// remove comment
int commentPosition = line.indexOf('#');
if (commentPosition != -1) {
line = line.substring(0, commentPosition);
}
// skip empty lines
line = line.trim();
if (line.isEmpty()) {
continue;
}
// split
List<String> lineParts = new ArrayList<String>();
for (String s : WHITESPACES.split(line)) {
if (!s.isEmpty()) {
lineParts.add(s);
}
}
// a valid line should be [IP, hostname, alias*]
if (lineParts.size() < 2) {
// skip invalid line
continue;
}
byte[] ipBytes = NetUtil.createByteArrayFromIpAddressString(lineParts.get(0));
if (ipBytes == null) {
// skip invalid IP
continue;
}
// loop over hostname and aliases
for (int i = 1; i < lineParts.size(); i++) {
String hostname = lineParts.get(i);
String hostnameLower = hostname.toLowerCase(Locale.ENGLISH);
InetAddress address = InetAddress.getByAddress(hostname, ipBytes);
List<InetAddress> addresses;
if (address instanceof Inet4Address) {
addresses = ipv4Entries.get(hostnameLower);
if (addresses == null) {
addresses = new ArrayList<InetAddress>();
ipv4Entries.put(hostnameLower, addresses);
}
} else {
addresses = ipv6Entries.get(hostnameLower);
if (addresses == null) {
addresses = new ArrayList<InetAddress>();
ipv6Entries.put(hostnameLower, addresses);
}
}
addresses.add(address);
}
}
return ipv4Entries.isEmpty() && ipv6Entries.isEmpty() ?
HostsFileEntriesProvider.EMPTY :
new HostsFileEntriesProvider(ipv4Entries, ipv6Entries);
} finally {
try {
buff.close();
} catch (IOException e) {
logger.warn("Failed to close a reader", e);
}
}
}
@Override
public HostsFileEntriesProvider parseSilently() {
return parseSilently(locateHostsFile(), Charset.defaultCharset());
}
@Override
public HostsFileEntriesProvider parseSilently(Charset... charsets) {
return parseSilently(locateHostsFile(), charsets);
}
@Override
public HostsFileEntriesProvider parseSilently(File file, Charset... charsets) {
try {
return parse(file, charsets);
} catch (IOException e) {
if (logger.isWarnEnabled()) {
logger.warn("Failed to load and parse hosts file at " + file.getPath(), e);
}
return HostsFileEntriesProvider.EMPTY;
}
}
private static File locateHostsFile() {
File hostsFile;
if (PlatformDependent.isWindows()) {
hostsFile = new File(System.getenv("SystemRoot") + WINDOWS_HOSTS_FILE_RELATIVE_PATH);
if (!hostsFile.exists()) {
hostsFile = new File(WINDOWS_DEFAULT_SYSTEM_ROOT + WINDOWS_HOSTS_FILE_RELATIVE_PATH);
}
} else {
hostsFile = new File(X_PLATFORMS_HOSTS_FILE_PATH);
}
return hostsFile;
}
}
}

View File

@ -15,63 +15,31 @@
*/ */
package io.netty.resolver; package io.netty.resolver;
import io.netty.util.NetUtil;
import io.netty.util.internal.PlatformDependent;
import io.netty.util.internal.logging.InternalLogger;
import io.netty.util.internal.logging.InternalLoggerFactory;
import java.io.BufferedReader;
import java.io.File; import java.io.File;
import java.io.FileInputStream;
import java.io.IOException; import java.io.IOException;
import java.io.InputStreamReader;
import java.io.Reader; import java.io.Reader;
import java.net.Inet4Address; import java.net.Inet4Address;
import java.net.Inet6Address; import java.net.Inet6Address;
import java.net.InetAddress; import java.net.InetAddress;
import java.nio.charset.Charset; import java.nio.charset.Charset;
import java.util.ArrayList;
import java.util.List; import java.util.List;
import java.util.Locale;
import java.util.HashMap; import java.util.HashMap;
import java.util.Map; import java.util.Map;
import java.util.regex.Pattern;
import static io.netty.util.internal.ObjectUtil.*;
/** /**
* A parser for hosts files. * A parser for hosts files.
* The produced mappings contain only the first entry per hostname.
* Consider using {@link HostsFileEntriesProvider} when mappings with all entries per hostname are needed.
*/ */
public final class HostsFileParser { public final class HostsFileParser {
private static final String WINDOWS_DEFAULT_SYSTEM_ROOT = "C:\\Windows";
private static final String WINDOWS_HOSTS_FILE_RELATIVE_PATH = "\\system32\\drivers\\etc\\hosts";
private static final String X_PLATFORMS_HOSTS_FILE_PATH = "/etc/hosts";
private static final Pattern WHITESPACES = Pattern.compile("[ \t]+");
private static final InternalLogger logger = InternalLoggerFactory.getInstance(HostsFileParser.class);
private static File locateHostsFile() {
File hostsFile;
if (PlatformDependent.isWindows()) {
hostsFile = new File(System.getenv("SystemRoot") + WINDOWS_HOSTS_FILE_RELATIVE_PATH);
if (!hostsFile.exists()) {
hostsFile = new File(WINDOWS_DEFAULT_SYSTEM_ROOT + WINDOWS_HOSTS_FILE_RELATIVE_PATH);
}
} else {
hostsFile = new File(X_PLATFORMS_HOSTS_FILE_PATH);
}
return hostsFile;
}
/** /**
* Parse hosts file at standard OS location using the systems default {@link Charset} for decoding. * Parse hosts file at standard OS location using the systems default {@link Charset} for decoding.
* *
* @return a {@link HostsFileEntries} * @return a {@link HostsFileEntries}
*/ */
public static HostsFileEntries parseSilently() { public static HostsFileEntries parseSilently() {
return parseSilently(Charset.defaultCharset()); return hostsFileEntries(HostsFileEntriesProvider.parser().parseSilently());
} }
/** /**
@ -82,15 +50,7 @@ public final class HostsFileParser {
* @return a {@link HostsFileEntries} * @return a {@link HostsFileEntries}
*/ */
public static HostsFileEntries parseSilently(Charset... charsets) { public static HostsFileEntries parseSilently(Charset... charsets) {
File hostsFile = locateHostsFile(); return hostsFileEntries(HostsFileEntriesProvider.parser().parseSilently(charsets));
try {
return parse(hostsFile, charsets);
} catch (IOException e) {
if (logger.isWarnEnabled()) {
logger.warn("Failed to load and parse hosts file at " + hostsFile.getPath(), e);
}
return HostsFileEntries.EMPTY;
}
} }
/** /**
@ -100,7 +60,7 @@ public final class HostsFileParser {
* @throws IOException file could not be read * @throws IOException file could not be read
*/ */
public static HostsFileEntries parse() throws IOException { public static HostsFileEntries parse() throws IOException {
return parse(locateHostsFile()); return hostsFileEntries(HostsFileEntriesProvider.parser().parse());
} }
/** /**
@ -111,7 +71,7 @@ public final class HostsFileParser {
* @throws IOException file could not be read * @throws IOException file could not be read
*/ */
public static HostsFileEntries parse(File file) throws IOException { public static HostsFileEntries parse(File file) throws IOException {
return parse(file, Charset.defaultCharset()); return hostsFileEntries(HostsFileEntriesProvider.parser().parse(file));
} }
/** /**
@ -123,23 +83,7 @@ public final class HostsFileParser {
* @throws IOException file could not be read * @throws IOException file could not be read
*/ */
public static HostsFileEntries parse(File file, Charset... charsets) throws IOException { public static HostsFileEntries parse(File file, Charset... charsets) throws IOException {
checkNotNull(file, "file"); return hostsFileEntries(HostsFileEntriesProvider.parser().parse(file, charsets));
checkNotNull(charsets, "charsets");
if (file.exists() && file.isFile()) {
for (Charset charset: charsets) {
BufferedReader reader = new BufferedReader(
new InputStreamReader(new FileInputStream(file), charset));
try {
HostsFileEntries entries = parse(reader);
if (entries != HostsFileEntries.EMPTY) {
return entries;
}
} finally {
reader.close();
}
}
}
return HostsFileEntries.EMPTY;
} }
/** /**
@ -150,75 +94,7 @@ public final class HostsFileParser {
* @throws IOException file could not be read * @throws IOException file could not be read
*/ */
public static HostsFileEntries parse(Reader reader) throws IOException { public static HostsFileEntries parse(Reader reader) throws IOException {
checkNotNull(reader, "reader"); return hostsFileEntries(HostsFileEntriesProvider.parser().parse(reader));
BufferedReader buff = new BufferedReader(reader);
try {
Map<String, Inet4Address> ipv4Entries = new HashMap<String, Inet4Address>();
Map<String, Inet6Address> ipv6Entries = new HashMap<String, Inet6Address>();
String line;
while ((line = buff.readLine()) != null) {
// remove comment
int commentPosition = line.indexOf('#');
if (commentPosition != -1) {
line = line.substring(0, commentPosition);
}
// skip empty lines
line = line.trim();
if (line.isEmpty()) {
continue;
}
// split
List<String> lineParts = new ArrayList<String>();
for (String s: WHITESPACES.split(line)) {
if (!s.isEmpty()) {
lineParts.add(s);
}
}
// a valid line should be [IP, hostname, alias*]
if (lineParts.size() < 2) {
// skip invalid line
continue;
}
byte[] ipBytes = NetUtil.createByteArrayFromIpAddressString(lineParts.get(0));
if (ipBytes == null) {
// skip invalid IP
continue;
}
// loop over hostname and aliases
for (int i = 1; i < lineParts.size(); i ++) {
String hostname = lineParts.get(i);
String hostnameLower = hostname.toLowerCase(Locale.ENGLISH);
InetAddress address = InetAddress.getByAddress(hostname, ipBytes);
if (address instanceof Inet4Address) {
Inet4Address previous = ipv4Entries.put(hostnameLower, (Inet4Address) address);
if (previous != null) {
// restore, we want to keep the first entry
ipv4Entries.put(hostnameLower, previous);
}
} else {
Inet6Address previous = ipv6Entries.put(hostnameLower, (Inet6Address) address);
if (previous != null) {
// restore, we want to keep the first entry
ipv6Entries.put(hostnameLower, previous);
}
}
}
}
return ipv4Entries.isEmpty() && ipv6Entries.isEmpty() ?
HostsFileEntries.EMPTY :
new HostsFileEntries(ipv4Entries, ipv6Entries);
} finally {
try {
buff.close();
} catch (IOException e) {
logger.warn("Failed to close a reader", e);
}
}
} }
/** /**
@ -226,4 +102,22 @@ public final class HostsFileParser {
*/ */
private HostsFileParser() { private HostsFileParser() {
} }
@SuppressWarnings("unchecked")
private static HostsFileEntries hostsFileEntries(HostsFileEntriesProvider provider) {
return provider == HostsFileEntriesProvider.EMPTY ? HostsFileEntries.EMPTY :
new HostsFileEntries((Map<String, Inet4Address>) toMapWithSingleValue(provider.ipv4Entries()),
(Map<String, Inet6Address>) toMapWithSingleValue(provider.ipv6Entries()));
}
private static Map<String, ?> toMapWithSingleValue(Map<String, List<InetAddress>> fromMapWithListValue) {
Map<String, InetAddress> result = new HashMap<String, InetAddress>();
for (Map.Entry<String, List<InetAddress>> entry : fromMapWithListValue.entrySet()) {
List<InetAddress> value = entry.getValue();
if (!value.isEmpty()) {
result.put(entry.getKey(), value.get(0));
}
}
return result;
}
} }

View File

@ -22,9 +22,16 @@ import org.junit.Test;
import java.net.Inet4Address; import java.net.Inet4Address;
import java.net.Inet6Address; import java.net.Inet6Address;
import java.net.InetAddress; import java.net.InetAddress;
import java.util.Collections;
import java.util.HashMap; import java.util.HashMap;
import java.util.List;
import java.util.Map; import java.util.Map;
import static org.junit.Assert.assertEquals;
import static org.junit.Assert.assertNotNull;
import static org.junit.Assert.assertNull;
import static org.junit.Assert.assertTrue;
public class DefaultHostsFileEntriesResolverTest { public class DefaultHostsFileEntriesResolverTest {
/** /**
@ -32,53 +39,103 @@ public class DefaultHostsFileEntriesResolverTest {
* HostsFileParser tries to resolve hostnames as case-sensitive * HostsFileParser tries to resolve hostnames as case-sensitive
*/ */
@Test @Test
public void testCaseInsensitivity() throws Exception { public void testCaseInsensitivity() {
DefaultHostsFileEntriesResolver resolver = new DefaultHostsFileEntriesResolver(); DefaultHostsFileEntriesResolver resolver = new DefaultHostsFileEntriesResolver();
//normalized somehow //normalized somehow
Assert.assertEquals(resolver.normalize("localhost"), resolver.normalize("LOCALHOST")); assertEquals(resolver.normalize("localhost"), resolver.normalize("LOCALHOST"));
} }
@Test @Test
public void shouldntFindWhenAddressTypeDoesntMatch() { public void shouldntFindWhenAddressTypeDoesntMatch() {
Map<String, Inet4Address> inet4Entries = new HashMap<String, Inet4Address>(); Map<String, List<InetAddress>> inet4Entries = new HashMap<String, List<InetAddress>>();
Map<String, Inet6Address> inet6Entries = new HashMap<String, Inet6Address>(); Map<String, List<InetAddress>> inet6Entries = new HashMap<String, List<InetAddress>>();
inet4Entries.put("localhost", NetUtil.LOCALHOST4); inet4Entries.put("localhost", Collections.<InetAddress>singletonList(NetUtil.LOCALHOST4));
DefaultHostsFileEntriesResolver resolver = DefaultHostsFileEntriesResolver resolver =
new DefaultHostsFileEntriesResolver(new HostsFileEntries(inet4Entries, inet6Entries)); new DefaultHostsFileEntriesResolver(new HostsFileEntriesProvider(inet4Entries, inet6Entries));
InetAddress address = resolver.address("localhost", ResolvedAddressTypes.IPV6_ONLY); InetAddress address = resolver.address("localhost", ResolvedAddressTypes.IPV6_ONLY);
Assert.assertNull("Should pick an IPv6 address", address); assertNull("Should pick an IPv6 address", address);
} }
@Test @Test
public void shouldPickIpv4WhenBothAreDefinedButIpv4IsPreferred() { public void shouldPickIpv4WhenBothAreDefinedButIpv4IsPreferred() {
Map<String, Inet4Address> inet4Entries = new HashMap<String, Inet4Address>(); Map<String, List<InetAddress>> inet4Entries = new HashMap<String, List<InetAddress>>();
Map<String, Inet6Address> inet6Entries = new HashMap<String, Inet6Address>(); Map<String, List<InetAddress>> inet6Entries = new HashMap<String, List<InetAddress>>();
inet4Entries.put("localhost", NetUtil.LOCALHOST4); inet4Entries.put("localhost", Collections.<InetAddress>singletonList(NetUtil.LOCALHOST4));
inet6Entries.put("localhost", NetUtil.LOCALHOST6); inet6Entries.put("localhost", Collections.<InetAddress>singletonList(NetUtil.LOCALHOST6));
DefaultHostsFileEntriesResolver resolver = DefaultHostsFileEntriesResolver resolver =
new DefaultHostsFileEntriesResolver(new HostsFileEntries(inet4Entries, inet6Entries)); new DefaultHostsFileEntriesResolver(new HostsFileEntriesProvider(inet4Entries, inet6Entries));
InetAddress address = resolver.address("localhost", ResolvedAddressTypes.IPV4_PREFERRED); InetAddress address = resolver.address("localhost", ResolvedAddressTypes.IPV4_PREFERRED);
Assert.assertTrue("Should pick an IPv4 address", address instanceof Inet4Address); assertTrue("Should pick an IPv4 address", address instanceof Inet4Address);
} }
@Test @Test
public void shouldPickIpv6WhenBothAreDefinedButIpv6IsPreferred() { public void shouldPickIpv6WhenBothAreDefinedButIpv6IsPreferred() {
Map<String, Inet4Address> inet4Entries = new HashMap<String, Inet4Address>(); Map<String, List<InetAddress>> inet4Entries = new HashMap<String, List<InetAddress>>();
Map<String, Inet6Address> inet6Entries = new HashMap<String, Inet6Address>(); Map<String, List<InetAddress>> inet6Entries = new HashMap<String, List<InetAddress>>();
inet4Entries.put("localhost", NetUtil.LOCALHOST4); inet4Entries.put("localhost", Collections.<InetAddress>singletonList(NetUtil.LOCALHOST4));
inet6Entries.put("localhost", NetUtil.LOCALHOST6); inet6Entries.put("localhost", Collections.<InetAddress>singletonList(NetUtil.LOCALHOST6));
DefaultHostsFileEntriesResolver resolver = DefaultHostsFileEntriesResolver resolver =
new DefaultHostsFileEntriesResolver(new HostsFileEntries(inet4Entries, inet6Entries)); new DefaultHostsFileEntriesResolver(new HostsFileEntriesProvider(inet4Entries, inet6Entries));
InetAddress address = resolver.address("localhost", ResolvedAddressTypes.IPV6_PREFERRED); InetAddress address = resolver.address("localhost", ResolvedAddressTypes.IPV6_PREFERRED);
Assert.assertTrue("Should pick an IPv6 address", address instanceof Inet6Address); assertTrue("Should pick an IPv6 address", address instanceof Inet6Address);
}
@Test
public void shouldntFindWhenAddressesTypeDoesntMatch() {
Map<String, List<InetAddress>> inet4Entries = new HashMap<String, List<InetAddress>>();
Map<String, List<InetAddress>> inet6Entries = new HashMap<String, List<InetAddress>>();
inet4Entries.put("localhost", Collections.<InetAddress>singletonList(NetUtil.LOCALHOST4));
DefaultHostsFileEntriesResolver resolver =
new DefaultHostsFileEntriesResolver(new HostsFileEntriesProvider(inet4Entries, inet6Entries));
List<InetAddress> addresses = resolver.addresses("localhost", ResolvedAddressTypes.IPV6_ONLY);
assertNull("Should pick an IPv6 address", addresses);
}
@Test
public void shouldPickIpv4FirstWhenBothAreDefinedButIpv4IsPreferred() {
Map<String, List<InetAddress>> inet4Entries = new HashMap<String, List<InetAddress>>();
Map<String, List<InetAddress>> inet6Entries = new HashMap<String, List<InetAddress>>();
inet4Entries.put("localhost", Collections.<InetAddress>singletonList(NetUtil.LOCALHOST4));
inet6Entries.put("localhost", Collections.<InetAddress>singletonList(NetUtil.LOCALHOST6));
DefaultHostsFileEntriesResolver resolver =
new DefaultHostsFileEntriesResolver(new HostsFileEntriesProvider(inet4Entries, inet6Entries));
List<InetAddress> addresses = resolver.addresses("localhost", ResolvedAddressTypes.IPV4_PREFERRED);
assertNotNull(addresses);
assertEquals(2, addresses.size());
assertTrue("Should pick an IPv4 address", addresses.get(0) instanceof Inet4Address);
assertTrue("Should pick an IPv6 address", addresses.get(1) instanceof Inet6Address);
}
@Test
public void shouldPickIpv6FirstWhenBothAreDefinedButIpv6IsPreferred() {
Map<String, List<InetAddress>> inet4Entries = new HashMap<String, List<InetAddress>>();
Map<String, List<InetAddress>> inet6Entries = new HashMap<String, List<InetAddress>>();
inet4Entries.put("localhost", Collections.<InetAddress>singletonList(NetUtil.LOCALHOST4));
inet6Entries.put("localhost", Collections.<InetAddress>singletonList(NetUtil.LOCALHOST6));
DefaultHostsFileEntriesResolver resolver =
new DefaultHostsFileEntriesResolver(new HostsFileEntriesProvider(inet4Entries, inet6Entries));
List<InetAddress> addresses = resolver.addresses("localhost", ResolvedAddressTypes.IPV6_PREFERRED);
assertNotNull(addresses);
assertEquals(2, addresses.size());
assertTrue("Should pick an IPv6 address", addresses.get(0) instanceof Inet6Address);
assertTrue("Should pick an IPv4 address", addresses.get(1) instanceof Inet4Address);
} }
} }

View File

@ -0,0 +1,146 @@
/*
* Copyright 2021 The Netty Project
*
* The Netty Project 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:
*
* https://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 io.netty.resolver;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.function.Executable;
import java.io.BufferedReader;
import java.io.File;
import java.io.IOException;
import java.io.Reader;
import java.io.StringReader;
import java.net.InetAddress;
import java.nio.charset.Charset;
import java.util.List;
import java.util.Map;
import static org.junit.jupiter.api.Assertions.assertEquals;
import static org.junit.jupiter.api.Assertions.assertNotNull;
import static org.junit.jupiter.api.Assertions.assertThrows;
class HostsFileEntriesProviderTest {
@Test
void testParse() throws IOException {
String hostsString = new StringBuilder()
.append("127.0.0.1 host1").append("\n") // single hostname, separated with blanks
.append("::1 host1").append("\n") // same as above, but IPv6
.append("\n") // empty line
.append("192.168.0.1\thost2").append("\n") // single hostname, separated with tabs
.append("#comment").append("\n") // comment at the beginning of the line
.append(" #comment ").append("\n") // comment in the middle of the line
.append("192.168.0.2 host3 #comment").append("\n") // comment after hostname
.append("192.168.0.3 host4 host5 host6").append("\n") // multiple aliases
.append("192.168.0.4 host4").append("\n") // host mapped to a second address, must be considered
.append("192.168.0.5 HOST7").append("\n") // uppercase host, should match lowercase host
.append("192.168.0.6 host7").append("\n") // must be considered
.toString();
HostsFileEntriesProvider entries = HostsFileEntriesProvider.parser()
.parse(new BufferedReader(new StringReader(hostsString)));
Map<String, List<InetAddress>> inet4Entries = entries.ipv4Entries();
Map<String, List<InetAddress>> inet6Entries = entries.ipv6Entries();
assertEquals(7, inet4Entries.size(), "Expected 7 IPv4 entries");
assertEquals(1, inet6Entries.size(), "Expected 1 IPv6 entries");
assertEquals(1, inet4Entries.get("host1").size());
assertEquals("127.0.0.1", inet4Entries.get("host1").get(0).getHostAddress());
assertEquals(1, inet4Entries.get("host2").size());
assertEquals("192.168.0.1", inet4Entries.get("host2").get(0).getHostAddress());
assertEquals(1, inet4Entries.get("host3").size());
assertEquals("192.168.0.2", inet4Entries.get("host3").get(0).getHostAddress());
assertEquals(2, inet4Entries.get("host4").size());
assertEquals("192.168.0.3", inet4Entries.get("host4").get(0).getHostAddress());
assertEquals("192.168.0.4", inet4Entries.get("host4").get(1).getHostAddress());
assertEquals(1, inet4Entries.get("host5").size());
assertEquals("192.168.0.3", inet4Entries.get("host5").get(0).getHostAddress());
assertEquals(1, inet4Entries.get("host6").size());
assertEquals("192.168.0.3", inet4Entries.get("host6").get(0).getHostAddress());
assertNotNull(inet4Entries.get("host7"), "Uppercase host doesn't resolve");
assertEquals(2, inet4Entries.get("host7").size());
assertEquals("192.168.0.5", inet4Entries.get("host7").get(0).getHostAddress());
assertEquals("192.168.0.6", inet4Entries.get("host7").get(1).getHostAddress());
assertEquals(1, inet6Entries.get("host1").size());
assertEquals("0:0:0:0:0:0:0:1", inet6Entries.get("host1").get(0).getHostAddress());
}
@Test
void testCharsetInputValidation() {
assertThrows(NullPointerException.class, new Executable() {
@Override
public void execute() throws IOException {
HostsFileEntriesProvider.parser().parse((Charset[]) null);
}
});
assertThrows(NullPointerException.class, new Executable() {
@Override
public void execute() throws IOException {
HostsFileEntriesProvider.parser().parse(new File(""), (Charset[]) null);
}
});
assertThrows(NullPointerException.class, new Executable() {
@Override
public void execute() {
HostsFileEntriesProvider.parser().parseSilently((Charset[]) null);
}
});
assertThrows(NullPointerException.class, new Executable() {
@Override
public void execute() {
HostsFileEntriesProvider.parser().parseSilently(new File(""), (Charset[]) null);
}
});
}
@Test
void testFileInputValidation() {
assertThrows(NullPointerException.class, new Executable() {
@Override
public void execute() throws IOException {
HostsFileEntriesProvider.parser().parse((File) null);
}
});
assertThrows(NullPointerException.class, new Executable() {
@Override
public void execute() {
HostsFileEntriesProvider.parser().parseSilently((File) null);
}
});
}
@Test
void testReaderInputValidation() {
assertThrows(NullPointerException.class, new Executable() {
@Override
public void execute() throws IOException {
HostsFileEntriesProvider.parser().parse((Reader) null);
}
});
}
}