api_hammer 0.8.1 → 0.9.2
Sign up to get free protection for your applications and to get access to all the features.
- 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
|