Last active
November 9, 2023 04:18
-
-
Save nroose/c3d7697d680c36b306c567535b211764 to your computer and use it in GitHub Desktop.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
#!/usr/bin/env ruby | |
require 'benchmark' | |
require 'cgi' | |
require 'net/http' | |
require 'resolv' | |
require 'resolv-replace' | |
host = '' | |
period = 10 | |
count = 3 | |
verbose = false | |
user_agent = $PROGRAM_NAME | |
def sanitize_host(host) | |
return nil if host.nil? || host.strip == '' | |
protocol = host.sub(%r{^(https?://)?.*$}, '\1') | |
protocol = 'https://' if protocol.strip == '' | |
name = "#{host.sub(%r{^(?:https?://)?(.*[^/])/?$}, '\1')}/" | |
host && protocol + name | |
end | |
def resolve_host(host) | |
ip = Resolv.getaddress(host) | |
custom_hosts = '/tmp/custom_hosts' | |
File.open(custom_hosts, 'w') do |file| | |
file.write("#{ip} #{host}\n") | |
end | |
hosts_resolver = Resolv::Hosts.new(custom_hosts) | |
dns_resolver = Resolv::DNS.new | |
Resolv::DefaultResolver.replace_resolvers([hosts_resolver, dns_resolver]) | |
end | |
while ARGV[0] =~ /^-/ | |
if ARGV[0] == '-h' | |
ARGV.shift | |
host = ARGV.shift | |
host = sanitize_host(host) | |
end | |
if ARGV[0] == '-H' | |
ARGV.shift | |
header = ARGV.shift | |
value = ARGV.shift | |
end | |
if ARGV[0] == '-p' | |
ARGV.shift | |
period = ARGV.shift.to_i | |
end | |
if ARGV[0] == '-c' | |
ARGV.shift | |
count = ARGV.shift.to_i | |
end | |
if ARGV[0] == '-r' | |
ARGV.shift | |
ramp = ARGV.shift.to_i | |
end | |
if ARGV[0] == '-l' | |
ARGV.shift | |
log_header = ARGV.shift | |
end | |
if ARGV[0] == '-v' | |
verbose = true | |
ARGV.shift | |
end | |
if ARGV[0] == '-t' | |
terse = true | |
ARGV.shift | |
end | |
if ARGV[0] == '-w' | |
wait = true | |
ARGV.shift | |
end | |
if ARGV[0] == '-P' | |
ARGV.shift | |
pause = ARGV.shift.to_i | |
end | |
if ARGV[0] == '-u' | |
ARGV.shift | |
user_agent = ARGV.shift | |
end | |
if ARGV[0] == '-s' | |
ARGV.shift | |
strong = true | |
end | |
end | |
if ARGV.empty? | |
puts "#{$PROGRAM_NAME} [-v] [-p <period>] [-c <count>] [-l <log-header>] [-H <requewst header> <value>] " \ | |
'[-h <host>] [-r <ramp seconds>] [-P <seconds pause>] [-u <user agent>] [-s] url/path ...' | |
puts 'Send repeated get requests in a thead for each url (which are paths if you specify the host parameter). ' \ | |
'For period seconds. ' \ | |
'With count threads for each url. ' \ | |
'Log the log header. ' \ | |
'Verbose (-v) shows result of each request instead of running status.' \ | |
'Wait to start each additional thread for ramp seconds. ' \ | |
'Pause for pause seconds between each successive hits in each thread. ' \ | |
'Use the user_agent. ' \ | |
'If you need to use basic auth, set the env variables HTTP_USER and HTTP_PASS. ' \ | |
'Use strong (-s) to keeep going on excptions.' | |
exit | |
end | |
urls = {} | |
ARGV.each do |url| | |
urls[url] = {} | |
end | |
puts "Period: #{period}" | |
puts "Count: #{count}" | |
puts "Host: #{host}" if host | |
puts "Log Header: #{log_header}" if log_header | |
puts "Ramp: #{ramp}" if ramp | |
puts "Pause: #{pause}" if pause | |
puts "Agent: #{user_agent}" if user_agent | |
puts "Urls/Paths: #{urls.keys.inspect}" | |
recents = [nil] * count * urls.count | |
seconds = 0 | |
all = {} | |
all[:durations] = [] | |
all[:codes] = {} | |
all[log_header] = {} if log_header | |
start = Time.now | |
threads = 0 | |
urls.each_key do |url| | |
url_with_host = host + url.sub(%r{^/?(.*)}, '\1') | |
count.times do | |
break unless Time.now - start < period | |
urls[url][:thread] ||= [] | |
urls[url][:thread] << Thread.new do | |
cookies = nil | |
threads += 1 | |
while Time.now - start < period | |
begin | |
url_start = Time.now | |
uri = URI.parse(CGI.unescapeHTML(url_with_host)) | |
resolve_host(uri.hostname) | |
http = Net::HTTP.new(uri.host, uri.port) | |
http.use_ssl = uri.instance_of? URI::HTTPS | |
request = Net::HTTP::Get.new(uri.request_uri, { 'User-Agent' => user_agent }) | |
request['Accept'] = '*/*' | |
request[header] = value if header | |
request.basic_auth(ENV['HTTP_USER'], ENV['HTTP_PASS']) if ENV['HTTP_USER'] | |
request['Cookie'] = cookies if cookies | |
res = nil | |
dur = Benchmark.measure do | |
res = http.request(request) | |
end | |
cookies = res.get_fields('set-cookie') | |
next unless res && dur | |
all[:durations] << dur.real | |
all[:codes][res.code.to_s] ||= 0 | |
all[:codes][res.code.to_s] += 1 | |
if log_header && res[log_header] | |
all[log_header][res[log_header]] ||= 0 | |
all[log_header][res[log_header]] += 1 | |
end | |
urls[url][:durations] ||= [] | |
urls[url][:durations] << dur.real | |
urls[url][:codes] ||= {} | |
urls[url][:codes][res.code.to_s] ||= 0 | |
urls[url][:codes][res.code.to_s] += 1 | |
if log_header && res[log_header] | |
urls[url][log_header] ||= {} | |
urls[url][log_header][res[log_header]] ||= 0 | |
urls[url][log_header][res[log_header]] += 1 | |
end | |
recents.shift | |
recents << { code: res.code.to_s, duration: dur.real } | |
recent_durations = recents.compact.map { |recent| recent[:duration] } | |
recent_duration = recent_durations.compact.sum / recent_durations.compact.count | |
recent_codes = {} | |
recents.compact.each do |recent| | |
recent_codes[recent[:code]] ||= 0 | |
recent_codes[recent[:code]] += 1 | |
end | |
if verbose | |
puts "#{(Time.now - start).round}/#{period}s #{threads} #{res.code} #{dur.real.round(3)} #{host}#{url} #{res[log_header] if log_header}" | |
elsif terse | |
unless seconds == (Time.now - start).round | |
seconds = (Time.now - start).round | |
avg_duration = (all[:durations].sum / all[:durations].count) | |
puts "#{(Time.now - start).round}/#{period}s #{threads} threads " \ | |
"All: #{"%.3f" % avg_duration} #{all[:codes].inspect} " \ | |
"#{all[log_header].inspect if log_header} - " \ | |
"Recent: #{"%.3f" % recent_duration} #{recent_codes.inspect}" | |
end | |
else | |
avg_duration = (all[:durations].sum / all[:durations].count) | |
print "\r\033[0K#{(Time.now - start).round}/#{period}s #{threads} threads " \ | |
"All: #{"%.3f" % avg_duration} #{all[:codes].inspect} " \ | |
"#{all[log_header].inspect if log_header} - " \ | |
"Recent: #{"%.3f" % recent_duration} #{recent_codes.inspect}" | |
end | |
sleep(pause) if pause | |
sleep(avg_duration) if wait && res.code == 503 | |
rescue => e | |
all[:codes]['EXC'] ||= 0 | |
all[:codes]['EXC'] += 1 | |
urls[url][:codes] ||= {} | |
urls[url][:codes]['EXC'] ||= 0 | |
urls[url][:codes]['EXC'] += 1 | |
recents.shift | |
recents << { code: 'EXC', duration: nil } | |
puts "rescue on #{url_with_host} #{e} after #{Time.now - url_start}" | |
raise unless strong | |
end | |
end | |
ensure | |
threads -= 1 | |
end | |
sleep(ramp) if ramp | |
end | |
end | |
urls.each_key do |url| | |
urls[url][:thread]&.each(&:join) | |
rescue Exception | |
urls[url][:thread]&.each(&:exit) | |
break | |
end | |
all_avg_duration = (all[:durations].sum / all[:durations].count).round(3) | |
if File.directory?('tmp') | |
File.open('tmp/all_avg_duration', 'w') do |file| | |
file.write("All Avg Duration: #{all_avg_duration * 1000}\n") | |
end | |
end | |
puts "\r\033[0KAll: #{(Time.now - start).round}/#{period}s #{all_avg_duration} #{all[:codes].inspect}" | |
puts "#{log_header}: #{all[log_header].inspect}" if log_header | |
urls.each do |k, v| | |
puts "#{k}: #{(v[:durations].sum / v[:durations].count).round(3)} #{v[:codes].inspect}" if v[:durations] | |
end | |
exit all[:codes]['500'].to_i + all[:codes]['EXC'].to_i |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment