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.
checksums.yaml ADDED
@@ -0,0 +1,7 @@
1
+ ---
2
+ SHA256:
3
+ metadata.gz: 83172a1213c66ea2b83c177a7f7a374498d8167a286ce71bd829fc5786ccd0bb
4
+ data.tar.gz: 7b9b3620628d7587e06f38ac57bfc9aa7037d1442d3defdf0d6ba95d5cadebde
5
+ SHA512:
6
+ metadata.gz: 2225d0202e4ff24e1da86b9be15c1d81baf01a00548006f557da22c3032c9486a0b78124bf2853a253c70f9aed538411fb8dbe2f0c7d3ed2987a5956d2d22caf
7
+ data.tar.gz: 4ce9366261183a0ab2699da88fc404e8d3c52ef9d7073180ebf7547e3f90cf940b256755d61c45c159f51e9954072e6b666ebcc665786e7ff46ef0d4dffe62bd
data/LICENSE ADDED
@@ -0,0 +1,20 @@
1
+ The MIT License (MIT)
2
+
3
+ Copyright (c) 2013 CoTag Media
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy of
6
+ this software and associated documentation files (the "Software"), to deal in
7
+ the Software without restriction, including without limitation the rights to
8
+ use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of
9
+ the Software, and to permit persons to whom the Software is furnished to do so,
10
+ subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS
17
+ FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR
18
+ COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER
19
+ IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN
20
+ CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
data/README.md ADDED
@@ -0,0 +1,63 @@
1
+ # uv-rays
2
+
3
+ [![Build Status](https://travis-ci.org/cotag/uv-rays.svg?branch=master)](https://travis-ci.org/cotag/uv-rays)
4
+
5
+ UV-Rays was designed to eliminate the complexities of high-performance threaded network programming, allowing engineers to concentrate on their application logic.
6
+
7
+
8
+ ## Core Features
9
+
10
+ 1. TCP (and UDP) Connection abstractions
11
+ 2. Advanced stream tokenization
12
+ 3. Scheduled events (in, at, every, cron)
13
+ 4. HTTP 1.1 compatible client support
14
+
15
+ This adds to the features already available from [Libuv](https://github.com/cotag/libuv) on which the gem is based
16
+
17
+
18
+ ## Support
19
+
20
+ UV-Rays supports all platforms where ruby is available. Linux, OSX, BSD and Windows. MRI, jRuby and Rubinius.
21
+
22
+ Run `gem install uv-rays` to install
23
+
24
+
25
+ ## Getting Started
26
+
27
+ Here's a fully-functional echo server written with UV-Rays:
28
+
29
+ ```ruby
30
+ require 'uv-rays'
31
+
32
+ module EchoServer
33
+ def on_connect(socket)
34
+ @ip, @port = socket.peername
35
+ logger.info "-- #{@ip}:#{@port} connected"
36
+ end
37
+
38
+ def on_read(data, socket)
39
+ write ">>>you sent: #{data}"
40
+ close_connection if data =~ /quit/i
41
+ end
42
+
43
+ def on_close
44
+ puts "-- #{@ip}:#{@port} disconnected"
45
+ end
46
+ end
47
+
48
+ reactor {
49
+ UV.start_server "127.0.0.1", 8081, EchoServer
50
+ }
51
+
52
+ ```
53
+
54
+ # Integrations
55
+
56
+ UV-Rays works with many existing GEMs by integrating into common HTTP abstraction libraries
57
+
58
+ * [Faraday](https://github.com/lostisland/faraday)
59
+ * [HTTPI](https://github.com/savonrb/httpi)
60
+ * [Handsoap](https://github.com/unwire/handsoap)
61
+
62
+
63
+
data/Rakefile ADDED
@@ -0,0 +1,22 @@
1
+ require 'rubygems'
2
+ require 'rspec/core/rake_task' # testing framework
3
+ require 'yard' # yard documentation
4
+
5
+
6
+
7
+ # By default we don't run network tests
8
+ task :default => :limited_spec
9
+ RSpec::Core::RakeTask.new(:limited_spec) do |t|
10
+ # Exclude network tests
11
+ t.rspec_opts = "--tag ~mri_only --tag ~travis_skip"
12
+ end
13
+ RSpec::Core::RakeTask.new(:spec)
14
+
15
+
16
+ desc "Run all tests"
17
+ task :test => [:spec]
18
+
19
+
20
+ YARD::Rake::YardocTask.new do |t|
21
+ t.files = ['lib/**/*.rb', '-', 'ext/README.md', 'README.md']
22
+ end
@@ -0,0 +1,89 @@
1
+ require 'faraday'
2
+ require 'mt-uv-rays'
3
+
4
+
5
+ module Faraday
6
+ class Adapter < Middleware
7
+ register_middleware libuv: :MTLibuv
8
+
9
+ class MTLibuv < Faraday::Adapter
10
+ def initialize(app, connection_options = {})
11
+ @connection_options = connection_options
12
+ super(app)
13
+ end
14
+
15
+ def call(env)
16
+ super
17
+
18
+ opts = {}
19
+ if env[:url].scheme == 'https' && ssl = env[:ssl]
20
+ tls_opts = opts[:tls_options] = {}
21
+
22
+ # opts[:ssl_verify_peer] = !!ssl.fetch(:verify, true)
23
+ # TODO:: Need to provide verify callbacks
24
+
25
+ tls_opts[:cert_chain] = ssl[:ca_path] if ssl[:ca_path]
26
+ tls_opts[:client_ca] = ssl[:ca_file] if ssl[:ca_file]
27
+ #tls_opts[:client_cert] = ssl[:client_cert] if ssl[:client_cert]
28
+ #tls_opts[:client_key] = ssl[:client_key] if ssl[:client_key]
29
+ #tls_opts[:certificate] = ssl[:certificate] if ssl[:certificate]
30
+ tls_opts[:private_key] = ssl[:private_key] if ssl[:private_key]
31
+ end
32
+
33
+ if (req = env[:request])
34
+ opts[:inactivity_timeout] = (req[:timeout] * 1000) if req[:timeout]
35
+ end
36
+
37
+ if proxy = env[:request][:proxy]
38
+ opts[:proxy] = {
39
+ host: proxy[:uri].host,
40
+ port: proxy[:uri].port,
41
+ username: proxy[:user],
42
+ password: proxy[:password]
43
+ }
44
+ end
45
+
46
+ error = nil
47
+ thread = reactor
48
+ if thread.running?
49
+ error = perform_request(env, opts)
50
+ else
51
+ # Pretty much here for testing
52
+ thread.run {
53
+ error = perform_request(env, opts)
54
+ }
55
+ end
56
+
57
+ # Re-raise the error out of the event loop
58
+ # Really this is only required for tests as this will always run on the reactor
59
+ raise error if error
60
+ @app.call env
61
+ rescue ::CoroutineRejection => err
62
+ if err.value == :timeout
63
+ raise Error::TimeoutError, err
64
+ else
65
+ raise Error::ConnectionFailed, err
66
+ end
67
+ end
68
+
69
+ # TODO: support streaming requests
70
+ def read_body(env)
71
+ env[:body].respond_to?(:read) ? env[:body].read : env[:body]
72
+ end
73
+ end
74
+
75
+ def perform_request(env, opts)
76
+ conn = ::UV::HttpEndpoint.new(env[:url].to_s, opts.merge!(@connection_options))
77
+ resp = conn.request(env[:method].to_s.downcase.to_sym,
78
+ headers: env[:request_headers],
79
+ path: "/#{env[:url].to_s.split('/', 4)[-1]}",
80
+ keepalive: false,
81
+ body: read_body(env)).value
82
+
83
+ save_response(env, resp.status.to_i, resp.body, resp) #, resp.reason_phrase)
84
+ nil
85
+ rescue Exception => e
86
+ e
87
+ end
88
+ end
89
+ end
@@ -0,0 +1,43 @@
1
+ # encoding: ASCII-8BIT
2
+ # frozen_string_literal: true
3
+
4
+ require 'handsoap'
5
+
6
+ module Handsoap
7
+ module Http
8
+ module Drivers
9
+ class MTLibuvDriver < AbstractDriver
10
+ def self.load!
11
+ require 'mt-uv-rays'
12
+ end
13
+
14
+ def send_http_request_async(request)
15
+ endp = ::UV::HttpEndpoint.new(request.url)
16
+
17
+ if request.username && request.password
18
+ request.headers['Authorization'] = [request.username, request.password]
19
+ end
20
+
21
+ req = endp.request(request.http_method, {
22
+ headers: request.headers,
23
+ body: request.body
24
+ })
25
+
26
+ deferred = ::Handsoap::Deferred.new
27
+ req.then do |resp|
28
+ # Downcase headers and convert values to arrays
29
+ headers = Hash[resp.map { |k, v| [k.to_s.downcase, Array(v)] }]
30
+ http_response = parse_http_part(headers, resp.body, resp.status)
31
+ deferred.trigger_callback http_response
32
+ end
33
+ req.catch do |err|
34
+ deferred.trigger_errback err
35
+ end
36
+ deferred
37
+ end
38
+ end
39
+ end
40
+
41
+ @@drivers[:libuv] = ::Handsoap::Http::Drivers::MTLibuvDriver
42
+ end
43
+ end
@@ -0,0 +1,69 @@
1
+ require 'httpi'
2
+
3
+ module HTTPI; end
4
+ module HTTPI::Adapter; end
5
+ class HTTPI::Adapter::MTLibuv < HTTPI::Adapter::Base
6
+ register :mtlibuv, deps: %w(mt-uv-rays)
7
+
8
+ def initialize(request)
9
+ @request = request
10
+ @client = ::MTUV::HttpEndpoint.new request.url
11
+ end
12
+
13
+ attr_reader :client
14
+
15
+ def request(method)
16
+ @client.inactivity_timeout = (@request.read_timeout * 1000).to_i if @request.read_timeout && @request.read_timeout > 0
17
+
18
+ req = {
19
+ path: @request.url,
20
+ headers: @request.headers,
21
+ body: @request.body
22
+ }
23
+
24
+ if proxy = @request.proxy
25
+ req[:proxy] = {
26
+ host: proxy.host,
27
+ port: proxy.port,
28
+ username: proxy.user,
29
+ password: proxy.password
30
+ }
31
+ end
32
+
33
+ # Apply authentication settings
34
+ auth = @request.auth
35
+ type = auth.type
36
+ if auth.type
37
+ creds = auth.credentials
38
+
39
+ case auth.type
40
+ when :basic
41
+ req[:headers][:Authorization] = creds
42
+ when :digest
43
+ req[:digest] = {
44
+ user: creds[0],
45
+ password: creds[1]
46
+ }
47
+ when :ntlm
48
+ req[:ntlm] = {
49
+ username: creds[0],
50
+ password: creds[1],
51
+ domain: creds[2] || ''
52
+ }
53
+ end
54
+ end
55
+
56
+ # Apply Client certificates
57
+ ssl = auth.ssl
58
+ if ssl.verify_mode == :peer
59
+ tls_opts = req[:tls_options] = {}
60
+ tls_opts[:cert_chain] = ssl.cert.to_pem if ssl.cert
61
+ tls_opts[:client_ca] = ssl.ca_cert_file if ssl.ca_cert_file
62
+ tls_opts[:private_key] = ssl.cert_key.to_pem if ssl.cert_key
63
+ end
64
+
65
+ # Use co-routines to make non-blocking requests
66
+ response = @client.request(method, req).value
67
+ ::HTTPI::Response.new(response.status, response, response.body)
68
+ end
69
+ end
@@ -0,0 +1,121 @@
1
+ # encoding: ASCII-8BIT
2
+ # frozen_string_literal: true
3
+
4
+ module MTUV
5
+
6
+ # AbstractTokenizer is similar to BufferedTokernizer however should
7
+ # only be used when there is no delimiter to work with. It uses a
8
+ # callback based system for application level tokenization without
9
+ # the heavy lifting.
10
+ class AbstractTokenizer
11
+ DEFAULT_ENCODING = 'ASCII-8BIT'
12
+
13
+ attr_accessor :callback, :indicator, :size_limit, :verbose
14
+
15
+ # @param [Hash] options
16
+ def initialize(options)
17
+ @callback = options[:callback]
18
+ @indicator = options[:indicator]
19
+ @size_limit = options[:size_limit]
20
+ @verbose = options[:verbose] if @size_limit
21
+ @encoding = options[:encoding] || DEFAULT_ENCODING
22
+
23
+ raise ArgumentError, 'no callback provided' unless @callback
24
+
25
+ reset
26
+ if @indicator.is_a?(String)
27
+ @indicator = String.new(@indicator).force_encoding(@encoding).freeze
28
+ end
29
+ end
30
+
31
+ # Extract takes an arbitrary string of input data and returns an array of
32
+ # tokenized entities using a message start indicator
33
+ #
34
+ # @example
35
+ #
36
+ # tokenizer.extract(data).
37
+ # map { |entity| Decode(entity) }.each { ... }
38
+ #
39
+ # @param [String] data
40
+ def extract(data)
41
+ data.force_encoding(@encoding)
42
+ @input << data
43
+
44
+ entities = []
45
+
46
+ loop do
47
+ found = false
48
+
49
+ last = if @indicator
50
+ check = @input.partition(@indicator)
51
+ break unless check[1].length > 0
52
+
53
+ check[2]
54
+ else
55
+ @input
56
+ end
57
+
58
+ result = @callback.call(last)
59
+
60
+ if result
61
+ found = true
62
+
63
+ # Check for multi-byte indicator edge case
64
+ case result
65
+ when Integer
66
+ entities << last[0...result]
67
+ @input = last[result..-1]
68
+ else
69
+ entities << last
70
+ reset
71
+ end
72
+ end
73
+
74
+ break if not found
75
+ end
76
+
77
+ # Check to see if the buffer has exceeded capacity, if we're imposing a limit
78
+ if @size_limit && @input.size > @size_limit
79
+ if @indicator.respond_to?(:length) # check for regex
80
+ # save enough of the buffer that if one character of the indicator were
81
+ # missing we would match on next extract (very much an edge case) and
82
+ # best we can do with a full buffer.
83
+ @input = @input[-(@indicator.length - 1)..-1]
84
+ else
85
+ reset
86
+ end
87
+ raise 'input buffer exceeded limit' if @verbose
88
+ end
89
+
90
+ return entities
91
+ end
92
+
93
+ # Flush the contents of the input buffer, i.e. return the input buffer even though
94
+ # a token has not yet been encountered.
95
+ #
96
+ # @return [String]
97
+ def flush
98
+ buffer = @input
99
+ reset
100
+ buffer
101
+ end
102
+
103
+ # @return [Boolean]
104
+ def empty?
105
+ @input.empty?
106
+ end
107
+
108
+ # @return [Integer]
109
+ def bytesize
110
+ @input.bytesize
111
+ end
112
+
113
+
114
+ private
115
+
116
+
117
+ def reset
118
+ @input = String.new.force_encoding(@encoding)
119
+ end
120
+ end
121
+ end
@@ -0,0 +1,176 @@
1
+ # encoding: ASCII-8BIT
2
+ # frozen_string_literal: true
3
+
4
+ # BufferedTokenizer takes a delimiter upon instantiation.
5
+ # It allows input to be spoon-fed from some outside source which receives
6
+ # arbitrary length datagrams which may-or-may-not contain the token by which
7
+ # entities are delimited.
8
+ #
9
+ # @example Using BufferedTokernizer to parse lines out of incoming data
10
+ #
11
+ # module LineBufferedConnection
12
+ # def receive_data(data)
13
+ # (@buffer ||= BufferedTokenizer.new(delimiter: "\n")).extract(data).each do |line|
14
+ # receive_line(line)
15
+ # end
16
+ # end
17
+ # end
18
+ module MTUV
19
+ class BufferedTokenizer
20
+ DEFAULT_ENCODING = 'ASCII-8BIT'
21
+
22
+ attr_accessor :delimiter, :indicator, :size_limit, :verbose
23
+
24
+ # @param [Hash] options
25
+ def initialize(options)
26
+ @delimiter = options[:delimiter]
27
+ @indicator = options[:indicator]
28
+ @msg_length = options[:msg_length]
29
+ @size_limit = options[:size_limit]
30
+ @min_length = options[:min_length] || 1
31
+ @verbose = options[:verbose] if @size_limit
32
+ @encoding = options[:encoding] || DEFAULT_ENCODING
33
+
34
+ if @delimiter
35
+ @extract_method = method(:delimiter_extract)
36
+ elsif @indicator && @msg_length
37
+ @extract_method = method(:length_extract)
38
+ else
39
+ raise ArgumentError, 'no delimiter provided'
40
+ end
41
+
42
+ init_buffer
43
+ end
44
+
45
+ # Extract takes an arbitrary string of input data and returns an array of
46
+ # tokenized entities, provided there were any available to extract.
47
+ #
48
+ # @example
49
+ #
50
+ # tokenizer.extract(data).
51
+ # map { |entity| Decode(entity) }.each { ... }
52
+ #
53
+ # @param [String] data
54
+ def extract(data)
55
+ data.force_encoding(@encoding)
56
+ @input << data
57
+
58
+ @extract_method.call
59
+ end
60
+
61
+ # Flush the contents of the input buffer, i.e. return the input buffer even though
62
+ # a token has not yet been encountered.
63
+ #
64
+ # @return [String]
65
+ def flush
66
+ buffer = @input
67
+ reset
68
+ buffer
69
+ end
70
+
71
+ # @return [Boolean]
72
+ def empty?
73
+ @input.empty?
74
+ end
75
+
76
+ # @return [Integer]
77
+ def bytesize
78
+ @input.bytesize
79
+ end
80
+
81
+
82
+ private
83
+
84
+
85
+ def delimiter_extract
86
+ # Extract token-delimited entities from the input string with the split command.
87
+ # There's a bit of craftiness here with the -1 parameter. Normally split would
88
+ # behave no differently regardless of if the token lies at the very end of the
89
+ # input buffer or not (i.e. a literal edge case) Specifying -1 forces split to
90
+ # return "" in this case, meaning that the last entry in the list represents a
91
+ # new segment of data where the token has not been encountered
92
+ messages = @input.split(@delimiter, -1)
93
+
94
+ if @indicator
95
+ @input = messages.pop || empty_string
96
+ entities = []
97
+ messages.each do |msg|
98
+ res = msg.split(@indicator, -1)
99
+ entities << res.last if res.length > 1
100
+ end
101
+ else
102
+ entities = messages
103
+ @input = entities.pop || empty_string
104
+ end
105
+
106
+ check_buffer_limits
107
+
108
+ # Check min-length is met
109
+ entities.select! {|msg| msg.length >= @min_length}
110
+
111
+ return entities
112
+ end
113
+
114
+ def length_extract
115
+ messages = @input.split(@indicator, -1)
116
+ messages.shift # discard junk data
117
+
118
+ last = messages.pop || empty_string
119
+
120
+ # Select messages of the right size then remove junk data
121
+ messages.select! { |msg| msg.length >= @msg_length ? true : false }
122
+ messages.map! { |msg| msg[0...@msg_length] }
123
+
124
+ if last.length >= @msg_length
125
+ messages << last[0...@msg_length]
126
+ @input = last[@msg_length..-1]
127
+ else
128
+ reset("#{@indicator}#{last}")
129
+ end
130
+
131
+ check_buffer_limits
132
+
133
+ return messages
134
+ end
135
+
136
+ # Check to see if the buffer has exceeded capacity, if we're imposing a limit
137
+ def check_buffer_limits
138
+ if @size_limit && @input.size > @size_limit
139
+ if @indicator && @indicator.respond_to?(:length) # check for regex
140
+ # save enough of the buffer that if one character of the indicator were
141
+ # missing we would match on next extract (very much an edge case) and
142
+ # best we can do with a full buffer. If we were one char short of a
143
+ # delimiter it would be unfortunate
144
+ @input = @input[-(@indicator.length - 1)..-1]
145
+ else
146
+ reset
147
+ end
148
+ raise 'input buffer exceeded limit' if @verbose
149
+ end
150
+ end
151
+
152
+ def init_buffer
153
+ @input = empty_string
154
+
155
+ if @delimiter.is_a?(String)
156
+ @delimiter = String.new(@delimiter).force_encoding(@encoding).freeze
157
+ end
158
+
159
+ if @indicator.is_a?(String)
160
+ @indicator = String.new(@indicator).force_encoding(@encoding).freeze
161
+ end
162
+ end
163
+
164
+ def reset(value = nil)
165
+ @input = String.new(value || '').force_encoding(@encoding)
166
+ end
167
+
168
+
169
+ protected
170
+
171
+
172
+ def empty_string
173
+ String.new.force_encoding(@encoding)
174
+ end
175
+ end
176
+ end