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 |