| OLD | NEW |
| (Empty) |
| 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 | |
| OLD | NEW |