qreplay 1.0.0

Sign up to get free protection for your applications and to get access to all the features.
data/Gemfile ADDED
@@ -0,0 +1,3 @@
1
+ source 'https://rubygems.org'
2
+ gemspec
3
+
@@ -0,0 +1,10 @@
1
+ Copyright (C) 2014 Old School Industries LLC
2
+
3
+ (MIT License)
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions:
6
+
7
+ The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software.
8
+
9
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
10
+
@@ -0,0 +1,70 @@
1
+ # qreplay
2
+
3
+ This is a simple tool to drive capturing, saving, and replaying HTTP requests. It depends on `pcap_tools`, `tshark`, `dumpcap`, and `httperf`.
4
+
5
+ ## Use
6
+
7
+ ```shell
8
+ gem install qreplay
9
+ qreplay [capture|replay|transform|capture_only] [options]
10
+ ```
11
+
12
+ Options:
13
+ ```
14
+ --capture-time, -c <f>: Capture time length in seconds (default: 60.0)
15
+ --port, -p <i>: Capture/replay traffic to port (default: 80)
16
+ --host, -h <s>: Replay host (default: 0.0.0.0)
17
+ --req-sec, -r <i>: Requests per second for replays (default: 20)
18
+ --total-requests, -t <i>: Total replay requests to send (default: 10000)
19
+ --capture-file, -a <s>: Output file (default: ./qreplay.sesslog)
20
+ --tshark-binary, -s <s>: TShark binary file location (default: tshark)
21
+ --dumpcap-binary, -d <s>: dumpcap binary file location (default: dumpcap)
22
+ --pcap-file, -f <s>: Temporary intermediate pcap file path (default: ./qreplay.pcap)
23
+ --httperf-binary, -e <s>: httperf binary file location (default: httperf)
24
+ --help, -l: Show this message
25
+ ```
26
+
27
+ ## Example
28
+
29
+ You want to capture traffic on a live web server. On the remote machine run:
30
+
31
+ ```
32
+ qreplay capture --capture-time 60 --port 80
33
+ ```
34
+
35
+ This will use `tshark` to capture TCP traffic to/from port 80 for 60 seconds, stitch together HTTP requests from the TCP traffic, and save requests to `./qreplay.sesslog`. If you observe this file you will notice that each request line contains an HTTP method, path, and body in a format acceptable to `httperf`.
36
+
37
+ You can then replay with:
38
+
39
+ ```
40
+ qreplay replay --host 127.0.0.1 --port 80 --req-sec 50
41
+ ```
42
+
43
+ To replay these requests to the local server at port 80 at a rate of 50 per second.
44
+
45
+ ## Installing
46
+
47
+ Mac OS X (homebrew):
48
+ ```
49
+ gem install qreplay
50
+ brew install wireshark httperf
51
+ ```
52
+
53
+ Yum package manager:
54
+ ```
55
+ gem install qreplay
56
+ yum install wireshark httperf
57
+ ```
58
+
59
+ ## httperf
60
+
61
+ By default, httperf only handles HTTP bodies with 10,000 bytes or less, which is easy to exceed if you're testing an application that POSTs significant amounts of data. We've increased a few of the arbitrary limitations in httperf in our fork, [here](https://github.com/quizlet/httperf), we recommend using it if you're hitting limits in the mainline httperf.
62
+
63
+ ## License
64
+
65
+ The qreplay copyright is owned by Old School Industries LLC. We've licensed it under the MIT License, which can be found in `LICENCE.md`.
66
+
67
+ ## Thanks
68
+
69
+ Thanks to Bertrand Paquet for developing pcap tools and licensing it under the Apache 2 license. You can find more about pcap_tools [here](https://github.com/bpaquet/pcap_tools).
70
+
@@ -0,0 +1,142 @@
1
+ #!/usr/bin/env ruby
2
+
3
+ require 'trollop'
4
+ require 'pcap_tools'
5
+ require 'pp'
6
+ require 'qreplay'
7
+
8
+ p = Trollop::Parser.new do
9
+ version "qreplay #{QReplay::VERSION} - (c) Old School Industries LLC"
10
+ banner <<-EOS
11
+ qreplay is a tool for capturing and replaying HTTP traffic.
12
+
13
+ qreplay [capture|replay|transform|capture-only] <args>
14
+
15
+ capture - Use tshark to capture http packets.
16
+ replay - Replay HTTP session file with requests to a host/port.
17
+ transform - Transform a dumpcap file to a sesslog file. This is executed automatically in capture mode.
18
+ capture-only - Perform a capture without a transform step.
19
+
20
+ Capturing TCP traffic requires root privileges on most systems.
21
+
22
+ Examples:
23
+
24
+ > sudo qreplay capture --capture-time 60 --port 80
25
+ > qreplay replay --host 127.0.0.1 --port 80 --req-sec 50
26
+
27
+ qreplay depends on tshark, dumpcap, and httperf, by default attempting to find them in the environment, with the option of passing the paths to them directly on the command line.
28
+
29
+ More info can be found at the gem website at github.com/quizlet/qreplay
30
+
31
+ Command Line Options:
32
+
33
+ EOS
34
+
35
+ opt :capture_time, 'Capture time length in seconds', :default => 60.0
36
+ opt :capture_interface, 'Traffic capture interface (uses dumpcap default if not specified)', :type => :string
37
+ opt :port, 'Capture/replay traffic to port', :default => 80
38
+ opt :host, 'Replay host', :default => '0.0.0.0'
39
+ opt :req_sec, 'Requests per second for replays', :default => 20
40
+ opt :total_requests, 'Total replay requests to send', :default => 10000
41
+ opt :capture_file, 'Output file', :default => './qreplay.sesslog'
42
+ opt :pcap_file, 'Temporary intermediate pcap file path', :default => './qreplay.pcap'
43
+ opt :timeout, 'Timeout for replay requests', :default => 10
44
+ opt :tshark_binary, 'TShark binary file location', :default => 'tshark'
45
+ opt :dumpcap_binary, 'dumpcap binary file location', :default => 'dumpcap'
46
+ opt :httperf_binary, 'httperf binary file location', :default => 'httperf'
47
+ end
48
+
49
+ COMMANDS = ['capture', 'replay', 'transform', 'capture-only']
50
+
51
+ OPT = Trollop::with_standard_exception_handling p do
52
+ opt = p.parse ARGV
53
+ raise Trollop::HelpNeeded if ARGV.size < 1 || !COMMANDS.include?(ARGV[0])
54
+ opt
55
+ end
56
+
57
+
58
+ def check_binary(name, binary)
59
+ r = `which #{binary}`
60
+ raise "Could not find #{name} binary at path #{binary}" unless r and r.size > 0
61
+ end
62
+
63
+ class Printer
64
+ def initialize(capture_file)
65
+ @counter = 0
66
+ @fhandle = File.open(capture_file, 'w+')
67
+ end
68
+
69
+ def process_stream stream
70
+ stream.each do |index, req, resp|
71
+ body = req.body
72
+ @fhandle.puts "#{req.path} method=#{req.method} contents=#{body.inspect}"
73
+ @fhandle.puts "\n"
74
+ @counter += 1
75
+ end
76
+ end
77
+
78
+ def finalize
79
+ puts "Number of HTTP Requests : #{@counter}"
80
+ @fhandle.close
81
+ end
82
+ end
83
+
84
+ def transform_pcap(tshark_binary, pcap_file, capture_file)
85
+ puts "Writing HTTP requests with #{tshark_binary} from #{pcap_file} to #{capture_file}"
86
+ check_binary('tshark', tshark_binary)
87
+
88
+ processor = QReplay::TcpProcessor.new
89
+ processor.add_stream_processor(PcapTools::TcpStreamRebuilder.new)
90
+ processor.add_stream_processor(QReplay::HttpExtractor.new)
91
+ processor.add_stream_processor(Printer.new(capture_file))
92
+
93
+ begin
94
+ PcapTools::Loader::load_file(pcap_file, {}) do |index, packet|
95
+ begin
96
+ processor.inject index, packet
97
+ rescue Exception => e
98
+ puts "Skipping unparseable request:"
99
+ puts e.message
100
+ pp e.backtrace
101
+ end
102
+ end
103
+ rescue Exception => e
104
+ puts "Exception while parsing tshark output, saving current requests and bailing out."
105
+ puts e.message
106
+ pp e.backtrace
107
+ end
108
+
109
+ processor.finalize
110
+ end
111
+
112
+ def capture_pcap(port, pcap_file, capture_time, dumpcap_binary, capture_interface)
113
+ check_binary('dumpcap', dumpcap_binary)
114
+
115
+ puts "Capturing HTTP on port #{port} to file #{pcap_file} for #{capture_time} seconds"
116
+ cmd = "#{dumpcap_binary} -w #{pcap_file} -f 'tcp port #{port}' -a duration:#{capture_time.to_i}"
117
+ cmd += " -i #{capture_interface}" if capture_interface
118
+ puts cmd
119
+ system(cmd)
120
+ end
121
+
122
+ def replay_httperf(binary, req_sec, total_requests, capture_file, host, port, timeout)
123
+ check_binary('httperf', binary)
124
+ thinktime = 0.1
125
+ cmd = "#{binary} --server=#{host} --port=#{port} --rate=#{req_sec} --verbose --wsesslog=#{total_requests},#{thinktime},#{capture_file} --hog --timeout=#{timeout}"
126
+ puts cmd
127
+ system(cmd)
128
+ end
129
+
130
+ command = ARGV[0]
131
+ case command
132
+ when 'capture'
133
+ capture_pcap(OPT[:port], OPT[:pcap_file], OPT[:capture_time], OPT[:dumpcap_binary], OPT[:capture_interface])
134
+ transform_pcap(OPT[:tshark_binary], OPT[:pcap_file], OPT[:capture_file])
135
+ when 'capture-only'
136
+ capture_pcap(OPT[:port], OPT[:pcap_file], OPT[:capture_time], OPT[:dumpcap_binary], OPT[:capture_interface])
137
+ when 'transform'
138
+ transform_pcap(OPT[:tshark_binary], OPT[:pcap_file], OPT[:capture_file])
139
+ when 'replay'
140
+ replay_httperf(OPT[:httperf_binary], OPT[:req_sec], OPT[:total_requests], OPT[:capture_file], OPT[:host], OPT[:port], OPT[:timeout])
141
+ end
142
+
@@ -0,0 +1,4 @@
1
+ require_relative 'qreplay/tcpprocessor'
2
+ require_relative 'qreplay/httpextractor'
3
+ require_relative 'qreplay/version'
4
+
@@ -0,0 +1,127 @@
1
+ # This is a modified version of the code found in https://github.com/bpaquet/pcap_tools/blob/master/lib/pcap_tools/stream_processors/http.rb
2
+ # Modified to handle more HTTP traffic cases.
3
+
4
+ require 'pp'
5
+
6
+ module QReplay
7
+
8
+ class HttpExtractor
9
+
10
+ def process_stream stream
11
+ calls = []
12
+ i = 0
13
+ last_req = nil
14
+ req = nil
15
+
16
+ while i + 1 < stream[:data].size
17
+ if last_req
18
+ req = parse_request(last_req, stream[:data][i][:data])
19
+ last_req = nil
20
+ req['Expect'] = nil
21
+ else
22
+ req = parse_request(stream[:data][i])
23
+ end
24
+
25
+ resp = parse_response(stream[:data][i + 1])
26
+
27
+ if req && req['Expect'] && req['Expect'].strip == '100-continue'
28
+ last_req = stream[:data][i]
29
+ elsif !req.nil? && !resp.nil?
30
+ calls << [stream[:index], req, resp]
31
+ end
32
+
33
+ i += 2
34
+ end
35
+
36
+ calls
37
+ end
38
+
39
+ def finalize
40
+ end
41
+
42
+ private
43
+
44
+ def parse_request stream, separate_body = nil
45
+ headers, body = split_headers(stream[:data])
46
+ return nil unless headers
47
+ body = separate_body if separate_body
48
+ line0 = headers.shift
49
+ m = /(\S+)\s+(\S+)\s+(\S+)/.match(line0) or raise "Unable to parse first line of http request #{line0}"
50
+ clazz = {
51
+ 'POST' => Net::HTTP::Post,
52
+ 'HEAD' => Net::HTTP::Head,
53
+ 'GET' => Net::HTTP::Get,
54
+ 'PUT' => Net::HTTP::Put,
55
+ 'DELETE' => Net::HTTP::Delete
56
+ }[m[1]] or raise "Unknown http request type [#{m[1]}]"
57
+ req = clazz.new m[2]
58
+ req['Pcap-Src'] = stream[:from]
59
+ req['Pcap-Src-Port'] = stream[:from_port]
60
+ req['Pcap-Dst'] = stream[:to]
61
+ req['Pcap-Dst-Port'] = stream[:to_port]
62
+ req.time = stream[:time]
63
+ req.body = body
64
+ req['user-agent'] = nil
65
+ req['accept'] = nil
66
+ add_headers req, headers
67
+ if req['Content-Length']
68
+ if req.body.bytesize != req['Content-Length'].to_i && (req['Expect'].nil? || req['Expect'].strip != '100-continue')
69
+ puts "Wrong content-length for http request, header say [#{req['Content-Length'].chomp}], found #{req.body.size}"
70
+ return nil
71
+ end
72
+ end
73
+ req
74
+ end
75
+
76
+ def parse_response stream
77
+ headers, body = split_headers(stream[:data])
78
+ line0 = headers.shift
79
+ m = /^(\S+)\s+(\S+)\s+(.*)$/.match(line0) or raise "Unable to parse first line of http response [#{line0}]"
80
+ resp = Net::HTTPResponse.send(:response_class, m[2]).new(m[1], m[2], m[3])
81
+ resp.time = stream[:time]
82
+ add_headers resp, headers
83
+ if resp.chunked?
84
+ resp.body = read_chunked("\r\n" + body)
85
+ else
86
+ resp.body = body
87
+ if resp['Content-Length']
88
+ unless resp.body.size == resp['Content-Length'].to_i
89
+ puts "Wrong content-length for http response, header say [#{resp['Content-Length'].chomp}], found #{resp.body.size}"
90
+ return nil
91
+ end
92
+ end
93
+ end
94
+ begin
95
+ resp.body = Zlib::GzipReader.new(StringIO.new(resp.body)).read if resp['Content-Encoding'] == 'gzip'
96
+ rescue Zlib::GzipFile::Error
97
+ warn "Response body is not in gzip: [#{resp.body}]"
98
+ end
99
+ resp
100
+ end
101
+
102
+ def add_headers o, headers
103
+ headers.each do |line|
104
+ m = /\A([^:]+):\s*/.match(line) or raise "Unable to parse header line [#{line}]"
105
+ o[m[1]] = m.post_match
106
+ end
107
+ end
108
+
109
+ def split_headers str
110
+ index = str.index("\r\n\r\n")
111
+ return nil, nil if index.nil?
112
+ return str[0 .. index].split("\r\n"), str[index + 4 .. -1]
113
+ end
114
+
115
+ def read_chunked str
116
+ if str.nil? || (str == "\r\n")
117
+ return ''
118
+ end
119
+ m = /\r\n([0-9a-fA-F]+)\r\n/.match(str) or raise "Unable to read chunked body in #{str.split("\r\n")[0]}"
120
+ len = m[1].hex
121
+ return '' if len == 0
122
+ m.post_match[0..len - 1] + read_chunked(m.post_match[len .. -1])
123
+ end
124
+
125
+ end
126
+
127
+ end
@@ -0,0 +1,68 @@
1
+ # This is a modified version of the code found in https://github.com/bpaquet/pcap_tools/blob/master/lib/pcap_tools/packet_processors/tcp.rb.
2
+
3
+ require 'time'
4
+
5
+ module QReplay
6
+
7
+ class TcpProcessor
8
+
9
+ def initialize
10
+ @streams = {}
11
+ @stream_processors = []
12
+ end
13
+
14
+ def add_stream_processor processor
15
+ @stream_processors << processor
16
+ end
17
+
18
+ def inject index, packet
19
+ stream_index = packet[:stream]
20
+ if stream_index
21
+ if packet[:tcp_flags][:syn] && packet[:tcp_flags][:ack] === false
22
+ @streams[stream_index] = {
23
+ :first => packet,
24
+ :data => [],
25
+ }
26
+ elsif packet[:tcp_flags][:fin] || packet[:tcp_flags][:rst]
27
+ if @streams[stream_index]
28
+ current = {:index => stream_index, :data => @streams[stream_index][:data]}
29
+ @stream_processors.each do |p|
30
+ current = p.process_stream current
31
+ break unless current
32
+ end
33
+ @streams.delete stream_index
34
+ end
35
+ else
36
+ unless @streams[stream_index]
37
+ @streams[stream_index] = {
38
+ :first => packet,
39
+ :data => [],
40
+ }
41
+ end
42
+
43
+ packet[:type] = (packet[:from] == @streams[stream_index][:first][:from] && packet[:from_port] == @streams[stream_index][:first][:from_port]) ? :out : :in
44
+ packet.delete :tcp_flags
45
+ @streams[stream_index][:data] << packet if packet[:size] > 0
46
+ end
47
+ end
48
+ end
49
+
50
+ def finalize
51
+ @streams.each do |k, stream|
52
+ current = {:index => k, :data => stream[:data]}
53
+ @stream_processors.each do |p|
54
+ current = p.process_stream current
55
+ break unless current
56
+ end
57
+ end
58
+
59
+ @stream_processors.each do |p|
60
+ p.finalize
61
+ end
62
+ end
63
+
64
+ end
65
+
66
+ end
67
+
68
+
@@ -0,0 +1,4 @@
1
+ module QReplay
2
+ VERSION = '1.0.0'
3
+ end
4
+
@@ -0,0 +1,24 @@
1
+ # encoding: utf-8
2
+ require File.expand_path("../lib/qreplay.rb", __FILE__)
3
+
4
+ Gem::Specification.new do |gem|
5
+ gem.add_runtime_dependency 'pcap_tools'
6
+ gem.add_runtime_dependency 'trollop'
7
+
8
+ gem.authors = ["Peter Bakkum"]
9
+ gem.bindir = 'bin'
10
+ gem.description = %q{Capture and replay HTTP traffic}
11
+ gem.email = ['peter@quizlet.com']
12
+ gem.executables = ['qreplay']
13
+ gem.extra_rdoc_files = ['LICENSE.md', 'README.md']
14
+ gem.files = Dir['LICENSE.md', 'README.md', 'qreplay.gemspec', 'Gemfile', 'bin/*', 'lib/**/*']
15
+ gem.homepage = 'http://github.com/quizlet/qreplay'
16
+ gem.name = 'qreplay'
17
+ gem.rdoc_options = ["--charset=UTF-8"]
18
+ gem.require_paths = ['lib']
19
+ gem.required_rubygems_version = Gem::Requirement.new(">= 1.3.6")
20
+ gem.summary = %q{Capture and replay HTTP traffic}
21
+ gem.version = QReplay::VERSION
22
+ gem.license = 'MIT'
23
+ end
24
+
metadata ADDED
@@ -0,0 +1,91 @@
1
+ --- !ruby/object:Gem::Specification
2
+ name: qreplay
3
+ version: !ruby/object:Gem::Version
4
+ version: 1.0.0
5
+ prerelease:
6
+ platform: ruby
7
+ authors:
8
+ - Peter Bakkum
9
+ autorequire:
10
+ bindir: bin
11
+ cert_chain: []
12
+ date: 2015-03-10 00:00:00.000000000 Z
13
+ dependencies:
14
+ - !ruby/object:Gem::Dependency
15
+ name: pcap_tools
16
+ requirement: !ruby/object:Gem::Requirement
17
+ none: false
18
+ requirements:
19
+ - - ! '>='
20
+ - !ruby/object:Gem::Version
21
+ version: '0'
22
+ type: :runtime
23
+ prerelease: false
24
+ version_requirements: !ruby/object:Gem::Requirement
25
+ none: false
26
+ requirements:
27
+ - - ! '>='
28
+ - !ruby/object:Gem::Version
29
+ version: '0'
30
+ - !ruby/object:Gem::Dependency
31
+ name: trollop
32
+ requirement: !ruby/object:Gem::Requirement
33
+ none: false
34
+ requirements:
35
+ - - ! '>='
36
+ - !ruby/object:Gem::Version
37
+ version: '0'
38
+ type: :runtime
39
+ prerelease: false
40
+ version_requirements: !ruby/object:Gem::Requirement
41
+ none: false
42
+ requirements:
43
+ - - ! '>='
44
+ - !ruby/object:Gem::Version
45
+ version: '0'
46
+ description: Capture and replay HTTP traffic
47
+ email:
48
+ - peter@quizlet.com
49
+ executables:
50
+ - qreplay
51
+ extensions: []
52
+ extra_rdoc_files:
53
+ - LICENSE.md
54
+ - README.md
55
+ files:
56
+ - LICENSE.md
57
+ - README.md
58
+ - qreplay.gemspec
59
+ - Gemfile
60
+ - bin/qreplay
61
+ - lib/qreplay/httpextractor.rb
62
+ - lib/qreplay/tcpprocessor.rb
63
+ - lib/qreplay/version.rb
64
+ - lib/qreplay.rb
65
+ homepage: http://github.com/quizlet/qreplay
66
+ licenses:
67
+ - MIT
68
+ post_install_message:
69
+ rdoc_options:
70
+ - --charset=UTF-8
71
+ require_paths:
72
+ - lib
73
+ required_ruby_version: !ruby/object:Gem::Requirement
74
+ none: false
75
+ requirements:
76
+ - - ! '>='
77
+ - !ruby/object:Gem::Version
78
+ version: '0'
79
+ required_rubygems_version: !ruby/object:Gem::Requirement
80
+ none: false
81
+ requirements:
82
+ - - ! '>='
83
+ - !ruby/object:Gem::Version
84
+ version: 1.3.6
85
+ requirements: []
86
+ rubyforge_project:
87
+ rubygems_version: 1.8.23.2
88
+ signing_key:
89
+ specification_version: 3
90
+ summary: Capture and replay HTTP traffic
91
+ test_files: []