arachni 1.3.2 → 1.4

Sign up to get free protection for your applications and to get access to all the features.
Files changed (727) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.md +108 -0
  3. data/Gemfile +2 -6
  4. data/LICENSE.md +1 -1
  5. data/README.md +34 -16
  6. data/Rakefile +1 -1
  7. data/arachni.gemspec +28 -20
  8. data/bin/arachni +1 -1
  9. data/bin/arachni_console +1 -1
  10. data/bin/arachni_multi +1 -1
  11. data/bin/arachni_reporter +1 -1
  12. data/bin/arachni_rest_server +13 -0
  13. data/bin/arachni_restore +1 -1
  14. data/bin/arachni_rpc +1 -1
  15. data/bin/arachni_rpcd +1 -1
  16. data/bin/arachni_rpcd_monitor +1 -1
  17. data/bin/arachni_script +1 -1
  18. data/components/checks/active/code_injection.rb +8 -10
  19. data/components/checks/active/code_injection_php_input_wrapper.rb +5 -6
  20. data/components/checks/active/code_injection_timing.rb +1 -1
  21. data/components/checks/active/csrf.rb +1 -1
  22. data/components/checks/active/file_inclusion.rb +20 -26
  23. data/components/checks/active/ldap_injection.rb +4 -5
  24. data/components/checks/active/no_sql_injection.rb +11 -20
  25. data/components/checks/active/no_sql_injection/substrings/mongodb +1 -0
  26. data/components/checks/active/no_sql_injection_differential.rb +3 -4
  27. data/components/checks/active/os_cmd_injection.rb +5 -9
  28. data/components/checks/active/os_cmd_injection_timing.rb +1 -1
  29. data/components/checks/active/path_traversal.rb +4 -17
  30. data/components/checks/active/response_splitting.rb +8 -2
  31. data/components/checks/active/rfi.rb +4 -5
  32. data/components/checks/active/session_fixation.rb +9 -3
  33. data/components/checks/active/source_code_disclosure.rb +5 -20
  34. data/components/checks/active/sql_injection.rb +30 -18
  35. data/components/checks/active/sql_injection/{regexp_ignore.txt → ignore_substrings} +0 -0
  36. data/components/checks/active/sql_injection/regexps/db2.yaml +2 -0
  37. data/components/checks/active/sql_injection/regexps/frontbase.yaml +1 -0
  38. data/components/checks/active/sql_injection/regexps/informix.yaml +1 -0
  39. data/components/checks/active/sql_injection/regexps/ingres.yaml +2 -0
  40. data/components/checks/active/sql_injection/regexps/maxdb.yaml +2 -0
  41. data/components/checks/active/sql_injection/regexps/mssql.yaml +8 -0
  42. data/components/checks/active/sql_injection/regexps/mysql.yaml +4 -0
  43. data/components/checks/active/sql_injection/regexps/oracle.yaml +4 -0
  44. data/components/checks/active/sql_injection/regexps/pgsql.yaml +3 -0
  45. data/components/checks/active/sql_injection/regexps/sqlite.yaml +2 -0
  46. data/components/checks/active/sql_injection/regexps/sybase.yaml +2 -0
  47. data/components/checks/active/sql_injection/substrings/access +3 -0
  48. data/components/checks/active/sql_injection/substrings/db2 +2 -0
  49. data/components/checks/active/sql_injection/{patterns → substrings}/emc +1 -1
  50. data/components/checks/active/sql_injection/{patterns → substrings}/firebird +0 -1
  51. data/components/checks/active/sql_injection/substrings/hsqldb +1 -0
  52. data/components/checks/active/sql_injection/{patterns → substrings}/informix +1 -2
  53. data/components/checks/active/sql_injection/substrings/ingres +1 -0
  54. data/components/checks/active/sql_injection/{patterns → substrings}/interbase +0 -0
  55. data/components/checks/active/sql_injection/substrings/mssql +17 -0
  56. data/components/checks/active/sql_injection/{patterns → substrings}/mysql +3 -6
  57. data/components/checks/active/sql_injection/substrings/oracle +2 -0
  58. data/components/checks/active/sql_injection/{patterns → substrings}/pgsql +3 -6
  59. data/components/checks/active/sql_injection/substrings/sqlite +3 -0
  60. data/components/checks/active/sql_injection/substrings/sybase +1 -0
  61. data/components/checks/active/sql_injection_differential.rb +5 -7
  62. data/components/checks/active/sql_injection_differential/payloads.txt +1 -1
  63. data/components/checks/active/sql_injection_timing.rb +1 -1
  64. data/components/checks/active/trainer.rb +5 -4
  65. data/components/checks/active/unvalidated_redirect.rb +1 -1
  66. data/components/checks/active/unvalidated_redirect_dom.rb +1 -1
  67. data/components/checks/active/xpath_injection.rb +3 -4
  68. data/components/checks/active/xss.rb +33 -12
  69. data/components/checks/active/xss_dom.rb +7 -4
  70. data/components/checks/active/xss_dom_script_context.rb +1 -1
  71. data/components/checks/active/xss_event.rb +43 -20
  72. data/components/checks/active/xss_path.rb +5 -4
  73. data/components/checks/active/xss_script_context.rb +41 -11
  74. data/components/checks/active/xss_tag.rb +14 -15
  75. data/components/checks/active/xxe.rb +5 -16
  76. data/components/checks/passive/allowed_methods.rb +1 -1
  77. data/components/checks/passive/backdoors.rb +4 -2
  78. data/components/checks/passive/backup_directories.rb +4 -2
  79. data/components/checks/passive/backup_files.rb +4 -2
  80. data/components/checks/passive/common_admin_interfaces.rb +4 -3
  81. data/components/checks/passive/common_directories.rb +3 -1
  82. data/components/checks/passive/common_files.rb +3 -1
  83. data/components/checks/passive/directory_listing.rb +4 -4
  84. data/components/checks/passive/grep/captcha.rb +1 -1
  85. data/components/checks/passive/grep/cookie_set_for_parent_domain.rb +1 -1
  86. data/components/checks/passive/grep/credit_card.rb +5 -7
  87. data/components/checks/passive/grep/cvs_svn_users.rb +1 -1
  88. data/components/checks/passive/grep/emails.rb +135 -8
  89. data/components/checks/passive/grep/form_upload.rb +1 -1
  90. data/components/checks/passive/grep/hsts.rb +4 -3
  91. data/components/checks/passive/grep/html_objects.rb +1 -1
  92. data/components/checks/passive/grep/http_only_cookies.rb +5 -3
  93. data/components/checks/passive/grep/insecure_cookies.rb +5 -3
  94. data/components/checks/passive/grep/insecure_cors_policy.rb +1 -1
  95. data/components/checks/passive/grep/mixed_resource.rb +1 -1
  96. data/components/checks/passive/grep/password_autocomplete.rb +1 -1
  97. data/components/checks/passive/grep/private_ip.rb +1 -1
  98. data/components/checks/passive/grep/ssn.rb +6 -3
  99. data/components/checks/passive/grep/unencrypted_password_forms.rb +1 -1
  100. data/components/checks/passive/grep/x_frame_options.rb +4 -3
  101. data/components/checks/passive/htaccess_limit.rb +1 -1
  102. data/components/checks/passive/http_put.rb +1 -1
  103. data/components/checks/passive/insecure_client_access_policy.rb +2 -2
  104. data/components/checks/passive/insecure_cross_domain_policy_access.rb +2 -2
  105. data/components/checks/passive/insecure_cross_domain_policy_headers.rb +2 -2
  106. data/components/checks/passive/interesting_responses.rb +1 -1
  107. data/components/checks/passive/localstart_asp.rb +1 -1
  108. data/components/checks/passive/origin_spoof_access_restriction_bypass.rb +1 -1
  109. data/components/checks/passive/webdav.rb +1 -1
  110. data/components/checks/passive/xst.rb +1 -1
  111. data/components/fingerprinters/frameworks/aspx_mvc.rb +1 -1
  112. data/components/fingerprinters/frameworks/cakephp.rb +1 -1
  113. data/components/fingerprinters/frameworks/cherrypy.rb +1 -1
  114. data/components/fingerprinters/frameworks/django.rb +1 -1
  115. data/components/fingerprinters/frameworks/jsf.rb +1 -1
  116. data/components/fingerprinters/frameworks/nette.rb +1 -1
  117. data/components/fingerprinters/frameworks/rack.rb +1 -1
  118. data/components/fingerprinters/frameworks/rails.rb +1 -1
  119. data/components/fingerprinters/frameworks/symfony.rb +1 -1
  120. data/components/fingerprinters/languages/asp.rb +1 -1
  121. data/components/fingerprinters/languages/aspx.rb +1 -1
  122. data/components/fingerprinters/languages/java.rb +1 -1
  123. data/components/fingerprinters/languages/php.rb +1 -1
  124. data/components/fingerprinters/languages/python.rb +1 -1
  125. data/components/fingerprinters/languages/ruby.rb +1 -1
  126. data/components/fingerprinters/os/bsd.rb +1 -1
  127. data/components/fingerprinters/os/linux.rb +1 -1
  128. data/components/fingerprinters/os/solaris.rb +1 -1
  129. data/components/fingerprinters/os/unix.rb +1 -1
  130. data/components/fingerprinters/os/windows.rb +1 -1
  131. data/components/fingerprinters/servers/apache.rb +1 -1
  132. data/components/fingerprinters/servers/gunicorn.rb +1 -1
  133. data/components/fingerprinters/servers/iis.rb +1 -1
  134. data/components/fingerprinters/servers/jetty.rb +1 -1
  135. data/components/fingerprinters/servers/nginx.rb +1 -1
  136. data/components/fingerprinters/servers/tomcat.rb +1 -1
  137. data/components/path_extractors/anchors.rb +1 -1
  138. data/components/path_extractors/areas.rb +1 -1
  139. data/components/path_extractors/comments.rb +1 -1
  140. data/components/path_extractors/data_url.rb +1 -1
  141. data/components/path_extractors/forms.rb +1 -1
  142. data/components/path_extractors/frames.rb +1 -1
  143. data/components/path_extractors/generic.rb +1 -1
  144. data/components/path_extractors/links.rb +1 -1
  145. data/components/path_extractors/meta_refresh.rb +3 -3
  146. data/components/path_extractors/scripts.rb +1 -1
  147. data/components/plugins/autologin.rb +16 -24
  148. data/components/plugins/beep_notify.rb +1 -1
  149. data/components/plugins/content_types.rb +1 -1
  150. data/components/plugins/cookie_collector.rb +1 -1
  151. data/components/plugins/defaults/autothrottle.rb +1 -1
  152. data/components/plugins/defaults/healthmap.rb +1 -1
  153. data/components/plugins/defaults/meta/remedies/discovery.rb +10 -9
  154. data/components/plugins/defaults/meta/remedies/timing_attacks.rb +1 -1
  155. data/components/plugins/defaults/meta/uniformity.rb +1 -1
  156. data/components/plugins/email_notify.rb +3 -5
  157. data/components/plugins/exec.rb +1 -1
  158. data/components/plugins/form_dicattack.rb +1 -1
  159. data/components/plugins/headers_collector.rb +1 -1
  160. data/components/plugins/http_dicattack.rb +1 -1
  161. data/components/plugins/login_script.rb +47 -22
  162. data/components/plugins/metrics.rb +1 -1
  163. data/components/plugins/proxy.rb +69 -44
  164. data/components/plugins/proxy/panel/help.html.erb +1 -18
  165. data/components/plugins/proxy/panel/inspect.html.erb +4 -3
  166. data/components/plugins/proxy/panel/page_accordion.html.erb +78 -43
  167. data/components/plugins/proxy/panel/panel.html.erb +2 -7
  168. data/components/plugins/proxy/template_scope.rb +1 -1
  169. data/components/plugins/restrict_to_dom_state.rb +3 -15
  170. data/components/plugins/script.rb +1 -1
  171. data/components/plugins/uncommon_headers.rb +1 -1
  172. data/components/plugins/vector_collector.rb +1 -1
  173. data/components/plugins/vector_feed.rb +3 -11
  174. data/components/plugins/waf_detector.rb +1 -1
  175. data/components/reporters/ap.rb +1 -1
  176. data/components/reporters/html.rb +2 -2
  177. data/components/reporters/json.rb +1 -1
  178. data/components/reporters/marshal.rb +1 -1
  179. data/components/reporters/plugin_formatters/html/autologin.rb +1 -1
  180. data/components/reporters/plugin_formatters/html/content_types.rb +1 -1
  181. data/components/reporters/plugin_formatters/html/cookie_collector.rb +1 -1
  182. data/components/reporters/plugin_formatters/html/exec.rb +1 -1
  183. data/components/reporters/plugin_formatters/html/form_dicattack.rb +1 -1
  184. data/components/reporters/plugin_formatters/html/healthmap.rb +1 -1
  185. data/components/reporters/plugin_formatters/html/http_dicattack.rb +1 -1
  186. data/components/reporters/plugin_formatters/html/login_script.rb +1 -1
  187. data/components/reporters/plugin_formatters/html/metrics.rb +1 -1
  188. data/components/reporters/plugin_formatters/html/uncommon_headers.rb +1 -1
  189. data/components/reporters/plugin_formatters/html/uniformity.rb +1 -1
  190. data/components/reporters/plugin_formatters/html/vector_collector.rb +1 -1
  191. data/components/reporters/plugin_formatters/html/waf_detector.rb +1 -1
  192. data/components/reporters/plugin_formatters/stdout/autologin.rb +1 -1
  193. data/components/reporters/plugin_formatters/stdout/content_types.rb +1 -1
  194. data/components/reporters/plugin_formatters/stdout/cookie_collector.rb +1 -1
  195. data/components/reporters/plugin_formatters/stdout/exec.rb +1 -1
  196. data/components/reporters/plugin_formatters/stdout/form_dicattack.rb +1 -1
  197. data/components/reporters/plugin_formatters/stdout/healthmap.rb +1 -1
  198. data/components/reporters/plugin_formatters/stdout/http_dicattack.rb +1 -1
  199. data/components/reporters/plugin_formatters/stdout/login_script.rb +1 -1
  200. data/components/reporters/plugin_formatters/stdout/metrics.rb +1 -1
  201. data/components/reporters/plugin_formatters/stdout/uncommon_headers.rb +1 -1
  202. data/components/reporters/plugin_formatters/stdout/uniformity.rb +1 -1
  203. data/components/reporters/plugin_formatters/stdout/vector_collector.rb +1 -1
  204. data/components/reporters/plugin_formatters/stdout/waf_detector.rb +1 -1
  205. data/components/reporters/plugin_formatters/xml/autologin.rb +1 -1
  206. data/components/reporters/plugin_formatters/xml/content_types.rb +1 -1
  207. data/components/reporters/plugin_formatters/xml/cookie_collector.rb +1 -1
  208. data/components/reporters/plugin_formatters/xml/exec.rb +1 -1
  209. data/components/reporters/plugin_formatters/xml/form_dicattack.rb +1 -1
  210. data/components/reporters/plugin_formatters/xml/healthmap.rb +1 -1
  211. data/components/reporters/plugin_formatters/xml/http_dicattack.rb +1 -1
  212. data/components/reporters/plugin_formatters/xml/login_script.rb +1 -1
  213. data/components/reporters/plugin_formatters/xml/metrics.rb +1 -1
  214. data/components/reporters/plugin_formatters/xml/uncommon_headers.rb +1 -1
  215. data/components/reporters/plugin_formatters/xml/uniformity.rb +1 -1
  216. data/components/reporters/plugin_formatters/xml/vector_collector.rb +1 -1
  217. data/components/reporters/plugin_formatters/xml/waf_detector.rb +1 -1
  218. data/components/reporters/stdout.rb +1 -1
  219. data/components/reporters/txt.rb +1 -1
  220. data/components/reporters/xml.rb +29 -4
  221. data/components/reporters/yaml.rb +1 -1
  222. data/lib/arachni.rb +48 -3
  223. data/lib/arachni/banner.rb +1 -1
  224. data/lib/arachni/browser.rb +601 -358
  225. data/lib/arachni/browser/element_locator.rb +25 -6
  226. data/lib/arachni/browser/javascript.rb +103 -35
  227. data/lib/arachni/browser/javascript/dom_monitor.rb +1 -1
  228. data/lib/arachni/browser/javascript/proxy.rb +28 -16
  229. data/lib/arachni/browser/javascript/proxy/stub.rb +1 -1
  230. data/lib/arachni/browser/javascript/scripts/dom_monitor.js +138 -67
  231. data/lib/arachni/browser/javascript/scripts/polyfills.js +28 -0
  232. data/lib/arachni/browser/javascript/scripts/taint_tracer.js +27 -6
  233. data/lib/arachni/browser/javascript/taint_tracer.rb +1 -1
  234. data/lib/arachni/browser/javascript/taint_tracer/frame.rb +1 -1
  235. data/lib/arachni/browser/javascript/taint_tracer/frame/called_function.rb +1 -1
  236. data/lib/arachni/browser/javascript/taint_tracer/sink/base.rb +1 -1
  237. data/lib/arachni/browser/javascript/taint_tracer/sink/data_flow.rb +1 -1
  238. data/lib/arachni/browser/javascript/taint_tracer/sink/execution_flow.rb +1 -1
  239. data/lib/arachni/browser_cluster.rb +10 -14
  240. data/lib/arachni/browser_cluster/job.rb +1 -1
  241. data/lib/arachni/browser_cluster/job/result.rb +1 -1
  242. data/lib/arachni/browser_cluster/jobs/browser_provider.rb +1 -1
  243. data/lib/arachni/browser_cluster/jobs/{resource_exploration.rb → dom_exploration.rb} +5 -5
  244. data/lib/arachni/browser_cluster/jobs/{resource_exploration → dom_exploration}/event_trigger.rb +7 -4
  245. data/lib/arachni/browser_cluster/jobs/{resource_exploration → dom_exploration}/event_trigger/result.rb +3 -3
  246. data/lib/arachni/browser_cluster/jobs/{resource_exploration → dom_exploration}/result.rb +2 -2
  247. data/lib/arachni/browser_cluster/jobs/taint_trace.rb +3 -3
  248. data/lib/arachni/browser_cluster/jobs/taint_trace/event_trigger.rb +2 -2
  249. data/lib/arachni/browser_cluster/jobs/taint_trace/event_trigger/result.rb +2 -2
  250. data/lib/arachni/browser_cluster/jobs/taint_trace/result.rb +1 -1
  251. data/lib/arachni/browser_cluster/worker.rb +12 -40
  252. data/lib/arachni/check.rb +1 -1
  253. data/lib/arachni/check/auditor.rb +15 -1
  254. data/lib/arachni/check/base.rb +1 -1
  255. data/lib/arachni/check/manager.rb +1 -1
  256. data/lib/arachni/component.rb +1 -1
  257. data/lib/arachni/component/base.rb +5 -5
  258. data/lib/arachni/component/manager.rb +39 -13
  259. data/lib/arachni/component/options.rb +1 -1
  260. data/lib/arachni/component/options/address.rb +1 -1
  261. data/lib/arachni/component/options/base.rb +1 -1
  262. data/lib/arachni/component/options/bool.rb +1 -1
  263. data/lib/arachni/component/options/float.rb +1 -1
  264. data/lib/arachni/component/options/int.rb +1 -1
  265. data/lib/arachni/component/options/multiple_choice.rb +1 -1
  266. data/lib/arachni/component/options/object.rb +1 -1
  267. data/lib/arachni/component/options/path.rb +1 -1
  268. data/lib/arachni/component/options/port.rb +1 -1
  269. data/lib/arachni/component/options/string.rb +1 -1
  270. data/lib/arachni/component/options/url.rb +1 -1
  271. data/lib/arachni/component/output.rb +1 -1
  272. data/lib/arachni/component/utilities.rb +1 -1
  273. data/lib/arachni/data.rb +1 -1
  274. data/lib/arachni/data/framework.rb +1 -1
  275. data/lib/arachni/data/framework/rpc.rb +1 -1
  276. data/lib/arachni/data/issues.rb +1 -1
  277. data/lib/arachni/data/plugins.rb +1 -1
  278. data/lib/arachni/data/session.rb +1 -1
  279. data/lib/arachni/element/base.rb +19 -5
  280. data/lib/arachni/element/body.rb +1 -1
  281. data/lib/arachni/element/capabilities/analyzable.rb +1 -1
  282. data/lib/arachni/element/capabilities/analyzable/differential.rb +15 -5
  283. data/lib/arachni/element/capabilities/analyzable/signature.rb +147 -89
  284. data/lib/arachni/element/capabilities/analyzable/timeout.rb +43 -16
  285. data/lib/arachni/element/capabilities/auditable.rb +20 -15
  286. data/lib/arachni/element/capabilities/dom_only.rb +5 -4
  287. data/lib/arachni/element/capabilities/inputtable.rb +62 -12
  288. data/lib/arachni/element/capabilities/mutable.rb +74 -13
  289. data/lib/arachni/element/capabilities/refreshable.rb +1 -1
  290. data/lib/arachni/element/capabilities/submittable.rb +5 -2
  291. data/lib/arachni/element/capabilities/with_auditor.rb +1 -1
  292. data/lib/arachni/element/capabilities/with_auditor/output.rb +5 -5
  293. data/lib/arachni/element/capabilities/with_dom.rb +1 -1
  294. data/lib/arachni/element/capabilities/with_node.rb +2 -2
  295. data/lib/arachni/element/capabilities/with_scope.rb +1 -1
  296. data/lib/arachni/element/capabilities/with_scope/scope.rb +1 -1
  297. data/lib/arachni/element/capabilities/with_source.rb +4 -4
  298. data/lib/arachni/element/cookie.rb +57 -34
  299. data/lib/arachni/element/cookie/capabilities/inputtable.rb +1 -1
  300. data/lib/arachni/element/cookie/capabilities/mutable.rb +10 -1
  301. data/lib/arachni/element/cookie/capabilities/with_dom.rb +1 -1
  302. data/lib/arachni/element/cookie/dom.rb +1 -1
  303. data/lib/arachni/element/dom.rb +1 -15
  304. data/lib/arachni/element/dom/capabilities/auditable.rb +1 -1
  305. data/lib/arachni/element/dom/capabilities/inputtable.rb +1 -1
  306. data/lib/arachni/element/dom/capabilities/locatable.rb +29 -0
  307. data/lib/arachni/element/dom/capabilities/mutable.rb +11 -1
  308. data/lib/arachni/element/dom/capabilities/submittable.rb +2 -2
  309. data/lib/arachni/element/form.rb +33 -14
  310. data/lib/arachni/element/form/capabilities/auditable.rb +1 -1
  311. data/lib/arachni/element/form/capabilities/mutable.rb +18 -17
  312. data/lib/arachni/element/form/capabilities/submittable.rb +1 -1
  313. data/lib/arachni/element/form/capabilities/with_dom.rb +2 -1
  314. data/lib/arachni/element/form/dom.rb +3 -2
  315. data/lib/arachni/element/generic_dom.rb +1 -1
  316. data/lib/arachni/element/header.rb +16 -4
  317. data/lib/arachni/element/header/capabilities/inputtable.rb +1 -1
  318. data/lib/arachni/element/header/capabilities/mutable.rb +11 -1
  319. data/lib/arachni/element/json.rb +2 -2
  320. data/lib/arachni/element/json/capabilities/inputtable.rb +1 -1
  321. data/lib/arachni/element/json/capabilities/mutable.rb +8 -2
  322. data/lib/arachni/element/link.rb +14 -7
  323. data/lib/arachni/element/link/capabilities/auditable.rb +1 -1
  324. data/lib/arachni/element/link/capabilities/submittable.rb +1 -1
  325. data/lib/arachni/element/link/capabilities/with_dom.rb +8 -1
  326. data/lib/arachni/element/link/dom.rb +2 -1
  327. data/lib/arachni/element/link/dom/capabilities/submittable.rb +1 -1
  328. data/lib/arachni/element/link_template.rb +8 -3
  329. data/lib/arachni/element/link_template/capabilities/auditable.rb +1 -1
  330. data/lib/arachni/element/link_template/capabilities/inputtable.rb +1 -1
  331. data/lib/arachni/element/link_template/capabilities/with_dom.rb +1 -1
  332. data/lib/arachni/element/link_template/dom.rb +2 -1
  333. data/lib/arachni/element/link_template/dom/capabilities/submittable.rb +1 -1
  334. data/lib/arachni/element/path.rb +1 -1
  335. data/lib/arachni/element/server.rb +3 -3
  336. data/lib/arachni/element/ui_form.rb +24 -21
  337. data/lib/arachni/element/ui_form/dom.rb +12 -3
  338. data/lib/arachni/element/ui_input.rb +17 -11
  339. data/lib/arachni/element/{input → ui_input}/dom.rb +11 -2
  340. data/lib/arachni/element/xml.rb +3 -3
  341. data/lib/arachni/element/xml/capabilities/inputtable.rb +7 -1
  342. data/lib/arachni/element/xml/capabilities/mutable.rb +7 -13
  343. data/lib/arachni/element_filter.rb +1 -1
  344. data/lib/arachni/error.rb +1 -1
  345. data/lib/arachni/ethon/easy.rb +1 -1
  346. data/lib/arachni/framework.rb +2 -5
  347. data/lib/arachni/framework/parts/audit.rb +8 -2
  348. data/lib/arachni/framework/parts/browser.rb +8 -9
  349. data/lib/arachni/framework/parts/check.rb +2 -6
  350. data/lib/arachni/framework/parts/data.rb +23 -8
  351. data/lib/arachni/framework/parts/platform.rb +1 -1
  352. data/lib/arachni/framework/parts/plugin.rb +2 -8
  353. data/lib/arachni/framework/parts/report.rb +3 -9
  354. data/lib/arachni/framework/parts/scope.rb +1 -1
  355. data/lib/arachni/framework/parts/state.rb +8 -8
  356. data/lib/arachni/http.rb +1 -1
  357. data/lib/arachni/http/client.rb +72 -68
  358. data/lib/arachni/http/client/dynamic_404_handler.rb +85 -60
  359. data/lib/arachni/http/cookie_jar.rb +48 -27
  360. data/lib/arachni/http/headers.rb +4 -3
  361. data/lib/arachni/http/message.rb +17 -3
  362. data/lib/arachni/http/message/scope.rb +1 -1
  363. data/lib/arachni/http/proxy_server.rb +46 -344
  364. data/lib/arachni/http/proxy_server/connection.rb +316 -0
  365. data/lib/arachni/http/proxy_server/ssl_interceptor.rb +102 -0
  366. data/lib/arachni/http/proxy_server/tunnel.rb +54 -0
  367. data/lib/arachni/http/request.rb +126 -29
  368. data/lib/arachni/http/request/scope.rb +1 -1
  369. data/lib/arachni/http/response.rb +42 -12
  370. data/lib/arachni/http/response/scope.rb +1 -1
  371. data/lib/arachni/issue.rb +2 -2
  372. data/lib/arachni/issue/severity.rb +1 -1
  373. data/lib/arachni/issue/severity/base.rb +1 -1
  374. data/lib/arachni/option_group.rb +1 -1
  375. data/lib/arachni/option_groups.rb +1 -1
  376. data/lib/arachni/option_groups/audit.rb +20 -4
  377. data/lib/arachni/option_groups/browser_cluster.rb +8 -4
  378. data/lib/arachni/option_groups/datastore.rb +1 -1
  379. data/lib/arachni/option_groups/dispatcher.rb +1 -1
  380. data/lib/arachni/option_groups/http.rb +2 -2
  381. data/lib/arachni/option_groups/input.rb +6 -3
  382. data/lib/arachni/option_groups/output.rb +1 -1
  383. data/lib/arachni/option_groups/paths.rb +10 -3
  384. data/lib/arachni/option_groups/rpc.rb +1 -1
  385. data/lib/arachni/option_groups/scope.rb +35 -6
  386. data/lib/arachni/option_groups/session.rb +1 -1
  387. data/lib/arachni/option_groups/snapshot.rb +1 -1
  388. data/lib/arachni/options.rb +1 -1
  389. data/lib/arachni/page.rb +26 -12
  390. data/lib/arachni/page/dom.rb +29 -22
  391. data/lib/arachni/page/dom/transition.rb +2 -2
  392. data/lib/arachni/page/scope.rb +1 -1
  393. data/lib/arachni/parser.rb +42 -5
  394. data/lib/arachni/platform.rb +1 -1
  395. data/lib/arachni/platform/fingerprinter.rb +1 -1
  396. data/lib/arachni/platform/list.rb +1 -1
  397. data/lib/arachni/platform/manager.rb +2 -2
  398. data/lib/arachni/plugin.rb +1 -1
  399. data/lib/arachni/plugin/base.rb +1 -1
  400. data/lib/arachni/plugin/formatter.rb +1 -1
  401. data/lib/arachni/plugin/manager.rb +7 -13
  402. data/lib/arachni/processes.rb +1 -1
  403. data/lib/arachni/processes/dispatchers.rb +2 -2
  404. data/lib/arachni/processes/executables/base.rb +45 -4
  405. data/lib/arachni/processes/executables/browser.rb +91 -0
  406. data/lib/arachni/processes/executables/rest_service.rb +14 -0
  407. data/lib/arachni/processes/helpers.rb +1 -1
  408. data/lib/arachni/processes/helpers/dispatchers.rb +1 -1
  409. data/lib/arachni/processes/helpers/instances.rb +1 -1
  410. data/lib/arachni/processes/helpers/processes.rb +1 -1
  411. data/lib/arachni/processes/instances.rb +5 -5
  412. data/lib/arachni/processes/manager.rb +68 -9
  413. data/lib/arachni/report.rb +1 -1
  414. data/lib/arachni/reporter.rb +1 -1
  415. data/lib/arachni/reporter/base.rb +1 -1
  416. data/lib/arachni/reporter/formatter_manager.rb +4 -2
  417. data/lib/arachni/reporter/manager.rb +3 -2
  418. data/lib/arachni/reporter/options.rb +1 -1
  419. data/lib/arachni/rest/server.rb +231 -0
  420. data/lib/arachni/rest/server/instance_helpers.rb +37 -0
  421. data/lib/arachni/rpc/client/base.rb +1 -1
  422. data/lib/arachni/rpc/client/dispatcher.rb +1 -1
  423. data/lib/arachni/rpc/client/instance.rb +1 -1
  424. data/lib/arachni/rpc/client/instance/framework.rb +1 -1
  425. data/lib/arachni/rpc/client/instance/service.rb +1 -1
  426. data/lib/arachni/rpc/serializer.rb +1 -1
  427. data/lib/arachni/rpc/server/active_options.rb +20 -3
  428. data/lib/arachni/rpc/server/base.rb +1 -1
  429. data/lib/arachni/rpc/server/check/manager.rb +1 -1
  430. data/lib/arachni/rpc/server/dispatcher.rb +4 -4
  431. data/lib/arachni/rpc/server/dispatcher/node.rb +1 -1
  432. data/lib/arachni/rpc/server/dispatcher/service.rb +1 -1
  433. data/lib/arachni/rpc/server/framework.rb +3 -1
  434. data/lib/arachni/rpc/server/framework/distributor.rb +1 -1
  435. data/lib/arachni/rpc/server/framework/master.rb +1 -1
  436. data/lib/arachni/rpc/server/framework/multi_instance.rb +1 -1
  437. data/lib/arachni/rpc/server/framework/slave.rb +1 -1
  438. data/lib/arachni/rpc/server/instance.rb +1 -3
  439. data/lib/arachni/rpc/server/output.rb +1 -1
  440. data/lib/arachni/rpc/server/plugin/manager.rb +1 -1
  441. data/lib/arachni/ruby.rb +1 -2
  442. data/lib/arachni/ruby/array.rb +1 -1
  443. data/lib/arachni/ruby/hash.rb +1 -1
  444. data/lib/arachni/ruby/object.rb +15 -1
  445. data/lib/arachni/ruby/set.rb +1 -1
  446. data/lib/arachni/ruby/string.rb +23 -4
  447. data/lib/arachni/ruby/webrick.rb +1 -1
  448. data/lib/arachni/ruby/webrick/cookie.rb +1 -1
  449. data/lib/arachni/ruby/webrick/httprequest.rb +1 -1
  450. data/lib/arachni/scope.rb +1 -1
  451. data/lib/arachni/{watir → selenium/webdriver}/element.rb +12 -13
  452. data/lib/arachni/session.rb +19 -4
  453. data/lib/arachni/snapshot.rb +9 -5
  454. data/lib/arachni/state.rb +1 -1
  455. data/lib/arachni/state/audit.rb +1 -1
  456. data/lib/arachni/state/element_filter.rb +1 -1
  457. data/lib/arachni/state/framework.rb +1 -1
  458. data/lib/arachni/state/framework/rpc.rb +1 -1
  459. data/lib/arachni/state/http.rb +1 -1
  460. data/lib/arachni/state/options.rb +1 -1
  461. data/lib/arachni/state/plugins.rb +1 -1
  462. data/lib/arachni/support.rb +2 -1
  463. data/lib/arachni/support/buffer.rb +1 -1
  464. data/lib/arachni/support/buffer/autoflush.rb +1 -1
  465. data/lib/arachni/support/buffer/base.rb +1 -1
  466. data/lib/arachni/support/cache.rb +1 -1
  467. data/lib/arachni/support/cache/base.rb +20 -8
  468. data/lib/arachni/support/cache/least_cost_replacement.rb +1 -1
  469. data/lib/arachni/support/cache/least_recently_pushed.rb +1 -1
  470. data/lib/arachni/support/cache/least_recently_used.rb +8 -9
  471. data/lib/arachni/support/cache/preference.rb +7 -20
  472. data/lib/arachni/support/cache/random_replacement.rb +1 -1
  473. data/lib/arachni/support/crypto.rb +1 -1
  474. data/lib/arachni/support/crypto/rsa_aes_cbc.rb +1 -1
  475. data/lib/arachni/support/database.rb +1 -1
  476. data/lib/arachni/support/database/base.rb +2 -2
  477. data/lib/arachni/support/database/hash.rb +1 -1
  478. data/lib/arachni/support/database/queue.rb +1 -1
  479. data/lib/arachni/support/glob.rb +35 -0
  480. data/lib/arachni/support/lookup.rb +1 -1
  481. data/lib/arachni/support/lookup/base.rb +1 -1
  482. data/lib/arachni/support/lookup/hash_set.rb +1 -1
  483. data/lib/arachni/support/lookup/moolb.rb +1 -1
  484. data/lib/arachni/support/mixins.rb +1 -1
  485. data/lib/arachni/support/mixins/observable.rb +1 -1
  486. data/lib/arachni/support/mixins/terminal.rb +1 -1
  487. data/lib/arachni/support/profiler.rb +12 -10
  488. data/lib/arachni/support/signature.rb +12 -5
  489. data/lib/arachni/trainer.rb +18 -4
  490. data/lib/arachni/ui/foo/output.rb +17 -1
  491. data/lib/arachni/uri.rb +285 -203
  492. data/lib/arachni/uri/scope.rb +13 -2
  493. data/lib/arachni/utilities.rb +22 -5
  494. data/lib/arachni/version.rb +1 -1
  495. data/lib/version +1 -1
  496. data/spec/arachni/browser/element_locator_spec.rb +42 -14
  497. data/spec/arachni/browser/javascript/dom_monitor_spec.rb +34 -304
  498. data/spec/arachni/browser/javascript/polyfills_spec.rb +35 -0
  499. data/spec/arachni/browser/javascript/taint_tracer_spec.rb +24 -4
  500. data/spec/arachni/browser/javascript_spec.rb +92 -65
  501. data/spec/arachni/browser_cluster/job_spec.rb +3 -3
  502. data/spec/arachni/browser_cluster/jobs/{resource_exploration → dom_exploration}/event_trigger/result_spec.rb +1 -1
  503. data/spec/arachni/browser_cluster/jobs/{resource_exploration → dom_exploration}/event_trigger_spec.rb +4 -4
  504. data/spec/arachni/browser_cluster/jobs/{resource_exploration → dom_exploration}/result_spec.rb +1 -1
  505. data/spec/arachni/browser_cluster/jobs/{resource_exploration_spec.rb → dom_exploration_spec.rb} +4 -4
  506. data/spec/arachni/browser_cluster/jobs/taint_tracer_spec.rb +9 -9
  507. data/spec/arachni/browser_cluster/worker_spec.rb +46 -67
  508. data/spec/arachni/browser_cluster_spec.rb +19 -17
  509. data/spec/arachni/browser_spec.rb +506 -183
  510. data/spec/arachni/check/auditor_spec.rb +70 -25
  511. data/spec/arachni/component/manager_spec.rb +19 -20
  512. data/spec/arachni/data/framework/rpc_spec.rb +1 -1
  513. data/spec/arachni/data/framework_spec.rb +1 -1
  514. data/spec/arachni/data/issues_spec.rb +3 -3
  515. data/spec/arachni/element/capabilities/analyzable/differential_spec.rb +44 -0
  516. data/spec/arachni/element/capabilities/analyzable/signature_spec.rb +33 -162
  517. data/spec/arachni/element/capabilities/analyzable/timeout_spec.rb +4 -4
  518. data/spec/arachni/element/cookie_spec.rb +98 -49
  519. data/spec/arachni/element/form/dom_spec.rb +1 -22
  520. data/spec/arachni/element/form_spec.rb +7 -7
  521. data/spec/arachni/element/header_spec.rb +2 -2
  522. data/spec/arachni/element/json_spec.rb +2 -2
  523. data/spec/arachni/element/link/dom_spec.rb +1 -22
  524. data/spec/arachni/element/link_spec.rb +17 -1
  525. data/spec/arachni/element/link_template/dom_spec.rb +1 -22
  526. data/spec/arachni/element/link_template_spec.rb +3 -3
  527. data/spec/arachni/element/ui_form/{ui_form_dom_spec.rb → dom_spec.rb} +72 -22
  528. data/spec/arachni/element/ui_form_spec.rb +1 -0
  529. data/spec/arachni/element/ui_input/dom_spec.rb +64 -22
  530. data/spec/arachni/element/ui_input_spec.rb +1 -0
  531. data/spec/arachni/element/xml_spec.rb +1 -0
  532. data/spec/arachni/framework/parts/audit_spec.rb +7 -5
  533. data/spec/arachni/framework/parts/browser_spec.rb +8 -8
  534. data/spec/arachni/framework/parts/check_spec.rb +1 -1
  535. data/spec/arachni/framework/parts/data_spec.rb +4 -4
  536. data/spec/arachni/framework/parts/scope_spec.rb +2 -2
  537. data/spec/arachni/framework_spec.rb +1 -1
  538. data/spec/arachni/http/client/dynamic_404_handlers_spec.rb +26 -13
  539. data/spec/arachni/http/client_spec.rb +80 -45
  540. data/spec/arachni/http/cookie_jar_spec.rb +6 -6
  541. data/spec/arachni/http/proxy_server_spec.rb +69 -66
  542. data/spec/arachni/http/request_spec.rb +147 -23
  543. data/spec/arachni/http/response/scope_spec.rb +12 -12
  544. data/spec/arachni/http/response_spec.rb +62 -4
  545. data/spec/arachni/issue_spec.rb +6 -6
  546. data/spec/arachni/option_groups/audit_spec.rb +25 -8
  547. data/spec/arachni/option_groups/browser_cluster_spec.rb +27 -1
  548. data/spec/arachni/option_groups/dispatcher_spec.rb +3 -3
  549. data/spec/arachni/option_groups/input_spec.rb +9 -9
  550. data/spec/arachni/option_groups/paths_spec.rb +2 -2
  551. data/spec/arachni/option_groups/scope_spec.rb +32 -16
  552. data/spec/arachni/options_spec.rb +4 -4
  553. data/spec/arachni/page/dom/transition_spec.rb +17 -10
  554. data/spec/arachni/page/dom_spec.rb +19 -0
  555. data/spec/arachni/page/scope_spec.rb +4 -4
  556. data/spec/arachni/page_spec.rb +15 -15
  557. data/spec/arachni/platform/manager_spec.rb +2 -2
  558. data/spec/arachni/plugin/base_spec.rb +1 -0
  559. data/spec/arachni/reporter/base_spec.rb +2 -2
  560. data/spec/arachni/reporter/manager_spec.rb +2 -2
  561. data/spec/arachni/rest/server_spec.rb +495 -0
  562. data/spec/arachni/rpc/server/active_options_spec.rb +63 -12
  563. data/spec/arachni/rpc/server/base_spec.rb +1 -1
  564. data/spec/arachni/rpc/server/framework/distributor_spec.rb +2 -2
  565. data/spec/arachni/rpc/server/framework_multi_spec.rb +6 -6
  566. data/spec/arachni/rpc/server/framework_spec.rb +4 -4
  567. data/spec/arachni/rpc/server/instance_spec.rb +24 -24
  568. data/spec/arachni/ruby/array_spec.rb +2 -2
  569. data/spec/arachni/ruby/string_spec.rb +52 -0
  570. data/spec/arachni/session_spec.rb +19 -2
  571. data/spec/arachni/snapshot_spec.rb +1 -1
  572. data/spec/arachni/state/audit_spec.rb +1 -1
  573. data/spec/arachni/state/framework_spec.rb +2 -2
  574. data/spec/arachni/support/cache/least_recently_used_spec.rb +0 -2
  575. data/spec/arachni/support/glob_spec.rb +75 -0
  576. data/spec/arachni/support/lookup/hash_set_spec.rb +1 -1
  577. data/spec/arachni/support/lookup/moolb_spec.rb +2 -2
  578. data/spec/arachni/support/signature_spec.rb +4 -4
  579. data/spec/arachni/trainer_spec.rb +48 -4
  580. data/spec/arachni/uri/scope_spec.rb +54 -10
  581. data/spec/arachni/uri_spec.rb +110 -89
  582. data/spec/arachni/utilities_spec.rb +8 -8
  583. data/spec/components/checks/active/code_injection_spec.rb +9 -9
  584. data/spec/components/checks/active/file_inclusion_spec.rb +20 -20
  585. data/spec/components/checks/active/ldap_injection_spec.rb +1 -1
  586. data/spec/components/checks/active/no_sql_injection_spec.rb +1 -1
  587. data/spec/components/checks/active/os_cmd_injection_spec.rb +3 -3
  588. data/spec/components/checks/active/path_traversal_spec.rb +11 -11
  589. data/spec/components/checks/active/response_splitting_spec.rb +2 -2
  590. data/spec/components/checks/active/rfi_spec.rb +3 -3
  591. data/spec/components/checks/active/session_fixation_spec.rb +1 -1
  592. data/spec/components/checks/active/source_code_disclosure_spec.rb +4 -4
  593. data/spec/components/checks/active/sql_injection_spec.rb +58 -59
  594. data/spec/components/checks/active/unvalidated_redirect_spec.rb +2 -2
  595. data/spec/components/checks/active/xpath_injection_spec.rb +3 -3
  596. data/spec/components/checks/active/xss_dom_script_context_spec.rb +1 -1
  597. data/spec/components/checks/active/xss_dom_spec.rb +1 -1
  598. data/spec/components/checks/active/xss_script_context_spec.rb +5 -5
  599. data/spec/components/checks/active/xss_spec.rb +5 -5
  600. data/spec/components/checks/passive/grep/credit_card_spec.rb +1 -1
  601. data/spec/components/checks/passive/grep/emails_spec.rb +12 -2
  602. data/spec/components/checks/passive/grep/ssn_spec.rb +1 -1
  603. data/spec/components/path_extractors/meta_refresh_spec.rb +3 -1
  604. data/spec/components/plugins/exec_spec.rb +2 -2
  605. data/spec/components/plugins/login_script_spec.rb +22 -2
  606. data/spec/components/plugins/vector_feed_spec.rb +3 -3
  607. data/spec/spec_helper.rb +10 -4
  608. data/spec/support/factories/browser_cluster/job.rb +1 -0
  609. data/spec/support/fixtures/check_with_invalid_platforms/with_invalid_platforms.rb +1 -1
  610. data/spec/support/fixtures/checks/test.rb +1 -1
  611. data/spec/support/fixtures/checks/test2.rb +1 -1
  612. data/spec/support/fixtures/checks/test3.rb +1 -1
  613. data/spec/support/fixtures/fingerprinters/test.rb +1 -1
  614. data/spec/support/fixtures/plugins/bad.rb +1 -1
  615. data/spec/support/fixtures/plugins/defaults/default.rb +1 -1
  616. data/spec/support/fixtures/plugins/distributable.rb +1 -1
  617. data/spec/support/fixtures/plugins/loop.rb +1 -1
  618. data/spec/support/fixtures/plugins/suspendable.rb +1 -1
  619. data/spec/support/fixtures/plugins/wait.rb +1 -1
  620. data/spec/support/fixtures/plugins/with_options.rb +1 -1
  621. data/spec/support/fixtures/plugins_with_priorities/p0.rb +1 -1
  622. data/spec/support/fixtures/plugins_with_priorities/p00.rb +1 -1
  623. data/spec/support/fixtures/plugins_with_priorities/p1.rb +1 -1
  624. data/spec/support/fixtures/plugins_with_priorities/p2.rb +1 -1
  625. data/spec/support/fixtures/plugins_with_priorities/p22.rb +1 -1
  626. data/spec/support/fixtures/plugins_with_priorities/p222.rb +1 -1
  627. data/spec/support/fixtures/plugins_with_priorities/p_nil.rb +1 -1
  628. data/spec/support/fixtures/plugins_with_priorities/p_nil2.rb +1 -1
  629. data/spec/support/fixtures/report.afr +0 -0
  630. data/spec/support/fixtures/reporters/base_spec/plugin_formatters/with_formatters/foobar.rb +1 -1
  631. data/spec/support/fixtures/reporters/base_spec/with_formatters.rb +1 -1
  632. data/spec/support/fixtures/reporters/base_spec/with_outfile.rb +1 -1
  633. data/spec/support/fixtures/reporters/base_spec/without_outfile.rb +1 -1
  634. data/spec/support/fixtures/reporters/manager_spec/afr.rb +1 -1
  635. data/spec/support/fixtures/reporters/manager_spec/error.rb +1 -1
  636. data/spec/support/fixtures/reporters/manager_spec/foo.rb +1 -1
  637. data/spec/support/fixtures/run_check/body.rb +1 -1
  638. data/spec/support/fixtures/run_check/cookies.rb +1 -1
  639. data/spec/support/fixtures/run_check/empty.rb +1 -1
  640. data/spec/support/fixtures/run_check/flch.rb +1 -1
  641. data/spec/support/fixtures/run_check/forms.rb +1 -1
  642. data/spec/support/fixtures/run_check/headers.rb +1 -1
  643. data/spec/support/fixtures/run_check/links.rb +1 -1
  644. data/spec/support/fixtures/run_check/nil.rb +1 -1
  645. data/spec/support/fixtures/run_check/path.rb +1 -1
  646. data/spec/support/fixtures/run_check/server.rb +1 -1
  647. data/spec/support/fixtures/signature_check/signature.rb +1 -1
  648. data/spec/support/fixtures/wait_check/wait.rb +1 -1
  649. data/spec/support/helpers/framework.rb +1 -1
  650. data/spec/support/helpers/misc.rb +1 -1
  651. data/spec/support/helpers/paths.rb +1 -1
  652. data/spec/support/helpers/request_helpers.rb +38 -0
  653. data/spec/support/helpers/requires.rb +1 -1
  654. data/spec/support/helpers/resets.rb +1 -1
  655. data/spec/support/helpers/web_server.rb +1 -1
  656. data/spec/support/lib/factory.rb +1 -1
  657. data/spec/support/lib/web_server_client.rb +1 -1
  658. data/spec/support/lib/web_server_dispatcher.rb +1 -1
  659. data/spec/support/lib/web_server_manager.rb +2 -2
  660. data/spec/support/servers/arachni/browser.rb +182 -15
  661. data/spec/support/servers/arachni/browser/javascript/angular-1.2.8.js +1 -1
  662. data/spec/support/servers/arachni/browser/javascript/angular-route.js +1 -1
  663. data/spec/support/servers/arachni/browser/javascript/dom_monitor.rb +27 -4
  664. data/spec/support/servers/arachni/element/capabilities/analyzable/differential.rb +103 -0
  665. data/spec/support/servers/arachni/element/capabilities/analyzable/timeout.rb +5 -2
  666. data/spec/support/servers/arachni/element/header.rb +1 -1
  667. data/spec/support/servers/arachni/http/client.rb +46 -0
  668. data/spec/support/servers/arachni/http/client/dynamic_404_handler.rb +7 -1
  669. data/spec/support/servers/checks/active/code_injection.rb +5 -5
  670. data/spec/support/servers/checks/active/no_sql_injection.rb +0 -6
  671. data/spec/support/servers/checks/active/no_sql_injection_differential.rb +1 -1
  672. data/spec/support/servers/checks/active/sql_injection.rb +5 -2
  673. data/spec/support/servers/checks/active/sql_injection_differential.rb +1 -1
  674. data/spec/support/servers/checks/active/trainer_check.rb +6 -6
  675. data/spec/support/servers/checks/passive/backdoors.rb +1 -0
  676. data/spec/support/servers/checks/passive/backup_directories.rb +2 -0
  677. data/spec/support/servers/checks/passive/backup_files.rb +2 -0
  678. data/spec/support/servers/checks/passive/grep/emails.rb +6 -6
  679. data/spec/support/shared/check.rb +28 -0
  680. data/spec/support/shared/element/capabilities/auditable.rb +76 -13
  681. data/spec/support/shared/element/capabilities/dom_only.rb +5 -6
  682. data/spec/support/shared/element/capabilities/inputtable.rb +74 -4
  683. data/spec/support/shared/element/capabilities/mutable.rb +86 -14
  684. data/spec/support/shared/element/capabilities/submittable.rb +12 -0
  685. data/spec/support/shared/element/capabilities/with_dom.rb +13 -4
  686. data/spec/support/shared/element/capabilities/with_node.rb +1 -1
  687. data/spec/support/shared/element/capabilities/with_source.rb +1 -6
  688. data/spec/support/shared/element/dom/locatable.rb +20 -0
  689. data/spec/support/shared/element/dom/submittable.rb +4 -17
  690. data/spec/support/shared/http/message.rb +37 -5
  691. data/spec/support/shared/support/cache.rb +5 -4
  692. data/ui/cli/framework.rb +4 -3
  693. data/ui/cli/framework/option_parser.rb +20 -8
  694. data/ui/cli/option_parser.rb +1 -1
  695. data/ui/cli/output.rb +40 -4
  696. data/ui/cli/reporter.rb +1 -1
  697. data/ui/cli/reporter/option_parser.rb +4 -4
  698. data/ui/cli/rest/server.rb +43 -0
  699. data/ui/cli/rest/server/option_parser.rb +115 -0
  700. data/ui/cli/restored_framework.rb +1 -1
  701. data/ui/cli/restored_framework/option_parser.rb +1 -1
  702. data/ui/cli/rpc/client/dispatcher_monitor.rb +1 -1
  703. data/ui/cli/rpc/client/dispatcher_monitor/option_parser.rb +1 -1
  704. data/ui/cli/rpc/client/instance.rb +1 -1
  705. data/ui/cli/rpc/client/local.rb +1 -1
  706. data/ui/cli/rpc/client/local/option_parser.rb +1 -1
  707. data/ui/cli/rpc/client/remote.rb +1 -1
  708. data/ui/cli/rpc/client/remote/option_parser.rb +1 -1
  709. data/ui/cli/rpc/server/dispatcher.rb +1 -1
  710. data/ui/cli/rpc/server/dispatcher/option_parser.rb +1 -1
  711. data/ui/cli/utilities.rb +1 -1
  712. metadata +197 -84
  713. data/components/checks/active/no_sql_injection/patterns/mongodb +0 -1
  714. data/components/checks/active/no_sql_injection/regexp_ignore.txt +0 -0
  715. data/components/checks/active/sql_injection/patterns/access +0 -3
  716. data/components/checks/active/sql_injection/patterns/db2 +0 -5
  717. data/components/checks/active/sql_injection/patterns/frontbase +0 -1
  718. data/components/checks/active/sql_injection/patterns/hsqldb +0 -1
  719. data/components/checks/active/sql_injection/patterns/ingres +0 -3
  720. data/components/checks/active/sql_injection/patterns/maxdb +0 -2
  721. data/components/checks/active/sql_injection/patterns/mssql +0 -25
  722. data/components/checks/active/sql_injection/patterns/oracle +0 -6
  723. data/components/checks/active/sql_injection/patterns/sqlite +0 -5
  724. data/components/checks/active/sql_injection/patterns/sybase +0 -3
  725. data/lib/arachni/ruby/io.rb +0 -39
  726. data/lib/arachni/selenium/webdriver/remote/http/typhoeus.rb +0 -63
  727. data/spec/arachni/ruby/io_spec.rb +0 -26
@@ -1,5 +1,5 @@
1
1
  =begin
2
- Copyright 2010-2015 Tasos Laskos <tasos.laskos@arachni-scanner.com>
2
+ Copyright 2010-2016 Tasos Laskos <tasos.laskos@arachni-scanner.com>
3
3
 
4
4
  This file is part of the Arachni Framework project and is subject to
5
5
  redistribution and commercial restrictions. Please see the Arachni Framework
@@ -1,5 +1,5 @@
1
1
  =begin
2
- Copyright 2010-2015 Tasos Laskos <tasos.laskos@arachni-scanner.com>
2
+ Copyright 2010-2016 Tasos Laskos <tasos.laskos@arachni-scanner.com>
3
3
 
4
4
  This file is part of the Arachni Framework project and is subject to
5
5
  redistribution and commercial restrictions. Please see the Arachni Framework
@@ -1,5 +1,5 @@
1
1
  =begin
2
- Copyright 2010-2015 Tasos Laskos <tasos.laskos@arachni-scanner.com>
2
+ Copyright 2010-2016 Tasos Laskos <tasos.laskos@arachni-scanner.com>
3
3
 
4
4
  This file is part of the Arachni Framework project and is subject to
5
5
  redistribution and commercial restrictions. Please see the Arachni Framework
@@ -1,5 +1,5 @@
1
1
  =begin
2
- Copyright 2010-2015 Tasos Laskos <tasos.laskos@arachni-scanner.com>
2
+ Copyright 2010-2016 Tasos Laskos <tasos.laskos@arachni-scanner.com>
3
3
 
4
4
  This file is part of the Arachni Framework project and is subject to
5
5
  redistribution and commercial restrictions. Please see the Arachni Framework
@@ -1,5 +1,5 @@
1
1
  =begin
2
- Copyright 2010-2015 Tasos Laskos <tasos.laskos@arachni-scanner.com>
2
+ Copyright 2010-2016 Tasos Laskos <tasos.laskos@arachni-scanner.com>
3
3
 
4
4
  This file is part of the Arachni Framework project and is subject to
5
5
  redistribution and commercial restrictions. Please see the Arachni Framework
@@ -1,5 +1,5 @@
1
1
  =begin
2
- Copyright 2010-2015 Tasos Laskos <tasos.laskos@arachni-scanner.com>
2
+ Copyright 2010-2016 Tasos Laskos <tasos.laskos@arachni-scanner.com>
3
3
 
4
4
  This file is part of the Arachni Framework project and is subject to
5
5
  redistribution and commercial restrictions. Please see the Arachni Framework
@@ -1,5 +1,5 @@
1
1
  =begin
2
- Copyright 2010-2015 Tasos Laskos <tasos.laskos@arachni-scanner.com>
2
+ Copyright 2010-2016 Tasos Laskos <tasos.laskos@arachni-scanner.com>
3
3
 
4
4
  This file is part of the Arachni Framework project and is subject to
5
5
  redistribution and commercial restrictions. Please see the Arachni Framework
@@ -1,5 +1,5 @@
1
1
  =begin
2
- Copyright 2010-2015 Tasos Laskos <tasos.laskos@arachni-scanner.com>
2
+ Copyright 2010-2016 Tasos Laskos <tasos.laskos@arachni-scanner.com>
3
3
 
4
4
  This file is part of the Arachni Framework project and is subject to
5
5
  redistribution and commercial restrictions. Please see the Arachni Framework
@@ -1,5 +1,5 @@
1
1
  =begin
2
- Copyright 2010-2015 Tasos Laskos <tasos.laskos@arachni-scanner.com>
2
+ Copyright 2010-2016 Tasos Laskos <tasos.laskos@arachni-scanner.com>
3
3
 
4
4
  This file is part of the Arachni Framework project and is subject to
5
5
  redistribution and commercial restrictions. Please see the Arachni Framework
@@ -1,5 +1,5 @@
1
1
  =begin
2
- Copyright 2010-2015 Tasos Laskos <tasos.laskos@arachni-scanner.com>
2
+ Copyright 2010-2016 Tasos Laskos <tasos.laskos@arachni-scanner.com>
3
3
 
4
4
  This file is part of the Arachni Framework project and is subject to
5
5
  redistribution and commercial restrictions. Please see the Arachni Framework
@@ -11,7 +11,6 @@ require 'nokogiri'
11
11
  # Creates an XML report of the audit.
12
12
  #
13
13
  # @author Tasos "Zapotek" Laskos <tasos.laskos@arachni-scanner.com>
14
- # @version 0.3.4
15
14
  class Arachni::Reporters::XML < Arachni::Reporter::Base
16
15
 
17
16
  LOCAL_SCHEMA = File.dirname( __FILE__ ) + '/xml/schema.xsd'
@@ -136,11 +135,37 @@ class Arachni::Reporters::XML < Arachni::Reporter::Base
136
135
  has_errors = false
137
136
  xsd.validate( Nokogiri::XML( xml ) ).each do |error|
138
137
  puts error.message
139
- ap error
138
+ puts " -- Line #{error.line}, column #{error.column}, level #{error.level}."
139
+ puts '-' * 100
140
+
141
+ justify = (error.line+10).to_s.size
142
+ lines = xml.lines
143
+ ((error.line-10)..(error.line+10)).each do |i|
144
+ line = lines[i]
145
+ next if i < 0 || !line
146
+ i = i + 1
147
+
148
+ printf( "%#{justify}s | %s", i, line )
149
+
150
+ if i == error.line
151
+ printf( "%#{justify}s |", i )
152
+ line.size.times.each do |c|
153
+ print error.column == c ? '^' : '-'
154
+ end
155
+ puts
156
+ end
157
+ end
158
+
159
+ puts '-' * 100
160
+ puts
161
+
140
162
  has_errors = true
141
163
  end
142
164
 
143
- fail 'XML report could not be validated against the XSD.' if has_errors
165
+ if has_errors
166
+ print_error 'Report could not be validated against the XSD due to the above errors.'
167
+ return
168
+ end
144
169
 
145
170
  IO.binwrite( outfile, xml )
146
171
  print_status "Saved in '#{outfile}'."
@@ -1,5 +1,5 @@
1
1
  =begin
2
- Copyright 2010-2015 Tasos Laskos <tasos.laskos@arachni-scanner.com>
2
+ Copyright 2010-2016 Tasos Laskos <tasos.laskos@arachni-scanner.com>
3
3
 
4
4
  This file is part of the Arachni Framework project and is subject to
5
5
  redistribution and commercial restrictions. Please see the Arachni Framework
@@ -1,5 +1,5 @@
1
1
  =begin
2
- Copyright 2010-2015 Tasos Laskos <tasos.laskos@arachni-scanner.com>
2
+ Copyright 2010-2016 Tasos Laskos <tasos.laskos@arachni-scanner.com>
3
3
 
4
4
  This file is part of the Arachni Framework project and is subject to
5
5
  redistribution and commercial restrictions. Please see the Arachni Framework
@@ -8,8 +8,9 @@
8
8
 
9
9
  require 'rubygems'
10
10
  require 'bundler/setup'
11
-
12
- require 'oj_mimic_json'
11
+ require 'concurrent'
12
+ require 'pp'
13
+ require 'ap'
13
14
 
14
15
  def ap( obj )
15
16
  super obj, raw: true
@@ -19,6 +20,20 @@ module Arachni
19
20
 
20
21
  class <<self
21
22
 
23
+ # Runs a minor GC to collect young, short-lived objects.
24
+ #
25
+ # Generally called after analysis operations that generate a lot of
26
+ # new temporary objects.
27
+ def collect_young_objects
28
+ GC.start( full_mark: false )
29
+ end
30
+
31
+ def tmpdir
32
+ # On MS Windows Dir.tmpdir can return the path with a shortname,
33
+ # better avoid that as it can be insonsistent with other paths.
34
+ get_long_win32_filename( Dir.tmpdir )
35
+ end
36
+
22
37
  def null_device
23
38
  Gem.win_platform? ? 'NUL' : '/dev/null'
24
39
  end
@@ -39,10 +54,40 @@ module Arachni
39
54
  !!ENV['ARACHNI_PROFILER']
40
55
  end
41
56
 
57
+ if Arachni.windows?
58
+ require 'find'
59
+ require 'fileutils'
60
+ require 'Win32API'
61
+ require 'win32ole'
62
+
63
+ def get_long_win32_filename( short_name )
64
+ short_name = short_name.dup
65
+ max_path = 1024
66
+ long_name = ' ' * max_path
67
+
68
+ lfn_size = Win32API.new(
69
+ "kernel32",
70
+ "GetLongPathName",
71
+ ['P','P','L'],
72
+ 'L'
73
+ ).call( short_name, long_name, max_path )
74
+
75
+ (1..max_path).include?( lfn_size ) ?
76
+ long_name[0..lfn_size-1] : short_name
77
+ end
78
+ else
79
+ def get_long_win32_filename( short_name )
80
+ short_name
81
+ end
82
+ end
42
83
  end
43
84
 
44
85
  end
45
86
 
87
+ if !Arachni.jruby?
88
+ require 'oj_mimic_json'
89
+ end
90
+
46
91
  require_relative 'arachni/version'
47
92
  require_relative 'arachni/banner'
48
93
 
@@ -1,5 +1,5 @@
1
1
  =begin
2
- Copyright 2010-2015 Tasos Laskos <tasos.laskos@arachni-scanner.com>
2
+ Copyright 2010-2016 Tasos Laskos <tasos.laskos@arachni-scanner.com>
3
3
 
4
4
  This file is part of the Arachni Framework project and is subject to
5
5
  redistribution and commercial restrictions. Please see the Arachni Framework
@@ -1,5 +1,5 @@
1
1
  =begin
2
- Copyright 2010-2015 Tasos Laskos <tasos.laskos@arachni-scanner.com>
2
+ Copyright 2010-2016 Tasos Laskos <tasos.laskos@arachni-scanner.com>
3
3
 
4
4
  This file is part of the Arachni Framework project and is subject to
5
5
  redistribution and commercial restrictions. Please see the Arachni Framework
@@ -8,8 +8,7 @@
8
8
 
9
9
  require 'childprocess'
10
10
  require 'watir-webdriver'
11
- require_relative 'watir/element'
12
- require_relative 'selenium/webdriver/remote/http/typhoeus'
11
+ require_relative 'selenium/webdriver/element'
13
12
  require_relative 'processes/manager'
14
13
  require_relative 'browser/element_locator'
15
14
  require_relative 'browser/javascript'
@@ -70,9 +69,14 @@ class Browser
70
69
  # Let the browser take as long as it needs to complete an operation.
71
70
  WATIR_COM_TIMEOUT = 3600 # 1 hour.
72
71
 
73
- ASSET_EXTENSIONS = Set.new(
74
- ['css', 'js', 'jpg', 'jpeg', 'png', 'gif', 'woff', 'json']
75
- )
72
+ ASSET_EXTENSIONS = Set.new(%w( css js jpg jpeg png gif json ))
73
+
74
+ INPUT_EVENTS = Set.new([
75
+ :change, :blur, :focus, :select, :keyup, :keypress, :keydown, :input
76
+ ])
77
+ INPUT_EVENTS_TO_FORCE = Set.new([
78
+ :focus, :change, :blur, :select
79
+ ])
76
80
 
77
81
  ASSET_EXTRACTORS = [
78
82
  /<\s*link.*?href=['"](.*?)['"].*?>/im,
@@ -92,6 +96,10 @@ class Browser
92
96
  # Watir driver interface.
93
97
  attr_reader :watir
94
98
 
99
+ # @return [Selenium::WebDriver]
100
+ # Selenium driver interface.
101
+ attr_reader :selenium
102
+
95
103
  # @return [Array<Page>]
96
104
  # Same as {#page_snapshots} but it doesn't deduplicate and only contains
97
105
  # pages with sink ({Page::DOM#data_flow_sinks} or {Page::DOM#execution_flow_sinks})
@@ -114,36 +122,44 @@ class Browser
114
122
  attr_reader :skip_states
115
123
 
116
124
  # @return [Integer]
117
- attr_reader :pid
125
+ # PID of the lifeline process managing the browser process.
126
+ attr_reader :lifeline_pid
127
+
128
+ # @return [Integer]
129
+ # PID of the browser process.
130
+ attr_reader :browser_pid
118
131
 
119
132
  attr_reader :last_url
120
133
 
121
- attr_reader :last_dom_url
134
+ class <<self
122
135
 
123
- # @return [Bool]
124
- # `true` if a supported browser is in the OS PATH, `false` otherwise.
125
- def self.has_executable?
126
- !!executable
127
- end
136
+ # @return [Bool]
137
+ # `true` if a supported browser is in the OS PATH, `false` otherwise.
138
+ def has_executable?
139
+ !!executable
140
+ end
128
141
 
129
- # @return [String]
130
- # Path to the PhantomJS executable.
131
- def self.executable
132
- Selenium::WebDriver::PhantomJS.path
133
- end
142
+ # @return [String]
143
+ # Path to the PhantomJS executable.
144
+ def executable
145
+ Selenium::WebDriver::PhantomJS.path
146
+ end
134
147
 
135
- def self.asset_domains
136
- @asset_domains ||= Set.new
137
- end
138
- asset_domains
148
+ def asset_domains
149
+ @asset_domains ||= Set.new
150
+ end
139
151
 
140
- def self.add_asset_domain( url )
141
- return if url.to_s.empty?
142
- return if !(curl = Arachni::URI( url ))
143
- return if !(domain = curl.domain)
152
+ def add_asset_domain( url )
153
+ return if url.to_s.empty?
154
+ return if !(curl = Arachni::URI( url ))
155
+ return if !(domain = curl.domain)
156
+
157
+ asset_domains << domain
158
+ domain
159
+ end
144
160
 
145
- asset_domains << domain
146
161
  end
162
+ asset_domains
147
163
 
148
164
  # @param [Hash] options
149
165
  # @option options [Integer] :concurrency
@@ -167,10 +183,10 @@ class Browser
167
183
  concurrency: @options[:concurrency],
168
184
  address: '127.0.0.1',
169
185
  request_handler: proc do |request, response|
170
- synchronize { exception_jail { request_handler( request, response ) } }
186
+ exception_jail { request_handler( request, response ) }
171
187
  end,
172
188
  response_handler: proc do |request, response|
173
- synchronize { exception_jail { response_handler( request, response ) } }
189
+ exception_jail { response_handler( request, response ) }
174
190
  end
175
191
  )
176
192
 
@@ -200,6 +216,8 @@ class Browser
200
216
  # Captures HTTP::Response objects per URL for open windows.
201
217
  @window_responses = {}
202
218
 
219
+ @elements_with_events = {}
220
+
203
221
  # Keeps track of resources which should be skipped -- like already fired
204
222
  # events and clicked links etc.
205
223
  @skip_states = Support::LookUp::HashSet.new( hasher: :persistent_hash )
@@ -212,12 +230,11 @@ class Browser
212
230
  @last_url = nil
213
231
 
214
232
  @javascript = Javascript.new( self )
215
-
216
- ensure_open_window
217
233
  end
218
234
 
219
235
  def clear_buffers
220
236
  synchronize do
237
+ @elements_with_events.clear
221
238
  @preloads.clear
222
239
  @cache.clear
223
240
  @captured_pages.clear
@@ -235,14 +252,13 @@ class Browser
235
252
  end.join
236
253
  end
237
254
 
238
- # @param [String, HTTP::Response, Page] resource
255
+ # @param [String, HTTP::Response, Page, Page:::DOM] resource
239
256
  # Loads the given resource in the browser. If it is a string it will be
240
257
  # treated like a URL.
241
258
  #
242
259
  # @return [Browser]
243
260
  # `self`
244
261
  def load( resource, options = {} )
245
- @last_dom_url = nil
246
262
 
247
263
  case resource
248
264
  when String
@@ -254,13 +270,14 @@ class Browser
254
270
  when Page
255
271
  HTTP::Client.update_cookies resource.cookie_jar
256
272
 
257
- @transitions = resource.dom.transitions.dup
258
- update_skip_states resource.dom.skip_states
273
+ load resource.dom
259
274
 
260
- @last_dom_url = resource.dom.url
275
+ when Page::DOM
276
+ @transitions = resource.transitions.dup
277
+ update_skip_states resource.skip_states
261
278
 
262
279
  @add_request_transitions = false if @transitions.any?
263
- resource.dom.restore self
280
+ resource.restore self
264
281
  @add_request_transitions = true
265
282
 
266
283
  else
@@ -350,22 +367,22 @@ class Browser
350
367
  url: url,
351
368
  cookies: extra_cookies
352
369
  ) do
353
- print_debug_level_2 "Loading: #{url}"
354
- watir.goto url
355
- print_debug_level_2 'Done'
370
+ print_debug_level_2 "Loading #{url} ..."
371
+ @selenium.navigate.to url
372
+ print_debug_level_2 '...done.'
356
373
 
357
374
  wait_till_ready
358
375
 
359
- url = watir.url
360
376
  Options.browser_cluster.css_to_wait_for( url ).each do |css|
361
377
  print_info "Waiting for #{css.inspect} to appear for: #{url}"
362
378
 
363
379
  begin
364
- watir.element( css: css ).
365
- wait_until_present( Options.browser_cluster.job_timeout )
380
+ Selenium::WebDriver::Wait.new(
381
+ timeout: Options.browser_cluster.job_timeout
382
+ ).until { @selenium.find_element( :css, css ) }
366
383
 
367
384
  print_info "#{css.inspect} appeared for: #{url}"
368
- rescue Watir::Wait::TimeoutError
385
+ rescue Selenium::WebDriver::Error::TimeOutError
369
386
  print_bad "#{css.inspect} did not appeared for: #{url}"
370
387
  end
371
388
 
@@ -380,7 +397,7 @@ class Browser
380
397
 
381
398
  @add_request_transitions = pre_add_request_transitions
382
399
 
383
- HTTP::Client.update_cookies cookies
400
+ update_cookies
384
401
 
385
402
  # Capture the page at its initial state.
386
403
  capture_snapshot if take_snapshot
@@ -391,32 +408,35 @@ class Browser
391
408
  def wait_till_ready
392
409
  print_debug_level_2 'Waiting for custom JS...'
393
410
  @javascript.wait_till_ready
394
- print_debug_level_2 'Done'
411
+ print_debug_level_2 '...done.'
395
412
 
396
- print_debug_level_2 "Waiting for #{@proxy.active_connections} connections to close.."
397
- wait_for_pending_requests
398
- print_debug_level_2 'Done'
399
-
400
- print_debug_level_2 'Waiting for timers...'
401
413
  wait_for_timers
402
- print_debug_level_2 'Done'
414
+
415
+ wait_for_pending_requests
403
416
  end
404
417
 
405
418
  def shutdown
406
419
  begin
407
- watir.close if browser_alive?
408
- rescue Selenium::WebDriver::Error::WebDriverError,
409
- Watir::Exception::Error
420
+ watir.close if alive?
421
+ # Bucnh of dirrent errors can be raised here, Selenium, HTTP client,
422
+ # don't try to catch them by type because we'll probably miss some.
423
+ rescue
410
424
  end
411
425
 
412
426
  kill_process
413
- @proxy.shutdown
427
+ @proxy.shutdown rescue Reactor::Error::NotRunning
414
428
  end
415
429
 
416
430
  # @return [String]
417
- # Current URL.
431
+ # Current URL, noralized via #{Arachni::URI.}
418
432
  def url
419
- normalize_url watir.url
433
+ normalize_url dom_url
434
+ end
435
+
436
+ # @return [String]
437
+ # Current URL, as provided by the browser.
438
+ def dom_url
439
+ javascript.run( 'return document.URL;' )
420
440
  end
421
441
 
422
442
  # Explores the browser's DOM tree and captures page snapshots for each
@@ -448,15 +468,11 @@ class Browser
448
468
  # Iterates over all elements which have events and passes their info to the
449
469
  # given block.
450
470
  #
451
- # @param [Bool] mark_state
452
- # Mark each element/events as visited and skip it if it has already been
453
- # seen.
454
- #
455
471
  # @yield [ElementLocator,Array<Symbol>]
456
- # Hash with information about the element, its tag name, applicable events
457
- # along with their handlers and attributes.
458
- def each_element_with_events( mark_state = true )
459
- current_url = url
472
+ # Element locator along with the element's applicable events along with
473
+ # their handlers and attributes.
474
+ def each_element_with_events
475
+ current_url = self.url
460
476
 
461
477
  javascript.dom_elements_with_events.each do |element|
462
478
  tag_name = element['tag_name']
@@ -469,7 +485,7 @@ class Browser
469
485
 
470
486
  if !href.empty?
471
487
  if href.downcase.start_with?( 'javascript:' )
472
- events << [ :click, href ]
488
+ (events[:click] ||= []) << href
473
489
  else
474
490
  next if skip_path?( to_absolute( href, current_url ) )
475
491
  end
@@ -477,7 +493,7 @@ class Browser
477
493
 
478
494
  when 'input'
479
495
  if attributes['type'].to_s.downcase == 'image'
480
- events << [ :click, 'image' ]
496
+ (events[:click] ||= []) << 'image'
481
497
  end
482
498
 
483
499
  when 'form'
@@ -485,7 +501,7 @@ class Browser
485
501
 
486
502
  if !action.empty?
487
503
  if action.downcase.start_with?( 'javascript:' )
488
- events << [ :submit, action ]
504
+ (events[:submit] ||= []) << action
489
505
  else
490
506
  next if skip_path?( to_absolute( action, current_url ) )
491
507
  end
@@ -494,12 +510,6 @@ class Browser
494
510
 
495
511
  next if events.empty?
496
512
 
497
- if mark_state
498
- state = "#{tag_name}#{attributes}#{events}"
499
- next if skip_state?( state )
500
- skip_state state
501
- end
502
-
503
513
  yield ElementLocator.new( tag_name: tag_name, attributes: attributes ),
504
514
  events
505
515
  end
@@ -507,21 +517,47 @@ class Browser
507
517
  self
508
518
  end
509
519
 
520
+ # @note The results will be cached, if direct access in necessary
521
+ # use {#each_element_with_events}.
522
+ #
523
+ # @return [Hash<ElementLocator,Array<Symbol>>]
524
+ # Element locator along with the element's applicable events along with
525
+ # their handlers and attributes.
526
+ def elements_with_events( clear_cache = false )
527
+ current_url = self.url
528
+
529
+ @elements_with_events.clear if clear_cache
530
+
531
+ if @elements_with_events.include?( current_url )
532
+ return @elements_with_events[current_url]
533
+ end
534
+
535
+ @elements_with_events.clear
536
+ @elements_with_events[current_url] ||= {}
537
+
538
+ each_element_with_events do |locator, events|
539
+ @elements_with_events[current_url][locator] = events
540
+ end
541
+
542
+ @elements_with_events[current_url]
543
+ end
544
+
510
545
  # @return [String]
511
546
  # Snapshot ID used to determine whether or not a page snapshot has already
512
- # been seen. Uses both elements and their DOM events and possible audit
513
- # workload to determine the ID, as page snapshots should be retained both
514
- # when further browser analysis can be performed and when new element
515
- # audit workload (but possibly without any DOM relevance) is available.
547
+ # been seen.
548
+ #
549
+ # Uses both elements and their DOM events and possible audit workload to
550
+ # determine the ID, as page snapshots should be retained both when further
551
+ # browser analysis can be performed and when new element audit workload
552
+ # (but possibly without any DOM relevance) is available.
516
553
  def snapshot_id
517
- current_url = url
554
+ current_url = self.url
518
555
 
519
- id = []
556
+ id = Set.new
520
557
  javascript.dom_elements_with_events.each do |element|
521
558
  tag_name = element['tag_name']
522
559
  attributes = element['attributes']
523
- events = element['events'] +
524
- Javascript.select_event_attributes( attributes ).to_a
560
+ events = element['events']
525
561
  element_id = attributes['id'].to_s
526
562
 
527
563
  case tag_name
@@ -531,19 +567,19 @@ class Browser
531
567
 
532
568
  if !href.empty?
533
569
  if href.downcase.start_with?( 'javascript:' )
534
- events << [ :click, href ]
570
+ (events[:click] ||= []) << href
535
571
  else
536
572
  absolute = to_absolute( href, current_url )
537
573
  next if skip_path?( absolute )
538
574
 
539
- events << [ :click, href ]
575
+ (events[:click] ||= []) << href
540
576
  end
541
577
  else
542
- events << [ :click, current_url ]
578
+ (events[:click] ||= []) << current_url
543
579
  end
544
580
 
545
581
  when 'input', 'textarea', 'select'
546
- events << [ tag_name.to_sym ]
582
+ (events[:input] ||= []) << tag_name.to_sym
547
583
  element_id << attributes['name'].to_s
548
584
 
549
585
  when 'form'
@@ -552,24 +588,26 @@ class Browser
552
588
 
553
589
  if !action.empty?
554
590
  if action.downcase.start_with?( 'javascript:' )
555
- events << [ :submit, action ]
591
+ (events[:submit] ||= []) << action
556
592
  else
557
593
  absolute = to_absolute( action, current_url )
558
594
  if !skip_path?( absolute )
559
- events << [ :submit, absolute ]
595
+ (events[:submit] ||= []) << absolute
560
596
  end
561
597
  end
562
598
  else
563
- events << [ :submit, current_url ]
599
+ (events[:submit] ||= []) << current_url
564
600
  end
565
601
  end
566
602
 
567
603
  next if events.empty?
568
604
 
569
- id << "#{tag_name}:#{element_id}:#{events}"
605
+ id << "#{tag_name}:#{element_id}:#{events.keys.sort}".persistent_hash
570
606
  end
571
607
 
572
- id.uniq.sort.to_s
608
+ id << [:cookies, cookies.map(&:name).sort].to_s.persistent_hash
609
+
610
+ id.to_a.sort.map(&:to_s).join(':')
573
611
  end
574
612
 
575
613
  # Triggers all events on all elements (**once**) and captures
@@ -578,11 +616,15 @@ class Browser
578
616
  # @return [Browser]
579
617
  # `self`
580
618
  def trigger_events
581
- root_page = to_page
619
+ dom = self.state
620
+
621
+ elements_with_events( true ).each do |locator, events|
622
+ state = "#{locator.tag_name}:#{locator.attributes}:#{events.keys.sort}"
623
+ next if skip_state?( state )
624
+ skip_state state
582
625
 
583
- each_element_with_events do |locator, events|
584
626
  events.each do |name, _|
585
- distribute_event( root_page, locator, name.to_sym )
627
+ distribute_event( dom, locator, name.to_sym )
586
628
  end
587
629
  end
588
630
 
@@ -595,23 +637,23 @@ class Browser
595
637
  # Distributes the triggering of `event` on the element at `element_index`
596
638
  # on `page`.
597
639
  #
598
- # @param [Page] page
640
+ # @param [String, Page, Page::DOM, HTTP::Response] resource
599
641
  # @param [ElementLocator] locator
600
642
  # @param [Symbol] event
601
- def distribute_event( page, locator, event )
602
- trigger_event( page, locator, event )
643
+ def distribute_event( resource, locator, event )
644
+ trigger_event( resource, locator, event )
603
645
  end
604
646
 
605
647
  # @note Captures page {#page_snapshots}.
606
648
  #
607
649
  # Triggers `event` on the element described by `tag` on `page`.
608
650
  #
609
- # @param [Page] page
651
+ # @param [String, Page, Page::DOM, HTTP::Response] resource
610
652
  # Page containing the element's `tag`.
611
653
  # @param [ElementLocator] element
612
654
  # @param [Symbol] event
613
655
  # Event to trigger.
614
- def trigger_event( page, element, event )
656
+ def trigger_event( resource, element, event, restore = true )
615
657
  event = event.to_sym
616
658
  transition = fire_event( element, event )
617
659
 
@@ -620,18 +662,21 @@ class Browser
620
662
  ' the page has changed, capturing a new snapshot.'
621
663
  capture_snapshot
622
664
 
623
- print_info 'Restoring page.'
624
- restore page
665
+ if restore
666
+ print_info 'Restoring page.'
667
+ restore( resource )
668
+ end
669
+
625
670
  return
626
671
  end
627
672
 
628
673
  capture_snapshot( transition )
629
- restore page
674
+ restore( resource ) if restore
630
675
  end
631
676
 
632
677
  # Triggers `event` on `element`.
633
678
  #
634
- # @param [Watir::Element, ElementLocator] element
679
+ # @param [Selenium::WebDriver::Element, ElementLocator] element
635
680
  # @param [Symbol] event
636
681
  # @param [Hash] options
637
682
  # @option options [Hash<Symbol,String=>String>] :inputs
@@ -651,10 +696,10 @@ class Browser
651
696
  locator = element
652
697
 
653
698
  begin
654
- element = element.locate( self )
655
- rescue Selenium::WebDriver::Error::WebDriverError,
656
- Watir::Exception::Error => e
699
+ Selenium::WebDriver::Wait.new( timeout: ELEMENT_APPEARANCE_TIMEOUT ).
700
+ until { element = element.locate( self ) }
657
701
 
702
+ rescue Selenium::WebDriver::Error::WebDriverError => e
658
703
  print_debug "Element '#{element.inspect}' could not be " <<
659
704
  "located for triggering '#{event}'."
660
705
  print_debug
@@ -663,23 +708,6 @@ class Browser
663
708
  end
664
709
  end
665
710
 
666
- # The page may need a bit to settle and the element is lazily located
667
- # by Watir so give it a few tries.
668
- begin
669
- with_timeout ELEMENT_APPEARANCE_TIMEOUT do
670
- sleep 0.1 while !element.exists?
671
- end
672
- rescue Timeout::Error
673
- print_debug_level_2 "#{element.inspect} did not appear in " <<
674
- "#{ELEMENT_APPEARANCE_TIMEOUT}."
675
- return
676
- end
677
-
678
- if !element.visible?
679
- print_debug_level_2 "#{element.inspect} is not visible, skipping..."
680
- return
681
- end
682
-
683
711
  if locator
684
712
  opening_tag = locator.to_s
685
713
  tag_name = locator.tag_name
@@ -689,71 +717,115 @@ class Browser
689
717
  locator = ElementLocator.from_html( opening_tag )
690
718
  end
691
719
 
692
- print_debug_level_2 "#{__method__} [start]: #{event} (#{options}) #{locator}"
720
+ print_debug_level_2 "[start]: #{event} (#{options}) #{locator}"
693
721
 
694
722
  tag_name = tag_name.to_sym
695
723
 
696
724
  notify_on_fire_event( element, event )
697
725
 
698
- tries = 0
726
+ pre_timeouts = javascript.timeouts
727
+
699
728
  begin
700
- Page::DOM::Transition.new( locator, event, options ) do
701
- had_special_trigger = false
729
+ transition = Page::DOM::Transition.new( locator, event, options ) do
730
+ force = true
702
731
 
732
+ # It's better to use the Watir helpers whenever possible instead
733
+ # of firing events manually.
703
734
  if tag_name == :form
704
735
  fill_in_form_inputs( element, options[:inputs] )
705
736
 
706
737
  if event == :submit
707
- element.to_subtype.submit
708
- had_special_trigger = true
738
+ force = false
739
+
740
+ element.submit
709
741
  end
710
- elsif tag_name == :input && event == :click &&
711
- element.attribute_value(:type) == 'image'
712
742
 
713
- element.to_subtype.click
714
- had_special_trigger = true
743
+ elsif event == :click
744
+ force = false
715
745
 
716
- elsif [:keyup, :keypress, :keydown, :change, :input, :focus, :blur, :select].include? event
746
+ element.click
717
747
 
718
- # Some of these need an explicit event triggers.
719
- had_special_trigger = true if ![:change, :blur, :focus, :select].include? event
748
+ elsif INPUT_EVENTS.include? event
749
+ force = INPUT_EVENTS_TO_FORCE.include?( event )
720
750
 
721
751
  # Send keys will append to the existing value, so we need to
722
- # clear it first. The receiving input may support values
752
+ # clear it first. The receiving input may not support values
723
753
  # though, so watch out.
724
- if (subtype = element.to_subtype).respond_to?( :value= )
725
- subtype.value = ''
726
- end
754
+ element.clear if [:input, :textarea].include?( tag_name )
727
755
 
756
+ # Simulates real text input and will trigger associated events.
757
+ # Except for INPUT_EVENTS_TO_FORCE of course.
728
758
  element.send_keys( (options[:value] || value_for( element )).to_s )
729
759
  end
730
760
 
731
- element.fire_event( event ) if !had_special_trigger
761
+ if force
762
+ print_debug_level_2 "[forcing event]: #{event} (#{options}) #{locator}"
763
+ fire_event_js locator, event
764
+ end
732
765
 
733
- print_debug_level_2 "#{__method__} [waiting for requests]: #{event} (#{options}) #{locator}"
766
+ print_debug_level_2 "[waiting for requests]: #{event} (#{options}) #{locator}"
734
767
  wait_for_pending_requests
735
- print_debug_level_2 "#{__method__} [done waiting for requests]: #{event} (#{options}) #{locator}"
768
+ print_debug_level_2 "[done waiting for requests]: #{event} (#{options}) #{locator}"
736
769
 
737
- # puts source_with_line_numbers
738
- print_debug_level_2 "#{__method__} [done]: #{event} (#{options}) #{locator}"
770
+ update_cookies
739
771
  end
740
- rescue Selenium::WebDriver::Error::WebDriverError,
741
- Watir::Exception::Error => e
742
772
 
743
- sleep 0.1
773
+ print_debug_level_2 "[done in #{transition.time}s]: #{event} (#{options}) #{locator}"
774
+
775
+ delay = (javascript.timeouts - pre_timeouts).compact.map { |t| t[1].to_i }.max
776
+ if delay
777
+ print_debug_level_2 "Found new timers with max #{delay}ms."
778
+ delay = [Options.http.request_timeout, delay].min / 1000.0
779
+
780
+ print_debug_level_2 "Will wait for #{delay}s."
781
+ sleep delay
782
+ end
744
783
 
745
- tries += 1
746
- retry if tries < 5
784
+ transition
785
+ rescue Selenium::WebDriver::Error::WebDriverError => e
747
786
 
748
787
  print_debug "Error when triggering event for: #{url}"
749
- print_debug "-- '#{event}' on: #{opening_tag}"
788
+ print_debug "-- '#{event}' on: #{opening_tag} -- #{locator.css}"
750
789
  print_debug
751
790
  print_debug_exception e
752
-
753
791
  nil
754
792
  end
755
793
  end
756
794
 
795
+ # This is essentially the same thing as Watir::Element#fire_event
796
+ # but 10 times faster.
797
+ #
798
+ # Does not perform any sort of sanitization nor sanity checking, it will
799
+ # just try to trigger the event.
800
+ #
801
+ # @param [Browser::ElementLocator] locator
802
+ # @param [Symbol,String] event
803
+ # @param [Bool] ret
804
+ # Return JS result?
805
+ # @param [Numeric] wait
806
+ # Amount of time to wait (in seconds) after triggering the event.
807
+ def fire_event_js( locator, event, ret: false, wait: 0.1 )
808
+ r = javascript.run <<-EOJS
809
+ var element = document.querySelector( #{locator.css.inspect} );
810
+ var event = document.createEvent( "Events" );
811
+
812
+ event.initEvent( "#{event}", true, true );
813
+
814
+ event.view = window;
815
+ event.altKey = false;
816
+ event.ctrlKey = false;
817
+ event.shiftKey = false;
818
+ event.metaKey = false;
819
+ event.keyCode = 0;
820
+ event.charCode = 'a';
821
+
822
+ #{'return' if ret} element.dispatchEvent( event );
823
+ EOJS
824
+
825
+ sleep wait
826
+ r
827
+ end
828
+
757
829
  # Starts capturing requests and parses them into elements of pages,
758
830
  # accessible via {#captured_pages}.
759
831
  #
@@ -805,13 +877,29 @@ class Browser
805
877
  @captured_pages
806
878
  end
807
879
 
880
+ # @return [Page::DOM]
881
+ def state
882
+ d_url = dom_url
883
+
884
+ return if !response
885
+
886
+ Page::DOM.new(
887
+ url: d_url,
888
+ transitions: @transitions.dup,
889
+ digest: @javascript.dom_digest,
890
+ skip_states: skip_states.dup
891
+ )
892
+ end
893
+
808
894
  # @return [Page]
809
895
  # Converts the current browser window to a {Page page}.
810
896
  def to_page
897
+ d_url = dom_url
898
+
811
899
  if !(r = response)
812
900
  return Page.from_data(
813
901
  dom: {
814
- url: watir.url
902
+ url: d_url
815
903
  },
816
904
  response: {
817
905
  code: 0,
@@ -820,12 +908,19 @@ class Browser
820
908
  )
821
909
  end
822
910
 
911
+ # We need sink data for both the current taint and to determine cookie
912
+ # usage, so grab all of the data-flow sinks once.
913
+ data_flow_sinks = {}
914
+ if @javascript.supported?
915
+ data_flow_sinks = @javascript.taint_tracer.data_flow_sinks
916
+ end
917
+
823
918
  page = r.to_page
824
919
  page.body = source
825
- page.dom.url = watir.url
920
+ page.dom.url = d_url
826
921
  page.dom.digest = @javascript.dom_digest
827
922
  page.dom.execution_flow_sinks = @javascript.execution_flow_sinks
828
- page.dom.data_flow_sinks = @javascript.data_flow_sinks
923
+ page.dom.data_flow_sinks = data_flow_sinks[@javascript.taint] || []
829
924
  page.dom.transitions = @transitions.dup
830
925
  page.dom.skip_states = skip_states.dup
831
926
 
@@ -860,10 +955,9 @@ class Browser
860
955
  end
861
956
 
862
957
  if Options.audit.cookie_doms?
863
- sinks = @javascript.taint_tracer.data_flow_sinks
864
958
  page.cookies.each do |cookie|
865
- next if sinks.include?( cookie.name ) ||
866
- sinks.include?( cookie.value )
959
+ next if data_flow_sinks.include?( cookie.name ) ||
960
+ data_flow_sinks.include?( cookie.value )
867
961
 
868
962
  cookie.skip_dom = true
869
963
  end
@@ -881,32 +975,46 @@ class Browser
881
975
  request_transitions = flush_request_transitions
882
976
  transitions = ([transition] + request_transitions).flatten.compact
883
977
 
978
+ window_handles = @selenium.window_handles
979
+
884
980
  begin
885
- # Skip about:blank windows.
886
- watir.windows( url: /^http/ ).each do |window|
887
- window.use do
888
- next if !(page = to_page)
889
-
890
- if pages.empty?
891
- transitions.each do |t|
892
- @transitions << t
893
- page.dom.push_transition t
894
- end
895
- end
981
+ window_handles.each do |handle|
982
+ if window_handles.size > 1
983
+ @selenium.switch_to.window( handle )
984
+ end
896
985
 
897
- capture_snapshot_with_sink( page )
986
+ # We don't even have an HTTP response for the page, don't
987
+ # bother trying anything else.
988
+ next if !response
898
989
 
899
- unique_id = self.snapshot_id
900
- next if skip_state? unique_id
901
- skip_state unique_id
990
+ unique_id = self.snapshot_id
991
+ already_seen = skip_state?( unique_id )
992
+ skip_state unique_id
902
993
 
903
- notify_on_new_page( page )
994
+ with_sinks = javascript.has_sinks?
904
995
 
905
- if store_pages?
906
- @page_snapshots[unique_id.hash] = page
907
- pages << page
996
+ # Avoid a #to_page call if at all possible because it'll generate
997
+ # loads of data.
998
+ next if (already_seen && !with_sinks) ||
999
+ (page = to_page).code == 0
1000
+
1001
+ if pages.empty?
1002
+ transitions.each do |t|
1003
+ @transitions << t
1004
+ page.dom.push_transition t
908
1005
  end
909
1006
  end
1007
+
1008
+ capture_snapshot_with_sink( page )
1009
+
1010
+ next if already_seen
1011
+
1012
+ notify_on_new_page( page )
1013
+
1014
+ if store_pages?
1015
+ @page_snapshots[unique_id] = page
1016
+ pages << page
1017
+ end
910
1018
  end
911
1019
  rescue => e
912
1020
  print_debug "Could not capture snapshot for: #{@last_url}"
@@ -917,6 +1025,8 @@ class Browser
917
1025
 
918
1026
  print_debug
919
1027
  print_debug_exception e
1028
+ ensure
1029
+ @selenium.switch_to.default_content
920
1030
  end
921
1031
 
922
1032
  pages
@@ -947,33 +1057,49 @@ class Browser
947
1057
  end
948
1058
 
949
1059
  # @return [Array<Cookie>]
950
- # Browser cookies.
1060
+ # Cookies visible to JS.
951
1061
  def cookies
952
1062
  js_cookies = begin
953
- # Watir doesn't tell us if cookies are HttpOnly, so we need to figure
954
- # this out ourselves, by checking for JS visibility.
1063
+ # Watir doesn't tell us if cookies are HttpOnly, so we need to figure
1064
+ # this out ourselves, by checking for JS visibility.
955
1065
  javascript.run( 'return document.cookie' )
956
1066
  # We may not have a page.
957
1067
  rescue Selenium::WebDriver::Error::WebDriverError
958
1068
  ''
959
1069
  end
960
1070
 
961
- watir.cookies.to_a.map do |c|
962
- original_name = c[:name].to_s
1071
+ # The domain attribute cannot be trusted, PhantomJS thinks all cookies
1072
+ # are for subdomains too.
1073
+ # Do not try to hack around this because it'll be a waste of time,
1074
+ # leading to confusion and duplicate cookies.
1075
+ #
1076
+ # Still, we ask Selenium for cookies instead of parsing the JS ones
1077
+ # and merging with the HTTP cookiejar because this allows us to get
1078
+ # a path attribute for JS cookies.
1079
+ @selenium.manage.all_cookies.map do |c|
1080
+
1081
+ c[:httponly] = !js_cookies.include?( c[:name].to_s )
1082
+ c[:path] = c[:path].gsub( /\/+/, '/' )
1083
+ c[:expires] = Time.parse( c[:expires].to_s ) if c[:expires]
963
1084
 
964
- c[:path] = '/' if c[:path] == '//'
965
- c[:name] = Cookie.decode( c[:name].to_s )
966
- c[:value] = Cookie.value_to_v0( c[:value].to_s )
967
- c[:httponly] = !js_cookies.include?( original_name )
1085
+ c[:raw_name] = c[:name].to_s
1086
+ c[:raw_value] = c[:value].to_s
968
1087
 
969
- Cookie.new c.merge( url: @last_url || url )
1088
+ c[:name] = Cookie.decode( c[:name].to_s )
1089
+ c[:value] = Cookie.value_to_v0( c[:value].to_s )
1090
+
1091
+ Cookie.new c.merge( url: @last_url || self.url )
970
1092
  end
971
1093
  end
972
1094
 
1095
+ def update_cookies
1096
+ HTTP::Client.update_cookies self.cookies
1097
+ end
1098
+
973
1099
  # @return [String]
974
1100
  # HTML code of the evaluated (DOM/JS/AJAX) page.
975
1101
  def source
976
- watir.html
1102
+ @selenium.page_source
977
1103
  end
978
1104
 
979
1105
  def load_delay
@@ -985,7 +1111,11 @@ class Browser
985
1111
  delay = load_delay
986
1112
  return if !delay
987
1113
 
1114
+ print_debug_level_2 'Waiting for timers...'
1115
+
988
1116
  sleep [Options.http.request_timeout, delay].min / 1000.0
1117
+
1118
+ print_debug_level_2 '...done.'
989
1119
  end
990
1120
 
991
1121
  def skip_path?( path )
@@ -993,22 +1123,26 @@ class Browser
993
1123
  end
994
1124
 
995
1125
  def response
996
- u = watir.url
1126
+ u = dom_url
997
1127
 
998
- return if skip_path?( u )
1128
+ if dom_url == 'about:blank'
1129
+ print_debug 'Blank page.'
1130
+ return
1131
+ end
999
1132
 
1000
- begin
1001
- with_timeout Options.http.request_timeout / 1_000 do
1002
- while !(r = get_response(u))
1003
- sleep 0.1
1004
- end
1133
+ if skip_path?( u )
1134
+ print_debug "Response is out of scope: #{u}"
1135
+ return
1136
+ end
1005
1137
 
1006
- fail Timeout::Error if r.timed_out?
1138
+ r = get_response( u )
1007
1139
 
1008
- return r
1009
- end
1010
- rescue Timeout::Error
1011
- print_debug "Response for '#{u}' never arrived."
1140
+ return r if r && r.code != 504
1141
+
1142
+ if r
1143
+ print_debug "Origin server timed-out when requesting: #{u}"
1144
+ else
1145
+ print_debug "Response never arrived: #{u}"
1012
1146
  end
1013
1147
 
1014
1148
  nil
@@ -1019,7 +1153,7 @@ class Browser
1019
1153
  def selenium
1020
1154
  return @selenium if @selenium
1021
1155
 
1022
- client = Selenium::WebDriver::Remote::Http::Typhoeus.new
1156
+ client = Selenium::WebDriver::Remote::Http::Default.new
1023
1157
  client.timeout = WATIR_COM_TIMEOUT
1024
1158
 
1025
1159
  @selenium = Selenium::WebDriver.for(
@@ -1033,47 +1167,53 @@ class Browser
1033
1167
  )
1034
1168
  end
1035
1169
 
1170
+ def alive?
1171
+ @lifeline_pid && Processes::Manager.alive?( @lifeline_pid )
1172
+ end
1173
+
1036
1174
  def inspect
1037
1175
  s = "#<#{self.class} "
1038
- s << "pid=#{@pid} "
1176
+ s << "pid=#{@lifeline_pid} "
1177
+ s << "browser_pid=#{@browser_pid} "
1039
1178
  s << "last-url=#{@last_url.inspect} "
1040
1179
  s << "transitions=#{@transitions.size}"
1041
1180
  s << '>'
1042
1181
  end
1043
1182
 
1044
- def filter_events( element, events )
1045
- supported = Set.new( Arachni::Browser::Javascript.events_for( element ) )
1046
- events.reject { |name, _| !supported.include? ('on' + name.to_s.gsub( /^on/, '' )).to_sym }
1047
- end
1048
-
1049
1183
  private
1050
1184
 
1051
1185
  def fill_in_form_inputs( form, inputs = nil )
1052
- form.text_fields.each do |input|
1186
+ form.find_elements( :css, 'input, textarea' ).each do |input|
1053
1187
  name_or_id = name_or_id_for( input )
1054
- value = inputs ? inputs[name_or_id] : value_for( input )
1188
+ value = inputs ? inputs[name_or_id] : value_for_name( name_or_id )
1055
1189
 
1056
1190
  begin
1057
- input.set( value.to_s.recode )
1191
+ input.clear
1192
+ input.send_keys( value.to_s.recode )
1058
1193
  # Disabled inputs and such...
1059
- rescue Selenium::WebDriver::Error::WebDriverError,
1060
- Watir::Exception::Error => e
1194
+ rescue Selenium::WebDriver::Error::WebDriverError => e
1061
1195
  print_debug_level_2 "Could not fill in form input '#{name_or_id}'" <<
1062
1196
  " because: #{e} [#{e.class}"
1063
1197
  end
1064
1198
  end
1065
1199
 
1066
- form.selects.each do |input|
1067
- name_or_id = name_or_id_for( input )
1068
- value = inputs ? inputs[name_or_id] : value_for( input )
1200
+ form.find_elements( :tag_name, 'select' ).each do |select|
1201
+ name_or_id = name_or_id_for( select )
1202
+ value = inputs ? inputs[name_or_id] : value_for_name( name_or_id )
1069
1203
 
1070
- begin
1071
- input.select_value( value.to_s.recode )
1072
- # Disabled inputs and such...
1073
- rescue Selenium::WebDriver::Error::WebDriverError,
1074
- Watir::Exception::Error => e
1075
- print_debug_level_2 "Could not fill in form select '#{name_or_id}'" <<
1076
- " because: #{e} [#{e.class}"
1204
+ options = select.find_elements( tag_name: 'option' )
1205
+ options.each do |option|
1206
+
1207
+ begin
1208
+ if option[:value] == value || option.text == value
1209
+ option.click
1210
+ return
1211
+ end
1212
+ # Disabled inputs and such...
1213
+ rescue Selenium::WebDriver::Error::WebDriverError => e
1214
+ print_debug_level_2 "Could not fill in form select '#{name_or_id}'" <<
1215
+ " because: #{e} [#{e.class}"
1216
+ end
1077
1217
  end
1078
1218
  end
1079
1219
  end
@@ -1091,10 +1231,10 @@ class Browser
1091
1231
  end
1092
1232
 
1093
1233
  def name_or_id_for( element )
1094
- name = element.attribute_value(:name).to_s
1234
+ name = element[:name].to_s
1095
1235
  return name if !name.empty?
1096
1236
 
1097
- id = element.attribute_value(:id).to_s
1237
+ id = element[:id].to_s
1098
1238
  return id if !id.empty?
1099
1239
 
1100
1240
  nil
@@ -1119,6 +1259,10 @@ class Browser
1119
1259
  Options.input.value_for_name( name_or_id_for( element ) )
1120
1260
  end
1121
1261
 
1262
+ def value_for_name( name )
1263
+ Options.input.value_for_name( name )
1264
+ end
1265
+
1122
1266
  def spawn_browser
1123
1267
  if !spawn_phantomjs
1124
1268
  fail Error::Spawn, 'Could not start the browser process.'
@@ -1134,11 +1278,14 @@ class Browser
1134
1278
 
1135
1279
  ChildProcess.posix_spawn = true
1136
1280
 
1137
- port = nil
1138
- last_attempt_output = nil
1281
+ port = nil
1282
+ output = ''
1283
+
1139
1284
  10.times do |i|
1140
- done = false
1141
- port = available_port
1285
+ # Clear output of previous attempt.
1286
+ output = ''
1287
+ done = false
1288
+ port = Utilities.available_port
1142
1289
 
1143
1290
  print_debug "Attempt ##{i}, chose port number #{port}"
1144
1291
 
@@ -1146,52 +1293,48 @@ class Browser
1146
1293
  with_timeout 10 do
1147
1294
  print_debug "Spawning process: #{self.class.executable}"
1148
1295
 
1149
- @process = ChildProcess.build(
1150
- self.class.executable,
1151
- "--webdriver=#{port}",
1152
- "--proxy=http://#{@proxy.address}/",
1153
-
1154
- # As lax as possible to allow for easy SSL interception.
1155
- # The actual request to the origin server will obey
1156
- # the system-side SSL options.
1157
- '--ignore-ssl-errors=true',
1158
- '--ssl-protocol=any',
1159
-
1160
- '--disk-cache=true',
1161
- "--debug=#{!!debug?}"
1296
+ r, w = IO.pipe
1297
+ ri, @kill_process = IO.pipe
1298
+
1299
+ @lifeline_pid = Processes::Manager.spawn(
1300
+ :browser,
1301
+ executable: self.class.executable,
1302
+ without_arachni: true,
1303
+ fork: false,
1304
+ new_pgroup: true,
1305
+ stdin: ri,
1306
+ stdout: w,
1307
+ stderr: w,
1308
+ port: port,
1309
+ proxy_url: @proxy.url
1162
1310
  )
1163
- # @process.leader = true
1164
- @process.detach = true
1165
1311
 
1166
- @process.io.stdout = Tempfile.new( 'phantomjs-out' )
1167
- @process.io.stderr = @process.io.stdout
1168
- @process.io.stderr.sync = @process.io.stdout.sync = true
1312
+ w.close
1313
+ ri.close
1169
1314
 
1170
- @process.start
1171
1315
  print_debug 'Process spawned, waiting for it to boot-up...'
1172
1316
 
1173
- File.open( @process.io.stdout.path, 'r' ) do |out|
1174
- buff = ''
1175
- # Wait for PhantomJS to initialize.
1176
- while !buff.include?( 'running on port' )
1177
- # This can be problematic on something other than
1178
- # MRI.
1179
- buff << (out.readline rescue '').to_s
1317
+ # Wait for PhantomJS to initialize.
1318
+ while !output.include?( 'running on port' )
1319
+ begin
1320
+ output << r.readpartial( 8192 )
1321
+ # EOF or something, take a breather before retrying.
1322
+ rescue
1323
+ sleep 0.05
1180
1324
  end
1325
+ end
1181
1326
 
1182
- buff = nil
1183
- print_debug 'Boot-up complete.'
1184
- done = true
1185
- end
1327
+ @browser_pid = output.scan( /^PID: (\d+)/ ).flatten.first.to_i
1328
+
1329
+ print_debug 'Boot-up complete.'
1330
+ done = true
1186
1331
  end
1187
1332
  rescue Timeout::Error
1188
1333
  print_debug 'Spawn timed-out.'
1189
1334
  end
1190
1335
 
1191
- if @process.io.stdout
1192
- last_attempt_output = IO.read( @process.io.stdout )
1193
- print_debug last_attempt_output
1194
- @process.io.stdout.close!
1336
+ if !output.empty?
1337
+ print_debug output
1195
1338
  end
1196
1339
 
1197
1340
  if done
@@ -1208,42 +1351,29 @@ class Browser
1208
1351
  #
1209
1352
  # Bail out for now and count on the BrowserCluster to retry to boot
1210
1353
  # another process ass needed.
1211
- if !@process
1354
+ if !@lifeline_pid
1212
1355
  log_error 'Could not spawn browser process.'
1213
- log_error last_attempt_output
1356
+ log_error output
1214
1357
  return
1215
1358
  end
1216
1359
 
1217
- begin
1218
- @pid = @process.pid
1219
- # Not supported on JRuby on MS Windows.
1220
- rescue NotImplementedError
1221
- end
1222
-
1223
1360
  @browser_url = "http://127.0.0.1:#{port}"
1224
1361
  end
1225
1362
 
1226
1363
  def kill_process
1227
- begin
1228
- if @process && @process.alive?
1229
- @process.stop
1230
- @process.io.close rescue nil
1364
+ if @kill_process
1365
+ begin
1366
+ @kill_process.close
1367
+ rescue
1231
1368
  end
1232
- rescue Errno::ECHILD
1233
- false
1234
1369
  end
1235
1370
 
1236
- @process = nil
1237
- @watir = nil
1238
- @selenium = nil
1239
- @pid = nil
1240
- @browser_url = nil
1241
- end
1242
-
1243
- def browser_alive?
1244
- @watir && @process && @process.alive?
1245
- rescue Errno::ECHILD
1246
- false
1371
+ @kill_process = nil
1372
+ @watir = nil
1373
+ @selenium = nil
1374
+ @lifeline_pid = nil
1375
+ @browser_pid = nil
1376
+ @browser_url = nil
1247
1377
  end
1248
1378
 
1249
1379
  def store_pages?
@@ -1267,52 +1397,50 @@ class Browser
1267
1397
  end
1268
1398
 
1269
1399
  def wait_for_pending_requests
1270
- # With AJAX requests being asynchronous and everything we need
1271
- # to wait a split second to give the browser time to initialize
1272
- # a connection.
1273
- #
1274
- # TODO: Add XMLHttpRequest.send() overrides to the DOMMonitor so
1275
- # that we'll know for sure when to wait.
1276
- sleep 0.1
1400
+ print_debug_level_2 "Waiting for #{@proxy.pending_requests} requests to complete..."
1277
1401
 
1278
- # Wait for pending requests to complete.
1279
- #
1280
- # The HTTP timeout option already guards us against this but I don't
1281
- # fully trust the proxy so we're using #with_timeout as a fallback.
1282
- with_timeout Options.http.request_timeout / 1_000 do
1283
- sleep 0.1 while @proxy.has_connections?
1284
- end
1402
+ sleep 0.1
1403
+ sleep 0.01 while @proxy.has_pending_requests?
1285
1404
 
1286
- true
1287
- rescue Timeout::Error
1288
- #ap 'PENDING REQUESTS TIMEOUT'
1289
- #ap caller
1290
- false
1405
+ print_debug_level_2 '...done.'
1291
1406
  end
1292
1407
 
1293
1408
  def load_cookies( url, cookies = {} )
1294
1409
  # First clears the browser's cookies and then tricks it into accepting
1295
1410
  # the system cookies for its cookie-jar.
1411
+ #
1412
+ # Well, it doesn't really clear the browser's cookie-jar, but that's
1413
+ # not necessary because whatever cookies the browser has have already
1414
+ # gotten into the system-wide cookiejar, and since we're passing
1415
+ # all applicable cookies to the browser the end result will be that
1416
+ # it'll have the wanted values.
1296
1417
 
1297
1418
  url = normalize_url( url )
1298
- watir.cookies.clear
1299
1419
 
1300
1420
  set_cookies = {}
1301
1421
  HTTP::Client.cookie_jar.for_url( url ).each do |cookie|
1302
1422
  cookie = cookie.dup
1303
- cookie.data.delete :domain
1304
1423
  set_cookies[cookie.name] = cookie
1305
1424
  end
1425
+
1306
1426
  cookies.each do |name, value|
1307
1427
  if set_cookies[name]
1308
1428
  set_cookies[name] = set_cookies[name].dup
1429
+
1430
+ # Don't forget this, otherwise the #to_set_cookie call will
1431
+ # return the original raw data.
1432
+ set_cookies[name].affected_input_name = name
1309
1433
  set_cookies[name].update( name => value )
1310
1434
  else
1311
1435
  set_cookies[name] = Cookie.new( url: url, inputs: { name => value } )
1312
1436
  end
1313
1437
  end
1314
1438
 
1315
- url = "#{url}/set-cookies-#{request_token}"
1439
+ return if set_cookies.empty? &&
1440
+ Arachni::Options::browser_cluster.local_storage.empty?
1441
+
1442
+ set_cookie = set_cookies.values.map(&:to_set_cookie)
1443
+ print_debug_level_2 "Setting cookies: #{set_cookie}"
1316
1444
 
1317
1445
  body = ''
1318
1446
  if Arachni::Options::browser_cluster.local_storage.any?
@@ -1327,11 +1455,12 @@ class Browser
1327
1455
  EOJS
1328
1456
  end
1329
1457
 
1330
- watir.goto preload( HTTP::Response.new(
1331
- url: url,
1458
+ @selenium.navigate.to preload( HTTP::Response.new(
1459
+ code: 200,
1460
+ url: "#{url}/set-cookies-#{request_token}",
1332
1461
  body: body,
1333
1462
  headers: {
1334
- 'Set-Cookie' => set_cookies.values.map(&:to_set_cookie)
1463
+ 'Set-Cookie' => set_cookie
1335
1464
  }
1336
1465
  ))
1337
1466
  end
@@ -1339,12 +1468,26 @@ EOJS
1339
1468
  # Makes sure we have at least 2 windows open so that we can switch to the
1340
1469
  # last available one in case there's some JS in the page that closes one.
1341
1470
  def ensure_open_window
1342
- return if watir.windows.size > 1
1471
+ window_handles = @selenium.window_handles
1472
+
1473
+ if window_handles.size == 0
1474
+ @javascript.run( 'window.open()' )
1475
+ @selenium.switch_to.window( @selenium.window_handles.last )
1476
+ else
1477
+ if window_handles.size > 1
1478
+ # Keep the first
1479
+ window_handles[1..-1].each do |handle|
1480
+ @selenium.switch_to.window( handle )
1481
+ @selenium.close
1482
+ end
1343
1483
 
1344
- watir.windows.last.use
1345
- watir.window.resize_to( @width, @height )
1484
+ @selenium.switch_to.window( @selenium.window_handles.first )
1485
+ end
1346
1486
 
1347
- @javascript.run( 'window.open()' )
1487
+ @selenium.navigate.to 'about:blank'
1488
+ end
1489
+
1490
+ @selenium.manage.window.resize_to( @width, @height )
1348
1491
  end
1349
1492
 
1350
1493
  # # Firefox driver, only used for debugging.
@@ -1362,6 +1505,11 @@ EOJS
1362
1505
 
1363
1506
  def capabilities
1364
1507
  Selenium::WebDriver::Remote::Capabilities.phantomjs(
1508
+ # Selenium tries to be helpful by including screenshots for errors
1509
+ # in the JSON response. That's not gonna fly in this use case as
1510
+ # parsing lots of massive JSON responses at the same time will
1511
+ # have a significant impact on performance.
1512
+ takes_screenshot: false,
1365
1513
  'phantomjs.page.settings.userAgent' => Options.http.user_agent,
1366
1514
  'phantomjs.page.customHeaders.X-Arachni-Browser-Auth' => auth_token,
1367
1515
  'phantomjs.page.settings.resourceTimeout' => Options.http.request_timeout,
@@ -1389,6 +1537,8 @@ EOJS
1389
1537
  return if request.headers['X-Arachni-Browser-Auth'] != auth_token
1390
1538
  request.headers.delete 'X-Arachni-Browser-Auth'
1391
1539
 
1540
+ print_debug_level_2 "Request: #{request.url}"
1541
+
1392
1542
  # We can't have 304 page responses in the framework, we need full request
1393
1543
  # and response data, the browser cache doesn't help us here.
1394
1544
  #
@@ -1399,41 +1549,47 @@ EOJS
1399
1549
  request.headers.delete 'If-Modified-Since'
1400
1550
  end
1401
1551
 
1402
- return if @javascript.serve( request, response )
1552
+ if @javascript.serve( request, response )
1553
+ print_debug_level_2 "Serving local JS."
1554
+ return
1555
+ end
1403
1556
 
1404
1557
  if !request.url.include?( request_token )
1405
- return if ignore_request?( request )
1558
+ if ignore_request?( request )
1559
+ print_debug_level_2 "Out of scope, ignoring."
1560
+ return
1561
+ end
1406
1562
 
1407
1563
  if @add_request_transitions
1408
- @request_transitions << Page::DOM::Transition.new( request.url, :request )
1564
+ synchronize do
1565
+ @request_transitions << Page::DOM::Transition.new(
1566
+ request.url, :request
1567
+ )
1568
+ end
1409
1569
  end
1410
1570
  end
1411
1571
 
1412
1572
  # Signal the proxy to not actually perform the request if we have a
1413
1573
  # preloaded or cached response for it.
1414
1574
  if from_preloads( request, response ) || from_cache( request, response )
1575
+ print_debug_level_2 'Resource has been preloaded.'
1576
+
1415
1577
  # There may be taints or custom JS code that need to be updated.
1416
1578
  javascript.inject response
1417
1579
  return
1418
1580
  end
1419
1581
 
1582
+ print_debug_level_2 'Request can proceed to origin.'
1583
+
1420
1584
  # Capture the request as elements of pages -- let's us grab AJAX and
1421
1585
  # other browser requests and convert them into elements we can analyze
1422
1586
  # and audit.
1423
- capture( request )
1587
+ if request.scope.in?
1588
+ capture( request )
1589
+ end
1424
1590
 
1425
1591
  request.headers['user-agent'] = Options.http.user_agent
1426
1592
 
1427
- # The proxy has an unlimited response_max_size so if we're not requesting
1428
- # an asset remove the response_max_size option so that it'll end up using
1429
- # the system settings.
1430
- #
1431
- # However, this is not foolproof, a lot of assets don't have the expected
1432
- # extension.
1433
- if !request_for_asset?( request )
1434
- request.response_max_size = nil
1435
- end
1436
-
1437
1593
  # Signal the proxy to continue with its request to the origin server.
1438
1594
  true
1439
1595
  end
@@ -1441,41 +1597,99 @@ EOJS
1441
1597
  def response_handler( request, response )
1442
1598
  return if request.url.include?( request_token )
1443
1599
 
1600
+ print_debug_level_2 "Got response: #{response.url}"
1601
+
1444
1602
  @request_transitions.each do |transition|
1445
1603
  next if !transition.running? || transition.element != request.url
1446
1604
  transition.complete
1447
1605
  end
1448
1606
 
1449
- javascript.inject response
1607
+ # If we abort the request because it's out of scope we need to emulate
1608
+ # an OK response because we **do** want to be able to grab a page with
1609
+ # the out of scope URL, even if it's empty.
1610
+ # For example, unvalidated_redirect checks need this.
1611
+ if response.code == 0
1612
+ if enforce_scope? && response.scope.out?
1613
+ response.code = 200
1614
+ end
1615
+ else
1616
+ if javascript.inject( response )
1617
+ print_debug_level_2 'Injected custom JS.'
1618
+ end
1619
+ end
1450
1620
 
1451
1621
  # Don't store assets, the browsers will cache them accordingly.
1452
- return if request_for_asset?( request ) || !response.text?
1622
+ if request_for_asset?( request ) || !response.text?
1623
+ print_debug_level_2 'Asset detected, will not store.'
1624
+ return
1625
+ end
1453
1626
 
1454
1627
  # No-matter the scope, don't store resources for external domains.
1455
- return if !response.scope.in_domain?
1628
+ if !response.scope.in_domain?
1629
+ print_debug_level_2 'Outside of domain scope, will not store.'
1630
+ return
1631
+ end
1456
1632
 
1457
- return if enforce_scope? && response.scope.out?
1633
+ if enforce_scope? && response.scope.out?
1634
+ print_debug_level_2 'Outside of general scope, will not store.'
1635
+ return
1636
+ end
1458
1637
 
1459
1638
  whitelist_asset_domains( response )
1460
1639
  save_response response
1461
1640
 
1641
+ print_debug_level_2 'Stored.'
1642
+
1462
1643
  nil
1463
1644
  end
1464
1645
 
1465
1646
  def ignore_request?( request )
1466
- return if !enforce_scope?
1647
+ print_debug_level_2 "Checking: #{request.url}"
1648
+
1649
+ if !enforce_scope?
1650
+ print_debug_level_2 'Allow: Scope enforcement disabled.'
1651
+ return
1652
+ end
1653
+
1654
+ if request_for_asset?( request )
1655
+ print_debug_level_2 'Allow: Asset detected.'
1656
+ return false
1657
+ end
1658
+
1659
+ if !request.scope.follow_protocol?
1660
+ print_debug_level_2 'Disallow: Cannot follow protocol.'
1661
+ return true
1662
+ end
1663
+
1664
+ if !request.scope.in_domain?
1665
+ if self.class.asset_domains.include?( request.parsed_url.domain )
1666
+ print_debug_level_2 'Allow: Out of scope but in CDN list.'
1667
+ return false
1668
+ end
1467
1669
 
1468
- return false if request_for_asset?( request )
1670
+ print_debug_level_2 'Disallow: Domain out of scope and not in CDN list.'
1671
+ return true
1672
+ end
1469
1673
 
1470
- return true if !request.scope.follow_protocol?
1674
+ if request.scope.too_deep?
1675
+ print_debug_level_2 'Disallow: Too deep.'
1676
+ return true
1677
+ end
1471
1678
 
1472
- return true if !request.scope.in_domain? &&
1473
- !self.class.asset_domains.include?( request.parsed_url.domain )
1679
+ if !request.scope.include?
1680
+ print_debug_level_2 'Disallow: Does not match inclusion rules.'
1681
+ return true
1682
+ end
1474
1683
 
1475
- return true if request.scope.too_deep?
1476
- return true if !request.scope.include?
1477
- return true if request.scope.exclude?
1478
- return true if request.scope.redundant?
1684
+ if request.scope.exclude?
1685
+ print_debug_level_2 'Disallow: Matches exclusion rules.'
1686
+ return true
1687
+ end
1688
+
1689
+ if request.scope.redundant?
1690
+ print_debug_level_2 'Disallow: Matches redundant rules.'
1691
+ return true
1692
+ end
1479
1693
 
1480
1694
  false
1481
1695
  end
@@ -1485,9 +1699,13 @@ EOJS
1485
1699
  end
1486
1700
 
1487
1701
  def whitelist_asset_domains( response )
1488
- ASSET_EXTRACTORS.each do |regexp|
1489
- response.body.scan( regexp ).flatten.compact.each do |url|
1490
- self.class.add_asset_domain url
1702
+ synchronize do
1703
+ ASSET_EXTRACTORS.each do |regexp|
1704
+ response.body.scan( regexp ).flatten.compact.each do |url|
1705
+ next if !(domain = self.class.add_asset_domain( url ))
1706
+
1707
+ print_debug_level_2 "#{domain} from #{url} based on #{regexp.source}"
1708
+ end
1491
1709
  end
1492
1710
  end
1493
1711
  end
@@ -1504,11 +1722,15 @@ EOJS
1504
1722
  found_element = false
1505
1723
 
1506
1724
  if (json = JSON.from_request( @last_url, request ))
1725
+ print_debug_level_2 "Extracted JSON input:\n#{json.source}"
1726
+
1507
1727
  elements[:jsons] << json
1508
1728
  found_element = true
1509
1729
  end
1510
1730
 
1511
1731
  if !found_element && (xml = XML.from_request( @last_url, request ))
1732
+ print_debug_level_2 "Extracted XML input:\n#{xml.source}"
1733
+
1512
1734
  elements[:xmls] << xml
1513
1735
  found_element = true
1514
1736
  end
@@ -1539,13 +1761,30 @@ EOJS
1539
1761
  return
1540
1762
  end
1541
1763
 
1764
+ if (form = elements[:forms].last)
1765
+ print_debug_level_2 "Extracted form input:\n" <<
1766
+ "#{form.method.to_s.upcase} #{form.action} #{form.inputs}"
1767
+ end
1768
+
1542
1769
  el = elements.values.flatten
1543
1770
 
1771
+ if el.empty?
1772
+ print_debug_level_2 'No elements found.'
1773
+ return
1774
+ end
1775
+
1544
1776
  # Don't bother if the system in general has already seen the vectors.
1545
- return if el.empty? || !el.find { |e| !ElementFilter.include?( e ) }
1777
+ if el.empty? || !el.find { |e| !ElementFilter.include?( e ) }
1778
+ print_debug_level_2 'Ignoring, already seen.'
1779
+ return
1780
+ end
1546
1781
 
1547
1782
  begin
1548
- return if !el.find { |e| !skip_state?( e ) }
1783
+ if !el.find { |e| !skip_state?( e ) }
1784
+ print_debug_level_2 'Ignoring, already seen.'
1785
+ return
1786
+ end
1787
+
1549
1788
  el.each { |e| skip_state e.id }
1550
1789
  # This could be an orphaned HTTP request, without a job, if running in
1551
1790
  # BrowserCluster::Worker.
@@ -1565,21 +1804,25 @@ EOJS
1565
1804
  end
1566
1805
 
1567
1806
  def from_preloads( request, response )
1568
- return if !(preloaded = preloads.delete( request.url ))
1807
+ synchronize do
1808
+ return if !(preloaded = preloads.delete( request.url ))
1569
1809
 
1570
- copy_response_data( preloaded, response )
1571
- response.request = request
1572
- save_response( response ) if !preloaded.url.include?( request_token )
1810
+ copy_response_data( preloaded, response )
1811
+ response.request = request
1812
+ save_response( response ) if !preloaded.url.include?( request_token )
1573
1813
 
1574
- preloaded
1814
+ preloaded
1815
+ end
1575
1816
  end
1576
1817
 
1577
1818
  def from_cache( request, response )
1578
- return if !@cache.include?( request.url )
1819
+ synchronize do
1820
+ return if !@cache.include?( request.url )
1579
1821
 
1580
- copy_response_data( @cache[request.url], response )
1581
- response.request = request
1582
- save_response response
1822
+ copy_response_data( @cache[request.url], response )
1823
+ response.request = request
1824
+ save_response response
1825
+ end
1583
1826
  end
1584
1827
 
1585
1828
  def copy_response_data( source, destination )