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 +4 -4
- data/CHANGELOG.md +4 -0
- data/README.md +15 -50
- data/lib/web_tsunami/scenario.rb +65 -0
- data/lib/web_tsunami/session.rb +86 -0
- data/lib/web_tsunami/version.rb +1 -1
- data/lib/web_tsunami.rb +3 -66
- metadata +4 -2
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA256:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: feec552f2db4cf26a58dc9349313afd80bfd7da50f5143e3475f4c9cad497d6d
|
4
|
+
data.tar.gz: 54fafe2b562ba52ae68c12930ae8c33f80e3e6557b4e3d74cb5e3fa741a3ea9d
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: 6359c7e63dae34c480395d4f27f040b524fca85e1bb48422ab522ea8da899a39ec59613ff4ce3d7068b50f0c31e06b27f1087560d8506aa8fbf27a4df5e7b804
|
7
|
+
data.tar.gz: c967132a8f0ad8a9346683ed3dd84436e33d0a01275a73511aadd87a65bb45b84455aa89e54501e974b1d6d58b150b6dfb2597dd7db6f8a63df4dbafde2778bd
|
data/CHANGELOG.md
CHANGED
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
|
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
|
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
|
-
|
71
|
-
|
72
|
-
|
73
|
-
|
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
|
-
|
117
|
-
RegistrationTsunami.start(concurrency: 100, duration: 10)
|
82
|
+
SessionTsunami.start(concurrency: 100, duration: 60 * 10)
|
118
83
|
```
|
119
84
|
|
120
|
-
This is more
|
121
|
-
|
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
|
data/lib/web_tsunami/version.rb
CHANGED
data/lib/web_tsunami.rb
CHANGED
@@ -1,68 +1,5 @@
|
|
1
1
|
# -*- encoding : utf-8 -*-
|
2
2
|
|
3
|
-
require
|
4
|
-
|
5
|
-
|
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.
|
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
|
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
|