mt-uv-rays 2.4.7

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.
@@ -0,0 +1,262 @@
1
+ # encoding: ASCII-8BIT
2
+ # frozen_string_literal: true
3
+
4
+ require 'uri'
5
+ require 'cgi'
6
+ require 'rubyntlm'
7
+ require 'net/http/digest_auth'
8
+
9
+ module MTUV
10
+ module Http
11
+ class Request < ::MTLibuv::Q::DeferredPromise
12
+ include Encoding
13
+
14
+
15
+ COOKIE = 'cookie'
16
+ CONNECTION = :Connection
17
+ CRLF="\r\n"
18
+
19
+
20
+ attr_reader :path, :method, :options
21
+
22
+
23
+ def cookies_hash
24
+ @cookiejar.get_hash(@uri)
25
+ end
26
+
27
+ def set_cookie(value)
28
+ @cookiejar.set(@uri, value)
29
+ end
30
+
31
+
32
+ def initialize(endpoint, options)
33
+ super(endpoint.thread, endpoint.thread.defer)
34
+
35
+ @host = endpoint.host
36
+ @port = endpoint.port
37
+ @http_proxy = endpoint.http_proxy? ? endpoint.proxy : nil
38
+ @encoded_host = endpoint.encoded_host
39
+
40
+ path = options[:path]
41
+ if path.is_a?(::URI)
42
+ @path = path.to_s.split(@encoded_host)[1] || '/'
43
+ elsif path.include?("://")
44
+ @path = path.split(@encoded_host)[1] || '/'
45
+ else
46
+ @path = path
47
+ end
48
+
49
+ @method = options[:method]
50
+ @cookiejar = endpoint.cookiejar
51
+ @middleware = endpoint.middleware
52
+ @uri = "#{endpoint.scheme}://#{@encoded_host}#{@path}"
53
+ @path = @uri if @http_proxy
54
+ endpoint = nil
55
+
56
+ @options = options
57
+ @ntlm_creds = options[:ntlm]
58
+ @digest_creds = options[:digest]
59
+ @challenge_retries = 0
60
+
61
+ # Don't hold references to vars we don't require anymore
62
+ self.finally {
63
+ @host = @port = nil
64
+ @cookiejar = nil
65
+ @middleware = nil
66
+ }
67
+ @error = proc { |reason| reject(reason) }
68
+ end
69
+
70
+
71
+
72
+ def resolve(response, parser = nil)
73
+ if response.status == 401 && @challenge_retries == 0 && response[:"WWW-Authenticate"]
74
+ challenge = Array(response[:"WWW-Authenticate"]).reject { |auth| auth.downcase == 'negotiate' }[0]
75
+
76
+ begin
77
+ if @ntlm_creds && challenge[0..3] == 'NTLM'
78
+ @options[:headers] ||= {}
79
+ @options[:headers][:Authorization] = ntlm_auth_header(challenge)
80
+ @challenge_retries += 1
81
+
82
+ execute(@transport)
83
+ return false
84
+ elsif @digest_creds && challenge[0..5] == 'Digest'
85
+ @options[:headers] ||= {}
86
+ @options[:headers][:Authorization] = digest_auth_header(challenge)
87
+ @challenge_retries += 1
88
+
89
+ execute(@transport)
90
+ return false
91
+ end
92
+ rescue => e
93
+ reject e
94
+ true
95
+ end
96
+ end
97
+
98
+ @transport = nil
99
+ @defer.resolve(response)
100
+ true
101
+ end
102
+
103
+ def reject(reason)
104
+ @defer.reject(reason)
105
+ end
106
+
107
+ def execute(transport)
108
+ # configure ntlm request headers
109
+ if @options[:ntlm]
110
+ @options[:headers] ||= {}
111
+ @options[:headers][:Authorization] ||= ntlm_auth_header
112
+ end
113
+
114
+ head, body = build_request, @options[:body]
115
+ @transport = transport
116
+
117
+ @middleware.each do |m|
118
+ begin
119
+ head, body = m.request(self, head, body) if m.respond_to?(:request)
120
+ rescue => e
121
+ reject e
122
+ return
123
+ end
124
+ end
125
+
126
+ body = body.is_a?(Hash) ? form_encode_body(body) : body
127
+ file = @options[:file]
128
+ query = @options[:query]
129
+
130
+ # Set the Content-Length if file is given
131
+ head['content-length'] = File.size(file) if file
132
+
133
+ # Set the Content-Length if body is given,
134
+ # or we're doing an empty post or put
135
+ if body
136
+ head['content-length'] = body.bytesize
137
+ elsif method == :post or method == :put
138
+ # wont happen if body is set and we already set content-length above
139
+ head['content-length'] ||= 0
140
+ end
141
+
142
+ # Set content-type header if missing and body is a Ruby hash
143
+ if !head['content-type'] and @options[:body].is_a? Hash
144
+ head['content-type'] = 'application/x-www-form-urlencoded'
145
+ end
146
+
147
+ request_header = encode_request(method, @path, query)
148
+ if @http_proxy && (@http_proxy[:username] || @http_proxy[:password])
149
+ request_header << encode_auth('Proxy-Authorization', [@http_proxy[:username], @http_proxy[:password]])
150
+ end
151
+ request_header << encode_headers(head)
152
+ request_header << CRLF
153
+
154
+ if body
155
+ request_header << body
156
+ transport.write(request_header).catch &@error
157
+ elsif file
158
+ transport.write(request_header).catch &@error
159
+
160
+ # Send file
161
+ fileRef = @reactor.file file, File::RDONLY do
162
+ # File is open and available for reading
163
+ pSend = fileRef.send_file(transport, using: :raw, wait: :promise)
164
+ pSend.catch &@error
165
+ pSend.finally do
166
+ fileRef.close
167
+ end
168
+ end
169
+ fileRef.catch &@error
170
+ else
171
+ transport.write(request_header).catch &@error
172
+ end
173
+ end
174
+
175
+ def notify(*args)
176
+ @defer.notify(*args)
177
+ end
178
+
179
+ def set_headers(head)
180
+ @headers_callback.call(head) if @headers_callback
181
+ end
182
+
183
+ def on_headers(&callback)
184
+ @headers_callback = callback
185
+ end
186
+
187
+ def streaming?
188
+ @options[:streaming]
189
+ end
190
+
191
+
192
+ protected
193
+
194
+
195
+ def build_request
196
+ head = @options[:headers] ? munge_header_keys(@options[:headers]) : {}
197
+
198
+ # Set the cookie header if provided
199
+ @cookies = @cookiejar.get(@uri)
200
+ if cookie = head[COOKIE]
201
+ @cookies << encode_cookie(cookie)
202
+ end
203
+ head[COOKIE] = @cookies.compact.uniq.join("; ").squeeze(";") unless @cookies.empty?
204
+
205
+ # Set connection close unless keep-alive
206
+ if !@options[:keepalive]
207
+ head['connection'] = 'close'
208
+ end
209
+
210
+ # Set the Host header if it hasn't been specified already
211
+ head['host'] ||= @encoded_host
212
+
213
+ # Set the User-Agent if it hasn't been specified
214
+ if !head.key?('user-agent')
215
+ head['user-agent'] = "MTUV HttpClient"
216
+ elsif head['user-agent'].nil?
217
+ head.delete('user-agent')
218
+ end
219
+
220
+ head
221
+ end
222
+
223
+ def ntlm_auth_header(challenge = nil)
224
+ if @ntlm_auth && challenge.nil?
225
+ return @ntlm_auth
226
+ elsif challenge
227
+ scheme, param_str = parse_ntlm_challenge_header(challenge)
228
+ if param_str.nil?
229
+ @ntlm_auth = nil
230
+ return ntlm_auth_header(@ntlm_creds)
231
+ else
232
+ t2 = Net::NTLM::Message.decode64(param_str)
233
+ t3 = t2.response(@ntlm_creds, ntlmv2: true)
234
+ @ntlm_auth = "NTLM #{t3.encode64}"
235
+ return @ntlm_auth
236
+ end
237
+ else
238
+ domain = @ntlm_creds[:domain]
239
+ t1 = Net::NTLM::Message::Type1.new()
240
+ t1.domain = domain if domain
241
+ @ntlm_auth = "NTLM #{t1.encode64}"
242
+ return @ntlm_auth
243
+ end
244
+ end
245
+
246
+ def parse_ntlm_challenge_header(challenge)
247
+ scheme, param_str = challenge.scan(/\A(\S+)(?:\s+(.*))?\z/)[0]
248
+ return nil if scheme.nil?
249
+ return scheme, param_str
250
+ end
251
+
252
+ def digest_auth_header(challenge)
253
+ uri = URI.parse @uri
254
+ uri.userinfo = "#{CGI::escape(@digest_creds[:user])}:#{CGI::escape(@digest_creds[:password])}"
255
+
256
+ digest_auth = Net::HTTP::DigestAuth.new
257
+ auth = digest_auth.auth_header uri, challenge, method.to_s.upcase
258
+ auth
259
+ end
260
+ end
261
+ end
262
+ end
@@ -0,0 +1,336 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'uri'
4
+ require 'cookiejar' # Manages cookies
5
+ require 'http-parser' # Parses HTTP request / responses
6
+ require 'addressable/uri' # URI parser
7
+ require 'mt-uv-rays/http/encoding'
8
+ require 'mt-uv-rays/http/request'
9
+ require 'mt-uv-rays/http/parser'
10
+
11
+ module MTUV
12
+ class CookieJar
13
+ def initialize
14
+ @jar = ::CookieJar::Jar.new
15
+ end
16
+
17
+ def set(uri, string)
18
+ @jar.set_cookie(uri, string) rescue nil # drop invalid cookies
19
+ end
20
+
21
+ def get(uri)
22
+ uri = URI.parse(uri) rescue nil
23
+ uri ? @jar.get_cookies(uri).map(&:to_s) : []
24
+ end
25
+
26
+ def get_hash(uri)
27
+ uri = URI.parse(uri) rescue nil
28
+ cookies = {}
29
+ if uri
30
+ @jar.get_cookies(uri).each do |cookie|
31
+ cookies[cookie.name.to_sym] = cookie.value
32
+ end
33
+ end
34
+ cookies
35
+ end
36
+
37
+ def clear_cookies
38
+ @jar = ::CookieJar::Jar.new
39
+ end
40
+ end # CookieJar
41
+
42
+ # HTTPS Proxy - connect to proxy
43
+ # CONNECT #{target_host}:#{target_port} HTTP/1.0\r\n"
44
+ # Proxy-Authorization: Basic #{encoded_credentials}\r\n
45
+ # \r\n
46
+ # Parse response =~ %r{\AHTTP/1\.[01] 200 .*\r\n\r\n}m
47
+ # use_tls
48
+ # send requests as usual
49
+
50
+ # HTTP Proxy - connect to proxy
51
+ # GET #{url_with_host} HTTP/1.1\r\n"
52
+ # Proxy-Authorization: Basic #{encoded_credentials}\r\n
53
+ # \r\n
54
+
55
+ class HttpEndpoint
56
+ class Connection < OutboundConnection
57
+ def initialize(host, port, tls, proxy, client)
58
+ @target_host = host
59
+ @client = client
60
+ @request = nil
61
+
62
+ if proxy
63
+ super(proxy[:host], proxy[:port])
64
+ if tls
65
+ @negotiating = true
66
+ @proxy = proxy
67
+ @connect_host = host
68
+ @connect_port = port
69
+ end
70
+ else
71
+ super(host, port)
72
+ start_tls if tls
73
+ end
74
+ end
75
+
76
+ def start_tls
77
+ opts = {host_name: @target_host}.merge(@client.tls_options)
78
+ use_tls(opts)
79
+ end
80
+
81
+ def connect_send_handshake(target_host, target_port, proxy)
82
+ header = String.new("CONNECT #{target_host}:#{target_port} HTTP/1.0\r\n")
83
+ if proxy[:username] || proxy[:password]
84
+ encoded_credentials = Base64.strict_encode64([proxy[:username], proxy[:password]].join(":"))
85
+ header << "Proxy-Authorization: Basic #{encoded_credentials}\r\n"
86
+ end
87
+ header << "\r\n"
88
+ write(header)
89
+ end
90
+
91
+ attr_accessor :request, :reason
92
+
93
+ def on_read(data, *args)
94
+ if @negotiating
95
+ @negotiating = false
96
+ if data =~ %r{\AHTTP/1\.[01] 200 .*\r\n\r\n}m
97
+ start_tls
98
+ @client.connection_ready
99
+ else
100
+ @reason = "Unexpected response from proxy: #{data}"
101
+ close_connection
102
+ end
103
+ else
104
+ @client.data_received(data)
105
+ end
106
+ end
107
+
108
+ def post_init(*args)
109
+ end
110
+
111
+ def on_connect(transport)
112
+ if @negotiating
113
+ connect_send_handshake(@connect_host, @connect_port, @proxy)
114
+ else
115
+ @client.connection_ready
116
+ end
117
+ end
118
+
119
+ def on_close
120
+ @client.connection_closed(@request, @reason)
121
+ ensure
122
+ @request = nil
123
+ @client = nil
124
+ @reason = nil
125
+ end
126
+
127
+ def close_connection(request = nil)
128
+ if request.is_a? Http::Request
129
+ @request = request
130
+ super(:after_writing)
131
+ else
132
+ super(request)
133
+ end
134
+ end
135
+ end
136
+
137
+
138
+ @@defaults = {
139
+ :path => '/',
140
+ :keepalive => true
141
+ }
142
+
143
+
144
+ def initialize(host, options = {})
145
+ @queue = []
146
+ @parser = Http::Parser.new
147
+ @thread = reactor
148
+ @connection = nil
149
+
150
+ @options = @@defaults.merge(options)
151
+ @tls_options = options[:tls_options] || {}
152
+ @inactivity_timeout = options[:inactivity_timeout] || 10000
153
+
154
+ uri = host.is_a?(::URI) ? host : ::URI.parse(host)
155
+ @port = uri.port
156
+ @host = uri.host
157
+
158
+ default_port = uri.port == uri.default_port
159
+ @encoded_host = default_port ? @host : "#{@host}:#{@port}"
160
+ @proxy = @options[:proxy]
161
+
162
+ @scheme = uri.scheme
163
+ @tls = @scheme == 'https'
164
+ @cookiejar = CookieJar.new
165
+ @middleware = []
166
+
167
+ @closing = false
168
+ @connecting = false
169
+ end
170
+
171
+
172
+ attr_accessor :inactivity_timeout
173
+ attr_reader :tls_options, :port, :host, :tls, :scheme, :encoded_host
174
+ attr_reader :cookiejar, :middleware, :thread, :proxy
175
+
176
+
177
+ def get(options = {}); request(:get, options); end
178
+ def head(options = {}); request(:head, options); end
179
+ def delete(options = {}); request(:delete, options); end
180
+ def put(options = {}); request(:put, options); end
181
+ def post(options = {}); request(:post, options); end
182
+ def patch(options = {}); request(:patch, options); end
183
+ def options(options = {}); request(:options, options); end
184
+
185
+
186
+ def request(method, options = {})
187
+ options = @options.merge(options)
188
+ options[:method] = method.to_sym
189
+
190
+ # Setup the request with callbacks
191
+ request = Http::Request.new(self, options)
192
+ request.then(proc { |response|
193
+ if response.keep_alive
194
+ restart_timer
195
+ else
196
+ # We might have already started processing the next request
197
+ # at this point. So don't want to disconnect if already
198
+ # disconnected.
199
+ close_connection unless @connecting
200
+ end
201
+
202
+ next_request
203
+
204
+ response
205
+ }, proc { |err|
206
+ # @parser.eof
207
+ close_connection unless @connecting
208
+ next_request
209
+ ::MTLibuv::Q.reject(@thread, err)
210
+ })
211
+
212
+ @queue.unshift(request)
213
+
214
+ next_request
215
+ request
216
+ end
217
+
218
+ # Callbacks
219
+ def connection_ready
220
+ # A connection can be closed while still connecting
221
+ return if @closing
222
+
223
+ @connecting = false
224
+ if @queue.length > 0
225
+ restart_timer
226
+ next_request
227
+ else
228
+ close_connection
229
+ end
230
+ end
231
+
232
+ def connection_closed(request, reason)
233
+ # A connection might close due to a connection failure
234
+ awaiting_close = @closing
235
+ awaiting_connect = @connecting
236
+ @closing = false
237
+ @connecting = false
238
+ @connection = nil
239
+
240
+ # We may have closed a previous connection
241
+ if @parser.request && (request.nil? || request == @parser.request)
242
+ stop_timer
243
+ @parser.eof
244
+ elsif request.nil? && @parser.request.nil? && @queue.length > 0
245
+ req = @queue.pop
246
+ req.reject(reason || :connection_failure)
247
+ end
248
+
249
+ next_request if awaiting_close || awaiting_connect
250
+ end
251
+
252
+ def data_received(data)
253
+ restart_timer
254
+ close_connection if @parser.received(data)
255
+ end
256
+
257
+ def cancel_all
258
+ @queue.each do |request|
259
+ request.reject(:cancelled)
260
+ end
261
+ if @parser.request
262
+ @parser.request.reject(:cancelled)
263
+ @parser.eof
264
+ end
265
+ @queue = []
266
+ close_connection
267
+ end
268
+
269
+ def http_proxy?
270
+ @proxy && !@tls
271
+ end
272
+
273
+
274
+ private
275
+
276
+
277
+ def next_request
278
+ # Don't start a request while transitioning state
279
+ return if @closing || @connecting
280
+ return if @parser.request || @queue.length == 0
281
+
282
+ if @connection
283
+ req = @queue.pop
284
+ @connection.request = req
285
+ @parser.new_request(req)
286
+
287
+ req.execute(@connection)
288
+ else
289
+ new_connection
290
+ end
291
+ end
292
+
293
+ def new_connection
294
+ # no new connections while transitioning state
295
+ return if @closing || @connecting
296
+ if @queue.length > 0 && @connection.nil?
297
+ @connecting = true
298
+ @connection = Connection.new(@host, @port, @tls, @proxy, self)
299
+ start_timer
300
+ end
301
+ @connection
302
+ end
303
+
304
+ def close_connection
305
+ # Close connection can be called while connecting
306
+ return if @closing || @connection.nil?
307
+ @closing = true
308
+ @connection.close_connection
309
+ stop_timer
310
+ @connection = nil
311
+ end
312
+
313
+ def start_timer
314
+ # Only start the timer if there is a connection starting or in place
315
+ return if @closing || @connection.nil?
316
+ @timer.cancel if @timer
317
+ @timer = @thread.scheduler.in(@inactivity_timeout) do
318
+ @timer = nil
319
+ idle_timeout
320
+ end
321
+ end
322
+ alias_method :restart_timer, :start_timer
323
+
324
+ def stop_timer
325
+ @timer.cancel unless @timer.nil?
326
+ @timer = nil
327
+ end
328
+
329
+ def idle_timeout
330
+ connection = @connection
331
+ close_connection
332
+ @parser.reason = :timeout if @parser.request
333
+ connection.reason = :timeout if connection
334
+ end
335
+ end
336
+ end