mt-uv-rays 2.4.7

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