browsed 0.1.0

Sign up to get free protection for your applications and to get access to all the features.
@@ -0,0 +1,7 @@
1
+ ---
2
+ SHA256:
3
+ metadata.gz: 273de99ce82d529db724b003f381012c5b1f92334d51cf313c8f6eb7cdbc924a
4
+ data.tar.gz: 57b78a4285b0345005e5e1b1fd62c5d971a8a4f4158a54b4e9ff54cfc05c98e7
5
+ SHA512:
6
+ metadata.gz: 7e410f72cb0b1ddede87f09f2693378cb8e7821a2519e15f724fd4fafe7ac24486e1c7db6a3da1c1f5bd482e6c5423a4b857654e6ce6d6384262e2e571e1f170
7
+ data.tar.gz: 542852786da4017490e028df8e2ff1eaad8021c92ef17efec8900684b0200343b8315d212db78e46b84561a5f9c37cd07d175c33345a66876193239432d0b218
@@ -0,0 +1,23 @@
1
+ /.bundle/
2
+ /.yardoc
3
+ /_yardoc/
4
+ /coverage/
5
+ /doc/
6
+ /pkg/
7
+ /spec/reports/
8
+ /spec/tmp/
9
+ /tmp/
10
+ /files/
11
+
12
+ # rspec failure tracking
13
+ .rspec_status
14
+
15
+ # pry history
16
+ .pry_history
17
+
18
+ # RVM
19
+ .ruby-version
20
+ .ruby-gemset
21
+
22
+ # DS_Store
23
+ .DS_Store
data/.rspec ADDED
@@ -0,0 +1,3 @@
1
+ --format documentation
2
+ --color
3
+ --require spec_helper
@@ -0,0 +1,7 @@
1
+ ---
2
+ sudo: false
3
+ language: ruby
4
+ cache: bundler
5
+ rvm:
6
+ - 2.6.0
7
+ before_install: gem install bundler -v 1.16.3
@@ -0,0 +1,74 @@
1
+ # Contributor Covenant Code of Conduct
2
+
3
+ ## Our Pledge
4
+
5
+ In the interest of fostering an open and welcoming environment, we as
6
+ contributors and maintainers pledge to making participation in our project and
7
+ our community a harassment-free experience for everyone, regardless of age, body
8
+ size, disability, ethnicity, gender identity and expression, level of experience,
9
+ nationality, personal appearance, race, religion, or sexual identity and
10
+ orientation.
11
+
12
+ ## Our Standards
13
+
14
+ Examples of behavior that contributes to creating a positive environment
15
+ include:
16
+
17
+ * Using welcoming and inclusive language
18
+ * Being respectful of differing viewpoints and experiences
19
+ * Gracefully accepting constructive criticism
20
+ * Focusing on what is best for the community
21
+ * Showing empathy towards other community members
22
+
23
+ Examples of unacceptable behavior by participants include:
24
+
25
+ * The use of sexualized language or imagery and unwelcome sexual attention or
26
+ advances
27
+ * Trolling, insulting/derogatory comments, and personal or political attacks
28
+ * Public or private harassment
29
+ * Publishing others' private information, such as a physical or electronic
30
+ address, without explicit permission
31
+ * Other conduct which could reasonably be considered inappropriate in a
32
+ professional setting
33
+
34
+ ## Our Responsibilities
35
+
36
+ Project maintainers are responsible for clarifying the standards of acceptable
37
+ behavior and are expected to take appropriate and fair corrective action in
38
+ response to any instances of unacceptable behavior.
39
+
40
+ Project maintainers have the right and responsibility to remove, edit, or
41
+ reject comments, commits, code, wiki edits, issues, and other contributions
42
+ that are not aligned to this Code of Conduct, or to ban temporarily or
43
+ permanently any contributor for other behaviors that they deem inappropriate,
44
+ threatening, offensive, or harmful.
45
+
46
+ ## Scope
47
+
48
+ This Code of Conduct applies both within project spaces and in public spaces
49
+ when an individual is representing the project or its community. Examples of
50
+ representing a project or community include using an official project e-mail
51
+ address, posting via an official social media account, or acting as an appointed
52
+ representative at an online or offline event. Representation of a project may be
53
+ further defined and clarified by project maintainers.
54
+
55
+ ## Enforcement
56
+
57
+ Instances of abusive, harassing, or otherwise unacceptable behavior may be
58
+ reported by contacting the project team at sebastian.johnsson@gmail.com. All
59
+ complaints will be reviewed and investigated and will result in a response that
60
+ is deemed necessary and appropriate to the circumstances. The project team is
61
+ obligated to maintain confidentiality with regard to the reporter of an incident.
62
+ Further details of specific enforcement policies may be posted separately.
63
+
64
+ Project maintainers who do not follow or enforce the Code of Conduct in good
65
+ faith may face temporary or permanent repercussions as determined by other
66
+ members of the project's leadership.
67
+
68
+ ## Attribution
69
+
70
+ This Code of Conduct is adapted from the [Contributor Covenant][homepage], version 1.4,
71
+ available at [http://contributor-covenant.org/version/1/4][version]
72
+
73
+ [homepage]: http://contributor-covenant.org
74
+ [version]: http://contributor-covenant.org/version/1/4/
data/Gemfile ADDED
@@ -0,0 +1,6 @@
1
+ source "https://rubygems.org"
2
+
3
+ git_source(:github) {|repo_name| "https://github.com/#{repo_name}" }
4
+
5
+ # Specify your gem's dependencies in browsed.gemspec
6
+ gemspec
@@ -0,0 +1,83 @@
1
+ PATH
2
+ remote: .
3
+ specs:
4
+ browsed (0.1.0)
5
+ agents
6
+ capybara (~> 3.4, >= 3.4.1)
7
+ poltergeist (~> 1.18, >= 1.18.1)
8
+ selenium-webdriver (~> 3.13, >= 3.13.1)
9
+
10
+ GEM
11
+ remote: https://rubygems.org/
12
+ specs:
13
+ addressable (2.5.2)
14
+ public_suffix (>= 2.0.2, < 4.0)
15
+ agents (0.1.2)
16
+ capybara (3.4.2)
17
+ addressable
18
+ mini_mime (>= 0.1.3)
19
+ nokogiri (~> 1.8)
20
+ rack (>= 1.6.0)
21
+ rack-test (>= 0.6.3)
22
+ xpath (~> 3.1)
23
+ childprocess (0.9.0)
24
+ ffi (~> 1.0, >= 1.0.11)
25
+ cliver (0.3.2)
26
+ coderay (1.1.2)
27
+ diff-lcs (1.3)
28
+ ffi (1.9.25)
29
+ launchy (2.4.3)
30
+ addressable (~> 2.3)
31
+ method_source (0.9.0)
32
+ mini_mime (1.0.0)
33
+ mini_portile2 (2.3.0)
34
+ nokogiri (1.8.4)
35
+ mini_portile2 (~> 2.3.0)
36
+ poltergeist (1.18.1)
37
+ capybara (>= 2.1, < 4)
38
+ cliver (~> 0.3.1)
39
+ websocket-driver (>= 0.2.0)
40
+ pry (0.11.3)
41
+ coderay (~> 1.1.0)
42
+ method_source (~> 0.9.0)
43
+ public_suffix (3.0.2)
44
+ rack (2.0.5)
45
+ rack-test (1.1.0)
46
+ rack (>= 1.0, < 3)
47
+ rake (10.5.0)
48
+ rspec (3.7.0)
49
+ rspec-core (~> 3.7.0)
50
+ rspec-expectations (~> 3.7.0)
51
+ rspec-mocks (~> 3.7.0)
52
+ rspec-core (3.7.1)
53
+ rspec-support (~> 3.7.0)
54
+ rspec-expectations (3.7.0)
55
+ diff-lcs (>= 1.2.0, < 2.0)
56
+ rspec-support (~> 3.7.0)
57
+ rspec-mocks (3.7.0)
58
+ diff-lcs (>= 1.2.0, < 2.0)
59
+ rspec-support (~> 3.7.0)
60
+ rspec-support (3.7.1)
61
+ rubyzip (1.2.1)
62
+ selenium-webdriver (3.13.1)
63
+ childprocess (~> 0.5)
64
+ rubyzip (~> 1.2)
65
+ websocket-driver (0.7.0)
66
+ websocket-extensions (>= 0.1.0)
67
+ websocket-extensions (0.1.3)
68
+ xpath (3.1.0)
69
+ nokogiri (~> 1.8)
70
+
71
+ PLATFORMS
72
+ ruby
73
+
74
+ DEPENDENCIES
75
+ browsed!
76
+ bundler (~> 1.16)
77
+ launchy (~> 2.4, >= 2.4.3)
78
+ pry (~> 0.11.3)
79
+ rake (~> 10.0)
80
+ rspec (~> 3.0)
81
+
82
+ BUNDLED WITH
83
+ 1.16.3
@@ -0,0 +1,21 @@
1
+ The MIT License (MIT)
2
+
3
+ Copyright (c) 2018 Sebastian
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in
13
+ all copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
21
+ THE SOFTWARE.
@@ -0,0 +1,59 @@
1
+ # Browsed
2
+
3
+ Browsed is a lightweight Capybara/PhantomJS/Selenium framework with tools and utilities for randomizing user agents, resolutions etc.
4
+
5
+ ## Installation
6
+
7
+ Add this line to your application's Gemfile:
8
+
9
+ ```ruby
10
+ gem 'browsed'
11
+ ```
12
+
13
+ And then execute:
14
+
15
+ $ bundle
16
+
17
+ Or install it yourself as:
18
+
19
+ $ gem install browsed
20
+
21
+ ## Usage
22
+
23
+ ```ruby
24
+ # driver can be :poltergeist (PhantomJS) or :selenium/:selenium_chrome (Firefox/Chrome)
25
+ # browser can be: :phantomjs, :firefox, :chrome
26
+ # device can be :desktop, :phone (iphone/android), :tablet (ipad/android), :iphone, :ipad, :android_phone, :android_tablet
27
+ client = Browsed::Client.new(driver: :poltergeist, browser: :phantomjs, device: :desktop)
28
+ ```
29
+
30
+ If you want to use proxies (note that proxy authentication is only possible using Poltergeist/PhantomJS):
31
+
32
+ ```ruby
33
+ proxy = {host: "127.0.0.1", port: 8080, username: "foo", password: "bar"}
34
+ client = Browsed::Client.new(driver: :poltergeist, browser: :phantomjs, device: :desktop, proxy: proxy)
35
+ ```
36
+
37
+ Use the session property to interact with the underlying Capybara::Session object:
38
+
39
+ ```ruby
40
+ client.session.visit("https://www.google.com")
41
+ ```
42
+
43
+ ## Development
44
+
45
+ After checking out the repo, run `bin/setup` to install dependencies. Then, run `rake spec` to run the tests. You can also run `bin/console` for an interactive prompt that will allow you to experiment.
46
+
47
+ To install this gem onto your local machine, run `bundle exec rake install`. To release a new version, update the version number in `version.rb`, and then run `bundle exec rake release`, which will create a git tag for the version, push git commits and tags, and push the `.gem` file to [rubygems.org](https://rubygems.org).
48
+
49
+ ## Contributing
50
+
51
+ Bug reports and pull requests are welcome on GitHub at https://github.com/SebastianJ/browsed. This project is intended to be a safe, welcoming space for collaboration, and contributors are expected to adhere to the [Contributor Covenant](http://contributor-covenant.org) code of conduct.
52
+
53
+ ## License
54
+
55
+ The gem is available as open source under the terms of the [MIT License](https://opensource.org/licenses/MIT).
56
+
57
+ ## Code of Conduct
58
+
59
+ Everyone interacting in the Browsed project’s codebases, issue trackers, chat rooms and mailing lists is expected to follow the [code of conduct](https://github.com/SebastianJ/browsed/blob/master/CODE_OF_CONDUCT.md).
@@ -0,0 +1,6 @@
1
+ require "bundler/gem_tasks"
2
+ require "rspec/core/rake_task"
3
+
4
+ RSpec::Core::RakeTask.new(:spec)
5
+
6
+ task :default => :spec
@@ -0,0 +1,20 @@
1
+ #!/usr/bin/env ruby
2
+
3
+ require "bundler/setup"
4
+ require "browsed"
5
+ require "launchy"
6
+
7
+ driver = :selenium
8
+ browser = :firefox
9
+
10
+ url = "https://whatismyipaddress.com"
11
+ path = File.expand_path("../../files/#{Time.now.to_i}.png", __FILE__)
12
+
13
+ client = Browsed::Client.new(driver: driver, browser: browser, device: :desktop, environment: :development)
14
+
15
+ client.session.visit(url)
16
+
17
+ if driver.eql?(:poltergeist)
18
+ client.session.save_screenshot(path)
19
+ client.display_screenshot!(path)
20
+ end
@@ -0,0 +1,15 @@
1
+ #!/usr/bin/env ruby
2
+
3
+ require "bundler/setup"
4
+ require "browsed"
5
+
6
+ # You can add fixtures and/or initialization code here to make experimenting
7
+ # with your gem easier. You can also use a different console, if you like.
8
+
9
+ # (If you use this, don't forget to add pry to your Gemfile!)
10
+ require "pry"
11
+ Pry.config.history.file = File.join(__FILE__, "../.pry_history")
12
+ Pry.start
13
+
14
+ #require "irb"
15
+ #IRB.start(__FILE__)
@@ -0,0 +1,8 @@
1
+ #!/usr/bin/env bash
2
+ set -euo pipefail
3
+ IFS=$'\n\t'
4
+ set -vx
5
+
6
+ bundle install
7
+
8
+ # Do any other automated setup that you need to do here
@@ -0,0 +1,38 @@
1
+
2
+ lib = File.expand_path("../lib", __FILE__)
3
+ $LOAD_PATH.unshift(lib) unless $LOAD_PATH.include?(lib)
4
+ require "browsed/version"
5
+
6
+ Gem::Specification.new do |spec|
7
+ spec.name = "browsed"
8
+ spec.version = Browsed::VERSION
9
+ spec.authors = ["Sebastian"]
10
+ spec.email = ["sebastian.johnsson@gmail.com"]
11
+
12
+ spec.summary = %q{Browsed: Lightweight Capybara/PhantomJS/Selenium framework}
13
+ spec.description = %q{A lightweight framework for making it easier to work with Capybara/PhantomJS/Selenium}
14
+ spec.homepage = "https://github.com/SebastianJ"
15
+ spec.license = "MIT"
16
+
17
+ # Specify which files should be added to the gem when it is released.
18
+ # The `git ls-files -z` loads the files in the RubyGem that have been added into git.
19
+ spec.files = Dir.chdir(File.expand_path('..', __FILE__)) do
20
+ `git ls-files -z`.split("\x0").reject { |f| f.match(%r{^(test|spec|features)/}) }
21
+ end
22
+ spec.bindir = "exe"
23
+ spec.executables = spec.files.grep(%r{^exe/}) { |f| File.basename(f) }
24
+ spec.require_paths = ["lib"]
25
+
26
+ spec.add_dependency "capybara", '~> 3.4', '>= 3.4.1'
27
+ spec.add_dependency "poltergeist", '~> 1.18', '>= 1.18.1'
28
+ spec.add_dependency "selenium-webdriver", '~> 3.13', '>= 3.13.1'
29
+
30
+ spec.add_dependency "agents"
31
+
32
+ spec.add_development_dependency "bundler", "~> 1.16"
33
+ spec.add_development_dependency "rake", "~> 10.0"
34
+ spec.add_development_dependency "rspec", "~> 3.0"
35
+
36
+ spec.add_development_dependency "pry", "~> 0.11.3"
37
+ spec.add_development_dependency "launchy", '~> 2.4', '>= 2.4.3'
38
+ end
@@ -0,0 +1,40 @@
1
+ require "selenium-webdriver"
2
+
3
+ require "capybara"
4
+ require "capybara/dsl"
5
+ require "capybara/poltergeist"
6
+
7
+ require "agents"
8
+
9
+ require "browsed/version"
10
+
11
+ require "browsed/constants"
12
+ require "browsed/errors"
13
+ require "browsed/configuration"
14
+
15
+ require "browsed/manager"
16
+
17
+ require "browsed/poltergeist"
18
+ require "browsed/firefox"
19
+ require "browsed/chrome"
20
+ require "browsed/client"
21
+
22
+ module Browsed
23
+
24
+ class << self
25
+ attr_writer :configuration
26
+ end
27
+
28
+ def self.configuration
29
+ @configuration ||= ::Browsed::Configuration.new
30
+ end
31
+
32
+ def self.reset
33
+ @configuration = ::Browsed::Configuration.new
34
+ end
35
+
36
+ def self.configure
37
+ yield(configuration)
38
+ end
39
+
40
+ end
@@ -0,0 +1,35 @@
1
+ module Browsed
2
+ module Chrome
3
+
4
+ private
5
+ def register_chrome_driver(driver_options: {}, timeout: 60, debug: false)
6
+ profile = Selenium::WebDriver::Chrome::Profile.new
7
+
8
+ profile["user-agent"] = self.user_agent unless self.user_agent.to_s.empty?
9
+
10
+ proxy_options = chrome_proxy_options
11
+ capabilities = !proxy_options.nil? ? Selenium::WebDriver::Remote::Capabilities.chrome(proxy: proxy_options) : {}
12
+ options = Selenium::WebDriver::Chrome::Options.new(profile: profile)
13
+
14
+ Capybara.register_driver self.driver do |app|
15
+ Capybara::Selenium::Driver.new(app, browser: :chrome, options: options, desired_capabilities: capabilities)
16
+ end
17
+ end
18
+
19
+ def chrome_proxy_options
20
+ proxy_options = nil
21
+
22
+ if self.proxy && !self.proxy.empty? && self.proxy.has_key?(:host) && self.proxy.has_key?(:port)
23
+ proxy_options = Selenium::WebDriver::Proxy.new(
24
+ http: "#{self.proxy.fetch(:host)}:#{self.proxy.fetch(:port)}",
25
+ ssl: "#{self.proxy.fetch(:host)}:#{self.proxy.fetch(:port)}"
26
+ )
27
+
28
+ log("Will use proxy #{self.proxy.fetch(:host)}:#{self.proxy.fetch(:port)} to initiate the request.")
29
+ end
30
+
31
+ return proxy_options
32
+ end
33
+
34
+ end
35
+ end
@@ -0,0 +1,182 @@
1
+ module Browsed
2
+ class Client
3
+ attr_accessor :configuration
4
+ attr_accessor :manager, :maximum_processes
5
+ attr_accessor :driver, :browser, :environment
6
+ attr_accessor :session
7
+ attr_accessor :device, :proxy
8
+ attr_accessor :user_agent
9
+ attr_accessor :resolution
10
+
11
+ include Capybara::DSL
12
+
13
+ def initialize(configuration: ::Browsed.configuration,
14
+ driver: :poltergeist,
15
+ browser: :phantomjs,
16
+ device: :desktop,
17
+ proxy: nil,
18
+ user_agent: nil,
19
+ resolution: nil,
20
+ environment: :production,
21
+ driver_options: {},
22
+ maximum_processes: nil)
23
+
24
+ self.configuration = configuration
25
+
26
+ self.driver = driver || self.configuration.driver
27
+ self.browser = browser || self.configuration.browser
28
+ self.environment = environment || self.configuration.environment
29
+
30
+ self.device = device
31
+ self.proxy = proxy
32
+
33
+ self.manager = Browsed::Manager.new(browser: self.browser)
34
+ self.maximum_processes = maximum_processes || self.configuration.maximum_processes
35
+
36
+ set_user_agent(user_agent)
37
+ set_resolution(resolution)
38
+
39
+ setup_capybara(driver_options: driver_options)
40
+ end
41
+
42
+ include ::Browsed::Poltergeist
43
+ include ::Browsed::Firefox
44
+ include ::Browsed::Chrome
45
+
46
+ def setup_capybara(driver_options: {}, retries: 3)
47
+ if can_start_new_process?
48
+ if poltergeist?
49
+ register_poltergeist_driver(driver_options: driver_options)
50
+ elsif selenium?
51
+ if firefox_browser?
52
+ register_firefox_driver(driver_options: driver_options)
53
+ elsif chrome_browser?
54
+ self.driver = :selenium_chrome
55
+ register_chrome_driver(driver_options: driver_options)
56
+ end
57
+ end
58
+
59
+ puts "Self.driver: #{self.driver}"
60
+ puts "Self.browser: #{self.browser}"
61
+
62
+ Capybara.default_driver = self.driver
63
+ Capybara.javascript_driver = self.driver
64
+
65
+ Capybara.default_max_wait_time = driver_options.fetch(:wait_time, 30) #seconds
66
+
67
+ self.session = Capybara::Session.new(self.driver)
68
+ else
69
+ raise Browsed::TooManyProcessesError, "Too many PhantomJS processes running, reached maximum allowed number of #{self.maximum_processes}"
70
+ end
71
+ end
72
+
73
+ def can_start_new_process?
74
+ self.maximum_processes.nil? || self.manager.can_start_more_processes?(max_count: self.maximum_processes)
75
+ end
76
+
77
+ def display_screenshot!(path)
78
+ Launchy.open path if development?
79
+ end
80
+
81
+ private
82
+ def poltergeist?
83
+ self.driver.to_sym.eql?(:poltergeist)
84
+ end
85
+
86
+ def selenium?
87
+ self.driver.to_sym.eql?(:selenium)
88
+ end
89
+
90
+ def firefox_browser?
91
+ self.browser.to_sym.eql?(:firefox)
92
+ end
93
+
94
+ def chrome_browser?
95
+ self.browser.to_sym.eql?(:chrome)
96
+ end
97
+
98
+ def development?
99
+ in_environment?(:development)
100
+ end
101
+
102
+ def in_environment?(env)
103
+ self.environment.eql?(env)
104
+ end
105
+
106
+ def terminate_session!(retries: 3)
107
+ self.session.driver.quit
108
+ self.session = nil
109
+ end
110
+
111
+ # User Agents
112
+ def set_user_agent(user_agent)
113
+ if !user_agent.to_s.empty?
114
+ self.user_agent = user_agent
115
+ else
116
+ case self.device
117
+ when :iphone
118
+ self.user_agent = Agents.random_user_agent(:phones, :iphone)
119
+ when :android_phone
120
+ self.user_agent = Agents.random_user_agent(:phones, :android)
121
+ when :ipad
122
+ self.user_agent = Agents.random_user_agent(:tablets, :ipad)
123
+ when :android_tablet
124
+ self.user_agent = Agents.random_user_agent(:tablets, :android)
125
+ else
126
+ self.user_agent = Agents.random_user_agent(self.device)
127
+ end
128
+ end
129
+ end
130
+
131
+ def runs_ios?
132
+ Agents.runs_ios?(self.user_agent)
133
+ end
134
+
135
+ def is_iphone?
136
+ Agents.is_iphone?(self.user_agent)
137
+ end
138
+
139
+ def is_ipad?
140
+ Agents.is_ipad?(self.user_agent)
141
+ end
142
+
143
+ # Resolution
144
+ def set_resolution(res)
145
+ self.resolution = res&.any? ? res : randomize_resolution
146
+ end
147
+
148
+ def randomize_resolution
149
+ runs_ios? ? randomize_ios_resolution : Browsed::Constants::RESOLUTIONS.fetch(self.device, :desktop).sample
150
+ end
151
+
152
+ def randomize_ios_resolution
153
+ resolution_device = case self.device
154
+ when :iphone, :android_phone
155
+ :phone
156
+ when :ipad, :android_tablet
157
+ :tablet
158
+ else
159
+ self.device
160
+ end
161
+
162
+ random_key = Browsed::Constants::RESOLUTIONS.fetch(resolution_device, :desktop).keys.sample
163
+ resolution = Browsed::Constants::RESOLUTIONS.fetch(resolution_device, :desktop)[random_key]
164
+ end
165
+
166
+ def wait_for_ajax
167
+ Timeout.timeout(Capybara.default_max_wait_time) do
168
+ loop until finished_all_ajax_requests?
169
+ end
170
+ end
171
+
172
+ def finished_all_ajax_requests?
173
+ evald = self.session.evaluate_script('jQuery.active')
174
+ evald.nil? || evald.zero?
175
+ end
176
+
177
+ def log(message)
178
+ puts "[Browsed::Client] - #{Time.now}: #{message}" if self.configuration.verbose?
179
+ end
180
+
181
+ end
182
+ end
@@ -0,0 +1,29 @@
1
+ module Browsed
2
+ class Configuration
3
+ attr_accessor :driver, :browser, :environment
4
+ attr_accessor :phantomjs_path, :download_path
5
+ attr_accessor :maximum_processes, :processes_max_ttl
6
+ attr_accessor :verbose
7
+
8
+ def initialize
9
+ self.driver = :poltergeist
10
+ self.browser = :phantomjs
11
+
12
+ self.environment = :production
13
+
14
+ self.phantomjs_path = "/usr/local/bin/phantomjs"
15
+
16
+ self.download_path = nil
17
+
18
+ self.maximum_processes = nil
19
+ self.processes_max_ttl = 60 * 30 # 30 minutes
20
+
21
+ self.verbose = false
22
+ end
23
+
24
+ def verbose?
25
+ self.verbose
26
+ end
27
+
28
+ end
29
+ end
@@ -0,0 +1,29 @@
1
+ module Browsed
2
+ class Constants
3
+
4
+ RESOLUTIONS = {
5
+ desktop: [
6
+ [1920, 1080], #17%
7
+ [1366, 768], #35%
8
+ [1280, 1024], #5%
9
+ [1280, 800], #4%
10
+ [1024, 768] #3%
11
+ ],
12
+
13
+ phone:
14
+ {
15
+ '4' => [320, 480],
16
+ '5' => [320, 568],
17
+ '6' => [375, 667],
18
+ '6+' => [414, 736]
19
+ },
20
+
21
+ tablet:
22
+ {
23
+ 'standard' => [1024, 768],
24
+ 'retina' => [2048, 1536]
25
+ }
26
+ }
27
+
28
+ end
29
+ end
@@ -0,0 +1,4 @@
1
+ module Browsed
2
+ class TooManyProcessesError < StandardError; end
3
+ class PotentiallyStaleProxyError < StandardError; end
4
+ end
@@ -0,0 +1,46 @@
1
+ module Browsed
2
+ module Firefox
3
+
4
+ private
5
+ def register_firefox_driver(driver_options: {}, timeout: 60, debug: false)
6
+ download_path = driver_options.fetch(:download_path, self.configuration.download_path)
7
+ enable_firebug = driver_options.fetch(:enable_firebug, false)
8
+
9
+ profile = Selenium::WebDriver::Firefox::Profile.new
10
+
11
+ profile.enable_firebug if enable_firebug
12
+
13
+ unless download_path.to_s.empty?
14
+ profile["browser.download.useDownloadDir"] = true
15
+ profile["browser.download.dir"] = download_directory
16
+ profile["browser.download.folderList"] = 2
17
+ profile["browser.helperApps.neverAsk.saveToDisk"] = "text/plain, application/vnd.ms-excel, text/csv, text/comma-separated-values, application/octet-stream, text/x-comma-separated-values, application/csv, application/x-filler"
18
+ profile["toolkit.telemetry.prompted"] = true
19
+ profile["pdfjs.disabled"] = true
20
+ end
21
+
22
+ profile["general.useragent.override"] = self.user_agent unless self.user_agent.to_s.empty?
23
+
24
+ profile = firefox_proxy_options(profile)
25
+ options = Selenium::WebDriver::Firefox::Options.new(profile: profile)
26
+
27
+ Capybara.register_driver self.driver do |app|
28
+ Capybara::Selenium::Driver.new(app, browser: :firefox, options: options)
29
+ end
30
+ end
31
+
32
+ def firefox_proxy_options(profile)
33
+ if self.proxy && !self.proxy.empty? && self.proxy.has_key?(:host) && self.proxy.has_key?(:port)
34
+ profile.proxy = Selenium::WebDriver::Proxy.new(
35
+ http: "#{self.proxy.fetch(:host)}:#{self.proxy.fetch(:port)}",
36
+ ssl: "#{self.proxy.fetch(:host)}:#{self.proxy.fetch(:port)}"
37
+ )
38
+
39
+ log("Will use proxy #{self.proxy.fetch(:host)}:#{self.proxy.fetch(:port)} to initiate the request.")
40
+ end
41
+
42
+ return profile
43
+ end
44
+
45
+ end
46
+ end
@@ -0,0 +1,98 @@
1
+ module Browsed
2
+ class Manager
3
+ attr_accessor :command, :kill_signal
4
+
5
+ def initialize(browser: :phantomjs, kill_signal: 9)
6
+ case browser
7
+ when :phantomjs
8
+ self.command = "ps -ef | grep /[p]hantomjs"
9
+ when :firefox
10
+ self.command = "ps -ef | grep /[g]eckodriver"
11
+ when :chrome
12
+ self.command = "ps -ef | grep /[c]hromedriver"
13
+ else
14
+ self.command = "ps -ef | grep /[p]hantomjs"
15
+ end
16
+
17
+ self.kill_signal = kill_signal
18
+ end
19
+
20
+ def can_start_more_processes?(max_count: 5)
21
+ return get_current_processes.size < max_count
22
+ end
23
+
24
+ def reap_stale_processes(started_after: ::Browsed.configuration.processes_max_ttl.call)
25
+ processes = get_current_processes
26
+
27
+ processes.each do |process|
28
+ if (process[:date] && process[:date] < (Time.now - started_after))
29
+ info "[Browsed::Manager] - #{Time.now.to_s(:db)}: Process with PID #{process[:pid]} was started before #{started_after.to_s(:db)}. Process should be terminated."
30
+ kill_process(process)
31
+ end
32
+ end if processes && processes.any?
33
+ end
34
+
35
+ def kill_process(process)
36
+ info "[Browsed::Manager] - #{Time.now.to_s(:db)}: Killing process with PID #{process[:pid]} matching command #{self.command}."
37
+
38
+ begin
39
+ ::Process.kill(self.kill_signal, process[:pid])
40
+
41
+ rescue StandardError => e
42
+ info "[Browsed::Manager] - #{Time.now.to_s(:db)}: Failed to kill process with pid '#{process[:pid]}'. Error Class: #{e.class.name}. Error Message: #{e.message}"
43
+ end
44
+ end
45
+
46
+ def get_current_processes
47
+ processes = []
48
+ procs = `#{self.command}`.split("\n")
49
+
50
+ procs.each do |process_data|
51
+ process = parse_process(process_data)
52
+ processes << process
53
+ end if procs && procs.any?
54
+
55
+ return processes
56
+ end
57
+
58
+ private
59
+
60
+ def parse_process(process_data)
61
+ process = {}
62
+
63
+ parts = process_data.split(' ')
64
+ pid = parts[1].to_i
65
+ started = parts[4].to_s
66
+ date = parse_date(started)
67
+
68
+ process[:pid] = pid
69
+ process[:started] = started
70
+ process[:date] = date
71
+
72
+ info "[Browsed::Manager] - #{Time.now.to_s(:db)}: Pid: #{pid}. Started: #{started}. Date: #{date}.\n"
73
+
74
+ return process
75
+ end
76
+
77
+ def parse_date(date, retries = 3)
78
+ begin
79
+ if (!(date =~ /^[a-z]{3,4}\d*/i).nil?) #Sep16
80
+ parsed_date = DateTime.strptime(date, "%b%d")
81
+
82
+ elsif (!(date =~ /^\d*:\d*/i).nil?) #11:34
83
+ parsed_date = Time.strptime(date, "%H:%M").to_datetime
84
+ end
85
+
86
+ rescue StandardError => e
87
+ info "[Browsed::Manager] - #{Time.now.to_s(:db)}: Exception occurred while trying to parse date/time string '#{date}'. Error Class: #{e.class.name}. Error: #{e.message}."
88
+ retries -= 1
89
+ retry if retries > 0
90
+ end
91
+ end
92
+
93
+ def info(message, enabled = true)
94
+ puts message if enabled
95
+ end
96
+
97
+ end
98
+ end
@@ -0,0 +1,57 @@
1
+ module Browsed
2
+ module Poltergeist
3
+
4
+ private
5
+ def register_poltergeist_driver(driver_options: {}, timeout: 60, debug: false)
6
+ phantom_opts = ['--ignore-ssl-errors=true', '--ssl-protocol=any']
7
+ disable_images = driver_options.fetch(:disable_images, false)
8
+
9
+ if disable_images
10
+ phantom_opts << "--load-images=false"
11
+ end
12
+
13
+ phantom_opts = phantom_opts | poltergeist_proxy_options
14
+
15
+ options = {
16
+ timeout: timeout,
17
+ js_errors: false,
18
+ debug: debug,
19
+ phantomjs_options: phantom_opts
20
+ }
21
+
22
+ options[:phantomjs] = self.configuration.phantomjs_path if in_environment?(:production)
23
+ options[:window_size] = self.resolution if self.resolution&.any?
24
+
25
+ headers = {}
26
+ headers['User-Agent'] = self.user_agent unless self.user_agent.to_s.empty?
27
+
28
+ log("Will register a new poltergeist driver:\nOptions: #{options.inspect}\nHeaders: #{headers.inspect}\n")
29
+
30
+ Capybara.register_driver self.driver do |app|
31
+ poltergeist = Capybara::Poltergeist::Driver.new(app, options)
32
+ poltergeist.headers = headers
33
+ poltergeist
34
+ end
35
+ end
36
+
37
+ def poltergeist_proxy_options
38
+ proxy_options = []
39
+
40
+ if self.proxy && !self.proxy.empty? && self.proxy.has_key?(:host) && self.proxy.has_key?(:port)
41
+ proxy_address = "#{self.proxy.fetch(:host)}:#{self.proxy.fetch(:port)}"
42
+
43
+ proxy_options << "--proxy=#{proxy_address}" if !proxy_address.to_s.empty?
44
+
45
+ if !self.proxy.fetch(:username, nil).to_s.empty? && !self.proxy.fetch(:password, nil).to_s.empty?
46
+ credentials = "#{self.proxy.fetch(:username)}:#{self.proxy.fetch(:password)}"
47
+ proxy_options << "--proxy-auth=#{credentials}"
48
+ end
49
+
50
+ log("Will use proxy options #{proxy_options} to initiate the request.")
51
+ end
52
+
53
+ return proxy_options
54
+ end
55
+
56
+ end
57
+ end
@@ -0,0 +1,3 @@
1
+ module Browsed
2
+ VERSION = "0.1.0"
3
+ end
metadata ADDED
@@ -0,0 +1,217 @@
1
+ --- !ruby/object:Gem::Specification
2
+ name: browsed
3
+ version: !ruby/object:Gem::Version
4
+ version: 0.1.0
5
+ platform: ruby
6
+ authors:
7
+ - Sebastian
8
+ autorequire:
9
+ bindir: exe
10
+ cert_chain: []
11
+ date: 2018-07-25 00:00:00.000000000 Z
12
+ dependencies:
13
+ - !ruby/object:Gem::Dependency
14
+ name: capybara
15
+ requirement: !ruby/object:Gem::Requirement
16
+ requirements:
17
+ - - "~>"
18
+ - !ruby/object:Gem::Version
19
+ version: '3.4'
20
+ - - ">="
21
+ - !ruby/object:Gem::Version
22
+ version: 3.4.1
23
+ type: :runtime
24
+ prerelease: false
25
+ version_requirements: !ruby/object:Gem::Requirement
26
+ requirements:
27
+ - - "~>"
28
+ - !ruby/object:Gem::Version
29
+ version: '3.4'
30
+ - - ">="
31
+ - !ruby/object:Gem::Version
32
+ version: 3.4.1
33
+ - !ruby/object:Gem::Dependency
34
+ name: poltergeist
35
+ requirement: !ruby/object:Gem::Requirement
36
+ requirements:
37
+ - - "~>"
38
+ - !ruby/object:Gem::Version
39
+ version: '1.18'
40
+ - - ">="
41
+ - !ruby/object:Gem::Version
42
+ version: 1.18.1
43
+ type: :runtime
44
+ prerelease: false
45
+ version_requirements: !ruby/object:Gem::Requirement
46
+ requirements:
47
+ - - "~>"
48
+ - !ruby/object:Gem::Version
49
+ version: '1.18'
50
+ - - ">="
51
+ - !ruby/object:Gem::Version
52
+ version: 1.18.1
53
+ - !ruby/object:Gem::Dependency
54
+ name: selenium-webdriver
55
+ requirement: !ruby/object:Gem::Requirement
56
+ requirements:
57
+ - - "~>"
58
+ - !ruby/object:Gem::Version
59
+ version: '3.13'
60
+ - - ">="
61
+ - !ruby/object:Gem::Version
62
+ version: 3.13.1
63
+ type: :runtime
64
+ prerelease: false
65
+ version_requirements: !ruby/object:Gem::Requirement
66
+ requirements:
67
+ - - "~>"
68
+ - !ruby/object:Gem::Version
69
+ version: '3.13'
70
+ - - ">="
71
+ - !ruby/object:Gem::Version
72
+ version: 3.13.1
73
+ - !ruby/object:Gem::Dependency
74
+ name: agents
75
+ requirement: !ruby/object:Gem::Requirement
76
+ requirements:
77
+ - - ">="
78
+ - !ruby/object:Gem::Version
79
+ version: '0'
80
+ type: :runtime
81
+ prerelease: false
82
+ version_requirements: !ruby/object:Gem::Requirement
83
+ requirements:
84
+ - - ">="
85
+ - !ruby/object:Gem::Version
86
+ version: '0'
87
+ - !ruby/object:Gem::Dependency
88
+ name: bundler
89
+ requirement: !ruby/object:Gem::Requirement
90
+ requirements:
91
+ - - "~>"
92
+ - !ruby/object:Gem::Version
93
+ version: '1.16'
94
+ type: :development
95
+ prerelease: false
96
+ version_requirements: !ruby/object:Gem::Requirement
97
+ requirements:
98
+ - - "~>"
99
+ - !ruby/object:Gem::Version
100
+ version: '1.16'
101
+ - !ruby/object:Gem::Dependency
102
+ name: rake
103
+ requirement: !ruby/object:Gem::Requirement
104
+ requirements:
105
+ - - "~>"
106
+ - !ruby/object:Gem::Version
107
+ version: '10.0'
108
+ type: :development
109
+ prerelease: false
110
+ version_requirements: !ruby/object:Gem::Requirement
111
+ requirements:
112
+ - - "~>"
113
+ - !ruby/object:Gem::Version
114
+ version: '10.0'
115
+ - !ruby/object:Gem::Dependency
116
+ name: rspec
117
+ requirement: !ruby/object:Gem::Requirement
118
+ requirements:
119
+ - - "~>"
120
+ - !ruby/object:Gem::Version
121
+ version: '3.0'
122
+ type: :development
123
+ prerelease: false
124
+ version_requirements: !ruby/object:Gem::Requirement
125
+ requirements:
126
+ - - "~>"
127
+ - !ruby/object:Gem::Version
128
+ version: '3.0'
129
+ - !ruby/object:Gem::Dependency
130
+ name: pry
131
+ requirement: !ruby/object:Gem::Requirement
132
+ requirements:
133
+ - - "~>"
134
+ - !ruby/object:Gem::Version
135
+ version: 0.11.3
136
+ type: :development
137
+ prerelease: false
138
+ version_requirements: !ruby/object:Gem::Requirement
139
+ requirements:
140
+ - - "~>"
141
+ - !ruby/object:Gem::Version
142
+ version: 0.11.3
143
+ - !ruby/object:Gem::Dependency
144
+ name: launchy
145
+ requirement: !ruby/object:Gem::Requirement
146
+ requirements:
147
+ - - "~>"
148
+ - !ruby/object:Gem::Version
149
+ version: '2.4'
150
+ - - ">="
151
+ - !ruby/object:Gem::Version
152
+ version: 2.4.3
153
+ type: :development
154
+ prerelease: false
155
+ version_requirements: !ruby/object:Gem::Requirement
156
+ requirements:
157
+ - - "~>"
158
+ - !ruby/object:Gem::Version
159
+ version: '2.4'
160
+ - - ">="
161
+ - !ruby/object:Gem::Version
162
+ version: 2.4.3
163
+ description: A lightweight framework for making it easier to work with Capybara/PhantomJS/Selenium
164
+ email:
165
+ - sebastian.johnsson@gmail.com
166
+ executables: []
167
+ extensions: []
168
+ extra_rdoc_files: []
169
+ files:
170
+ - ".gitignore"
171
+ - ".rspec"
172
+ - ".travis.yml"
173
+ - CODE_OF_CONDUCT.md
174
+ - Gemfile
175
+ - Gemfile.lock
176
+ - LICENSE.txt
177
+ - README.md
178
+ - Rakefile
179
+ - bin/browse
180
+ - bin/console
181
+ - bin/setup
182
+ - browsed.gemspec
183
+ - lib/browsed.rb
184
+ - lib/browsed/chrome.rb
185
+ - lib/browsed/client.rb
186
+ - lib/browsed/configuration.rb
187
+ - lib/browsed/constants.rb
188
+ - lib/browsed/errors.rb
189
+ - lib/browsed/firefox.rb
190
+ - lib/browsed/manager.rb
191
+ - lib/browsed/poltergeist.rb
192
+ - lib/browsed/version.rb
193
+ homepage: https://github.com/SebastianJ
194
+ licenses:
195
+ - MIT
196
+ metadata: {}
197
+ post_install_message:
198
+ rdoc_options: []
199
+ require_paths:
200
+ - lib
201
+ required_ruby_version: !ruby/object:Gem::Requirement
202
+ requirements:
203
+ - - ">="
204
+ - !ruby/object:Gem::Version
205
+ version: '0'
206
+ required_rubygems_version: !ruby/object:Gem::Requirement
207
+ requirements:
208
+ - - ">="
209
+ - !ruby/object:Gem::Version
210
+ version: '0'
211
+ requirements: []
212
+ rubyforge_project:
213
+ rubygems_version: 2.7.7
214
+ signing_key:
215
+ specification_version: 4
216
+ summary: 'Browsed: Lightweight Capybara/PhantomJS/Selenium framework'
217
+ test_files: []