aquatone 0.1.0
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +7 -0
- data/.gitignore +12 -0
- data/CHANGELOG.md +18 -0
- data/Gemfile +4 -0
- data/LICENSE.txt +21 -0
- data/README.md +168 -0
- data/Rakefile +10 -0
- data/aquatone.gemspec +29 -0
- data/aquatone.js +164 -0
- data/bin/console +14 -0
- data/bin/setup +8 -0
- data/exe/aquatone-discover +129 -0
- data/exe/aquatone-gather +55 -0
- data/exe/aquatone-scan +76 -0
- data/lib/aquatone.rb +43 -0
- data/lib/aquatone/assessment.rb +40 -0
- data/lib/aquatone/browser.rb +18 -0
- data/lib/aquatone/browser/drivers/nightmare.rb +52 -0
- data/lib/aquatone/collector.rb +106 -0
- data/lib/aquatone/collectors/dictionary.rb +20 -0
- data/lib/aquatone/collectors/dnsdb.rb +45 -0
- data/lib/aquatone/collectors/gtr.rb +58 -0
- data/lib/aquatone/collectors/hackertarget.rb +24 -0
- data/lib/aquatone/collectors/netcraft.rb +48 -0
- data/lib/aquatone/collectors/shodan.rb +45 -0
- data/lib/aquatone/collectors/threatcrowd.rb +25 -0
- data/lib/aquatone/collectors/virustotal.rb +24 -0
- data/lib/aquatone/command.rb +152 -0
- data/lib/aquatone/commands/discover.rb +187 -0
- data/lib/aquatone/commands/gather.rb +167 -0
- data/lib/aquatone/commands/scan.rb +108 -0
- data/lib/aquatone/domain.rb +33 -0
- data/lib/aquatone/http_client.rb +5 -0
- data/lib/aquatone/key_store.rb +72 -0
- data/lib/aquatone/port_lists.rb +36 -0
- data/lib/aquatone/report.rb +88 -0
- data/lib/aquatone/resolver.rb +47 -0
- data/lib/aquatone/thread_pool.rb +31 -0
- data/lib/aquatone/url_maker.rb +27 -0
- data/lib/aquatone/validation.rb +22 -0
- data/lib/aquatone/version.rb +3 -0
- data/subdomains.lst +8214 -0
- data/templates/default.html.erb +225 -0
- metadata +159 -0
@@ -0,0 +1,106 @@
|
|
1
|
+
module Aquatone
|
2
|
+
class Collector
|
3
|
+
class Error < StandardError; end
|
4
|
+
class InvalidMetadataError < Error; end
|
5
|
+
class MetadataNotSetError < Error; end
|
6
|
+
class MissingKeyRequirement < Error; end
|
7
|
+
|
8
|
+
attr_reader :domain, :hosts
|
9
|
+
|
10
|
+
DEFAULT_PRIORITY = 1
|
11
|
+
|
12
|
+
def self.meta
|
13
|
+
@meta || fail(MetadataNotSetError, "Metadata has not been set")
|
14
|
+
end
|
15
|
+
|
16
|
+
def self.meta=(meta)
|
17
|
+
validate_metadata(meta)
|
18
|
+
@meta = meta
|
19
|
+
end
|
20
|
+
|
21
|
+
def self.descendants
|
22
|
+
collectors = ObjectSpace.each_object(Class).select { |klass| klass < self }
|
23
|
+
collectors.sort { |x, y| x.priority <=> y.priority }
|
24
|
+
end
|
25
|
+
|
26
|
+
def self.sluggified_name
|
27
|
+
return meta[:slug].downcase if meta[:slug]
|
28
|
+
meta[:name].strip.downcase.gsub(/[^a-z0-9]+/, '-').gsub("--", "-")
|
29
|
+
end
|
30
|
+
|
31
|
+
def initialize(domain)
|
32
|
+
check_key_requirements!
|
33
|
+
@domain = domain
|
34
|
+
@hosts = []
|
35
|
+
end
|
36
|
+
|
37
|
+
def run
|
38
|
+
fail NotImplementedError
|
39
|
+
end
|
40
|
+
|
41
|
+
def execute!
|
42
|
+
run
|
43
|
+
hosts
|
44
|
+
end
|
45
|
+
|
46
|
+
def self.priority
|
47
|
+
meta[:priority] || DEFAULT_PRIORITY
|
48
|
+
end
|
49
|
+
|
50
|
+
protected
|
51
|
+
|
52
|
+
def add_host(host)
|
53
|
+
host.downcase!
|
54
|
+
return unless Aquatone::Validation.valid_domain_name?(host)
|
55
|
+
@hosts << host unless @hosts.include?(host)
|
56
|
+
end
|
57
|
+
|
58
|
+
def get_request(uri, options={})
|
59
|
+
Aquatone::HttpClient.get(uri)
|
60
|
+
end
|
61
|
+
|
62
|
+
def post_request(uri, body=nil, options={})
|
63
|
+
options = {
|
64
|
+
:body => body
|
65
|
+
}.merge(options)
|
66
|
+
Aquatone::HttpClient.post(uri, options)
|
67
|
+
end
|
68
|
+
|
69
|
+
def url_escape(string)
|
70
|
+
CGI.escape(string)
|
71
|
+
end
|
72
|
+
|
73
|
+
def random_sleep(seconds)
|
74
|
+
random_sleep = ((1 - (rand(30) * 0.01)) * seconds.to_i)
|
75
|
+
sleep(random_sleep)
|
76
|
+
end
|
77
|
+
|
78
|
+
def get_key(name)
|
79
|
+
Aquatone::KeyStore.get(name)
|
80
|
+
end
|
81
|
+
|
82
|
+
def has_key?(name)
|
83
|
+
Aquatone::KeyStore.key?(name)
|
84
|
+
end
|
85
|
+
|
86
|
+
def failure(message)
|
87
|
+
fail Error, message
|
88
|
+
end
|
89
|
+
|
90
|
+
def check_key_requirements!
|
91
|
+
return unless self.class.meta[:require_keys]
|
92
|
+
keys = self.class.meta[:require_keys]
|
93
|
+
keys.each do |key|
|
94
|
+
fail MissingKeyRequirement, "Key '#{key}' has not been set" unless has_key?(key)
|
95
|
+
end
|
96
|
+
end
|
97
|
+
|
98
|
+
def self.validate_metadata(meta)
|
99
|
+
fail InvalidMetadataError, "Metadata is not a hash" unless meta.is_a?(Hash)
|
100
|
+
fail InvalidMetadataError, "Metadata is empty" if meta.empty?
|
101
|
+
fail InvalidMetadataError, "Metadata is missing key: name" unless meta.key?(:name)
|
102
|
+
fail InvalidMetadataError, "Metadata is missing key: author" unless meta.key?(:author)
|
103
|
+
fail InvalidMetadataError, "Metadata is missing key: description" unless meta.key?(:description)
|
104
|
+
end
|
105
|
+
end
|
106
|
+
end
|
@@ -0,0 +1,20 @@
|
|
1
|
+
module Aquatone
|
2
|
+
module Collectors
|
3
|
+
class Dictionary < Aquatone::Collector
|
4
|
+
self.meta = {
|
5
|
+
:name => "Dictionary",
|
6
|
+
:author => "Michael Henriksen (@michenriksen)",
|
7
|
+
:description => "Uses a dictionary to find hostnames"
|
8
|
+
}
|
9
|
+
|
10
|
+
DICTIONARY = File.join(Aquatone::AQUATONE_ROOT, "subdomains.lst").freeze
|
11
|
+
|
12
|
+
def run
|
13
|
+
dictionary = File.open(DICTIONARY, "r")
|
14
|
+
dictionary.each_line do |subdomain|
|
15
|
+
add_host("#{subdomain.strip}.#{domain.name}")
|
16
|
+
end
|
17
|
+
end
|
18
|
+
end
|
19
|
+
end
|
20
|
+
end
|
@@ -0,0 +1,45 @@
|
|
1
|
+
module Aquatone
|
2
|
+
module Collectors
|
3
|
+
class Dnsdb < Aquatone::Collector
|
4
|
+
self.meta = {
|
5
|
+
:name => "DNSDB",
|
6
|
+
:author => "Michael Henriksen (@michenriksen)",
|
7
|
+
:description => "Uses dnsdb.org to find hostnames"
|
8
|
+
}
|
9
|
+
|
10
|
+
LINK_REGEX = /<a href="(.*?)\">(.*?)<\/a>/.freeze
|
11
|
+
INDEX_REGEX = /<a href="([0-9a-z]{1})">[0-9a-z]{1}<\/a>/.freeze
|
12
|
+
|
13
|
+
def run
|
14
|
+
@base_url = "http://www.dnsdb.org/f/#{url_escape(domain.name)}.dnsdb.org/"
|
15
|
+
parse_page(@base_url)
|
16
|
+
end
|
17
|
+
|
18
|
+
private
|
19
|
+
|
20
|
+
def parse_page(url)
|
21
|
+
response = get_request(url)
|
22
|
+
if response.code != 200
|
23
|
+
failure("DNSDB returned unexpected response code: #{response.code}")
|
24
|
+
end
|
25
|
+
|
26
|
+
if response.body.include?("index for")
|
27
|
+
response.body.scan(INDEX_REGEX) do |index|
|
28
|
+
response = get_request("#{@base_url}#{url_escape(index.first)}")
|
29
|
+
extract_hosts(response.body)
|
30
|
+
end
|
31
|
+
else
|
32
|
+
extract_hosts(response.body)
|
33
|
+
end
|
34
|
+
end
|
35
|
+
|
36
|
+
def extract_hosts(body)
|
37
|
+
body.scan(LINK_REGEX) do |href, hostname|
|
38
|
+
if hostname.end_with?(".#{domain.name}")
|
39
|
+
add_host(hostname)
|
40
|
+
end
|
41
|
+
end
|
42
|
+
end
|
43
|
+
end
|
44
|
+
end
|
45
|
+
end
|
@@ -0,0 +1,58 @@
|
|
1
|
+
module Aquatone
|
2
|
+
module Collectors
|
3
|
+
class Gtr < Aquatone::Collector
|
4
|
+
self.meta = {
|
5
|
+
:name => "Google Transparency Report",
|
6
|
+
:author => "Michael Henriksen (@michenriksen)",
|
7
|
+
:description => "Uses Google Transparency Report to find hostnames",
|
8
|
+
:slug => "gtr"
|
9
|
+
}
|
10
|
+
|
11
|
+
BASE_URI = "https://www.google.com/transparencyreport/jsonp/ct/search"
|
12
|
+
PAGES_TO_PROCESS = 30.freeze
|
13
|
+
|
14
|
+
def run
|
15
|
+
token = nil
|
16
|
+
PAGES_TO_PROCESS.times do
|
17
|
+
response = parse_response(request_page(token))
|
18
|
+
response["results"].each do |result|
|
19
|
+
host = result["subject"]
|
20
|
+
add_host(host) if valid_host?(host)
|
21
|
+
end
|
22
|
+
break if !response.key?("nextPageToken")
|
23
|
+
token = response["nextPageToken"]
|
24
|
+
end
|
25
|
+
end
|
26
|
+
|
27
|
+
private
|
28
|
+
|
29
|
+
def request_page(token = nil)
|
30
|
+
if token.nil?
|
31
|
+
uri = "#{BASE_URI}?domain=#{url_escape(domain.name)}&incl_exp=true&incl_sub=true&c=_callbacks_._#{random_jsonp_callback}"
|
32
|
+
else
|
33
|
+
uri = "#{BASE_URI}?domain=#{url_escape(domain.name)}&incl_exp=true&incl_sub=true&token=#{url_escape(token)}&c=_callbacks_._#{random_jsonp_callback}"
|
34
|
+
end
|
35
|
+
|
36
|
+
get_request(uri,
|
37
|
+
{ :headers => { "Referer" => "https://www.google.com/transparencyreport/https/ct/?hl=en-US" } }
|
38
|
+
)
|
39
|
+
end
|
40
|
+
|
41
|
+
def random_jsonp_callback
|
42
|
+
"abcdefghijklmnopqrstuvwxyz0123456789".split("").sample(9).join
|
43
|
+
end
|
44
|
+
|
45
|
+
def parse_response(body)
|
46
|
+
body = body.split("(", 2).last
|
47
|
+
body.gsub!(");", "")
|
48
|
+
JSON.parse(body)
|
49
|
+
end
|
50
|
+
|
51
|
+
def valid_host?(host)
|
52
|
+
return false if host.start_with?("*.")
|
53
|
+
return false unless host.end_with?(".#{domain.name}")
|
54
|
+
true
|
55
|
+
end
|
56
|
+
end
|
57
|
+
end
|
58
|
+
end
|
@@ -0,0 +1,24 @@
|
|
1
|
+
module Aquatone
|
2
|
+
module Collectors
|
3
|
+
class Hackertarget < Aquatone::Collector
|
4
|
+
self.meta = {
|
5
|
+
:name => "HackerTarget",
|
6
|
+
:author => "Michael Henriksen (@michenriksen)",
|
7
|
+
:description => "Uses hackertarget.com to find hostnames"
|
8
|
+
}
|
9
|
+
|
10
|
+
API_BASE_URI = "https://api.hackertarget.com"
|
11
|
+
|
12
|
+
def run
|
13
|
+
response = get_request("#{API_BASE_URI}/hostsearch/?q=#{url_escape(domain.name)}")
|
14
|
+
if response.code != 200
|
15
|
+
failure("HackerTarget API returned unexpected response code: #{response.code}")
|
16
|
+
end
|
17
|
+
response.body.each_line do |line|
|
18
|
+
host = line.split(",", 2).first.strip
|
19
|
+
add_host(host) unless host.empty?
|
20
|
+
end
|
21
|
+
end
|
22
|
+
end
|
23
|
+
end
|
24
|
+
end
|
@@ -0,0 +1,48 @@
|
|
1
|
+
module Aquatone
|
2
|
+
module Collectors
|
3
|
+
class Netcraft < Aquatone::Collector
|
4
|
+
self.meta = {
|
5
|
+
:name => "Netcraft",
|
6
|
+
:author => "Michael Henriksen (@michenriksen)",
|
7
|
+
:description => "Uses searchdns.netcraft.com to find hostnames"
|
8
|
+
}
|
9
|
+
|
10
|
+
BASE_URI = "http://searchdns.netcraft.com/".freeze
|
11
|
+
HOSTNAME_REGEX = /<a href="http:\/\/(.*?)\/" rel="nofollow">/.freeze
|
12
|
+
RESULTS_PER_PAGE = 20.freeze
|
13
|
+
PAGES_TO_PROCESS = 10.freeze
|
14
|
+
|
15
|
+
def run
|
16
|
+
last = nil
|
17
|
+
count = 0
|
18
|
+
PAGES_TO_PROCESS.times do |i|
|
19
|
+
page = i + 1
|
20
|
+
if page == 1
|
21
|
+
uri = "#{BASE_URI}/?restriction=site+contains&host=*.#{url_escape(domain.name)}&lookup=wait..&position=limited"
|
22
|
+
else
|
23
|
+
uri = "#{BASE_URI}/?host=*.#{url_escape(domain.name)}&last=#{url_escape(last)}&from=#{count + 1}&restriction=site%20contains&position=limited"
|
24
|
+
end
|
25
|
+
response = get_request(uri,
|
26
|
+
{ :headers => { "Referer" => "http://searchdns.netcraft.com/" } }
|
27
|
+
)
|
28
|
+
hosts = extract_hostnames_from_response(response.body)
|
29
|
+
last = hosts.last
|
30
|
+
count += hosts.count
|
31
|
+
hosts.each { |host| add_host(host) }
|
32
|
+
break if hosts.count != RESULTS_PER_PAGE
|
33
|
+
random_sleep(5)
|
34
|
+
end
|
35
|
+
end
|
36
|
+
|
37
|
+
private
|
38
|
+
|
39
|
+
def extract_hostnames_from_response(body)
|
40
|
+
hosts = []
|
41
|
+
body.scan(HOSTNAME_REGEX).each do |match|
|
42
|
+
hosts << match.last.to_s.strip.downcase
|
43
|
+
end
|
44
|
+
hosts
|
45
|
+
end
|
46
|
+
end
|
47
|
+
end
|
48
|
+
end
|
@@ -0,0 +1,45 @@
|
|
1
|
+
module Aquatone
|
2
|
+
module Collectors
|
3
|
+
class Shodan < Aquatone::Collector
|
4
|
+
self.meta = {
|
5
|
+
:name => "Shodan",
|
6
|
+
:author => "Michael Henriksen (@michenriksen)",
|
7
|
+
:description => "Uses the Shodan API to find hostnames",
|
8
|
+
:require_keys => ["shodan"]
|
9
|
+
}
|
10
|
+
|
11
|
+
API_BASE_URI = "https://api.shodan.io/shodan".freeze
|
12
|
+
API_RESULTS_PER_PAGE = 100.freeze
|
13
|
+
PAGES_TO_PROCESS = 10.freeze
|
14
|
+
|
15
|
+
def run
|
16
|
+
request_shodan_page
|
17
|
+
end
|
18
|
+
|
19
|
+
private
|
20
|
+
|
21
|
+
def request_shodan_page(page=1)
|
22
|
+
response = get_request(construct_uri("hostname:#{domain.name}", page))
|
23
|
+
if response.code != 200
|
24
|
+
failure(response.parsed_response["error"] || "Shodan API returned unexpected response code: #{response.code}")
|
25
|
+
end
|
26
|
+
return unless response.parsed_response["matches"]
|
27
|
+
response.parsed_response["matches"].each do |match|
|
28
|
+
next unless match["hostnames"]
|
29
|
+
match["hostnames"].each do |hostname|
|
30
|
+
add_host(hostname) if hostname.end_with?(".#{domain.name}")
|
31
|
+
end
|
32
|
+
end
|
33
|
+
request_shodan_page(page + 1) if next_page?(page, response.parsed_response)
|
34
|
+
end
|
35
|
+
|
36
|
+
def construct_uri(query, page)
|
37
|
+
"#{API_BASE_URI}/host/search?query=#{url_escape(query)}&page=#{page}&key=#{get_key('shodan')}"
|
38
|
+
end
|
39
|
+
|
40
|
+
def next_page?(page, body)
|
41
|
+
page <= PAGES_TO_PROCESS && body["total"] && API_RESULTS_PER_PAGE * page < body["total"].to_i
|
42
|
+
end
|
43
|
+
end
|
44
|
+
end
|
45
|
+
end
|
@@ -0,0 +1,25 @@
|
|
1
|
+
module Aquatone
|
2
|
+
module Collectors
|
3
|
+
class Threatcrowd < Aquatone::Collector
|
4
|
+
self.meta = {
|
5
|
+
:name => "Threat Crowd",
|
6
|
+
:author => "Michael Henriksen (@michenriksen)",
|
7
|
+
:description => "Uses threadcrowd.org API to find hostnames",
|
8
|
+
:slug => "threatcrowd"
|
9
|
+
}
|
10
|
+
|
11
|
+
API_URI = "https://www.threatcrowd.org/searchApi/v2/domain/report/".freeze
|
12
|
+
|
13
|
+
def run
|
14
|
+
response = get_request("#{API_URI}?domain=#{url_escape(domain.name)}")
|
15
|
+
if response.code != 200
|
16
|
+
failure("Threat Crowd API returned unexpected status code: #{response.code}")
|
17
|
+
end
|
18
|
+
body = JSON.parse(response.body)
|
19
|
+
if body.key?("subdomains")
|
20
|
+
body["subdomains"].each { |host| add_host(host) }
|
21
|
+
end
|
22
|
+
end
|
23
|
+
end
|
24
|
+
end
|
25
|
+
end
|
@@ -0,0 +1,24 @@
|
|
1
|
+
module Aquatone
|
2
|
+
module Collectors
|
3
|
+
class Virustotal < Aquatone::Collector
|
4
|
+
self.meta = {
|
5
|
+
:name => "VirusTotal",
|
6
|
+
:author => "Michael Henriksen (@michenriksen)",
|
7
|
+
:description => "Uses virustotal.com domain search to find hostnames",
|
8
|
+
:require_keys => ["virustotal"]
|
9
|
+
}
|
10
|
+
|
11
|
+
API_URI = "http://www.virustotal.com/vtapi/v2/domain/report".freeze
|
12
|
+
|
13
|
+
def run
|
14
|
+
response = get_request("#{API_URI}?domain=#{url_escape(domain.name)}&apikey=#{get_key('virustotal')}")
|
15
|
+
if response.code != 200
|
16
|
+
failure("VirusTotal API returned unexpected status code: #{response.code}")
|
17
|
+
end
|
18
|
+
if response.parsed_response.key?("subdomains")
|
19
|
+
response.parsed_response["subdomains"].each { |host| add_host(host) }
|
20
|
+
end
|
21
|
+
end
|
22
|
+
end
|
23
|
+
end
|
24
|
+
end
|
@@ -0,0 +1,152 @@
|
|
1
|
+
module Aquatone
|
2
|
+
class Command
|
3
|
+
attr_reader :options
|
4
|
+
|
5
|
+
def initialize(options)
|
6
|
+
@options = options
|
7
|
+
end
|
8
|
+
|
9
|
+
def self.run(options)
|
10
|
+
self.new(options).execute!
|
11
|
+
rescue Interrupt
|
12
|
+
output("Caught interrupt; exiting.\n", true)
|
13
|
+
rescue => e
|
14
|
+
output(red("An unexpected error occurred: #{e.class}: #{e.message}\n"))
|
15
|
+
output("#{e.backtrace.join("\n")}\n")
|
16
|
+
end
|
17
|
+
|
18
|
+
def execute!
|
19
|
+
fail NotImplementedError, "Commands must overwrite #execute! method"
|
20
|
+
end
|
21
|
+
|
22
|
+
protected
|
23
|
+
|
24
|
+
def output(text, clear_line = false)
|
25
|
+
self.class.output(text, clear_line)
|
26
|
+
end
|
27
|
+
|
28
|
+
def status(message)
|
29
|
+
self.class.status(message)
|
30
|
+
end
|
31
|
+
|
32
|
+
def self.output(text, clear_line = false)
|
33
|
+
if clear_line
|
34
|
+
text = "\r\e[0K#{text}"
|
35
|
+
end
|
36
|
+
with_output_mutex { print text }
|
37
|
+
end
|
38
|
+
|
39
|
+
def self.status(message)
|
40
|
+
output(message, true)
|
41
|
+
end
|
42
|
+
|
43
|
+
def self.colorize(text, color_code)
|
44
|
+
"\e[1m\e[#{color_code}m#{text}\e[0m"
|
45
|
+
end
|
46
|
+
|
47
|
+
def uncolorize(text)
|
48
|
+
self.class.uncolorize(text)
|
49
|
+
end
|
50
|
+
|
51
|
+
def red(text)
|
52
|
+
self.class.red(text)
|
53
|
+
end
|
54
|
+
|
55
|
+
def green(text)
|
56
|
+
self.class.green(text)
|
57
|
+
end
|
58
|
+
|
59
|
+
def blue(text)
|
60
|
+
self.class.blue(text)
|
61
|
+
end
|
62
|
+
|
63
|
+
def yellow(text)
|
64
|
+
self.class.yellow(text)
|
65
|
+
end
|
66
|
+
|
67
|
+
def bold(text)
|
68
|
+
self.class.bold(text)
|
69
|
+
end
|
70
|
+
|
71
|
+
def banner(subtitle)
|
72
|
+
output(" __\n")
|
73
|
+
output(" ____ _____ ___ ______ _/ /_____ ____ ___\n")
|
74
|
+
output(" / __ `/ __ `/ / / / __ `/ __/ __ \\/ __ \\/ _ \\\n")
|
75
|
+
output("/ /_/ / /_/ / /_/ / /_/ / /_/ /_/ / / / / __/\n")
|
76
|
+
output("\\__,_/\\__, /\\__,_/\\__,_/\\__/\\____/_/ /_/\\___/\n")
|
77
|
+
output(" /_/ #{subtitle.downcase} v#{Aquatone::VERSION} - by @michenriksen\n\n")
|
78
|
+
end
|
79
|
+
|
80
|
+
def self.uncolorize(text); colorize(text.to_s, 0); end
|
81
|
+
def self.red(text); colorize(text.to_s, 31); end
|
82
|
+
def self.green(text); colorize(text.to_s, 32); end
|
83
|
+
def self.blue(text); colorize(text.to_s, 34); end
|
84
|
+
def self.yellow(text); colorize(text.to_s, 33); end
|
85
|
+
def self.bold(text); colorize(text.to_s, 1); end
|
86
|
+
|
87
|
+
def thread_pool
|
88
|
+
if options[:sleep]
|
89
|
+
Aquatone::ThreadPool.new(1)
|
90
|
+
else
|
91
|
+
Aquatone::ThreadPool.new(options[:threads])
|
92
|
+
end
|
93
|
+
end
|
94
|
+
|
95
|
+
def jitter_sleep
|
96
|
+
return unless options[:sleep]
|
97
|
+
seconds = options[:sleep].to_i
|
98
|
+
if options[:jitter]
|
99
|
+
jitter = (options[:jitter].to_f / 100) * seconds
|
100
|
+
if rand.round == 0
|
101
|
+
seconds = seconds - Random.rand(0..jitter.round)
|
102
|
+
else
|
103
|
+
seconds = seconds + Random.rand(0..jitter.round)
|
104
|
+
end
|
105
|
+
seconds = 1 if seconds < 1
|
106
|
+
end
|
107
|
+
sleep seconds
|
108
|
+
end
|
109
|
+
|
110
|
+
def seconds_to_time(seconds)
|
111
|
+
Time.at(seconds).utc.strftime("%H:%M:%S")
|
112
|
+
end
|
113
|
+
|
114
|
+
def asked_for_progress?
|
115
|
+
while c = STDIN.read_nonblock(1)
|
116
|
+
return true if c == "\n"
|
117
|
+
end
|
118
|
+
false
|
119
|
+
rescue IO::EAGAINWaitReadable
|
120
|
+
false
|
121
|
+
rescue Errno::EINTR, Errno::EAGAIN, EOFError
|
122
|
+
true
|
123
|
+
end
|
124
|
+
|
125
|
+
def has_executable?(name)
|
126
|
+
exts = ENV['PATHEXT'] ? ENV['PATHEXT'].split(';') : ['']
|
127
|
+
ENV["PATH"].split(File::PATH_SEPARATOR).each do |path|
|
128
|
+
exts.each do |ext|
|
129
|
+
exe = File.join(path, "#{name}#{ext}")
|
130
|
+
return true if File.executable?(exe) && !File.directory?(exe)
|
131
|
+
end
|
132
|
+
end
|
133
|
+
false
|
134
|
+
end
|
135
|
+
|
136
|
+
def self.jitter
|
137
|
+
seconds = @options[:jitter]
|
138
|
+
if seconds != 0
|
139
|
+
random_sleep = ((1 - (rand(30) * 0.01)) * seconds)
|
140
|
+
sleep(random_sleep)
|
141
|
+
end
|
142
|
+
end
|
143
|
+
|
144
|
+
def self.with_output_mutex
|
145
|
+
output_mutex.synchronize { yield }
|
146
|
+
end
|
147
|
+
|
148
|
+
def self.output_mutex
|
149
|
+
@output_mutex ||= Mutex.new
|
150
|
+
end
|
151
|
+
end
|
152
|
+
end
|