mint_http 0.1.8 → 0.1.9

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 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.