api_hammer 0.10.2 → 0.11.0

Sign up to get free protection for your applications and to get access to all the features.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA1:
3
- metadata.gz: e22063457c4fd23dcfacf1dd263bb86bed720da4
4
- data.tar.gz: 59d655dfeb7c3521bda18fa73cc58da1a0cee266
3
+ metadata.gz: 6d475db83c102fc0746ea2da97c992a9ab1e6a1f
4
+ data.tar.gz: f82cbddaf738a334f7779bed1ce64005c6e46f93
5
5
  SHA512:
6
- metadata.gz: e26ef7aec22dc8114044bb52947d3d1a49e00395ae45c80feb6903487385f3db6bf4f0871c32558e943cc7a77d85358d9d705695c9ae2a5acec9d25ad41eb76e
7
- data.tar.gz: 7b0d75819bfd4c637d24a0ee74cea82e589b3ac3d5f767129a7ac43b4370cb44fb0d4df01d5398f7324b5f4a1805a4cbc6727a19456f1c5cc2bb34373b1bec09
6
+ metadata.gz: 42161c34bc9260717cba35c2ce308636490e332722e43d1435fa2300b404f97156701d9bed860a727ed4dbf2fcab010968f6cb360d33dd10b63dfaf4f73b7d75
7
+ data.tar.gz: 59f6ccc971e7d4c925a7141c7fa91670bf36e83972598c0583015c0df5c48759195bf34fb6499887b3703fce63f9610a700f9d91b334720e9627bf93792ac0cf
data/CHANGELOG.md CHANGED
@@ -1,3 +1,6 @@
1
+ # v0.11.0
2
+ - improved handling of text and binary bodies in logging middleware and hc
3
+
1
4
  # v0.10.2
2
5
  - rails request logging logs exception backtrace
3
6
 
data/bin/hc CHANGED
@@ -44,6 +44,9 @@ opt_parser = OptionParser.new do |opts|
44
44
  opts.on("-t", "--content-type CONTENT-TYPE", "Sets the Content-Type header of the request. This defaults to application/json if a body is included.") do |v|
45
45
  $options[:headers]['Content-Type'.downcase] = v
46
46
  end
47
+ opts.on("-o", "--output OUTPUT", "write response to file") do |v|
48
+ $options[:output] = v
49
+ end
47
50
  opts.on("--oauth-token TOKEN", "OAuth 1.0 token") do |token|
48
51
  $oauth[:token] = token
49
52
  end
@@ -116,3 +119,9 @@ end
116
119
  # OH LOOK IT'S FINALLY ACTUALLY CONNECTING TO SOMETHING
117
120
 
118
121
  response = connection.run_request(httpmethod.downcase.to_sym, url, body, headers)
122
+
123
+ if $options[:output]
124
+ File.open($options[:output], 'wb') do |f|
125
+ f.write(response.body)
126
+ end
127
+ end
@@ -0,0 +1,103 @@
1
+ require 'rack'
2
+
3
+ module ApiHammer
4
+ class Body
5
+ attr_reader :body, :content_type
6
+
7
+ def initialize(body, content_type)
8
+ @body = body
9
+ @content_type = content_type
10
+ end
11
+
12
+ # parses the body to an object
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(options)
24
+ @filtered ||= Body.new(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, content_type)
37
+ end
38
+
39
+ def content_type_attrs
40
+ @content_type_attrs ||= ContentTypeAttrs.new(content_type)
41
+ end
42
+
43
+ def media_type
44
+ content_type_attrs.media_type
45
+ end
46
+
47
+ # deal with the vagaries of getting the response body in a form which JSON
48
+ # gem will not cry about generating
49
+ def jsonifiable
50
+ @jsonifiable ||= Body.new(catch(:jsonifiable) do
51
+ original_body = self.body
52
+ unless original_body.is_a?(String)
53
+ begin
54
+ # if the response body is not a string, but JSON doesn't complain
55
+ # about dumping whatever it is, go ahead and use it
56
+ JSON.generate([original_body])
57
+ throw :jsonifiable, original_body
58
+ rescue
59
+ # otherwise return nil - don't know what to do with whatever this object is
60
+ throw :jsonifiable, nil
61
+ end
62
+ end
63
+
64
+ # first try to change the string's encoding per the Content-Type header
65
+ body = original_body.dup
66
+ unless body.valid_encoding?
67
+ # I think this always comes in as ASCII-8BIT anyway so may never get here. hopefully.
68
+ body.force_encoding('ASCII-8BIT')
69
+ end
70
+
71
+ content_type_attrs = ContentTypeAttrs.new(content_type)
72
+ if content_type_attrs.parsed?
73
+ charset = content_type_attrs['charset'].first
74
+ if charset && Encoding.list.any? { |enc| enc.to_s.downcase == charset.downcase }
75
+ if body.dup.force_encoding(charset).valid_encoding?
76
+ body.force_encoding(charset)
77
+ else
78
+ # I guess just ignore the specified encoding if the result is not valid. fall back to
79
+ # something else below.
80
+ end
81
+ end
82
+ end
83
+ begin
84
+ JSON.generate([body])
85
+ rescue Encoding::UndefinedConversionError
86
+ # if updating by content-type didn't do it, try UTF8 since JSON wants that - but only
87
+ # if it seems to be valid utf8.
88
+ # don't try utf8 if the response content-type indicated something else.
89
+ try_utf8 = !(content_type_attrs && content_type_attrs.parsed? && content_type_attrs['charset'].any?)
90
+ if try_utf8 && body.dup.force_encoding('UTF-8').valid_encoding?
91
+ body.force_encoding('UTF-8')
92
+ else
93
+ # I'm not sure if there is a way in this situation to get JSON gem to generate the
94
+ # string correctly. fall back to an array of codepoints I guess? this is a weird
95
+ # solution but the best I've got for now.
96
+ body = body.codepoints.to_a
97
+ end
98
+ end
99
+ body
100
+ end, content_type)
101
+ end
102
+ end
103
+ end
@@ -27,7 +27,7 @@ module ApiHammer
27
27
  end
28
28
  end
29
29
 
30
- attr_reader :media_type
30
+ attr_reader :content_type, :media_type
31
31
 
32
32
  def parsed?
33
33
  @parsed
@@ -47,12 +47,19 @@ module ApiHammer
47
47
  %r(model/.*) => :binary,
48
48
  %r(text/.*) => :text,
49
49
  %r(message/.*) => :text,
50
+ %r(application/(.+\+)?json) => :text,
51
+ %r(application/(.+\+)?xml) => :text,
52
+ %r(model/(.+\+)?xml) => :text,
53
+ 'application/x-www-form-urlencoded' => :text,
54
+ 'application/javascript' => :text,
55
+ 'application/ecmascript' => :text,
50
56
  'application/octet-stream' => :binary,
51
57
  'application/ogg' => :binary,
52
58
  'application/pdf' => :binary,
53
59
  'application/postscript' => :binary,
54
60
  'application/zip' => :binary,
55
61
  'application/gzip' => :binary,
62
+ 'application/vnd.apple.pkpass' => :binary,
56
63
  }
57
64
  types.each do |match, type|
58
65
  matched = match.is_a?(Regexp) ? media_type =~ %r(\A#{match.source}\z) : media_type == match
@@ -60,8 +67,13 @@ module ApiHammer
60
67
  return type == :text
61
68
  end
62
69
  end
63
- # fallback (unknown or not given) assume text
64
- return true
70
+ # fallback (unknown or not given) assume that unknown content types are binary but omitted
71
+ # content-type means text
72
+ if content_type && content_type =~ /\S/
73
+ return false
74
+ else
75
+ return true
76
+ end
65
77
  end
66
78
  end
67
79
  end
@@ -1,5 +1,6 @@
1
1
  require 'faraday'
2
2
  require 'rack'
3
+ require 'api_hammer'
3
4
 
4
5
  module ApiHammer
5
6
  # outputs the response body to the given logger or output device (defaulting to STDOUT)
@@ -55,6 +56,8 @@ module ApiHammer
55
56
  color :response_header
56
57
  color :response_blankline, :intense_green, :bold
57
58
 
59
+ color :omitted_body, :intense_yellow
60
+
58
61
  def call(request_env)
59
62
  puts "#{info('*')} #{info_body("connect to #{request_env[:url].host} on port #{request_env[:url].port}")}"
60
63
  puts "#{info('*')} #{info_body("getting our SSL on")}" if request_env[:url].scheme=='https'
@@ -125,23 +128,27 @@ module ApiHammer
125
128
  # - formatted prettily if #pretty? is true
126
129
  def alter_body_by_content_type(body, content_type)
127
130
  return body unless body.is_a?(String)
128
- media_type = ::Rack::Request.new({'CONTENT_TYPE' => content_type}).media_type
129
- if pretty?
130
- case media_type
131
- when 'application/json'
132
- require 'json'
133
- begin
134
- body = JSON.pretty_generate(JSON.parse(body))
135
- rescue JSON::ParserError
131
+ content_type_attrs = ApiHammer::ContentTypeAttrs.new(content_type)
132
+ if content_type_attrs.text?
133
+ if pretty?
134
+ case content_type_attrs.media_type
135
+ when 'application/json'
136
+ require 'json'
137
+ begin
138
+ body = JSON.pretty_generate(JSON.parse(body))
139
+ rescue JSON::ParserError
140
+ end
136
141
  end
137
142
  end
138
- end
139
- if color?
140
- coderay_scanner = CodeRayForMediaTypes.reject{|k,v| !v.any?{|type| type === media_type} }.keys.first
141
- if coderay_scanner
142
- require 'coderay'
143
- body = CodeRay.scan(body, coderay_scanner).encode(:terminal)
143
+ if color?
144
+ coderay_scanner = CodeRayForMediaTypes.reject{|k,v| !v.any?{|type| type === content_type_attrs.media_type} }.keys.first
145
+ if coderay_scanner
146
+ require 'coderay'
147
+ body = CodeRay.scan(body, coderay_scanner).encode(:terminal)
148
+ end
144
149
  end
150
+ else
151
+ body = omitted_body("[[omitted binary body (size = #{body.size})]]")
145
152
  end
146
153
  body
147
154
  end
@@ -5,72 +5,6 @@ require 'strscan'
5
5
 
6
6
  module ApiHammer
7
7
  module Faraday
8
- class Request
9
- def initialize(request_env, response_env)
10
- @request_env = request_env
11
- @response_env = response_env
12
- end
13
-
14
- attr_reader :request_env
15
- attr_reader :response_env
16
-
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
40
-
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
72
- end
73
-
74
8
  # Faraday middleware for logging.
75
9
  #
76
10
  # logs two lines:
@@ -101,8 +35,6 @@ module ApiHammer
101
35
  @app.call(request_env).on_complete do |response_env|
102
36
  now = Time.now
103
37
 
104
- request = ApiHammer::Faraday::Request.new(request_env, response_env)
105
-
106
38
  status_color = case response_env.status.to_i
107
39
  when 200..299
108
40
  :intense_green
@@ -115,17 +47,17 @@ module ApiHammer
115
47
  end
116
48
  status_s = bold(send(status_color, response_env.status.to_s))
117
49
 
118
- filtered_request_body = request_body.dup if request_body
119
- filtered_response_body = request.response_body.dup if request.response_body
50
+ bodies = [
51
+ ['request', request_body, request_env.request_headers],
52
+ ['response', response_env.body, response_env.response_headers]
53
+ ].map do |(role, body, headers)|
54
+ {role => Body.new(body, headers['Content-Type']).jsonifiable}
55
+ end.inject({}, &:update)
120
56
 
121
57
  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
58
+ bodies = bodies.map do |(role, body)|
59
+ {role => body.filtered(@options.slice(:filter_keys))}
60
+ end.inject({}, &:update)
129
61
  end
130
62
 
131
63
  data = {
@@ -134,12 +66,12 @@ module ApiHammer
134
66
  'method' => request_env[:method],
135
67
  'uri' => request_env[:url].normalize.to_s,
136
68
  'headers' => request_env.request_headers,
137
- 'body' => (filtered_request_body if ContentTypeAttrs.new(request_env.request_headers['Content-Type']).text?),
69
+ 'body' => (bodies['request'].body if bodies['request'].content_type_attrs.text?),
138
70
  }.reject{|k,v| v.nil? },
139
71
  'response' => {
140
72
  'status' => response_env.status.to_s,
141
73
  'headers' => response_env.response_headers,
142
- 'body' => (filtered_response_body if ContentTypeAttrs.new(response_env.response_headers['Content-Type']).text?),
74
+ 'body' => (bodies['response'].body if bodies['response'].content_type_attrs.text?),
143
75
  }.reject{|k,v| v.nil? },
144
76
  'processing' => {
145
77
  'began_at' => began_at.utc.to_f,
@@ -112,24 +112,27 @@ module ApiHammer
112
112
  response_body_string = response_body.to_enum.to_a.join('')
113
113
  body_info = [['request', request_body, request.content_type], ['response', response_body_string, response.content_type]]
114
114
  body_info.map do |(role, body, content_type)|
115
- parsed_body = ApiHammer::ParsedBody.new(body, content_type)
116
- if (400..599).include?(status.to_i) || body.size < LARGE_BODY_SIZE
117
- # log bodies if they are not large, or if there was an error (either client or server)
118
- data[role]['body'] = parsed_body.filtered_body(@options.reject { |k,v| ![:filter_keys].include?(k) })
119
- else
120
- # otherwise, log id and uuid fields
121
- body_object = parsed_body.object
122
- sep = /(?:\b|\W|_)/
123
- hash_ids = proc do |hash|
124
- hash.reject { |key, value| !(key =~ /#{sep}([ug]u)?id#{sep}/ && value.is_a?(String)) }
125
- end
126
- if body_object.is_a?(Hash)
127
- body_ids = hash_ids.call(body_object)
128
- elsif body_object.is_a?(Array) && body_object.all? { |e| e.is_a?(Hash) }
129
- body_ids = body_object.map(&hash_ids)
130
- end
115
+ parsed_body = ApiHammer::Body.new(body, content_type)
116
+ content_type_attrs = ApiHammer::ContentTypeAttrs.new(content_type)
117
+ if content_type_attrs.text?
118
+ if (400..599).include?(status.to_i) || body.size < LARGE_BODY_SIZE
119
+ # log bodies if they are not large, or if there was an error (either client or server)
120
+ data[role]['body'] = parsed_body.jsonifiable.filtered(@options.reject { |k,v| ![:filter_keys].include?(k) }).body
121
+ else
122
+ # otherwise, log id and uuid fields
123
+ body_object = parsed_body.object
124
+ sep = /(?:\b|\W|_)/
125
+ hash_ids = proc do |hash|
126
+ hash.reject { |key, value| !(key =~ /#{sep}([ug]u)?id#{sep}/ && value.is_a?(String)) }
127
+ end
128
+ if body_object.is_a?(Hash)
129
+ body_ids = hash_ids.call(body_object)
130
+ elsif body_object.is_a?(Array) && body_object.all? { |e| e.is_a?(Hash) }
131
+ body_ids = body_object.map(&hash_ids)
132
+ end
131
133
 
132
- data[role]['body_ids'] = body_ids if body_ids && body_ids.any?
134
+ data[role]['body_ids'] = body_ids if body_ids && body_ids.any?
135
+ end
133
136
  end
134
137
  end
135
138
  Thread.current['request_logger.info'] = nil
@@ -26,7 +26,7 @@ module ApiHammer
26
26
  end
27
27
  def call(env)
28
28
  status, headers, body = *@app.call(env)
29
- if env['REQUEST_METHOD'].downcase != 'head'
29
+ if env['REQUEST_METHOD'].downcase != 'head' && ApiHammer::ContentTypeAttrs.new(env['CONTENT_TYPE']).text?
30
30
  body = TNLBodyProxy.new(body){}
31
31
  if headers["Content-Length"]
32
32
  headers["Content-Length"] = body.map(&Rack::Utils.method(:bytesize)).inject(0, &:+).to_s
@@ -1,3 +1,3 @@
1
1
  module ApiHammer
2
- VERSION = "0.10.2"
2
+ VERSION = "0.11.0"
3
3
  end
data/lib/api_hammer.rb CHANGED
@@ -12,7 +12,7 @@ module ApiHammer
12
12
  autoload :RailsOrSidekiqLogger, 'api_hammer/rails_or_sidekiq_logger'
13
13
  autoload :FaradayOutputter, 'api_hammer/faraday/outputter'
14
14
  autoload :FaradayCurlVOutputter, 'api_hammer/faraday/outputter'
15
- autoload :ParsedBody, 'api_hammer/parsed_body'
15
+ autoload :Body, 'api_hammer/body'
16
16
  autoload :ContentTypeAttrs, 'api_hammer/content_type_attrs'
17
17
  autoload :JsonScriptEscapeHelper, 'api_hammer/json_script_escape_helper'
18
18
  module Faraday
@@ -21,7 +21,7 @@ class LogStash::Filters::RequestBodiesParsed < LogStash::Filters::Base
21
21
  if event[re]['headers'].is_a?(Hash) && !content_type
22
22
  _, content_type = event[re]['headers'].detect { |(k,_)| k =~ /\Acontent.type\z/i }
23
23
  end
24
- parsed_body = ApiHammer::ParsedBody.new(event[re]['body'], content_type)
24
+ parsed_body = ApiHammer::Body.new(event[re]['body'], content_type)
25
25
  event[re]['body_parsed'] = parsed_body.object if parsed_body.object
26
26
  end
27
27
  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.10.2
4
+ version: 0.11.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - Ethan
8
8
  autorequire:
9
9
  bindir: bin
10
10
  cert_chain: []
11
- date: 2015-07-19 00:00:00.000000000 Z
11
+ date: 2015-09-21 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: rack
@@ -244,6 +244,7 @@ files:
244
244
  - bin/hc
245
245
  - lib/api_hammer.rb
246
246
  - lib/api_hammer/active_record_cache_find_by.rb
247
+ - lib/api_hammer/body.rb
247
248
  - lib/api_hammer/check_required_params.rb
248
249
  - lib/api_hammer/content_type_attrs.rb
249
250
  - lib/api_hammer/faraday/outputter.rb
@@ -252,7 +253,6 @@ files:
252
253
  - lib/api_hammer/filtration/json.rb
253
254
  - lib/api_hammer/halt.rb
254
255
  - lib/api_hammer/json_script_escape_helper.rb
255
- - lib/api_hammer/parsed_body.rb
256
256
  - lib/api_hammer/public_instance_exec.rb
257
257
  - lib/api_hammer/rails.rb
258
258
  - lib/api_hammer/rails_or_sidekiq_logger.rb
@@ -1,39 +0,0 @@
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