stratagem 0.1.7

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.
Files changed (101) hide show
  1. data/Manifest +99 -0
  2. data/Rakefile +17 -0
  3. data/bin/stratagem +10 -0
  4. data/init.rb +2 -0
  5. data/lib/bootstrap.rb +31 -0
  6. data/lib/stratagem/authentication.rb +64 -0
  7. data/lib/stratagem/auto_mock/aquifer.rb +86 -0
  8. data/lib/stratagem/auto_mock/factory.rb +213 -0
  9. data/lib/stratagem/auto_mock/value_generator.rb +174 -0
  10. data/lib/stratagem/auto_mock.rb +6 -0
  11. data/lib/stratagem/blocker.rb +16 -0
  12. data/lib/stratagem/client.rb +32 -0
  13. data/lib/stratagem/command.rb +13 -0
  14. data/lib/stratagem/commands/analyze.rb +22 -0
  15. data/lib/stratagem/commands/base.rb +11 -0
  16. data/lib/stratagem/commands/devel_crawl.rb +27 -0
  17. data/lib/stratagem/commands/devel_mock.rb +10 -0
  18. data/lib/stratagem/commands.rb +7 -0
  19. data/lib/stratagem/crawler/authentication.rb +109 -0
  20. data/lib/stratagem/crawler/form.rb +101 -0
  21. data/lib/stratagem/crawler/html_utils.rb +92 -0
  22. data/lib/stratagem/crawler/session.rb +296 -0
  23. data/lib/stratagem/crawler/site_model.rb +138 -0
  24. data/lib/stratagem/crawler/trace_utils.rb +10 -0
  25. data/lib/stratagem/crawler.rb +9 -0
  26. data/lib/stratagem/extensions/class.rb +9 -0
  27. data/lib/stratagem/extensions/hash.rb +16 -0
  28. data/lib/stratagem/extensions/module.rb +11 -0
  29. data/lib/stratagem/extensions/object.rb +15 -0
  30. data/lib/stratagem/extensions/red_parse.rb +86 -0
  31. data/lib/stratagem/extensions/string.rb +20 -0
  32. data/lib/stratagem/extensions.rb +6 -0
  33. data/lib/stratagem/framework_extensions/controllers/action_controller.rb +10 -0
  34. data/lib/stratagem/framework_extensions/controllers/action_mailer.rb +12 -0
  35. data/lib/stratagem/framework_extensions/controllers.rb +5 -0
  36. data/lib/stratagem/framework_extensions/models/adapters/active_model/detect.rb +7 -0
  37. data/lib/stratagem/framework_extensions/models/adapters/active_model/extensions.rb +35 -0
  38. data/lib/stratagem/framework_extensions/models/adapters/active_model/metadata.rb +103 -0
  39. data/lib/stratagem/framework_extensions/models/adapters/active_model/tracing.rb +50 -0
  40. data/lib/stratagem/framework_extensions/models/adapters/authlogic/detect.rb +11 -0
  41. data/lib/stratagem/framework_extensions/models/adapters/authlogic/extensions.rb +10 -0
  42. data/lib/stratagem/framework_extensions/models/adapters/authlogic/metadata.rb +30 -0
  43. data/lib/stratagem/framework_extensions/models/adapters/authlogic/tracing.rb +4 -0
  44. data/lib/stratagem/framework_extensions/models/adapters/common/authentication_metadata.rb +21 -0
  45. data/lib/stratagem/framework_extensions/models/adapters/restful_authentication/detect.rb +13 -0
  46. data/lib/stratagem/framework_extensions/models/adapters/restful_authentication/extensions.rb +19 -0
  47. data/lib/stratagem/framework_extensions/models/adapters/restful_authentication/metadata.rb +30 -0
  48. data/lib/stratagem/framework_extensions/models/adapters/restful_authentication/tracing.rb +4 -0
  49. data/lib/stratagem/framework_extensions/models/annotations.rb +79 -0
  50. data/lib/stratagem/framework_extensions/models/detect.rb +7 -0
  51. data/lib/stratagem/framework_extensions/models/metadata.rb +85 -0
  52. data/lib/stratagem/framework_extensions/models/mocking.rb +23 -0
  53. data/lib/stratagem/framework_extensions/models/tracing.rb +71 -0
  54. data/lib/stratagem/framework_extensions/models.rb +21 -0
  55. data/lib/stratagem/framework_extensions/rails.rb +8 -0
  56. data/lib/stratagem/framework_extensions.rb +6 -0
  57. data/lib/stratagem/interface/browser.rb +37 -0
  58. data/lib/stratagem/interface/public/images/backgrounds/content.png +0 -0
  59. data/lib/stratagem/interface/public/images/backgrounds/shadow.png +0 -0
  60. data/lib/stratagem/interface/public/javascripts/jquery-1.4.2.min.js +154 -0
  61. data/lib/stratagem/interface/public/javascripts/stratagem.js +27 -0
  62. data/lib/stratagem/interface/public/javascripts/stratagem_debug.js +53 -0
  63. data/lib/stratagem/interface/public/stylesheets/960.css +1 -0
  64. data/lib/stratagem/interface/public/stylesheets/reset.css +10 -0
  65. data/lib/stratagem/interface/public/stylesheets/stratagem.css +20 -0
  66. data/lib/stratagem/interface/public/stylesheets/stratagem_debug.css +20 -0
  67. data/lib/stratagem/interface/views/debug.haml +43 -0
  68. data/lib/stratagem/interface/views/index.haml +35 -0
  69. data/lib/stratagem/labs/auto_mock.rb +7 -0
  70. data/lib/stratagem/labs/crawler.rb +0 -0
  71. data/lib/stratagem/logger.rb +46 -0
  72. data/lib/stratagem/model/application.rb +157 -0
  73. data/lib/stratagem/model/components/base.rb +55 -0
  74. data/lib/stratagem/model/components/controller.rb +118 -0
  75. data/lib/stratagem/model/components/model.rb +170 -0
  76. data/lib/stratagem/model/components/reference.rb +30 -0
  77. data/lib/stratagem/model/components/route.rb +53 -0
  78. data/lib/stratagem/model/components/static_file.rb +18 -0
  79. data/lib/stratagem/model/components/view.rb +186 -0
  80. data/lib/stratagem/model/parse_util.rb +61 -0
  81. data/lib/stratagem/model.rb +12 -0
  82. data/lib/stratagem/model_builder.rb +146 -0
  83. data/lib/stratagem/recipes/deploy.rb +30 -0
  84. data/lib/stratagem/scan/checks/capistrano/secure_deploy.rb +43 -0
  85. data/lib/stratagem/scan/checks/email_address.rb +15 -0
  86. data/lib/stratagem/scan/checks/error_pages.rb +25 -0
  87. data/lib/stratagem/scan/checks/filter_parameter_logging.rb +6 -0
  88. data/lib/stratagem/scan/checks/mongo_mapper/base.rb +19 -0
  89. data/lib/stratagem/scan/checks/mongo_mapper/foreign_keys_exposed.rb +32 -0
  90. data/lib/stratagem/scan/checks/routes.rb +16 -0
  91. data/lib/stratagem/scan/checks/ssl/secure_login_page.rb +19 -0
  92. data/lib/stratagem/scan/checks/ssl/secure_login_submit.rb +18 -0
  93. data/lib/stratagem/scan/result.rb +45 -0
  94. data/lib/stratagem/scan.rb +19 -0
  95. data/lib/stratagem/scanner.rb +32 -0
  96. data/lib/stratagem/site_crawler.rb +47 -0
  97. data/lib/stratagem/snapshot.rb +33 -0
  98. data/lib/stratagem.rb +77 -0
  99. data/lib/tasks/_old_stratagem.rake +99 -0
  100. data/stratagem.gemspec +56 -0
  101. metadata +380 -0
@@ -0,0 +1,174 @@
1
+ module Stratagem::AutoMock
2
+ module ValueGenerator
3
+ ADDRESSES = [
4
+ {:street => "1600 Pennsylvania Avenue, NW", :city => "Washington", :state => "DC", :zip_code => '20500', :phone => "(202) 395-2020", :fax => "(202) 395-2020"},
5
+ {:street => "525 8th Ave", :city => "New York", :state => "NY", :zip_code => '10018', :phone => "(212) 869-6427", :fax => "(202) 395-2020" }
6
+ ]
7
+ USERS = [
8
+ {:email => "wchurchill@stratagemapp.com", :first => "winston", :last => "churchill", :login => "wchurchill#{rand 1000}", :password => 'P7D8y5fycaaaa00pa' },
9
+ {:email => "bwalsh@stratagemapp.com", :first => "brenda", :last => "walsh", :login => "bwalsh#{rand 1000}", :password => '@.04@49\PFtB00a0d' },
10
+ {:email => "hiro@stratagemapp.com", :first => "hiro", :last => "protagonist", :login => "bigtimehiro#{rand 1000}", :password => '4Kq651p0A534p7T01klj' }
11
+ ]
12
+ GENERIC_NAMES = [
13
+ 'supercalifragilisticexpialidocious',
14
+ 'hippopotomonstrosesquipedalian',
15
+ 'honorificabilitudinitatibus',
16
+ 'otorhinolaryngological',
17
+ 'tom'
18
+ ]
19
+ GENERIC_URLS = [
20
+ 'http://www.thewebsiteisdown.com',
21
+ 'http://www.failblog.org',
22
+ 'http://www.xkcd.org'
23
+ ]
24
+
25
+ NUMERIC_TYPES = [
26
+ :decimal, :float, :integer
27
+ ]
28
+
29
+ def generate_value(attribute_name, attribute_type)
30
+ @address ||= ADDRESSES[rand ADDRESSES.size]
31
+ @user ||= USERS[rand USERS.size]
32
+ value = case attribute_type
33
+ when :string
34
+ generate_string(attribute_name)
35
+ when :integer
36
+ generate_int(attribute_name)
37
+ when :datetime
38
+ generate_datetime(attribute_name)
39
+ when :date
40
+ generate_datetime(attribute_name)
41
+ when :text
42
+ generate_text(attribute_name)
43
+ when :boolean
44
+ generate_boolean(attribute_name)
45
+ when :decimal
46
+ generate_float(attribute_name)
47
+ when :float
48
+ generate_float(attribute_name)
49
+ when :symbol
50
+ generate_string(attribute_name).to_sym
51
+ else
52
+ raise Stratagem::AutoMock::UnsupportedColumnTypeError.new("Attribute #{attribute_name} of type #{attribute_type} is not supported")
53
+ end
54
+ value = nil if (rand(20) == 1) && !NUMERIC_TYPES.include?(attribute_type)
55
+ value
56
+ end
57
+
58
+ def generate_string(attribute_name)
59
+ attribute_name = attribute_name.to_s
60
+ if (attribute_name.stratagem_contains_token?('email') || attribute_name.stratagem_contains_token?('e-mail'))
61
+ (random_noise(4,false) || '') + @user[:email]
62
+ elsif (attribute_name.stratagem_contains_token?('login') || attribute_name.stratagem_contains_token?('username') || attribute_name.stratagem_contains_token?('user_name'))
63
+ @user[:login] + (random_noise(4, false) || '')
64
+ elsif (attribute_name.stratagem_contains_token?('first') && attribute_name.stratagem_contains_token?('name'))
65
+ (random_noise(2, false) || '') + @user[:first]
66
+ elsif (attribute_name.stratagem_contains_token?('last') && attribute_name.stratagem_contains_token?('name'))
67
+ (random_noise(2, false) || '') + @user[:last]
68
+ elsif (attribute_name.stratagem_contains_token?('password'))
69
+ @user[:password]
70
+ elsif (attribute_name.stratagem_contains_token?('zip_code') || attribute_name.stratagem_contains_token?('zipcode'))
71
+ @address[:zip_code]
72
+ elsif (attribute_name.stratagem_contains_token?('phone'))
73
+ @address[:phone]
74
+ elsif (attribute_name.stratagem_contains_token?('fax'))
75
+ @address[:fax]
76
+ elsif (attribute_name.stratagem_contains_token?('address'))
77
+ @address[:street]
78
+ elsif (attribute_name.stratagem_contains_token?('city'))
79
+ @address[:city]
80
+ elsif (attribute_name.stratagem_contains_token?('state'))
81
+ @address[:state]
82
+ elsif (attribute_name.stratagem_contains_token?('name'))
83
+ value = GENERIC_NAMES[rand GENERIC_NAMES.size]
84
+ noise = random_noise(4)
85
+ value += noise unless value.nil? || noise.nil?
86
+ elsif (attribute_name.stratagem_contains_token?('url'))
87
+ GENERIC_URLS[rand GENERIC_URLS.size]
88
+ elsif (attribute_name.stratagem_contains_token?('ip'))
89
+ "#{rand(156)+100}.#{rand(156)+100}.#{rand(156)+100}.#{rand(156)+100}"
90
+ else
91
+ random_noise
92
+ end
93
+ end
94
+
95
+ def generate_float(attribute_name)
96
+ if (attribute_name.to_s.stratagem_contains_token?('price'))
97
+ ((rand + rand(10)) * 100).round.to_f / 100
98
+ else
99
+ rand
100
+ end
101
+ end
102
+
103
+ def generate_boolean(attribute_name)
104
+ case rand(4)
105
+ when 0
106
+ false
107
+ when 1
108
+ true
109
+ when 2
110
+ '0'
111
+ when 3
112
+ '1'
113
+ end
114
+ end
115
+
116
+ def generate_int(attribute_name)
117
+ rand(1000)+1
118
+ end
119
+
120
+ def generate_datetime(attribute_name)
121
+ Time.now+rand(10.days)
122
+ end
123
+
124
+ def generate_text(attribute_name)
125
+ random_noise(200)
126
+ end
127
+
128
+ private
129
+
130
+ def random_noise(size=nil, use_non_alpha_numeric=true)
131
+ size ||= rand(20)
132
+ options = {
133
+ :ssn => [
134
+ Proc.new { "#{rand(800)+100}-#{rand(80)+10}-#{rand(9000)+1000}" },
135
+ Proc.new { "#{rand(800)+100}#{rand(80)+10}#{rand(9000)+1000}" },
136
+ Proc.new { "#{rand(800)+100} #{rand(80)+10} #{rand(9000)+1000}" }
137
+ ],
138
+ :phone => [
139
+ Proc.new { "#{rand(800)+100}-#{rand(800)+100}-#{rand(9000)+1000}" },
140
+ Proc.new { "(#{rand(800)+100}) #{rand(800)+100}-#{rand(9000)+1000}" },
141
+ Proc.new { "(#{rand(800)+100})#{rand(800)+100}-#{rand(9000)+1000}" },
142
+ ],
143
+ :zip_code => [
144
+ Proc.new { "#{rand(90000)+10000}" },
145
+ Proc.new { "#{rand(90000)+10000}-#{rand(900)+100}" },
146
+ Proc.new { "#{rand(90000)+10000} #{rand(900)+100}" }
147
+ ],
148
+ :credit_card => [
149
+ Proc.new { "#{rand(900)+100} #{rand(900)+100} #{rand(900)+100} #{rand(900)+100}" },
150
+ Proc.new { "#{rand(900)+100}-#{rand(900)+100}-#{rand(900)+100}-#{rand(900)+100}" },
151
+ Proc.new { "#{rand(900)+100}-#{rand(900)+100}-#{rand(900)+100}-#{rand(900)+100}" }
152
+ ],
153
+ :date => [
154
+ Proc.new { "#{rand(9)+1}/#{rand(90)+10}" },
155
+ Proc.new { "#{rand(9)+1}/#{rand(100)+1990}" }
156
+ ],
157
+ :random => [
158
+ Proc.new {
159
+ c = 'abcdefghjkmnpqrstuvwxyzABCDEFGHJKLMNOPQRSTUVWXYZ0123456789-_@. '
160
+ s = ''
161
+ size.times { |i| s << c[rand(c.length)] }
162
+ s
163
+ }
164
+ ],
165
+ :nil => [Proc.new { nil }]
166
+ }
167
+ procs = options.values[rand(options.values.size)]
168
+ proc = procs[rand(procs.size)]
169
+ result = proc.call
170
+ result.gsub!(/[^A-Za-z0-9]/, '') unless use_non_alpha_numeric || result.nil?
171
+ result
172
+ end
173
+ end
174
+ end
@@ -0,0 +1,6 @@
1
+ module Stratagem::AutoMock
2
+ end
3
+
4
+ require 'stratagem/auto_mock/value_generator'
5
+ require 'stratagem/auto_mock/factory'
6
+ require 'stratagem/auto_mock/aquifer'
@@ -0,0 +1,16 @@
1
+ require 'thread'
2
+
3
+ class Blocker
4
+ def initialize
5
+ @mutex = Mutex.new
6
+ @cv = ConditionVariable.new
7
+ end
8
+
9
+ def wait
10
+ @mutex.synchronize { @cv.wait(@mutex) }
11
+ end
12
+
13
+ def notify
14
+ @mutex.synchronize { @cv.signal }
15
+ end
16
+ end
@@ -0,0 +1,32 @@
1
+ require 'uri'
2
+
3
+ module Stratagem
4
+ class Client
5
+ def initialize(authentication)
6
+ @authentication = authentication
7
+ end
8
+
9
+ def send(snapshot)
10
+ Stratagem.logger.debug "Sending report to server"
11
+ url = URI.parse("#{@authentication.base_url}/snapshots")
12
+ req = Net::HTTP::Post.new(url.path)
13
+ req.set_form_data({
14
+ 'api_key' => @authentication.credentials[:token],
15
+ 'project_id' => @authentication.credentials[:project],
16
+ 'timestamp' => snapshot.timestamp.to_i,
17
+ 'model' => snapshot.model.export.to_json
18
+ }, ';')
19
+ res = Net::HTTP.new(url.host, url.port).start {|http| http.request(req) }
20
+ puts "response:"
21
+ case res
22
+ when Net::HTTPSuccess, Net::HTTPRedirection
23
+ puts "Visit #{@authentication.base_url} for your security posture."
24
+ # OK
25
+ else
26
+ res.error!
27
+ end
28
+
29
+ Stratagem.logger.phase('complete')
30
+ end
31
+ end
32
+ end
@@ -0,0 +1,13 @@
1
+ module Stratagem::Command
2
+ class << self
3
+ def run(command)
4
+ klass = load_command_class(command)
5
+ klass.new.run
6
+ end
7
+
8
+ def load_command_class(command)
9
+ module_eval("Stratagem::Command::#{command.camelize}")
10
+ end
11
+
12
+ end
13
+ end
@@ -0,0 +1,22 @@
1
+ module Stratagem::Command
2
+ class Analyze < Base
3
+ def run
4
+ require './config/environment'
5
+ require 'stratagem/interface/browser'
6
+
7
+ authentication = Stratagem::Authentication.instance
8
+
9
+ while !Sinatra::Application.running
10
+ puts "Waiting for Sinatra to launch"
11
+ sleep 0.5
12
+ end
13
+
14
+ puts "Launching your web browser"
15
+ Launchy::Browser.run(authentication.url)
16
+ Stratagem.wait_for_completion
17
+
18
+ log "" # ensure logger sends final message
19
+ puts "analysis complete. exiting."
20
+ end
21
+ end
22
+ end
@@ -0,0 +1,11 @@
1
+ module Stratagem::Command
2
+ class Base
3
+ def logger
4
+ Stratagem.logger
5
+ end
6
+
7
+ def log(msg)
8
+ logger.debug(msg)
9
+ end
10
+ end
11
+ end
@@ -0,0 +1,27 @@
1
+ module Stratagem::Command
2
+ class DevelCrawl < Base
3
+ include Stratagem::Crawler::Session
4
+
5
+ def run
6
+ require './config/environment'
7
+
8
+ crawler_session do
9
+ phase(:unauthenticated)
10
+ crawl
11
+ display
12
+ authenticated = authenticate(true)
13
+
14
+ if (authenticated)
15
+ phase(:authenticated)
16
+ crawl
17
+ display
18
+ end
19
+ end
20
+
21
+ puts "SSL? #{authentication.ssl}"
22
+ puts "AUTHENTICATED? #{authentication.success}"
23
+
24
+
25
+ end
26
+ end
27
+ end
@@ -0,0 +1,10 @@
1
+ module Stratagem::Command
2
+ class DevelMock < Base
3
+ def run
4
+ require 'config/environment'
5
+ require 'stratagem/labs/auto_mock'
6
+
7
+ Stratagem::Labs::AutoMock.new.run
8
+ end
9
+ end
10
+ end
@@ -0,0 +1,7 @@
1
+ module Stratagem::Commands
2
+ end
3
+
4
+ require 'stratagem/commands/base'
5
+ require 'stratagem/commands/analyze'
6
+ require 'stratagem/commands/devel_crawl'
7
+ require 'stratagem/commands/devel_mock'
@@ -0,0 +1,109 @@
1
+ module Stratagem::Crawler
2
+ class AuthenticationData
3
+ attr_accessor :success, :login_page, :form, :response_page, :ssl
4
+ end
5
+
6
+
7
+ module Authentication
8
+ include Stratagem::Crawler::TraceUtils
9
+
10
+ def authentication
11
+ @authentication_data ||= AuthenticationData.new()
12
+ end
13
+
14
+ def authenticate
15
+ page, form = find_login_form
16
+ if (page && form)
17
+ authentication.login_page = page
18
+ login(form)
19
+ form.submit {|action,params|
20
+ post(action, params)
21
+ response
22
+ }
23
+
24
+ route = application_model.routes.recognize(response.request.path, :post)
25
+ page = site_model.add(route, response) {|response| }
26
+ authentication.response_page = page
27
+
28
+ begin
29
+ authentication.success = authentication.response_page.login_form.nil?
30
+ rescue
31
+ puts $!.message
32
+ puts $!.backtrace
33
+ end
34
+ puts "authenticated? #{authentication.success}"
35
+ else
36
+ puts "Authentication Error: Unable to locate sign in form"
37
+ end
38
+
39
+ if (response)
40
+ authentication.ssl = response.request.ssl?
41
+ authentication.success
42
+ else
43
+ false
44
+ end
45
+ end
46
+
47
+ def find_login_form
48
+ site_model.pages.sort {|a,b| b.inbound_edges(:redirect).size <=> a.inbound_edges(:redirect).size }.each do |page|
49
+ puts "Testing page #{page.url} for sign in form"
50
+ page.reload {|url| get url; response }
51
+ form = page.login_form
52
+ return [page, form] if (form)
53
+ end
54
+ []
55
+ end
56
+
57
+ def login(form)
58
+ attr_names = form.inputs.map {|input| input.guess_attribute.to_sym }
59
+ model = guess_login_model(attr_names)
60
+
61
+ if model
62
+ record = Stratagem::AutoMock::Aquifer.instance.random_instance(model.klass)
63
+
64
+ if (record)
65
+ puts "populating login form"
66
+ populate_login_form(model, form, record)
67
+ else
68
+ log "ERROR: Unable to find suitable model to populate authentication form with."
69
+ end
70
+ else
71
+ raise "Unable to infer model from inputs"
72
+ end
73
+ end
74
+
75
+ def guess_login_model(attr_names)
76
+ selections = application_model.models.select {|model|
77
+ intersect = (model.model_attributes.keys & attr_names)
78
+ intersect.size > 0
79
+ }.sort {|a,b|
80
+ a_intersect = (a.model_attributes.keys & attr_names)
81
+ b_intersect = (b.model_attributes.keys & attr_names)
82
+ b_intersect.size <=> a_intersect.size
83
+ }
84
+ puts "selecting model #{selections.first.klass.name} for authentication"
85
+ selections.first
86
+ end
87
+
88
+
89
+ def populate_login_form(model, form, record)
90
+ form.inputs.each do |input|
91
+ attribute_name = input.guess_attribute.to_sym
92
+ attribute_value = record.stratagem.read_mock_attribute(attribute_name)
93
+
94
+ puts "authentication field: #{attribute_name} -> #{attribute_value}"
95
+
96
+ if (input.kind_of? Stratagem::Crawler::Toggle)
97
+ input.check
98
+ elsif (record.stratagem.mock_attributes.keys.include?(attribute_name))
99
+ input.value = record.stratagem.read_mock_attribute(attribute_name) unless input.hidden?
100
+ else
101
+ puts record.stratagem.mock_attributes.inspect
102
+ puts "ERROR: Cannot find attribute #{attribute_name} in model #{record.class.name}"
103
+ end
104
+ end
105
+ form.generate_parameters
106
+ end
107
+
108
+ end
109
+ end
@@ -0,0 +1,101 @@
1
+ # Primarily used to fill out login forms rather than trying to fudge the before_filters
2
+ module Stratagem::Crawler
3
+ class Form
4
+ attr_accessor :action, :method, :fields, :buttons
5
+ attr_reader :inputs, :buttons
6
+
7
+ def initialize
8
+ @inputs = []
9
+ @buttons = []
10
+ end
11
+
12
+ def << (input)
13
+ if (input.kind_of?(Button))
14
+ @buttons << input
15
+ else
16
+ @inputs << input
17
+ end
18
+ end
19
+
20
+ def password?
21
+ !(inputs.find {|i| i.type == 'password' }.nil?)
22
+ end
23
+
24
+ def submit(&block)
25
+ response = block.call(action, generate_parameters)
26
+ end
27
+
28
+ def generate_parameters
29
+ params = {}
30
+ inputs.each do |input|
31
+ params[input.name] = input.value
32
+ end
33
+ params
34
+ end
35
+ end
36
+
37
+ class Input
38
+ attr_accessor :name, :id
39
+ attr_reader :type, :value
40
+
41
+ def type=(type)
42
+ @type = type.downcase.strip
43
+ end
44
+
45
+ def value=(value)
46
+ if (value.kind_of?(String))
47
+ @value = value.nil? ? nil : value.strip
48
+ else
49
+ @value = value
50
+ end
51
+ end
52
+
53
+ def guess_attribute
54
+ if (name =~ /.*\[(.*)\]/)
55
+ $1.to_sym
56
+ else
57
+ name.to_sym
58
+ end
59
+ end
60
+
61
+ def guess_model
62
+ return $1.camelize if (name =~ /(.*)?\[/)
63
+ return nil
64
+ end
65
+
66
+ def hidden?
67
+ self.type == 'hidden'
68
+ end
69
+ end
70
+
71
+ class Button < Input
72
+ end
73
+
74
+ class Toggle < Input
75
+ def check
76
+ @value = '1'
77
+ end
78
+
79
+ def uncheck
80
+ @value = '0'
81
+ end
82
+
83
+ def checked?
84
+ @value == '1'
85
+ end
86
+ end
87
+
88
+ class Select < Input
89
+ def initialize
90
+ @options = []
91
+ end
92
+
93
+ def choose(value)
94
+ @value = value
95
+ end
96
+
97
+ def <<(value)
98
+ @options << value
99
+ end
100
+ end
101
+ end
@@ -0,0 +1,92 @@
1
+ module Stratagem::Crawler
2
+ class CrawlError < StratagemError; attr_accessor :html, :route; end
3
+ class FormParseError < CrawlError; end
4
+
5
+ module HtmlUtils
6
+ INPUT_TEXT = ['text', 'password', 'hidden']
7
+ INPUT_BUTTON = ['button', 'submit', 'reset', 'image', 'src']
8
+ INPUT_TOGGLE = ['checkbox']
9
+
10
+ def find_login_form(document)
11
+ possibilities = parse_forms(document).select {|form|
12
+ # this maps to the form action, not the controller action
13
+ form.action =~ /log[-]*in/ ||
14
+ form.action =~ /sign[-]*in/ ||
15
+ !form.inputs.find {|input| input.type == 'password' }.nil?
16
+ }.sort {|a,b| a.inputs.size <=> b.inputs.size }
17
+ possibilities.first
18
+ end
19
+
20
+ def parse_forms(document)
21
+ document.xpath('//form').map do |form_tag|
22
+ form = Form.new()
23
+ form.action = form_load_attribute(form_tag, 'action')
24
+ form.method = form_load_attribute(form_tag, 'method', false) || 'post'
25
+ (form_tag/'input').each do |input_tag|
26
+ form_add_input(form, input_tag)
27
+ end
28
+ (form_tag/'textarea').each do |input_tag|
29
+ form_add_input(form, input_tag)
30
+ end
31
+ (form_tag/'select').each do |select_tag|
32
+ form_add_select(form, select_tag)
33
+ end
34
+
35
+ form
36
+ end
37
+ end
38
+
39
+ private
40
+
41
+ def form_add_select(form, select_tag)
42
+ input = Select.new()
43
+ input.id = form_load_attribute(select_tag, 'id', false)
44
+ input.name = form_load_attribute(select_tag, 'name')
45
+
46
+ (select_tag/'option').each do |option_tag|
47
+ value = option_tag.attributes['value']
48
+ value = (value ? value.value : option_tag.inner_html).strip
49
+ input << value unless value.empty?
50
+ end
51
+ end
52
+
53
+ def form_add_input(form, input_tag)
54
+ type = form_load_attribute(input_tag, 'type', false)
55
+
56
+ input = nil
57
+
58
+ if ((type.nil?) || INPUT_TEXT.include?(type))
59
+ if (input_tag.attributes['src'])
60
+ input = Button.new()
61
+ else
62
+ input = Input.new()
63
+ end
64
+ elsif (INPUT_BUTTON.include?(type))
65
+ input = Button.new()
66
+ elsif (INPUT_TOGGLE.include?(type))
67
+ input = Toggle.new()
68
+ else
69
+ raise FormParseError.new("Unsupported <input> type: '#{type}'", :html => input_tag.to_html)
70
+ end
71
+
72
+ input.id = form_load_attribute(input_tag, 'id', false)
73
+ input.type = form_load_attribute(input_tag, 'type', false) || 'text'
74
+ input.name = form_load_attribute(input_tag, 'name', false)
75
+ input.value = form_load_attribute(input_tag, 'value', false)
76
+
77
+ form << input
78
+ end
79
+
80
+ def form_load_attribute(node, attribute_name, raise_error = true)
81
+ # determine action
82
+ attr = node.attributes[attribute_name]
83
+ value = nil
84
+ if (attr)
85
+ value = attr.value.strip.downcase
86
+ elsif (raise_error)
87
+ raise FormParseError.new("#{attribute_name} attribute not found in tag - #{node.to_html}")
88
+ end
89
+ value
90
+ end
91
+ end
92
+ end