stratagem 0.1.7

Sign up to get free protection for your applications and to get access to all the features.
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