| OLD | NEW |
| 1 #!/usr/bin/env python | 1 #!/usr/bin/env python |
| 2 # Copyright 2010 Google Inc. All Rights Reserved. | 2 # Copyright 2010 Google Inc. All Rights Reserved. |
| 3 # | 3 # |
| 4 # Licensed under the Apache License, Version 2.0 (the "License"); | 4 # Licensed under the Apache License, Version 2.0 (the "License"); |
| 5 # you may not use this file except in compliance with 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 | 6 # You may obtain a copy of the License at |
| 7 # | 7 # |
| 8 # http://www.apache.org/licenses/LICENSE-2.0 | 8 # http://www.apache.org/licenses/LICENSE-2.0 |
| 9 # | 9 # |
| 10 # Unless required by applicable law or agreed to in writing, software | 10 # Unless required by applicable law or agreed to in writing, software |
| (...skipping 21 matching lines...) Expand all Loading... |
| 32 resources not in the recorded archive. | 32 resources not in the recorded archive. |
| 33 | 33 |
| 34 Network simulation examples: | 34 Network simulation examples: |
| 35 # 128KByte/s uplink bandwidth, 4Mbps/s downlink bandwidth with 100ms RTT time | 35 # 128KByte/s uplink bandwidth, 4Mbps/s downlink bandwidth with 100ms RTT time |
| 36 $ sudo ./replay.py --up 128KByte/s --down 4Mbit/s --delay_ms=100 archive.wpr | 36 $ sudo ./replay.py --up 128KByte/s --down 4Mbit/s --delay_ms=100 archive.wpr |
| 37 | 37 |
| 38 # 1% packet loss rate | 38 # 1% packet loss rate |
| 39 $ sudo ./replay.py --packet_loss_rate=0.01 archive.wpr | 39 $ sudo ./replay.py --packet_loss_rate=0.01 archive.wpr |
| 40 """ | 40 """ |
| 41 | 41 |
| 42 import argparse |
| 42 import json | 43 import json |
| 43 import logging | 44 import logging |
| 44 import optparse | |
| 45 import os | 45 import os |
| 46 import socket | 46 import socket |
| 47 import sys | 47 import sys |
| 48 import traceback | 48 import traceback |
| 49 | 49 |
| 50 import customhandlers | 50 import customhandlers |
| 51 import dnsproxy | 51 import dnsproxy |
| 52 import httparchive | 52 import httparchive |
| 53 import httpclient | 53 import httpclient |
| 54 import httpproxy | 54 import httpproxy |
| (...skipping 115 matching lines...) Expand 10 before | Expand all | Expand 10 after Loading... |
| 170 server_manager.AppendTrafficShaper( | 170 server_manager.AppendTrafficShaper( |
| 171 trafficshaper.TrafficShaper, host=host, | 171 trafficshaper.TrafficShaper, host=host, |
| 172 use_loopback=not options.server_mode and host == '127.0.0.1', | 172 use_loopback=not options.server_mode and host == '127.0.0.1', |
| 173 **options.shaping_dummynet) | 173 **options.shaping_dummynet) |
| 174 | 174 |
| 175 | 175 |
| 176 class OptionsWrapper(object): | 176 class OptionsWrapper(object): |
| 177 """Add checks, updates, and methods to option values. | 177 """Add checks, updates, and methods to option values. |
| 178 | 178 |
| 179 Example: | 179 Example: |
| 180 options, args = option_parser.parse_args() | 180 options, args = arg_parser.parse_args() |
| 181 options = OptionsWrapper(options, option_parser) # run checks and updates | 181 options = OptionsWrapper(options, arg_parser) # run checks and updates |
| 182 if options.record and options.HasTrafficShaping(): | 182 if options.record and options.HasTrafficShaping(): |
| 183 [...] | 183 [...] |
| 184 """ | 184 """ |
| 185 _TRAFFICSHAPING_OPTIONS = { | 185 _TRAFFICSHAPING_OPTIONS = { |
| 186 'down', 'up', 'delay_ms', 'packet_loss_rate', 'init_cwnd', 'net'} | 186 'down', 'up', 'delay_ms', 'packet_loss_rate', 'init_cwnd', 'net'} |
| 187 _CONFLICTING_OPTIONS = ( | 187 _CONFLICTING_OPTIONS = ( |
| 188 ('record', ('down', 'up', 'delay_ms', 'packet_loss_rate', 'net', | 188 ('record', ('down', 'up', 'delay_ms', 'packet_loss_rate', 'net', |
| 189 'spdy', 'use_server_delay')), | 189 'spdy', 'use_server_delay')), |
| 190 ('append', ('down', 'up', 'delay_ms', 'packet_loss_rate', 'net', | 190 ('append', ('down', 'up', 'delay_ms', 'packet_loss_rate', 'net', |
| 191 'use_server_delay')), # same as --record | 191 'use_server_delay')), # same as --record |
| 192 ('net', ('down', 'up', 'delay_ms')), | 192 ('net', ('down', 'up', 'delay_ms')), |
| 193 ('server', ('server_mode',)), | 193 ('server', ('server_mode',)), |
| 194 ) | 194 ) |
| 195 | 195 |
| 196 def __init__(self, options, parser): | 196 def __init__(self, options, parser): |
| 197 self._options = options | 197 self._options = options |
| 198 self._parser = parser | 198 self._parser = parser |
| 199 self._nondefaults = set([ | 199 self._nondefaults = set([ |
| 200 name for name, value in parser.defaults.items() | 200 action.dest for action in parser._optionals._actions |
| 201 if getattr(options, name) != value]) | 201 if getattr(options, action.dest, action.default) is not action.default]) |
| 202 self._CheckConflicts() | 202 self._CheckConflicts() |
| 203 self._CheckValidIp('host') | 203 self._CheckValidIp('host') |
| 204 self._CheckFeatureSupport() | 204 self._CheckFeatureSupport() |
| 205 self._MassageValues() | 205 self._MassageValues() |
| 206 | 206 |
| 207 def _CheckConflicts(self): | 207 def _CheckConflicts(self): |
| 208 """Give an error if mutually exclusive options are used.""" | 208 """Give an error if mutually exclusive options are used.""" |
| 209 for option, bad_options in self._CONFLICTING_OPTIONS: | 209 for option, bad_options in self._CONFLICTING_OPTIONS: |
| 210 if option in self._nondefaults: | 210 if option in self._nondefaults: |
| 211 for bad_option in bad_options: | 211 for bad_option in bad_options: |
| (...skipping 154 matching lines...) Expand 10 before | Expand all | Expand 10 after Loading... |
| 366 except Exception: | 366 except Exception: |
| 367 logging.critical(traceback.format_exc()) | 367 logging.critical(traceback.format_exc()) |
| 368 exit_status = 2 | 368 exit_status = 2 |
| 369 | 369 |
| 370 if options.record: | 370 if options.record: |
| 371 http_archive.Persist(replay_filename) | 371 http_archive.Persist(replay_filename) |
| 372 logging.info('Saved %d responses to %s', len(http_archive), replay_filename) | 372 logging.info('Saved %d responses to %s', len(http_archive), replay_filename) |
| 373 return exit_status | 373 return exit_status |
| 374 | 374 |
| 375 | 375 |
| 376 def GetOptionParser(): | 376 def GetParser(): |
| 377 class PlainHelpFormatter(optparse.IndentedHelpFormatter): | 377 arg_parser = argparse.ArgumentParser( |
| 378 def format_description(self, description): | 378 usage='%(prog)s [options] replay_file', |
| 379 if description: | |
| 380 return description + '\n' | |
| 381 else: | |
| 382 return '' | |
| 383 option_parser = optparse.OptionParser( | |
| 384 usage='%prog [options] replay_file', | |
| 385 formatter=PlainHelpFormatter(), | |
| 386 description=__doc__, | 379 description=__doc__, |
| 380 formatter_class=argparse.RawDescriptionHelpFormatter, |
| 387 epilog='http://code.google.com/p/web-page-replay/') | 381 epilog='http://code.google.com/p/web-page-replay/') |
| 388 | 382 |
| 389 option_parser.add_option('-r', '--record', default=False, | 383 arg_parser.add_argument('replay_filename', type=str, help='Replay file', |
| 384 nargs='?') |
| 385 |
| 386 arg_parser.add_argument('-r', '--record', default=False, |
| 390 action='store_true', | 387 action='store_true', |
| 391 help='Download real responses and record them to replay_file') | 388 help='Download real responses and record them to replay_file') |
| 392 option_parser.add_option('--append', default=False, | 389 arg_parser.add_argument('--append', default=False, |
| 393 action='store_true', | 390 action='store_true', |
| 394 help='Append responses to replay_file.') | 391 help='Append responses to replay_file.') |
| 395 option_parser.add_option('-l', '--log_level', default='debug', | 392 arg_parser.add_argument('-l', '--log_level', default='debug', |
| 396 action='store', | 393 action='store', |
| 397 type='choice', | 394 type=str, |
| 398 choices=('debug', 'info', 'warning', 'error', 'critical'), | 395 choices=('debug', 'info', 'warning', 'error', 'critical'), |
| 399 help='Minimum verbosity level to log') | 396 help='Minimum verbosity level to log') |
| 400 option_parser.add_option('-f', '--log_file', default=None, | 397 arg_parser.add_argument('-f', '--log_file', default=None, |
| 401 action='store', | 398 action='store', |
| 402 type='string', | 399 type=str, |
| 403 help='Log file to use in addition to writting logs to stderr.') | 400 help='Log file to use in addition to writting logs to stderr.') |
| 404 | 401 |
| 405 network_group = optparse.OptionGroup(option_parser, | 402 network_group = arg_parser.add_argument_group( |
| 406 'Network Simulation Options', | 403 title='Network Simulation Options', |
| 407 'These options configure the network simulation in replay mode') | 404 description=('These options configure the network simulation in ' |
| 408 network_group.add_option('-u', '--up', default='0', | 405 'replay mode')) |
| 406 network_group.add_argument('-u', '--up', default='0', |
| 409 action='store', | 407 action='store', |
| 410 type='string', | 408 type=str, |
| 411 help='Upload Bandwidth in [K|M]{bit/s|Byte/s}. Zero means unlimited.') | 409 help='Upload Bandwidth in [K|M]{bit/s|Byte/s}. Zero means unlimited.') |
| 412 network_group.add_option('-d', '--down', default='0', | 410 network_group.add_argument('-d', '--down', default='0', |
| 413 action='store', | 411 action='store', |
| 414 type='string', | 412 type=str, |
| 415 help='Download Bandwidth in [K|M]{bit/s|Byte/s}. Zero means unlimited.') | 413 help='Download Bandwidth in [K|M]{bit/s|Byte/s}. Zero means unlimited.') |
| 416 network_group.add_option('-m', '--delay_ms', default='0', | 414 network_group.add_argument('-m', '--delay_ms', default='0', |
| 417 action='store', | 415 action='store', |
| 418 type='string', | 416 type=str, |
| 419 help='Propagation delay (latency) in milliseconds. Zero means no delay.') | 417 help='Propagation delay (latency) in milliseconds. Zero means no delay.') |
| 420 network_group.add_option('-p', '--packet_loss_rate', default='0', | 418 network_group.add_argument('-p', '--packet_loss_rate', default='0', |
| 421 action='store', | 419 action='store', |
| 422 type='string', | 420 type=str, |
| 423 help='Packet loss rate in range [0..1]. Zero means no loss.') | 421 help='Packet loss rate in range [0..1]. Zero means no loss.') |
| 424 network_group.add_option('-w', '--init_cwnd', default='0', | 422 network_group.add_argument('-w', '--init_cwnd', default='0', |
| 425 action='store', | 423 action='store', |
| 426 type='string', | 424 type=str, |
| 427 help='Set initial cwnd (linux only, requires kernel patch)') | 425 help='Set initial cwnd (linux only, requires kernel patch)') |
| 428 network_group.add_option('--net', default=None, | 426 network_group.add_argument('--net', default=None, |
| 429 action='store', | 427 action='store', |
| 430 type='choice', | 428 type=str, |
| 431 choices=net_configs.NET_CONFIG_NAMES, | 429 choices=net_configs.NET_CONFIG_NAMES, |
| 432 help='Select a set of network options: %s.' % ', '.join( | 430 help='Select a set of network options: %s.' % ', '.join( |
| 433 net_configs.NET_CONFIG_NAMES)) | 431 net_configs.NET_CONFIG_NAMES)) |
| 434 network_group.add_option('--shaping_type', default='dummynet', | 432 network_group.add_argument('--shaping_type', default='dummynet', |
| 435 action='store', | 433 action='store', |
| 436 choices=('dummynet', 'proxy'), | 434 choices=('dummynet', 'proxy'), |
| 437 help='When shaping is configured (i.e. --up, --down, etc.) decides ' | 435 help='When shaping is configured (i.e. --up, --down, etc.) decides ' |
| 438 'whether to use |dummynet| (default), or |proxy| servers.') | 436 'whether to use |dummynet| (default), or |proxy| servers.') |
| 439 option_parser.add_option_group(network_group) | |
| 440 | 437 |
| 441 harness_group = optparse.OptionGroup(option_parser, | 438 harness_group = arg_parser.add_argument_group( |
| 442 'Replay Harness Options', | 439 title='Replay Harness Options', |
| 443 'These advanced options configure various aspects of the replay harness') | 440 description=('These advanced options configure various aspects ' |
| 444 harness_group.add_option('-S', '--server', default=None, | 441 'of the replay harness')) |
| 442 harness_group.add_argument('-S', '--server', default=None, |
| 445 action='store', | 443 action='store', |
| 446 type='string', | 444 type=str, |
| 447 help='IP address of host running "replay.py --server_mode". ' | 445 help='IP address of host running "replay.py --server_mode". ' |
| 448 'This only changes the primary DNS nameserver to use the given IP.') | 446 'This only changes the primary DNS nameserver to use the given IP.') |
| 449 harness_group.add_option('-M', '--server_mode', default=False, | 447 harness_group.add_argument('-M', '--server_mode', default=False, |
| 450 action='store_true', | 448 action='store_true', |
| 451 help='Run replay DNS & http proxies, and trafficshaping on --port ' | 449 help='Run replay DNS & http proxies, and trafficshaping on --port ' |
| 452 'without changing the primary DNS nameserver. ' | 450 'without changing the primary DNS nameserver. ' |
| 453 'Other hosts may connect to this using "replay.py --server" ' | 451 'Other hosts may connect to this using "replay.py --server" ' |
| 454 'or by pointing their DNS to this server.') | 452 'or by pointing their DNS to this server.') |
| 455 harness_group.add_option('-i', '--inject_scripts', default='deterministic.js', | 453 harness_group.add_argument('-i', '--inject_scripts', default='deterministic.js
', |
| 456 action='store', | 454 action='store', |
| 457 dest='inject_scripts', | 455 dest='inject_scripts', |
| 458 help='A comma separated list of JavaScript sources to inject in all ' | 456 help='A comma separated list of JavaScript sources to inject in all ' |
| 459 'pages. By default a script is injected that eliminates sources ' | 457 'pages. By default a script is injected that eliminates sources ' |
| 460 'of entropy such as Date() and Math.random() deterministic. ' | 458 'of entropy such as Date() and Math.random() deterministic. ' |
| 461 'CAUTION: Without deterministic.js, many pages will not replay.') | 459 'CAUTION: Without deterministic.js, many pages will not replay.') |
| 462 harness_group.add_option('-D', '--no-diff_unknown_requests', default=True, | 460 harness_group.add_argument('-D', '--no-diff_unknown_requests', default=True, |
| 463 action='store_false', | 461 action='store_false', |
| 464 dest='diff_unknown_requests', | 462 dest='diff_unknown_requests', |
| 465 help='During replay, do not show a diff of unknown requests against ' | 463 help='During replay, do not show a diff of unknown requests against ' |
| 466 'their nearest match in the archive.') | 464 'their nearest match in the archive.') |
| 467 harness_group.add_option('-C', '--use_closest_match', default=False, | 465 harness_group.add_argument('-C', '--use_closest_match', default=False, |
| 468 action='store_true', | 466 action='store_true', |
| 469 dest='use_closest_match', | 467 dest='use_closest_match', |
| 470 help='During replay, if a request is not found, serve the closest match' | 468 help='During replay, if a request is not found, serve the closest match' |
| 471 'in the archive instead of giving a 404.') | 469 'in the archive instead of giving a 404.') |
| 472 harness_group.add_option('-U', '--use_server_delay', default=False, | 470 harness_group.add_argument('-U', '--use_server_delay', default=False, |
| 473 action='store_true', | 471 action='store_true', |
| 474 dest='use_server_delay', | 472 dest='use_server_delay', |
| 475 help='During replay, simulate server delay by delaying response time to' | 473 help='During replay, simulate server delay by delaying response time to' |
| 476 'requests.') | 474 'requests.') |
| 477 harness_group.add_option('-I', '--screenshot_dir', default=None, | 475 harness_group.add_argument('-I', '--screenshot_dir', default=None, |
| 478 action='store', | 476 action='store', |
| 479 type='string', | 477 type=str, |
| 480 help='Save PNG images of the loaded page in the given directory.') | 478 help='Save PNG images of the loaded page in the given directory.') |
| 481 harness_group.add_option('-P', '--no-dns_private_passthrough', default=True, | 479 harness_group.add_argument('-P', '--no-dns_private_passthrough', default=True, |
| 482 action='store_false', | 480 action='store_false', |
| 483 dest='dns_private_passthrough', | 481 dest='dns_private_passthrough', |
| 484 help='Don\'t forward DNS requests that resolve to private network ' | 482 help='Don\'t forward DNS requests that resolve to private network ' |
| 485 'addresses. CAUTION: With this option important services like ' | 483 'addresses. CAUTION: With this option important services like ' |
| 486 'Kerberos will resolve to the HTTP proxy address.') | 484 'Kerberos will resolve to the HTTP proxy address.') |
| 487 harness_group.add_option('-x', '--no-dns_forwarding', default=True, | 485 harness_group.add_argument('-x', '--no-dns_forwarding', default=True, |
| 488 action='store_false', | 486 action='store_false', |
| 489 dest='dns_forwarding', | 487 dest='dns_forwarding', |
| 490 help='Don\'t forward DNS requests to the local replay server. ' | 488 help='Don\'t forward DNS requests to the local replay server. ' |
| 491 'CAUTION: With this option an external mechanism must be used to ' | 489 'CAUTION: With this option an external mechanism must be used to ' |
| 492 'forward traffic to the replay server.') | 490 'forward traffic to the replay server.') |
| 493 harness_group.add_option('--host', default=None, | 491 harness_group.add_argument('--host', default=None, |
| 494 action='store', | 492 action='store', |
| 495 type='str', | 493 type=str, |
| 496 help='The IP address to bind all servers to. Defaults to 0.0.0.0 or ' | 494 help='The IP address to bind all servers to. Defaults to 0.0.0.0 or ' |
| 497 '127.0.0.1, depending on --server_mode and platform.') | 495 '127.0.0.1, depending on --server_mode and platform.') |
| 498 harness_group.add_option('-o', '--port', default=80, | 496 harness_group.add_argument('-o', '--port', default=80, |
| 499 action='store', | 497 action='store', |
| 500 type='int', | 498 type=int, |
| 501 help='Port number to listen on.') | 499 help='Port number to listen on.') |
| 502 harness_group.add_option('--ssl_port', default=443, | 500 harness_group.add_argument('--ssl_port', default=443, |
| 503 action='store', | 501 action='store', |
| 504 type='int', | 502 type=int, |
| 505 help='SSL port number to listen on.') | 503 help='SSL port number to listen on.') |
| 506 harness_group.add_option('--http_to_https_port', default=None, | 504 harness_group.add_argument('--http_to_https_port', default=None, |
| 507 action='store', | 505 action='store', |
| 508 type='int', | 506 type=int, |
| 509 help='Port on which WPR will listen for HTTP requests that it will send ' | 507 help='Port on which WPR will listen for HTTP requests that it will send ' |
| 510 'along as HTTPS requests.') | 508 'along as HTTPS requests.') |
| 511 harness_group.add_option('--dns_port', default=53, | 509 harness_group.add_argument('--dns_port', default=53, |
| 512 action='store', | 510 action='store', |
| 513 type='int', | 511 type=int, |
| 514 help='DNS port number to listen on.') | 512 help='DNS port number to listen on.') |
| 515 harness_group.add_option('-c', '--https_root_ca_cert_path', default=None, | 513 harness_group.add_argument('-c', '--https_root_ca_cert_path', default=None, |
| 516 action='store', | 514 action='store', |
| 517 type='string', | 515 type=str, |
| 518 help='Certificate file to use with SSL (gets auto-generated if needed).') | 516 help='Certificate file to use with SSL (gets auto-generated if needed).') |
| 519 harness_group.add_option('--no-ssl', default=True, | 517 harness_group.add_argument('--no-ssl', default=True, |
| 520 action='store_false', | 518 action='store_false', |
| 521 dest='ssl', | 519 dest='ssl', |
| 522 help='Do not setup an SSL proxy.') | 520 help='Do not setup an SSL proxy.') |
| 523 option_parser.add_option_group(harness_group) | 521 harness_group.add_argument('--should_generate_certs', default=False, |
| 524 harness_group.add_option('--should_generate_certs', default=False, | |
| 525 action='store_true', | 522 action='store_true', |
| 526 help='Use OpenSSL to generate certificate files for requested hosts.') | 523 help='Use OpenSSL to generate certificate files for requested hosts.') |
| 527 harness_group.add_option('--no-admin-check', default=True, | 524 harness_group.add_argument('--no-admin-check', default=True, |
| 528 action='store_false', | 525 action='store_false', |
| 529 dest='admin_check', | 526 dest='admin_check', |
| 530 help='Do not check if administrator access is needed.') | 527 help='Do not check if administrator access is needed.') |
| 531 harness_group.add_option('--scramble_images', default=False, | 528 harness_group.add_argument('--scramble_images', default=False, |
| 532 action='store_true', | 529 action='store_true', |
| 533 dest='scramble_images', | 530 dest='scramble_images', |
| 534 help='Scramble image responses.') | 531 help='Scramble image responses.') |
| 535 harness_group.add_option('--rules_path', default=None, | 532 harness_group.add_argument('--rules_path', default=None, |
| 536 action='store', | 533 action='store', |
| 537 help='Path of file containing Python rules.') | 534 help='Path of file containing Python rules.') |
| 538 harness_group.add_option('--allowed_rule_imports', default='rules', | 535 harness_group.add_argument('--allowed_rule_imports', default='rules', |
| 539 action='store', | 536 action='store', |
| 540 help='A comma-separate list of allowed rule imports, or \'*\' to allow' | 537 help='A comma-separate list of allowed rule imports, or \'*\' to allow' |
| 541 ' all packages. Defaults to \'%default\'.') | 538 ' all packages. Defaults to %(default)s.') |
| 542 return option_parser | 539 return arg_parser |
| 543 | 540 |
| 544 | 541 |
| 545 def main(): | 542 def main(): |
| 546 option_parser = GetOptionParser() | 543 arg_parser = GetParser() |
| 547 options, args = option_parser.parse_args() | 544 options = arg_parser.parse_args() |
| 548 options = OptionsWrapper(options, option_parser) | 545 options = OptionsWrapper(options, arg_parser) |
| 549 | 546 |
| 550 if options.server: | 547 if options.server: |
| 551 replay_filename = None | 548 options.replay_filename = None |
| 552 elif len(args) != 1: | 549 elif options.replay_filename is None: |
| 553 option_parser.error('Must specify a replay_file') | 550 arg_parser.error('Must specify a replay_file') |
| 554 else: | 551 return replay(options, options.replay_filename) |
| 555 replay_filename = args[0] | |
| 556 | |
| 557 return replay(options, replay_filename) | |
| 558 | 552 |
| 559 | 553 |
| 560 if __name__ == '__main__': | 554 if __name__ == '__main__': |
| 561 sys.exit(main()) | 555 sys.exit(main()) |
| OLD | NEW |