timing_attack 0.1.0 → 0.2.1
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/README.md +40 -38
- data/exe/timing_attack +17 -8
- data/lib/timing_attack/cli_attacker.rb +106 -0
- data/lib/timing_attack/grouper.rb +99 -0
- data/lib/timing_attack/test_case.rb +11 -24
- data/lib/timing_attack/version.rb +1 -1
- data/lib/timing_attack.rb +2 -1
- data/timing_attack.gemspec +1 -1
- metadata +5 -4
- data/lib/timing_attack/attacker.rb +0 -130
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA1:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: 5b6f971fe9e7eb6b8aa282dffbd7c0eea3a2f009
|
4
|
+
data.tar.gz: e05deb06b3c31be39f128c7906ef3534abad6d09
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: c0e1f43f47fafd477678072023fcba97c6db2dbe12f6d09f6c1face92feed5fe6a17f2a16e79bb692e0db1b4e912f7136f5cd47d3f07e66dabafbf52ab293df3
|
7
|
+
data.tar.gz: 8977b5ddbea3da6e3511c1e9b0c9921d194a99e1cfabe7cbe104882579a949a562458fcc17adf72ded2ddb8cbc08e51cf4950349fe30046f9e342f85d5743e73
|
data/README.md
CHANGED
@@ -12,24 +12,21 @@ discrepancies in the application's response time.
|
|
12
12
|
## Usage
|
13
13
|
|
14
14
|
```
|
15
|
-
timing_attack [options] -
|
15
|
+
timing_attack [options] -u <target> <inputs>
|
16
16
|
-u, --url URL URL of endpoint to profile
|
17
|
-
-a, --a-example A_EXAMPLE Known test case that belongs to Group A
|
18
|
-
-b, --b-example B_EXAMPLE Known test case that belongs to Group B
|
19
17
|
-n, --number NUM Requests per input
|
20
|
-
|
21
|
-
--b-name B_NAME Name of Group B
|
18
|
+
-t, --threshold NUM Minimum threshold, in seconds, for meaningfulness (default: 0.05)
|
22
19
|
-p, --post Use POST, not GET
|
23
20
|
-q, --quiet Quiet mode (don't display progress bars)
|
21
|
+
--mean Use mean for calculations
|
22
|
+
--median Use median for calculations
|
23
|
+
--percentile N Use Nth percentile for calculations
|
24
24
|
-v, --version Print version information
|
25
25
|
-h, --help Display this screen
|
26
26
|
```
|
27
27
|
|
28
|
-
**NB**: If the provided examples are invalid, discvery will fail. Always check
|
29
|
-
your results! If very similar inputs are being sorted differently, you may have
|
30
|
-
used bad training data.
|
31
|
-
|
32
28
|
### An example
|
29
|
+
|
33
30
|
Consider that we we want to gather information from a Rails server running
|
34
31
|
locally at `http://localhost:3000`. Let's say that we know the following:
|
35
32
|
* `charles@poodles.com` exists in the database
|
@@ -41,34 +38,39 @@ the database.
|
|
41
38
|
We execute (using `-q` to suppress the progress bar)
|
42
39
|
```bash
|
43
40
|
% timing_attack -q -u http://localhost:3000/login \
|
44
|
-
|
45
|
-
|
41
|
+
candidate@address.com other@address.com \
|
42
|
+
charles@poodles.com invalid@fake.fake
|
46
43
|
```
|
47
44
|
```
|
48
|
-
|
49
|
-
|
50
|
-
|
51
|
-
|
45
|
+
Short tests:
|
46
|
+
other@address.com 0.0926
|
47
|
+
invalid@fake.fake 0.0947
|
48
|
+
Long tests:
|
49
|
+
candidate@address.com 0.1708
|
50
|
+
charles@poodles.com 0.1823
|
52
51
|
```
|
53
|
-
`candidate@address.com` is in the same group as `charles@poodles.com` (Group A),
|
54
|
-
while `other@address.com` is in Group B with `invalid@fake.fake`
|
55
|
-
Thus we know that `candidate@address.com` exists in the database, and that
|
56
|
-
`other@example.com` does not.
|
57
52
|
|
58
|
-
|
59
|
-
|
60
|
-
|
61
|
-
|
62
|
-
|
63
|
-
|
64
|
-
|
65
|
-
|
66
|
-
|
67
|
-
|
68
|
-
|
69
|
-
|
70
|
-
|
71
|
-
|
53
|
+
Note that you don't need to know anything about the database when attacking. It
|
54
|
+
is, however, nice to have a bit of information as a sanity check.
|
55
|
+
|
56
|
+
## How it works
|
57
|
+
|
58
|
+
The various inputs are each thrown at the endpoint `--number` times. The
|
59
|
+
`--percentile`th percentile of each input's results is considered the
|
60
|
+
representative result for that input. Inputs are then sorted according to
|
61
|
+
their representative results and the largest spike in their graph is found.
|
62
|
+
Results then split into short and long groups according to this spike.
|
63
|
+
|
64
|
+
The `--mean` flag uses the average of results for a particular input as its
|
65
|
+
representative result. The `--median` flag simply uses the 50th percentile.
|
66
|
+
According to [Crosby, Wallach, and
|
67
|
+
Reidi](https://www.cs.rice.edu/~dwallach/pub/crosby-timing2009.pdf), results
|
68
|
+
with percentiles above ~15, median, and mean are all quite noisy, so you should
|
69
|
+
probably keep `--percentile` low.
|
70
|
+
|
71
|
+
I was very surprised to find that I get correct results against remote targets
|
72
|
+
with `--num` around 20. Default is 5, as that has been sufficient in my tests
|
73
|
+
for LAN and local targets.
|
72
74
|
|
73
75
|
## Contributing
|
74
76
|
|
@@ -76,14 +78,14 @@ Bug reports and pull requests are welcome [here](https://github.com/ffleming/tim
|
|
76
78
|
|
77
79
|
## Disclaimer
|
78
80
|
|
79
|
-
TimingAttack is quick and dirty.
|
80
|
-
upon two known values. TimingAttack is *not* for discovering the existence of
|
81
|
-
timing-based vulnerabilities.
|
81
|
+
TimingAttack is quick and dirty.
|
82
82
|
|
83
83
|
Also, don't use TimingAttack against machines that aren't yours.
|
84
84
|
|
85
85
|
## Todo
|
86
86
|
* Tests
|
87
|
-
*
|
88
|
-
*
|
87
|
+
* More intelligent filtering than nth-percentile + spike detection
|
88
|
+
* CW&R's box test
|
89
89
|
* Customizable query parameters
|
90
|
+
* Threading for requests?
|
91
|
+
* Custom or just use Typhoeus
|
data/exe/timing_attack
CHANGED
@@ -5,15 +5,17 @@ require 'optparse'
|
|
5
5
|
options = {}
|
6
6
|
opt_parser = OptionParser.new do |opts|
|
7
7
|
opts.program_name = File.basename(__FILE__)
|
8
|
-
opts.banner = "#{opts.program_name} [options] -
|
8
|
+
opts.banner = "#{opts.program_name} [options] -u <target> <inputs>"
|
9
9
|
opts.on("-u URL", "--url URL", "URL of endpoint to profile") { |str| options[:url] = str }
|
10
|
-
opts.on("-
|
11
|
-
opts.on("-
|
12
|
-
|
13
|
-
|
14
|
-
opts.on("--b-name B_NAME", "Name of Group B") { |str| options[:b_name] = str }
|
10
|
+
opts.on("-n NUM", "--number NUM", "Requests per input (default: 0.025)") { |num| options[:iterations] = num.to_i }
|
11
|
+
opts.on("-t NUM", "--threshold NUM", "Minimum threshold, in seconds, for meaningfulness (default: 0.025)") do |num|
|
12
|
+
options[:threshold] = num.to_f
|
13
|
+
end
|
15
14
|
opts.on("-p", "--post", "Use POST, not GET") { |bool| options[:method] = bool ? :post : :get }
|
16
15
|
opts.on("-q", "--quiet", "Quiet mode (don't display progress bars)") { |bool| options[:verbose] = !bool }
|
16
|
+
opts.on("--mean", "Use mean for calculations") { |bool| options[:mean] = bool }
|
17
|
+
opts.on("--median", "Use median for calculations") { |bool| options[:median] = bool }
|
18
|
+
opts.on("--percentile N", "Use Nth percentile for calculations (default: 10)") { |num| options[:percentile] = num.to_i }
|
17
19
|
opts.on_tail("-v", "--version", "Print version information") do
|
18
20
|
gem = Gem::Specification.find_by_name('timing_attack')
|
19
21
|
puts "#{gem.name} #{gem.version}"
|
@@ -29,11 +31,18 @@ rescue OptionParser::InvalidOption => e
|
|
29
31
|
puts opt_parser
|
30
32
|
exit
|
31
33
|
end
|
34
|
+
options[:verbose] = true if options[:verbose].nil?
|
35
|
+
if options[:percentile]
|
36
|
+
options.delete(:mean)
|
37
|
+
elsif options[:median]
|
38
|
+
options[:percentile] = 50
|
39
|
+
elsif options[:mean]
|
40
|
+
options.delete(:percentile)
|
41
|
+
end
|
32
42
|
|
33
43
|
begin
|
34
|
-
atk = TimingAttack::
|
44
|
+
atk = TimingAttack::CliAttacker.new(inputs: ARGV, options: options)
|
35
45
|
atk.run!
|
36
|
-
puts atk
|
37
46
|
rescue ArgumentError => e
|
38
47
|
STDERR.puts e.message
|
39
48
|
puts opt_parser
|
@@ -0,0 +1,106 @@
|
|
1
|
+
module TimingAttack
|
2
|
+
class CliAttacker
|
3
|
+
def initialize(inputs: [], options: {})
|
4
|
+
@options = DEFAULT_OPTIONS.merge(options)
|
5
|
+
raise ArgumentError.new("url is a required argument") unless options.has_key? :url
|
6
|
+
raise ArgumentError.new("Need at least 2 inputs") if inputs.count < 2
|
7
|
+
raise ArgumentError.new("Iterations can't be < 3") if iterations < 3
|
8
|
+
unless @options.has_key? :width
|
9
|
+
@options[:width] = inputs.dup.map(&:length).push(30).sort.last
|
10
|
+
end
|
11
|
+
@attacks = inputs.map { |input| TestCase.new(input: input, options: @options) }
|
12
|
+
end
|
13
|
+
|
14
|
+
def run!
|
15
|
+
puts "Target: #{url}" if verbose?
|
16
|
+
warmup!
|
17
|
+
attack!
|
18
|
+
puts report
|
19
|
+
end
|
20
|
+
|
21
|
+
private
|
22
|
+
|
23
|
+
attr_reader :attacks, :options, :grouper
|
24
|
+
|
25
|
+
def report
|
26
|
+
ret = ''
|
27
|
+
hsh = grouper.serialize
|
28
|
+
if hsh[:spike_delta] < threshold
|
29
|
+
ret << "\n* Spike delta of #{sprintf('%.4f', hsh[:spike_delta])} is less than #{sprintf('%.4f', threshold)} * \n\n"
|
30
|
+
end
|
31
|
+
[:short, :long].each do |sym|
|
32
|
+
ret << "#{sym.to_s.capitalize} tests:\n"
|
33
|
+
hsh.fetch(sym).each do |input, time|
|
34
|
+
ret << " #{input.ljust(width)}"
|
35
|
+
ret << sprintf('%.4f', time) << "\n"
|
36
|
+
end
|
37
|
+
end
|
38
|
+
ret
|
39
|
+
end
|
40
|
+
|
41
|
+
def warmup!
|
42
|
+
2.times do
|
43
|
+
warmup = TestCase.new(input: attacks.sample.input, options: options)
|
44
|
+
warmup.test!
|
45
|
+
end
|
46
|
+
end
|
47
|
+
|
48
|
+
def attack!
|
49
|
+
iterations.times do
|
50
|
+
attacks.each do |attack|
|
51
|
+
attack.test!
|
52
|
+
attack_bar.increment
|
53
|
+
end
|
54
|
+
end
|
55
|
+
end
|
56
|
+
|
57
|
+
def grouper
|
58
|
+
return @grouper unless @grouper.nil?
|
59
|
+
group_by = if options.fetch(:mean, false)
|
60
|
+
:mean
|
61
|
+
else
|
62
|
+
{ percentile: options.fetch(:percentile) }
|
63
|
+
end
|
64
|
+
@grouper = Grouper.new(attacks: attacks, group_by: group_by)
|
65
|
+
end
|
66
|
+
|
67
|
+
def attack_bar
|
68
|
+
return null_bar unless verbose?
|
69
|
+
@attack_bar ||= ProgressBar.create(title: " Attacking".ljust(15),
|
70
|
+
total: iterations * attacks.length,
|
71
|
+
format: bar_format
|
72
|
+
)
|
73
|
+
end
|
74
|
+
|
75
|
+
def benchmark_bar
|
76
|
+
return null_bar unless verbose?
|
77
|
+
@benchmark_bar ||= ProgressBar.create(title: " Benchmarking".ljust(15),
|
78
|
+
total: iterations * 2,
|
79
|
+
format: bar_format
|
80
|
+
)
|
81
|
+
end
|
82
|
+
|
83
|
+
def bar_format
|
84
|
+
@bar_format ||= "%t (%E) |%B|"
|
85
|
+
end
|
86
|
+
|
87
|
+
def null_bar
|
88
|
+
@null_bar_klass ||= Struct.new('NullProgressBar', :increment)
|
89
|
+
@null_bar ||= @null_bar_klass.new
|
90
|
+
end
|
91
|
+
|
92
|
+
%i(iterations url verbose width method mean percentile threshold).each do |sym|
|
93
|
+
define_method(sym) { options.fetch sym }
|
94
|
+
end
|
95
|
+
alias_method :verbose?, :verbose
|
96
|
+
|
97
|
+
DEFAULT_OPTIONS = {
|
98
|
+
verbose: false,
|
99
|
+
method: :get,
|
100
|
+
iterations: 5,
|
101
|
+
mean: false,
|
102
|
+
threshold: 0.025,
|
103
|
+
percentile: 10
|
104
|
+
}.freeze
|
105
|
+
end
|
106
|
+
end
|
@@ -0,0 +1,99 @@
|
|
1
|
+
module TimingAttack
|
2
|
+
class Grouper
|
3
|
+
attr_reader :short_tests, :long_tests
|
4
|
+
def initialize(attacks: , group_by: {})
|
5
|
+
@attacks = attacks
|
6
|
+
setup_grouping_opts!(group_by)
|
7
|
+
@short_tests = []
|
8
|
+
@long_tests = []
|
9
|
+
group_attacks
|
10
|
+
serialize
|
11
|
+
freeze
|
12
|
+
end
|
13
|
+
|
14
|
+
def serialize
|
15
|
+
@serialize ||= {}.tap do |h|
|
16
|
+
h[:attack_method] = test_method
|
17
|
+
h[:attack_args] = test_args
|
18
|
+
h[:short] = serialize_tests(short_tests)
|
19
|
+
h[:long] = serialize_tests(long_tests)
|
20
|
+
h[:spike_delta] = spike_delta
|
21
|
+
end
|
22
|
+
end
|
23
|
+
|
24
|
+
private
|
25
|
+
|
26
|
+
ALLOWED_TEST_SYMBOLS = %i(mean median percentile).freeze
|
27
|
+
|
28
|
+
attr_reader :test_method, :test_args, :attacks, :test_hash, :spike_delta
|
29
|
+
|
30
|
+
def setup_grouping_opts!(group_by)
|
31
|
+
case group_by
|
32
|
+
when Symbol
|
33
|
+
setup_symbol_opts!(group_by)
|
34
|
+
when Hash
|
35
|
+
setup_hash_opts!(group_by)
|
36
|
+
else
|
37
|
+
raise ArgumentError.new("Don't know what to do with #{group_by.class} #{group_by}")
|
38
|
+
end
|
39
|
+
end
|
40
|
+
|
41
|
+
def setup_symbol_opts!(symbol)
|
42
|
+
case symbol
|
43
|
+
when :mean
|
44
|
+
@test_method = :mean
|
45
|
+
@test_args = []
|
46
|
+
when :median
|
47
|
+
@test_method = :percentile
|
48
|
+
@test_args = [50]
|
49
|
+
when :percentile
|
50
|
+
@test_method = :percentile
|
51
|
+
@test_args = [10]
|
52
|
+
else
|
53
|
+
raise ArgumentError.new("Allowed symbols are #{ALLOWED_TEST_SYMBOLS.join(', ')}")
|
54
|
+
end
|
55
|
+
end
|
56
|
+
|
57
|
+
def setup_hash_opts!(hash)
|
58
|
+
raise ArgumentError.new("Must provide configuration to Grouper") if hash.empty?
|
59
|
+
key, value = hash.first
|
60
|
+
unless ALLOWED_TEST_SYMBOLS.include? key
|
61
|
+
raise ArgumentError.new("Allowed keys are #{ALLOWED_TEST_SYMBOLS.join(', ')}")
|
62
|
+
end
|
63
|
+
@test_method = key
|
64
|
+
@test_args = value.is_a?(Array) ? value : [value]
|
65
|
+
end
|
66
|
+
|
67
|
+
def value_from_test(test)
|
68
|
+
test.public_send(test_method, *test_args)
|
69
|
+
end
|
70
|
+
|
71
|
+
def serialize_tests(test_arr)
|
72
|
+
test_arr.each_with_object({}) do |test, ret|
|
73
|
+
ret[test.input] = value_from_test(test)
|
74
|
+
end
|
75
|
+
end
|
76
|
+
|
77
|
+
def group_attacks
|
78
|
+
spike = decorated_attacks.max { |a,b| a[:delta] <=> b[:delta] }
|
79
|
+
index = decorated_attacks.index(spike)
|
80
|
+
stripped = decorated_attacks.map {|a| a[:attack] }
|
81
|
+
@short_tests = stripped[0..(index-1)]
|
82
|
+
@long_tests = stripped[index..-1]
|
83
|
+
@spike_delta = spike[:delta]
|
84
|
+
end
|
85
|
+
|
86
|
+
def decorated_attacks
|
87
|
+
return @decorated_attacks unless @decorated_attacks.nil?
|
88
|
+
sorted = attacks.sort { |a,b| value_from_test(a) <=> value_from_test(b) }
|
89
|
+
@decorated_attacks = sorted.each_with_object([]).with_index do |(attack, memo), index|
|
90
|
+
delta = if index == 0
|
91
|
+
0.0
|
92
|
+
else
|
93
|
+
value_from_test(attack) - value_from_test(sorted[index-1])
|
94
|
+
end
|
95
|
+
memo << { attack: attack, delta: delta }
|
96
|
+
end
|
97
|
+
end
|
98
|
+
end
|
99
|
+
end
|
@@ -5,6 +5,7 @@ module TimingAttack
|
|
5
5
|
@input = input
|
6
6
|
@options = options
|
7
7
|
@times = []
|
8
|
+
@percentiles = []
|
8
9
|
end
|
9
10
|
|
10
11
|
def test!
|
@@ -21,36 +22,22 @@ module TimingAttack
|
|
21
22
|
times.push(diff)
|
22
23
|
end
|
23
24
|
|
24
|
-
def
|
25
|
-
|
25
|
+
def mean
|
26
|
+
times.reduce(:+) / times.size.to_f
|
26
27
|
end
|
27
28
|
|
28
|
-
def
|
29
|
-
|
30
|
-
|
29
|
+
def percentile(n)
|
30
|
+
raise ArgumentError.new("Can't have a percentile > 100") if n > 100
|
31
|
+
if percentiles[n].nil?
|
32
|
+
position = ((times.length - 1) * (n/100.0)).to_i
|
33
|
+
percentiles[n] = times.sort[position]
|
34
|
+
else
|
35
|
+
percentiles[n]
|
31
36
|
end
|
32
|
-
d_a = (mean_time - a_test.mean_time).abs
|
33
|
-
d_b = (mean_time - b_test.mean_time).abs
|
34
|
-
@group_a = (d_a < d_b)
|
35
|
-
end
|
36
|
-
|
37
|
-
def group_a
|
38
|
-
raise ArgumentError.new("Have not yet determined group membership") if @group_a.nil?
|
39
|
-
@group_a
|
40
|
-
end
|
41
|
-
alias_method :group_a?, :group_a
|
42
|
-
|
43
|
-
def group_b
|
44
|
-
!group_a
|
45
|
-
end
|
46
|
-
alias_method :group_b?, :group_b
|
47
|
-
|
48
|
-
def mean_time
|
49
|
-
times.reduce(:+) / times.size.to_f
|
50
37
|
end
|
51
38
|
|
52
39
|
private
|
53
40
|
|
54
|
-
attr_reader :times, :options
|
41
|
+
attr_reader :times, :options, :percentiles
|
55
42
|
end
|
56
43
|
end
|
data/lib/timing_attack.rb
CHANGED
data/timing_attack.gemspec
CHANGED
@@ -9,7 +9,7 @@ Gem::Specification.new do |spec|
|
|
9
9
|
spec.authors = ["Forrest Fleming"]
|
10
10
|
spec.email = ["ffleming@gmail.com"]
|
11
11
|
|
12
|
-
spec.summary = "
|
12
|
+
spec.summary = "Perform timing attacks against web applications"
|
13
13
|
spec.description = "Profile web applications by noting differences in response times based on input values"
|
14
14
|
spec.homepage = "https://www.github.com/ffleming/timing_attack"
|
15
15
|
|
metadata
CHANGED
@@ -1,14 +1,14 @@
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
2
2
|
name: timing_attack
|
3
3
|
version: !ruby/object:Gem::Version
|
4
|
-
version: 0.1
|
4
|
+
version: 0.2.1
|
5
5
|
platform: ruby
|
6
6
|
authors:
|
7
7
|
- Forrest Fleming
|
8
8
|
autorequire:
|
9
9
|
bindir: exe
|
10
10
|
cert_chain: []
|
11
|
-
date: 2016-07-
|
11
|
+
date: 2016-07-24 00:00:00.000000000 Z
|
12
12
|
dependencies:
|
13
13
|
- !ruby/object:Gem::Dependency
|
14
14
|
name: ruby-progressbar
|
@@ -99,7 +99,8 @@ files:
|
|
99
99
|
- bin/setup
|
100
100
|
- exe/timing_attack
|
101
101
|
- lib/timing_attack.rb
|
102
|
-
- lib/timing_attack/
|
102
|
+
- lib/timing_attack/cli_attacker.rb
|
103
|
+
- lib/timing_attack/grouper.rb
|
103
104
|
- lib/timing_attack/test_case.rb
|
104
105
|
- lib/timing_attack/version.rb
|
105
106
|
- timing_attack.gemspec
|
@@ -126,5 +127,5 @@ rubyforge_project:
|
|
126
127
|
rubygems_version: 2.4.8
|
127
128
|
signing_key:
|
128
129
|
specification_version: 4
|
129
|
-
summary:
|
130
|
+
summary: Perform timing attacks against web applications
|
130
131
|
test_files: []
|
@@ -1,130 +0,0 @@
|
|
1
|
-
module TimingAttack
|
2
|
-
class Attacker
|
3
|
-
def initialize(inputs: [], options: {})
|
4
|
-
@options = DEFAULT_OPTIONS.merge(options)
|
5
|
-
unless @options.has_key? :width
|
6
|
-
@options[:width] = [a_name, b_name, *inputs].map(&:length).push(30).sort.last
|
7
|
-
end
|
8
|
-
%i(a_example b_example url).each do |arg|
|
9
|
-
raise ArgumentError.new("#{arg} is a required argument") unless options.has_key? arg
|
10
|
-
end
|
11
|
-
@attacks = inputs.map { |input| TestCase.new(input: input, options: @options) }
|
12
|
-
end
|
13
|
-
|
14
|
-
def run!
|
15
|
-
puts "Target: #{url}" if verbose?
|
16
|
-
warmup!
|
17
|
-
benchmark!
|
18
|
-
attack!
|
19
|
-
end
|
20
|
-
|
21
|
-
def to_s
|
22
|
-
ret = ""
|
23
|
-
if verbose?
|
24
|
-
ret << "Benchmark results\n"
|
25
|
-
ret << " #{a_name.ljust(width)}~#{sprintf('%.4f', a_benchmark.mean_time,)}s\n"
|
26
|
-
ret << " #{b_name.ljust(width)}~#{sprintf('%.4f', b_benchmark.mean_time)}s\n"
|
27
|
-
end
|
28
|
-
ret << attack_string
|
29
|
-
end
|
30
|
-
|
31
|
-
private
|
32
|
-
|
33
|
-
attr_reader :attacks, :options
|
34
|
-
|
35
|
-
def warmup!
|
36
|
-
@warmup_test ||= TestCase.new(input: a_example, options: options)
|
37
|
-
2.times { @warmup_test.test! }
|
38
|
-
end
|
39
|
-
|
40
|
-
def benchmark!
|
41
|
-
iterations.times do
|
42
|
-
[a_benchmark, b_benchmark].each do |test_case|
|
43
|
-
test_case.test!
|
44
|
-
benchmark_bar.increment
|
45
|
-
end
|
46
|
-
end
|
47
|
-
end
|
48
|
-
|
49
|
-
def attack!
|
50
|
-
iterations.times do
|
51
|
-
attacks.each do |attack|
|
52
|
-
attack.test!
|
53
|
-
attack_bar.increment
|
54
|
-
end
|
55
|
-
end
|
56
|
-
attacks.each { |attack| attack.derive_group_from(a_test: a_benchmark, b_test: b_benchmark) }
|
57
|
-
end
|
58
|
-
|
59
|
-
def a_benchmark
|
60
|
-
@a_benchmark ||= TestCase.new(input: a_example, options: options)
|
61
|
-
end
|
62
|
-
|
63
|
-
def b_benchmark
|
64
|
-
@b_benchmark ||= TestCase.new(input: b_example, options: options)
|
65
|
-
end
|
66
|
-
|
67
|
-
def indent(str)
|
68
|
-
" #{str.ljust(width)}"
|
69
|
-
end
|
70
|
-
|
71
|
-
def a_attacks
|
72
|
-
attacks.select { |a| a.group_a? }
|
73
|
-
end
|
74
|
-
|
75
|
-
def b_attacks
|
76
|
-
attacks.select { |a| a.group_b? }
|
77
|
-
end
|
78
|
-
|
79
|
-
def attack_string
|
80
|
-
ret = ""
|
81
|
-
unless a_attacks.empty?
|
82
|
-
ret << "#{a_name}:\n"
|
83
|
-
ret << a_attacks.map {|a| indent(a.to_s)}.join("\n")
|
84
|
-
end
|
85
|
-
unless b_attacks.empty?
|
86
|
-
ret << "\n#{b_name}:\n"
|
87
|
-
ret << b_attacks.map {|a| indent(a.to_s)}.join("\n")
|
88
|
-
end
|
89
|
-
"#{ret}\n"
|
90
|
-
end
|
91
|
-
|
92
|
-
def attack_bar
|
93
|
-
return null_bar unless verbose?
|
94
|
-
@attack_bar ||= ProgressBar.create(title: " Attacking".ljust(15),
|
95
|
-
total: iterations * attacks.length,
|
96
|
-
format: bar_format
|
97
|
-
)
|
98
|
-
end
|
99
|
-
|
100
|
-
def benchmark_bar
|
101
|
-
return null_bar unless verbose?
|
102
|
-
@benchmark_bar ||= ProgressBar.create(title: " Benchmarking".ljust(15),
|
103
|
-
total: iterations * 2,
|
104
|
-
format: bar_format
|
105
|
-
)
|
106
|
-
end
|
107
|
-
|
108
|
-
def bar_format
|
109
|
-
@bar_format ||= "%t (%E) |%B|"
|
110
|
-
end
|
111
|
-
|
112
|
-
def null_bar
|
113
|
-
@null_bar_klass ||= Struct.new('NullProgressBar', :increment)
|
114
|
-
@null_bar ||= @null_bar_klass.new
|
115
|
-
end
|
116
|
-
|
117
|
-
%i(iterations url verbose a_name b_name a_example b_example width method).each do |sym|
|
118
|
-
define_method(sym) { options.fetch sym }
|
119
|
-
end
|
120
|
-
alias_method :verbose?, :verbose
|
121
|
-
|
122
|
-
DEFAULT_OPTIONS = {
|
123
|
-
verbose: true,
|
124
|
-
a_name: "Group A",
|
125
|
-
b_name: "Group B",
|
126
|
-
method: :get,
|
127
|
-
iterations: 5
|
128
|
-
}.freeze
|
129
|
-
end
|
130
|
-
end
|