web_tsunami 0.2.0 → 0.3.0

Sign up to get free protection for your applications and to get access to all the features.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 5684946d08e860b309d812707e690d91f45698140d5a22b1d8fe963f85203f25
4
- data.tar.gz: dd58e555fc6c6dc89c3e1205299fec88c3879635ea36f4e78baa49a5f00993e5
3
+ metadata.gz: feec552f2db4cf26a58dc9349313afd80bfd7da50f5143e3475f4c9cad497d6d
4
+ data.tar.gz: 54fafe2b562ba52ae68c12930ae8c33f80e3e6557b4e3d74cb5e3fa741a3ea9d
5
5
  SHA512:
6
- metadata.gz: 1a29b20791f56ba1e87b4168777fd9a362a6bc06c3548bd5bc1a7a2d44b4d765153fb24ca732fea99172d0a2d2ffbdbad08aec7d0063cc018fa5309e740c647f
7
- data.tar.gz: 3991dbf3d1b3d72c2aa40f734a153a8ed40f4bfbc903afdd6f3780c4b81ef918b0ed4033ee5f3521837342987ff16cd1a5f2a5f2d5a68990039c0a2d67356189
6
+ metadata.gz: 6359c7e63dae34c480395d4f27f040b524fca85e1bb48422ab522ea8da899a39ec59613ff4ce3d7068b50f0c31e06b27f1087560d8506aa8fbf27a4df5e7b804
7
+ data.tar.gz: c967132a8f0ad8a9346683ed3dd84436e33d0a01275a73511aadd87a65bb45b84455aa89e54501e974b1d6d58b150b6dfb2597dd7db6f8a63df4dbafde2778bd
data/CHANGELOG.md CHANGED
@@ -1,5 +1,9 @@
1
1
  # Changelog of Web Tsunami
2
2
 
3
+ ## 0.3.0 (2024-02-01)
4
+
5
+ * Add session object to handle automatically cookies and CSRF tokens
6
+
3
7
  ## 0.2.0 (2024-01-25)
4
8
 
5
9
  * Add methods post, put, patch and delete
data/README.md CHANGED
@@ -56,69 +56,34 @@ SearchTsunami.start(concurrency: 100, duration: 60 * 10)
56
56
  In this scenario, a visitor comes on the index page, then search for _stress test_, then go on a random page of the search result, and finally found the stress test page.
57
57
  It introduces a unique parameters which is the page number.
58
58
  It's nice, but it could have almost be done with Siege.
59
- Let me show you a more advanced scenario.
59
+ Let me show you a more realistic scenario.
60
60
 
61
61
  ```ruby
62
62
  require "web_tsunami"
63
- require "json"
64
63
 
65
- class RegistrationTsunami < WebTsunami::Scenario
66
- # Simulate a visitor coming on the index page,
67
- # then going to the registration form,
68
- # and finally submitting the form.
64
+ class SessionTsunami < WebTsunami::Scenario
69
65
  def run
70
- get("http://site.example/") do |response|
71
- get("http://site.example/account/new") do |response|
72
- post("http://site.example/account", post_account_options(response)) do |response|
73
- # Then visiting the dashboard, and so on.
66
+ # The session object stores cookies and automatically submit CSRF token with forms
67
+ session = WebTsunami::Session.new(self, "https://site.example")
68
+ session.get("/") do
69
+ session.get("/account/new") do
70
+ # An authenticity_token param is automatically added by the session
71
+ session.post("/account", body: {account: "#{rand(1000000)}@email.test", password: "password"}}) do |response|
72
+ # The session stores the Set-Cookie header and will provide it to the next requests
73
+ session.get("/dashboard") do # Redirection after registration
74
+ # And so on
75
+ end
74
76
  end
75
77
  end
76
78
  end
77
79
  end
78
-
79
- private
80
-
81
- def post_account_options(response)
82
- # In order to not be blocked by the Cross-Site Request Forgery, the request must contain :
83
- # 1. Cookie header
84
- # 2. authenticity_token form param
85
- {
86
- headers: build_post_headers(response),
87
- body: JSON.generate(
88
- authenticity_token: extract_csrf_token(response.body),
89
- user: {
90
- name: name = rand.to_s[2..-1],
91
- email: "#{name}@domain.test",
92
- password: name,
93
- }
94
- ),
95
- }
96
- end
97
-
98
- def build_post_headers(response)
99
- {
100
- "Origin" => response.request&.base_url,
101
- "Content-Type" => "application/json",
102
- "Cookie" => response.headers["Set-Cookie"],
103
- # To Simulate a post XmlHttpRequest from JavaScript, you should provide these headers :
104
- # "X-CSRF-Token" => extract_csrf_token(response.body),
105
- # "X-Requested-With" => "XMLHttpRequest"
106
- }
107
- end
108
-
109
- CSRF_REGEX = /<meta name="csrf-token" content="([^"]+)"/
110
-
111
- def extract_csrf_token(html)
112
- html.match(CSRF_REGEX)[1]
113
- end
114
80
  end
115
81
 
116
- # Simulates 100 concurrent visitors every second for 10 minutes
117
- RegistrationTsunami.start(concurrency: 100, duration: 10)
82
+ SessionTsunami.start(concurrency: 100, duration: 60 * 10)
118
83
  ```
119
84
 
120
- This is more complex because it handles CSRF and every submitted forms are unique.
121
- Indeed emails must be unique, so it's not possible to send the same data everytime.
85
+ This is more realistic because it handles CSRF tokens and cookies.
86
+ Thus the scenario can submit forms and behaves a little bit more like a real visitor.
122
87
 
123
88
  ## Output and result
124
89
 
@@ -0,0 +1,65 @@
1
+ # frozen_string_literal: true
2
+
3
+ module WebTsunami
4
+ # Scenario is the class that handle all the parallel requests.
5
+ class Scenario
6
+ attr_reader :concurrency
7
+
8
+ def self.start(options)
9
+ options[:duration].times { fork { new(options[:concurrency]).start } and sleep(1) }
10
+ Process.wait
11
+ end
12
+
13
+ def initialize(concurrency)
14
+ @concurrency = concurrency
15
+ end
16
+
17
+ def requests
18
+ @requests ||= Typhoeus::Hydra.new
19
+ end
20
+
21
+ def get(url, options = {}, &block)
22
+ request(:get, url, options, &block)
23
+ end
24
+
25
+ def post(url, options = {}, &block)
26
+ request(:post, url, options, &block)
27
+ end
28
+
29
+ def put(url, options = {}, &block)
30
+ request(:put, url, options, &block)
31
+ end
32
+
33
+ def patch(url, options = {}, &block)
34
+ request(:patch, url, options, &block)
35
+ end
36
+
37
+ def delete(url, options = {}, &block)
38
+ request(:delete, url, options, &block)
39
+ end
40
+
41
+ def request(method, url, options, &block)
42
+ req = Typhoeus::Request.new(url, {method: method}.merge(options))
43
+ requests.queue(req)
44
+ req.on_complete do |response|
45
+ if response.timed_out?
46
+ puts "Timeout #{url}"
47
+ elsif response.code == 0
48
+ puts "#{response.return_message} #{response.request.options[:method]} #{url}"
49
+ elsif !response.success? && ![302, 303].include?(response.code)
50
+ puts "#{response.code} #{response.request.options[:method]} #{url}"
51
+ end
52
+ block.call(response) if block
53
+ end
54
+ end
55
+
56
+ def start
57
+ concurrency.times { run }
58
+ requests.run
59
+ end
60
+
61
+ def run
62
+ raise NotImplementedError
63
+ end
64
+ end
65
+ end
@@ -0,0 +1,86 @@
1
+ # frozen_string_literal: true
2
+
3
+ module WebTsunami
4
+ # Session is a helper class to be used inside a scenario.
5
+ # It's purpose is to avoid low level manipulations to handle cookies and CSRF tokens automatically.
6
+ class Session
7
+ attr_reader :scenario, :root_url
8
+
9
+ attr_reader :cookies, :last_response
10
+
11
+ def initialize(scenario, root_url)
12
+ @scenario = scenario
13
+ @root_url = root_url
14
+ @cookies = {}
15
+ @last_response = nil
16
+ end
17
+
18
+ def get(path, options = {}, &block)
19
+ url = File.join(root_url, path)
20
+ inject_headers(default_headers, options)
21
+ scenario.get(url, options) do |response|
22
+ @last_response = response
23
+ save_cookies(response)
24
+ block&.call(response)
25
+ end
26
+ end
27
+
28
+ def post(path, options = {}, &block)
29
+ url = File.join(root_url, path)
30
+ inject_headers(default_post_headers, options)
31
+ inject_csrf_token(options)
32
+ scenario.post(url, options) do |response|
33
+ @last_response = response
34
+ save_cookies(response)
35
+ block&.call(response)
36
+ end
37
+ end
38
+
39
+ private
40
+
41
+ def default_headers
42
+ {
43
+ "Origin" => last_response && request_origin_header(last_response.request),
44
+ "Cookie" => cookies.map { |(k,v)| "#{k}=#{v}" }.join(" "),
45
+ }
46
+ end
47
+
48
+ def default_post_headers
49
+ default_headers.merge("Content-Type" => "application/x-www-form-urlencoded;charset=UTF-8")
50
+ end
51
+
52
+ def request_origin_header(request)
53
+ return "null" unless request
54
+ uri = URI(request.base_url.to_s)
55
+ if [80, 443].include?(uri.port)
56
+ "#{uri.scheme}://#{uri.host}"
57
+ else
58
+ "#{uri.scheme}://#{uri.host}:#{uri.port}"
59
+ end
60
+ end
61
+
62
+ CSRF_REGEX = /<meta name="csrf-token" content="([^"]+)"/
63
+
64
+ def extract_csrf_token(html)
65
+ html.match(CSRF_REGEX)[1]
66
+ end
67
+
68
+ def save_cookies(response)
69
+ return unless header = response.headers["Set-Cookie"]
70
+ Array(header).each do |cookie|
71
+ name, value = cookie.split(" ", 2)[0].split("=")
72
+ @cookies[name] = value
73
+ end
74
+ end
75
+
76
+ def inject_headers(headers, options)
77
+ options[:headers] = headers.merge(options[:headers] || {})
78
+ end
79
+
80
+ def inject_csrf_token(options)
81
+ if options[:body].is_a?(Hash) && last_response
82
+ options[:body] = {authenticity_token: extract_csrf_token(last_response.body)}.merge(options[:body])
83
+ end
84
+ end
85
+ end
86
+ end
@@ -1,3 +1,3 @@
1
1
  module WebTsunami
2
- VERSION = "0.2.0"
2
+ VERSION = "0.3.0"
3
3
  end
data/lib/web_tsunami.rb CHANGED
@@ -1,68 +1,5 @@
1
1
  # -*- encoding : utf-8 -*-
2
2
 
3
- require 'typhoeus'
4
-
5
- module WebTsunami
6
- class Scenario
7
-
8
- attr_reader :concurrency
9
-
10
- def self.start(options)
11
- options[:duration].times { fork { new(options[:concurrency]).start } and sleep(1) }
12
- Process.wait
13
- end
14
-
15
- def initialize(concurrency)
16
- @concurrency = concurrency
17
- end
18
-
19
- def requests
20
- @requests ||= Typhoeus::Hydra.new
21
- end
22
-
23
- def get(url, options = {}, &block)
24
- request(:get, url, options, &block)
25
- end
26
-
27
- def post(url, options = {}, &block)
28
- request(:post, url, options, &block)
29
- end
30
-
31
- def put(url, options = {}, &block)
32
- request(:put, url, options, &block)
33
- end
34
-
35
- def patch(url, options = {}, &block)
36
- request(:patch, url, options, &block)
37
- end
38
-
39
- def delete(url, options = {}, &block)
40
- request(:delete, url, options, &block)
41
- end
42
-
43
- def request(method, url, options, &block)
44
- req = Typhoeus::Request.new(url, {method: method}.merge(options))
45
- requests.queue(req)
46
- req.on_complete do |response|
47
- if response.timed_out?
48
- puts "Timeout #{url}"
49
- elsif response.code == 0
50
- puts "#{response.return_message} #{url}"
51
- elsif !response.success? && response.code != 302
52
- puts "#{response.code} #{url}"
53
- end
54
- block.call(response) if block
55
- end
56
- end
57
-
58
- def start
59
- concurrency.times { run }
60
- requests.run
61
- end
62
-
63
- def run
64
- raise NotImplementedError
65
- end
66
-
67
- end
68
- end
3
+ require "typhoeus"
4
+ require "web_tsunami/scenario"
5
+ require "web_tsunami/session"
metadata CHANGED
@@ -1,14 +1,14 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: web_tsunami
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.2.0
4
+ version: 0.3.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - Alexis Bernard
8
8
  autorequire:
9
9
  bindir: bin
10
10
  cert_chain: []
11
- date: 2024-01-25 00:00:00.000000000 Z
11
+ date: 2024-02-01 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: typhoeus
@@ -37,6 +37,8 @@ files:
37
37
  - LICENSE.txt
38
38
  - README.md
39
39
  - lib/web_tsunami.rb
40
+ - lib/web_tsunami/scenario.rb
41
+ - lib/web_tsunami/session.rb
40
42
  - lib/web_tsunami/version.rb
41
43
  - web_tsunami.gemspec
42
44
  - web_tsunami.png