uv-rays 0.0.1

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
+ SHA1:
3
+ metadata.gz: 3800c7d8ef446765e933dbf2f732dd037d4185f2
4
+ data.tar.gz: 8cb367445a00f73ad52e16ab4d26dac7453c1cd6
5
+ SHA512:
6
+ metadata.gz: a4812d336b5366a07b4ee4f77c888cc323a24d9f700c93ea0c0335aa7e18bc08918328ddeabd47b858ad2e6773c78ce9ebba6c4cb4a468bc449697dfd4b41b63
7
+ data.tar.gz: 07285a027794ac7f3d4332595f936ea56000b251909665e4dd02edeb5e77a7b6f74fe901617883199fd07332329c48bd03ea152db7fe40ec25bfe5515e831d48
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,53 @@
1
+ # uv-rays
2
+
3
+ [![Build Status](https://travis-ci.org/cotag/uv-rays.png?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
+
31
+ require 'uv-rays'
32
+
33
+ module EchoServer
34
+ def post_init
35
+ puts "-- someone connected to the echo server!"
36
+ end
37
+
38
+ def on_read data, *args
39
+ write ">>>you sent: #{data}"
40
+ close_connection if data =~ /quit/i
41
+ end
42
+
43
+ def on_close
44
+ puts "-- someone disconnected from the echo server!"
45
+ end
46
+ end
47
+
48
+ # Note that this will block current thread.
49
+ Libuv::Loop.default.run {
50
+ UV.start_server "127.0.0.1", 8081, EchoServer
51
+ }
52
+
53
+ ```
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 ~network"
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,100 @@
1
+
2
+ module UvRays
3
+
4
+ # AbstractTokenizer is similar to BufferedTokernizer however should
5
+ # only be used when there is no delimiter to work with. It uses a
6
+ # callback based system for application level tokenization without
7
+ # the heavy lifting.
8
+ class AbstractTokenizer
9
+
10
+ attr_accessor :callback, :indicator, :size_limit, :verbose
11
+
12
+ # @param [Hash] options
13
+ def initialize(options)
14
+ @callback = options[:callback]
15
+ @indicator = options[:indicator]
16
+ @size_limit = options[:size_limit]
17
+ @verbose = options[:verbose] if @size_limit
18
+
19
+ raise ArgumentError, 'no indicator provided' unless @indicator
20
+ raise ArgumentError, 'no callback provided' unless @callback
21
+
22
+ @input = ''
23
+ end
24
+
25
+ # Extract takes an arbitrary string of input data and returns an array of
26
+ # tokenized entities using a message start indicator
27
+ #
28
+ # @example
29
+ #
30
+ # tokenizer.extract(data).
31
+ # map { |entity| Decode(entity) }.each { ... }
32
+ #
33
+ # @param [String] data
34
+ def extract(data)
35
+ @input << data
36
+
37
+ messages = @input.split(@indicator, -1)
38
+ if messages.length > 1
39
+ messages.shift # the first item will always be junk
40
+ last = messages.pop # the last item may require buffering
41
+
42
+ entities = []
43
+ messages.each do |msg|
44
+ entities << msg if @callback.call(msg)
45
+ end
46
+
47
+ # Check if buffering is required
48
+ result = @callback.call(last)
49
+ if result
50
+ # Check for multi-byte indicator edge case
51
+ if result.is_a? Fixnum
52
+ entities << last[0...result]
53
+ @input = last[result..-1]
54
+ else
55
+ @input = ''
56
+ entities << last
57
+ end
58
+ else
59
+ # This will work with a regex
60
+ index = messages.last.nil? ? 0 : @input[0...-last.length].rindex(messages.last) + messages.last.length
61
+ indicator_val = @input[index...-last.length]
62
+ @input = indicator_val + last
63
+ end
64
+ else
65
+ @input = messages.pop
66
+ entities = messages
67
+ end
68
+
69
+ # Check to see if the buffer has exceeded capacity, if we're imposing a limit
70
+ if @size_limit && @input.size > @size_limit
71
+ if @indicator.respond_to?(:length) # check for regex
72
+ # save enough of the buffer that if one character of the indicator were
73
+ # missing we would match on next extract (very much an edge case) and
74
+ # best we can do with a full buffer.
75
+ @input = @input[-(@indicator.length - 1)..-1]
76
+ else
77
+ @input = ''
78
+ end
79
+ raise 'input buffer exceeded limit' if @verbose
80
+ end
81
+
82
+ return entities
83
+ end
84
+
85
+ # Flush the contents of the input buffer, i.e. return the input buffer even though
86
+ # a token has not yet been encountered.
87
+ #
88
+ # @return [String]
89
+ def flush
90
+ buffer = @input
91
+ @input = ''
92
+ buffer
93
+ end
94
+
95
+ # @return [Boolean]
96
+ def empty?
97
+ @input.empty?
98
+ end
99
+ end
100
+ end
@@ -0,0 +1,97 @@
1
+
2
+ # BufferedTokenizer takes a delimiter upon instantiation.
3
+ # It allows input to be spoon-fed from some outside source which receives
4
+ # arbitrary length datagrams which may-or-may-not contain the token by which
5
+ # entities are delimited.
6
+ #
7
+ # @example Using BufferedTokernizer to parse lines out of incoming data
8
+ #
9
+ # module LineBufferedConnection
10
+ # def receive_data(data)
11
+ # (@buffer ||= BufferedTokenizer.new(delimiter: "\n")).extract(data).each do |line|
12
+ # receive_line(line)
13
+ # end
14
+ # end
15
+ # end
16
+ module UvRays
17
+ class BufferedTokenizer
18
+
19
+ attr_accessor :delimiter, :indicator, :size_limit, :verbose
20
+
21
+ # @param [Hash] options
22
+ def initialize(options)
23
+ @delimiter = options[:delimiter]
24
+ @indicator = options[:indicator]
25
+ @size_limit = options[:size_limit]
26
+ @verbose = options[:verbose] if @size_limit
27
+
28
+ raise ArgumentError, 'no delimiter provided' unless @delimiter
29
+
30
+ @input = ''
31
+ end
32
+
33
+ # Extract takes an arbitrary string of input data and returns an array of
34
+ # tokenized entities, provided there were any available to extract.
35
+ #
36
+ # @example
37
+ #
38
+ # tokenizer.extract(data).
39
+ # map { |entity| Decode(entity) }.each { ... }
40
+ #
41
+ # @param [String] data
42
+ def extract(data)
43
+ @input << data
44
+
45
+ # Extract token-delimited entities from the input string with the split command.
46
+ # There's a bit of craftiness here with the -1 parameter. Normally split would
47
+ # behave no differently regardless of if the token lies at the very end of the
48
+ # input buffer or not (i.e. a literal edge case) Specifying -1 forces split to
49
+ # return "" in this case, meaning that the last entry in the list represents a
50
+ # new segment of data where the token has not been encountered
51
+ messages = @input.split(@delimiter, -1)
52
+
53
+ if @indicator
54
+ @input = messages.pop
55
+ entities = []
56
+ messages.each do |msg|
57
+ res = msg.split(@indicator, -1)
58
+ entities << res.last if res.length > 1
59
+ end
60
+ else
61
+ entities = messages
62
+ @input = entities.pop
63
+ end
64
+
65
+ # Check to see if the buffer has exceeded capacity, if we're imposing a limit
66
+ if @size_limit && @input.size > @size_limit
67
+ if @indicator && @indicator.respond_to?(:length) # check for regex
68
+ # save enough of the buffer that if one character of the indicator were
69
+ # missing we would match on next extract (very much an edge case) and
70
+ # best we can do with a full buffer. If we were one char short of a
71
+ # delimiter it would be unfortunate
72
+ @input = @input[-(@indicator.length - 1)..-1]
73
+ else
74
+ @input = ''
75
+ end
76
+ raise 'input buffer exceeded limit' if @verbose
77
+ end
78
+
79
+ return entities
80
+ end
81
+
82
+ # Flush the contents of the input buffer, i.e. return the input buffer even though
83
+ # a token has not yet been encountered.
84
+ #
85
+ # @return [String]
86
+ def flush
87
+ buffer = @input
88
+ @input = ''
89
+ buffer
90
+ end
91
+
92
+ # @return [Boolean]
93
+ def empty?
94
+ @input.empty?
95
+ end
96
+ end
97
+ end
@@ -0,0 +1,175 @@
1
+
2
+ module UvRays
3
+ def self.try_connect(tcp, handler, server, port)
4
+ if IPAddress.valid? server
5
+ tcp.finally handler.method(:on_close)
6
+ tcp.progress handler.method(:on_read)
7
+ tcp.connect server, port do
8
+ tcp.enable_nodelay
9
+ tcp.start_tls(handler.using_tls) unless handler.using_tls == false
10
+
11
+ # on_connect could call use_tls so must come after start_tls
12
+ handler.on_connect(tcp)
13
+ tcp.start_read
14
+ end
15
+ else
16
+ tcp.loop.lookup(server).then(
17
+ proc { |result|
18
+ UvRays.try_connect(tcp, handler, result[0][0], port)
19
+ },
20
+ proc { |failure|
21
+ # TODO:: Log error on loop
22
+ handler.on_close
23
+ }
24
+ )
25
+ end
26
+ end
27
+
28
+
29
+ # @abstract
30
+ class Connection
31
+ attr_reader :using_tls
32
+
33
+ def initialize
34
+ @send_queue = []
35
+ @paused = false
36
+ @using_tls = false
37
+ end
38
+
39
+ def pause
40
+ @paused = true
41
+ @transport.stop_read
42
+ end
43
+
44
+ def paused?
45
+ @paused
46
+ end
47
+
48
+ def resume
49
+ @paused = false
50
+ @transport.start_read
51
+ end
52
+
53
+ # Compatible with TCP
54
+ def close_connection(*args)
55
+ @transport.close
56
+ end
57
+
58
+ def on_read(data, *args) # user to define
59
+ end
60
+
61
+ def post_init(*args)
62
+ end
63
+ end
64
+
65
+ class TcpConnection < Connection
66
+ def write(data)
67
+ @transport.write(data)
68
+ end
69
+
70
+ def close_connection(after_writing = false)
71
+ if after_writing
72
+ @transport.shutdown
73
+ else
74
+ @transport.close
75
+ end
76
+ end
77
+
78
+ def stream_file(filename)
79
+ end
80
+
81
+ def on_connect(transport) # user to define
82
+ end
83
+
84
+ def on_close # user to define
85
+ end
86
+ end
87
+
88
+ class InboundConnection < TcpConnection
89
+ def initialize(tcp)
90
+ super()
91
+
92
+ @loop = tcp.loop
93
+ @transport = tcp
94
+ @transport.finally method(:on_close)
95
+ @transport.progress method(:on_read)
96
+ end
97
+
98
+ def use_tls(args = {})
99
+ args[:server] = true
100
+
101
+ if @transport.connected
102
+ @transport.start_tls(args)
103
+ else
104
+ @using_tls = args
105
+ end
106
+ end
107
+ end
108
+
109
+ class OutboundConnection < TcpConnection
110
+
111
+ def initialize(server, port)
112
+ super()
113
+
114
+ @loop = Libuv::Loop.current
115
+ @server = server
116
+ @port = port
117
+ @transport = @loop.tcp
118
+
119
+ ::UvRays.try_connect(@transport, self, @server, @port)
120
+ end
121
+
122
+ def use_tls(args = {})
123
+ args.delete(:server)
124
+
125
+ if @transport.connected
126
+ @transport.start_tls(args)
127
+ else
128
+ @using_tls = args
129
+ end
130
+ end
131
+
132
+ def reconnect(server = nil, port = nil)
133
+ @loop = Libuv::Loop.current || @loop
134
+
135
+ @transport = @loop.tcp
136
+ @server = server || @server
137
+ @port = port || @port
138
+
139
+ ::UvRays.try_connect(@transport, self, @server, @port)
140
+ end
141
+ end
142
+
143
+ class DatagramConnection < Connection
144
+ def initialize(server = nil, port = nil)
145
+ super()
146
+
147
+ @loop = Libuv::Loop.current
148
+ @transport = @loop.udp
149
+ @transport.progress method(:on_read)
150
+
151
+ if not server.nil?
152
+ server = '127.0.0.1' if server == 'localhost'
153
+ if IPAddress.valid? server
154
+ @transport.bind(server, port)
155
+ else
156
+ raise ArgumentError, "Invalid server address #{server}"
157
+ end
158
+ end
159
+
160
+ @transport.start_read
161
+ end
162
+
163
+ def send_datagram(data, recipient_address, recipient_port)
164
+ if IPAddress.valid? recipient_address
165
+ @transport.send recipient_address, recipient_port, data
166
+ else
167
+ # Async DNS resolution
168
+ # Note:: send here will chain the promise
169
+ tcp.loop.lookup(server).then do |result|
170
+ @transport.send result[0][0], recipient_port, data
171
+ end
172
+ end
173
+ end
174
+ end
175
+ end
@@ -0,0 +1,119 @@
1
+ module UvRays
2
+ module Http
3
+ module Encoding
4
+ HTTP_REQUEST_HEADER="%s %s HTTP/1.1\r\n"
5
+ FIELD_ENCODING = "%s: %s\r\n"
6
+
7
+ def escape(s)
8
+ if defined?(EscapeUtils)
9
+ EscapeUtils.escape_url(s.to_s)
10
+ else
11
+ s.to_s.gsub(/([^a-zA-Z0-9_.-]+)/) {
12
+ '%'+$1.unpack('H2'*bytesize($1)).join('%').upcase
13
+ }
14
+ end
15
+ end
16
+
17
+ def unescape(s)
18
+ if defined?(EscapeUtils)
19
+ EscapeUtils.unescape_url(s.to_s)
20
+ else
21
+ s.tr('+', ' ').gsub(/((?:%[0-9a-fA-F]{2})+)/) {
22
+ [$1.delete('%')].pack('H*')
23
+ }
24
+ end
25
+ end
26
+
27
+ # Map all header keys to a downcased string version
28
+ def munge_header_keys(head)
29
+ head.inject({}) { |h, (k, v)| h[k.to_s.downcase] = v; h }
30
+ end
31
+
32
+
33
+ def encode_request(method, uri, query)
34
+ query = encode_query(uri, query)
35
+
36
+ HTTP_REQUEST_HEADER % [method.to_s.upcase, query]
37
+ end
38
+
39
+ def encode_query(uri, query)
40
+ encoded_query = if query.kind_of?(Hash)
41
+ query.map { |k, v| encode_param(k, v) }.join('&')
42
+ else
43
+ query.to_s
44
+ end
45
+ encoded_query.to_s.empty? ? uri : "#{uri}?#{encoded_query}"
46
+ end
47
+
48
+ # URL encodes query parameters:
49
+ # single k=v, or a URL encoded array, if v is an array of values
50
+ def encode_param(k, v)
51
+ if v.is_a?(Array)
52
+ v.map { |e| escape(k) + "[]=" + escape(e) }.join("&")
53
+ else
54
+ escape(k) + "=" + escape(v)
55
+ end
56
+ end
57
+
58
+ def form_encode_body(obj)
59
+ pairs = []
60
+ recursive = Proc.new do |h, prefix|
61
+ h.each do |k,v|
62
+ key = prefix == '' ? escape(k) : "#{prefix}[#{escape(k)}]"
63
+
64
+ if v.is_a? Array
65
+ nh = Hash.new
66
+ v.size.times { |t| nh[t] = v[t] }
67
+ recursive.call(nh, key)
68
+
69
+ elsif v.is_a? Hash
70
+ recursive.call(v, key)
71
+ else
72
+ pairs << "#{key}=#{escape(v)}"
73
+ end
74
+ end
75
+ end
76
+
77
+ recursive.call(obj, '')
78
+ return pairs.join('&')
79
+ end
80
+
81
+ # Encode a field in an HTTP header
82
+ def encode_field(k, v)
83
+ FIELD_ENCODING % [k, v]
84
+ end
85
+
86
+ # Encode basic auth in an HTTP header
87
+ # In: Array ([user, pass]) - for basic auth
88
+ # String - custom auth string (OAuth, etc)
89
+ def encode_auth(k,v)
90
+ if v.is_a? Array
91
+ FIELD_ENCODING % [k, ["Basic", Base64.encode64(v.join(":")).split.join].join(" ")]
92
+ else
93
+ encode_field(k,v)
94
+ end
95
+ end
96
+
97
+ def encode_headers(head)
98
+ head.inject('') do |result, (key, value)|
99
+ # Munge keys from foo-bar-baz to Foo-Bar-Baz
100
+ key = key.split('-').map { |k| k.to_s.capitalize }.join('-')
101
+ result << case key
102
+ when 'Authorization', 'Proxy-Authorization'
103
+ encode_auth(key, value)
104
+ else
105
+ encode_field(key, value)
106
+ end
107
+ end
108
+ end
109
+
110
+ def encode_cookie(cookie)
111
+ if cookie.is_a? Hash
112
+ cookie.inject('') { |result, (k, v)| result << encode_param(k, v) + ";" }
113
+ else
114
+ cookie
115
+ end
116
+ end
117
+ end
118
+ end
119
+ end