View Javadoc
1   /*
2    * Copyright (c) 2002-2017 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    * http://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 com.gargoylesoftware.htmlunit;
16  import java.lang.reflect.Method;
17  import java.net.InetAddress;
18  import java.net.URL;
19  import java.text.SimpleDateFormat;
20  import java.util.Calendar;
21  import java.util.Locale;
22  import java.util.TimeZone;
23  import java.util.regex.Pattern;
24  
25  import net.sourceforge.htmlunit.corejs.javascript.Context;
26  import net.sourceforge.htmlunit.corejs.javascript.FunctionObject;
27  import net.sourceforge.htmlunit.corejs.javascript.NativeFunction;
28  import net.sourceforge.htmlunit.corejs.javascript.Scriptable;
29  import net.sourceforge.htmlunit.corejs.javascript.ScriptableObject;
30  import net.sourceforge.htmlunit.corejs.javascript.Undefined;
31  
32  /**
33   * Provides an implementation of Proxy Auto-Config (PAC).
34   *
35   * @see <a href="http://lib.ru/WEBMASTER/proxy-live.txt">PAC file format</a>
36   *
37   * @author Ahmed Ashour
38   */
39  public final class ProxyAutoConfig {
40  
41      private static final Pattern DOT_SPLIT_PATTERN = Pattern.compile("\\.");
42  
43      private ProxyAutoConfig() {
44      }
45  
46      /**
47       * Evaluates the <tt>FindProxyForURL</tt> method of the specified content.
48       * @param content the JavaScript content
49       * @param url the URL to be retrieved
50       * @return semicolon-separated result
51       */
52      public static String evaluate(final String content, final URL url) {
53          final Context cx = Context.enter();
54          try {
55              final ProxyAutoConfig config = new ProxyAutoConfig();
56              final Scriptable scope = cx.initStandardObjects();
57  
58              config.defineMethod("isPlainHostName", scope);
59              config.defineMethod("dnsDomainIs", scope);
60              config.defineMethod("localHostOrDomainIs", scope);
61              config.defineMethod("isResolvable", scope);
62              config.defineMethod("isInNet", scope);
63              config.defineMethod("dnsResolve", scope);
64              config.defineMethod("myIpAddress", scope);
65              config.defineMethod("dnsDomainLevels", scope);
66              config.defineMethod("shExpMatch", scope);
67              config.defineMethod("weekdayRange", scope);
68              config.defineMethod("dateRange", scope);
69              config.defineMethod("timeRange", scope);
70  
71              cx.evaluateString(scope, "var ProxyConfig = function() {}; ProxyConfig.bindings = {}", "<init>", 1, null);
72              cx.evaluateString(scope, content, "<Proxy Auto-Config>", 1, null);
73              final Object[] functionArgs = {url.toExternalForm(), url.getHost()};
74              final Object fObj = scope.get("FindProxyForURL", scope);
75  
76              final NativeFunction f = (NativeFunction) fObj;
77              final Object result = f.call(cx, scope, scope, functionArgs);
78              return Context.toString(result);
79          }
80          finally {
81              Context.exit();
82          }
83      }
84  
85      private void defineMethod(final String methodName, final Scriptable scope) {
86          for (Method method : getClass().getMethods()) {
87              if (method.getName().equals(methodName)) {
88                  final FunctionObject functionObject = new FunctionObject(methodName, method, scope);
89                  ((ScriptableObject) scope).defineProperty(methodName, functionObject, ScriptableObject.EMPTY);
90              }
91          }
92      }
93  
94      /**
95       * Returns true if there is no domain name in the hostname (no dots).
96       * @param host the hostname from the URL (excluding port number).
97       * @return true if there is no domain name in the hostname (no dots).
98       */
99      public static boolean isPlainHostName(final String host) {
100         return host.indexOf('.') == -1;
101     }
102 
103     /**
104      * Returns true if the domain of hostname matches.
105      * @param host the hostname from the URL
106      * @param domain the domain name to test the hostname against
107      * @return true if the domain of hostname matches.
108      */
109     public static boolean dnsDomainIs(final String host, final String domain) {
110         return host.endsWith(domain);
111     }
112 
113     /**
114      * Returns true if the hostname matches exactly the specified hostname,
115      * or if there is no domain name part in the hostname, but the unqualified hostname matches.
116      * @param host the hostname from the URL
117      * @param hostdom fully qualified hostname to match against
118      * @return true if the hostname matches exactly the specified hostname,
119      * or if there is no domain name part in the hostname, but the unqualified hostname matches.
120      */
121     public static boolean localHostOrDomainIs(final String host, final String hostdom) {
122         return host.length() > 1 && host.equals(hostdom) || host.indexOf('.') == -1 && hostdom.startsWith(host);
123     }
124 
125     /**
126      * Tries to resolve the hostname. Returns true if succeeds.
127      * @param host the hostname from the URL.
128      * @return true if the specific hostname is resolvable.
129      */
130     public static boolean isResolvable(final String host) {
131         return dnsResolve(host) != null;
132     }
133 
134     /**
135      * Returns true if the IP address of the host matches the specified IP address pattern.
136      * @param host a DNS hostname, or IP address.
137      * If a hostname is passed, it will be resolved into an IP address by this function.
138      * @param pattern an IP address pattern in the dot-separated format
139      * @param mask mask for the IP address pattern informing which parts of the IP address should be matched against.
140      * 0 means ignore, 255 means match
141      * @return true if the IP address of the host matches the specified IP address pattern.
142      */
143     public static boolean isInNet(final String host, final String pattern, final String mask) {
144         final String dnsResolve = dnsResolve(host);
145         if (null == dnsResolve) {
146             return false;
147         }
148 
149         final String[] hostTokens = DOT_SPLIT_PATTERN.split(dnsResolve(host));
150         final String[] patternTokens = DOT_SPLIT_PATTERN.split(pattern);
151         final String[] maskTokens = DOT_SPLIT_PATTERN.split(mask);
152         for (int i = 0; i < hostTokens.length; i++) {
153             if (Integer.parseInt(maskTokens[i]) != 0 && !hostTokens[i].equals(patternTokens[i])) {
154                 return false;
155             }
156         }
157         return true;
158     }
159 
160     /**
161      * Resolves the given DNS hostname into an IP address, and returns it in the dot separated format as a string.
162      * @param host the hostname to resolve
163      * @return the resolved IP address
164      */
165     public static String dnsResolve(final String host) {
166         try {
167             return InetAddress.getByName(host).getHostAddress();
168         }
169         catch (final Exception e) {
170             return null;
171         }
172     }
173 
174     /**
175      * Returns the IP address of the local host, as a string in the dot-separated integer format.
176      * @return the IP address of the local host, as a string in the dot-separated integer format.
177      */
178     public static String myIpAddress() {
179         try {
180             return InetAddress.getLocalHost().getHostAddress();
181         }
182         catch (final Exception e) {
183             throw Context.throwAsScriptRuntimeEx(e);
184         }
185     }
186 
187     /**
188      * Returns the number (integer) of DNS domain levels (number of dots) in the hostname.
189      * @param host the hostname from the URL
190      * @return the number (integer) of DNS domain levels (number of dots) in the hostname.
191      */
192     public static int dnsDomainLevels(final String host) {
193         int levels = 0;
194         for (int i = host.length() - 1; i >= 0; i--) {
195             if (host.charAt(i) == '.') {
196                 levels++;
197             }
198         }
199         return levels;
200     }
201 
202     /**
203      * Matches the specified string against a shell expression, not regular expression.
204      * @param str a string to match
205      * @param shexp the shell expression
206      * @return if the string matches
207      */
208     public static boolean shExpMatch(final String str, final String shexp) {
209         final String regexp = shexp.replace(".", "\\.").replace("*", ".*").replace("?", ".");
210         return str.matches(regexp);
211     }
212 
213     /**
214      * Checks if today is included in the specified range.
215      * @param wd1 week day 1
216      * @param wd2 week day 2, optional
217      * @param gmt string of "GMT", or not specified
218      * @return if today is in range
219      */
220     public static boolean weekdayRange(final String wd1, Object wd2, final Object gmt) {
221         TimeZone timezone = TimeZone.getDefault();
222         if ("GMT".equals(Context.toString(gmt)) || "GMT".equals(Context.toString(wd2))) {
223             timezone = TimeZone.getTimeZone("GMT");
224         }
225         if (wd2 == Undefined.instance || "GMT".equals(Context.toString(wd2))) {
226             wd2 = wd1;
227         }
228         final Calendar calendar = Calendar.getInstance(timezone);
229         for (int i = 0; i < 7; i++) {
230             final String day = new SimpleDateFormat("EEE", Locale.ROOT)
231                     .format(calendar.getTime()).toUpperCase(Locale.ROOT);
232             if (day.equals(wd2)) {
233                 return true;
234             }
235             if (day.equals(wd1)) {
236                 return i == 0;
237             }
238             calendar.add(Calendar.DAY_OF_WEEK, 1);
239         }
240         return false;
241     }
242 
243     /**
244      * Checks if today is included in the specified range.
245      * @param value1 the value 1
246      * @param value2 the value 2
247      * @param value3 the value 3
248      * @param value4 the value 4
249      * @param value5 the value 5
250      * @param value6 the value 6
251      * @param value7 the value 7
252      * @return if today is in range
253      */
254     public static boolean dateRange(final String value1, final Object value2, final Object value3,
255             final Object value4, final Object value5, final Object value6, final Object value7) {
256         final Object[] values = {value1, value2, value3, value4, value5, value6, value7};
257         TimeZone timezone = TimeZone.getDefault();
258 
259         //actual values length
260         int length;
261         for (length = values.length - 1; length >= 0; length--) {
262             if ("GMT".equals(Context.toString(values[length]))) {
263                 timezone = TimeZone.getTimeZone("GMT");
264                 break;
265             }
266             else if (values[length] != Undefined.instance) {
267                 length++;
268                 break;
269             }
270         }
271 
272         final int day1, day2, month1, month2, year1, year2;
273         final Calendar cal1;
274         final Calendar cal2;
275         switch (length) {
276             case 1:
277                 final int day = getSmallInt(value1);
278                 final int month = dateRange_getMonth(value1);
279                 final int year = dateRange_getYear(value1);
280                 cal1 = dateRange_createCalendar(timezone, day, month, year);
281                 cal2 = (Calendar) cal1.clone();
282                 break;
283 
284             case 2:
285                 day1 = getSmallInt(value1);
286                 month1 = dateRange_getMonth(value1);
287                 year1 = dateRange_getYear(value1);
288                 cal1 = dateRange_createCalendar(timezone, day1, month1, year1);
289                 day2 = getSmallInt(value2);
290                 month2 = dateRange_getMonth(value2);
291                 year2 = dateRange_getYear(value2);
292                 cal2 = dateRange_createCalendar(timezone, day2, month2, year2);
293                 break;
294 
295             case 4:
296                 day1 = getSmallInt(value1);
297                 if (day1 != -1) {
298                     month1 = dateRange_getMonth(value2);
299                     day2 = getSmallInt(value3);
300                     month2 = dateRange_getMonth(value4);
301                     cal1 = dateRange_createCalendar(timezone, day1, month1, -1);
302                     cal2 = dateRange_createCalendar(timezone, day2, month2, -1);
303                 }
304                 else {
305                     month1 = dateRange_getMonth(value1);
306                     year1 = dateRange_getMonth(value2);
307                     month2 = getSmallInt(value3);
308                     year2 = dateRange_getMonth(value4);
309                     cal1 = dateRange_createCalendar(timezone, -1, month1, year1);
310                     cal2 = dateRange_createCalendar(timezone, -1, month2, year2);
311                 }
312                 break;
313 
314             default:
315                 day1 = getSmallInt(value1);
316                 month1 = dateRange_getMonth(value2);
317                 year1 = dateRange_getYear(value3);
318                 day2 = getSmallInt(value4);
319                 month2 = dateRange_getMonth(value5);
320                 year2 = dateRange_getYear(value6);
321                 cal1 = dateRange_createCalendar(timezone, day1, month1, year1);
322                 cal2 = dateRange_createCalendar(timezone, day2, month2, year2);
323         }
324 
325         final Calendar today = Calendar.getInstance(timezone);
326         today.set(Calendar.MILLISECOND, 0);
327         today.set(Calendar.SECOND, 0);
328         cal1.set(Calendar.MILLISECOND, 0);
329         cal1.set(Calendar.SECOND, 0);
330         cal2.set(Calendar.MILLISECOND, 0);
331         cal2.set(Calendar.SECOND, 0);
332         return today.equals(cal1) || (today.after(cal1) && today.before(cal2)) || today.equals(cal2);
333     }
334 
335     private static Calendar dateRange_createCalendar(final TimeZone timezone,
336             final int day, final int month, final int year) {
337         final Calendar calendar = Calendar.getInstance(timezone);
338         if (day != -1) {
339             calendar.set(Calendar.DAY_OF_MONTH, day);
340         }
341         if (month != -1) {
342             calendar.set(Calendar.MONTH, month);
343         }
344         if (year != -1) {
345             calendar.set(Calendar.YEAR, year);
346         }
347         return calendar;
348     }
349 
350     private static int getSmallInt(final Object object) {
351         final String s = Context.toString(object);
352         if (Character.isDigit(s.charAt(0))) {
353             final int i = Integer.parseInt(s);
354             if (i < 70) {
355                 return i;
356             }
357         }
358         return -1;
359     }
360 
361     private static int dateRange_getMonth(final Object object) {
362         final String s = Context.toString(object);
363         if (Character.isLetter(s.charAt(0))) {
364             try {
365                 final Calendar cal = Calendar.getInstance(Locale.ROOT);
366                 cal.clear();
367                 cal.setTime(new SimpleDateFormat("MMM", Locale.ROOT).parse(s));
368                 return cal.get(Calendar.MONTH);
369             }
370             catch (final Exception e) {
371                 //empty
372             }
373         }
374         return -1;
375     }
376 
377     private static int dateRange_getYear(final Object object) {
378         final String s = Context.toString(object);
379         if (Character.isDigit(s.charAt(0))) {
380             final int i = Integer.parseInt(s);
381             if (i > 1000) {
382                 return i;
383             }
384         }
385         return -1;
386     }
387 
388     /**
389      * Checks if the time now is included in the specified range.
390      * @param value1 the value 1
391      * @param value2 the value 2
392      * @param value3 the value 3
393      * @param value4 the value 4
394      * @param value5 the value 5
395      * @param value6 the value 6
396      * @param value7 the value 7
397      * @return if the time now is in the range
398      */
399     public static boolean timeRange(final String value1, final Object value2, final Object value3,
400             final Object value4, final Object value5, final Object value6, final Object value7) {
401         final Object[] values = {value1, value2, value3, value4, value5, value6, value7};
402         TimeZone timezone = TimeZone.getDefault();
403 
404         //actual values length
405         int length;
406         for (length = values.length - 1; length >= 0; length--) {
407             if ("GMT".equals(Context.toString(values[length]))) {
408                 timezone = TimeZone.getTimeZone("GMT");
409                 break;
410             }
411             else if (values[length] != Undefined.instance) {
412                 length++;
413                 break;
414             }
415         }
416 
417         final int hour1, hour2, min1, min2, second1, second2;
418         final Calendar cal1;
419         final Calendar cal2;
420         switch (length) {
421             case 1:
422                 hour1 = getSmallInt(value1);
423                 cal1 = timeRange_createCalendar(timezone, hour1, -1, -1);
424                 cal2 = (Calendar) cal1.clone();
425                 cal2.add(Calendar.HOUR_OF_DAY, 1);
426                 break;
427 
428             case 2:
429                 hour1 = getSmallInt(value1);
430                 cal1 = timeRange_createCalendar(timezone, hour1, -1, -1);
431                 hour2 = getSmallInt(value2);
432                 cal2 = timeRange_createCalendar(timezone, hour2, -1, -1);
433                 break;
434 
435             case 4:
436                 hour1 = getSmallInt(value1);
437                 min1 = getSmallInt(value2);
438                 hour2 = getSmallInt(value3);
439                 min2 = getSmallInt(value4);
440                 cal1 = dateRange_createCalendar(timezone, hour1, min1, -1);
441                 cal2 = dateRange_createCalendar(timezone, hour2, min2, -1);
442                 break;
443 
444             default:
445                 hour1 = getSmallInt(value1);
446                 min1 = getSmallInt(value2);
447                 second1 = getSmallInt(value3);
448                 hour2 = getSmallInt(value4);
449                 min2 = getSmallInt(value5);
450                 second2 = getSmallInt(value6);
451                 cal1 = dateRange_createCalendar(timezone, hour1, min1, second1);
452                 cal2 = dateRange_createCalendar(timezone, hour2, min2, second2);
453         }
454 
455         final Calendar now = Calendar.getInstance(timezone);
456         return now.equals(cal1) || now.after(cal1) && now.before(cal2) || now.equals(cal2);
457     }
458 
459     private static Calendar timeRange_createCalendar(final TimeZone timezone,
460             final int hour, final int minute, final int second) {
461         final Calendar calendar = Calendar.getInstance(timezone);
462         if (hour != -1) {
463             calendar.set(Calendar.HOUR_OF_DAY, hour);
464         }
465         if (minute != -1) {
466             calendar.set(Calendar.MINUTE, minute);
467         }
468         if (second != -1) {
469             calendar.set(Calendar.SECOND, second);
470         }
471         return calendar;
472     }
473 }