sniffer 0.0.1 → 0.1.0
Sign up to get free protection for your applications and to get access to all the features.
- 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 [![Build Status](https://travis-ci.org/aderyabin/sniffer.svg?branch=master)](https://travis-ci.org/aderyabin/sniffer) [![Gem Version](https://badge.fury.io/rb/sniffer.svg)](https://rubygems.org/gems/sniffer) [![Maintainability](https://api.codeclimate.com/v1/badges/640cb17b3d748a49653f/maintainability)](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
|
+
![demo](https://github.com/aderyabin/sniffer/blob/master/assets/demo.gif?raw=true)
|
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
|