web_tsunami 0.2.0 → 0.3.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
checksums.yaml 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