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.netty5.resolver;
17  
18  import io.netty5.util.NetUtil;
19  import io.netty5.util.internal.PlatformDependent;
20  import io.netty5.util.internal.logging.InternalLogger;
21  import io.netty5.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 java.util.Objects.requireNonNull;
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<>(ipv4Entries));
136         this.ipv6Entries = Collections.unmodifiableMap(new HashMap<>(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             requireNonNull(file, "file");
186             requireNonNull(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                     try (BufferedReader reader = new BufferedReader(
193                             new InputStreamReader(new FileInputStream(file), charset))) {
194                         HostsFileEntriesProvider entries = parse(reader);
195                         if (entries != HostsFileEntriesProvider.EMPTY) {
196                             return entries;
197                         }
198                     }
199                 }
200             }
201             return HostsFileEntriesProvider.EMPTY;
202         }
203 
204         @Override
205         public HostsFileEntriesProvider parse(Reader reader) throws IOException {
206             requireNonNull(reader, "reader");
207             BufferedReader buff = new BufferedReader(reader);
208             try {
209                 Map<String, List<InetAddress>> ipv4Entries = new HashMap<>();
210                 Map<String, List<InetAddress>> ipv6Entries = new HashMap<>();
211                 String line;
212                 while ((line = buff.readLine()) != null) {
213                     // remove comment
214                     int commentPosition = line.indexOf('#');
215                     if (commentPosition != -1) {
216                         line = line.substring(0, commentPosition);
217                     }
218                     // skip empty lines
219                     line = line.trim();
220                     if (line.isEmpty()) {
221                         continue;
222                     }
223 
224                     // split
225                     List<String> lineParts = new ArrayList<>();
226                     for (String s : WHITESPACES.split(line)) {
227                         if (!s.isEmpty()) {
228                             lineParts.add(s);
229                         }
230                     }
231 
232                     // a valid line should be [IP, hostname, alias*]
233                     if (lineParts.size() < 2) {
234                         // skip invalid line
235                         continue;
236                     }
237 
238                     byte[] ipBytes = NetUtil.createByteArrayFromIpAddressString(lineParts.get(0));
239 
240                     if (ipBytes == null) {
241                         // skip invalid IP
242                         continue;
243                     }
244 
245                     // loop over hostname and aliases
246                     for (int i = 1; i < lineParts.size(); i++) {
247                         String hostname = lineParts.get(i);
248                         String hostnameLower = hostname.toLowerCase(Locale.ENGLISH);
249                         InetAddress address = InetAddress.getByAddress(hostname, ipBytes);
250                         List<InetAddress> addresses;
251                         if (address instanceof Inet4Address) {
252                             addresses = ipv4Entries.get(hostnameLower);
253                             if (addresses == null) {
254                                 addresses = new ArrayList<>();
255                                 ipv4Entries.put(hostnameLower, addresses);
256                             }
257                         } else {
258                             addresses = ipv6Entries.get(hostnameLower);
259                             if (addresses == null) {
260                                 addresses = new ArrayList<>();
261                                 ipv6Entries.put(hostnameLower, addresses);
262                             }
263                         }
264                         addresses.add(address);
265                     }
266                 }
267                 return ipv4Entries.isEmpty() && ipv6Entries.isEmpty() ?
268                         HostsFileEntriesProvider.EMPTY :
269                         new HostsFileEntriesProvider(ipv4Entries, ipv6Entries);
270             } finally {
271                 try {
272                     buff.close();
273                 } catch (IOException e) {
274                     logger.warn("Failed to close a reader", e);
275                 }
276             }
277         }
278 
279         @Override
280         public HostsFileEntriesProvider parseSilently() {
281             return parseSilently(locateHostsFile(), Charset.defaultCharset());
282         }
283 
284         @Override
285         public HostsFileEntriesProvider parseSilently(Charset... charsets) {
286             return parseSilently(locateHostsFile(), charsets);
287         }
288 
289         @Override
290         public HostsFileEntriesProvider parseSilently(File file, Charset... charsets) {
291             try {
292                 return parse(file, charsets);
293             } catch (IOException e) {
294                 if (logger.isWarnEnabled()) {
295                     logger.warn("Failed to load and parse hosts file at " + file.getPath(), e);
296                 }
297                 return HostsFileEntriesProvider.EMPTY;
298             }
299         }
300 
301         private static File locateHostsFile() {
302             File hostsFile;
303             if (PlatformDependent.isWindows()) {
304                 hostsFile = new File(System.getenv("SystemRoot") + WINDOWS_HOSTS_FILE_RELATIVE_PATH);
305                 if (!hostsFile.exists()) {
306                     hostsFile = new File(WINDOWS_DEFAULT_SYSTEM_ROOT + WINDOWS_HOSTS_FILE_RELATIVE_PATH);
307                 }
308             } else {
309                 hostsFile = new File(X_PLATFORMS_HOSTS_FILE_PATH);
310             }
311             return hostsFile;
312         }
313     }
314 }