View Javadoc
1   /*
2    * Copyright (c) 2002-2025 Gargoyle Software Inc.
3    *
4    * Licensed under the Apache License, Version 2.0 (the "License");
5    * you may not use this file except in compliance with the License.
6    * You may obtain a copy of the License at
7    * https://www.apache.org/licenses/LICENSE-2.0
8    *
9    * Unless required by applicable law or agreed to in writing, software
10   * distributed under the License is distributed on an "AS IS" BASIS,
11   * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12   * See the License for the specific language governing permissions and
13   * limitations under the License.
14   */
15  package org.htmlunit.javascript.proxyautoconfig;
16  
17  import java.net.InetAddress;
18  import java.text.SimpleDateFormat;
19  import java.util.Calendar;
20  import java.util.Locale;
21  import java.util.TimeZone;
22  
23  import org.apache.commons.lang3.StringUtils;
24  import org.htmlunit.javascript.HtmlUnitScriptable;
25  import org.htmlunit.javascript.JavaScriptEngine;
26  import org.htmlunit.javascript.configuration.JsxClass;
27  import org.htmlunit.javascript.configuration.JsxFunction;
28  import org.htmlunit.util.SubnetUtils;
29  
30  /**
31   * Provides an implementation of Proxy Auto-Config (PAC).
32   *
33   * @see <a href=
34   * "https://developer.mozilla.org/en-US/docs/Web/HTTP/Proxy_servers_and_tunneling/Proxy_Auto-Configuration_PAC_file">
35   * Proxy Auto-Configuration (PAC) file</a>
36   *
37   * @author Ahmed Ashour
38   * @author Ronald Brill
39   */
40  @JsxClass
41  public final class ProxyAutoConfig extends HtmlUnitScriptable {
42  
43      private static final String TIMEZONE_GMT = "GMT";
44  
45      private ProxyAutoConfig() {
46          super();
47      }
48  
49      /**
50       * Returns true if there is no domain name in the hostname (no dots).
51       * @param host the hostname from the URL (excluding port number).
52       * @return true if there is no domain name in the hostname (no dots).
53       */
54      @JsxFunction
55      public static boolean isPlainHostName(final String host) {
56          return host.indexOf('.') == -1;
57      }
58  
59      /**
60       * Returns true if the domain of hostname matches.
61       * @param host the hostname from the URL
62       * @param domain the domain name to test the hostname against
63       * @return true if the domain of hostname matches.
64       */
65      @JsxFunction
66      public static boolean dnsDomainIs(final String host, final String domain) {
67          return host.endsWith(domain);
68      }
69  
70      /**
71       * Returns true if the hostname matches exactly the specified hostname,
72       * or if there is no domain name part in the hostname, but the unqualified hostname matches.
73       * @param host the hostname from the URL
74       * @param hostdom fully qualified hostname to match against
75       * @return true if the hostname matches exactly the specified hostname,
76       *         or if there is no domain name part in the hostname, but the unqualified hostname matches.
77       */
78      @JsxFunction
79      public static boolean localHostOrDomainIs(final String host, final String hostdom) {
80          return host.length() > 1 && host.equals(hostdom) || host.indexOf('.') == -1 && hostdom.startsWith(host);
81      }
82  
83      /**
84       * Tries to resolve the hostname. Returns true if succeeds.
85       * @param host the hostname from the URL.
86       * @return true if the specific hostname is resolvable.
87       */
88      @JsxFunction
89      public static boolean isResolvable(final String host) {
90          return dnsResolve(host) != null;
91      }
92  
93      /**
94       * Returns true if the IP address of the host matches the specified IP address pattern.
95       * @param host a DNS hostname, or IP address.
96       *        If a hostname is passed, it will be resolved into an IP address by this function.
97       * @param pattern an IP address pattern in the dot-separated format
98       * @param mask mask for the IP address pattern informing which parts of the IP address should be matched against.
99       *        0 means ignore, 255 means match
100      * @return true if the IP address of the host matches the specified IP address pattern.
101      */
102     @JsxFunction
103     public static boolean isInNet(final String host, final String pattern, final String mask) {
104         final String dnsResolve = dnsResolve(host);
105         if (null == dnsResolve) {
106             return false;
107         }
108 
109         final SubnetUtils subnetUtils = new SubnetUtils(pattern, mask);
110         return subnetUtils.isInRange(dnsResolve);
111     }
112 
113     /**
114      * Resolves the given DNS hostname into an IP address, and returns it in the dot separated format as a string.
115      * @param host the hostname to resolve
116      * @return the resolved IP address
117      */
118     @JsxFunction
119     public static String dnsResolve(final String host) {
120         try {
121             return InetAddress.getByName(host).getHostAddress();
122         }
123         catch (final Exception e) {
124             return null;
125         }
126     }
127 
128     /**
129      * Returns the IP address of the local host, as a string in the dot-separated integer format.
130      * @return the IP address of the local host, as a string in the dot-separated integer format.
131      */
132     @JsxFunction
133     public static String myIpAddress() {
134         try {
135             return InetAddress.getLocalHost().getHostAddress();
136         }
137         catch (final Exception e) {
138             throw JavaScriptEngine.throwAsScriptRuntimeEx(e);
139         }
140     }
141 
142     /**
143      * Returns the number (integer) of DNS domain levels (number of dots) in the hostname.
144      * @param host the hostname from the URL
145      * @return the number (integer) of DNS domain levels (number of dots) in the hostname.
146      */
147     @JsxFunction
148     public static int dnsDomainLevels(final String host) {
149         int levels = 0;
150         for (int i = host.length() - 1; i >= 0; i--) {
151             if (host.charAt(i) == '.') {
152                 levels++;
153             }
154         }
155         return levels;
156     }
157 
158     /**
159      * Matches the specified string against a shell expression, not regular expression.
160      * @param str a string to match
161      * @param shexp the shell expression
162      * @return if the string matches
163      */
164     @JsxFunction
165     public static boolean shExpMatch(final String str, final String shexp) {
166         final String regexp = shexp.replace(".", "\\.").replace("*", ".*").replace("?", ".");
167         return str.matches(regexp);
168     }
169 
170     /**
171      * Checks if today is included in the specified range.
172      * @param wd1 week day 1
173      * @param wd2 week day 2, optional
174      * @param gmt string of "GMT", or not specified
175      * @return if today is in range
176      */
177     @JsxFunction
178     public static boolean weekdayRange(final String wd1, Object wd2, final Object gmt) {
179         TimeZone timezone = TimeZone.getDefault();
180         if (TIMEZONE_GMT.equals(JavaScriptEngine.toString(gmt))
181                 || TIMEZONE_GMT.equals(JavaScriptEngine.toString(wd2))) {
182             timezone = TimeZone.getTimeZone(TIMEZONE_GMT);
183         }
184         if (JavaScriptEngine.isUndefined(wd2) || TIMEZONE_GMT.equals(JavaScriptEngine.toString(wd2))) {
185             wd2 = wd1;
186         }
187         final Calendar calendar = Calendar.getInstance(timezone);
188         for (int i = 0; i < 7; i++) {
189             final String day = new SimpleDateFormat("EEE", Locale.ROOT)
190                     .format(calendar.getTime()).toUpperCase(Locale.ROOT);
191             if (day.equals(wd2)) {
192                 return true;
193             }
194             if (day.equals(wd1)) {
195                 return i == 0;
196             }
197             calendar.add(Calendar.DAY_OF_WEEK, 1);
198         }
199         return false;
200     }
201 
202     /**
203      * Checks if today is included in the specified range.
204      * @param value1 the value 1
205      * @param value2 the value 2
206      * @param value3 the value 3
207      * @param value4 the value 4
208      * @param value5 the value 5
209      * @param value6 the value 6
210      * @param value7 the value 7
211      * @return if today is in range
212      */
213     @JsxFunction
214     public static boolean dateRange(final String value1, final Object value2, final Object value3,
215             final Object value4, final Object value5, final Object value6, final Object value7) {
216         final Object[] values = {value1, value2, value3, value4, value5, value6, value7};
217         TimeZone timezone = TimeZone.getDefault();
218 
219         //actual values length
220         int length;
221         for (length = values.length - 1; length >= 0; length--) {
222             if (TIMEZONE_GMT.equals(JavaScriptEngine.toString(values[length]))) {
223                 timezone = TimeZone.getTimeZone(TIMEZONE_GMT);
224                 break;
225             }
226             else if (!JavaScriptEngine.isUndefined(values[length])) {
227                 length++;
228                 break;
229             }
230         }
231 
232         final int day1;
233         final int day2;
234         final int month1;
235         final int month2;
236         final int year1;
237         final int year2;
238         final Calendar cal1;
239         final Calendar cal2;
240         switch (length) {
241             case 1:
242                 final int day = getSmallInt(value1);
243                 final int month = dateRange_getMonth(value1);
244                 final int year = dateRange_getYear(value1);
245                 cal1 = dateRange_createCalendar(timezone, day, month, year);
246                 cal2 = (Calendar) cal1.clone();
247                 break;
248 
249             case 2:
250                 day1 = getSmallInt(value1);
251                 month1 = dateRange_getMonth(value1);
252                 year1 = dateRange_getYear(value1);
253                 cal1 = dateRange_createCalendar(timezone, day1, month1, year1);
254                 day2 = getSmallInt(value2);
255                 month2 = dateRange_getMonth(value2);
256                 year2 = dateRange_getYear(value2);
257                 cal2 = dateRange_createCalendar(timezone, day2, month2, year2);
258                 break;
259 
260             case 4:
261                 day1 = getSmallInt(value1);
262                 if (day1 != -1) {
263                     month1 = dateRange_getMonth(value2);
264                     day2 = getSmallInt(value3);
265                     month2 = dateRange_getMonth(value4);
266                     cal1 = dateRange_createCalendar(timezone, day1, month1, -1);
267                     cal2 = dateRange_createCalendar(timezone, day2, month2, -1);
268                 }
269                 else {
270                     month1 = dateRange_getMonth(value1);
271                     year1 = dateRange_getMonth(value2);
272                     month2 = getSmallInt(value3);
273                     year2 = dateRange_getMonth(value4);
274                     cal1 = dateRange_createCalendar(timezone, -1, month1, year1);
275                     cal2 = dateRange_createCalendar(timezone, -1, month2, year2);
276                 }
277                 break;
278 
279             default:
280                 day1 = getSmallInt(value1);
281                 month1 = dateRange_getMonth(value2);
282                 year1 = dateRange_getYear(value3);
283                 day2 = getSmallInt(value4);
284                 month2 = dateRange_getMonth(value5);
285                 year2 = dateRange_getYear(value6);
286                 cal1 = dateRange_createCalendar(timezone, day1, month1, year1);
287                 cal2 = dateRange_createCalendar(timezone, day2, month2, year2);
288         }
289 
290         final Calendar today = Calendar.getInstance(timezone);
291         today.set(Calendar.MILLISECOND, 0);
292         today.set(Calendar.SECOND, 0);
293         cal1.set(Calendar.MILLISECOND, 0);
294         cal1.set(Calendar.SECOND, 0);
295         cal2.set(Calendar.MILLISECOND, 0);
296         cal2.set(Calendar.SECOND, 0);
297         return today.equals(cal1) || (today.after(cal1) && today.before(cal2)) || today.equals(cal2);
298     }
299 
300     private static Calendar dateRange_createCalendar(final TimeZone timezone,
301             final int day, final int month, final int year) {
302         final Calendar calendar = Calendar.getInstance(timezone);
303         if (day != -1) {
304             calendar.set(Calendar.DAY_OF_MONTH, day);
305         }
306         if (month != -1) {
307             calendar.set(Calendar.MONTH, month);
308         }
309         if (year != -1) {
310             calendar.set(Calendar.YEAR, year);
311         }
312         return calendar;
313     }
314 
315     private static int getSmallInt(final Object object) {
316         final String s = JavaScriptEngine.toString(object);
317         if (Character.isDigit(s.charAt(0))) {
318             final int i = Integer.parseInt(s);
319             if (i < 70) {
320                 return i;
321             }
322         }
323         return -1;
324     }
325 
326     private static int dateRange_getMonth(final Object object) {
327         final String s = JavaScriptEngine.toString(object);
328         if (Character.isLetter(s.charAt(0))) {
329             try {
330                 final Calendar cal = Calendar.getInstance(Locale.ROOT);
331                 cal.clear();
332                 cal.setTime(new SimpleDateFormat("MMM", Locale.ROOT).parse(s));
333                 return cal.get(Calendar.MONTH);
334             }
335             catch (final Exception ignored) {
336                 // empty
337             }
338         }
339         return -1;
340     }
341 
342     private static int dateRange_getYear(final Object object) {
343         final String s = JavaScriptEngine.toString(object);
344         if (Character.isDigit(s.charAt(0))) {
345             final int i = Integer.parseInt(s);
346             if (i > 1000) {
347                 return i;
348             }
349         }
350         return -1;
351     }
352 
353     /**
354      * Checks if the time now is included in the specified range.
355      * @param value1 the value 1
356      * @param value2 the value 2
357      * @param value3 the value 3
358      * @param value4 the value 4
359      * @param value5 the value 5
360      * @param value6 the value 6
361      * @param value7 the value 7
362      * @return if the time now is in the range
363      */
364     @JsxFunction
365     public static boolean timeRange(final String value1, final Object value2, final Object value3,
366             final Object value4, final Object value5, final Object value6, final Object value7) {
367         final Object[] values = {value1, value2, value3, value4, value5, value6, value7};
368         TimeZone timezone = TimeZone.getDefault();
369 
370         //actual values length
371         int length;
372         for (length = values.length - 1; length >= 0; length--) {
373             if (TIMEZONE_GMT.equals(JavaScriptEngine.toString(values[length]))) {
374                 timezone = TimeZone.getTimeZone(TIMEZONE_GMT);
375                 break;
376             }
377             else if (!JavaScriptEngine.isUndefined(values[length])) {
378                 length++;
379                 break;
380             }
381         }
382 
383         final int hour1;
384         final int hour2;
385         final int min1;
386         final int min2;
387         final int second1;
388         final int second2;
389         final Calendar cal1;
390         final Calendar cal2;
391         switch (length) {
392             case 1:
393                 hour1 = getSmallInt(value1);
394                 cal1 = timeRange_createCalendar(timezone, hour1, -1, -1);
395                 cal2 = (Calendar) cal1.clone();
396                 cal2.add(Calendar.HOUR_OF_DAY, 1);
397                 break;
398 
399             case 2:
400                 hour1 = getSmallInt(value1);
401                 cal1 = timeRange_createCalendar(timezone, hour1, -1, -1);
402                 hour2 = getSmallInt(value2);
403                 cal2 = timeRange_createCalendar(timezone, hour2, -1, -1);
404                 break;
405 
406             case 4:
407                 hour1 = getSmallInt(value1);
408                 min1 = getSmallInt(value2);
409                 hour2 = getSmallInt(value3);
410                 min2 = getSmallInt(value4);
411                 cal1 = dateRange_createCalendar(timezone, hour1, min1, -1);
412                 cal2 = dateRange_createCalendar(timezone, hour2, min2, -1);
413                 break;
414 
415             default:
416                 hour1 = getSmallInt(value1);
417                 min1 = getSmallInt(value2);
418                 second1 = getSmallInt(value3);
419                 hour2 = getSmallInt(value4);
420                 min2 = getSmallInt(value5);
421                 second2 = getSmallInt(value6);
422                 cal1 = dateRange_createCalendar(timezone, hour1, min1, second1);
423                 cal2 = dateRange_createCalendar(timezone, hour2, min2, second2);
424         }
425 
426         final Calendar now = Calendar.getInstance(timezone);
427         return now.equals(cal1) || now.after(cal1) && now.before(cal2) || now.equals(cal2);
428     }
429 
430     private static Calendar timeRange_createCalendar(final TimeZone timezone,
431             final int hour, final int minute, final int second) {
432         final Calendar calendar = Calendar.getInstance(timezone);
433         if (hour != -1) {
434             calendar.set(Calendar.HOUR_OF_DAY, hour);
435         }
436         if (minute != -1) {
437             calendar.set(Calendar.MINUTE, minute);
438         }
439         if (second != -1) {
440             calendar.set(Calendar.SECOND, second);
441         }
442         return calendar;
443     }
444 
445     /**
446      * Concatenates the four dot-separated bytes into one 4-byte word and converts it to decimal.
447      * @param ip any dotted address such as an IP address or mask.
448      * @return concatenates the four dot-separated bytes into one 4-byte word and converts it to decimal.
449      */
450     @JsxFunction(functionName = "convert_addr")
451     public static long convertAddr(final String ip) {
452         final String[] parts = StringUtils.split(ip, '.');
453 
454         return
455             ((Integer.parseInt(parts[0]) & 0xff) << 24)
456                 | ((Integer.parseInt(parts[1]) & 0xff) << 16)
457                 | ((Integer.parseInt(parts[2]) & 0xff) << 8)
458                 | (Integer.parseInt(parts[3]) & 0xff);
459     }
460 }