View Javadoc
1   /*
2    * Copyright 2021 The Netty Project
3    *
4    * The Netty Project licenses this file to you under the Apache License,
5    * version 2.0 (the "License"); you may not use this file except in compliance
6    * with the License. You may obtain a copy of the License at:
7    *
8    *   https://www.apache.org/licenses/LICENSE-2.0
9    *
10   * Unless required by applicable law or agreed to in writing, software
11   * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
12   * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
13   * License for the specific language governing permissions and limitations
14   * under the License.
15   */
16  package io.netty.resolver;
17  
18  import io.netty.util.NetUtil;
19  import io.netty.util.internal.PlatformDependent;
20  import io.netty.util.internal.logging.InternalLogger;
21  import io.netty.util.internal.logging.InternalLoggerFactory;
22  
23  import java.io.BufferedReader;
24  import java.io.File;
25  import java.io.FileInputStream;
26  import java.io.IOException;
27  import java.io.InputStreamReader;
28  import java.io.Reader;
29  import java.net.Inet4Address;
30  import java.net.InetAddress;
31  import java.nio.charset.Charset;
32  import java.util.ArrayList;
33  import java.util.Collections;
34  import java.util.HashMap;
35  import java.util.List;
36  import java.util.Locale;
37  import java.util.Map;
38  import java.util.regex.Pattern;
39  
40  import static io.netty.util.internal.ObjectUtil.checkNotNull;
41  
42  /**
43   * A container of hosts file entries
44   */
45  public final class HostsFileEntriesProvider {
46  
47      public interface Parser {
48  
49          /**
50           * Parses the hosts file at standard OS location using the system default {@link Charset} for decoding.
51           *
52           * @return a new {@link HostsFileEntriesProvider}
53           * @throws IOException file could not be read
54           */
55          HostsFileEntriesProvider parse() throws IOException;
56  
57          /**
58           * Parses the hosts file at standard OS location using the given {@link Charset}s one after another until
59           * parse something or none is left.
60           *
61           * @param charsets the {@link Charset}s to try as file encodings when parsing
62           * @return a new {@link HostsFileEntriesProvider}
63           * @throws IOException file could not be read
64           */
65          HostsFileEntriesProvider parse(Charset... charsets) throws IOException;
66  
67          /**
68           * Parses the provided hosts file using the given {@link Charset}s one after another until
69           * parse something or none is left. In case {@link Charset}s are not provided,
70           * the system default {@link Charset} is used for decoding.
71           *
72           * @param file the file to be parsed
73           * @param charsets the {@link Charset}s to try as file encodings when parsing, in case {@link Charset}s
74           * are not provided, the system default {@link Charset} is used for decoding
75           * @return a new {@link HostsFileEntriesProvider}
76           * @throws IOException file could not be read
77           */
78          HostsFileEntriesProvider parse(File file, Charset... charsets) throws IOException;
79  
80          /**
81           * Performs the parsing operation using the provided reader of hosts file format.
82           *
83           * @param reader the reader of hosts file format
84           * @return a new {@link HostsFileEntriesProvider}
85           */
86          HostsFileEntriesProvider parse(Reader reader) throws IOException;
87  
88          /**
89           * Parses the hosts file at standard OS location using the system default {@link Charset} for decoding.
90           *
91           * @return a new {@link HostsFileEntriesProvider}
92           */
93          HostsFileEntriesProvider parseSilently();
94  
95          /**
96           * Parses the hosts file at standard OS location using the given {@link Charset}s one after another until
97           * parse something or none is left.
98           *
99           * @param charsets the {@link Charset}s to try as file encodings when parsing
100          * @return a new {@link HostsFileEntriesProvider}
101          */
102         HostsFileEntriesProvider parseSilently(Charset... charsets);
103 
104         /**
105          * Parses the provided hosts file using the given {@link Charset}s one after another until
106          * parse something or none is left. In case {@link Charset}s are not provided,
107          * the system default {@link Charset} is used for decoding.
108          *
109          * @param file the file to be parsed
110          * @param charsets the {@link Charset}s to try as file encodings when parsing, in case {@link Charset}s
111          * are not provided, the system default {@link Charset} is used for decoding
112          * @return a new {@link HostsFileEntriesProvider}
113          */
114         HostsFileEntriesProvider parseSilently(File file, Charset... charsets);
115     }
116 
117     /**
118      * Creates a parser for {@link HostsFileEntriesProvider}.
119      *
120      * @return a new {@link HostsFileEntriesProvider.Parser}
121      */
122     public static Parser parser() {
123         return ParserImpl.INSTANCE;
124     }
125 
126     static final HostsFileEntriesProvider EMPTY =
127             new HostsFileEntriesProvider(
128                     Collections.<String, List<InetAddress>>emptyMap(),
129                     Collections.<String, List<InetAddress>>emptyMap());
130 
131     private final Map<String, List<InetAddress>> ipv4Entries;
132     private final Map<String, List<InetAddress>> ipv6Entries;
133 
134     HostsFileEntriesProvider(Map<String, List<InetAddress>> ipv4Entries, Map<String, List<InetAddress>> ipv6Entries) {
135         this.ipv4Entries = Collections.unmodifiableMap(new HashMap<String, List<InetAddress>>(ipv4Entries));
136         this.ipv6Entries = Collections.unmodifiableMap(new HashMap<String, List<InetAddress>>(ipv6Entries));
137     }
138 
139     /**
140      * The IPv4 entries.
141      *
142      * @return the IPv4 entries
143      */
144     public Map<String, List<InetAddress>> ipv4Entries() {
145         return ipv4Entries;
146     }
147 
148     /**
149      * The IPv6 entries.
150      *
151      * @return the IPv6 entries
152      */
153     public Map<String, List<InetAddress>> ipv6Entries() {
154         return ipv6Entries;
155     }
156 
157     private static final class ParserImpl implements Parser {
158 
159         private static final String WINDOWS_DEFAULT_SYSTEM_ROOT = "C:\\Windows";
160         private static final String WINDOWS_HOSTS_FILE_RELATIVE_PATH = "\\system32\\drivers\\etc\\hosts";
161         private static final String X_PLATFORMS_HOSTS_FILE_PATH = "/etc/hosts";
162 
163         private static final Pattern WHITESPACES = Pattern.compile("[ \t]+");
164 
165         private static final InternalLogger logger = InternalLoggerFactory.getInstance(Parser.class);
166 
167         static final ParserImpl INSTANCE = new ParserImpl();
168 
169         private ParserImpl() {
170             // singleton
171         }
172 
173         @Override
174         public HostsFileEntriesProvider parse() throws IOException {
175             return parse(locateHostsFile(), Charset.defaultCharset());
176         }
177 
178         @Override
179         public HostsFileEntriesProvider parse(Charset... charsets) throws IOException {
180             return parse(locateHostsFile(), charsets);
181         }
182 
183         @Override
184         public HostsFileEntriesProvider parse(File file, Charset... charsets) throws IOException {
185             checkNotNull(file, "file");
186             checkNotNull(charsets, "charsets");
187             if (charsets.length == 0) {
188                 charsets = new Charset[]{Charset.defaultCharset()};
189             }
190             if (file.exists() && file.isFile()) {
191                 for (Charset charset : charsets) {
192                     BufferedReader reader = new BufferedReader(
193                             new InputStreamReader(new FileInputStream(file), charset));
194                     try {
195                         HostsFileEntriesProvider entries = parse(reader);
196                         if (entries != HostsFileEntriesProvider.EMPTY) {
197                             return entries;
198                         }
199                     } finally {
200                         reader.close();
201                     }
202                 }
203             }
204             return HostsFileEntriesProvider.EMPTY;
205         }
206 
207         @Override
208         public HostsFileEntriesProvider parse(Reader reader) throws IOException {
209             checkNotNull(reader, "reader");
210             BufferedReader buff = new BufferedReader(reader);
211             try {
212                 Map<String, List<InetAddress>> ipv4Entries = new HashMap<String, List<InetAddress>>();
213                 Map<String, List<InetAddress>> ipv6Entries = new HashMap<String, List<InetAddress>>();
214                 String line;
215                 while ((line = buff.readLine()) != null) {
216                     // remove comment
217                     int commentPosition = line.indexOf('#');
218                     if (commentPosition != -1) {
219                         line = line.substring(0, commentPosition);
220                     }
221                     // skip empty lines
222                     line = line.trim();
223                     if (line.isEmpty()) {
224                         continue;
225                     }
226 
227                     // split
228                     List<String> lineParts = new ArrayList<String>();
229                     for (String s : WHITESPACES.split(line)) {
230                         if (!s.isEmpty()) {
231                             lineParts.add(s);
232                         }
233                     }
234 
235                     // a valid line should be [IP, hostname, alias*]
236                     if (lineParts.size() < 2) {
237                         // skip invalid line
238                         continue;
239                     }
240 
241                     byte[] ipBytes = NetUtil.createByteArrayFromIpAddressString(lineParts.get(0));
242 
243                     if (ipBytes == null) {
244                         // skip invalid IP
245                         continue;
246                     }
247 
248                     // loop over hostname and aliases
249                     for (int i = 1; i < lineParts.size(); i++) {
250                         String hostname = lineParts.get(i);
251                         String hostnameLower = hostname.toLowerCase(Locale.ENGLISH);
252                         InetAddress address = InetAddress.getByAddress(hostname, ipBytes);
253                         List<InetAddress> addresses;
254                         if (address instanceof Inet4Address) {
255                             addresses = ipv4Entries.get(hostnameLower);
256                             if (addresses == null) {
257                                 addresses = new ArrayList<InetAddress>();
258                                 ipv4Entries.put(hostnameLower, addresses);
259                             }
260                         } else {
261                             addresses = ipv6Entries.get(hostnameLower);
262                             if (addresses == null) {
263                                 addresses = new ArrayList<InetAddress>();
264                                 ipv6Entries.put(hostnameLower, addresses);
265                             }
266                         }
267                         addresses.add(address);
268                     }
269                 }
270                 return ipv4Entries.isEmpty() && ipv6Entries.isEmpty() ?
271                         HostsFileEntriesProvider.EMPTY :
272                         new HostsFileEntriesProvider(ipv4Entries, ipv6Entries);
273             } finally {
274                 try {
275                     buff.close();
276                 } catch (IOException e) {
277                     logger.warn("Failed to close a reader", e);
278                 }
279             }
280         }
281 
282         @Override
283         public HostsFileEntriesProvider parseSilently() {
284             return parseSilently(locateHostsFile(), Charset.defaultCharset());
285         }
286 
287         @Override
288         public HostsFileEntriesProvider parseSilently(Charset... charsets) {
289             return parseSilently(locateHostsFile(), charsets);
290         }
291 
292         @Override
293         public HostsFileEntriesProvider parseSilently(File file, Charset... charsets) {
294             try {
295                 return parse(file, charsets);
296             } catch (IOException e) {
297                 if (logger.isWarnEnabled()) {
298                     logger.warn("Failed to load and parse hosts file at " + file.getPath(), e);
299                 }
300                 return HostsFileEntriesProvider.EMPTY;
301             }
302         }
303 
304         private static File locateHostsFile() {
305             File hostsFile;
306             if (PlatformDependent.isWindows()) {
307                 hostsFile = new File(System.getenv("SystemRoot") + WINDOWS_HOSTS_FILE_RELATIVE_PATH);
308                 if (!hostsFile.exists()) {
309                     hostsFile = new File(WINDOWS_DEFAULT_SYSTEM_ROOT + WINDOWS_HOSTS_FILE_RELATIVE_PATH);
310                 }
311             } else {
312                 hostsFile = new File(X_PLATFORMS_HOSTS_FILE_PATH);
313             }
314             return hostsFile;
315         }
316     }
317 }