Home | History | Annotate | Download | only in webpagereplay
      1 #!/usr/bin/env python
      2 # Copyright 2010 Google Inc. All Rights Reserved.
      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 #
      8 #      http://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,
     12 # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
     13 # See the License for the specific language governing permissions and
     14 # limitations under the License.
     15 
     16 """Provides cross-platform utility functions.
     17 
     18 Example:
     19   import platformsettings
     20   ip = platformsettings.get_server_ip_address()
     21 
     22 Functions with "_temporary_" in their name automatically clean-up upon
     23 termination (via the atexit module).
     24 
     25 For the full list of functions, see the bottom of the file.
     26 """
     27 
     28 import atexit
     29 import distutils.spawn
     30 import distutils.version
     31 import fileinput
     32 import logging
     33 import os
     34 import platform
     35 import re
     36 import socket
     37 import stat
     38 import subprocess
     39 import sys
     40 import time
     41 import urlparse
     42 
     43 
     44 class PlatformSettingsError(Exception):
     45   """Module catch-all error."""
     46   pass
     47 
     48 
     49 class DnsReadError(PlatformSettingsError):
     50   """Raised when unable to read DNS settings."""
     51   pass
     52 
     53 
     54 class DnsUpdateError(PlatformSettingsError):
     55   """Raised when unable to update DNS settings."""
     56   pass
     57 
     58 
     59 class NotAdministratorError(PlatformSettingsError):
     60   """Raised when not running as administrator."""
     61   pass
     62 
     63 
     64 class CalledProcessError(PlatformSettingsError):
     65   """Raised when a _check_output() process returns a non-zero exit status."""
     66   def __init__(self, returncode, cmd):
     67     super(CalledProcessError, self).__init__()
     68     self.returncode = returncode
     69     self.cmd = cmd
     70 
     71   def __str__(self):
     72     return 'Command "%s" returned non-zero exit status %d' % (
     73         ' '.join(self.cmd), self.returncode)
     74 
     75 
     76 def FindExecutable(executable):
     77   """Finds the given executable in PATH.
     78 
     79   Since WPR may be invoked as sudo, meaning PATH is empty, we also hardcode a
     80   few common paths.
     81 
     82   Returns:
     83     The fully qualified path with .exe appended if appropriate or None if it
     84     doesn't exist.
     85   """
     86   return distutils.spawn.find_executable(executable,
     87                                          os.pathsep.join([os.environ['PATH'],
     88                                                           '/sbin',
     89                                                           '/usr/bin',
     90                                                           '/usr/sbin/',
     91                                                           '/usr/local/sbin',
     92                                                           ]))
     93 
     94 def HasSniSupport():
     95   try:
     96     import OpenSSL
     97     return (distutils.version.StrictVersion(OpenSSL.__version__) >=
     98             distutils.version.StrictVersion('0.13'))
     99   except ImportError:
    100     return False
    101 
    102 
    103 def SupportsFdLimitControl():
    104   """Whether the platform supports changing the process fd limit."""
    105   return os.name is 'posix'
    106 
    107 
    108 def GetFdLimit():
    109   """Returns a tuple of (soft_limit, hard_limit)."""
    110   import resource
    111   return resource.getrlimit(resource.RLIMIT_NOFILE)
    112 
    113 
    114 def AdjustFdLimit(new_soft_limit, new_hard_limit):
    115   """Sets a new soft and hard limit for max number of fds."""
    116   import resource
    117   resource.setrlimit(resource.RLIMIT_NOFILE, (new_soft_limit, new_hard_limit))
    118 
    119 
    120 class SystemProxy(object):
    121   """A host/port pair for a HTTP or HTTPS proxy configuration."""
    122 
    123   def __init__(self, host, port):
    124     """Initialize a SystemProxy instance.
    125 
    126     Args:
    127       host: a host name or IP address string (e.g. "example.com" or "1.1.1.1").
    128       port: a port string or integer (e.g. "8888" or 8888).
    129     """
    130     self.host = host
    131     self.port = int(port) if port else None
    132 
    133   def __nonzero__(self):
    134     """True if the host is set."""
    135     return bool(self.host)
    136 
    137   @classmethod
    138   def from_url(cls, proxy_url):
    139     """Create a SystemProxy instance.
    140 
    141     If proxy_url is None, an empty string, or an invalid URL, the
    142     SystemProxy instance with have None and None for the host and port
    143     (no exception is raised).
    144 
    145     Args:
    146       proxy_url: a proxy url string such as "http://proxy.com:8888/".
    147     Returns:
    148       a System proxy instance.
    149     """
    150     if proxy_url:
    151       parse_result = urlparse.urlparse(proxy_url)
    152       return cls(parse_result.hostname, parse_result.port)
    153     return cls(None, None)
    154 
    155 
    156 class _BasePlatformSettings(object):
    157 
    158   def get_system_logging_handler(self):
    159     """Return a handler for the logging module (optional)."""
    160     return None
    161 
    162   def rerun_as_administrator(self):
    163     """If needed, rerun the program with administrative privileges.
    164 
    165     Raises NotAdministratorError if unable to rerun.
    166     """
    167     pass
    168 
    169   def timer(self):
    170     """Return the current time in seconds as a floating point number."""
    171     return time.time()
    172 
    173   def get_server_ip_address(self, is_server_mode=False):
    174     """Returns the IP address to use for dnsproxy and ipfw."""
    175     if is_server_mode:
    176       return socket.gethostbyname(socket.gethostname())
    177     return '127.0.0.1'
    178 
    179   def get_httpproxy_ip_address(self, is_server_mode=False):
    180     """Returns the IP address to use for httpproxy."""
    181     if is_server_mode:
    182       return '0.0.0.0'
    183     return '127.0.0.1'
    184 
    185   def get_system_proxy(self, use_ssl):
    186     """Returns the system HTTP(S) proxy host, port."""
    187     del use_ssl
    188     return SystemProxy(None, None)
    189 
    190   def _ipfw_cmd(self):
    191     raise NotImplementedError
    192 
    193   def ipfw(self, *args):
    194     ipfw_cmd = (self._ipfw_cmd(), ) + args
    195     return self._check_output(*ipfw_cmd, elevate_privilege=True)
    196 
    197   def has_ipfw(self):
    198     try:
    199       self.ipfw('list')
    200       return True
    201     except AssertionError as e:
    202       logging.warning('Failed to start ipfw command. '
    203                       'Error: %s' % e.message)
    204       return False
    205 
    206   def _get_cwnd(self):
    207     return None
    208 
    209   def _set_cwnd(self, args):
    210     pass
    211 
    212   def _elevate_privilege_for_cmd(self, args):
    213     return args
    214 
    215   def _check_output(self, *args, **kwargs):
    216     """Run Popen(*args) and return its output as a byte string.
    217 
    218     Python 2.7 has subprocess.check_output. This is essentially the same
    219     except that, as a convenience, all the positional args are used as
    220     command arguments and the |elevate_privilege| kwarg is supported.
    221 
    222     Args:
    223       *args: sequence of program arguments
    224       elevate_privilege: Run the command with elevated privileges.
    225     Raises:
    226       CalledProcessError if the program returns non-zero exit status.
    227     Returns:
    228       output as a byte string.
    229     """
    230     command_args = [str(a) for a in args]
    231 
    232     if os.path.sep not in command_args[0]:
    233       qualified_command = FindExecutable(command_args[0])
    234       assert qualified_command, 'Failed to find %s in path' % command_args[0]
    235       command_args[0] = qualified_command
    236 
    237     if kwargs.get('elevate_privilege'):
    238       command_args = self._elevate_privilege_for_cmd(command_args)
    239 
    240     logging.debug(' '.join(command_args))
    241     process = subprocess.Popen(
    242         command_args, stdout=subprocess.PIPE, stderr=subprocess.STDOUT)
    243     output = process.communicate()[0]
    244     retcode = process.poll()
    245     if retcode:
    246       raise CalledProcessError(retcode, command_args)
    247     return output
    248 
    249   def set_temporary_tcp_init_cwnd(self, cwnd):
    250     cwnd = int(cwnd)
    251     original_cwnd = self._get_cwnd()
    252     if original_cwnd is None:
    253       raise PlatformSettingsError('Unable to get current tcp init_cwnd.')
    254     if cwnd == original_cwnd:
    255       logging.info('TCP init_cwnd already set to target value: %s', cwnd)
    256     else:
    257       self._set_cwnd(cwnd)
    258       if self._get_cwnd() == cwnd:
    259         logging.info('Changed cwnd to %s', cwnd)
    260         atexit.register(self._set_cwnd, original_cwnd)
    261       else:
    262         logging.error('Unable to update cwnd to %s', cwnd)
    263 
    264   def setup_temporary_loopback_config(self):
    265     """Setup the loopback interface similar to real interface.
    266 
    267     We use loopback for much of our testing, and on some systems, loopback
    268     behaves differently from real interfaces.
    269     """
    270     logging.error('Platform does not support loopback configuration.')
    271 
    272   def _save_primary_interface_properties(self):
    273     self._orig_nameserver = self.get_original_primary_nameserver()
    274 
    275   def _restore_primary_interface_properties(self):
    276     self._set_primary_nameserver(self._orig_nameserver)
    277 
    278   def _get_primary_nameserver(self):
    279     raise NotImplementedError
    280 
    281   def _set_primary_nameserver(self, _):
    282     raise NotImplementedError
    283 
    284   def get_original_primary_nameserver(self):
    285     if not hasattr(self, '_original_nameserver'):
    286       self._original_nameserver = self._get_primary_nameserver()
    287       logging.info('Saved original primary DNS nameserver: %s',
    288                    self._original_nameserver)
    289     return self._original_nameserver
    290 
    291   def set_temporary_primary_nameserver(self, nameserver):
    292     self._save_primary_interface_properties()
    293     self._set_primary_nameserver(nameserver)
    294     if self._get_primary_nameserver() == nameserver:
    295       logging.info('Changed temporary primary nameserver to %s', nameserver)
    296       atexit.register(self._restore_primary_interface_properties)
    297     else:
    298       raise self._get_dns_update_error()
    299 
    300 
    301 class _PosixPlatformSettings(_BasePlatformSettings):
    302 
    303   # pylint: disable=abstract-method
    304   # Suppress lint check for _get_primary_nameserver & _set_primary_nameserver
    305 
    306   def rerun_as_administrator(self):
    307     """If needed, rerun the program with administrative privileges.
    308 
    309     Raises NotAdministratorError if unable to rerun.
    310     """
    311     if os.geteuid() != 0:
    312       logging.warn('Rerunning with sudo: %s', sys.argv)
    313       os.execv('/usr/bin/sudo', ['--'] + sys.argv)
    314 
    315   def _elevate_privilege_for_cmd(self, args):
    316     def IsSetUID(path):
    317       return (os.stat(path).st_mode & stat.S_ISUID) == stat.S_ISUID
    318 
    319     def IsElevated():
    320       p = subprocess.Popen(
    321           ['sudo', '-nv'], stdin=subprocess.PIPE, stdout=subprocess.PIPE,
    322           stderr=subprocess.STDOUT)
    323       stdout = p.communicate()[0]
    324       # Some versions of sudo set the returncode based on whether sudo requires
    325       # a password currently. Other versions return output when password is
    326       # required and no output when the user is already authenticated.
    327       return not p.returncode and not stdout
    328 
    329     if not IsSetUID(args[0]):
    330       args = ['sudo'] + args
    331 
    332       if not IsElevated():
    333         print 'WPR needs to run %s under sudo. Please authenticate.' % args[1]
    334         subprocess.check_call(['sudo', '-v'])  # Synchronously authenticate.
    335 
    336         prompt = ('Would you like to always allow %s to run without sudo '
    337                   '(via `sudo chmod +s %s`)? (y/N)' % (args[1], args[1]))
    338         if raw_input(prompt).lower() == 'y':
    339           subprocess.check_call(['sudo', 'chmod', '+s', args[1]])
    340     return args
    341 
    342   def get_system_proxy(self, use_ssl):
    343     """Returns the system HTTP(S) proxy host, port."""
    344     proxy_url = os.environ.get('https_proxy' if use_ssl else 'http_proxy')
    345     return SystemProxy.from_url(proxy_url)
    346 
    347   def _ipfw_cmd(self):
    348     return 'ipfw'
    349 
    350   def _get_dns_update_error(self):
    351     return DnsUpdateError('Did you run under sudo?')
    352 
    353   def _sysctl(self, *args, **kwargs):
    354     sysctl_args = [FindExecutable('sysctl')]
    355     if kwargs.get('use_sudo'):
    356       sysctl_args = self._elevate_privilege_for_cmd(sysctl_args)
    357     sysctl_args.extend(str(a) for a in args)
    358     sysctl = subprocess.Popen(
    359         sysctl_args, stdin=subprocess.PIPE, stdout=subprocess.PIPE)
    360     stdout = sysctl.communicate()[0]
    361     return sysctl.returncode, stdout
    362 
    363   def has_sysctl(self, name):
    364     if not hasattr(self, 'has_sysctl_cache'):
    365       self.has_sysctl_cache = {}
    366     if name not in self.has_sysctl_cache:
    367       self.has_sysctl_cache[name] = self._sysctl(name)[0] == 0
    368     return self.has_sysctl_cache[name]
    369 
    370   def set_sysctl(self, name, value):
    371     rv = self._sysctl('%s=%s' % (name, value), use_sudo=True)[0]
    372     if rv != 0:
    373       logging.error('Unable to set sysctl %s: %s', name, rv)
    374 
    375   def get_sysctl(self, name):
    376     rv, value = self._sysctl('-n', name)
    377     if rv == 0:
    378       return value
    379     else:
    380       logging.error('Unable to get sysctl %s: %s', name, rv)
    381       return None
    382 
    383 
    384 class _OsxPlatformSettings(_PosixPlatformSettings):
    385   LOCAL_SLOWSTART_MIB_NAME = 'net.inet.tcp.local_slowstart_flightsize'
    386 
    387   def _scutil(self, cmd):
    388     scutil = subprocess.Popen([FindExecutable('scutil')],
    389                                stdin=subprocess.PIPE, stdout=subprocess.PIPE)
    390     return scutil.communicate(cmd)[0]
    391 
    392   def _ifconfig(self, *args):
    393     return self._check_output('ifconfig', *args, elevate_privilege=True)
    394 
    395   def set_sysctl(self, name, value):
    396     rv = self._sysctl('-w', '%s=%s' % (name, value), use_sudo=True)[0]
    397     if rv != 0:
    398       logging.error('Unable to set sysctl %s: %s', name, rv)
    399 
    400   def _get_cwnd(self):
    401     return int(self.get_sysctl(self.LOCAL_SLOWSTART_MIB_NAME))
    402 
    403   def _set_cwnd(self, size):
    404     self.set_sysctl(self.LOCAL_SLOWSTART_MIB_NAME, size)
    405 
    406   def _get_loopback_mtu(self):
    407     config = self._ifconfig('lo0')
    408     match = re.search(r'\smtu\s+(\d+)', config)
    409     return int(match.group(1)) if match else None
    410 
    411   def setup_temporary_loopback_config(self):
    412     """Configure loopback to temporarily use reasonably sized frames.
    413 
    414     OS X uses jumbo frames by default (16KB).
    415     """
    416     TARGET_LOOPBACK_MTU = 1500
    417     original_mtu = self._get_loopback_mtu()
    418     if original_mtu is None:
    419       logging.error('Unable to read loopback mtu. Setting left unchanged.')
    420       return
    421     if original_mtu == TARGET_LOOPBACK_MTU:
    422       logging.debug('Loopback MTU already has target value: %d', original_mtu)
    423     else:
    424       self._ifconfig('lo0', 'mtu', TARGET_LOOPBACK_MTU)
    425       if self._get_loopback_mtu() == TARGET_LOOPBACK_MTU:
    426         logging.debug('Set loopback MTU to %d (was %d)',
    427                       TARGET_LOOPBACK_MTU, original_mtu)
    428         atexit.register(self._ifconfig, 'lo0', 'mtu', original_mtu)
    429       else:
    430         logging.error('Unable to change loopback MTU from %d to %d',
    431                       original_mtu, TARGET_LOOPBACK_MTU)
    432 
    433   def _get_dns_service_key(self):
    434     output = self._scutil('show State:/Network/Global/IPv4')
    435     lines = output.split('\n')
    436     for line in lines:
    437       key_value = line.split(' : ')
    438       if key_value[0] == '  PrimaryService':
    439         return 'State:/Network/Service/%s/DNS' % key_value[1]
    440     raise DnsReadError('Unable to find DNS service key: %s', output)
    441 
    442   def _get_primary_nameserver(self):
    443     output = self._scutil('show %s' % self._get_dns_service_key())
    444     match = re.search(
    445         br'ServerAddresses\s+:\s+<array>\s+{\s+0\s+:\s+((\d{1,3}\.){3}\d{1,3})',
    446         output)
    447     if match:
    448       return match.group(1)
    449     else:
    450       raise DnsReadError('Unable to find primary DNS server: %s', output)
    451 
    452   def _set_primary_nameserver(self, dns):
    453     command = '\n'.join([
    454       'd.init',
    455       'd.add ServerAddresses * %s' % dns,
    456       'set %s' % self._get_dns_service_key()
    457     ])
    458     self._scutil(command)
    459 
    460 
    461 class _FreeBSDPlatformSettings(_PosixPlatformSettings):
    462   """Partial implementation for FreeBSD.  Does not allow a DNS server to be
    463   launched nor ipfw to be used.
    464   """
    465   RESOLV_CONF = '/etc/resolv.conf'
    466 
    467   def _get_default_route_line(self):
    468     raise NotImplementedError
    469 
    470   def _set_cwnd(self, cwnd):
    471     raise NotImplementedError
    472 
    473   def _get_cwnd(self):
    474     raise NotImplementedError
    475 
    476   def setup_temporary_loopback_config(self):
    477     raise NotImplementedError
    478 
    479   def _write_resolve_conf(self, dns):
    480     raise NotImplementedError
    481 
    482   def _get_primary_nameserver(self):
    483     try:
    484       resolv_file = open(self.RESOLV_CONF)
    485     except IOError:
    486       raise DnsReadError()
    487     for line in resolv_file:
    488       if line.startswith('nameserver '):
    489         return line.split()[1]
    490     raise DnsReadError()
    491 
    492   def _set_primary_nameserver(self, dns):
    493     raise NotImplementedError
    494 
    495 
    496 class _LinuxPlatformSettings(_PosixPlatformSettings):
    497   """The following thread recommends a way to update DNS on Linux:
    498 
    499   http://ubuntuforums.org/showthread.php?t=337553
    500 
    501          sudo cp /etc/dhcp3/dhclient.conf /etc/dhcp3/dhclient.conf.bak
    502          sudo gedit /etc/dhcp3/dhclient.conf
    503          #prepend domain-name-servers 127.0.0.1;
    504          prepend domain-name-servers 208.67.222.222, 208.67.220.220;
    505 
    506          prepend domain-name-servers 208.67.222.222, 208.67.220.220;
    507          request subnet-mask, broadcast-address, time-offset, routers,
    508              domain-name, domain-name-servers, host-name,
    509              netbios-name-servers, netbios-scope;
    510          #require subnet-mask, domain-name-servers;
    511 
    512          sudo /etc/init.d/networking restart
    513 
    514   The code below does not try to change dchp and does not restart networking.
    515   Update this as needed to make it more robust on more systems.
    516   """
    517   RESOLV_CONF = '/etc/resolv.conf'
    518   ROUTE_RE = re.compile('initcwnd (\d+)')
    519   TCP_BASE_MSS = 'net.ipv4.tcp_base_mss'
    520   TCP_MTU_PROBING = 'net.ipv4.tcp_mtu_probing'
    521 
    522   def _get_default_route_line(self):
    523     stdout = self._check_output('ip', 'route')
    524     for line in stdout.split('\n'):
    525       if line.startswith('default'):
    526         return line
    527     return None
    528 
    529   def _set_cwnd(self, cwnd):
    530     default_line = self._get_default_route_line()
    531     self._check_output(
    532         'ip', 'route', 'change', default_line, 'initcwnd', str(cwnd))
    533 
    534   def _get_cwnd(self):
    535     default_line = self._get_default_route_line()
    536     m = self.ROUTE_RE.search(default_line)
    537     if m:
    538       return int(m.group(1))
    539     # If 'initcwnd' wasn't found, then 0 means it's the system default.
    540     return 0
    541 
    542   def setup_temporary_loopback_config(self):
    543     """Setup Linux to temporarily use reasonably sized frames.
    544 
    545     Linux uses jumbo frames by default (16KB), using the combination
    546     of MTU probing and a base MSS makes it use normal sized packets.
    547 
    548     The reason this works is because tcp_base_mss is only used when MTU
    549     probing is enabled.  And since we're using the max value, it will
    550     always use the reasonable size.  This is relevant for server-side realism.
    551     The client-side will vary depending on the client TCP stack config.
    552     """
    553     ENABLE_MTU_PROBING = 2
    554     original_probing = self.get_sysctl(self.TCP_MTU_PROBING)
    555     self.set_sysctl(self.TCP_MTU_PROBING, ENABLE_MTU_PROBING)
    556     atexit.register(self.set_sysctl, self.TCP_MTU_PROBING, original_probing)
    557 
    558     TCP_FULL_MSS = 1460
    559     original_mss = self.get_sysctl(self.TCP_BASE_MSS)
    560     self.set_sysctl(self.TCP_BASE_MSS, TCP_FULL_MSS)
    561     atexit.register(self.set_sysctl, self.TCP_BASE_MSS, original_mss)
    562 
    563   def _write_resolve_conf(self, dns):
    564     is_first_nameserver_replaced = False
    565     # The fileinput module uses sys.stdout as the edited file output.
    566     for line in fileinput.input(self.RESOLV_CONF, inplace=1, backup='.bak'):
    567       if line.startswith('nameserver ') and not is_first_nameserver_replaced:
    568         print 'nameserver %s' % dns
    569         is_first_nameserver_replaced = True
    570       else:
    571         print line,
    572     if not is_first_nameserver_replaced:
    573       raise DnsUpdateError('Could not find a suitable nameserver entry in %s' %
    574                            self.RESOLV_CONF)
    575 
    576   def _get_primary_nameserver(self):
    577     try:
    578       resolv_file = open(self.RESOLV_CONF)
    579     except IOError:
    580       raise DnsReadError()
    581     for line in resolv_file:
    582       if line.startswith('nameserver '):
    583         return line.split()[1]
    584     raise DnsReadError()
    585 
    586   def _set_primary_nameserver(self, dns):
    587     """Replace the first nameserver entry with the one given."""
    588     try:
    589       self._write_resolve_conf(dns)
    590     except OSError, e:
    591       if 'Permission denied' in e:
    592         raise self._get_dns_update_error()
    593       raise
    594 
    595 
    596 class _WindowsPlatformSettings(_BasePlatformSettings):
    597 
    598   # pylint: disable=abstract-method
    599   # Suppress lint check for _ipfw_cmd
    600 
    601   def get_system_logging_handler(self):
    602     """Return a handler for the logging module (optional).
    603 
    604     For Windows, output can be viewed with DebugView.
    605     http://technet.microsoft.com/en-us/sysinternals/bb896647.aspx
    606     """
    607     import ctypes
    608     output_debug_string = ctypes.windll.kernel32.OutputDebugStringA
    609     output_debug_string.argtypes = [ctypes.c_char_p]
    610     class DebugViewHandler(logging.Handler):
    611       def emit(self, record):
    612         output_debug_string('[wpr] ' + self.format(record))
    613     return DebugViewHandler()
    614 
    615   def rerun_as_administrator(self):
    616     """If needed, rerun the program with administrative privileges.
    617 
    618     Raises NotAdministratorError if unable to rerun.
    619     """
    620     import ctypes
    621     if not ctypes.windll.shell32.IsUserAnAdmin():
    622       raise NotAdministratorError('Rerun with administrator privileges.')
    623       #os.execv('runas', sys.argv)  # TODO: replace needed Windows magic
    624 
    625   def timer(self):
    626     """Return the current time in seconds as a floating point number.
    627 
    628     From time module documentation:
    629        On Windows, this function [time.clock()] returns wall-clock
    630        seconds elapsed since the first call to this function, as a
    631        floating point number, based on the Win32 function
    632        QueryPerformanceCounter(). The resolution is typically better
    633        than one microsecond.
    634     """
    635     return time.clock()
    636 
    637   def _arp(self, *args):
    638     return self._check_output('arp', *args)
    639 
    640   def _route(self, *args):
    641     return self._check_output('route', *args)
    642 
    643   def _ipconfig(self, *args):
    644     return self._check_output('ipconfig', *args)
    645 
    646   def _get_mac_address(self, ip):
    647     """Return the MAC address for the given ip."""
    648     ip_re = re.compile(r'^\s*IP(?:v4)? Address[ .]+:\s+([0-9.]+)')
    649     for line in self._ipconfig('/all').splitlines():
    650       if line[:1].isalnum():
    651         current_ip = None
    652         current_mac = None
    653       elif ':' in line:
    654         line = line.strip()
    655         ip_match = ip_re.match(line)
    656         if ip_match:
    657           current_ip = ip_match.group(1)
    658         elif line.startswith('Physical Address'):
    659           current_mac = line.split(':', 1)[1].lstrip()
    660         if current_ip == ip and current_mac:
    661           return current_mac
    662     return None
    663 
    664   def setup_temporary_loopback_config(self):
    665     """On Windows, temporarily route the server ip to itself."""
    666     ip = self.get_server_ip_address()
    667     mac_address = self._get_mac_address(ip)
    668     if self.mac_address:
    669       self._arp('-s', ip, self.mac_address)
    670       self._route('add', ip, ip, 'mask', '255.255.255.255')
    671       atexit.register(self._arp, '-d', ip)
    672       atexit.register(self._route, 'delete', ip, ip, 'mask', '255.255.255.255')
    673     else:
    674       logging.warn('Unable to configure loopback: MAC address not found.')
    675     # TODO(slamm): Configure cwnd, MTU size
    676 
    677   def _get_dns_update_error(self):
    678     return DnsUpdateError('Did you run as administrator?')
    679 
    680   def _netsh_show_dns(self):
    681     """Return DNS information:
    682 
    683     Example output:
    684         Configuration for interface "Local Area Connection 3"
    685         DNS servers configured through DHCP:  None
    686         Register with which suffix:           Primary only
    687 
    688         Configuration for interface "Wireless Network Connection 2"
    689         DNS servers configured through DHCP:  192.168.1.1
    690         Register with which suffix:           Primary only
    691     """
    692     return self._check_output('netsh', 'interface', 'ip', 'show', 'dns')
    693 
    694   def _netsh_set_dns(self, iface_name, addr):
    695     """Modify DNS information on the primary interface."""
    696     output = self._check_output('netsh', 'interface', 'ip', 'set', 'dns',
    697                                 iface_name, 'static', addr)
    698 
    699   def _netsh_set_dns_dhcp(self, iface_name):
    700     """Modify DNS information on the primary interface."""
    701     output = self._check_output('netsh', 'interface', 'ip', 'set', 'dns',
    702                                 iface_name, 'dhcp')
    703 
    704   def _get_interfaces_with_dns(self):
    705     output = self._netsh_show_dns()
    706     lines = output.split('\n')
    707     iface_re = re.compile(r'^Configuration for interface \"(?P<name>.*)\"')
    708     dns_re = re.compile(r'(?P<kind>.*):\s+(?P<dns>\d+\.\d+\.\d+\.\d+)')
    709     iface_name = None
    710     iface_dns = None
    711     iface_kind = None
    712     ifaces = []
    713     for line in lines:
    714       iface_match = iface_re.match(line)
    715       if iface_match:
    716         iface_name = iface_match.group('name')
    717       dns_match = dns_re.match(line)
    718       if dns_match:
    719         iface_dns = dns_match.group('dns')
    720         iface_dns_config = dns_match.group('kind').strip()
    721         if iface_dns_config == "Statically Configured DNS Servers":
    722           iface_kind = "static"
    723         elif iface_dns_config == "DNS servers configured through DHCP":
    724           iface_kind = "dhcp"
    725       if iface_name and iface_dns and iface_kind:
    726         ifaces.append((iface_dns, iface_name, iface_kind))
    727         iface_name = None
    728         iface_dns = None
    729     return ifaces
    730 
    731   def _save_primary_interface_properties(self):
    732     # TODO(etienneb): On windows, an interface can have multiple DNS server
    733     # configured. We should save/restore all of them.
    734     ifaces = self._get_interfaces_with_dns()
    735     self._primary_interfaces = ifaces
    736 
    737   def _restore_primary_interface_properties(self):
    738     for iface in self._primary_interfaces:
    739       (iface_dns, iface_name, iface_kind) = iface
    740       self._netsh_set_dns(iface_name, iface_dns)
    741       if iface_kind == "dhcp":
    742         self._netsh_set_dns_dhcp(iface_name)
    743 
    744   def _get_primary_nameserver(self):
    745     ifaces = self._get_interfaces_with_dns()
    746     if not len(ifaces):
    747       raise DnsUpdateError("Interface with valid DNS configured not found.")
    748     (iface_dns, iface_name, iface_kind) = ifaces[0]
    749     return iface_dns
    750 
    751   def _set_primary_nameserver(self, dns):
    752     for iface in self._primary_interfaces:
    753       (iface_dns, iface_name, iface_kind) = iface
    754       self._netsh_set_dns(iface_name, dns)
    755 
    756 
    757 class _WindowsXpPlatformSettings(_WindowsPlatformSettings):
    758   def _ipfw_cmd(self):
    759     return (r'third_party\ipfw_win32\ipfw.exe',)
    760 
    761 
    762 def _new_platform_settings(system, release):
    763   """Make a new instance of PlatformSettings for the current system."""
    764   if system == 'Darwin':
    765     return _OsxPlatformSettings()
    766   if system == 'Linux':
    767     return _LinuxPlatformSettings()
    768   if system == 'Windows' and release == 'XP':
    769     return _WindowsXpPlatformSettings()
    770   if system == 'Windows':
    771     return _WindowsPlatformSettings()
    772   if system == 'FreeBSD':
    773     return _FreeBSDPlatformSettings()
    774   raise NotImplementedError('Sorry %s %s is not supported.' % (system, release))
    775 
    776 
    777 # Create one instance of the platform-specific settings and
    778 # make the functions available at the module-level.
    779 _inst = _new_platform_settings(platform.system(), platform.release())
    780 
    781 get_system_logging_handler = _inst.get_system_logging_handler
    782 rerun_as_administrator = _inst.rerun_as_administrator
    783 timer = _inst.timer
    784 
    785 get_server_ip_address = _inst.get_server_ip_address
    786 get_httpproxy_ip_address = _inst.get_httpproxy_ip_address
    787 get_system_proxy = _inst.get_system_proxy
    788 ipfw = _inst.ipfw
    789 has_ipfw = _inst.has_ipfw
    790 set_temporary_tcp_init_cwnd = _inst.set_temporary_tcp_init_cwnd
    791 setup_temporary_loopback_config = _inst.setup_temporary_loopback_config
    792 
    793 get_original_primary_nameserver = _inst.get_original_primary_nameserver
    794 set_temporary_primary_nameserver = _inst.set_temporary_primary_nameserver
    795