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
18Example:
19  import platformsettings
20  ip = platformsettings.get_server_ip_address()
21
22Functions with "_temporary_" in their name automatically clean-up upon
23termination (via the atexit module).
24
25For the full list of functions, see the bottom of the file.
26"""
27
28import atexit
29import distutils.spawn
30import distutils.version
31import fileinput
32import logging
33import os
34import platform
35import re
36import socket
37import stat
38import subprocess
39import sys
40import time
41import urlparse
42
43
44class PlatformSettingsError(Exception):
45  """Module catch-all error."""
46  pass
47
48
49class DnsReadError(PlatformSettingsError):
50  """Raised when unable to read DNS settings."""
51  pass
52
53
54class DnsUpdateError(PlatformSettingsError):
55  """Raised when unable to update DNS settings."""
56  pass
57
58
59class NotAdministratorError(PlatformSettingsError):
60  """Raised when not running as administrator."""
61  pass
62
63
64class 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
76def 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
94def 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
103def SupportsFdLimitControl():
104  """Whether the platform supports changing the process fd limit."""
105  return os.name is 'posix'
106
107
108def GetFdLimit():
109  """Returns a tuple of (soft_limit, hard_limit)."""
110  import resource
111  return resource.getrlimit(resource.RLIMIT_NOFILE)
112
113
114def 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
120class 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
156class _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
301class _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
384class _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
461class _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
496class _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
596class _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
757class _WindowsXpPlatformSettings(_WindowsPlatformSettings):
758  def _ipfw_cmd(self):
759    return (r'third_party\ipfw_win32\ipfw.exe',)
760
761
762def _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
781get_system_logging_handler = _inst.get_system_logging_handler
782rerun_as_administrator = _inst.rerun_as_administrator
783timer = _inst.timer
784
785get_server_ip_address = _inst.get_server_ip_address
786get_httpproxy_ip_address = _inst.get_httpproxy_ip_address
787get_system_proxy = _inst.get_system_proxy
788ipfw = _inst.ipfw
789has_ipfw = _inst.has_ipfw
790set_temporary_tcp_init_cwnd = _inst.set_temporary_tcp_init_cwnd
791setup_temporary_loopback_config = _inst.setup_temporary_loopback_config
792
793get_original_primary_nameserver = _inst.get_original_primary_nameserver
794set_temporary_primary_nameserver = _inst.set_temporary_primary_nameserver
795