├── .gitignore ├── .travis.yml ├── Gemfile ├── LICENSE ├── README.md ├── Rakefile ├── browsermob-proxy.gemspec ├── lib ├── browsermob-proxy.rb └── browsermob │ ├── proxy.rb │ └── proxy │ ├── client.rb │ ├── server.rb │ ├── version.rb │ └── webdriver_listener.rb └── spec ├── e2e ├── listener_spec.rb └── selenium_spec.rb ├── fixtures ├── 1.html ├── 2.html ├── 3.html ├── empty.gif └── google.har ├── spec_helper.rb └── unit ├── client_spec.rb └── webdriver_listener_spec.rb /.gitignore: -------------------------------------------------------------------------------- 1 | *.gem 2 | .bundle 3 | Gemfile.lock 4 | pkg/* 5 | .ruby-version 6 | .ruby-gemset 7 | .yardoc -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | rvm: 2 | - 2.0.0 3 | - 2.1.0 4 | before_script: 5 | - sudo apt-get install -y unzip 6 | - curl -k -L -O https://github.com/lightbody/browsermob-proxy/releases/download/browsermob-proxy-2.1.4/browsermob-proxy-2.1.4-bin.zip 7 | - unzip browsermob-proxy-2.1.4-bin.zip 8 | - export BROWSERMOB_PROXY_HOME=`pwd`/browsermob-proxy-2.1.4 9 | - sudo chmod +x $BROWSERMOB_PROXY_HOME/bin/browsermob-proxy 10 | - wget https://github.com/mozilla/geckodriver/releases/download/v0.19.1/geckodriver-v0.19.1-linux64.tar.gz 11 | - mkdir geckodriver 12 | - tar -xzf geckodriver-v0.19.1-linux64.tar.gz -C geckodriver 13 | - export PATH=$PATH:$PWD/geckodriver 14 | services: 15 | - xvfb 16 | -------------------------------------------------------------------------------- /Gemfile: -------------------------------------------------------------------------------- 1 | source "http://rubygems.org" 2 | 3 | gemspec 4 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Apache License 2 | Version 2.0, January 2004 3 | http://www.apache.org/licenses/ 4 | 5 | TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION 6 | 7 | 1. Definitions. 8 | 9 | "License" shall mean the terms and conditions for use, reproduction, 10 | and distribution as defined by Sections 1 through 9 of this document. 11 | 12 | "Licensor" shall mean the copyright owner or entity authorized by 13 | the copyright owner that is granting the License. 14 | 15 | "Legal Entity" shall mean the union of the acting entity and all 16 | other entities that control, are controlled by, or are under common 17 | control with that entity. For the purposes of this definition, 18 | "control" means (i) the power, direct or indirect, to cause the 19 | direction or management of such entity, whether by contract or 20 | otherwise, or (ii) ownership of fifty percent (50%) or more of the 21 | outstanding shares, or (iii) beneficial ownership of such entity. 22 | 23 | "You" (or "Your") shall mean an individual or Legal Entity 24 | exercising permissions granted by this License. 25 | 26 | "Source" form shall mean the preferred form for making modifications, 27 | including but not limited to software source code, documentation 28 | source, and configuration files. 29 | 30 | "Object" form shall mean any form resulting from mechanical 31 | transformation or translation of a Source form, including but 32 | not limited to compiled object code, generated documentation, 33 | and conversions to other media types. 34 | 35 | "Work" shall mean the work of authorship, whether in Source or 36 | Object form, made available under the License, as indicated by a 37 | copyright notice that is included in or attached to the work 38 | (an example is provided in the Appendix below). 39 | 40 | "Derivative Works" shall mean any work, whether in Source or Object 41 | form, that is based on (or derived from) the Work and for which the 42 | editorial revisions, annotations, elaborations, or other modifications 43 | represent, as a whole, an original work of authorship. For the purposes 44 | of this License, Derivative Works shall not include works that remain 45 | separable from, or merely link (or bind by name) to the interfaces of, 46 | the Work and Derivative Works thereof. 47 | 48 | "Contribution" shall mean any work of authorship, including 49 | the original version of the Work and any modifications or additions 50 | to that Work or Derivative Works thereof, that is intentionally 51 | submitted to Licensor for inclusion in the Work by the copyright owner 52 | or by an individual or Legal Entity authorized to submit on behalf of 53 | the copyright owner. For the purposes of this definition, "submitted" 54 | means any form of electronic, verbal, or written communication sent 55 | to the Licensor or its representatives, including but not limited to 56 | communication on electronic mailing lists, source code control systems, 57 | and issue tracking systems that are managed by, or on behalf of, the 58 | Licensor for the purpose of discussing and improving the Work, but 59 | excluding communication that is conspicuously marked or otherwise 60 | designated in writing by the copyright owner as "Not a Contribution." 61 | 62 | "Contributor" shall mean Licensor and any individual or Legal Entity 63 | on behalf of whom a Contribution has been received by Licensor and 64 | subsequently incorporated within the Work. 65 | 66 | 2. Grant of Copyright License. Subject to the terms and conditions of 67 | this License, each Contributor hereby grants to You a perpetual, 68 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 69 | copyright license to reproduce, prepare Derivative Works of, 70 | publicly display, publicly perform, sublicense, and distribute the 71 | Work and such Derivative Works in Source or Object form. 72 | 73 | 3. Grant of Patent License. Subject to the terms and conditions of 74 | this License, each Contributor hereby grants to You a perpetual, 75 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 76 | (except as stated in this section) patent license to make, have made, 77 | use, offer to sell, sell, import, and otherwise transfer the Work, 78 | where such license applies only to those patent claims licensable 79 | by such Contributor that are necessarily infringed by their 80 | Contribution(s) alone or by combination of their Contribution(s) 81 | with the Work to which such Contribution(s) was submitted. If You 82 | institute patent litigation against any entity (including a 83 | cross-claim or counterclaim in a lawsuit) alleging that the Work 84 | or a Contribution incorporated within the Work constitutes direct 85 | or contributory patent infringement, then any patent licenses 86 | granted to You under this License for that Work shall terminate 87 | as of the date such litigation is filed. 88 | 89 | 4. Redistribution. You may reproduce and distribute copies of the 90 | Work or Derivative Works thereof in any medium, with or without 91 | modifications, and in Source or Object form, provided that You 92 | meet the following conditions: 93 | 94 | (a) You must give any other recipients of the Work or 95 | Derivative Works a copy of this License; and 96 | 97 | (b) You must cause any modified files to carry prominent notices 98 | stating that You changed the files; and 99 | 100 | (c) You must retain, in the Source form of any Derivative Works 101 | that You distribute, all copyright, patent, trademark, and 102 | attribution notices from the Source form of the Work, 103 | excluding those notices that do not pertain to any part of 104 | the Derivative Works; and 105 | 106 | (d) If the Work includes a "NOTICE" text file as part of its 107 | distribution, then any Derivative Works that You distribute must 108 | include a readable copy of the attribution notices contained 109 | within such NOTICE file, excluding those notices that do not 110 | pertain to any part of the Derivative Works, in at least one 111 | of the following places: within a NOTICE text file distributed 112 | as part of the Derivative Works; within the Source form or 113 | documentation, if provided along with the Derivative Works; or, 114 | within a display generated by the Derivative Works, if and 115 | wherever such third-party notices normally appear. The contents 116 | of the NOTICE file are for informational purposes only and 117 | do not modify the License. You may add Your own attribution 118 | notices within Derivative Works that You distribute, alongside 119 | or as an addendum to the NOTICE text from the Work, provided 120 | that such additional attribution notices cannot be construed 121 | as modifying the License. 122 | 123 | You may add Your own copyright statement to Your modifications and 124 | may provide additional or different license terms and conditions 125 | for use, reproduction, or distribution of Your modifications, or 126 | for any such Derivative Works as a whole, provided Your use, 127 | reproduction, and distribution of the Work otherwise complies with 128 | the conditions stated in this License. 129 | 130 | 5. Submission of Contributions. Unless You explicitly state otherwise, 131 | any Contribution intentionally submitted for inclusion in the Work 132 | by You to the Licensor shall be under the terms and conditions of 133 | this License, without any additional terms or conditions. 134 | Notwithstanding the above, nothing herein shall supersede or modify 135 | the terms of any separate license agreement you may have executed 136 | with Licensor regarding such Contributions. 137 | 138 | 6. Trademarks. This License does not grant permission to use the trade 139 | names, trademarks, service marks, or product names of the Licensor, 140 | except as required for reasonable and customary use in describing the 141 | origin of the Work and reproducing the content of the NOTICE file. 142 | 143 | 7. Disclaimer of Warranty. Unless required by applicable law or 144 | agreed to in writing, Licensor provides the Work (and each 145 | Contributor provides its Contributions) on an "AS IS" BASIS, 146 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or 147 | implied, including, without limitation, any warranties or conditions 148 | of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A 149 | PARTICULAR PURPOSE. You are solely responsible for determining the 150 | appropriateness of using or redistributing the Work and assume any 151 | risks associated with Your exercise of permissions under this License. 152 | 153 | 8. Limitation of Liability. In no event and under no legal theory, 154 | whether in tort (including negligence), contract, or otherwise, 155 | unless required by applicable law (such as deliberate and grossly 156 | negligent acts) or agreed to in writing, shall any Contributor be 157 | liable to You for damages, including any direct, indirect, special, 158 | incidental, or consequential damages of any character arising as a 159 | result of this License or out of the use or inability to use the 160 | Work (including but not limited to damages for loss of goodwill, 161 | work stoppage, computer failure or malfunction, or any and all 162 | other commercial damages or losses), even if such Contributor 163 | has been advised of the possibility of such damages. 164 | 165 | 9. Accepting Warranty or Additional Liability. While redistributing 166 | the Work or Derivative Works thereof, You may choose to offer, 167 | and charge a fee for, acceptance of support, warranty, indemnity, 168 | or other liability obligations and/or rights consistent with this 169 | License. However, in accepting such obligations, You may act only 170 | on Your own behalf and on Your sole responsibility, not on behalf 171 | of any other Contributor, and only if You agree to indemnify, 172 | defend, and hold each Contributor harmless for any liability 173 | incurred by, or claims asserted against, such Contributor by reason 174 | of your accepting any such warranty or additional liability. 175 | 176 | END OF TERMS AND CONDITIONS 177 | 178 | APPENDIX: How to apply the Apache License to your work. 179 | 180 | To apply the Apache License to your work, attach the following 181 | boilerplate notice, with the fields enclosed by brackets "[]" 182 | replaced with your own identifying information. (Don't include 183 | the brackets!) The text should be enclosed in the appropriate 184 | comment syntax for the file format. We also recommend that a 185 | file or class name and description of purpose be included on the 186 | same "printed page" as the copyright notice for easier 187 | identification within third-party archives. 188 | 189 | Copyright [yyyy] [name of copyright owner] 190 | 191 | Licensed under the Apache License, Version 2.0 (the "License"); 192 | you may not use this file except in compliance with the License. 193 | You may obtain a copy of the License at 194 | 195 | http://www.apache.org/licenses/LICENSE-2.0 196 | 197 | Unless required by applicable law or agreed to in writing, software 198 | distributed under the License is distributed on an "AS IS" BASIS, 199 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 200 | See the License for the specific language governing permissions and 201 | limitations under the License. 202 | 203 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | browsermob-proxy-rb 2 | =================== 3 | 4 | Ruby client for the BrowserMob Proxy 2.0 REST API. 5 | 6 | NOTE: The BrowserMob Proxy is no longer actively maintained and has had no releases since 2016. 7 | 8 | A [BrowserMob Proxy alternative](http://browserup.com/blog/announcement-an-actively-maintained-fork-of-the-browsermob-proxy/) is now 9 | available in the [BrowserUp Proxy](https://github.com/browserup/browserup-proxy). The BrowserUp Proxy is an actively maintained fork of the BrowserMob proxy. It has added HTTP/2, Java 11, Brotli compression 10 | support, and more. It is an API compatible drop-in replacement, so using it is just a matter of using the BrowserUp proxy binary instead. 11 | 12 | The only compatibility exception is the deprecated legacy API. 13 | 14 | This release of browsermob-proxy-rb should work with the binary for either the BrowserUp Proxy, or the BrowserMob Proxy. 15 | 16 | How to use with selenium-webdriver 17 | ---------------------------------- 18 | 19 | Manually: 20 | 21 | ``` ruby 22 | require 'selenium/webdriver' 23 | require 'browsermob/proxy' 24 | 25 | server = BrowserMob::Proxy::Server.new("/path/to/downloads/browsermob-proxy/bin/browsermob-proxy") #=> # 26 | server.start 27 | 28 | proxy = server.create_proxy #=> # 29 | 30 | profile = Selenium::WebDriver::Firefox::Profile.new #=> # 31 | profile.proxy = proxy.selenium_proxy 32 | 33 | driver = Selenium::WebDriver.for :firefox, :profile => profile 34 | 35 | proxy.new_har "google" 36 | driver.get "http://google.com" 37 | 38 | har = proxy.har #=> # 39 | har.entries.first.request.url #=> "http://google.com" 40 | har.save_to "/tmp/google.har" 41 | 42 | proxy.close 43 | driver.quit 44 | ``` 45 | 46 | With event listener: 47 | 48 | ```ruby 49 | require 'selenium/webdriver' 50 | require 'browsermob/proxy' 51 | require 'browsermob/proxy/webdriver_listener' 52 | 53 | # start server, set up proxy like above 54 | proxy_listener = BrowserMob::Proxy::WebDriverListener.new(proxy) 55 | 56 | driver = Selenium::WebDriver.for :firefox, :profile => profile, :listener => proxy_listener 57 | # use driver 58 | driver.quit 59 | 60 | proxy_listener.hars #=> [#, #] 61 | proxy.close 62 | 63 | ``` 64 | 65 | Viewing HARs 66 | ------------ 67 | 68 | The HAR gem includes a HAR viewer. After running the code above, you can view the saved HAR by running 69 | 70 | $ har /tmp/google.har 71 | 72 | See also 73 | -------- 74 | * https://github.com/browserup/browserup-proxy 75 | 76 | * http://bmp.lightbody.net/ 77 | * https://github.com/lightbody/browsermob-proxy 78 | 79 | Note on Patches/Pull Requests 80 | ----------------------------- 81 | 82 | * Fork the project. 83 | * Make your feature addition or bug fix. 84 | * Add tests for it. This is important so I don't break it in a 85 | future version unintentionally. 86 | * Commit, do not mess with rakefile, version, or history. 87 | (if you want to have your own version, that is fine but bump version in a commit by itself I can ignore when I pull) 88 | * Send me a pull request. Bonus points for topic branches. 89 | 90 | Copyright 91 | --------- 92 | 93 | Copyright 2011-2019 Jari Bakken 94 | 95 | Copyright 2019 Eric Beland 96 | 97 | Licensed under the Apache License, Version 2.0 (the "License"); 98 | you may not use this file except in compliance with the License. 99 | You may obtain a copy of the License at 100 | 101 | http://www.apache.org/licenses/LICENSE-2.0 102 | 103 | Unless required by applicable law or agreed to in writing, software 104 | distributed under the License is distributed on an "AS IS" BASIS, 105 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 106 | See the License for the specific language governing permissions and 107 | limitations under the License. 108 | 109 | -------------------------------------------------------------------------------- /Rakefile: -------------------------------------------------------------------------------- 1 | require 'bundler' 2 | Bundler::GemHelper.install_tasks 3 | 4 | require 'rspec/core/rake_task' 5 | 6 | namespace :spec do 7 | desc 'Run unit specs.' 8 | RSpec::Core::RakeTask.new(:unit) do |t| 9 | t.pattern = "spec/unit/*_spec.rb" 10 | end 11 | 12 | desc 'Run end to end specs.' 13 | RSpec::Core::RakeTask.new(:e2e) do |t| 14 | t.pattern = "spec/e2e/*_spec.rb" 15 | end 16 | end 17 | 18 | desc 'Run all specs.' 19 | task :spec => %w[spec:unit spec:e2e] 20 | 21 | task :default => :spec 22 | -------------------------------------------------------------------------------- /browsermob-proxy.gemspec: -------------------------------------------------------------------------------- 1 | # -*- encoding: utf-8 -*- 2 | $:.push File.expand_path("../lib", __FILE__) 3 | require "browsermob/proxy/version" 4 | 5 | Gem::Specification.new do |s| 6 | s.name = "browsermob-proxy" 7 | s.version = BrowserMob::Proxy::VERSION 8 | s.platform = Gem::Platform::RUBY 9 | s.authors = ["Eric Beland"] 10 | s.email = ["ebeland@browserup.com"] 11 | s.homepage = "http://github.com/browserup/browsermob-proxy-rb" 12 | s.summary = %q{Ruby client for the BrowserMob and BrowserUp Proxy REST API} 13 | s.description = %q{Ruby client for the BrowserMob and BrowserUp Proxy REST API} 14 | s.license = 'Apache-2.0' 15 | 16 | s.rubyforge_project = "browsermob-proxy-rb" 17 | 18 | s.add_runtime_dependency "rest-client" 19 | s.add_runtime_dependency "childprocess", "~> 0.5" 20 | s.add_runtime_dependency "multi_json", "~> 1.0" 21 | s.add_runtime_dependency "har" 22 | 23 | s.add_development_dependency "rspec", "~> 2.0" 24 | s.add_development_dependency "selenium-webdriver", "~> 3.7" 25 | s.add_development_dependency "rake", "~> 0.9.2" 26 | s.add_development_dependency "rack", "~> 1.5" 27 | s.add_development_dependency "puma" 28 | 29 | s.files = `git ls-files`.split("\n") 30 | s.test_files = `git ls-files -- {test,spec,features}/*`.split("\n") 31 | s.executables = `git ls-files -- bin/*`.split("\n").map{ |f| File.basename(f) } 32 | s.require_paths = ["lib"] 33 | end 34 | -------------------------------------------------------------------------------- /lib/browsermob-proxy.rb: -------------------------------------------------------------------------------- 1 | require 'browsermob/proxy' -------------------------------------------------------------------------------- /lib/browsermob/proxy.rb: -------------------------------------------------------------------------------- 1 | require 'restclient' 2 | require 'multi_json' 3 | require 'har' 4 | 5 | require 'browsermob/proxy/client' 6 | require 'browsermob/proxy/server' 7 | 8 | module BrowserMob 9 | module Proxy 10 | 11 | end # Proxy 12 | end # BrowserMob 13 | 14 | -------------------------------------------------------------------------------- /lib/browsermob/proxy/client.rb: -------------------------------------------------------------------------------- 1 | module BrowserMob 2 | module Proxy 3 | 4 | class Client 5 | attr_reader :host, :port 6 | 7 | def self.from(server_url, port = nil) 8 | # ActiveSupport may define Object#load, so we can't use MultiJson.respond_to? here. 9 | sm = MultiJson.singleton_methods.map { |e| e.to_sym } 10 | decode_method = sm.include?(:load) ? :load : :decode 11 | 12 | new_proxy_url = URI.join(server_url, "proxy") 13 | new_proxy_url.query = "port=#{port}" if port 14 | 15 | port = MultiJson.send(decode_method, 16 | RestClient.post(new_proxy_url.to_s, '') 17 | ).fetch('port') 18 | 19 | uri = URI.parse(File.join(server_url, "proxy", port.to_s)) 20 | resource = RestClient::Resource.new(uri.to_s) 21 | 22 | Client.new resource, uri.host, port 23 | end 24 | 25 | def initialize(resource, host, port) 26 | @resource = resource 27 | @host = host 28 | @port = port 29 | end 30 | 31 | # 32 | # @example 33 | # client.new_har("page-name") 34 | # client.new_har("page-name", :capture_headers => true) 35 | # client.new_har(:capture_headers => true) 36 | # client.new_har(:capture_content => true) 37 | # client.new_har(:capture_binary_content => true) 38 | # 39 | 40 | def new_har(ref = nil, opts = {}) 41 | if opts.empty? && ref.kind_of?(Hash) 42 | opts = ref 43 | ref = nil 44 | end 45 | 46 | params = {} 47 | 48 | params[:initialPageRef] = ref if ref 49 | params[:captureHeaders] = true if opts[:capture_headers] 50 | params[:captureContent] = true if opts[:capture_content] 51 | 52 | if opts[:capture_binary_content] 53 | params[:captureContent] = true 54 | params[:captureBinaryContent] = true 55 | end 56 | 57 | previous = @resource["har"].put params 58 | HAR::Archive.from_string(previous) unless previous.empty? 59 | end 60 | 61 | def new_page(ref) 62 | @resource['har/pageRef'].put :pageRef => ref 63 | end 64 | 65 | def har 66 | HAR::Archive.from_string @resource["har"].get 67 | end 68 | 69 | def selenium_proxy(*protocols) 70 | require 'selenium-webdriver' unless defined?(Selenium) 71 | 72 | protocols += [:http] if protocols.empty? 73 | unless (protocols - [:http, :ssl, :ftp]).empty? 74 | raise "Invalid protocol specified. Must be one of: :http, :ssl, or :ftp." 75 | end 76 | 77 | proxy_mapping = {} 78 | protocols.each { |proto| proxy_mapping[proto] = "#{@host}:#{@port}" } 79 | Selenium::WebDriver::Proxy.new(proxy_mapping) 80 | end 81 | 82 | # 83 | # Set a list of URL regexes to whitelist 84 | # 85 | # Note that passed regexp/string should match string as a whole 86 | # (i.e. if /example\.com/ is whitelisted "http://www.example.com" won't be allowed 87 | # though if /.+example\.com" is whitelisted "http://www.example.com" will be allowed) 88 | # 89 | # @param regexp [Regexp, String, Array] a regexp, string or an array of regexps/strings that urls should match to 90 | # @param status_code [Integer] the HTTP status code to return for URLs that do not match the whitelist 91 | # 92 | 93 | def whitelist(regexp, status_code) 94 | regex = Array(regexp).map { |rx| Regexp === rx ? rx.source : rx.to_s }.join(',') 95 | @resource['whitelist'].put :regex => regex, :status => status_code 96 | end 97 | 98 | def clear_whitelist 99 | @resource['whitelist'].delete 100 | end 101 | 102 | def blacklist(regexp, status_code) 103 | regex = Regexp === regexp ? regexp.source : regexp.to_s 104 | @resource['blacklist'].put :regex => regex, :status => status_code 105 | end 106 | 107 | def clear_blacklist 108 | @resource['blacklist'].delete 109 | end 110 | 111 | def rewrite(match_regex, replace) 112 | regex = Regexp === match_regex ? match_regex.source : match_regex.to_s 113 | @resource['rewrite'].put :matchRegex => regex, :replace => replace 114 | end 115 | 116 | def clear_rewrites 117 | @resource['rewrite'].delete 118 | end 119 | 120 | def header(hash) 121 | @resource['headers'].post hash.to_json, :content_type => "application/json" 122 | end 123 | alias_method :headers, :header 124 | 125 | def basic_authentication(domain, username, password) 126 | data = { username: username, password: password } 127 | @resource["auth/basic/#{domain}"].post data.to_json, :content_type => "application/json" 128 | end 129 | 130 | TIMEOUTS = { 131 | request: :requestTimeout, 132 | read: :readTimeout, 133 | connection: :connectionTimeout, 134 | dns_cache: :dnsCacheTimeout 135 | } 136 | 137 | # 138 | # Specify timeouts that will be used by a proxy 139 | # (see README of browsermob-proxy itself for more info about what they mean) 140 | # 141 | # @param timeouts [Hash] options that specify desired timeouts (in seconds) 142 | # @option timeouts [Numeric] :request request timeout 143 | # @option timeouts [Numeric] :read read timeout 144 | # @option timeouts [Numeric] :connection connection timeout 145 | # @option timeouts [Numeric] :dns_cache dns cache timeout 146 | # 147 | 148 | def timeouts(timeouts = {}) 149 | params = {} 150 | 151 | timeouts.each do |key, value| 152 | unless TIMEOUTS.member?(key) 153 | raise ArgumentError, "invalid key: #{key.inspect}, should belong to: #{TIMEOUTS.keys.inspect}" 154 | end 155 | 156 | params[TIMEOUTS[key]] = (value * 1000).to_i 157 | end 158 | 159 | @resource['timeout'].put params 160 | end 161 | 162 | # 163 | # Override normal DNS lookups (remap the given hosts with the associated IP address). 164 | # 165 | # Each invocation of the method will add given hosts to existing BrowserMob's DNS cache 166 | # instead of overriding it. 167 | # 168 | # @example 169 | # remap_dns_hosts('example.com' => '1.2.3.4') 170 | # @param hash [Hash] a hash with domains as keys and IPs as values 171 | # 172 | 173 | def remap_dns_hosts(hash) 174 | @resource['hosts'].post hash.to_json, :content_type => 'application/json' 175 | end 176 | 177 | LIMITS = { 178 | :upstream_kbps => 'upstreamKbps', 179 | :downstream_kbps => 'downstreamKbps', 180 | :latency => 'latency' 181 | } 182 | 183 | def limit(opts) 184 | params = {} 185 | 186 | opts.each do |key, value| 187 | unless LIMITS.member?(key) 188 | raise ArgumentError, "invalid: #{key.inspect} (valid options: #{LIMITS.keys.inspect})" 189 | end 190 | 191 | params[LIMITS[key]] = Integer(value) 192 | end 193 | 194 | if params.empty? 195 | raise ArgumentError, "must specify one of #{LIMITS.keys.inspect}" 196 | end 197 | 198 | @resource['limit'].put params 199 | end 200 | 201 | def close 202 | @resource.delete 203 | end 204 | 205 | def request_interceptor=(data) 206 | @resource['interceptor/request'].post data, :content_type => "text/plain" 207 | end 208 | 209 | def response_interceptor=(data) 210 | @resource['interceptor/response'].post data, :content_type => "text/plain" 211 | end 212 | end # Client 213 | 214 | end # Proxy 215 | end # BrowserMob 216 | -------------------------------------------------------------------------------- /lib/browsermob/proxy/server.rb: -------------------------------------------------------------------------------- 1 | require 'childprocess' 2 | require 'socket' 3 | 4 | module BrowserMob 5 | module Proxy 6 | 7 | class Server 8 | attr_reader :port 9 | 10 | # 11 | # Create a new server instance 12 | # 13 | # @param [String] path Path to the BrowserMob Proxy server executable 14 | # @param [Hash] opts options to create the server with 15 | # @option opts [Integer] port What port to start the server on 16 | # @option opts [Boolean] log Show server output (server inherits stdout/stderr) 17 | # @option opts [Integer] timeout Seconds to wait for server to launch before timing out. 18 | # 19 | 20 | def initialize(path, opts = {}) 21 | assert_executable path 22 | 23 | @path = path 24 | @port = Integer(opts[:port] || 8080) 25 | @timeout = Integer(opts[:timeout] || 10) 26 | @log = !!opts[:log] 27 | 28 | @process = create_process 29 | end 30 | 31 | def start 32 | @process.start 33 | 34 | wait_for_startup 35 | 36 | pid = Process.pid 37 | at_exit { stop if Process.pid == pid } 38 | 39 | self 40 | end 41 | 42 | def url 43 | "http://localhost:#{port}" 44 | end 45 | 46 | def create_proxy(port = nil) 47 | Client.from url, port 48 | end 49 | 50 | def stop 51 | @process.stop if @process.alive? 52 | end 53 | 54 | private 55 | 56 | def create_process 57 | process = ChildProcess.new(@path, "--port", @port.to_s) 58 | process.leader = true 59 | 60 | process.io.inherit! if @log 61 | 62 | process 63 | end 64 | 65 | def wait_for_startup 66 | end_time = Time.now + @timeout 67 | 68 | sleep 0.1 until (listening? && initialized?) || Time.now > end_time || !@process.alive? 69 | 70 | if Time.now > end_time 71 | raise TimeoutError, "timed out waiting for the server to start (rerun with :log => true to see process output)" 72 | end 73 | 74 | unless @process.alive? 75 | raise ServerDiedError, "unable to launch the server (rerun with :log => true to see process output)" 76 | end 77 | end 78 | 79 | def listening? 80 | TCPSocket.new("127.0.0.1", port).close 81 | true 82 | rescue 83 | false 84 | end 85 | 86 | def initialized? 87 | RestClient.get("#{url}/proxy") 88 | true 89 | rescue RestClient::Exception 90 | false 91 | end 92 | 93 | def assert_executable(path) 94 | unless File.exist?(path) 95 | raise Errno::ENOENT, path 96 | end 97 | 98 | unless File.executable?(path) 99 | raise Errno::EACCES, "not executable: #{path}" 100 | end 101 | end 102 | 103 | class TimeoutError < StandardError 104 | end 105 | 106 | class ServerDiedError < StandardError 107 | end 108 | 109 | end # Server 110 | end # Proxy 111 | end # BrowserMob 112 | -------------------------------------------------------------------------------- /lib/browsermob/proxy/version.rb: -------------------------------------------------------------------------------- 1 | module BrowserMob 2 | module Proxy 3 | VERSION = "0.3.1" 4 | end 5 | end 6 | -------------------------------------------------------------------------------- /lib/browsermob/proxy/webdriver_listener.rb: -------------------------------------------------------------------------------- 1 | require 'selenium/webdriver/support' 2 | 3 | module BrowserMob 4 | module Proxy 5 | 6 | # 7 | # WebDriver event listener that assumes the following: 8 | # 9 | # driver.get - new HAR 10 | # driver.click - new page 11 | # driver.navigate.back - new page 12 | # driver.navigate.forward - new page 13 | # 14 | 15 | class WebDriverListener < Selenium::WebDriver::Support::AbstractEventListener 16 | attr_reader :hars 17 | 18 | def initialize(client, opts = {}) 19 | @client = client 20 | @hars = [] 21 | 22 | @new_har_opts = {} 23 | @new_har_opts[:capture_headers] = true if opts[:capture_headers] 24 | @new_har_opts[:capture_content] = true if opts[:capture_content] 25 | @new_har_opts[:capture_binary_content] = true if opts[:capture_binary_content] 26 | end 27 | 28 | def reset 29 | @hars.clear 30 | end 31 | 32 | def before_navigate_to(url, driver) 33 | save_har unless @hars.empty? # first request 34 | @client.new_har("navigate-to-#{url}", @new_har_opts) 35 | end 36 | 37 | def before_navigate_back(driver = nil) 38 | name = "navigate-back" 39 | name << "-from-#{driver.current_url}" if driver 40 | 41 | @client.new_page name 42 | end 43 | 44 | def before_navigate_forward(driver = nil) 45 | name = "navigate-forward" 46 | name << "-from-#{driver.current_url}" if driver 47 | 48 | @client.new_page name 49 | end 50 | 51 | def before_click(element, driver) 52 | name = "click-element-#{identifier_for element}" 53 | @client.new_page name 54 | end 55 | 56 | def before_quit(driver) 57 | save_har 58 | end 59 | 60 | private 61 | 62 | def save_har 63 | @hars << @client.har 64 | end 65 | 66 | def identifier_for(element) 67 | # can be ovverriden to provide more meaningful info 68 | element.ref 69 | end 70 | end 71 | end 72 | end 73 | -------------------------------------------------------------------------------- /spec/e2e/listener_spec.rb: -------------------------------------------------------------------------------- 1 | require 'spec_helper' 2 | 3 | describe "Proxy + WebDriverListener" do 4 | let(:proxy) { new_proxy } 5 | let(:listener) { BrowserMob::Proxy::WebDriverListener.new(proxy) } 6 | 7 | let(:driver) { Selenium::WebDriver.for :firefox, :profile => profile, :listener => listener } 8 | let(:wait) { Selenium::WebDriver::Wait.new(:timeout => 10) } 9 | 10 | let(:profile) { 11 | pr = Selenium::WebDriver::Firefox::Profile.new 12 | pr.proxy = proxy.selenium_proxy 13 | 14 | pr 15 | } 16 | 17 | after { proxy.close } 18 | 19 | it "should record events" do 20 | driver.get url_for("1.html") 21 | wait.until { driver.title == '1' } 22 | driver.find_element(:link_text => "2").click 23 | driver.quit 24 | 25 | hars = listener.hars 26 | hars.size.should == 1 27 | 28 | hars.first.pages.size.should == 2 29 | end 30 | end -------------------------------------------------------------------------------- /spec/e2e/selenium_spec.rb: -------------------------------------------------------------------------------- 1 | require 'spec_helper' 2 | 3 | describe "Proxy + WebDriver" do 4 | let(:driver) { Selenium::WebDriver.for :firefox, :profile => profile } 5 | let(:proxy) { new_proxy } 6 | let(:wait) { Selenium::WebDriver::Wait.new(:timeout => 10) } 7 | 8 | let(:profile) { 9 | pr = Selenium::WebDriver::Firefox::Profile.new 10 | pr.proxy = proxy.selenium_proxy 11 | 12 | pr 13 | } 14 | 15 | after { 16 | driver.quit 17 | proxy.close 18 | } 19 | 20 | describe 'request interceptor' do 21 | it "modifies request" do 22 | proxy.new_har("1", :capture_headers => true) 23 | proxy.request_interceptor = 'request.getMethod().setHeader("foo", "bar");' 24 | 25 | driver.get url_for("1.html") 26 | wait.until { driver.title == '1' } 27 | 28 | entry = proxy.har.entries.first 29 | header = entry.request.headers.find { |h| h['name'] == "foo" } 30 | header['value'].should == "bar" 31 | end 32 | end 33 | 34 | it "should fetch a HAR" do 35 | proxy.new_har("1") 36 | driver.get url_for("1.html") 37 | wait.until { driver.title == '1' } 38 | 39 | proxy.new_page "2" 40 | driver.get url_for("2.html") 41 | wait.until { driver.title == '2' } 42 | 43 | har = proxy.har 44 | 45 | har.should be_kind_of(HAR::Archive) 46 | har.pages.size.should == 2 47 | end 48 | 49 | it "should fetch a HAR and capture headers" do 50 | proxy.new_har("2", :capture_headers => true) 51 | 52 | driver.get url_for("2.html") 53 | wait.until { driver.title == '2' } 54 | 55 | entry = proxy.har.entries.first 56 | entry.should_not be_nil 57 | 58 | entry.request.headers.should_not be_empty 59 | end 60 | 61 | it "should fetch a HAR and capture content" do 62 | proxy.new_har("2", :capture_content => true) 63 | 64 | driver.get url_for("2.html") 65 | wait.until { driver.title == '2' } 66 | 67 | entry = proxy.har.entries.first 68 | entry.should_not be_nil 69 | 70 | entry.response.content.size.should be > 0 71 | entry.response.content.text.should == File.read("spec/fixtures/2.html") 72 | end 73 | 74 | it "should fetch a HAR and capture binary content as Base64 encoded string" do 75 | proxy.new_har("binary", :capture_binary_content => true) 76 | 77 | driver.get url_for("empty.gif") 78 | 79 | entry = proxy.har.entries.first 80 | entry.should_not be_nil 81 | 82 | entry.response.content.size.should be > 0 83 | require 'base64' 84 | expected_content = Base64.encode64(File.read("spec/fixtures/empty.gif")).strip 85 | entry.response.content.text.should == expected_content 86 | end 87 | 88 | describe 'whitelist' do 89 | it "allows access to urls in whitelist" do 90 | dest = url_for('1.html') 91 | 92 | proxy.whitelist(Regexp.quote(dest), 404) 93 | driver.get dest 94 | wait.until { driver.title == '1' } 95 | end 96 | 97 | it "disallows access to urls outside whitelist" do 98 | proxy.new_har('whitelist') 99 | proxy.whitelist('foo\.bar\.com', 404) 100 | driver.get url_for('2.html') 101 | proxy.har.entries.first.response.status.should == 404 102 | end 103 | 104 | it "can be cleared" do 105 | proxy.new_har('whitelist') 106 | proxy.whitelist('foo\.bar\.com', 404) 107 | 108 | proxy.clear_whitelist 109 | driver.get url_for('2.html') 110 | proxy.har.entries.first.response.status.should_not == 404 111 | end 112 | end 113 | 114 | describe 'blacklist' do 115 | it "disallows access to urls in blacklist" do 116 | proxy.new_har('blacklist') 117 | 118 | dest = url_for('1.html') 119 | proxy.blacklist(Regexp.quote(dest), 404) 120 | driver.get dest 121 | 122 | entry = proxy.har.entries.find { |e| e.request.url == dest } 123 | entry.should_not be_nil 124 | entry.response.status.should == 404 125 | end 126 | 127 | it "allows access to urls outside blacklist" do 128 | proxy.blacklist('foo\.bar\.com', 404) 129 | driver.get url_for('2.html') 130 | 131 | wait.until { driver.title == '2' } 132 | end 133 | 134 | it "can be cleared" do 135 | proxy.new_har('blacklist') 136 | 137 | dest = url_for('1.html') 138 | proxy.blacklist(Regexp.quote(dest), 404) 139 | 140 | proxy.clear_blacklist 141 | driver.get dest 142 | 143 | entry = proxy.har.entries.find { |e| e.request.url == dest } 144 | entry.should_not be_nil 145 | entry.response.status.should_not == 404 146 | end 147 | end 148 | 149 | describe 'rewrite rules' do 150 | 151 | let(:uri) { URI.parse url_for('1.html') } 152 | 153 | before do 154 | proxy.rewrite(%r{1\.html}, '2.html') 155 | end 156 | 157 | it 'fetches the rewritten url' do 158 | driver.get uri 159 | 160 | wait.until { driver.title == '2' } 161 | end 162 | 163 | it 'can be cleared' do 164 | proxy.clear_rewrites 165 | driver.get uri 166 | 167 | wait.until { driver.title == '1' } 168 | end 169 | 170 | end 171 | 172 | it 'should set timeouts' do 173 | proxy.timeouts(read: 0.001) 174 | driver.get url_for('slow') 175 | wait.until { driver.title == 'Problem loading page' } # This title appears in Firefox 176 | end 177 | 178 | it "should set headers" do 179 | proxy.headers('Content-Type' => "text/html") 180 | end 181 | 182 | it "should set limits" do 183 | proxy.limit(:downstream_kbps => 100, :upstream_kbps => 100, :latency => 2) 184 | end 185 | 186 | it 'should remap given DNS hosts' do 187 | uri = URI.parse url_for('1.html') 188 | host = 'plus.google.com' 189 | 190 | proxy.remap_dns_hosts(host => uri.host) 191 | uri.host = host 192 | 193 | driver.get uri 194 | wait.until { driver.title == '1' } 195 | end 196 | 197 | end 198 | -------------------------------------------------------------------------------- /spec/fixtures/1.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 1 6 | 7 | 8 | 2 9 | 10 | 11 | -------------------------------------------------------------------------------- /spec/fixtures/2.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 2 6 | 7 | 8 | 9 | 10 | 11 | -------------------------------------------------------------------------------- /spec/fixtures/3.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | Page Title 6 | 7 | 8 | 9 | 10 | 11 | -------------------------------------------------------------------------------- /spec/fixtures/empty.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/browserup/browsermob-proxy-rb/78a67bc38518db3c763992896959a8c1c2a5aa18/spec/fixtures/empty.gif -------------------------------------------------------------------------------- /spec/fixtures/google.har: -------------------------------------------------------------------------------- 1 | { 2 | "log":{ 3 | "version":"1.1", 4 | "creator":{ 5 | "name":"Firebug", 6 | "version":"1.5X.0b8" 7 | }, 8 | "browser":{ 9 | "name":"Firefox", 10 | "version":"3.6b6pre" 11 | }, 12 | "pages":[{ 13 | "startedDateTime":"2010-01-02T14:51:01.186+01:00", 14 | "id":"page_62143", 15 | "title":"Google", 16 | "pageTimings":{ 17 | "onContentLoad":90, 18 | "onLoad":245 19 | } 20 | } 21 | ], 22 | "entries":[{ 23 | "pageref":"page_62143", 24 | "startedDateTime":"2010-01-02T14:51:01.186+01:00", 25 | "time":63, 26 | "request":{ 27 | "method":"GET", 28 | "url":"http://www.google.cz/", 29 | "httpVersion":"HTTP/1.1", 30 | "cookies":[{ 31 | "name":"PREF", 32 | "value":"ID" 33 | }, 34 | { 35 | "name":"NID", 36 | "value":"29" 37 | } 38 | ], 39 | "headers":[{ 40 | "name":"Host", 41 | "value":"www.google.cz" 42 | }, 43 | { 44 | "name":"User-Agent", 45 | "value":"Mozilla/5.0 (Windows; U; Windows NT 6.0; en-US; rv:1.9.2b6pre) Gecko/20091230 Namoroka/3.6b6pre (.NET CLR 3.5.30729)" 46 | }, 47 | { 48 | "name":"Accept", 49 | "value":"text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8" 50 | }, 51 | { 52 | "name":"Accept-Language", 53 | "value":"en-us,en;q=0.5" 54 | }, 55 | { 56 | "name":"Accept-Encoding", 57 | "value":"gzip,deflate" 58 | }, 59 | { 60 | "name":"Accept-Charset", 61 | "value":"ISO-8859-1,utf-8;q=0.7,*;q=0.7" 62 | }, 63 | { 64 | "name":"Keep-Alive", 65 | "value":"115" 66 | }, 67 | { 68 | "name":"Connection", 69 | "value":"keep-alive" 70 | }, 71 | { 72 | "name":"Cookie", 73 | "value":"PREF=ID=580ec4c5a3534337:U=37a8fcc41ff49f78:TM=1260796678:LM=1260796682:S=9BgbomVM6pcnfah0; NID=29=OHyg2zMZl4IpG8C4a-Z5itM3gCXOuBPogGpTPVFPNsdpmIHJWX78ymRL_gqptvhr_IQrP319GQ1fxlKUsqaIokpxasPIIDq5ijatDmYiyamnCfJz8rXyNvt5GPjCJp2I" 74 | } 75 | ], 76 | "queryString":[], 77 | "headersSize":632, 78 | "bodySize":-1 79 | }, 80 | "response":{ 81 | "status":200, 82 | "statusText":"OK", 83 | "httpVersion":"HTTP/1.1", 84 | "cookies":[], 85 | "headers":[{ 86 | "name":"Date", 87 | "value":"Sat, 02 Jan 2010 13:51:06 GMT" 88 | }, 89 | { 90 | "name":"Expires", 91 | "value":"-1" 92 | }, 93 | { 94 | "name":"Cache-Control", 95 | "value":"private, max-age=0" 96 | }, 97 | { 98 | "name":"Content-Type", 99 | "value":"text/html; charset=UTF-8" 100 | }, 101 | { 102 | "name":"Content-Encoding", 103 | "value":"gzip" 104 | }, 105 | { 106 | "name":"Server", 107 | "value":"gws" 108 | }, 109 | { 110 | "name":"Content-Length", 111 | "value":"3694" 112 | }, 113 | { 114 | "name":"X-XSS-Protection", 115 | "value":"0" 116 | } 117 | ], 118 | "content":{ 119 | "size":8564, 120 | "mimeType":"text/html", 121 | "text":"Google


 
  Rozšířené vyhledávání
  Jazykové nástroje


Inzerujte s Googlem - Vše o Google - Google.com in English

©2010 - Osobní údaje

" 122 | }, 123 | "redirectURL":"", 124 | "headersSize":224, 125 | "bodySize":3694 126 | }, 127 | "cache":{}, 128 | "timings":{ 129 | "dns":0, 130 | "connect":0, 131 | "blocked":0, 132 | "send":0, 133 | "wait":63, 134 | "receive":0 135 | } 136 | }, 137 | { 138 | "pageref":"page_62143", 139 | "startedDateTime":"2010-01-02T14:51:01.280+01:00", 140 | "time":62, 141 | "request":{ 142 | "method":"GET", 143 | "url":"http://www.google.cz/intl/en_com/images/logo_plain.png", 144 | "httpVersion":"HTTP/1.1", 145 | "cookies":[{ 146 | "name":"PREF", 147 | "value":"ID" 148 | }, 149 | { 150 | "name":"NID", 151 | "value":"29" 152 | } 153 | ], 154 | "headers":[{ 155 | "name":"Host", 156 | "value":"www.google.cz" 157 | }, 158 | { 159 | "name":"User-Agent", 160 | "value":"Mozilla/5.0 (Windows; U; Windows NT 6.0; en-US; rv:1.9.2b6pre) Gecko/20091230 Namoroka/3.6b6pre (.NET CLR 3.5.30729)" 161 | }, 162 | { 163 | "name":"Accept", 164 | "value":"image/png,image/*;q=0.8,*/*;q=0.5" 165 | }, 166 | { 167 | "name":"Accept-Language", 168 | "value":"en-us,en;q=0.5" 169 | }, 170 | { 171 | "name":"Accept-Encoding", 172 | "value":"gzip,deflate" 173 | }, 174 | { 175 | "name":"Accept-Charset", 176 | "value":"ISO-8859-1,utf-8;q=0.7,*;q=0.7" 177 | }, 178 | { 179 | "name":"Keep-Alive", 180 | "value":"115" 181 | }, 182 | { 183 | "name":"Connection", 184 | "value":"keep-alive" 185 | }, 186 | { 187 | "name":"Referer", 188 | "value":"http://www.google.cz/" 189 | }, 190 | { 191 | "name":"Cookie", 192 | "value":"PREF=ID=580ec4c5a3534337:U=37a8fcc41ff49f78:TM=1260796678:LM=1260796682:S=9BgbomVM6pcnfah0; NID=29=OHyg2zMZl4IpG8C4a-Z5itM3gCXOuBPogGpTPVFPNsdpmIHJWX78ymRL_gqptvhr_IQrP319GQ1fxlKUsqaIokpxasPIIDq5ijatDmYiyamnCfJz8rXyNvt5GPjCJp2I" 193 | } 194 | ], 195 | "queryString":[], 196 | "headersSize":667, 197 | "bodySize":-1 198 | }, 199 | "response":{ 200 | "status":200, 201 | "statusText":"OK", 202 | "httpVersion":"HTTP/1.1", 203 | "cookies":[], 204 | "headers":[{ 205 | "name":"Content-Type", 206 | "value":"image/png" 207 | }, 208 | { 209 | "name":"Last-Modified", 210 | "value":"Mon, 17 Mar 2008 22:38:58 GMT" 211 | }, 212 | { 213 | "name":"Date", 214 | "value":"Sat, 02 Jan 2010 13:51:05 GMT" 215 | }, 216 | { 217 | "name":"Expires", 218 | "value":"Sun, 02 Jan 2011 13:51:05 GMT" 219 | }, 220 | { 221 | "name":"Server", 222 | "value":"gws" 223 | }, 224 | { 225 | "name":"Content-Length", 226 | "value":"7582" 227 | }, 228 | { 229 | "name":"Cache-Control", 230 | "value":"public, max-age=31536000" 231 | }, 232 | { 233 | "name":"Age", 234 | "value":"1" 235 | }, 236 | { 237 | "name":"X-XSS-Protection", 238 | "value":"0" 239 | } 240 | ], 241 | "content":{ 242 | "size":7582, 243 | "mimeType":"image/png" 244 | }, 245 | "redirectURL":"", 246 | "headersSize":272, 247 | "bodySize":7582 248 | }, 249 | "cache":{}, 250 | "timings":{ 251 | "dns":0, 252 | "connect":0, 253 | "blocked":0, 254 | "send":0, 255 | "wait":31, 256 | "receive":31 257 | } 258 | }, 259 | { 260 | "pageref":"page_62143", 261 | "startedDateTime":"2010-01-02T14:51:01.296+01:00", 262 | "time":78, 263 | "request":{ 264 | "method":"GET", 265 | "url":"http://www.google.cz/extern_js/f/CgJjcxICY3orMAo4QUAdLCswDjgKLCswFjgULCswFzgELCswGDgELCswGTgNLCswJTjJiAEsKzAmOAgsKzAnOAIsKzA8OAEsKzBFOAAs/n26cL0r9CnM.js", 266 | "httpVersion":"HTTP/1.1", 267 | "cookies":[{ 268 | "name":"PREF", 269 | "value":"ID" 270 | }, 271 | { 272 | "name":"NID", 273 | "value":"29" 274 | } 275 | ], 276 | "headers":[{ 277 | "name":"Host", 278 | "value":"www.google.cz" 279 | }, 280 | { 281 | "name":"User-Agent", 282 | "value":"Mozilla/5.0 (Windows; U; Windows NT 6.0; en-US; rv:1.9.2b6pre) Gecko/20091230 Namoroka/3.6b6pre (.NET CLR 3.5.30729)" 283 | }, 284 | { 285 | "name":"Accept", 286 | "value":"*/*" 287 | }, 288 | { 289 | "name":"Accept-Language", 290 | "value":"en-us,en;q=0.5" 291 | }, 292 | { 293 | "name":"Accept-Encoding", 294 | "value":"gzip,deflate" 295 | }, 296 | { 297 | "name":"Accept-Charset", 298 | "value":"ISO-8859-1,utf-8;q=0.7,*;q=0.7" 299 | }, 300 | { 301 | "name":"Keep-Alive", 302 | "value":"115" 303 | }, 304 | { 305 | "name":"Connection", 306 | "value":"keep-alive" 307 | }, 308 | { 309 | "name":"Referer", 310 | "value":"http://www.google.cz/" 311 | }, 312 | { 313 | "name":"Cookie", 314 | "value":"PREF=ID=580ec4c5a3534337:U=37a8fcc41ff49f78:TM=1260796678:LM=1260796682:S=9BgbomVM6pcnfah0; NID=29=OHyg2zMZl4IpG8C4a-Z5itM3gCXOuBPogGpTPVFPNsdpmIHJWX78ymRL_gqptvhr_IQrP319GQ1fxlKUsqaIokpxasPIIDq5ijatDmYiyamnCfJz8rXyNvt5GPjCJp2I" 315 | } 316 | ], 317 | "queryString":[], 318 | "headersSize":735, 319 | "bodySize":-1 320 | }, 321 | "response":{ 322 | "status":200, 323 | "statusText":"OK", 324 | "httpVersion":"HTTP/1.1", 325 | "cookies":[], 326 | "headers":[{ 327 | "name":"Content-Type", 328 | "value":"text/javascript; charset=UTF-8" 329 | }, 330 | { 331 | "name":"Expires", 332 | "value":"Sat, 01 Jan 2011 00:00:00 GMT" 333 | }, 334 | { 335 | "name":"Last-Modified", 336 | "value":"Sat, 03 Jan 2009 00:00:00 GMT" 337 | }, 338 | { 339 | "name":"Cache-Control", 340 | "value":"private, x-gzip-ok=\"\"" 341 | }, 342 | { 343 | "name":"Content-Encoding", 344 | "value":"gzip" 345 | }, 346 | { 347 | "name":"Date", 348 | "value":"Sat, 02 Jan 2010 13:51:06 GMT" 349 | }, 350 | { 351 | "name":"Server", 352 | "value":"gws" 353 | }, 354 | { 355 | "name":"Content-Length", 356 | "value":"8646" 357 | }, 358 | { 359 | "name":"X-XSS-Protection", 360 | "value":"0" 361 | } 362 | ], 363 | "content":{ 364 | "size":22832, 365 | "mimeType":"text/javascript", 366 | "text":"(function(){\u000aif(!google.nocsixjs&&google.timers&&google.timers.load.t)google.timers.load.t.xjses=(new Date).getTime();\u000a})();\u000a(function(){\u000agoogle.isOpera=false;google.isIE=false;google.isSafari=false;\u000agoogle.xhr=function(){var a=null;try{a=new XMLHttpRequest}catch(d){}return a};\u000agoogle.getComputedStyle=function(a,d,c){var b=c?\"\":0;var e=document.defaultView&&document.defaultView.getComputedStyle(a,\"\");b=e.getPropertyValue(d);b=c?b:parseInt(b,10);return b};google.getHeight=function(a){return google.getComputedStyle(a,\"height\")};google.getWidth=function(a){return google.getComputedStyle(a,\"width\")};google.getPageOffsetTop=function(a){return a.offsetTop+(a.offsetParent?google.getPageOffsetTop(a.offsetParent):0)};\u000agoogle.getPageOffsetLeft=function(a){return a.offsetLeft+(a.offsetParent?google.getPageOffsetLeft(a.offsetParent):0)};google.getPageOffsetStart=function(a){return google.getPageOffsetLeft(a)};google.getColor=function(a){return google.getComputedStyle(a,\"color\",true)};google.rhs=function(){};var f,h=location;\u000agoogle.nav=function(a,d){try{var c=location.protocol+\"//\"+location.host;if((new RegExp(\"^(\"+c+\")?/url\\\\?.*&rct=j(&|$)\")).test(a))if(d){google.r=1;d.location.replace(a)}else{if(!f){f=document.createElement(\"iframe\");f.style.display=\"none\";google.append(f)}google.r=1;f.src=a}else h.href=a}catch(b){h.href=a}};google.append=function(a){return(document.getElementById(\"xjsc\")||document.body).appendChild(a)};google.bind=function(a,d,c){a.addEventListener(d,c,false);};\u000a})();\u000a(function(){\u000avar c=window,f=Object,h=google,i=\"push\",j=\"length\",k=\"propertyIsEnumerable\",l=\"prototype\",m=\"call\";\u000afunction n(a){var b=typeof a;if(b==\"object\")if(a){if(a instanceof Array||!(a instanceof f)&&f[l].toString[m](a)==\"[object Array]\"||typeof a[j]==\"number\"&&typeof a.splice!=\"undefined\"&&typeof a[k]!=\"undefined\"&&!a[k](\"splice\"))return\"array\";if(!(a instanceof f)&&(f[l].toString[m](a)==\"[object Function]\"||typeof a[m]!=\"undefined\"&&typeof a[k]!=\"undefined\"&&!a[k](\"call\")))return\"function\"}else return\"null\";else if(b==\"function\"&&typeof a[m]==\"undefined\")return\"object\";return b}\u000afunction o(a){return(new p).serialize(a)}function p(){}p[l].serialize=function(a){var b=[];this.a(a,b);return b.join(\"\")};p[l].a=function(a,b){switch(typeof a){case \"string\":this.b(a,b);break;case \"number\":this.d(a,b);break;case \"boolean\":b[i](a);break;case \"undefined\":b[i](\"null\");break;case \"object\":if(a==null){b[i](\"null\");break}if(n(a)==\"array\"){this.c(a,b);break}this.e(a,b);break;case \"function\":break;default:throw Error(\"Unknown type: \"+typeof a);}};\u000avar q={'\"':'\\\\\"',\"\\\\\":\"\\\\\\\\\",\"/\":\"\\\\/\",\"\\u0008\":\"\\\\b\",\"\\u000c\":\"\\\\f\",\"\\n\":\"\\\\n\",\"\\r\":\"\\\\r\",\"\\t\":\"\\\\t\",\"\\u000b\":\"\\\\u000b\"},r=/\\uffff/.test(\"\\uffff\")?/[\\\\\\\"\\x00-\\x1f\\x7f-\\uffff]/g:/[\\\\\\\"\\x00-\\x1f\\x7f-\\xff]/g;p[l].b=function(a,b){b[i]('\"',a.replace(r,function(g){if(g in q)return q[g];var d=g.charCodeAt(0),e=\"\\\\u\";if(d<16)e+=\"000\";else if(d<256)e+=\"00\";else if(d<4096)e+=\"0\";return q[g]=e+d.toString(16)}),'\"')};p[l].d=function(a,b){b[i](isFinite(a)&&!isNaN(a)?a:\"null\")};\u000ap[l].c=function(a,b){var g=a[j];b[i](\"[\");for(var d=\"\",e=0;ed?Math.min(9999,c-d)+\"px\":(google.isIE?\"\":0)}};function e(){a=document.getElementById(\"tads\");b=document.getElementById(\"3po\");google.rhs()}e();google.bind(window,\"resize\",google.rhs);google.rein.push(e);\u000a})();\u000a(function(){\u000avar f=0,g=[];google.fx={};google.fx.linear=function(a){return a};google.fx.easeOut=function(a){return 1-Math.pow(1-a,3)};google.fx.easeInAndOut=function easeInAndOut(a){return(3-2*a)*a*a};google.fx.animate=function(a,d,e){for(var c=0,b;b=d[c++];){b[5]=b[5]==null?\"px\":b[5];b[4]=b[4]||google.fx.linear;h(b[0],b[1],b[2]+b[5])}g.push({b:a,a:e,d:google.time(),c:d});f=f||window.setInterval(i,15)};function i(){for(var a=0,d;d=g[a++];)j(d)||g.splice(--a,1);if(!g.length){window.clearInterval(f);f=0}}function j(a){var d=\u000agoogle.time()-a.d;if(d>=a.b){for(var e=0,c;c=a.c[e++];)h(c[0],c[1],c[3]+c[5]);a.a&&a.a();return 0}else{for(var e=0,c;c=a.c[e++];){var b=c[2]+(c[3]-c[2])*c[4](d/a.b);if(c[5]==\"px\")b=Math.round(b);h(c[0],c[1],b+c[5])}return 1}}function h(a){for(var d=1;db)a=0;else if(a<-1)a=b-1;q=a;p=o.item(a);p.className=\"gac_b\";X(p.completeString);w.value=p.completeId}\u000afunction P(){if(D){window.clearTimeout(D);D=e}v&&(v.visibility=\"hidden\");}function Y(){v&&(v.visibility=\"visible\");U();E=1}function pa(){return!!v&&v.visibility==\"visible\"}\u000afunction fa(){if(u){u.innerHTML=\"\";}}\u000afunction sa(a){B>0&&B--;if(!u||a[0]!=i)return;if(D){window.clearTimeout(D);D=e}m=a[0];fa();var d=c;for(var k=0,f;k0?Y:P)()}function ja(){P();if(z.value!=t.value)w.value=o&&o.item(q)&&o.item(q).completeId;else{z.value=\"\";if(B>=10)w.value=\"o\"}}\u000afunction W(){if(j!=i&&i){B++;F&&G.removeChild(F);F=document.createElement(\"script\");F.src=[\"http://\",ga,A,\"&q=\",encodeURIComponent(i),\"&cp=\"+I].join(\"\");G.appendChild(F);t.focus()}j=i;var a=100;for(var b=1;b<=(B-2)/2;++b)a*=\u000a2;a+=50;n=window.setTimeout(W,a)}function X(a){if(t)t.value=h=a}function na(a,b){var d=0;while(a){d+=a[b];a=a.offsetParent}return d}function $(a,b){a.appendChild(document.createTextNode(b))}\u000afunction Z(a){a.stopPropagation();return c}\u000afunction ma(a){var b=0,d=0;if(va(a)){b=a.selectionStart;d=a.selectionEnd}return b&&d&&b==d?b:0}\u000afunction va(a){try{return typeof a.selectionStart==\"number\"}catch(b){return c}}window.google.ac={i:ia,h:sa,u:X};google.bind(window,\"resize\",U);google.dstr.push(ea);\u000a})();\u000a(function(){\u000awindow.ManyBox={};var e,g,h=1,j=google.History.client(i);function k(a){for(var b in e)if(e[b].c&&a(e[b]))return}function l(a,b,c,d,f){this.c=a;this.i=b;this.C=d;this.o=f;this.q=\"/mbd?\"+(b?\"docid=\"+b:\"\")+\"&resnum=\"+a.replace(/[^0-9]/,\"\")+\"&mbtype=\"+d+\"&usg=\"+c+\"&hl=\"+(google.kHL||\"\");this.e={};this.l=\"\";g[a]={r:0,F:this.e,i:this.i,f:0};this.n=0}l.prototype.append=function(a){this.l+=\"&\"+a.join(\"&\")};function m(a,b){return document.getElementById(\"mb\"+b+a.c)}function n(a,b){a.h.style.paddingTop=b+\"px\";\u000aa.h.style.display=a.h.innerHTML?\"\":\"none\";if(b>a.n)a.n=b}function q(a){if(!a.B){a.B=1;a.d=m(a,\"b\");a.j=0;a.a=m(a,\"l\");a.m=a.a.getElementsByTagName(\"DIV\")[0];a.p=a.a.getElementsByTagName(\"A\")[0];a.z=a.p.innerHTML;a.o=a.o||a.z;a.m.title=\"Pro další informace klikněte zde...\";a.a.G=function(b,c){var d=google.getPageOffsetStart(a.a),f=google.getPageOffsetTop(a.a);return b>d-5&&bf-5&&cgoogle.getPageOffsetStart(b)+google.getWidth(b);a.b=document.createElement(\"div\");n(a,0);a.b.style.position=\"absolute\";\u000aa.b.style.paddingTop=(a.b.style.paddingBottom=\"6px\");a.b.style.display=\"none\";a.b.className=\"med\";var c=document.createElement(\"div\");a.b.appendChild(c);c.className=\"std\";c.innerHTML=a.e.k;a.h.parentNode.insertBefore(a.b,a.h)}}function i(a){h=0;ManyBox.init();k(function(b){if(b.i==a[b.c].i){b.e=a[b.c].F;if(a[b.c].r!=b.j)x(b)}else a[b.c].f=0});g=a;h=1;google.History.save(j,g)}ManyBox.create=function(a,\u000ab,c,d,f){return new l(a,b,c,d,f)};ManyBox.register=function(a,b,c,d,f){return e[a]=ManyBox.create(a,b,c,d,f)};google.bind(document,\"click\",function(a){a=a||window.event;var b=a.target||a.srcElement;while(b.parentNode){if(b.tagName==\"A\"||b.onclick)return;b=b.parentNode}k(function(c){if(c.a.G(a.clientX,a.clientY)){c.a.go();return 1}})});function z(){e={};g={};history.navigationMode=history.navigationMode&&\"fast\"}z();ManyBox.init=function(){k(q)};function A(a,b){a.b.style.clip=\"rect(0px,\"+(a.d.width||\u000a\"34em\")+\",\"+(b||1)+\"px,0px)\"}l.prototype.insert=function(a){this.e.k=a};function B(a){a.g=m(a,\"cb\");var b=a.g&&a.g.getAttribute(\"mbopen\");if(b){eval(b);a.onopen(a.g)}}function C(a){a.b.style.display=\"none\";a.m.style.backgroundPosition=\"-91px -74px\";a.p.innerHTML=a.z;a.j=(g[a.c].r=0);google.History.save(j,g)}function D(a,b,c,d,f){var u=c>0?150:75,v=google.time()-f,w=v1?c-10:c),o=Math.max(a.s,b+w),p=o-a.s;A(a,p);a.d.style.height=o<0?0:(p?o+\"px\":\"\");n(a,Math.max(0,p-5));google.rhs();if(Math.abs(w)<\u000aMath.abs(c))window.setTimeout(function(){D(a,b,c,d-1,f)},30);else window.setTimeout(function(){c<0?C(a):B(a);if(!google.isIE&&a.I)a.b.style.width=\"100px\";a.b.style.position=(a.d.style.height=\"\");n(a,0);google.rhs();a.d.w=0},0)}function x(a){a.u=0;if(!a.d.w){a.d.w=1;var b;if(!a.j){a.s=google.getHeight(a.d);y(a);n(a,0);a.n=0;k(function(d){d.m.title=\"\"});a.m.style.backgroundPosition=\"-105px -74px\";a.p.innerHTML=a.o;A(a,1);a.b.style.position=\"absolute\";a.b.style.display=\"\";a.j=(g[a.c].r=1);google.History.save(j,\u000ag);b=a.b.offsetHeight}else{var c=a.g&&a.g.getAttribute(\"mbclose\");if(c){eval(c);a.onclose(a.g)}b=a.s-google.getHeight(a.d);a.h.style.display=\"none\";n(a,a.n);a.b.style.position=\"absolute\"}D(a,google.getHeight(a.d),b,google.isSafari?2:1,google.time())}}google.dstr&&google.dstr.push(z);\u000a})();\u000a(function(){\u000avar h=false,i=true,k,m;\u000afunction o(){k=document.createElement(\"style\");document.getElementsByTagName(\"head\")[0].appendChild(k);m=k.sheet;}\u000agoogle.addCSSRule=function(a,b){k||o();var e=a+\"{\"+b+\"}\";m.insertRule(e,m.cssRules.length);};google.acrs=function(a){for(var b=a.split(/{|}/),c=1;c0,f=google.Toolbelt.isToolbeltOpen(),e=c||!!document.getElementById(\"tbt5\")||mbtb1.na;if(f&&!s){H(h,e);google.log(\"toolbelt\",\"0&ei=\"+google.kEI);r=i}else if(d){H(i,\u000ae);r&&google.log(\"toolbelt\",\"1&ei=\"+google.kEI)}else{mbtb1.insert=w;var g=google.xhr();if(g){g.open(\"GET\",[google.pageState?google.pageState.replace(\"#\",\"/mbd?\"):google.base_href.replace(/^\\/search\\?/,\"/mbd?\"),\"&mbtype=29&resnum=1&tbo=1\",mbtb1.tbs?\"&tbs=\"+mbtb1.tbs:\"\",\"&docid=\",mbtb1.docid,\"&usg=\",mbtb1.usg,\"&zx=\",google.time()].join(\"\"),i);g.onreadystatechange=function(){if(g.readyState==4&&g.status==200)try{eval(g.responseText)}catch(q){window.location.replace(a.href)}};g.send(null);s=i;H(i,e)}return h}google.History.save(y,\u000a{content:u,open:s||!f});return s=h};function I(a){for(;a&&a.className!=\"tbt\";)a=a.parentNode;return a}google.Toolbelt.ctlClk=function(a,b){a=a||\"cdr_opt\";b=b||\"cdr_min\";var c=document.getElementById(a);if(c){c.className=\"tbots\";var d=I(c);if(d){for(var f=0,e;e=d.childNodes[f++];)if(e.className==\"tbos\")e.className=\"tbotu\";var g=document.getElementById(b);g&&g.focus()}}return h};google.Toolbelt.cdrClk=google.Toolbelt.ctlClk;\u000afunction J(a){return a.replace(/_/g,\"_1\").replace(/,/g,\"_2\").replace(/:/g,\"_3\")}google.Toolbelt.cdrSbt=function(){return K(\"ctbs\",{cdr_min:\"cd_min\",cdr_max:\"cd_max\"})};google.Toolbelt.clSbt=function(){return K(\"ltbs\",{l_in:\"cl_loc\"})};function K(a,b){var c=document.getElementById(a);if(c)for(var d in b){var f=J(document.getElementById(d).value),e=new RegExp(\"(\"+b[d]+\":)([^,]*)\");c.value=c.value.replace(e,\"$1\"+f)}return i}\u000agoogle.Toolbelt.tbosClk=function(a){var b=a||window.event,c=b.target||b.srcElement;if(c&&c.className==\"tbotu\"){c.className=\"tbos\";var d=I(c);if(d)for(var f=0,e;e=d.childNodes[f++];)if(e.className==\"tbots\")e.className=\"tbou\"}};var L=google.fx.easeOut,M=[[\"tads\",\"margin-left\",\"marginLeft\"],[\"res\",\"margin-left\",\"marginLeft\"],[\"tbd\",\"margin-left\",\"marginLeft\"],[\"mbEnd\",\"width\",\"width\"],[\"tbt3\",\"left\",\"left\"],[\"tbt8\",\"left\",\"left\"],[\"tbt5\",\"margin-left\",\"marginLeft\"]],N=h;\u000afunction O(a){for(var b=0,c=0,d;d=M[c++];){var f=document.getElementById(d[0]);f&&a(f,d,b++)}}function P(){var a=[];O(function(b,c){var d=c[1];a.push(d==\"width\"?b.offsetWidth:google.getComputedStyle(b,d))});return a}\u000afunction H(a,b){if(!N){var c=[],d=P(),f=google.getComputedStyle(v,\"left\",i),e=document.getElementById(\"cdr_min\"),g=document.getElementById(\"cdr_max\");if(e&&g){e.style.width=google.getComputedStyle(e,\"width\",i);g.style.width=google.getComputedStyle(g,\"width\",i)}document.body.className=document.body.className.replace(/\\btbo\\b/,\"\")+(a?\" tbo\":\"\");google.Toolbelt.updateTbo();B();if(!b){if(a){f=google.getComputedStyle(v,\"left\",i);if(e&&g){e.style.width=google.getComputedStyle(e,\"width\",i);g.style.width=\u000agoogle.getComputedStyle(g,\"width\",i)}}var q=P();O(function(l,x,G){c.push([l,x[2],d[G],q[G],L])});var j=google.fx.wrap(v);j.style.position=\"absolute\";j.style.overflow=\"hidden\";j.style.left=f;v.style.display=\"block\";v.style.position=\"static\";N=i;google.fx.animate(a?400:200,c,function(){O(function(l,x){l.style[x[2]]=\"\"});v.style.position=\"absolute\";v.style.display=\"\";google.fx.unwrap(v);if(e&&g){e.style.width=\"\";g.style.width=\"\"}N=h})}}};\u000a})();\u000aif(!window.gbar||!gbar.close){window.gbar={};(function(){var e=window.gbar,g,j;function k(a){var b=window.encodeURIComponent&&(document.forms[0].q||\"\").value;if(b)a.href=a.href.replace(/([?&])q=[^&]*|$/,function(h,d){return(d||\"&\")+\"q=\"+encodeURIComponent(b)})}e.qs=k;function m(a,b,h,d,f,l){var i=document.getElementById(a),c=i.style;if(i){c.left=d?\"auto\":b+\"px\";c.right=d?b+\"px\":\"auto\";c.top=h+\"px\";c.visibility=j?\"hidden\":\"visible\";if(f){c.width=f+\"px\";c.height=l+\"px\"}else{m(g,b,h,d,i.offsetWidth,i.offsetHeight);j=j?\"\":a}}}e.tg=function(a){a=a||window.event;var b=a.target||a.srcElement;a.cancelBubble=true;if(!g){a=document.createElement(Array.every||window.createPopup?\"iframe\":\"div\");a.frameBorder=\"0\";a.src=\"#\";g=b.parentNode.appendChild(a).id=\"gbs\";document.onclick=e.close;if(e.alld){e.alld(function(){n(b)});return}}n(b)};function n(a){var b=0,h,d=window.navExtra;if(a.className!=\"gb3\")a=a.parentNode;var f=a.getAttribute(\"aria-owns\")||\"gbi\",l=a.offsetWidth,i=a.offsetTop>20?46:24,c;do b+=a.offsetLeft||0;while(a=a.offsetParent);if(f==\"gbi\")for(a=document.getElementById(f);d&&(h=d.pop());)a.insertBefore(h,a.firstChild).className=\"gb2\";else c=b=(document.documentElement.clientWidth||document.body.clientWidth)-b-l;j!=f&&e.close();m(f,b,i,c)}e.close=function(){j&&m(j,0,0)}})();;};\u000aif(google.y.first){for(var a=0,b;b=google.y.first[a];++a)b();delete google.y.first}for(a in google.y)google.y[a][1]?google.y[a][1].apply(google.y[a][0]):google.y[a][0].go();google.y.x=google.x;google.x=function(d,c){c&&c.apply(d);return false};google.y.first=[];\u000a(function (){\u000avar a=\"google\";if(window[a]){window[a].a={};window[a].c=1;function o(d,f,e){var b=d.t[f],c=d.t.start;if(!b||!(c||e))return undefined;if(e!=undefined)c=e;return b>c?b-c:c-b}window[a].report=function(d,f,e){var b=\"\";if(window[a].pt){b+=\"&srt=\"+window[a].pt;delete window[a].pt}{var c=document.getElementById(\"csi\");if(c){var h;if(window[a]._bfr!=undefined)h=window[a]._bfr;else{h=c.value;window[a]._bfr=h;c.value=1}if(h)return\"\"}}if(d.b)b+=\"&\"+d.b;if(window.parent!=window)b+=\"&wif=1\";var i=d.t,p=i.start,k=[];for(var j in i){if(j==\u000a\"start\")continue;p&&k.push(j+\".\"+o(d,j))}delete i.start;if(f)for(var l in f)b+=\"&\"+l+\"=\"+f[l];var m=[e?e:\"/csi\",\"?v=3\",\"&s=\"+(window[a].sn||\"GWS\")+\"&action=\",d.name,\"\",\"\",b,\"&rt=\",k.join(\",\")].join(\"\");{var g=new Image,n=window[a].c++;window[a].a[n]=g;g.onload=(g.onerror=function(){delete window[a].a[n]});g.src=m;g=null}return m}};if(google.timers&&google.timers.load.t){if(!google.nocsixjs)google.timers.load.t.xjsee=google.time();window.setTimeout(function(){if(google.timers.load.t){google.timers.load.t.xjs=google.time();google.timers.load.t.ol&&google.report(google.timers.load,google.kCSI)}},0)};\u000a})();\u000a" 367 | }, 368 | "redirectURL":"", 369 | "headersSize":306, 370 | "bodySize":8646 371 | }, 372 | "cache":{}, 373 | "timings":{ 374 | "dns":0, 375 | "connect":0, 376 | "blocked":0, 377 | "send":0, 378 | "wait":62, 379 | "receive":16 380 | } 381 | }, 382 | { 383 | "pageref":"page_62143", 384 | "startedDateTime":"2010-01-02T14:51:01.389+01:00", 385 | "time":47, 386 | "request":{ 387 | "method":"GET", 388 | "url":"http://clients1.google.cz/generate_204", 389 | "httpVersion":"HTTP/1.1", 390 | "cookies":[{ 391 | "name":"PREF", 392 | "value":"ID" 393 | }, 394 | { 395 | "name":"NID", 396 | "value":"29" 397 | } 398 | ], 399 | "headers":[{ 400 | "name":"Host", 401 | "value":"clients1.google.cz" 402 | }, 403 | { 404 | "name":"User-Agent", 405 | "value":"Mozilla/5.0 (Windows; U; Windows NT 6.0; en-US; rv:1.9.2b6pre) Gecko/20091230 Namoroka/3.6b6pre (.NET CLR 3.5.30729)" 406 | }, 407 | { 408 | "name":"Accept", 409 | "value":"image/png,image/*;q=0.8,*/*;q=0.5" 410 | }, 411 | { 412 | "name":"Accept-Language", 413 | "value":"en-us,en;q=0.5" 414 | }, 415 | { 416 | "name":"Accept-Encoding", 417 | "value":"gzip,deflate" 418 | }, 419 | { 420 | "name":"Accept-Charset", 421 | "value":"ISO-8859-1,utf-8;q=0.7,*;q=0.7" 422 | }, 423 | { 424 | "name":"Keep-Alive", 425 | "value":"115" 426 | }, 427 | { 428 | "name":"Connection", 429 | "value":"keep-alive" 430 | }, 431 | { 432 | "name":"Referer", 433 | "value":"http://www.google.cz/" 434 | }, 435 | { 436 | "name":"Cookie", 437 | "value":"PREF=ID=580ec4c5a3534337:U=37a8fcc41ff49f78:TM=1260796678:LM=1260796682:S=9BgbomVM6pcnfah0; NID=29=OHyg2zMZl4IpG8C4a-Z5itM3gCXOuBPogGpTPVFPNsdpmIHJWX78ymRL_gqptvhr_IQrP319GQ1fxlKUsqaIokpxasPIIDq5ijatDmYiyamnCfJz8rXyNvt5GPjCJp2I" 438 | } 439 | ], 440 | "queryString":[], 441 | "headersSize":651, 442 | "bodySize":-1 443 | }, 444 | "response":{ 445 | "status":204, 446 | "statusText":"No Content", 447 | "httpVersion":"HTTP/1.1", 448 | "cookies":[], 449 | "headers":[{ 450 | "name":"Content-Length", 451 | "value":"0" 452 | }, 453 | { 454 | "name":"Content-Type", 455 | "value":"text/html" 456 | }, 457 | { 458 | "name":"Date", 459 | "value":"Sat, 02 Jan 2010 13:51:06 GMT" 460 | }, 461 | { 462 | "name":"Server", 463 | "value":"GFE/2.0" 464 | }, 465 | { 466 | "name":"X-XSS-Protection", 467 | "value":"0" 468 | } 469 | ], 470 | "content":{ 471 | "size":0, 472 | "mimeType":"text/html" 473 | }, 474 | "redirectURL":"", 475 | "headersSize":146, 476 | "bodySize":0 477 | }, 478 | "cache":{}, 479 | "timings":{ 480 | "dns":0, 481 | "connect":0, 482 | "blocked":0, 483 | "send":0, 484 | "wait":47, 485 | "receive":0 486 | } 487 | }, 488 | { 489 | "pageref":"page_62143", 490 | "startedDateTime":"2010-01-02T14:51:01.452+01:00", 491 | "time":31, 492 | "request":{ 493 | "method":"GET", 494 | "url":"http://www.google.cz/images/nav_logo7.png", 495 | "httpVersion":"HTTP/1.1", 496 | "cookies":[{ 497 | "name":"PREF", 498 | "value":"ID" 499 | }, 500 | { 501 | "name":"NID", 502 | "value":"29" 503 | } 504 | ], 505 | "headers":[{ 506 | "name":"Host", 507 | "value":"www.google.cz" 508 | }, 509 | { 510 | "name":"User-Agent", 511 | "value":"Mozilla/5.0 (Windows; U; Windows NT 6.0; en-US; rv:1.9.2b6pre) Gecko/20091230 Namoroka/3.6b6pre (.NET CLR 3.5.30729)" 512 | }, 513 | { 514 | "name":"Accept", 515 | "value":"image/png,image/*;q=0.8,*/*;q=0.5" 516 | }, 517 | { 518 | "name":"Accept-Language", 519 | "value":"en-us,en;q=0.5" 520 | }, 521 | { 522 | "name":"Accept-Encoding", 523 | "value":"gzip,deflate" 524 | }, 525 | { 526 | "name":"Accept-Charset", 527 | "value":"ISO-8859-1,utf-8;q=0.7,*;q=0.7" 528 | }, 529 | { 530 | "name":"Keep-Alive", 531 | "value":"115" 532 | }, 533 | { 534 | "name":"Connection", 535 | "value":"keep-alive" 536 | }, 537 | { 538 | "name":"Referer", 539 | "value":"http://www.google.cz/" 540 | }, 541 | { 542 | "name":"Cookie", 543 | "value":"PREF=ID=580ec4c5a3534337:U=37a8fcc41ff49f78:TM=1260796678:LM=1260796682:S=9BgbomVM6pcnfah0; NID=29=OHyg2zMZl4IpG8C4a-Z5itM3gCXOuBPogGpTPVFPNsdpmIHJWX78ymRL_gqptvhr_IQrP319GQ1fxlKUsqaIokpxasPIIDq5ijatDmYiyamnCfJz8rXyNvt5GPjCJp2I" 544 | } 545 | ], 546 | "queryString":[], 547 | "headersSize":654, 548 | "bodySize":-1 549 | }, 550 | "response":{ 551 | "status":200, 552 | "statusText":"OK", 553 | "httpVersion":"HTTP/1.1", 554 | "cookies":[], 555 | "headers":[{ 556 | "name":"Content-Type", 557 | "value":"image/png" 558 | }, 559 | { 560 | "name":"Last-Modified", 561 | "value":"Thu, 23 Jul 2009 17:45:03 GMT" 562 | }, 563 | { 564 | "name":"Date", 565 | "value":"Sat, 02 Jan 2010 13:05:54 GMT" 566 | }, 567 | { 568 | "name":"Expires", 569 | "value":"Sun, 02 Jan 2011 13:05:54 GMT" 570 | }, 571 | { 572 | "name":"Server", 573 | "value":"gws" 574 | }, 575 | { 576 | "name":"Content-Length", 577 | "value":"5401" 578 | }, 579 | { 580 | "name":"Cache-Control", 581 | "value":"public, max-age=31536000" 582 | }, 583 | { 584 | "name":"Age", 585 | "value":"2712" 586 | }, 587 | { 588 | "name":"X-XSS-Protection", 589 | "value":"0" 590 | } 591 | ], 592 | "content":{ 593 | "size":5401, 594 | "mimeType":"image/png" 595 | }, 596 | "redirectURL":"", 597 | "headersSize":275, 598 | "bodySize":5401 599 | }, 600 | "cache":{}, 601 | "timings":{ 602 | "dns":0, 603 | "connect":0, 604 | "blocked":0, 605 | "send":0, 606 | "wait":31, 607 | "receive":0 608 | } 609 | } 610 | ] 611 | } 612 | } -------------------------------------------------------------------------------- /spec/spec_helper.rb: -------------------------------------------------------------------------------- 1 | require 'browsermob/proxy' 2 | require 'selenium-webdriver' 3 | require 'browsermob/proxy/webdriver_listener' 4 | require 'rack' 5 | 6 | RestClient.log = STDOUT 7 | 8 | module BrowserMob 9 | module Proxy 10 | module SpecHelper 11 | def self.httpd 12 | @httpd ||= HttpServer.new(SpecApp.new(Rack::File.new(fixture_dir))) 13 | end 14 | 15 | def self.fixture_dir 16 | @fixture_dir ||= File.join(File.expand_path("../", __FILE__), "fixtures") 17 | end 18 | 19 | def server 20 | $_bm_server ||= Server.new( 21 | File.join(home, "bin", "browsermob-proxy"), 22 | :port => Selenium::WebDriver::PortProber.above(3000), 23 | :log => true 24 | ).start 25 | end 26 | 27 | def new_proxy 28 | server.create_proxy 29 | end 30 | 31 | def home 32 | ENV['BROWSERMOB_PROXY_HOME'] or raise "BROWSERMOB_PROXY_HOME not set" 33 | end 34 | 35 | def fixture(name) 36 | File.read(fixture_path(name)) 37 | end 38 | 39 | def url_for(page) 40 | SpecHelper.httpd.url_for(page) 41 | end 42 | 43 | def fixture_path(name) 44 | File.join(SpecHelper.fixture_dir, name) 45 | end 46 | 47 | class HttpServer 48 | def initialize(app) 49 | @port = Selenium::WebDriver::PortProber.above(3000) 50 | 51 | pid = fork do 52 | Rack::Server.new(:app => app, :Port => @port).start 53 | end 54 | 55 | at_exit { Process.kill 'TERM', pid } 56 | 57 | poller = Selenium::WebDriver::SocketPoller.new("0.0.0.0", @port, 10) 58 | 59 | unless poller.connected? 60 | raise "unable to start web server in 5 seconds" 61 | end 62 | end 63 | 64 | def url_for(page) 65 | # avoid default no-proxy rules on localhost 66 | host = ENV['TRAVIS'] ? Selenium::WebDriver::Platform.ip : '0.0.0.0' 67 | "http://#{host}:#{@port}/#{page}" 68 | end 69 | end 70 | 71 | class SpecApp 72 | def initialize(app) 73 | @app = app 74 | end 75 | 76 | def call(env) 77 | case env['REQUEST_PATH'] 78 | when '/slow' 79 | sleep 0.1 80 | [200, {}, []] 81 | else 82 | @app.call(env) 83 | end 84 | end 85 | 86 | end 87 | end 88 | end 89 | end 90 | 91 | RSpec.configure do |c| 92 | c.include(BrowserMob::Proxy::SpecHelper) 93 | c.after(:suite) { $_bm_server.stop if $_bm_server } 94 | end 95 | -------------------------------------------------------------------------------- /spec/unit/client_spec.rb: -------------------------------------------------------------------------------- 1 | require 'spec_helper' 2 | 3 | module BrowserMob 4 | module Proxy 5 | 6 | DOMAIN = 'example.com' 7 | 8 | describe Client do 9 | let(:resource) { double(RestClient::Resource) } 10 | let(:client) { Client.new(resource, "localhost", 9091) } 11 | 12 | before do 13 | { 14 | "har" => double("resource[har]"), 15 | "har/pageRef" => double("resource[har/pageRef]"), 16 | "whitelist" => double("resource[whitelist]"), 17 | "blacklist" => double("resource[blacklist]"), 18 | "limit" => double("resource[limit]"), 19 | "headers" => double("resource[headers]"), 20 | "auth/basic/#{DOMAIN}" => double("resource[auth/basic/#{DOMAIN}]"), 21 | "hosts" => double("resource[hosts]"), 22 | "timeout" => double("resource[timeout]"), 23 | "rewrite" => double("resource[rewrite]"), 24 | "interceptor/request" => double("resource[interceptor/request]"), 25 | "interceptor/response" => double("resource[interceptor/response]") 26 | }.each do |path, mock| 27 | resource.stub(:[]).with(path).and_return(mock) 28 | end 29 | end 30 | 31 | it "creates a named har" do 32 | resource['har'].should_receive(:put). 33 | with(:initialPageRef => "foo"). 34 | and_return('') 35 | 36 | client.new_har("foo").should be_nil 37 | end 38 | 39 | it "creates a new har with no name" do 40 | resource['har'].should_receive(:put). 41 | with({}). 42 | and_return('') 43 | 44 | client.new_har.should be_nil 45 | end 46 | 47 | it "returns the previous archive if one exists" do 48 | resource['har'].should_receive(:put). 49 | with(:initialPageRef => "foo"). 50 | and_return(fixture("google.har")) 51 | 52 | client.new_har("foo").should be_kind_of(HAR::Archive) 53 | end 54 | 55 | it "turns on header capture when given a name" do 56 | resource['har'].should_receive(:put). 57 | with(:initialPageRef => "foo", :captureHeaders => true). 58 | and_return('') 59 | 60 | client.new_har("foo", :capture_headers => true).should be_nil 61 | end 62 | 63 | it "turns on header capture when not given a name" do 64 | resource['har'].should_receive(:put). 65 | with(:captureHeaders => true). 66 | and_return('') 67 | 68 | client.new_har(:capture_headers => true).should be_nil 69 | end 70 | 71 | it "turns on content capture when given a name" do 72 | resource['har'].should_receive(:put). 73 | with(:initialPageRef => "foo", :captureContent => true). 74 | and_return('') 75 | 76 | client.new_har("foo", :capture_content => true).should be_nil 77 | end 78 | 79 | it "turns on header capture when not given a name" do 80 | resource['har'].should_receive(:put). 81 | with(:captureContent => true). 82 | and_return('') 83 | 84 | client.new_har(:capture_content => true).should be_nil 85 | end 86 | 87 | it "turns on content capture and binary content capture when given a name" do 88 | resource['har'].should_receive(:put). 89 | with(:initialPageRef => "foo", 90 | :captureContent => true, 91 | :captureBinaryContent => true). 92 | and_return('') 93 | 94 | client.new_har("foo", :capture_binary_content => true).should be_nil 95 | end 96 | 97 | it "turns on content capture and binary content capture when not given a name" do 98 | resource['har'].should_receive(:put). 99 | with(:captureContent => true, 100 | :captureBinaryContent => true). 101 | and_return('') 102 | 103 | client.new_har(:capture_binary_content => true).should be_nil 104 | end 105 | 106 | it "gets the current har" do 107 | resource['har'].should_receive(:get). 108 | and_return(fixture("google.har")) 109 | 110 | client.har.should be_kind_of(HAR::Archive) 111 | end 112 | 113 | it "creates a new page" do 114 | resource['har/pageRef'].should_receive(:put). 115 | with :pageRef => "foo" 116 | 117 | client.new_page "foo" 118 | end 119 | 120 | it "sets the blacklist" do 121 | resource['blacklist'].should_receive(:put). 122 | with(:regex => "http://example.com", :status => 401) 123 | 124 | client.blacklist(%r[http://example.com], 401) 125 | end 126 | 127 | it "clears the blacklist" do 128 | resource['blacklist'].should_receive(:delete) 129 | 130 | client.clear_blacklist 131 | end 132 | 133 | it "creates request interceptor" do 134 | resource['interceptor/request'].should_receive(:post).with("foo", :content_type => "text/plain") 135 | client.request_interceptor = "foo" 136 | end 137 | 138 | it "creates response interceptor" do 139 | resource['interceptor/response'].should_receive(:post).with("foo", :content_type => "text/plain") 140 | client.response_interceptor = "foo" 141 | end 142 | 143 | describe 'whitelist' do 144 | it "supports a string" do 145 | resource['whitelist'].should_receive(:put). 146 | with(:regex => 'https?://example\.com', :status => 401) 147 | 148 | client.whitelist('https?://example\.com', 401) 149 | end 150 | 151 | it "supports a regexp" do 152 | resource['whitelist'].should_receive(:put). 153 | with(:regex => 'https?://example\.com', :status => 401) 154 | 155 | client.whitelist(%r{https?://example\.com}, 401) 156 | end 157 | 158 | it "supports an array of regexps and strings" do 159 | resource['whitelist'].should_receive(:put). 160 | with(:regex => 'http://example\.com/1/.+,http://example\.com/2/.+', :status => 401) 161 | 162 | client.whitelist([%r{http://example\.com/1/.+}, 'http://example\.com/2/.+'], 401) 163 | end 164 | 165 | it "clears the whitelist" do 166 | resource['whitelist'].should_receive(:delete) 167 | 168 | client.clear_whitelist 169 | end 170 | end 171 | 172 | it "sets the :downstream_kbps limit" do 173 | resource['limit'].should_receive(:put). 174 | with('downstreamKbps' => 100) 175 | 176 | client.limit(:downstream_kbps => 100) 177 | end 178 | 179 | it "sets the :upstream_kbps limit" do 180 | resource['limit'].should_receive(:put). 181 | with('upstreamKbps' => 100) 182 | 183 | client.limit(:upstream_kbps => 100) 184 | end 185 | 186 | it "sets the :latency limit" do 187 | resource['limit'].should_receive(:put). 188 | with('latency' => 100) 189 | 190 | client.limit(:latency => 100) 191 | end 192 | 193 | it "sets all limits" do 194 | resource['limit'].should_receive(:put). 195 | with('latency' => 100, 'downstreamKbps' => 200, 'upstreamKbps' => 300) 196 | 197 | client.limit(:latency => 100, :downstream_kbps => 200, :upstream_kbps => 300) 198 | end 199 | 200 | it "raises ArgumentError on invalid options" do 201 | lambda { client.limit(:foo => 1) }.should raise_error(ArgumentError) 202 | lambda { client.limit({}) }.should raise_error(ArgumentError) 203 | end 204 | 205 | it "sets headers" do 206 | resource['headers'].should_receive(:post).with('{"foo":"bar"}', :content_type => "application/json") 207 | 208 | client.headers(:foo => "bar") 209 | end 210 | 211 | it 'sets basic authentication' do 212 | user, password = 'user', 'pass' 213 | resource["auth/basic/#{DOMAIN}"].should_receive(:post).with(%({"username":"#{user}","password":"#{password}"}), :content_type => "application/json") 214 | 215 | client.basic_authentication(DOMAIN, user, password) 216 | end 217 | 218 | describe 'timeouts' do 219 | it 'supports valid options' do 220 | resource['timeout'].should_receive(:put).with( 221 | :requestTimeout => 1, 222 | :readTimeout => 2000, 223 | :connectionTimeout => 3000, 224 | :dnsCacheTimeout => 6_000_000 225 | ) 226 | 227 | client.timeouts( 228 | :request => 0.001, 229 | :read => 2, 230 | :connection => 3, 231 | :dns_cache => 6000 232 | ) 233 | end 234 | 235 | it 'raises ArgumentError when invalid options are passed' do 236 | expect { client.timeouts(:invalid => 2) }.to raise_error(ArgumentError, "invalid key: :invalid, should belong to: [:request, :read, :connection, :dns_cache]") 237 | end 238 | end 239 | 240 | it 'sets mapped dns hosts' do 241 | resource['hosts'].should_receive(:post).with(%({"#{DOMAIN}":"1.2.3.4"}), 242 | :content_type => "application/json") 243 | 244 | client.remap_dns_hosts(DOMAIN => '1.2.3.4') 245 | end 246 | 247 | describe 'rewrite rules' do 248 | 249 | context 'when using a regular expression' do 250 | it 'sets a rewrite rule' do 251 | resource['rewrite'].should_receive(:put). 252 | with(:matchRegex => 'old\.com', :replace => 'new.com') 253 | 254 | client.rewrite('old\.com', 'new.com') 255 | end 256 | end 257 | 258 | context 'when using a string' do 259 | it 'sets a rewrite rule' do 260 | resource['rewrite'].should_receive(:put). 261 | with(:matchRegex => 'old\.com', :replace => 'new.com') 262 | 263 | client.rewrite(%r{old\.com}, 'new.com') 264 | end 265 | end 266 | 267 | it 'clears the rewrite rules' do 268 | resource['rewrite'].should_receive(:delete) 269 | 270 | client.clear_rewrites 271 | end 272 | end 273 | 274 | 275 | context "#selenium_proxy" do 276 | it "defaults to HTTP proxy only" do 277 | proxy = client.selenium_proxy 278 | 279 | proxy.http.should == "#{client.host}:#{client.port}" 280 | proxy.ssl.should be_nil 281 | proxy.ftp.should be_nil 282 | end 283 | 284 | it "allows multiple protocols" do 285 | proxy = client.selenium_proxy(:http, :ssl) 286 | 287 | proxy.http.should == "#{client.host}:#{client.port}" 288 | proxy.ssl.should == "#{client.host}:#{client.port}" 289 | proxy.ftp.should be_nil 290 | end 291 | 292 | it "allows disabling HTTP proxy" do 293 | proxy = client.selenium_proxy(:ssl) 294 | 295 | proxy.ssl.should == "#{client.host}:#{client.port}" 296 | proxy.http.should be_nil 297 | proxy.ftp.should be_nil 298 | end 299 | 300 | it "raises an error when a bad protocol is used" do 301 | lambda { 302 | client.selenium_proxy(:htp) 303 | }.should raise_error 304 | end 305 | end 306 | end 307 | 308 | end 309 | end 310 | -------------------------------------------------------------------------------- /spec/unit/webdriver_listener_spec.rb: -------------------------------------------------------------------------------- 1 | require 'spec_helper' 2 | 3 | module BrowserMob 4 | module Proxy 5 | 6 | describe WebDriverListener do 7 | let(:client) { double(Client) } 8 | let(:driver) { double(Selenium::WebDriver::Driver, :current_url => 'http://foo') } 9 | let(:listener) { WebDriverListener.new(client) } 10 | let(:element) { double(Selenium::WebDriver::Element, :ref => "some-id")} 11 | let(:har) { double(HAR::Archive) } 12 | let(:url) { "http://example.com" } 13 | 14 | it 'creates a new har on navigate.to' do 15 | client.should_receive(:new_har).with("navigate-to-http://example.com", {}) 16 | client.should_receive(:har).and_return(:har) 17 | 18 | listener.before_navigate_to(url, driver) 19 | listener.before_quit(driver) 20 | listener.hars.size.should == 1 21 | end 22 | 23 | it 'creates a new page on navigate.back' do 24 | client.should_receive(:new_page).with(/^navigate-back/) 25 | 26 | listener.before_navigate_back(driver) 27 | end 28 | 29 | it 'creates a new page on navigate.forward' do 30 | client.should_receive(:new_page).with(/^navigate-forward/) 31 | 32 | listener.before_navigate_forward(driver) 33 | end 34 | 35 | it 'creates a new page on click' do 36 | client.should_receive(:new_page).with(/^click-element/) 37 | 38 | listener.before_click(element, driver) 39 | end 40 | 41 | it 'saves har before quit' do 42 | client.should_receive(:har).and_return(har) 43 | 44 | listener.before_quit(driver) 45 | listener.hars.size.should == 1 46 | end 47 | 48 | it 'passes the :capture_headers option' do 49 | listener = WebDriverListener.new(client, :capture_headers => true) 50 | client.should_receive(:new_har).with("navigate-to-http://example.com", :capture_headers => true) 51 | 52 | listener.before_navigate_to(url, driver) 53 | end 54 | end 55 | end 56 | end 57 | --------------------------------------------------------------------------------