aquatone 0.1.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 +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
|