api_hammer 0.8.1 → 0.9.2
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/CHANGELOG.md +11 -0
- data/lib/api_hammer/content_type_attrs.rb +67 -0
- data/lib/api_hammer/faraday/request_logger.rb +84 -122
- data/lib/api_hammer/filtration/form_encoded.rb +33 -0
- data/lib/api_hammer/filtration/json.rb +137 -0
- data/lib/api_hammer/parsed_body.rb +39 -0
- data/lib/api_hammer/rails_request_logging.rb +2 -1
- data/lib/api_hammer/request_logger.rb +17 -10
- data/lib/api_hammer/version.rb +1 -1
- data/lib/api_hammer.rb +6 -0
- data/lib/logstash/filters/api_hammer_request.rb +1 -0
- data/lib/logstash/filters/oauthenticator.rb +45 -0
- data/lib/logstash/filters/request_bodies_parsed.rb +4 -9
- data/test/faraday_request_logger_test.rb +88 -1
- data/test/request_logger_test.rb +74 -0
- metadata +7 -2
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA1:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: 25094fc3ff153feb9e2ac93c7c25a0e46d3143eb
|
4
|
+
data.tar.gz: 42cbb9d5226111853163fd94043cd33ee16eee70
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: f2b197a3c690c2e77a5cbebfcca7b6f98cbc1c4e5d7883a2cc1af545934be7433e58c5910eeffb77110e1da308a8753b993d5d2c683999192e18b178b4f26701
|
7
|
+
data.tar.gz: 351a18b0ac2e23726b7614743f7097402a17330d80a30a81fa2cb418791bd8a598d682eba94947ce9529d92d1bcdc02b5f4f25d99bc43a60ab2b769c177211d7
|
data/CHANGELOG.md
CHANGED
@@ -1,3 +1,14 @@
|
|
1
|
+
# v0.9.2
|
2
|
+
- bugfix form encoded filtering
|
3
|
+
|
4
|
+
# v0.9.1
|
5
|
+
- recognize `app.config.api_hammer_request_logging_options` for request logger options
|
6
|
+
|
7
|
+
# v0.9.0
|
8
|
+
- rack request logger logs ids in arrays of hashes when logging ids
|
9
|
+
- filtered logging of sensitive keys in bodies of requests (json and form encoded)
|
10
|
+
- logstash filter for oauth headers and oauthenticator log entries
|
11
|
+
|
1
12
|
# v0.8.1
|
2
13
|
- request log format tweaks
|
3
14
|
|
@@ -0,0 +1,67 @@
|
|
1
|
+
module ApiHammer
|
2
|
+
# parses attributes out of content type header
|
3
|
+
class ContentTypeAttrs
|
4
|
+
def initialize(content_type)
|
5
|
+
@media_type = content_type.split(/\s*[;]\s*/, 2).first if content_type
|
6
|
+
@media_type.strip! if @media_type
|
7
|
+
@content_type = content_type
|
8
|
+
@parsed = false
|
9
|
+
@attributes = Hash.new { |h,k| h[k] = [] }
|
10
|
+
catch(:unparseable) do
|
11
|
+
throw(:unparseable) unless content_type
|
12
|
+
uri_parser = URI.const_defined?(:Parser) ? URI::Parser.new : URI
|
13
|
+
scanner = StringScanner.new(content_type)
|
14
|
+
scanner.scan(/.*;\s*/) || throw(:unparseable)
|
15
|
+
while match = scanner.scan(/(\w+)=("?)([^"]*)("?)\s*(,?)\s*/)
|
16
|
+
key = scanner[1]
|
17
|
+
quote1 = scanner[2]
|
18
|
+
value = scanner[3]
|
19
|
+
quote2 = scanner[4]
|
20
|
+
comma_follows = !scanner[5].empty?
|
21
|
+
throw(:unparseable) unless quote1 == quote2
|
22
|
+
throw(:unparseable) if !comma_follows && !scanner.eos?
|
23
|
+
@attributes[uri_parser.unescape(key)] << uri_parser.unescape(value)
|
24
|
+
end
|
25
|
+
throw(:unparseable) unless scanner.eos?
|
26
|
+
@parsed = true
|
27
|
+
end
|
28
|
+
end
|
29
|
+
|
30
|
+
attr_reader :media_type
|
31
|
+
|
32
|
+
def parsed?
|
33
|
+
@parsed
|
34
|
+
end
|
35
|
+
|
36
|
+
def [](key)
|
37
|
+
@attributes[key]
|
38
|
+
end
|
39
|
+
|
40
|
+
def text?
|
41
|
+
# ordered hash by priority mapping types to binary or text
|
42
|
+
# regexps will have \A and \z added
|
43
|
+
types = {
|
44
|
+
%r(image/.*) => :binary,
|
45
|
+
%r(audio/.*) => :binary,
|
46
|
+
%r(video/.*) => :binary,
|
47
|
+
%r(model/.*) => :binary,
|
48
|
+
%r(text/.*) => :text,
|
49
|
+
%r(message/.*) => :text,
|
50
|
+
'application/octet-stream' => :binary,
|
51
|
+
'application/ogg' => :binary,
|
52
|
+
'application/pdf' => :binary,
|
53
|
+
'application/postscript' => :binary,
|
54
|
+
'application/zip' => :binary,
|
55
|
+
'application/gzip' => :binary,
|
56
|
+
}
|
57
|
+
types.each do |match, type|
|
58
|
+
matched = match.is_a?(Regexp) ? media_type =~ %r(\A#{match.source}\z) : media_type == match
|
59
|
+
if matched
|
60
|
+
return type == :text
|
61
|
+
end
|
62
|
+
end
|
63
|
+
# fallback (unknown or not given) assume text
|
64
|
+
return true
|
65
|
+
end
|
66
|
+
end
|
67
|
+
end
|
@@ -1,57 +1,86 @@
|
|
1
1
|
require 'faraday'
|
2
|
-
require 'rack'
|
3
2
|
require 'term/ansicolor'
|
4
3
|
require 'json'
|
5
4
|
require 'strscan'
|
6
5
|
|
7
6
|
module ApiHammer
|
8
|
-
|
9
|
-
|
10
|
-
|
11
|
-
|
12
|
-
|
13
|
-
@content_type = content_type
|
14
|
-
@parsed = false
|
15
|
-
@attributes = Hash.new { |h,k| h[k] = [] }
|
16
|
-
catch(:unparseable) do
|
17
|
-
throw(:unparseable) unless content_type
|
18
|
-
uri_parser = URI.const_defined?(:Parser) ? URI::Parser.new : URI
|
19
|
-
scanner = StringScanner.new(content_type)
|
20
|
-
scanner.scan(/.*;\s*/) || throw(:unparseable)
|
21
|
-
while match = scanner.scan(/(\w+)=("?)([^"]*)("?)\s*(,?)\s*/)
|
22
|
-
key = scanner[1]
|
23
|
-
quote1 = scanner[2]
|
24
|
-
value = scanner[3]
|
25
|
-
quote2 = scanner[4]
|
26
|
-
comma_follows = !scanner[5].empty?
|
27
|
-
throw(:unparseable) unless quote1 == quote2
|
28
|
-
throw(:unparseable) if !comma_follows && !scanner.eos?
|
29
|
-
@attributes[uri_parser.unescape(key)] << uri_parser.unescape(value)
|
30
|
-
end
|
31
|
-
throw(:unparseable) unless scanner.eos?
|
32
|
-
@parsed = true
|
7
|
+
module Faraday
|
8
|
+
class Request
|
9
|
+
def initialize(request_env, response_env)
|
10
|
+
@request_env = request_env
|
11
|
+
@response_env = response_env
|
33
12
|
end
|
34
|
-
end
|
35
13
|
|
36
|
-
|
14
|
+
attr_reader :request_env
|
15
|
+
attr_reader :response_env
|
37
16
|
|
38
|
-
|
39
|
-
|
40
|
-
|
17
|
+
# deal with the vagaries of getting the response body in a form which JSON
|
18
|
+
# gem will not cry about generating
|
19
|
+
def response_body
|
20
|
+
instance_variable_defined?(:@response_body) ? @response_body : @response_body = catch(:response_body) do
|
21
|
+
unless response_env.body.is_a?(String)
|
22
|
+
begin
|
23
|
+
# if the response body is not a string, but JSON doesn't complain
|
24
|
+
# about dumping whatever it is, go ahead and use it
|
25
|
+
JSON.generate([response_env.body])
|
26
|
+
throw :response_body, response_env.body
|
27
|
+
rescue
|
28
|
+
# otherwise return nil - don't know what to do with whatever this object is
|
29
|
+
throw :response_body, nil
|
30
|
+
end
|
31
|
+
end
|
32
|
+
|
33
|
+
# first try to change the string's encoding per the Content-Type header
|
34
|
+
content_type = response_env.response_headers['Content-Type']
|
35
|
+
response_body = response_env.body.dup
|
36
|
+
unless response_body.valid_encoding?
|
37
|
+
# I think this always comes in as ASCII-8BIT anyway so may never get here. hopefully.
|
38
|
+
response_body.force_encoding('ASCII-8BIT')
|
39
|
+
end
|
41
40
|
|
42
|
-
|
43
|
-
|
41
|
+
content_type_attrs = ContentTypeAttrs.new(content_type)
|
42
|
+
if content_type_attrs.parsed?
|
43
|
+
charset = content_type_attrs['charset'].first
|
44
|
+
if charset && Encoding.list.any? { |enc| enc.to_s.downcase == charset.downcase }
|
45
|
+
if response_body.dup.force_encoding(charset).valid_encoding?
|
46
|
+
response_body.force_encoding(charset)
|
47
|
+
else
|
48
|
+
# I guess just ignore the specified encoding if the result is not valid. fall back to
|
49
|
+
# something else below.
|
50
|
+
end
|
51
|
+
end
|
52
|
+
end
|
53
|
+
begin
|
54
|
+
JSON.generate([response_body])
|
55
|
+
rescue Encoding::UndefinedConversionError
|
56
|
+
# if updating by content-type didn't do it, try UTF8 since JSON wants that - but only
|
57
|
+
# if it seems to be valid utf8.
|
58
|
+
# don't try utf8 if the response content-type indicated something else.
|
59
|
+
try_utf8 = !(content_type_attrs && content_type_attrs.parsed? && content_type_attrs['charset'].any?)
|
60
|
+
if try_utf8 && response_body.dup.force_encoding('UTF-8').valid_encoding?
|
61
|
+
response_body.force_encoding('UTF-8')
|
62
|
+
else
|
63
|
+
# I'm not sure if there is a way in this situation to get JSON gem to generate the
|
64
|
+
# string correctly. fall back to an array of codepoints I guess? this is a weird
|
65
|
+
# solution but the best I've got for now.
|
66
|
+
response_body = response_body.codepoints.to_a
|
67
|
+
end
|
68
|
+
end
|
69
|
+
response_body
|
70
|
+
end
|
71
|
+
end
|
44
72
|
end
|
45
|
-
end
|
46
73
|
|
47
|
-
module Faraday
|
48
74
|
# Faraday middleware for logging.
|
49
75
|
#
|
50
|
-
# two lines:
|
76
|
+
# logs two lines:
|
51
77
|
#
|
52
78
|
# - an info line, colored prettily to show a brief summary of the request and response
|
53
79
|
# - a debug line of json to record all relevant info. this is a lot of stuff jammed into one line, not
|
54
80
|
# pretty, but informative.
|
81
|
+
#
|
82
|
+
# options:
|
83
|
+
# - :filter_keys defines keys whose values will be filtered out of the logging
|
55
84
|
class RequestLogger < ::Faraday::Middleware
|
56
85
|
include Term::ANSIColor
|
57
86
|
|
@@ -61,89 +90,6 @@ module ApiHammer
|
|
61
90
|
@options = options
|
62
91
|
end
|
63
92
|
|
64
|
-
# deal with the vagaries of getting the response body in a form which JSON
|
65
|
-
# gem will not cry about dumping
|
66
|
-
def response_body(response_env)
|
67
|
-
unless response_env.body.is_a?(String)
|
68
|
-
begin
|
69
|
-
# if the response body is not a string, but JSON doesn't complain
|
70
|
-
# about dumping whatever it is, go ahead and use it
|
71
|
-
JSON.dump([response_env.body])
|
72
|
-
return response_env.body
|
73
|
-
rescue
|
74
|
-
# otherwise return nil - don't know what to do with whatever this object is
|
75
|
-
return nil
|
76
|
-
end
|
77
|
-
end
|
78
|
-
|
79
|
-
# first try to change the string's encoding per the Content-Type header
|
80
|
-
content_type = response_env.response_headers['Content-Type']
|
81
|
-
response_body = response_env.body.dup
|
82
|
-
unless response_body.valid_encoding?
|
83
|
-
# I think this always comes in as ASCII-8BIT anyway so may never get here. hopefully.
|
84
|
-
response_body.force_encoding('ASCII-8BIT')
|
85
|
-
end
|
86
|
-
|
87
|
-
content_type_attrs = ContentTypeAttrs.new(content_type)
|
88
|
-
if content_type_attrs.parsed?
|
89
|
-
charset = content_type_attrs['charset'].first
|
90
|
-
if charset && Encoding.list.any? { |enc| enc.to_s.downcase == charset.downcase }
|
91
|
-
if response_body.dup.force_encoding(charset).valid_encoding?
|
92
|
-
response_body.force_encoding(charset)
|
93
|
-
else
|
94
|
-
# I guess just ignore the specified encoding if the result is not valid. fall back to
|
95
|
-
# something else below.
|
96
|
-
end
|
97
|
-
end
|
98
|
-
end
|
99
|
-
begin
|
100
|
-
JSON.dump([response_body])
|
101
|
-
rescue Encoding::UndefinedConversionError
|
102
|
-
# if updating by content-type didn't do it, try UTF8 since JSON wants that - but only
|
103
|
-
# if it seems to be valid utf8.
|
104
|
-
# don't try utf8 if the response content-type indicated something else.
|
105
|
-
try_utf8 = !(content_type_attrs && content_type_attrs.parsed? && content_type_attrs['charset'].any?)
|
106
|
-
if try_utf8 && response_body.dup.force_encoding('UTF-8').valid_encoding?
|
107
|
-
response_body.force_encoding('UTF-8')
|
108
|
-
else
|
109
|
-
# I'm not sure if there is a way in this situation to get JSON gem to dump the
|
110
|
-
# string correctly. fall back to an array of codepoints I guess? this is a weird
|
111
|
-
# solution but the best I've got for now.
|
112
|
-
response_body = response_body.codepoints.to_a
|
113
|
-
end
|
114
|
-
end
|
115
|
-
response_body
|
116
|
-
end
|
117
|
-
|
118
|
-
def text?(content_type)
|
119
|
-
content_type_attrs = ContentTypeAttrs.new(content_type)
|
120
|
-
media_type = content_type_attrs.media_type
|
121
|
-
# ordered hash by priority mapping types to binary or text
|
122
|
-
# regexps will have \A and \z added
|
123
|
-
types = {
|
124
|
-
%r(image/.*) => :binary,
|
125
|
-
%r(audio/.*) => :binary,
|
126
|
-
%r(video/.*) => :binary,
|
127
|
-
%r(model/.*) => :binary,
|
128
|
-
%r(text/.*) => :text,
|
129
|
-
%r(message/.*) => :text,
|
130
|
-
'application/octet-stream' => :binary,
|
131
|
-
'application/ogg' => :binary,
|
132
|
-
'application/pdf' => :binary,
|
133
|
-
'application/postscript' => :binary,
|
134
|
-
'application/zip' => :binary,
|
135
|
-
'application/gzip' => :binary,
|
136
|
-
}
|
137
|
-
types.each do |match, type|
|
138
|
-
matched = match.is_a?(Regexp) ? media_type =~ %r(\A#{match.source}\z) : media_type == match
|
139
|
-
if matched
|
140
|
-
return type == :text
|
141
|
-
end
|
142
|
-
end
|
143
|
-
# fallback (unknown or not given) assume text
|
144
|
-
return true
|
145
|
-
end
|
146
|
-
|
147
93
|
def call(request_env)
|
148
94
|
began_at = Time.now
|
149
95
|
|
@@ -155,6 +101,8 @@ module ApiHammer
|
|
155
101
|
@app.call(request_env).on_complete do |response_env|
|
156
102
|
now = Time.now
|
157
103
|
|
104
|
+
request = ApiHammer::Faraday::Request.new(request_env, response_env)
|
105
|
+
|
158
106
|
status_color = case response_env.status.to_i
|
159
107
|
when 200..299
|
160
108
|
:intense_green
|
@@ -166,18 +114,32 @@ module ApiHammer
|
|
166
114
|
:white
|
167
115
|
end
|
168
116
|
status_s = bold(send(status_color, response_env.status.to_s))
|
117
|
+
|
118
|
+
filtered_request_body = request_body.dup if request_body
|
119
|
+
filtered_response_body = request.response_body.dup if request.response_body
|
120
|
+
|
121
|
+
if @options[:filter_keys]
|
122
|
+
body_info = [['request', filtered_request_body, request_env.request_headers], ['response', filtered_response_body, response_env.response_headers]]
|
123
|
+
body_info.map do |(role, body, headers)|
|
124
|
+
if body
|
125
|
+
parsed_body = ApiHammer::ParsedBody.new(body, headers['Content-Type'])
|
126
|
+
body.replace(parsed_body.filtered_body(@options.slice(:filter_keys)))
|
127
|
+
end
|
128
|
+
end
|
129
|
+
end
|
130
|
+
|
169
131
|
data = {
|
170
132
|
'request_role' => 'client',
|
171
133
|
'request' => {
|
172
134
|
'method' => request_env[:method],
|
173
135
|
'uri' => request_env[:url].normalize.to_s,
|
174
136
|
'headers' => request_env.request_headers,
|
175
|
-
'body' => (
|
137
|
+
'body' => (filtered_request_body if ContentTypeAttrs.new(request_env.request_headers['Content-Type']).text?),
|
176
138
|
}.reject{|k,v| v.nil? },
|
177
139
|
'response' => {
|
178
140
|
'status' => response_env.status.to_s,
|
179
141
|
'headers' => response_env.response_headers,
|
180
|
-
'body' => (
|
142
|
+
'body' => (filtered_response_body if ContentTypeAttrs.new(response_env.response_headers['Content-Type']).text?),
|
181
143
|
}.reject{|k,v| v.nil? },
|
182
144
|
'processing' => {
|
183
145
|
'began_at' => began_at.utc.to_f,
|
@@ -186,7 +148,7 @@ module ApiHammer
|
|
186
148
|
}.reject{|k,v| v.nil? },
|
187
149
|
}
|
188
150
|
|
189
|
-
json_data = JSON.
|
151
|
+
json_data = JSON.generate(data)
|
190
152
|
dolog = proc do
|
191
153
|
now_s = now.strftime('%Y-%m-%d %H:%M:%S %Z')
|
192
154
|
@logger.info "#{bold(intense_magenta('>'))} #{status_s} : #{bold(intense_magenta(request_env[:method].to_s.upcase))} #{intense_magenta(request_env[:url].normalize.to_s)} @ #{intense_magenta(now_s)}"
|
@@ -0,0 +1,33 @@
|
|
1
|
+
require 'strscan'
|
2
|
+
|
3
|
+
module ApiHammer
|
4
|
+
module Filtration
|
5
|
+
class FormEncoded
|
6
|
+
def initialize(string, options = {})
|
7
|
+
@string = string
|
8
|
+
@options = options
|
9
|
+
end
|
10
|
+
|
11
|
+
def filter
|
12
|
+
ss = StringScanner.new(@string)
|
13
|
+
filtered = ''
|
14
|
+
while !ss.eos?
|
15
|
+
if ss.scan(/[&;]/)
|
16
|
+
filtered << ss[0]
|
17
|
+
end
|
18
|
+
if ss.scan(/[^&;]+/)
|
19
|
+
kv = ss[0]
|
20
|
+
key, value = kv.split('=', 2)
|
21
|
+
parsed_key = CGI::unescape(key)
|
22
|
+
if [*@options[:filter_keys]].any? { |fk| parsed_key =~ /(\A|[\[\]])#{Regexp.escape(fk)}(\z|[\[\]])/ }
|
23
|
+
filtered << [key, '[FILTERED]'].join('=')
|
24
|
+
else
|
25
|
+
filtered << ss[0]
|
26
|
+
end
|
27
|
+
end
|
28
|
+
end
|
29
|
+
filtered
|
30
|
+
end
|
31
|
+
end
|
32
|
+
end
|
33
|
+
end
|
@@ -0,0 +1,137 @@
|
|
1
|
+
require 'json'
|
2
|
+
require 'json/pure/parser'
|
3
|
+
|
4
|
+
module ApiHammer
|
5
|
+
module Filtration
|
6
|
+
class ParserError < JSON::ParserError; end
|
7
|
+
|
8
|
+
# This class implements the JSON filterer that is used to filter a JSON string
|
9
|
+
class Json < JSON::Pure::Parser
|
10
|
+
# Creates a new instance for the string _source_.
|
11
|
+
def initialize(source, opts = {})
|
12
|
+
super source
|
13
|
+
@options = opts
|
14
|
+
end
|
15
|
+
|
16
|
+
def filter
|
17
|
+
reset
|
18
|
+
obj = ''
|
19
|
+
while !eos? && scan_result(obj, IGNORE)
|
20
|
+
end
|
21
|
+
if eos?
|
22
|
+
raise ParserError, "source did not contain any JSON!"
|
23
|
+
else
|
24
|
+
value = filter_value
|
25
|
+
if value == UNPARSED
|
26
|
+
raise ParserError, "source did not contain any JSON!"
|
27
|
+
else
|
28
|
+
obj << value
|
29
|
+
end
|
30
|
+
end
|
31
|
+
obj
|
32
|
+
end
|
33
|
+
|
34
|
+
private
|
35
|
+
|
36
|
+
def filter_string
|
37
|
+
if scan(STRING)
|
38
|
+
self[0]
|
39
|
+
else
|
40
|
+
UNPARSED
|
41
|
+
end
|
42
|
+
end
|
43
|
+
|
44
|
+
def filter_value
|
45
|
+
simple = [FLOAT, INTEGER, TRUE, FALSE, NULL] + (@allow_nan ? [NAN, INFINITY, MINUS_INFINITY] : [])
|
46
|
+
if simple.any? { |type| scan(type) }
|
47
|
+
self[0]
|
48
|
+
elsif (string = filter_string) != UNPARSED
|
49
|
+
string
|
50
|
+
elsif scan(ARRAY_OPEN)
|
51
|
+
self[0] + filter_array
|
52
|
+
elsif scan(OBJECT_OPEN)
|
53
|
+
self[0] + filter_object
|
54
|
+
else
|
55
|
+
UNPARSED
|
56
|
+
end
|
57
|
+
end
|
58
|
+
|
59
|
+
def filter_array
|
60
|
+
result = ''
|
61
|
+
delim = false
|
62
|
+
until eos?
|
63
|
+
if (value = filter_value) != UNPARSED
|
64
|
+
delim = false
|
65
|
+
result << value
|
66
|
+
scan_result(result, IGNORE)
|
67
|
+
if scan_result(result, COLLECTION_DELIMITER)
|
68
|
+
delim = true
|
69
|
+
elsif !match?(ARRAY_CLOSE)
|
70
|
+
raise ParserError, "expected ',' or ']' in array at '#{peek(20)}'!"
|
71
|
+
end
|
72
|
+
elsif scan_result(result, ARRAY_CLOSE)
|
73
|
+
if delim
|
74
|
+
raise ParserError, "expected next element in array at '#{peek(20)}'!"
|
75
|
+
end
|
76
|
+
break
|
77
|
+
elsif scan_result(result, IGNORE)
|
78
|
+
#
|
79
|
+
else
|
80
|
+
raise ParserError, "unexpected token in array at '#{peek(20)}'!"
|
81
|
+
end
|
82
|
+
end
|
83
|
+
result
|
84
|
+
end
|
85
|
+
|
86
|
+
FILTERED_JSON = JSON.generate("[FILTERED]", :quirks_mode => true)
|
87
|
+
|
88
|
+
def filter_object
|
89
|
+
result = ''
|
90
|
+
delim = false
|
91
|
+
until eos?
|
92
|
+
if (string = filter_string) != UNPARSED
|
93
|
+
parsed_key = JSON.parse(string, :quirks_mode => true)
|
94
|
+
result << string
|
95
|
+
scan_result(result, IGNORE)
|
96
|
+
unless scan_result(result, PAIR_DELIMITER)
|
97
|
+
raise ParserError, "expected ':' in object at '#{peek(20)}'!"
|
98
|
+
end
|
99
|
+
scan_result(result, IGNORE)
|
100
|
+
unless (value = filter_value).equal? UNPARSED
|
101
|
+
if [*@options[:filter_keys]].include?(parsed_key)
|
102
|
+
result << FILTERED_JSON
|
103
|
+
else
|
104
|
+
result << value
|
105
|
+
end
|
106
|
+
delim = false
|
107
|
+
scan_result(result, IGNORE)
|
108
|
+
if scan_result(result, COLLECTION_DELIMITER)
|
109
|
+
delim = true
|
110
|
+
elsif !match?(OBJECT_CLOSE)
|
111
|
+
raise ParserError, "expected ',' or '}' in object at '#{peek(20)}'!"
|
112
|
+
end
|
113
|
+
else
|
114
|
+
raise ParserError, "expected value in object at '#{peek(20)}'!"
|
115
|
+
end
|
116
|
+
elsif scan_result(result, OBJECT_CLOSE)
|
117
|
+
if delim
|
118
|
+
raise ParserError, "expected next name, value pair in object at '#{peek(20)}'!"
|
119
|
+
end
|
120
|
+
break
|
121
|
+
elsif scan_result(result, IGNORE)
|
122
|
+
#
|
123
|
+
else
|
124
|
+
raise ParserError, "unexpected token in object at '#{peek(20)}'!"
|
125
|
+
end
|
126
|
+
end
|
127
|
+
result
|
128
|
+
end
|
129
|
+
|
130
|
+
def scan_result(result, match)
|
131
|
+
if scan(match)
|
132
|
+
result << self[0]
|
133
|
+
end
|
134
|
+
end
|
135
|
+
end
|
136
|
+
end
|
137
|
+
end
|
@@ -0,0 +1,39 @@
|
|
1
|
+
require 'rack'
|
2
|
+
|
3
|
+
module ApiHammer
|
4
|
+
class ParsedBody
|
5
|
+
attr_reader :body, :content_type, :media_type
|
6
|
+
|
7
|
+
def initialize(body, content_type)
|
8
|
+
@body = body
|
9
|
+
@content_type = content_type
|
10
|
+
@media_type = ::Rack::Request.new({'CONTENT_TYPE' => content_type}).media_type
|
11
|
+
end
|
12
|
+
|
13
|
+
def object
|
14
|
+
instance_variable_defined?(:@object) ? @object : @object = begin
|
15
|
+
if media_type == 'application/json'
|
16
|
+
JSON.parse(body) rescue nil
|
17
|
+
elsif media_type == 'application/x-www-form-urlencoded'
|
18
|
+
CGI.parse(body).map { |k, vs| {k => vs.last} }.inject({}, &:update)
|
19
|
+
end
|
20
|
+
end
|
21
|
+
end
|
22
|
+
|
23
|
+
def filtered_body(options)
|
24
|
+
@filtered_body ||= begin
|
25
|
+
if media_type == 'application/json'
|
26
|
+
begin
|
27
|
+
ApiHammer::Filtration::Json.new(body, options).filter
|
28
|
+
rescue JSON::ParserError
|
29
|
+
body
|
30
|
+
end
|
31
|
+
elsif media_type == 'application/x-www-form-urlencoded'
|
32
|
+
ApiHammer::Filtration::FormEncoded.new(body, options).filter
|
33
|
+
else
|
34
|
+
body
|
35
|
+
end
|
36
|
+
end
|
37
|
+
end
|
38
|
+
end
|
39
|
+
end
|
@@ -51,7 +51,8 @@ module ApiHammer
|
|
51
51
|
|
52
52
|
ApiHammer::RequestLogSubscriber.attach_to :action_controller
|
53
53
|
|
54
|
-
app.config.
|
54
|
+
options = app.config.respond_to?(:api_hammer_request_logging_options) ? app.config.api_hammer_request_logging_options : {}
|
55
|
+
app.config.middleware.insert_after(::Rails::Rack::Logger, ApiHammer::RequestLogger, ::Rails.logger, options)
|
55
56
|
|
56
57
|
ActionController::Base.send(:include, AddRequestToPayload)
|
57
58
|
end
|
@@ -17,6 +17,14 @@ module ApiHammer
|
|
17
17
|
|
18
18
|
LARGE_BODY_SIZE = 4096
|
19
19
|
|
20
|
+
# options
|
21
|
+
# - :logger
|
22
|
+
# - :filter_keys
|
23
|
+
def initialize(app, logger, options={})
|
24
|
+
@options = options
|
25
|
+
super(app, logger)
|
26
|
+
end
|
27
|
+
|
20
28
|
def call(env)
|
21
29
|
began_at = Time.now
|
22
30
|
|
@@ -102,22 +110,21 @@ module ApiHammer
|
|
102
110
|
response_body_string = response_body.to_enum.to_a.join('')
|
103
111
|
body_info = [['request', request_body, request.content_type], ['response', response_body_string, response.content_type]]
|
104
112
|
body_info.map do |(role, body, content_type)|
|
113
|
+
parsed_body = ApiHammer::ParsedBody.new(body, content_type)
|
105
114
|
if (400..599).include?(status.to_i) || body.size < LARGE_BODY_SIZE
|
106
115
|
# log bodies if they are not large, or if there was an error (either client or server)
|
107
|
-
data[role]['body'] =
|
116
|
+
data[role]['body'] = parsed_body.filtered_body(@options.reject { |k,v| ![:filter_keys].include?(k) })
|
108
117
|
else
|
109
118
|
# otherwise, log id and uuid fields
|
110
|
-
|
111
|
-
|
112
|
-
|
113
|
-
|
114
|
-
elsif media_type == 'application/x-www-form-urlencoded'
|
115
|
-
CGI.parse(body).map { |k, vs| {k => vs.last} }.inject({}, &:update)
|
116
|
-
end
|
119
|
+
body_object = parsed_body.object
|
120
|
+
sep = /(?:\b|\W|_)/
|
121
|
+
hash_ids = proc do |hash|
|
122
|
+
hash.reject { |key, value| !(key =~ /#{sep}([ug]u)?id#{sep}/ && value.is_a?(String)) }
|
117
123
|
end
|
118
124
|
if body_object.is_a?(Hash)
|
119
|
-
|
120
|
-
|
125
|
+
body_ids = hash_ids.call(body_object)
|
126
|
+
elsif body_object.is_a?(Array) && body_object.all? { |e| e.is_a?(Hash) }
|
127
|
+
body_ids = body_object.map(&hash_ids)
|
121
128
|
end
|
122
129
|
|
123
130
|
data[role]['body_ids'] = body_ids if body_ids && body_ids.any?
|
data/lib/api_hammer/version.rb
CHANGED
data/lib/api_hammer.rb
CHANGED
@@ -10,9 +10,15 @@ module ApiHammer
|
|
10
10
|
autoload :RailsOrSidekiqLogger, 'api_hammer/rails_or_sidekiq_logger'
|
11
11
|
autoload :FaradayOutputter, 'api_hammer/faraday/outputter'
|
12
12
|
autoload :FaradayCurlVOutputter, 'api_hammer/faraday/outputter'
|
13
|
+
autoload :ParsedBody, 'api_hammer/parsed_body'
|
14
|
+
autoload :ContentTypeAttrs, 'api_hammer/content_type_attrs'
|
13
15
|
module Faraday
|
14
16
|
autoload :RequestLogger, 'api_hammer/faraday/request_logger'
|
15
17
|
end
|
18
|
+
module Filtration
|
19
|
+
autoload :Json, 'api_hammer/filtration/json'
|
20
|
+
autoload :FormEncoded, 'api_hammer/filtration/form_encoded'
|
21
|
+
end
|
16
22
|
end
|
17
23
|
|
18
24
|
require 'faraday'
|
@@ -16,6 +16,7 @@ class LogStash::Filters::ApiHammerRequest < LogStash::Filters::Base
|
|
16
16
|
def filter(event)
|
17
17
|
# discard the request status line for humans - always followed by json which we'll parse
|
18
18
|
col = /[\e\[\dm]*/.source
|
19
|
+
# begin direction status method path time end
|
19
20
|
human_request = [/\A/, /[<>]/, /\s/, /\d+/, / : /, /\w+/, / /, /[^\e]+/, / @ /, /[^\e]+/, /\z/].map(&:source).join(col)
|
20
21
|
event.cancel if event[@source] =~ /#{human_request}/
|
21
22
|
|
@@ -0,0 +1,45 @@
|
|
1
|
+
require "logstash/filters/base"
|
2
|
+
require "logstash/namespace"
|
3
|
+
require 'oauthenticator'
|
4
|
+
|
5
|
+
class LogStash::Filters::OAuthenticator < LogStash::Filters::Base
|
6
|
+
config_name "oauthenticator"
|
7
|
+
milestone 1
|
8
|
+
|
9
|
+
config :consume, :validate => :boolean, :default => true
|
10
|
+
config :source, :validate => :string, :default => 'message'
|
11
|
+
|
12
|
+
public
|
13
|
+
def register
|
14
|
+
end
|
15
|
+
|
16
|
+
public
|
17
|
+
def filter(event)
|
18
|
+
#OAuthenticator authenticated an authentic request with Authorization: OAuth realm="", oauth_consumer_key="ios-production-lFo4Zqgs", oauth_token="aE7wU1VPPa7G2l2VLtVRalgIOM4zKJUu7BMnQZoH", oauth_signature_method="HMAC-SHA1", oauth_version="1.0", oauth_nonce="34DA75CB-7653-4AF5-A3F8-B0989AABFCDF", oauth_timestamp="1411935761", oauth_signature="H%2F0kt3aSPqdo2qgfRrbPPirR%2F4g%3D"
|
19
|
+
match = event[@source].match(/\A(OAuthenticator authenticated an authentic request) with Authorization: /)
|
20
|
+
if match
|
21
|
+
authorization = match.post_match
|
22
|
+
|
23
|
+
begin
|
24
|
+
event['oauth'] = OAuthenticator.parse_authorization(authorization)
|
25
|
+
rescue OAuthenticator::Error => parse_exception
|
26
|
+
nil
|
27
|
+
end
|
28
|
+
|
29
|
+
event[@source] = match[1] if @consume
|
30
|
+
end
|
31
|
+
|
32
|
+
# parse the authorization of a request filtered by LogStash::Filters::ApiHammerRequest
|
33
|
+
if event['request'].is_a?(Hash) && event['request']['headers'].is_a?(Hash)
|
34
|
+
authorization = event['request']['headers'].inject(nil) { |a, (k,v)| k.is_a?(String) && k.downcase == 'authorization' ? v : a }
|
35
|
+
if authorization.is_a?(String)
|
36
|
+
begin
|
37
|
+
event['request']['oauth'] = OAuthenticator.parse_authorization(authorization)
|
38
|
+
rescue OAuthenticator::Error => parse_exception
|
39
|
+
# if it is not oauth or badly formed oauth we don't care
|
40
|
+
nil
|
41
|
+
end
|
42
|
+
end
|
43
|
+
end
|
44
|
+
end
|
45
|
+
end
|
@@ -3,6 +3,8 @@ require "logstash/namespace"
|
|
3
3
|
require 'rack'
|
4
4
|
require 'cgi'
|
5
5
|
require 'json'
|
6
|
+
require 'api_hammer/parsed_body'
|
7
|
+
|
6
8
|
class LogStash::Filters::RequestBodiesParsed < LogStash::Filters::Base
|
7
9
|
config_name "request_bodies_parsed"
|
8
10
|
milestone 1
|
@@ -19,15 +21,8 @@ class LogStash::Filters::RequestBodiesParsed < LogStash::Filters::Base
|
|
19
21
|
if event[re]['headers'].is_a?(Hash) && !content_type
|
20
22
|
_, content_type = event[re]['headers'].detect { |(k,_)| k =~ /\Acontent.type\z/i }
|
21
23
|
end
|
22
|
-
|
23
|
-
body_parsed =
|
24
|
-
if media_type == 'application/json'
|
25
|
-
JSON.parse(event[re]['body']) rescue nil
|
26
|
-
elsif media_type == 'application/x-www-form-urlencoded'
|
27
|
-
CGI.parse(event[re]['body']).map { |k, vs| {k => vs.last} }.inject({}, &:update)
|
28
|
-
end
|
29
|
-
end
|
30
|
-
event[re]['body_parsed'] = body_parsed if body_parsed
|
24
|
+
parsed_body = ApiHammer::ParsedBody.new(event[re]['body'], content_type)
|
25
|
+
event[re]['body_parsed'] = parsed_body.object if parsed_body.object
|
31
26
|
end
|
32
27
|
end
|
33
28
|
end
|
@@ -103,7 +103,8 @@ describe ApiHammer::RequestLogger do
|
|
103
103
|
conn.get '/'
|
104
104
|
assert_match('[120,120,195]', logio.string)
|
105
105
|
end
|
106
|
-
|
106
|
+
end
|
107
|
+
describe 'logging body by content-type' do
|
107
108
|
{
|
108
109
|
'application/octet-stream' => false,
|
109
110
|
'image/png' => false,
|
@@ -124,4 +125,90 @@ describe ApiHammer::RequestLogger do
|
|
124
125
|
end
|
125
126
|
end
|
126
127
|
end
|
128
|
+
|
129
|
+
describe 'filtering' do
|
130
|
+
describe 'json response' do
|
131
|
+
it 'filters' do
|
132
|
+
app = proc { |env| [200, {'Content-Type' => 'application/json'}, ['{"pin": "foobar", "bar": "baz"}']] }
|
133
|
+
conn = Faraday.new do |f|
|
134
|
+
f.request :api_hammer_request_logger, logger, :filter_keys => 'pin'
|
135
|
+
f.use Faraday::Adapter::Rack, app
|
136
|
+
end
|
137
|
+
conn.get '/'
|
138
|
+
assert_includes(logio.string, %q("body":"{\"pin\": \"[FILTERED]\", \"bar\": \"baz\"}"))
|
139
|
+
end
|
140
|
+
it 'filters nested' do
|
141
|
+
app = proc { |env| [200, {'Content-Type' => 'application/json'}, ['{"object": {"pin": "foobar"}, "bar": "baz"}']] }
|
142
|
+
conn = Faraday.new do |f|
|
143
|
+
f.request :api_hammer_request_logger, logger, :filter_keys => 'pin'
|
144
|
+
f.use Faraday::Adapter::Rack, app
|
145
|
+
end
|
146
|
+
conn.get '/'
|
147
|
+
assert_includes(logio.string, %q("body":"{\"object\": {\"pin\": \"[FILTERED]\"}, \"bar\": \"baz\"}"))
|
148
|
+
end
|
149
|
+
it 'filters in array' do
|
150
|
+
app = proc { |env| [200, {'Content-Type' => 'application/json'}, ['[{"object": [{"pin": ["foobar"]}], "bar": "baz"}]']] }
|
151
|
+
conn = Faraday.new do |f|
|
152
|
+
f.request :api_hammer_request_logger, logger, :filter_keys => 'pin'
|
153
|
+
f.use Faraday::Adapter::Rack, app
|
154
|
+
end
|
155
|
+
conn.get '/'
|
156
|
+
assert_includes(logio.string, %q("body":"[{\"object\": [{\"pin\": \"[FILTERED]\"}], \"bar\": \"baz\"}]"))
|
157
|
+
end
|
158
|
+
end
|
159
|
+
|
160
|
+
describe 'json request' do
|
161
|
+
it 'filters a json request' do
|
162
|
+
app = proc { |env| [200, {}, []] }
|
163
|
+
conn = Faraday.new do |f|
|
164
|
+
f.request :api_hammer_request_logger, logger, :filter_keys => 'pin'
|
165
|
+
f.use Faraday::Adapter::Rack, app
|
166
|
+
end
|
167
|
+
conn.post '/', '[{"object": [{"pin": ["foobar"]}], "bar": "baz"}]', {'Content-Type' => 'application/json'}
|
168
|
+
assert_includes(logio.string, %q("body":"[{\"object\": [{\"pin\": \"[FILTERED]\"}], \"bar\": \"baz\"}]"))
|
169
|
+
end
|
170
|
+
end
|
171
|
+
|
172
|
+
describe 'form encoded response' do
|
173
|
+
it 'filters' do
|
174
|
+
app = proc { |env| [200, {'Content-Type' => 'application/x-www-form-urlencoded'}, ['pin=foobar&bar=baz']] }
|
175
|
+
conn = Faraday.new do |f|
|
176
|
+
f.request :api_hammer_request_logger, logger, :filter_keys => 'pin'
|
177
|
+
f.use Faraday::Adapter::Rack, app
|
178
|
+
end
|
179
|
+
conn.get '/'
|
180
|
+
assert_includes(logio.string, %q("body":"pin=[FILTERED]&bar=baz"))
|
181
|
+
end
|
182
|
+
it 'filters nested' do
|
183
|
+
app = proc { |env| [200, {'Content-Type' => 'application/x-www-form-urlencoded'}, ['object[pin]=foobar&bar=baz']] }
|
184
|
+
conn = Faraday.new do |f|
|
185
|
+
f.request :api_hammer_request_logger, logger, :filter_keys => 'pin'
|
186
|
+
f.use Faraday::Adapter::Rack, app
|
187
|
+
end
|
188
|
+
conn.get '/'
|
189
|
+
assert_includes(logio.string, %q("body":"object[pin]=[FILTERED]&bar=baz"))
|
190
|
+
end
|
191
|
+
it 'filters in array' do
|
192
|
+
app = proc { |env| [200, {'Content-Type' => 'application/x-www-form-urlencoded'}, ['object[][pin][]=foobar&bar=baz']] }
|
193
|
+
conn = Faraday.new do |f|
|
194
|
+
f.request :api_hammer_request_logger, logger, :filter_keys => 'pin'
|
195
|
+
f.use Faraday::Adapter::Rack, app
|
196
|
+
end
|
197
|
+
conn.get '/'
|
198
|
+
assert_includes(logio.string, %q("body":"object[][pin][]=[FILTERED]&bar=baz"))
|
199
|
+
end
|
200
|
+
end
|
201
|
+
|
202
|
+
describe 'form encoded request' do
|
203
|
+
it 'filters a json request' do
|
204
|
+
app = proc { |env| [200, {}, []] }
|
205
|
+
conn = Faraday.new do |f|
|
206
|
+
f.request :api_hammer_request_logger, logger, :filter_keys => 'pin'
|
207
|
+
f.use Faraday::Adapter::Rack, app
|
208
|
+
end
|
209
|
+
conn.post '/', 'object[pin]=foobar&bar=baz', {'Content-Type' => 'application/x-www-form-urlencoded'}
|
210
|
+
assert_includes(logio.string, %q(object[pin]=[FILTERED]&bar=baz))
|
211
|
+
end
|
212
|
+
end
|
213
|
+
end
|
127
214
|
end
|
data/test/request_logger_test.rb
CHANGED
@@ -28,6 +28,13 @@ describe ApiHammer::RequestLogger do
|
|
28
28
|
assert_match(%q("body_ids":{"uuid":"theuuid","foo_uuid":"thefoouuid","id":"theid","id_for_x":"theidforx","bar.id":"thebarid","baz-guid":"bazzz"}), logio.string)
|
29
29
|
end
|
30
30
|
|
31
|
+
it 'logs id and uuid (json array)' do
|
32
|
+
body = %Q([{"uuid": "theuuid", "foo_uuid": "thefoouuid"}, {"id": "theid", "id_for_x": "theidforx"}, {"bar.id": "thebarid", "baz-guid": "bazzz", "bigthing": "#{' ' * 4096}"}])
|
33
|
+
app = ApiHammer::RequestLogger.new(proc { |env| [200, {"Content-Type" => 'application/json; charset=UTF8'}, [body]] }, logger)
|
34
|
+
app.call(Rack::MockRequest.env_for('/')).last.close
|
35
|
+
assert_match(%q("body_ids":[{"uuid":"theuuid","foo_uuid":"thefoouuid"},{"id":"theid","id_for_x":"theidforx"},{"bar.id":"thebarid","baz-guid":"bazzz"}]), logio.string)
|
36
|
+
end
|
37
|
+
|
31
38
|
it 'logs id and uuid (form encoded)' do
|
32
39
|
body = %Q(uuid=theuuid&foo_uuid=thefoouuid&id=theid&id_for_x=theidforx&bar.id=thebarid&baz-guid=bazzz&bigthing=#{' ' * 4096})
|
33
40
|
app = ApiHammer::RequestLogger.new(proc { |env| [200, {"Content-Type" => 'application/x-www-form-urlencoded; charset=UTF8'}, [body]] }, logger)
|
@@ -48,4 +55,71 @@ describe ApiHammer::RequestLogger do
|
|
48
55
|
assert_match(/"request":\{.*"body":"the_request_body/, logio.string)
|
49
56
|
assert_match(/"response":\{.*"body":"the_response_body/, logio.string)
|
50
57
|
end
|
58
|
+
|
59
|
+
describe 'filtering' do
|
60
|
+
describe 'json response' do
|
61
|
+
it 'filters' do
|
62
|
+
body = %Q({"pin": "foobar"})
|
63
|
+
app = proc { |env| [200, {"Content-Type" => 'application/json; charset=UTF8'}, [body]] }
|
64
|
+
app = ApiHammer::RequestLogger.new(app, logger, :filter_keys => 'pin')
|
65
|
+
app.call(Rack::MockRequest.env_for('/')).last.close
|
66
|
+
|
67
|
+
assert_includes(logio.string, %q("body":"{\"pin\": \"[FILTERED]\"}"))
|
68
|
+
end
|
69
|
+
it 'filters nested' do
|
70
|
+
body = %Q({"object": {"pin": "foobar"}})
|
71
|
+
app = proc { |env| [200, {"Content-Type" => 'application/json; charset=UTF8'}, [body]] }
|
72
|
+
app = ApiHammer::RequestLogger.new(app, logger, :filter_keys => 'pin')
|
73
|
+
app.call(Rack::MockRequest.env_for('/')).last.close
|
74
|
+
|
75
|
+
assert_includes(logio.string, %q("body":"{\"object\": {\"pin\": \"[FILTERED]\"}}"))
|
76
|
+
end
|
77
|
+
it 'filters in array' do
|
78
|
+
body = %Q([{"object": [{"pin": ["foobar"]}]}])
|
79
|
+
app = proc { |env| [200, {"Content-Type" => 'application/json; charset=UTF8'}, [body]] }
|
80
|
+
app = ApiHammer::RequestLogger.new(app, logger, :filter_keys => 'pin')
|
81
|
+
app.call(Rack::MockRequest.env_for('/')).last.close
|
82
|
+
|
83
|
+
assert_includes(logio.string, %q("body":"[{\"object\": [{\"pin\": \"[FILTERED]\"}]}]"))
|
84
|
+
end
|
85
|
+
end
|
86
|
+
|
87
|
+
describe 'json request' do
|
88
|
+
it 'filters a json request' do
|
89
|
+
app = ApiHammer::RequestLogger.new(proc { |env| [200, {}, []] }, logger, :filter_keys => 'pin')
|
90
|
+
app.call(Rack::MockRequest.env_for('/', :input => '[{"object": [{"pin": ["foobar"]}]}]', 'CONTENT_TYPE' => 'application/json')).last.close
|
91
|
+
assert_includes(logio.string, %q("body":"[{\"object\": [{\"pin\": \"[FILTERED]\"}]}]"))
|
92
|
+
end
|
93
|
+
end
|
94
|
+
|
95
|
+
describe('form encoded response') do
|
96
|
+
it 'filters' do
|
97
|
+
app = proc { |env| [200, {"Content-Type" => 'application/x-www-form-urlencoded'}, ['pin=foobar']] }
|
98
|
+
app = ApiHammer::RequestLogger.new(app, logger, :filter_keys => 'pin')
|
99
|
+
app.call(Rack::MockRequest.env_for('/')).last.close
|
100
|
+
assert_includes(logio.string, %q("body":"pin=[FILTERED]"))
|
101
|
+
end
|
102
|
+
it 'filters nested' do
|
103
|
+
app = proc { |env| [200, {'Content-Type' => 'application/x-www-form-urlencoded'}, ['object[pin]=foobar']] }
|
104
|
+
app = ApiHammer::RequestLogger.new(app, logger, :filter_keys => 'pin')
|
105
|
+
app.call(Rack::MockRequest.env_for('/')).last.close
|
106
|
+
assert_includes(logio.string, %q("body":"object[pin]=[FILTERED]"))
|
107
|
+
end
|
108
|
+
it 'filters in array' do
|
109
|
+
app = proc { |env| [200, {'Content-Type' => 'application/x-www-form-urlencoded'}, ['object[][pin][]=foobar']] }
|
110
|
+
app = ApiHammer::RequestLogger.new(app, logger, :filter_keys => 'pin')
|
111
|
+
app.call(Rack::MockRequest.env_for('/')).last.close
|
112
|
+
assert_includes(logio.string, %q("body":"object[][pin][]=[FILTERED]"))
|
113
|
+
end
|
114
|
+
end
|
115
|
+
|
116
|
+
describe 'form encoded request' do
|
117
|
+
it 'filters a json request' do
|
118
|
+
app = proc { |env| [200, {}, []] }
|
119
|
+
app = ApiHammer::RequestLogger.new(app, logger, :filter_keys => 'pin')
|
120
|
+
app.call(Rack::MockRequest.env_for('/', :input => 'object[pin]=foobar', 'CONTENT_TYPE' => 'application/x-www-form-urlencoded')).last.close
|
121
|
+
assert_includes(logio.string, %q(object[pin]=[FILTERED]))
|
122
|
+
end
|
123
|
+
end
|
124
|
+
end
|
51
125
|
end
|
metadata
CHANGED
@@ -1,14 +1,14 @@
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
2
2
|
name: api_hammer
|
3
3
|
version: !ruby/object:Gem::Version
|
4
|
-
version: 0.
|
4
|
+
version: 0.9.2
|
5
5
|
platform: ruby
|
6
6
|
authors:
|
7
7
|
- Ethan
|
8
8
|
autorequire:
|
9
9
|
bindir: bin
|
10
10
|
cert_chain: []
|
11
|
-
date:
|
11
|
+
date: 2015-01-08 00:00:00.000000000 Z
|
12
12
|
dependencies:
|
13
13
|
- !ruby/object:Gem::Dependency
|
14
14
|
name: rack
|
@@ -239,9 +239,13 @@ files:
|
|
239
239
|
- lib/api_hammer.rb
|
240
240
|
- lib/api_hammer/active_record_cache_find_by.rb
|
241
241
|
- lib/api_hammer/check_required_params.rb
|
242
|
+
- lib/api_hammer/content_type_attrs.rb
|
242
243
|
- lib/api_hammer/faraday/outputter.rb
|
243
244
|
- lib/api_hammer/faraday/request_logger.rb
|
245
|
+
- lib/api_hammer/filtration/form_encoded.rb
|
246
|
+
- lib/api_hammer/filtration/json.rb
|
244
247
|
- lib/api_hammer/halt.rb
|
248
|
+
- lib/api_hammer/parsed_body.rb
|
245
249
|
- lib/api_hammer/public_instance_exec.rb
|
246
250
|
- lib/api_hammer/rails.rb
|
247
251
|
- lib/api_hammer/rails_or_sidekiq_logger.rb
|
@@ -257,6 +261,7 @@ files:
|
|
257
261
|
- lib/api_hammer/weblink.rb
|
258
262
|
- lib/logstash/filters/active_support_tags.rb
|
259
263
|
- lib/logstash/filters/api_hammer_request.rb
|
264
|
+
- lib/logstash/filters/oauthenticator.rb
|
260
265
|
- lib/logstash/filters/request_bodies_parsed.rb
|
261
266
|
- lib/logstash/filters/ruby_logger.rb
|
262
267
|
- lib/logstash/filters/sidekiq.rb
|