moby.rb 1.0.1 → 2.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +4 -4
- data/CHANGELOG +31 -0
- data/Gemfile +4 -0
- data/bin/moby +24 -27
- data/lib/Moby/Backends/MechanizeBackend.rb +157 -0
- data/lib/Moby/Backends/SeleniumBackend.rb +242 -0
- data/lib/Moby/Backends.rb +20 -0
- data/lib/Moby/Configuration.rb +119 -0
- data/lib/Moby/RandomInputGenerator.rb +49 -0
- data/lib/Moby/VERSION.rb +1 -1
- data/lib/Moby.rb +15 -160
- data/lib/Selenium/WebDriver/Driver/Attempt/attempt.rb +18 -0
- data/lib/Selenium/WebDriver/SearchContext/ElementPresentQ/element_presentQ.rb +13 -0
- data/lib/Thoran/Selenium/WebDriver/Driver/Attempt/attempt.rb +53 -0
- data/lib/Thoran/Selenium/WebDriver/SearchContext/ElementPresentQ/element_presentQ.rb +39 -0
- data/moby.rb.gemspec +2 -1
- data/test/Moby/Backends/MechanizeBackend_test.rb +83 -0
- data/test/Moby/Backends/SeleniumBackend_test.rb +44 -0
- data/test/Moby/Backends_test.rb +27 -0
- data/test/Moby/Configuration_test.rb +134 -0
- data/test/Moby/RandomInputGenerator_test.rb +37 -0
- data/test/Moby/VERSION_test.rb +2 -2
- data/test/moby_test.rb +6 -282
- data/test/test_helper.rb +15 -0
- metadata +31 -5
- data/lib/File/self.collect.rb +0 -10
- data/lib/Thoran/File/SelfCollect/self.collect.rb +0 -22
|
@@ -0,0 +1,119 @@
|
|
|
1
|
+
# lib/Moby/Configuration.rb
|
|
2
|
+
# Moby::Configuration
|
|
3
|
+
|
|
4
|
+
require 'mechanize'
|
|
5
|
+
|
|
6
|
+
class Moby
|
|
7
|
+
class Configuration
|
|
8
|
+
AVAILABLE_USER_AGENTS = (Mechanize::AGENT_ALIASES.keys - ['Mechanize']).freeze
|
|
9
|
+
|
|
10
|
+
attr_accessor(
|
|
11
|
+
:url,
|
|
12
|
+
:backend,
|
|
13
|
+
:browser,
|
|
14
|
+
:debug,
|
|
15
|
+
:verbose,
|
|
16
|
+
:form_name,
|
|
17
|
+
:form_number,
|
|
18
|
+
:username_field_name,
|
|
19
|
+
:username_field_number,
|
|
20
|
+
:password_field_name,
|
|
21
|
+
:password_field_number,
|
|
22
|
+
:username_hostname,
|
|
23
|
+
:username_is_email_address,
|
|
24
|
+
:username_prefilled,
|
|
25
|
+
:iterations,
|
|
26
|
+
:delay,
|
|
27
|
+
:word_list_path
|
|
28
|
+
)
|
|
29
|
+
|
|
30
|
+
attr_writer(
|
|
31
|
+
:user_agent,
|
|
32
|
+
)
|
|
33
|
+
|
|
34
|
+
def user_agent
|
|
35
|
+
@user_agent || random_user_agent
|
|
36
|
+
end
|
|
37
|
+
|
|
38
|
+
def using_form_name?
|
|
39
|
+
!@form_name.nil?
|
|
40
|
+
end
|
|
41
|
+
|
|
42
|
+
def using_username_field_name?
|
|
43
|
+
!@username_field_name.nil?
|
|
44
|
+
end
|
|
45
|
+
|
|
46
|
+
def using_password_field_name?
|
|
47
|
+
!@password_field_name.nil?
|
|
48
|
+
end
|
|
49
|
+
|
|
50
|
+
def mechanize?
|
|
51
|
+
@backend == :mechanize
|
|
52
|
+
end
|
|
53
|
+
|
|
54
|
+
def selenium?
|
|
55
|
+
@backend == :selenium
|
|
56
|
+
end
|
|
57
|
+
|
|
58
|
+
def username_is_email_address?
|
|
59
|
+
@username_is_email_address || @username_hostname || @username_field_name == 'email'
|
|
60
|
+
end
|
|
61
|
+
|
|
62
|
+
def username_prefilled?
|
|
63
|
+
@username_prefilled
|
|
64
|
+
end
|
|
65
|
+
|
|
66
|
+
def debug?
|
|
67
|
+
@debug
|
|
68
|
+
end
|
|
69
|
+
|
|
70
|
+
def verbose?
|
|
71
|
+
@verbose
|
|
72
|
+
end
|
|
73
|
+
|
|
74
|
+
def fixed_number_of_submissions?
|
|
75
|
+
!!@iterations
|
|
76
|
+
end
|
|
77
|
+
|
|
78
|
+
private
|
|
79
|
+
|
|
80
|
+
# @param url [String] The target URL
|
|
81
|
+
# @param backend [Symbol] :mechanize or :selenium (default: :mechanize)
|
|
82
|
+
# @param browser [Symbol] :chrome or :firefox (default: :chrome)
|
|
83
|
+
|
|
84
|
+
def initialize(url:, backend: :mechanize, browser: :chrome, **args)
|
|
85
|
+
@url = url
|
|
86
|
+
@backend = (backend || :mechanize).to_sym
|
|
87
|
+
|
|
88
|
+
# Browser configuration (for Selenium)
|
|
89
|
+
@browser = (browser || :chrome).to_sym
|
|
90
|
+
@user_agent = args[:user_agent]
|
|
91
|
+
|
|
92
|
+
# Form identification
|
|
93
|
+
@form_name = args[:form_name]
|
|
94
|
+
@form_number = (args[:form_number] || 0).to_i
|
|
95
|
+
|
|
96
|
+
# Field identification
|
|
97
|
+
@username_field_name = args[:username_field_name] || 'username'
|
|
98
|
+
@username_field_number = (args[:username_field_number] || 0).to_i
|
|
99
|
+
@password_field_name = args[:password_field_name] || 'password'
|
|
100
|
+
@password_field_number = (args[:password_field_number] || 1).to_i
|
|
101
|
+
|
|
102
|
+
# Username configuration
|
|
103
|
+
@username_hostname = args[:username_hostname]
|
|
104
|
+
@username_is_email_address = args[:username_is_email_address] || false
|
|
105
|
+
@username_prefilled = args[:username_prefilled] || false
|
|
106
|
+
|
|
107
|
+
# Runtime configuration
|
|
108
|
+
@debug = args[:debug] || false
|
|
109
|
+
@verbose = args[:verbose] || false
|
|
110
|
+
@iterations = args[:iterations] && args[:iterations].to_i
|
|
111
|
+
@delay = (args[:delay] || 0).to_i
|
|
112
|
+
@word_list_path = args[:word_list_path] || '/usr/share/dict/words'
|
|
113
|
+
end
|
|
114
|
+
|
|
115
|
+
def random_user_agent
|
|
116
|
+
AVAILABLE_USER_AGENTS.sample
|
|
117
|
+
end
|
|
118
|
+
end
|
|
119
|
+
end
|
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
# lib/Moby/RandomInputGenerator.rb
|
|
2
|
+
# Moby::RandomInputGenerator
|
|
3
|
+
|
|
4
|
+
class Moby
|
|
5
|
+
class RandomInputGenerator
|
|
6
|
+
TLD = %w{com net org edu int mil gov arpa biz aero name coop info pro museum}
|
|
7
|
+
|
|
8
|
+
def random_username
|
|
9
|
+
if username_is_email_address?
|
|
10
|
+
if @username_hostname
|
|
11
|
+
"#{random_word}@#{@username_hostname}"
|
|
12
|
+
else
|
|
13
|
+
"#{random_word}@#{random_word}.#{random_TLD}"
|
|
14
|
+
end
|
|
15
|
+
else
|
|
16
|
+
random_word
|
|
17
|
+
end
|
|
18
|
+
end
|
|
19
|
+
|
|
20
|
+
def random_password
|
|
21
|
+
random_word
|
|
22
|
+
end
|
|
23
|
+
|
|
24
|
+
private
|
|
25
|
+
|
|
26
|
+
def initialize(word_list_path:, username_hostname: nil, username_is_email_address: false)
|
|
27
|
+
@word_list_path = word_list_path
|
|
28
|
+
@username_hostname = username_hostname
|
|
29
|
+
@username_is_email_address = username_is_email_address
|
|
30
|
+
end
|
|
31
|
+
|
|
32
|
+
def random_word
|
|
33
|
+
words.sample
|
|
34
|
+
end
|
|
35
|
+
|
|
36
|
+
def words
|
|
37
|
+
@words ||= File.readlines(@word_list_path).map(&:chomp).reject(&:empty?)
|
|
38
|
+
end
|
|
39
|
+
alias_method :load_words, :words
|
|
40
|
+
|
|
41
|
+
def random_TLD
|
|
42
|
+
TLD.sample
|
|
43
|
+
end
|
|
44
|
+
|
|
45
|
+
def username_is_email_address?
|
|
46
|
+
@username_is_email_address
|
|
47
|
+
end
|
|
48
|
+
end
|
|
49
|
+
end
|
data/lib/Moby/VERSION.rb
CHANGED
data/lib/Moby.rb
CHANGED
|
@@ -1,179 +1,34 @@
|
|
|
1
1
|
# Moby.rb
|
|
2
2
|
# Moby
|
|
3
3
|
|
|
4
|
-
require 'mechanize'
|
|
5
|
-
require 'pp'
|
|
6
|
-
|
|
7
|
-
require_relative './File/self.collect'
|
|
8
4
|
require_relative './Moby/VERSION'
|
|
5
|
+
require_relative './Moby/Configuration'
|
|
6
|
+
require_relative './Moby/RandomInputGenerator'
|
|
7
|
+
require_relative './Moby/Backends'
|
|
9
8
|
|
|
10
9
|
class Moby
|
|
11
|
-
TLD = %w{com net org edu int mil gov arpa biz aero name coop info pro museum}
|
|
12
|
-
|
|
13
|
-
attr_accessor\
|
|
14
|
-
:debug,
|
|
15
|
-
:form_name,
|
|
16
|
-
:form_number,
|
|
17
|
-
:password_field_name,
|
|
18
|
-
:password_field_number,
|
|
19
|
-
:url,
|
|
20
|
-
:username_field_name,
|
|
21
|
-
:username_field_number,
|
|
22
|
-
:username_hostname,
|
|
23
|
-
:username_is_email_address,
|
|
24
|
-
:verbose
|
|
25
|
-
|
|
26
|
-
attr_writer\
|
|
27
|
-
:user_agent
|
|
28
|
-
|
|
29
|
-
def initialize(
|
|
30
|
-
debug: false,
|
|
31
|
-
form_name: nil,
|
|
32
|
-
form_number: 0,
|
|
33
|
-
password_field_name: 'password',
|
|
34
|
-
password_field_number: 1,
|
|
35
|
-
url: nil,
|
|
36
|
-
user_agent: nil,
|
|
37
|
-
username_field_name: 'username',
|
|
38
|
-
username_field_number: 0,
|
|
39
|
-
username_hostname: nil,
|
|
40
|
-
username_is_email_address: false,
|
|
41
|
-
verbose: false
|
|
42
|
-
)
|
|
43
|
-
@debug = debug
|
|
44
|
-
@form_name = form_name
|
|
45
|
-
@form_number = form_number.to_i
|
|
46
|
-
@password_field_name = password_field_name
|
|
47
|
-
@password_field_number = password_field_number.to_i
|
|
48
|
-
@url = url
|
|
49
|
-
@user_agent = user_agent
|
|
50
|
-
@username_field_name = username_field_name
|
|
51
|
-
@username_field_number = username_field_number.to_i
|
|
52
|
-
@username_hostname = username_hostname
|
|
53
|
-
@username_is_email_address = username_is_email_address
|
|
54
|
-
@verbose = verbose
|
|
55
|
-
end
|
|
56
|
-
|
|
57
10
|
def counter_phish
|
|
58
|
-
|
|
59
|
-
puts "Moby session begun #{start_time}."
|
|
60
|
-
submission_count = 0
|
|
61
|
-
begin
|
|
62
|
-
loop do
|
|
63
|
-
mechanize.user_agent_alias = user_agent
|
|
64
|
-
username_field.value = username
|
|
65
|
-
password_field.value = password
|
|
66
|
-
pp page if @debug
|
|
67
|
-
result = mechanize.submit(form)
|
|
68
|
-
pp result if @debug
|
|
69
|
-
submission_count += 1
|
|
70
|
-
puts "#{submission_count} #{username}:#{password}" if @verbose
|
|
71
|
-
rescue Net::HTTP::Persistent::Error
|
|
72
|
-
puts "\n\nNet::HTTP::Persistent::Error rescued.\n\n" if @verbose
|
|
73
|
-
rescue Net::OpenTimeout
|
|
74
|
-
puts "\n\nNet::OpenTimeout rescued.\n\n" if @verbose
|
|
75
|
-
end
|
|
76
|
-
ensure
|
|
77
|
-
finish_time = Time.now
|
|
78
|
-
time_delta_in_minutes = (finish_time - start_time) / 60
|
|
79
|
-
submissions_per_minute = submission_count / time_delta_in_minutes
|
|
80
|
-
puts "Moby session terminated #{finish_time} with #{submission_count} counter-phishes served in #{time_delta_in_minutes} minutes for an average of #{submissions_per_minute} submissions per minute."
|
|
81
|
-
end
|
|
11
|
+
backend.counter_phish
|
|
82
12
|
end
|
|
83
13
|
|
|
84
14
|
private
|
|
85
15
|
|
|
86
|
-
def
|
|
87
|
-
@
|
|
88
|
-
end
|
|
89
|
-
|
|
90
|
-
def words(filename = '/usr/share/dict/words')
|
|
91
|
-
@words ||= File.collect(filename){|line| line.chomp}
|
|
92
|
-
end
|
|
93
|
-
|
|
94
|
-
def random_word
|
|
95
|
-
words[rand(words.size)]
|
|
96
|
-
end
|
|
97
|
-
|
|
98
|
-
def random_TLD
|
|
99
|
-
TLD[rand(TLD.size)]
|
|
100
|
-
end
|
|
101
|
-
|
|
102
|
-
def random_user_agent
|
|
103
|
-
agent_aliases = Mechanize::AGENT_ALIASES
|
|
104
|
-
agent_aliases.delete('Mechanize')
|
|
105
|
-
agent_aliases_keys = agent_aliases.keys
|
|
106
|
-
agent_aliases_keys[rand(agent_aliases.size)]
|
|
16
|
+
def initialize(**args)
|
|
17
|
+
@configuration = Configuration.new(**args)
|
|
107
18
|
end
|
|
108
19
|
|
|
109
|
-
def
|
|
110
|
-
@
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
if md
|
|
114
|
-
puts 'meta_refresh_url found' if @debug
|
|
115
|
-
page = mechanize.get(md[1])
|
|
116
|
-
end
|
|
117
|
-
pp page if @debug
|
|
118
|
-
page
|
|
20
|
+
def backend
|
|
21
|
+
@backend ||= Backends.for(
|
|
22
|
+
config: @configuration,
|
|
23
|
+
random_input_generator: random_input_generator
|
|
119
24
|
)
|
|
120
25
|
end
|
|
121
26
|
|
|
122
|
-
def
|
|
123
|
-
@
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
page.forms[@form_number]
|
|
128
|
-
else
|
|
129
|
-
page.forms[0]
|
|
130
|
-
end
|
|
27
|
+
def random_input_generator
|
|
28
|
+
@random_input_generator ||= RandomInputGenerator.new(
|
|
29
|
+
word_list_path: @configuration.word_list_path,
|
|
30
|
+
username_hostname: @configuration.username_hostname,
|
|
31
|
+
username_is_email_address: @configuration.username_is_email_address?
|
|
131
32
|
)
|
|
132
33
|
end
|
|
133
|
-
|
|
134
|
-
def username
|
|
135
|
-
if username_is_email_address?
|
|
136
|
-
if username_hostname
|
|
137
|
-
"#{random_word}@#{@username_hostname}"
|
|
138
|
-
else
|
|
139
|
-
"#{random_word}@#{random_word}.#{random_TLD}"
|
|
140
|
-
end
|
|
141
|
-
else
|
|
142
|
-
random_word
|
|
143
|
-
end
|
|
144
|
-
end
|
|
145
|
-
|
|
146
|
-
def password
|
|
147
|
-
random_word
|
|
148
|
-
end
|
|
149
|
-
|
|
150
|
-
def user_agent
|
|
151
|
-
@user_agent || random_user_agent
|
|
152
|
-
end
|
|
153
|
-
|
|
154
|
-
def username_field
|
|
155
|
-
if @username_field_name
|
|
156
|
-
form.field(@username_field_name)
|
|
157
|
-
elsif @username_field_number
|
|
158
|
-
form.fields[@username_field_number]
|
|
159
|
-
else
|
|
160
|
-
form.fields[0]
|
|
161
|
-
end
|
|
162
|
-
end
|
|
163
|
-
|
|
164
|
-
def password_field
|
|
165
|
-
if @password_field_name
|
|
166
|
-
form.field(@password_field_name)
|
|
167
|
-
elsif @password_field_number
|
|
168
|
-
form.fields[@password_field_number]
|
|
169
|
-
else
|
|
170
|
-
form.fields[1]
|
|
171
|
-
end
|
|
172
|
-
end
|
|
173
|
-
|
|
174
|
-
# predicate methods
|
|
175
|
-
|
|
176
|
-
def username_is_email_address?
|
|
177
|
-
@username_is_email_address || @username_hostname || username_field.name == 'email'
|
|
178
|
-
end
|
|
179
34
|
end
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
# Selenium/WebDriver/Driver/Attempt/attempt.rb
|
|
2
|
+
# Selenium::WebDriver::Driver::Attempt.attempt
|
|
3
|
+
|
|
4
|
+
# 20200418
|
|
5
|
+
# 0.3.0
|
|
6
|
+
|
|
7
|
+
# Examples:
|
|
8
|
+
# 1. driver.attempt do |driver|
|
|
9
|
+
# driver.get('https://example.com/users/sign_in')
|
|
10
|
+
# enter_username
|
|
11
|
+
# enter_password
|
|
12
|
+
# sign_in
|
|
13
|
+
# end
|
|
14
|
+
|
|
15
|
+
# Changes:
|
|
16
|
+
# 1. Moved all the logic into the Thoran namespace.
|
|
17
|
+
|
|
18
|
+
require 'Thoran/Selenium/WebDriver/Driver/Attempt/attempt'
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
# Selenium/WebDriver/SearchContext/ElementPresentQ/element_presentQ.rb
|
|
2
|
+
# Selenium::WebDriver::SearchContext::ElementPresentQ.element_present?
|
|
3
|
+
|
|
4
|
+
# 20200418
|
|
5
|
+
# 0.3.0
|
|
6
|
+
|
|
7
|
+
# Examples:
|
|
8
|
+
# 1. driver.element_present?(:xpath, '//a[@id="submit"]')
|
|
9
|
+
|
|
10
|
+
# Changes:
|
|
11
|
+
# 1. Moved all the logic into the Thoran namespace.
|
|
12
|
+
|
|
13
|
+
require 'Thoran/Selenium/WebDriver/SearchContext/ElementPresentQ/element_presentQ'
|
|
@@ -0,0 +1,53 @@
|
|
|
1
|
+
# Thoran/Selenium/WebDriver/Attempt/attempt.rb
|
|
2
|
+
# Thoran::Selenium::WebDriver::Attempt.attempt
|
|
3
|
+
|
|
4
|
+
# 20200419
|
|
5
|
+
# 0.0.1
|
|
6
|
+
|
|
7
|
+
# Examples:
|
|
8
|
+
# 1. driver.attempt do |driver|
|
|
9
|
+
# driver.get('https://example.com/users/sign_in')
|
|
10
|
+
# enter_username
|
|
11
|
+
# enter_password
|
|
12
|
+
# sign_in
|
|
13
|
+
# end
|
|
14
|
+
|
|
15
|
+
# Changes:
|
|
16
|
+
# 1. + Thoran namespace.
|
|
17
|
+
|
|
18
|
+
module Thoran
|
|
19
|
+
module Selenium
|
|
20
|
+
module WebDriver
|
|
21
|
+
module Driver
|
|
22
|
+
module Attempt
|
|
23
|
+
|
|
24
|
+
def attempt(max_attempts = 3, &block)
|
|
25
|
+
attempts = 0
|
|
26
|
+
loop do
|
|
27
|
+
begin
|
|
28
|
+
yield
|
|
29
|
+
break
|
|
30
|
+
rescue Timeout::Error, ::Selenium::WebDriver::Error::UnknownError, ::Selenium::WebDriver::Error::NoSuchElementError => e
|
|
31
|
+
attempts += 1
|
|
32
|
+
if attempts >= max_attempts
|
|
33
|
+
@driver.quit
|
|
34
|
+
puts "Giving up after #{max_attempts} attempts to #{__method__} because #{e}."
|
|
35
|
+
exit
|
|
36
|
+
end
|
|
37
|
+
end
|
|
38
|
+
end
|
|
39
|
+
end
|
|
40
|
+
|
|
41
|
+
end
|
|
42
|
+
end
|
|
43
|
+
end
|
|
44
|
+
end
|
|
45
|
+
end
|
|
46
|
+
|
|
47
|
+
module Selenium
|
|
48
|
+
module WebDriver
|
|
49
|
+
class Driver
|
|
50
|
+
include Thoran::Selenium::WebDriver::Driver::Attempt
|
|
51
|
+
end
|
|
52
|
+
end
|
|
53
|
+
end
|
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
# Thoran/Selenium/WebDriver/SearchContext/ElementPresentQ/element_presentQ.rb
|
|
2
|
+
# Thoran::Selenium::WebDriver::SearchContext::ElementPresentQ.element_present?
|
|
3
|
+
|
|
4
|
+
# 20200418
|
|
5
|
+
# 0.0.0
|
|
6
|
+
|
|
7
|
+
# Examples:
|
|
8
|
+
# 1. driver.element_present?(:xpath, '//a[@id="submit"]')
|
|
9
|
+
# 2. driver.element_present?(:id, @logout_button_id)
|
|
10
|
+
|
|
11
|
+
# Changes:
|
|
12
|
+
# 1. + Thoran namespace.
|
|
13
|
+
|
|
14
|
+
module Thoran
|
|
15
|
+
module Selenium
|
|
16
|
+
module WebDriver
|
|
17
|
+
module SearchContext
|
|
18
|
+
module ElementPresentQ
|
|
19
|
+
|
|
20
|
+
def element_present?(selector_type, selector)
|
|
21
|
+
bridge.find_element_by(selector_type, selector)
|
|
22
|
+
true
|
|
23
|
+
rescue ::Selenium::WebDriver::Error::NoSuchElementError
|
|
24
|
+
false
|
|
25
|
+
end
|
|
26
|
+
|
|
27
|
+
end
|
|
28
|
+
end
|
|
29
|
+
end
|
|
30
|
+
end
|
|
31
|
+
end
|
|
32
|
+
|
|
33
|
+
module Selenium
|
|
34
|
+
module WebDriver
|
|
35
|
+
class Driver
|
|
36
|
+
include Thoran::Selenium::WebDriver::SearchContext::ElementPresentQ
|
|
37
|
+
end
|
|
38
|
+
end
|
|
39
|
+
end
|
data/moby.rb.gemspec
CHANGED
|
@@ -16,9 +16,10 @@ Gem::Specification.new do |spec|
|
|
|
16
16
|
spec.homepage = 'https://github.com/thoran/moby'
|
|
17
17
|
spec.license = 'MIT'
|
|
18
18
|
|
|
19
|
-
spec.required_ruby_version = ">=
|
|
19
|
+
spec.required_ruby_version = ">= 3.3.0"
|
|
20
20
|
|
|
21
21
|
spec.add_dependency('mechanize', '~> 2')
|
|
22
|
+
spec.add_dependency('selenium-webdriver', '~> 4')
|
|
22
23
|
spec.add_dependency('switches.rb', '~> 0')
|
|
23
24
|
|
|
24
25
|
spec.add_development_dependency('minitest')
|
|
@@ -0,0 +1,83 @@
|
|
|
1
|
+
# test/Moby/Backends/MechanizeBackend_test.rb
|
|
2
|
+
|
|
3
|
+
require_relative '../../test_helper'
|
|
4
|
+
|
|
5
|
+
describe Moby::Backends::MechanizeBackend do
|
|
6
|
+
let(:url){'https://phishing.example.com/login'}
|
|
7
|
+
let(:generator){Moby::RandomInputGenerator.new(word_list_path: TestFixtures::WORD_LIST_PATH)}
|
|
8
|
+
|
|
9
|
+
def backend(**config_args)
|
|
10
|
+
config = Moby::Configuration.new(url: url, **config_args)
|
|
11
|
+
Moby::Backends::MechanizeBackend.new(config: config, random_input_generator: generator)
|
|
12
|
+
end
|
|
13
|
+
|
|
14
|
+
before do
|
|
15
|
+
WebMock.disable_net_connect!
|
|
16
|
+
stub_request(:get, url)
|
|
17
|
+
.to_return(status: 200, body: TestFixtures::SIMPLE_LOGIN_FORM, headers: {'Content-Type' => 'text/html'})
|
|
18
|
+
stub_request(:post, url).to_return(status: 200, body: 'Success')
|
|
19
|
+
end
|
|
20
|
+
|
|
21
|
+
after do
|
|
22
|
+
WebMock.reset!
|
|
23
|
+
WebMock.allow_net_connect!
|
|
24
|
+
end
|
|
25
|
+
|
|
26
|
+
describe "page loading" do
|
|
27
|
+
it "loads the target url once and memoises the page" do
|
|
28
|
+
b = backend
|
|
29
|
+
page1 = b.send(:page)
|
|
30
|
+
page2 = b.send(:page)
|
|
31
|
+
|
|
32
|
+
_(page1).must_be_instance_of(Mechanize::Page)
|
|
33
|
+
_(page1).must_be_same_as(page2)
|
|
34
|
+
assert_requested(:get, url, times: 1)
|
|
35
|
+
end
|
|
36
|
+
end
|
|
37
|
+
|
|
38
|
+
describe "form finding" do
|
|
39
|
+
it "finds the form by name" do
|
|
40
|
+
form = backend(form_name: 'login').send(:form)
|
|
41
|
+
|
|
42
|
+
_(form).must_be_instance_of(Mechanize::Form)
|
|
43
|
+
_(form.name).must_equal('login')
|
|
44
|
+
end
|
|
45
|
+
|
|
46
|
+
it "finds the form by number" do
|
|
47
|
+
multi_url = 'https://multi.example.com/page'
|
|
48
|
+
stub_request(:get, multi_url)
|
|
49
|
+
.to_return(status: 200, body: TestFixtures::MULTIPLE_FORMS, headers: {'Content-Type' => 'text/html'})
|
|
50
|
+
config = Moby::Configuration.new(url: multi_url, form_number: 1)
|
|
51
|
+
b = Moby::Backends::MechanizeBackend.new(config: config, random_input_generator: generator)
|
|
52
|
+
|
|
53
|
+
_(b.send(:form).node['id']).must_equal('login_form')
|
|
54
|
+
end
|
|
55
|
+
end
|
|
56
|
+
|
|
57
|
+
describe "field finding" do
|
|
58
|
+
it "finds the username and password fields by name" do
|
|
59
|
+
b = backend(form_name: 'login', username_field_name: 'username', password_field_name: 'password')
|
|
60
|
+
|
|
61
|
+
_(b.send(:username_field).name).must_equal('username')
|
|
62
|
+
_(b.send(:password_field).name).must_equal('password')
|
|
63
|
+
end
|
|
64
|
+
end
|
|
65
|
+
|
|
66
|
+
describe "submitting" do
|
|
67
|
+
it "fills both fields and submits" do
|
|
68
|
+
backend.send(:fill_and_submit_login_form)
|
|
69
|
+
|
|
70
|
+
assert_requested(:post, url) do |request|
|
|
71
|
+
request.body.match?(/username=[^&]+/) && request.body.match?(/password=[^&]+/)
|
|
72
|
+
end
|
|
73
|
+
end
|
|
74
|
+
|
|
75
|
+
it "submits only the password when the username is prefilled" do
|
|
76
|
+
backend(username_prefilled: true).send(:fill_and_submit_login_form)
|
|
77
|
+
|
|
78
|
+
assert_requested(:post, url) do |request|
|
|
79
|
+
request.body.match?(/username=(&|\z)/) && request.body.match?(/password=[^&]+/)
|
|
80
|
+
end
|
|
81
|
+
end
|
|
82
|
+
end
|
|
83
|
+
end
|
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
# test/Moby/Backends/SeleniumBackend_test.rb
|
|
2
|
+
|
|
3
|
+
require_relative '../../test_helper'
|
|
4
|
+
|
|
5
|
+
describe Moby::Backends::SeleniumBackend do
|
|
6
|
+
let(:url){'https://phishing.example.com/login'}
|
|
7
|
+
let(:generator){Moby::RandomInputGenerator.new(word_list_path: TestFixtures::WORD_LIST_PATH)}
|
|
8
|
+
|
|
9
|
+
def backend(**config_args)
|
|
10
|
+
config = Moby::Configuration.new(url: url, backend: :selenium, **config_args)
|
|
11
|
+
Moby::Backends::SeleniumBackend.new(config: config, random_input_generator: generator)
|
|
12
|
+
end
|
|
13
|
+
|
|
14
|
+
# Stub the real browser launch so no Chrome/Firefox process is started.
|
|
15
|
+
def with_stubbed_driver
|
|
16
|
+
built = 0
|
|
17
|
+
fake_driver = Object.new
|
|
18
|
+
original = Selenium::WebDriver.method(:for)
|
|
19
|
+
Selenium::WebDriver.define_singleton_method(:for) do |*_args, **_kwargs|
|
|
20
|
+
built += 1
|
|
21
|
+
fake_driver
|
|
22
|
+
end
|
|
23
|
+
begin
|
|
24
|
+
yield(fake_driver, ->{built})
|
|
25
|
+
ensure
|
|
26
|
+
Selenium::WebDriver.define_singleton_method(:for, original)
|
|
27
|
+
end
|
|
28
|
+
end
|
|
29
|
+
|
|
30
|
+
describe "#driver" do
|
|
31
|
+
it "builds the driver once and memoises it (returning a non-nil driver)" do
|
|
32
|
+
with_stubbed_driver do |fake_driver, build_count|
|
|
33
|
+
b = backend
|
|
34
|
+
first = b.send(:driver)
|
|
35
|
+
second = b.send(:driver)
|
|
36
|
+
|
|
37
|
+
_(first).wont_be_nil
|
|
38
|
+
_(first).must_be_same_as(fake_driver)
|
|
39
|
+
_(first).must_be_same_as(second)
|
|
40
|
+
_(build_count.call).must_equal(1)
|
|
41
|
+
end
|
|
42
|
+
end
|
|
43
|
+
end
|
|
44
|
+
end
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
# test/Moby/Backends_test.rb
|
|
2
|
+
|
|
3
|
+
require_relative '../test_helper'
|
|
4
|
+
|
|
5
|
+
describe Moby::Backends do
|
|
6
|
+
let(:url){'https://phishing.example.com/login'}
|
|
7
|
+
let(:generator){Moby::RandomInputGenerator.new(word_list_path: TestFixtures::WORD_LIST_PATH)}
|
|
8
|
+
|
|
9
|
+
def backend_for(**config_args)
|
|
10
|
+
config = Moby::Configuration.new(url: url, **config_args)
|
|
11
|
+
Moby::Backends.for(config: config, random_input_generator: generator)
|
|
12
|
+
end
|
|
13
|
+
|
|
14
|
+
describe ".for" do
|
|
15
|
+
it "returns a MechanizeBackend for the mechanize configuration" do
|
|
16
|
+
_(backend_for).must_be_instance_of(Moby::Backends::MechanizeBackend)
|
|
17
|
+
end
|
|
18
|
+
|
|
19
|
+
it "returns a SeleniumBackend for the selenium configuration" do
|
|
20
|
+
_(backend_for(backend: :selenium)).must_be_instance_of(Moby::Backends::SeleniumBackend)
|
|
21
|
+
end
|
|
22
|
+
|
|
23
|
+
it "raises for an unknown backend" do
|
|
24
|
+
_(proc{backend_for(backend: :ftp)}).must_raise(RuntimeError)
|
|
25
|
+
end
|
|
26
|
+
end
|
|
27
|
+
end
|