mint_http 0.1.8 → 0.1.9

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
  SHA256:
3
- metadata.gz: 5928a4bac42cf6e1c6039ddbff011d758d7f0835feab6def492b72b0a3efda14
4
- data.tar.gz: 0d4aba076e15a19bca4e91dd96f5b6fe63ba52509ca4ad72cef9ac22fe5e0c03
3
+ metadata.gz: b0d6eb6fc74d83a79da5384f6ecec034ea2402752539ac29b40b43c85a0422a0
4
+ data.tar.gz: 29ba4aa2fdd0f1bb3dfeccb44d83766ea1908d945318db6cf918127c39c5eea7
5
5
  SHA512:
6
- metadata.gz: 6603a0dde40391ec153b797d1cd5725feef3773f36ea5eb1c45c576cdd1c6b14dffbe2748e089850531b87656c48487e59ef9268a3416166fe5b2a0b57c0b141
7
- data.tar.gz: 2f7cf463b45f12da07b663b3679781041539b66d56b046be2df8e9d86e812dd610345d48ccd189d8a3918a35f6ba1c0187fd7b14480fb0142900ff7e5f51db4f
6
+ metadata.gz: f0d28be37857d0943a281ff4469095e0a9cb6d58a67a47a14dd7290970564e39306bde41d772a00511136d7cf0a3fef2a202ea3d300e64c40a44b5bc2783079d
7
+ data.tar.gz: f0c4050bb642956395b7ccf13279659ef73e23787304127b44c663e9153d48510151438bf4295d8c3e3eeb8e86f80548d0be695daeb8c36829863ae8b532455b
@@ -0,0 +1,17 @@
1
+ # frozen_string_literal: true
2
+
3
+ module MintHttp
4
+ class Config
5
+ # @!attribute [rw] logger
6
+ # @return [Logger] logger to be used for logging request details
7
+ attr_accessor :logger
8
+
9
+ # @!attribute [rw] filter_params_list
10
+ # @return [Array[String|Symbol]] logger to be used for logging request details
11
+ attr_accessor :filter_params_list
12
+
13
+ # @!attribute [rw] filter_params
14
+ # @return [Boolean] indicate if MintHttp should filter out params from request logs
15
+ attr_accessor :filter_params
16
+ end
17
+ end
@@ -23,6 +23,10 @@ module MintHttp
23
23
  attr_reader :ssl_verify_mode
24
24
  attr_reader :ssl_verify_hostname
25
25
 
26
+ # Attributes only available when request is made
27
+ attr_reader :method
28
+ attr_reader :request_url
29
+
26
30
  def initialize
27
31
  @pool = nil
28
32
  @base_url = nil
@@ -44,6 +48,10 @@ module MintHttp
44
48
  @ssl_verify_mode = nil
45
49
  @ssl_verify_hostname = nil
46
50
 
51
+ @logger = MintHttp.config.logger
52
+ @filter_params_list = MintHttp.config.filter_params_list
53
+ @filter_params = MintHttp.config.filter_params
54
+
47
55
  header('User-Agent' => 'Mint Http')
48
56
  as_json
49
57
  end
@@ -204,6 +212,28 @@ module MintHttp
204
212
  self
205
213
  end
206
214
 
215
+ # @param [Logger] logger
216
+ def use_logger(logger)
217
+ @logger = logger
218
+ self
219
+ end
220
+
221
+ def no_logger
222
+ @logger = Logger.new('/dev/null')
223
+ self
224
+ end
225
+
226
+ # @param [Array[String]] params
227
+ def filter_param(*params)
228
+ @filter_params_list.concat(params)
229
+ self
230
+ end
231
+
232
+ def should_filter_params(filter = true)
233
+ @filter_params = filter
234
+ self
235
+ end
236
+
207
237
  def get(url, params = {})
208
238
  query(params).send_request('get', url)
209
239
  end
@@ -233,20 +263,60 @@ module MintHttp
233
263
  end
234
264
 
235
265
  def send_request(method, url)
266
+ @method = method
267
+
268
+ logger = RequestLogger.new(@logger, @filter_params_list, @filter_params)
236
269
  url, net_request, options = build_request(method, url)
237
270
 
238
- res = with_client(url.hostname, url.port, options) do |http|
239
- http.request(net_request)
271
+ logger.log_request(self, net_request)
272
+ logger.log_start
273
+
274
+ begin
275
+ res = with_client(url.hostname, url.port, options) do |http|
276
+ logger.log_connected
277
+ logger.log_connection_info(http)
278
+ http.request(net_request)
279
+ end
280
+ rescue StandardError => error
281
+ logger.log_end
282
+ logger.log_error(error)
283
+ logger.write_log
284
+ raise error
240
285
  end
241
286
 
242
- Response.new(res)
287
+ logger.log_end
288
+
289
+ response = Response.new(res, net_request, self)
290
+
291
+ logger.log_response(response)
292
+ logger.put_timing(response)
293
+ logger.write_log
294
+
295
+ response
243
296
  end
244
297
 
245
298
  private
246
299
 
247
- def build_request(method, url)
300
+ def build_url(url)
248
301
  url = URI.parse(url)
249
- url = @base_url + url if @base_url
302
+
303
+ unless url.is_a?(URI::Generic)
304
+ return url
305
+ end
306
+
307
+ unless @base_url
308
+ return url
309
+ end
310
+
311
+ unless @base_url.path.match?(/\/$/)
312
+ @base_url.path += '/'
313
+ end
314
+
315
+ @base_url + url.path.gsub(/^\/+/, '')
316
+ end
317
+
318
+ def build_request(method, url)
319
+ @request_url = url = build_url(url)
250
320
 
251
321
  unless %w[http https].include?(url.scheme)
252
322
  raise ArgumentError, "Only HTTP and HTTPS URLs are allowed"
@@ -348,5 +418,9 @@ module MintHttp
348
418
  def net_factory
349
419
  @net_factory ||= NetHttpFactory.new
350
420
  end
421
+
422
+ def clock_time
423
+ Process.clock_gettime(Process::CLOCK_MONOTONIC)
424
+ end
351
425
  end
352
426
  end
@@ -0,0 +1,218 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'cgi'
4
+
5
+ module MintHttp
6
+ class RequestLogger
7
+ MAX_BODY_SIZE = 15 * 1024
8
+ BODY_ALLOWED_TYPES = [/application\/.+/, /text\/.+/]
9
+
10
+ attr_reader :logger
11
+ attr_reader :filter_list
12
+ attr_reader :time_started
13
+ attr_reader :time_ended
14
+ attr_reader :time_connected
15
+ attr_reader :time_total
16
+ attr_reader :time_connecting
17
+ attr_reader :tls_config
18
+
19
+ # @param [Logger] logger
20
+ # @param [Array[String|Symbol]] request
21
+ def initialize(logger, filter_list, filter = true)
22
+ @logger = logger
23
+ @filter_list = filter_list.map(&:downcase)
24
+ @filter = filter
25
+ @time_started = 0.0
26
+ @time_ended = 0.0
27
+ @time_connected = 0.0
28
+ @time_total = 0.0
29
+ @time_connecting = 0.0
30
+
31
+ @request = nil
32
+ @net_request = nil
33
+ @response = nil
34
+ @error = nil
35
+ end
36
+
37
+ def log_request(request, net_request)
38
+ @request = request
39
+ @net_request = net_request
40
+ end
41
+
42
+ def log_response(response)
43
+ @response = response
44
+ end
45
+
46
+ def log_error(error)
47
+ @error = error
48
+ end
49
+
50
+ def log_start
51
+ @time_started = clock_time
52
+ end
53
+
54
+ def log_end
55
+ @time_ended = clock_time
56
+ @time_total = @time_ended - @time_started
57
+ end
58
+
59
+ def log_connected
60
+ @time_connected = clock_time
61
+ @time_connecting = @time_connected - @time_started
62
+ end
63
+
64
+ # @param [MintHttp::Response] response
65
+ def put_timing(response)
66
+ response.time_started = @time_started
67
+ response.time_ended = @time_ended
68
+ response.time_connected = @time_connected
69
+ response.time_total = @time_total
70
+ response.time_connecting = @time_connecting
71
+ end
72
+
73
+ def write_log
74
+ path = build_path(@request.request_url)
75
+ version = @response&.version || '1.1'
76
+
77
+ tls = 'None'
78
+ if @tls_config
79
+ tls = "#{@tls_config[:version]} Cipher: #{@tls_config[:cipher]}"
80
+ end
81
+
82
+ buffer = String.new
83
+ buffer << <<~TXT
84
+ MintHttp Log (#{@request.request_url})
85
+ @@ Timeouts: #{@request.open_timeout}, #{@request.write_timeout}, #{@request.read_timeout}
86
+ @@ Time: #{@time_started.round(3)} -> #{@time_connected.round(3)} connecting: #{time_connecting.round(3)} total: #{@time_total.round(3)} seconds
87
+ @@ TLS: #{tls}
88
+ -> #{@request.method.upcase} #{path} HTTP/#{version}
89
+ #{masked_headers(@net_request.each_header.to_h, '-> ')}
90
+ -> #{masked_body(@request.body, @request.headers['content-type'])}
91
+ =======
92
+ TXT
93
+
94
+ if @response
95
+ buffer << <<~TXT
96
+ <- Response: HTTP/#{@response.version} #{@response.status_code} #{@response.status_text}
97
+ #{masked_headers(@response.headers, '<- ')}
98
+ <- Length: #{@response.body.bytesize} Body: #{masked_body(@response.body, @response.headers['content-type'])}
99
+ TXT
100
+ end
101
+
102
+ if @error
103
+ buffer << "!! Error: #{@error.class}, message: #{@error.message}"
104
+ end
105
+
106
+ unless buffer.valid_encoding?
107
+ raise 'Buffer has not valid encoding'
108
+ end
109
+
110
+ @logger.info(buffer.strip)
111
+ end
112
+
113
+ def log_connection_info(http)
114
+ @tls_config = http.instance_eval do
115
+ if use_ssl?
116
+ { version: @socket.io.ssl_version, cipher: @socket.io.cipher[0] }
117
+ else
118
+ nil
119
+ end
120
+ end
121
+ end
122
+
123
+ private
124
+
125
+ def clock_time
126
+ Process.clock_gettime(Process::CLOCK_MONOTONIC)
127
+ end
128
+
129
+ def build_path(url)
130
+ full_path = url.path.dup
131
+
132
+ if url.query
133
+ full_path << "?#{url.query}"
134
+ end
135
+
136
+ if url.fragment
137
+ full_path << "##{url.fragment}"
138
+ end
139
+
140
+ full_path
141
+ end
142
+
143
+ def lower_case_filter_list
144
+ @_lower_case_filter_list ||= @filter_list.map(&:downcase)
145
+ end
146
+
147
+ def filter_query(query)
148
+ unless @filter
149
+ return query
150
+ end
151
+
152
+ filtered = CGI::parse(query || '')
153
+ .to_h { |k, v| [k, lower_case_filter_list.include?(v) ? '[FILTERED]' : v] }
154
+
155
+ URI.encode_www_form(filtered)
156
+ end
157
+
158
+ def masked_headers(headers, prefix = '')
159
+ headers
160
+ .map { |k, v| [k.split('-').map(&:capitalize).join('-'), v] }
161
+ .map { |k, v| "#{prefix}#{k}: #{@filter && lower_case_filter_list.include?(k.downcase) ? '[FILTERED]' : v}" }
162
+ .join("\n")
163
+ end
164
+
165
+ def masked_body(body, type)
166
+ size = body&.bytesize || 0
167
+ if size == 0
168
+ return '[EMPTY]'
169
+ end
170
+
171
+ if size > MAX_BODY_SIZE
172
+ return '[LARGE]'
173
+ end
174
+
175
+ unless body_allowed?(type)
176
+ return '[COMPLEX]'
177
+ end
178
+
179
+ unless @filter
180
+ return body
181
+ end
182
+
183
+ if type.match?(/json/)
184
+ redact_json(body)
185
+ elsif type.match?(/xml/)
186
+ redact_xml(body)
187
+ else
188
+ body
189
+ end
190
+ end
191
+
192
+ def body_allowed?(content_type)
193
+ BODY_ALLOWED_TYPES.any? { |pattern| pattern.match?(content_type) }
194
+ end
195
+
196
+ def redact_json(json)
197
+ @json_patterns ||= @filter_list.map do |keyword|
198
+ keyword = Regexp.escape(keyword)
199
+ Regexp.compile("\"(#{keyword})\"(\\s*):(\\s*)(?>\".+?(?<!\\\\)\"|\\d+(?>\\.\\d+)?)", Regexp::IGNORECASE | Regexp::EXTENDED)
200
+ end
201
+
202
+ @json_patterns.inject(json) do |carry, pattern|
203
+ carry.gsub(pattern, '"\1"\2:\3"[FILTERED]"')
204
+ end
205
+ end
206
+
207
+ def redact_xml(raw)
208
+ @xml_patterns ||= @filter_list.map do |keyword|
209
+ keyword = Regexp.escape(keyword)
210
+ Regexp.compile("<#{keyword}(?>.|\\n)+?</(#{keyword})>", Regexp::IGNORECASE | Regexp::EXTENDED)
211
+ end
212
+
213
+ @xml_patterns.inject(raw) do |carry, pattern|
214
+ carry.gsub(pattern, '<\1>[FILTERED]</\1>')
215
+ end
216
+ end
217
+ end
218
+ end
@@ -2,14 +2,47 @@
2
2
 
3
3
  module MintHttp
4
4
  class Response
5
+ # @!attribute [r] net_request
6
+ # @return [Net::HTTPRequest]
7
+ attr_reader :net_request
8
+
9
+ # @!attribute [r] net_response
10
+ # @return [Net::HTTPResponse]
5
11
  attr_reader :net_response
12
+
13
+ # @!attribute [r] mint_request
14
+ # @return [MintHttp::Request]
15
+ attr_reader :mint_request
16
+
17
+ # @!attribute [r] version
18
+ # @return [String]
6
19
  attr_reader :version
20
+
21
+ # @!attribute [r] status_code
22
+ # @return [Integer]
7
23
  attr_reader :status_code
24
+
25
+ # @!attribute [r] status_text
26
+ # @return [String]
8
27
  attr_reader :status_text
28
+
29
+ # @!attribute [r] headers
30
+ # @return [Hash<String,Array[String]>]
9
31
  attr_reader :headers
10
32
 
11
- def initialize(net_response)
33
+ attr_accessor :time_started
34
+ attr_accessor :time_ended
35
+ attr_accessor :time_connected
36
+ attr_accessor :time_total
37
+ attr_accessor :time_connecting
38
+
39
+ # @param [Net::HTTPResponse] net_response
40
+ # @param [Net::HTTPRequest] net_request
41
+ # @param [MintHttp::Request] mint_request
42
+ def initialize(net_response, net_request, mint_request)
12
43
  @net_response = net_response
44
+ @net_request = net_request
45
+ @mint_request = mint_request
13
46
  @version = net_response.http_version
14
47
  @status_code = net_response.code.to_i
15
48
  @status_text = net_response.message
@@ -69,6 +102,18 @@ module MintHttp
69
102
  @json ||= JSON.parse(body)
70
103
  end
71
104
 
105
+ def json?
106
+ headers['content-type']&.include?('application/json')
107
+ end
108
+
109
+ def xml?
110
+ headers['content-type']&.match?(/\/xml/)
111
+ end
112
+
113
+ def https?
114
+ @net_request
115
+ end
116
+
72
117
  def inspect
73
118
  "#<#{self.class}/#{@version} #{@status_code} #{@status_text} #{@headers.inspect}>"
74
119
  end
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module MintHttp
4
- VERSION = "0.1.8"
4
+ VERSION = "0.1.9"
5
5
  end
data/lib/mint_http.rb CHANGED
@@ -1,6 +1,9 @@
1
1
  # frozen_string_literal: true
2
2
 
3
+ require 'logger'
4
+
3
5
  require_relative 'mint_http/version'
6
+ require_relative 'mint_http/config'
4
7
  require_relative 'mint_http/pool_entry'
5
8
  require_relative 'mint_http/pool'
6
9
  require_relative 'mint_http/headers'
@@ -11,11 +14,24 @@ require_relative 'mint_http/errors/authorization_error'
11
14
  require_relative 'mint_http/errors/authentication_error'
12
15
  require_relative 'mint_http/errors/not_found_error'
13
16
  require_relative 'mint_http/net_http_factory'
17
+ require_relative 'mint_http/request_logger'
14
18
  require_relative 'mint_http/response'
15
19
  require_relative 'mint_http/request'
16
20
 
17
21
  module MintHttp
18
22
  class << self
23
+ def init_mint
24
+ config.logger = Logger.new('/dev/null')
25
+ config.filter_params_list = []
26
+ config.filter_params = false
27
+ end
28
+
29
+ # @return [MintHttp::Config]
30
+ # noinspection RbsMissingTypeSignature,RubyClassVariableUsageInspection
31
+ def config
32
+ @@config ||= MintHttp::Config.new
33
+ end
34
+
19
35
  # @return [::MintHttp::Request]
20
36
  def method_missing(method, *args)
21
37
  request = Request.new
@@ -23,3 +39,5 @@ module MintHttp
23
39
  end
24
40
  end
25
41
  end
42
+
43
+ MintHttp.init_mint
metadata CHANGED
@@ -1,14 +1,14 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: mint_http
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.1.8
4
+ version: 0.1.9
5
5
  platform: ruby
6
6
  authors:
7
7
  - Ali Alhoshaiyan
8
8
  autorequire:
9
9
  bindir: exe
10
10
  cert_chain: []
11
- date: 2024-06-06 00:00:00.000000000 Z
11
+ date: 2024-07-20 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: net-http
@@ -95,6 +95,7 @@ files:
95
95
  - README.md
96
96
  - Rakefile
97
97
  - lib/mint_http.rb
98
+ - lib/mint_http/config.rb
98
99
  - lib/mint_http/errors/authentication_error.rb
99
100
  - lib/mint_http/errors/authorization_error.rb
100
101
  - lib/mint_http/errors/client_error.rb
@@ -106,6 +107,7 @@ files:
106
107
  - lib/mint_http/pool.rb
107
108
  - lib/mint_http/pool_entry.rb
108
109
  - lib/mint_http/request.rb
110
+ - lib/mint_http/request_logger.rb
109
111
  - lib/mint_http/response.rb
110
112
  - lib/mint_http/version.rb
111
113
  - sig/mint_http.rbs
@@ -130,7 +132,7 @@ required_rubygems_version: !ruby/object:Gem::Requirement
130
132
  - !ruby/object:Gem::Version
131
133
  version: '0'
132
134
  requirements: []
133
- rubygems_version: 3.2.33
135
+ rubygems_version: 3.4.10
134
136
  signing_key:
135
137
  specification_version: 4
136
138
  summary: A small fluent HTTP client.