#!/usr/bin/env python3
# -*- coding: utf-8 -*-
'''
Source from https://github.com/carsonyl/pypac/blob/master/pypac/parser_functions.py
Apache License Version 2.0
No content change except format
'''
"""
Functions and classes for parsing and executing PAC files.
"""
"""
Python implementations of JavaScript functions needed to execute a PAC file.
These are injected into the JavaScript execution context.
They aren't meant to be called directly from Python, so the function signatures may look unusual.
Most docstrings below are adapted from http://findproxyforurl.com/netscape-documentation/.
"""
import socket
from calendar import monthrange
from datetime import datetime, time, date
from fnmatch import fnmatch
from requests.utils import is_ipv4_address
import struct
def dnsDomainIs(host, domain):
"""
:param str host: is the hostname from the URL.
:param str domain: is the domain name to test the hostname against.
:return: true iff the domain of hostname matches.
:rtype: bool
"""
return host.lower().endswith(domain.lower())
def shExpMatch(host, pattern):
"""
Case-insensitive host comparison using a shell expression pattern.
:param str host:
:param str pattern: Shell expression pattern to match against.
:rtype: bool
"""
return fnmatch(host.lower(), pattern.lower())
def _address_in_network(ip, netaddr, mask):
"""
Like :func:`requests.utils.address_in_network` but takes a quad-dotted netmask.
"""
ipaddr = struct.unpack("=L", socket.inet_aton(ip))[0]
netmask = struct.unpack("=L", socket.inet_aton(mask))[0]
network = struct.unpack("=L", socket.inet_aton(netaddr))[0] & netmask
return (ipaddr & netmask) == (network & netmask)
def isInNet(host, pattern, mask):
"""
Pattern and mask specification is done the same way as for SOCKS configuration.
:param str host: a DNS hostname, or IP address.
If a hostname is passed, it will be resolved into an IP address by this function.
:param str pattern: an IP address pattern in the dot-separated format
:param str mask: mask for the IP address pattern informing which parts of
the IP address should be matched against. 0 means ignore, 255 means match.
:returns: True iff the IP address of the host matches the specified IP address pattern.
:rtype: bool
"""
host_ip = host if is_ipv4_address(host) else dnsResolve(host)
if not host_ip or not is_ipv4_address(pattern) or not is_ipv4_address(mask):
return False
return _address_in_network(host_ip, pattern, mask)
def localHostOrDomainIs(host, hostdom):
"""
:param str host: the hostname from the URL.
:param str hostdom: fully qualified hostname to match against.
:return: true if the hostname matches exactly the specified hostname,
or if there is no domain name part in the hostname, but the unqualified hostname matches.
:rtype: bool
"""
return hostdom.lower().startswith(host.lower())
def myIpAddress():
"""
:returns: the IP address of the host that the Navigator is running on,
as a string in the dot-separated integer format.
:rtype: str
"""
return dnsResolve(socket.gethostname())
def dnsResolve(host):
"""
Resolves the given DNS hostname into an IP address, and returns it in the dot separated format as a string.
Returns an empty string if there is an error
:param str host: hostname to resolve
:return: Resolved IP address, or empty string if resolution failed.
:rtype: str
"""
try:
return socket.gethostbyname(host)
except socket.gaierror:
return ""
def isPlainHostName(host):
"""
:param str host: the hostname from the URL (excluding port number).
:return: True iff there is no domain name in the hostname (no dots).
:rtype: bool
"""
return dnsDomainLevels(host) == 0
def isResolvable(host):
"""
Tries to resolve the hostname.
:param str host: is the hostname from the URL.
:return: true if succeeds.
:rtype: bool
"""
try:
socket.gethostbyname(host)
except socket.gaierror:
return False
return True
def dnsDomainLevels(host):
"""
:param str host: is the hostname from the URL.
:return: the number (integer) of DNS domain levels (number of dots) in the hostname.
:rtype: int
"""
return host.count(".")
def weekdayRange(start_day, end_day=None, gmt=None):
"""
Accepted forms:
* ``weekdayRange(wd1)``
* ``weekdayRange(wd1, gmt)``
* ``weekdayRange(wd1, wd2)``
* ``weekdayRange(wd1, wd2, gmt)``
If only one parameter is present, the function yields a true value on the weekday that the parameter represents.
If the string "GMT" is specified as a second parameter, times are taken to be in GMT, otherwise in local timezone.
If both ``wd1`` and wd2`` are defined, the condition is true if the current weekday is in between those two weekdays.
Bounds are inclusive. If the ``gmt`` parameter is specified, times are taken to be in GMT,
otherwise the local timezone is used.
Weekday arguments are one of ``MON TUE WED THU FRI SAT SUN``.
:param str start_day: Weekday string.
:param str end_day: Weekday string.
:param str gmt: is either the string: GMT or is left out.
:rtype: bool
"""
now_weekday_num = _now("GMT" if end_day == "GMT" else gmt).weekday()
weekdays = ["MON", "TUE", "WED", "THU", "FRI", "SAT", "SUN"]
if start_day not in weekdays or (end_day not in weekdays and end_day != "GMT"):
return False
start_day_num = weekdays.index(start_day)
if start_day and (not end_day or end_day == "GMT"):
return start_day_num == now_weekday_num
end_day_num = weekdays.index(end_day)
if end_day_num < start_day_num: # Range past Sunday.
return now_weekday_num >= start_day_num or now_weekday_num <= end_day_num
return start_day_num <= now_weekday_num <= end_day_num
def _now(gmt=None):
"""
:param str|None gmt: Use 'GMT' to get GMT.
:rtype: datetime
"""
return datetime.utcnow() if gmt == "GMT" else datetime.today()
def dateRange(*args):
"""
Accepted forms:
* ``dateRange(day)``
* ``dateRange(day1, day2)``
* ``dateRange(mon)``
* ``dateRange(month1, month2)``
* ``dateRange(year)``
* ``dateRange(year1, year2)``
* ``dateRange(day1, month1, day2, month2)``
* ``dateRange(month1, year1, month2, year2)``
* ``dateRange(day1, month1, year1, day2, month2, year2)``
* ``dateRange(day1, month1, year1, day2, month2, year2, gmt)``
``day``
is the day of month between 1 and 31 (as an integer).
``month``
is one of the month strings:
``JAN FEB MAR APR MAY JUN JUL AUG SEP OCT NOV DEC``
``year``
is the full year number, for example 1995 (but not 95). Integer.
``gmt``
is either the string "GMT", which makes time comparison occur in GMT timezone;
if left unspecified, times are taken to be in the local timezone.
Even though the above examples don't show,
the "GMT" parameter can be specified in any of the 9 different call profiles, always as the last parameter.
If only a single value is specified (from each category: ``day``, ``month``, ``year``),
the function returns a true value only on days that match that specification.
If both values are specified, the result is true between those times, including bounds.
:rtype: bool
"""
months = [None, "JAN", "FEB", "MAR", "APR", "MAY",
"JUN", "JUL", "AUG", "SEP", "OCT", "NOV", "DEC"]
gmt_arg_present = (len(args) == 2 and args[1] == "GMT") or (
len(args) % 2 == 1 and len(args) > 1)
if gmt_arg_present:
# Remove and handle GMT argument.
today = _now(args[-1])
args = args[:-1]
else:
today = _now()
today = today.date()
num_args = len(args)
try:
if num_args == 1: