sniffer 0.0.1 → 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 +4 -4
- data/.rubocop.yml +7 -4
- data/.travis.yml +9 -0
- data/Gemfile +0 -2
- data/README.md +109 -4
- data/assets/demo.gif +0 -0
- data/lib/sniffer/adapters/curb_adapter.rb +84 -0
- data/lib/sniffer/adapters/ethon_adapter.rb +89 -0
- data/lib/sniffer/adapters/http_adapter.rb +72 -0
- data/lib/sniffer/adapters/httpclient_adapter.rb +59 -0
- data/lib/sniffer/adapters/net_http_adapter.rb +50 -0
- data/lib/sniffer/adapters/patron_adapter.rb +49 -0
- data/lib/sniffer/config.rb +13 -8
- data/lib/sniffer/data_item.rb +106 -2
- data/lib/sniffer/version.rb +1 -1
- data/lib/sniffer.rb +37 -8
- data/sniffer.gemspec +10 -2
- metadata +150 -3
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA1:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: e61d38c32a8f74c8fabdd9113a45892685be34c8
|
4
|
+
data.tar.gz: 5ab43be1021421ec32cde7df4d2f33692318fda9
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: 66ed0687f93be90af88b3a6b440350163f1f25284827e8359ed36dec78554c4fc8f55607f365090a358539f4a1e4e8db7ba4ac4f7b993c1a7d6c477c6c47fe55
|
7
|
+
data.tar.gz: ae3bb49ab20fd5fb84f9fe7e70c05c00e16141f43ee04aec53b627ec2e9f0269a2feb3fd54cd66a2de897390ebb96bfecd392f1e9bc4ce7945a75b7058002463
|
data/.rubocop.yml
CHANGED
@@ -13,7 +13,7 @@ AllCops:
|
|
13
13
|
StyleGuideCopsOnly: false
|
14
14
|
TargetRubyVersion: 2.4
|
15
15
|
|
16
|
-
|
16
|
+
Naming/AccessorMethodName:
|
17
17
|
Enabled: false
|
18
18
|
|
19
19
|
Style/PercentLiteralDelimiters:
|
@@ -26,7 +26,7 @@ Style/Documentation:
|
|
26
26
|
Exclude:
|
27
27
|
- 'spec/**/*.rb'
|
28
28
|
|
29
|
-
Style/StringLiterals:
|
29
|
+
Style/StringLiterals:
|
30
30
|
Enabled: false
|
31
31
|
|
32
32
|
Style/BlockDelimiters:
|
@@ -44,7 +44,7 @@ Metrics/MethodLength:
|
|
44
44
|
- 'spec/**/*.rb'
|
45
45
|
|
46
46
|
Metrics/LineLength:
|
47
|
-
Max:
|
47
|
+
Max: 120
|
48
48
|
Exclude:
|
49
49
|
- 'spec/**/*.rb'
|
50
50
|
|
@@ -56,4 +56,7 @@ Rails/Date:
|
|
56
56
|
Enabled: false
|
57
57
|
|
58
58
|
Rails/TimeZone:
|
59
|
-
Enabled: false
|
59
|
+
Enabled: false
|
60
|
+
|
61
|
+
Security/YAMLLoad:
|
62
|
+
Enabled: false
|
data/.travis.yml
CHANGED
@@ -1,5 +1,14 @@
|
|
1
1
|
sudo: false
|
2
2
|
language: ruby
|
3
|
+
env:
|
4
|
+
global:
|
5
|
+
- CC_TEST_REPORTER_ID=bc054bdf814431bf40559f1cd62a86956fa5666296895400e1282a47ec6d1c69
|
3
6
|
rvm:
|
4
7
|
- 2.4.2
|
5
8
|
before_install: gem install bundler -v 1.15.4
|
9
|
+
before_script:
|
10
|
+
- curl -L https://codeclimate.com/downloads/test-reporter/test-reporter-latest-linux-amd64 > ./cc-test-reporter
|
11
|
+
- chmod +x ./cc-test-reporter
|
12
|
+
- ./cc-test-reporter before-build
|
13
|
+
after_script:
|
14
|
+
- ./cc-test-reporter after-build --exit-code $TRAVIS_TEST_RESULT
|
data/Gemfile
CHANGED
data/README.md
CHANGED
@@ -1,8 +1,28 @@
|
|
1
|
-
# Sniffer
|
1
|
+
# Sniffer [](https://travis-ci.org/aderyabin/sniffer) [](https://rubygems.org/gems/sniffer) [](https://codeclimate.com/github/aderyabin/sniffer/maintainability)
|
2
2
|
|
3
|
-
Welcome to your new gem! In this directory, you'll find the files you need to be able to package up your Ruby library into a gem. Put your Ruby code in the file `lib/sniffer`. To experiment with that code, run `bin/console` for an interactive prompt.
|
4
3
|
|
5
|
-
|
4
|
+
Sniffer aims to help:
|
5
|
+
|
6
|
+
* Log outgoing HTTP requests. Sniffer logs as JSON format for export to ELK, Logentries and etc.
|
7
|
+
* Debug requests. Sniffer allows to save all requests/responses in storage for future debugging
|
8
|
+
|
9
|
+
Sniffer supports most common HTTP accessing libraries:
|
10
|
+
|
11
|
+
* [Net::HTTP](http://ruby-doc.org/stdlib-2.4.2/libdoc/net/http/rdoc/Net/HTTP.html)
|
12
|
+
* [HTTP](https://github.com/httprb/http)
|
13
|
+
* [HTTPClient](https://github.com/nahi/httpclient)
|
14
|
+
* [Patron](https://github.com/toland/patron)
|
15
|
+
* [Curb](https://github.com/taf2/curb/)
|
16
|
+
* [Ethon](https://github.com/typhoeus/ethon)
|
17
|
+
* [Typhoeus](https://github.com/typhoeus/typhoeus)
|
18
|
+
|
19
|
+
|
20
|
+
<a href="https://evilmartians.com/">
|
21
|
+
<img src="https://evilmartians.com/badges/sponsored-by-evil-martians.svg" alt="Sponsored by Evil Martians" width="236" height="54"></a>
|
22
|
+
|
23
|
+
## Demo
|
24
|
+
|
25
|
+

|
6
26
|
|
7
27
|
## Installation
|
8
28
|
|
@@ -20,9 +40,94 @@ Or install it yourself as:
|
|
20
40
|
|
21
41
|
$ gem install sniffer
|
22
42
|
|
43
|
+
## Configuration
|
44
|
+
|
45
|
+
Sniffer default options:
|
46
|
+
|
47
|
+
```ruby
|
48
|
+
Sniffer.config do
|
49
|
+
logger: Logger.new($stdout),
|
50
|
+
severity: Logger::Severity::DEBUG,
|
51
|
+
# HTTP options to log
|
52
|
+
log: {
|
53
|
+
request_url: true,
|
54
|
+
request_headers: true,
|
55
|
+
request_body: true,
|
56
|
+
request_method: true,
|
57
|
+
response_status: true,
|
58
|
+
response_headers: true,
|
59
|
+
response_body: true,
|
60
|
+
timing: true
|
61
|
+
},
|
62
|
+
store: true, # save requests/responses to Sniffer.data
|
63
|
+
enabled: false # Sniffer disabled by default
|
64
|
+
end
|
65
|
+
```
|
66
|
+
|
23
67
|
## Usage
|
24
68
|
|
25
|
-
|
69
|
+
Here's some simple examples to get you started:
|
70
|
+
|
71
|
+
```ruby
|
72
|
+
require 'http'
|
73
|
+
require 'sniffer'
|
74
|
+
|
75
|
+
Sniffer.enable!
|
76
|
+
|
77
|
+
HTTP.get('http://example.com/?lang=ruby&author=matz')
|
78
|
+
Sniffer.data[0].to_h
|
79
|
+
# => {:request=>
|
80
|
+
# {:host=>"example.com",
|
81
|
+
# :query=>"/?lang=ruby&author=matz",
|
82
|
+
# :port=>80,
|
83
|
+
# :headers=>{"Accept-Encoding"=>"gzip;q=1.0,deflate;q=0.6,identity;q=0.3", "Connection"=>"close"},
|
84
|
+
# :body=>"",
|
85
|
+
# :method=>:get},
|
86
|
+
# :response=>
|
87
|
+
# {:status=>200,
|
88
|
+
# :headers=>
|
89
|
+
# {"Content-Encoding"=>"gzip",
|
90
|
+
# "Cache-Control"=>"max-age=604800",
|
91
|
+
# "Content-Type"=>"text/html",
|
92
|
+
# "Date"=>"Thu, 26 Oct 2017 13:47:00 GMT",
|
93
|
+
# "Etag"=>"\"359670651+gzip\"",
|
94
|
+
# "Expires"=>"Thu, 02 Nov 2017 13:47:00 GMT",
|
95
|
+
# "Last-Modified"=>"Fri, 09 Aug 2013 23:54:35 GMT",
|
96
|
+
# "Server"=>"ECS (lga/1372)",
|
97
|
+
# "Vary"=>"Accept-Encoding",
|
98
|
+
# "X-Cache"=>"HIT",
|
99
|
+
# "Content-Length"=>"606",
|
100
|
+
# "Connection"=>"close"},
|
101
|
+
# :body=> "OK",
|
102
|
+
# :timing=>0.23753299983218312}}
|
103
|
+
```
|
104
|
+
|
105
|
+
You can clear saved data
|
106
|
+
|
107
|
+
```
|
108
|
+
Sniffer.clear!
|
109
|
+
```
|
110
|
+
|
111
|
+
You can reset config to default
|
112
|
+
|
113
|
+
```
|
114
|
+
Sniffer.reset!
|
115
|
+
```
|
116
|
+
|
117
|
+
You can enable and disable Sniffer
|
118
|
+
|
119
|
+
```
|
120
|
+
Sniffer.enable!
|
121
|
+
Sniffer.disable!
|
122
|
+
```
|
123
|
+
|
124
|
+
By default output log looks like that:
|
125
|
+
|
126
|
+
```
|
127
|
+
D, [2017-10-26T16:47:14.007152 #59511] DEBUG -- : {"port":80,"host":"example.com","query":"/?lang=ruby&author=matz","rq_connection":"close","method":"get","request_body":"","status":200,"rs_accept_ranges":"bytes","rs_cache_control":"max-age=604800","rs_content_type":"text/html","rs_date":"Thu, 26 Oct 2017 13:47:13 GMT","rs_etag":"\"359670651+gzip\"","rs_expires":"Thu, 02 Nov 2017 13:47:13 GMT","rs_last_modified":"Fri, 09 Aug 2013 23:54:35 GMT","rs_server":"ECS (lga/1385)","rs_vary":"Accept-Encoding","rs_x_cache":"HIT","rs_content_length":"1270","rs_connection":"close","timing":0.513012999901548,"response_body":"OK"}
|
128
|
+
```
|
129
|
+
where `rq_xxx` is request header and `rs_xxx` - response header
|
130
|
+
|
26
131
|
|
27
132
|
## Development
|
28
133
|
|
data/assets/demo.gif
ADDED
Binary file
|
@@ -0,0 +1,84 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module Sniffer
|
4
|
+
module Adapters
|
5
|
+
# Curl adapter
|
6
|
+
module CurlAdapter
|
7
|
+
def self.included(base)
|
8
|
+
base.class_eval do
|
9
|
+
alias_method :http_without_sniffer, :http
|
10
|
+
alias_method :http, :http_with_sniffer
|
11
|
+
|
12
|
+
alias_method :http_post_without_sniffer, :http_post
|
13
|
+
alias_method :http_post, :http_post_with_sniffer
|
14
|
+
end
|
15
|
+
end
|
16
|
+
|
17
|
+
# rubocop:disable Metrics/AbcSize, Metrics/MethodLength
|
18
|
+
def http_with_sniffer(verb)
|
19
|
+
sniffer_request(verb)
|
20
|
+
|
21
|
+
http_without_sniffer(verb)
|
22
|
+
|
23
|
+
bm = Benchmark.realtime do
|
24
|
+
@res = http_without_sniffer(verb)
|
25
|
+
end
|
26
|
+
|
27
|
+
sniffer_response(bm)
|
28
|
+
|
29
|
+
@res
|
30
|
+
end
|
31
|
+
|
32
|
+
def http_post_with_sniffer(*args)
|
33
|
+
sniffer_request(:POST, *args)
|
34
|
+
|
35
|
+
bm = Benchmark.realtime do
|
36
|
+
@res = http_post_without_sniffer(*args)
|
37
|
+
end
|
38
|
+
|
39
|
+
sniffer_response(bm)
|
40
|
+
|
41
|
+
@res
|
42
|
+
end
|
43
|
+
|
44
|
+
private
|
45
|
+
|
46
|
+
def data_item
|
47
|
+
@data_item ||= Sniffer::DataItem.new if Sniffer.enabled?
|
48
|
+
end
|
49
|
+
|
50
|
+
def sniffer_request(verb, *args)
|
51
|
+
return unless data_item
|
52
|
+
|
53
|
+
uri = URI(url)
|
54
|
+
query = uri.path
|
55
|
+
query += "?#{uri.query}" if uri.query
|
56
|
+
|
57
|
+
data_item.request = Sniffer::DataItem::Request.new(host: uri.host,
|
58
|
+
method: verb,
|
59
|
+
query: query,
|
60
|
+
headers: headers.collect.to_h,
|
61
|
+
body: args.join("&"),
|
62
|
+
port: uri.port)
|
63
|
+
|
64
|
+
Sniffer.store(data_item)
|
65
|
+
end
|
66
|
+
|
67
|
+
def sniffer_response(timing)
|
68
|
+
return unless data_item
|
69
|
+
|
70
|
+
_, *http_headers = header_str.split(/[\r\n]+/).map(&:strip)
|
71
|
+
http_headers = Hash[http_headers.flat_map { |s| s.scan(/^(\S+): (.+)/) }]
|
72
|
+
|
73
|
+
data_item.response = Sniffer::DataItem::Response.new(status: status.to_i,
|
74
|
+
headers: http_headers,
|
75
|
+
body: body_str,
|
76
|
+
timing: timing)
|
77
|
+
|
78
|
+
data_item.log
|
79
|
+
end
|
80
|
+
end
|
81
|
+
end
|
82
|
+
end
|
83
|
+
|
84
|
+
Curl::Easy.send(:include, Sniffer::Adapters::CurlAdapter) if defined?(::Curl::Easy)
|
@@ -0,0 +1,89 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module Sniffer
|
4
|
+
module Adapters
|
5
|
+
# Ethon adapter
|
6
|
+
module EthonAdapter
|
7
|
+
# overrides http_request method
|
8
|
+
module Http
|
9
|
+
def self.included(base)
|
10
|
+
base.class_eval do
|
11
|
+
alias_method :http_request_without_sniffer, :http_request
|
12
|
+
alias_method :http_request, :http_request_with_sniffer
|
13
|
+
end
|
14
|
+
end
|
15
|
+
|
16
|
+
# rubocop:disable Metrics/AbcSize,Metrics/MethodLength
|
17
|
+
def http_request_with_sniffer(url, action_name, options = {})
|
18
|
+
if Sniffer.enabled?
|
19
|
+
@data_item = Sniffer::DataItem.new
|
20
|
+
uri = URI("http://" + url)
|
21
|
+
|
22
|
+
@data_item.request = Sniffer::DataItem::Request.new(host: uri.host,
|
23
|
+
method: action_name.upcase,
|
24
|
+
port: options[:port] || uri.port,
|
25
|
+
headers: options[:headers].to_h,
|
26
|
+
body: options[:body].to_s)
|
27
|
+
|
28
|
+
Sniffer.store(@data_item)
|
29
|
+
end
|
30
|
+
|
31
|
+
http_request_without_sniffer(url, action_name, options)
|
32
|
+
end
|
33
|
+
end
|
34
|
+
# rubocop:enable Metrics/AbcSize,Metrics/MethodLength
|
35
|
+
|
36
|
+
# overrides perform method
|
37
|
+
module Operations
|
38
|
+
def self.included(base)
|
39
|
+
base.class_eval do
|
40
|
+
alias_method :perform_without_sniffer, :perform
|
41
|
+
alias_method :perform, :perform_with_sniffer
|
42
|
+
end
|
43
|
+
end
|
44
|
+
|
45
|
+
# rubocop:disable Metrics/AbcSize,Metrics/MethodLength
|
46
|
+
def perform_with_sniffer
|
47
|
+
bm = Benchmark.realtime do
|
48
|
+
@return_code = Ethon::Curl.easy_perform(handle)
|
49
|
+
end
|
50
|
+
|
51
|
+
if Sniffer.enabled?
|
52
|
+
uri = URI("http://" + @url)
|
53
|
+
query = uri.path
|
54
|
+
query += "?#{uri.query}" if uri.query
|
55
|
+
@data_item.request.query = query
|
56
|
+
|
57
|
+
status = @response_headers.scan(%r{HTTP\/... (\d{3})}).flatten[0].to_i
|
58
|
+
hash_headers = @response_headers
|
59
|
+
.split(/\r?\n/)[1..-1]
|
60
|
+
.each_with_object({}) do |item, res|
|
61
|
+
k, v = item.split(": ")
|
62
|
+
res[k] = v
|
63
|
+
end
|
64
|
+
|
65
|
+
@data_item.response = Sniffer::DataItem::Response.new(status: status,
|
66
|
+
headers: hash_headers,
|
67
|
+
body: @response_body,
|
68
|
+
timing: bm)
|
69
|
+
@data_item.log
|
70
|
+
|
71
|
+
end
|
72
|
+
|
73
|
+
if Ethon.logger.debug?
|
74
|
+
Ethon.logger.debug { "ETHON: performed #{log_inspect}" }
|
75
|
+
end
|
76
|
+
complete
|
77
|
+
|
78
|
+
@return_code
|
79
|
+
end
|
80
|
+
# rubocop:enable Metrics/AbcSize,Metrics/MethodLength
|
81
|
+
end
|
82
|
+
end
|
83
|
+
end
|
84
|
+
end
|
85
|
+
|
86
|
+
if defined?(::Ethon::Easy)
|
87
|
+
Ethon::Easy::Http.send(:include, Sniffer::Adapters::EthonAdapter::Http)
|
88
|
+
Ethon::Easy::Operations.send(:include, Sniffer::Adapters::EthonAdapter::Operations)
|
89
|
+
end
|
@@ -0,0 +1,72 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module Sniffer
|
4
|
+
module Adapters
|
5
|
+
# HTTP adapter
|
6
|
+
module HTTPAdapter
|
7
|
+
def self.included(base)
|
8
|
+
base.class_eval do
|
9
|
+
alias_method :request_without_sniffer, :request
|
10
|
+
alias_method :request, :request_with_sniffer
|
11
|
+
end
|
12
|
+
end
|
13
|
+
|
14
|
+
# private
|
15
|
+
|
16
|
+
# rubocop:disable Metrics/AbcSize, Metrics/MethodLength
|
17
|
+
def request_with_sniffer(verb, uri, opts = {})
|
18
|
+
opts = @default_options.merge(opts)
|
19
|
+
uri = make_request_uri(uri, opts)
|
20
|
+
headers = make_request_headers(opts)
|
21
|
+
body = make_request_body(opts, headers)
|
22
|
+
proxy = opts.proxy
|
23
|
+
|
24
|
+
req = HTTP::Request.new(
|
25
|
+
verb: verb,
|
26
|
+
uri: uri,
|
27
|
+
headers: headers,
|
28
|
+
proxy: proxy,
|
29
|
+
body: body,
|
30
|
+
auto_deflate: opts.feature(:auto_deflate)
|
31
|
+
)
|
32
|
+
|
33
|
+
if Sniffer.enabled?
|
34
|
+
data_item = Sniffer::DataItem.new
|
35
|
+
query = uri.path
|
36
|
+
query += "?#{uri.query}" if uri.query
|
37
|
+
|
38
|
+
data_item.request = Sniffer::DataItem::Request.new(host: uri.host,
|
39
|
+
method: verb,
|
40
|
+
query: query,
|
41
|
+
headers: headers.collect.to_h,
|
42
|
+
body: body.to_s,
|
43
|
+
port: uri.port)
|
44
|
+
|
45
|
+
Sniffer.store(data_item)
|
46
|
+
end
|
47
|
+
|
48
|
+
bm = Benchmark.realtime do
|
49
|
+
@res = perform(req, opts)
|
50
|
+
end
|
51
|
+
|
52
|
+
if Sniffer.enabled?
|
53
|
+
data_item.response = Sniffer::DataItem::Response.new(status: @res.code,
|
54
|
+
headers: @res.headers.collect.to_h,
|
55
|
+
body: @res.body.to_s,
|
56
|
+
timing: bm)
|
57
|
+
|
58
|
+
data_item.log
|
59
|
+
end
|
60
|
+
|
61
|
+
return @res unless opts.follow
|
62
|
+
|
63
|
+
HTTP::Redirector.new(opts.follow).perform(req, @res) do |request|
|
64
|
+
perform(request, opts)
|
65
|
+
end
|
66
|
+
end
|
67
|
+
# rubocop:enable Metrics/AbcSize, Metrics/CyclomaticComplexity, Metrics/MethodLength
|
68
|
+
end
|
69
|
+
end
|
70
|
+
end
|
71
|
+
|
72
|
+
HTTP::Client.send(:include, Sniffer::Adapters::HTTPAdapter) if defined?(::HTTP::Client)
|
@@ -0,0 +1,59 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module Sniffer
|
4
|
+
module Adapters
|
5
|
+
# HttpClient adapter
|
6
|
+
module HTTPClientAdapter
|
7
|
+
def self.included(base)
|
8
|
+
base.class_eval do
|
9
|
+
alias_method :do_get_block_without_sniffer, :do_get_block
|
10
|
+
alias_method :do_get_block, :do_get_block_with_sniffer
|
11
|
+
end
|
12
|
+
end
|
13
|
+
|
14
|
+
# private
|
15
|
+
|
16
|
+
# rubocop:disable Metrics/AbcSize, Metrics/MethodLength
|
17
|
+
def do_get_block_with_sniffer(req, proxy, conn, &block)
|
18
|
+
if Sniffer.enabled?
|
19
|
+
data_item = Sniffer::DataItem.new
|
20
|
+
data_item.request = Sniffer::DataItem::Request.new(host: req.header.request_uri.host,
|
21
|
+
query: req.header.create_query_uri,
|
22
|
+
method: req.header.request_method,
|
23
|
+
headers: req.headers,
|
24
|
+
body: req.body,
|
25
|
+
port: req.header.request_uri.port)
|
26
|
+
|
27
|
+
Sniffer.store(data_item)
|
28
|
+
end
|
29
|
+
|
30
|
+
retryable_response = nil
|
31
|
+
|
32
|
+
bm = Benchmark.realtime do
|
33
|
+
begin
|
34
|
+
do_get_block_without_sniffer(req, proxy, conn, &block)
|
35
|
+
rescue HTTPClient::RetryableResponse => e
|
36
|
+
retryable_response = e
|
37
|
+
end
|
38
|
+
end
|
39
|
+
|
40
|
+
if Sniffer.enabled?
|
41
|
+
res = conn.pop
|
42
|
+
data_item.response = Sniffer::DataItem::Response.new(status: res.status_code.to_i,
|
43
|
+
headers: res.headers,
|
44
|
+
body: res.body,
|
45
|
+
timing: bm)
|
46
|
+
|
47
|
+
conn.push(res)
|
48
|
+
|
49
|
+
data_item.log
|
50
|
+
end
|
51
|
+
|
52
|
+
raise retryable_response unless retryable_response.nil?
|
53
|
+
end
|
54
|
+
# rubocop:enable Metrics/AbcSize, Metrics/CyclomaticComplexity, Metrics/MethodLength
|
55
|
+
end
|
56
|
+
end
|
57
|
+
end
|
58
|
+
|
59
|
+
HTTPClient.send(:include, Sniffer::Adapters::HTTPClientAdapter) if defined?(::HTTPClient)
|
@@ -0,0 +1,50 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require 'net/http'
|
4
|
+
require 'benchmark'
|
5
|
+
|
6
|
+
module Sniffer
|
7
|
+
module Adapters
|
8
|
+
# Net::HTTP adapter
|
9
|
+
module NetHttpAdapter
|
10
|
+
def self.included(base)
|
11
|
+
base.class_eval do
|
12
|
+
alias_method :request_without_sniffer, :request
|
13
|
+
alias_method :request, :request_with_sniffer
|
14
|
+
end
|
15
|
+
end
|
16
|
+
|
17
|
+
# rubocop:disable Metrics/AbcSize, Metrics/MethodLength
|
18
|
+
def request_with_sniffer(req, body = nil, &block)
|
19
|
+
if started? && Sniffer.enabled?
|
20
|
+
data_item = Sniffer::DataItem.new
|
21
|
+
data_item.request = Sniffer::DataItem::Request.new(host: @address,
|
22
|
+
method: req.method,
|
23
|
+
query: req.path,
|
24
|
+
port: @port,
|
25
|
+
headers: req.each_header.collect.to_h,
|
26
|
+
body: req.body.to_s)
|
27
|
+
|
28
|
+
Sniffer.store(data_item)
|
29
|
+
end
|
30
|
+
|
31
|
+
bm = Benchmark.realtime do
|
32
|
+
@response = request_without_sniffer(req, body, &block)
|
33
|
+
end
|
34
|
+
|
35
|
+
if started? && Sniffer.enabled?
|
36
|
+
data_item.response = Sniffer::DataItem::Response.new(status: @response.code.to_i,
|
37
|
+
headers: @response.each_header.collect.to_h,
|
38
|
+
body: @response.body.to_s,
|
39
|
+
timing: bm)
|
40
|
+
|
41
|
+
data_item.log
|
42
|
+
end
|
43
|
+
|
44
|
+
@response
|
45
|
+
end
|
46
|
+
# rubocop:enable Metrics/AbcSize, Metrics/CyclomaticComplexity, Metrics/MethodLength
|
47
|
+
end
|
48
|
+
end
|
49
|
+
end
|
50
|
+
Net::HTTP.send(:include, Sniffer::Adapters::NetHttpAdapter)
|
@@ -0,0 +1,49 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module Sniffer
|
4
|
+
module Adapters
|
5
|
+
# HTTP adapter
|
6
|
+
module PatronAdapter
|
7
|
+
def self.included(base)
|
8
|
+
base.class_eval do
|
9
|
+
alias_method :request_without_sniffer, :request
|
10
|
+
alias_method :request, :request_with_sniffer
|
11
|
+
end
|
12
|
+
end
|
13
|
+
|
14
|
+
# rubocop:disable Metrics/AbcSize,Metrics/MethodLength
|
15
|
+
def request_with_sniffer(action_name, url, headers, options = {})
|
16
|
+
if Sniffer.enabled?
|
17
|
+
data_item = Sniffer::DataItem.new
|
18
|
+
uri = URI(base_url)
|
19
|
+
data_item.request = Sniffer::DataItem::Request.new(host: uri.host,
|
20
|
+
method: action_name,
|
21
|
+
query: url,
|
22
|
+
headers: headers.dup,
|
23
|
+
body: options[:data].to_s,
|
24
|
+
port: uri.port)
|
25
|
+
|
26
|
+
Sniffer.store(data_item)
|
27
|
+
end
|
28
|
+
|
29
|
+
bm = Benchmark.realtime do
|
30
|
+
@res = request_without_sniffer(action_name, url, headers, options)
|
31
|
+
end
|
32
|
+
|
33
|
+
if Sniffer.enabled?
|
34
|
+
data_item.response = Sniffer::DataItem::Response.new(status: @res.status,
|
35
|
+
headers: @res.headers,
|
36
|
+
body: @res.body.to_s,
|
37
|
+
timing: bm)
|
38
|
+
|
39
|
+
data_item.log
|
40
|
+
end
|
41
|
+
|
42
|
+
@res
|
43
|
+
end
|
44
|
+
# rubocop:enable Metrics/AbcSize,Metrics/MethodLength
|
45
|
+
end
|
46
|
+
end
|
47
|
+
end
|
48
|
+
|
49
|
+
::Patron::Session.send(:include, Sniffer::Adapters::PatronAdapter) if defined?(::Patron::Session)
|
data/lib/sniffer/config.rb
CHANGED
@@ -8,13 +8,18 @@ module Sniffer
|
|
8
8
|
config_name :sniffer
|
9
9
|
|
10
10
|
attr_config logger: Logger.new($stdout),
|
11
|
-
|
12
|
-
|
13
|
-
|
14
|
-
|
15
|
-
|
16
|
-
|
17
|
-
|
18
|
-
|
11
|
+
severity: Logger::Severity::DEBUG,
|
12
|
+
log: {
|
13
|
+
request_url: true,
|
14
|
+
request_headers: true,
|
15
|
+
request_body: true,
|
16
|
+
request_method: true,
|
17
|
+
response_status: true,
|
18
|
+
response_headers: true,
|
19
|
+
response_body: true,
|
20
|
+
timing: true
|
21
|
+
},
|
22
|
+
store: true,
|
23
|
+
enabled: false
|
19
24
|
end
|
20
25
|
end
|
data/lib/sniffer/data_item.rb
CHANGED
@@ -1,9 +1,113 @@
|
|
1
1
|
# frozen_string_literal: true
|
2
2
|
|
3
|
+
require 'active_attr'
|
4
|
+
require 'json'
|
5
|
+
|
3
6
|
module Sniffer
|
4
7
|
# Sniffer data item stores a request info
|
5
8
|
class DataItem
|
6
|
-
|
7
|
-
|
9
|
+
include ActiveAttr::MassAssignment
|
10
|
+
attr_accessor :request, :response
|
11
|
+
|
12
|
+
def to_h
|
13
|
+
{
|
14
|
+
request: request&.to_h,
|
15
|
+
response: response&.to_h
|
16
|
+
}
|
17
|
+
end
|
18
|
+
|
19
|
+
def log
|
20
|
+
Sniffer.logger.log(Sniffer.config.severity, to_json)
|
21
|
+
end
|
22
|
+
|
23
|
+
def to_log
|
24
|
+
return {} unless Sniffer.config.logger
|
25
|
+
request.to_log.merge(response.to_log)
|
26
|
+
end
|
27
|
+
|
28
|
+
def to_json
|
29
|
+
to_log.to_json
|
30
|
+
end
|
31
|
+
|
32
|
+
# Basic object for request and response objects
|
33
|
+
class HttpObject
|
34
|
+
include ActiveAttr::MassAssignment
|
35
|
+
|
36
|
+
def log_message
|
37
|
+
raise NotImplementedError
|
38
|
+
end
|
39
|
+
|
40
|
+
def log_settings
|
41
|
+
Sniffer.config.log || {}
|
42
|
+
end
|
43
|
+
end
|
44
|
+
|
45
|
+
# Stores http request data
|
46
|
+
class Request < HttpObject
|
47
|
+
attr_accessor :host, :port, :query, :method, :headers, :body
|
48
|
+
|
49
|
+
def to_h
|
50
|
+
{
|
51
|
+
host: host,
|
52
|
+
query: query,
|
53
|
+
port: port,
|
54
|
+
headers: headers,
|
55
|
+
body: body,
|
56
|
+
method: method
|
57
|
+
}
|
58
|
+
end
|
59
|
+
|
60
|
+
# rubocop:disable Metrics/AbcSize,Metrics/MethodLength
|
61
|
+
def to_log
|
62
|
+
{}.tap do |hash|
|
63
|
+
if log_settings["request_url"]
|
64
|
+
hash[:port] = port
|
65
|
+
hash[:host] = host
|
66
|
+
hash[:query] = query
|
67
|
+
end
|
68
|
+
|
69
|
+
if log_settings["request_headers"]
|
70
|
+
headers.each do |(k, v)|
|
71
|
+
hash[:"rq_#{k.to_s.tr("-", '_').downcase}"] = v
|
72
|
+
end
|
73
|
+
end
|
74
|
+
|
75
|
+
hash[:method] = method if log_settings["request_method"]
|
76
|
+
hash[:request_body] = body if log_settings["request_body"]
|
77
|
+
end
|
78
|
+
end
|
79
|
+
end
|
80
|
+
# rubocop:enable Metrics/AbcSize,Metrics/MethodLength
|
81
|
+
|
82
|
+
# Stores http response data
|
83
|
+
class Response < HttpObject
|
84
|
+
attr_accessor :status, :headers, :body, :timing
|
85
|
+
|
86
|
+
def to_h
|
87
|
+
{
|
88
|
+
status: status,
|
89
|
+
headers: headers,
|
90
|
+
body: body,
|
91
|
+
timing: timing
|
92
|
+
}
|
93
|
+
end
|
94
|
+
|
95
|
+
# rubocop:disable Metrics/AbcSize
|
96
|
+
def to_log
|
97
|
+
{}.tap do |hash|
|
98
|
+
hash[:status] = status if log_settings["response_status"]
|
99
|
+
|
100
|
+
if log_settings["response_headers"]
|
101
|
+
headers.each do |(k, v)|
|
102
|
+
hash[:"rs_#{k.to_s.tr("-", '_').downcase}"] = v
|
103
|
+
end
|
104
|
+
end
|
105
|
+
|
106
|
+
hash[:timing] = timing if log_settings["timing"]
|
107
|
+
hash[:response_body] = body if log_settings["response_body"]
|
108
|
+
end
|
109
|
+
end
|
110
|
+
# rubocop:enable Metrics/AbcSize
|
111
|
+
end
|
8
112
|
end
|
9
113
|
end
|
data/lib/sniffer/version.rb
CHANGED
data/lib/sniffer.rb
CHANGED
@@ -2,15 +2,33 @@
|
|
2
2
|
|
3
3
|
require "logger"
|
4
4
|
|
5
|
-
|
6
|
-
|
7
|
-
|
5
|
+
require_relative "sniffer/version"
|
6
|
+
require_relative "sniffer/config"
|
7
|
+
require_relative "sniffer/data_item"
|
8
8
|
|
9
9
|
# Sniffer allows to log http requests
|
10
10
|
module Sniffer
|
11
|
+
@data = Set.new
|
12
|
+
|
11
13
|
class << self
|
14
|
+
attr_reader :data
|
15
|
+
|
12
16
|
def config
|
13
17
|
@config ||= Config.new
|
18
|
+
yield @config if block_given?
|
19
|
+
@config
|
20
|
+
end
|
21
|
+
|
22
|
+
def enable!
|
23
|
+
config.enabled = true
|
24
|
+
end
|
25
|
+
|
26
|
+
def disable!
|
27
|
+
config.enabled = false
|
28
|
+
end
|
29
|
+
|
30
|
+
def enabled?
|
31
|
+
config.enabled
|
14
32
|
end
|
15
33
|
|
16
34
|
def configure
|
@@ -18,16 +36,27 @@ module Sniffer
|
|
18
36
|
end
|
19
37
|
|
20
38
|
def clear!
|
21
|
-
|
39
|
+
data.clear
|
22
40
|
end
|
23
41
|
|
24
|
-
def
|
25
|
-
@
|
42
|
+
def reset!
|
43
|
+
@config = Config.new
|
44
|
+
clear!
|
26
45
|
end
|
27
46
|
|
28
47
|
def store(data_item)
|
29
|
-
data
|
30
|
-
|
48
|
+
@data.add(data_item) if config.store
|
49
|
+
end
|
50
|
+
|
51
|
+
def logger
|
52
|
+
config.logger
|
31
53
|
end
|
32
54
|
end
|
33
55
|
end
|
56
|
+
|
57
|
+
require_relative "sniffer/adapters/net_http_adapter"
|
58
|
+
require_relative "sniffer/adapters/httpclient_adapter"
|
59
|
+
require_relative "sniffer/adapters/http_adapter"
|
60
|
+
require_relative "sniffer/adapters/patron_adapter"
|
61
|
+
require_relative "sniffer/adapters/curb_adapter"
|
62
|
+
require_relative "sniffer/adapters/ethon_adapter"
|
data/sniffer.gemspec
CHANGED
@@ -17,14 +17,22 @@ Gem::Specification.new do |spec|
|
|
17
17
|
spec.files = `git ls-files -z`.split("\x0").reject do |f|
|
18
18
|
f.match(%r{^(test|spec|features)/})
|
19
19
|
end
|
20
|
-
spec.bindir = "exe"
|
21
|
-
spec.executables = spec.files.grep(%r{^exe/}) { |f| File.basename(f) }
|
22
20
|
spec.require_paths = ["lib"]
|
23
21
|
|
24
22
|
spec.add_dependency "anyway_config", "~> 1.0"
|
23
|
+
spec.add_dependency "active_attr", ">= 0.10.2"
|
25
24
|
|
26
25
|
spec.add_development_dependency "bundler", "~> 1.15"
|
27
26
|
spec.add_development_dependency "rake", "~> 10.0"
|
28
27
|
spec.add_development_dependency "rspec", "~> 3.0"
|
29
28
|
spec.add_development_dependency "rubocop", "~> 0.50"
|
29
|
+
spec.add_development_dependency "pry-byebug"
|
30
|
+
spec.add_development_dependency "sinatra", "~> 2.0"
|
31
|
+
spec.add_development_dependency "puma", ">= 3.10.0"
|
32
|
+
spec.add_development_dependency "httpclient", ">= 2.8.3"
|
33
|
+
spec.add_development_dependency "http", ">= 3.0.0"
|
34
|
+
spec.add_development_dependency "patron", ">= 0.10.0"
|
35
|
+
spec.add_development_dependency "curb", ">= 0.9.4"
|
36
|
+
spec.add_development_dependency "ethon", ">= 0.11.0"
|
37
|
+
spec.add_development_dependency "typhoeus", ">= 0.9.0"
|
30
38
|
end
|
metadata
CHANGED
@@ -1,14 +1,14 @@
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
2
2
|
name: sniffer
|
3
3
|
version: !ruby/object:Gem::Version
|
4
|
-
version: 0.0
|
4
|
+
version: 0.1.0
|
5
5
|
platform: ruby
|
6
6
|
authors:
|
7
7
|
- Andrey Deryabin
|
8
8
|
autorequire:
|
9
|
-
bindir:
|
9
|
+
bindir: bin
|
10
10
|
cert_chain: []
|
11
|
-
date: 2017-
|
11
|
+
date: 2017-11-14 00:00:00.000000000 Z
|
12
12
|
dependencies:
|
13
13
|
- !ruby/object:Gem::Dependency
|
14
14
|
name: anyway_config
|
@@ -24,6 +24,20 @@ dependencies:
|
|
24
24
|
- - "~>"
|
25
25
|
- !ruby/object:Gem::Version
|
26
26
|
version: '1.0'
|
27
|
+
- !ruby/object:Gem::Dependency
|
28
|
+
name: active_attr
|
29
|
+
requirement: !ruby/object:Gem::Requirement
|
30
|
+
requirements:
|
31
|
+
- - ">="
|
32
|
+
- !ruby/object:Gem::Version
|
33
|
+
version: 0.10.2
|
34
|
+
type: :runtime
|
35
|
+
prerelease: false
|
36
|
+
version_requirements: !ruby/object:Gem::Requirement
|
37
|
+
requirements:
|
38
|
+
- - ">="
|
39
|
+
- !ruby/object:Gem::Version
|
40
|
+
version: 0.10.2
|
27
41
|
- !ruby/object:Gem::Dependency
|
28
42
|
name: bundler
|
29
43
|
requirement: !ruby/object:Gem::Requirement
|
@@ -80,6 +94,132 @@ dependencies:
|
|
80
94
|
- - "~>"
|
81
95
|
- !ruby/object:Gem::Version
|
82
96
|
version: '0.50'
|
97
|
+
- !ruby/object:Gem::Dependency
|
98
|
+
name: pry-byebug
|
99
|
+
requirement: !ruby/object:Gem::Requirement
|
100
|
+
requirements:
|
101
|
+
- - ">="
|
102
|
+
- !ruby/object:Gem::Version
|
103
|
+
version: '0'
|
104
|
+
type: :development
|
105
|
+
prerelease: false
|
106
|
+
version_requirements: !ruby/object:Gem::Requirement
|
107
|
+
requirements:
|
108
|
+
- - ">="
|
109
|
+
- !ruby/object:Gem::Version
|
110
|
+
version: '0'
|
111
|
+
- !ruby/object:Gem::Dependency
|
112
|
+
name: sinatra
|
113
|
+
requirement: !ruby/object:Gem::Requirement
|
114
|
+
requirements:
|
115
|
+
- - "~>"
|
116
|
+
- !ruby/object:Gem::Version
|
117
|
+
version: '2.0'
|
118
|
+
type: :development
|
119
|
+
prerelease: false
|
120
|
+
version_requirements: !ruby/object:Gem::Requirement
|
121
|
+
requirements:
|
122
|
+
- - "~>"
|
123
|
+
- !ruby/object:Gem::Version
|
124
|
+
version: '2.0'
|
125
|
+
- !ruby/object:Gem::Dependency
|
126
|
+
name: puma
|
127
|
+
requirement: !ruby/object:Gem::Requirement
|
128
|
+
requirements:
|
129
|
+
- - ">="
|
130
|
+
- !ruby/object:Gem::Version
|
131
|
+
version: 3.10.0
|
132
|
+
type: :development
|
133
|
+
prerelease: false
|
134
|
+
version_requirements: !ruby/object:Gem::Requirement
|
135
|
+
requirements:
|
136
|
+
- - ">="
|
137
|
+
- !ruby/object:Gem::Version
|
138
|
+
version: 3.10.0
|
139
|
+
- !ruby/object:Gem::Dependency
|
140
|
+
name: httpclient
|
141
|
+
requirement: !ruby/object:Gem::Requirement
|
142
|
+
requirements:
|
143
|
+
- - ">="
|
144
|
+
- !ruby/object:Gem::Version
|
145
|
+
version: 2.8.3
|
146
|
+
type: :development
|
147
|
+
prerelease: false
|
148
|
+
version_requirements: !ruby/object:Gem::Requirement
|
149
|
+
requirements:
|
150
|
+
- - ">="
|
151
|
+
- !ruby/object:Gem::Version
|
152
|
+
version: 2.8.3
|
153
|
+
- !ruby/object:Gem::Dependency
|
154
|
+
name: http
|
155
|
+
requirement: !ruby/object:Gem::Requirement
|
156
|
+
requirements:
|
157
|
+
- - ">="
|
158
|
+
- !ruby/object:Gem::Version
|
159
|
+
version: 3.0.0
|
160
|
+
type: :development
|
161
|
+
prerelease: false
|
162
|
+
version_requirements: !ruby/object:Gem::Requirement
|
163
|
+
requirements:
|
164
|
+
- - ">="
|
165
|
+
- !ruby/object:Gem::Version
|
166
|
+
version: 3.0.0
|
167
|
+
- !ruby/object:Gem::Dependency
|
168
|
+
name: patron
|
169
|
+
requirement: !ruby/object:Gem::Requirement
|
170
|
+
requirements:
|
171
|
+
- - ">="
|
172
|
+
- !ruby/object:Gem::Version
|
173
|
+
version: 0.10.0
|
174
|
+
type: :development
|
175
|
+
prerelease: false
|
176
|
+
version_requirements: !ruby/object:Gem::Requirement
|
177
|
+
requirements:
|
178
|
+
- - ">="
|
179
|
+
- !ruby/object:Gem::Version
|
180
|
+
version: 0.10.0
|
181
|
+
- !ruby/object:Gem::Dependency
|
182
|
+
name: curb
|
183
|
+
requirement: !ruby/object:Gem::Requirement
|
184
|
+
requirements:
|
185
|
+
- - ">="
|
186
|
+
- !ruby/object:Gem::Version
|
187
|
+
version: 0.9.4
|
188
|
+
type: :development
|
189
|
+
prerelease: false
|
190
|
+
version_requirements: !ruby/object:Gem::Requirement
|
191
|
+
requirements:
|
192
|
+
- - ">="
|
193
|
+
- !ruby/object:Gem::Version
|
194
|
+
version: 0.9.4
|
195
|
+
- !ruby/object:Gem::Dependency
|
196
|
+
name: ethon
|
197
|
+
requirement: !ruby/object:Gem::Requirement
|
198
|
+
requirements:
|
199
|
+
- - ">="
|
200
|
+
- !ruby/object:Gem::Version
|
201
|
+
version: 0.11.0
|
202
|
+
type: :development
|
203
|
+
prerelease: false
|
204
|
+
version_requirements: !ruby/object:Gem::Requirement
|
205
|
+
requirements:
|
206
|
+
- - ">="
|
207
|
+
- !ruby/object:Gem::Version
|
208
|
+
version: 0.11.0
|
209
|
+
- !ruby/object:Gem::Dependency
|
210
|
+
name: typhoeus
|
211
|
+
requirement: !ruby/object:Gem::Requirement
|
212
|
+
requirements:
|
213
|
+
- - ">="
|
214
|
+
- !ruby/object:Gem::Version
|
215
|
+
version: 0.9.0
|
216
|
+
type: :development
|
217
|
+
prerelease: false
|
218
|
+
version_requirements: !ruby/object:Gem::Requirement
|
219
|
+
requirements:
|
220
|
+
- - ">="
|
221
|
+
- !ruby/object:Gem::Version
|
222
|
+
version: 0.9.0
|
83
223
|
description: Analyze HTTP Requests
|
84
224
|
email:
|
85
225
|
- aderyabin@evilmartians.com
|
@@ -97,9 +237,16 @@ files:
|
|
97
237
|
- LICENSE.txt
|
98
238
|
- README.md
|
99
239
|
- Rakefile
|
240
|
+
- assets/demo.gif
|
100
241
|
- bin/console
|
101
242
|
- bin/setup
|
102
243
|
- lib/sniffer.rb
|
244
|
+
- lib/sniffer/adapters/curb_adapter.rb
|
245
|
+
- lib/sniffer/adapters/ethon_adapter.rb
|
246
|
+
- lib/sniffer/adapters/http_adapter.rb
|
247
|
+
- lib/sniffer/adapters/httpclient_adapter.rb
|
248
|
+
- lib/sniffer/adapters/net_http_adapter.rb
|
249
|
+
- lib/sniffer/adapters/patron_adapter.rb
|
103
250
|
- lib/sniffer/config.rb
|
104
251
|
- lib/sniffer/data_item.rb
|
105
252
|
- lib/sniffer/version.rb
|