mt-uv-rays 2.4.7

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