uv-rays 0.0.1
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 +7 -0
- data/LICENSE +20 -0
- data/README.md +53 -0
- data/Rakefile +22 -0
- data/lib/uv-rays/abstract_tokenizer.rb +100 -0
- data/lib/uv-rays/buffered_tokenizer.rb +97 -0
- data/lib/uv-rays/connection.rb +175 -0
- data/lib/uv-rays/http/encoding.rb +119 -0
- data/lib/uv-rays/http/request.rb +150 -0
- data/lib/uv-rays/http/response.rb +119 -0
- data/lib/uv-rays/http_endpoint.rb +253 -0
- data/lib/uv-rays/scheduler/cron.rb +386 -0
- data/lib/uv-rays/scheduler/time.rb +275 -0
- data/lib/uv-rays/scheduler.rb +319 -0
- data/lib/uv-rays/tcp_server.rb +48 -0
- data/lib/uv-rays/version.rb +3 -0
- data/lib/uv-rays.rb +96 -0
- data/spec/abstract_tokenizer_spec.rb +87 -0
- data/spec/buffered_tokenizer_spec.rb +253 -0
- data/spec/connection_spec.rb +137 -0
- data/spec/http_endpoint_spec.rb +279 -0
- data/spec/scheduler_cron_spec.rb +429 -0
- data/spec/scheduler_spec.rb +90 -0
- data/spec/scheduler_time_spec.rb +132 -0
- data/uv-rays.gemspec +31 -0
- metadata +217 -0
@@ -0,0 +1,150 @@
|
|
1
|
+
module UvRays
|
2
|
+
module Http
|
3
|
+
class Request < ::Libuv::Q::DeferredPromise
|
4
|
+
include Encoding
|
5
|
+
|
6
|
+
|
7
|
+
COOKIE = 'cookie'
|
8
|
+
CONNECTION = :Connection
|
9
|
+
CRLF="\r\n"
|
10
|
+
|
11
|
+
|
12
|
+
attr_reader :path
|
13
|
+
attr_reader :method
|
14
|
+
|
15
|
+
|
16
|
+
def cookies_hash
|
17
|
+
@endpoint.cookiejar.get_hash(@uri)
|
18
|
+
end
|
19
|
+
|
20
|
+
def set_cookie(value)
|
21
|
+
@endpoint.cookiejar.set(@uri, value)
|
22
|
+
end
|
23
|
+
|
24
|
+
|
25
|
+
def initialize(endpoint, options)
|
26
|
+
super(endpoint.loop, endpoint.loop.defer)
|
27
|
+
|
28
|
+
@options = options
|
29
|
+
@endpoint = endpoint
|
30
|
+
|
31
|
+
@path = options[:path]
|
32
|
+
@method = options[:method]
|
33
|
+
@uri = encode_host(endpoint.host, endpoint.port) + @path
|
34
|
+
end
|
35
|
+
|
36
|
+
def resolve(response)
|
37
|
+
@defer.resolve({
|
38
|
+
headers: @headers,
|
39
|
+
body: response
|
40
|
+
})
|
41
|
+
end
|
42
|
+
|
43
|
+
def reject(reason)
|
44
|
+
@defer.reject(reason)
|
45
|
+
end
|
46
|
+
|
47
|
+
def send(transport, error)
|
48
|
+
head, body = build_request, @options[:body]
|
49
|
+
|
50
|
+
@endpoint.middleware.each do |m|
|
51
|
+
head, body = m.request(self, head, body) if m.respond_to?(:request)
|
52
|
+
end
|
53
|
+
|
54
|
+
body = body.is_a?(Hash) ? form_encode_body(body) : body
|
55
|
+
file = @options[:file]
|
56
|
+
query = @options[:query]
|
57
|
+
|
58
|
+
# Set the Content-Length if file is given
|
59
|
+
head['content-length'] = File.size(file) if file
|
60
|
+
|
61
|
+
# Set the Content-Length if body is given,
|
62
|
+
# or we're doing an empty post or put
|
63
|
+
if body
|
64
|
+
head['content-length'] = body.bytesize
|
65
|
+
elsif method == :post or method == :put
|
66
|
+
# wont happen if body is set and we already set content-length above
|
67
|
+
head['content-length'] ||= 0
|
68
|
+
end
|
69
|
+
|
70
|
+
# Set content-type header if missing and body is a Ruby hash
|
71
|
+
if !head['content-type'] and @options[:body].is_a? Hash
|
72
|
+
head['content-type'] = 'application/x-www-form-urlencoded'
|
73
|
+
end
|
74
|
+
|
75
|
+
request_header = encode_request(method, path, query)
|
76
|
+
request_header << encode_headers(head)
|
77
|
+
request_header << CRLF
|
78
|
+
|
79
|
+
transport.write(request_header).catch error
|
80
|
+
|
81
|
+
if body
|
82
|
+
transport.write(body).catch error
|
83
|
+
elsif file
|
84
|
+
# TODO:: Send file
|
85
|
+
#@conn.stream_file_data @req.file, :http_chunks => false
|
86
|
+
end
|
87
|
+
end
|
88
|
+
|
89
|
+
def notify(*args)
|
90
|
+
@defer.notify(*args)
|
91
|
+
end
|
92
|
+
|
93
|
+
def set_headers(head)
|
94
|
+
@headers = head
|
95
|
+
if not @headers_callback.nil?
|
96
|
+
@headers_callback.call(@headers)
|
97
|
+
end
|
98
|
+
end
|
99
|
+
|
100
|
+
def headers(callback, &blk)
|
101
|
+
callback ||= blk
|
102
|
+
if @headers.nil?
|
103
|
+
@headers_callback = callback
|
104
|
+
else
|
105
|
+
callback.call(@headers)
|
106
|
+
end
|
107
|
+
end
|
108
|
+
|
109
|
+
|
110
|
+
protected
|
111
|
+
|
112
|
+
|
113
|
+
def encode_host(host, port)
|
114
|
+
if port.nil? || port == 80 || port == 443
|
115
|
+
host
|
116
|
+
else
|
117
|
+
host + ":#{port}"
|
118
|
+
end
|
119
|
+
end
|
120
|
+
|
121
|
+
def build_request
|
122
|
+
head = @options[:headers] ? munge_header_keys(@options[:headers]) : {}
|
123
|
+
|
124
|
+
# Set the cookie header if provided
|
125
|
+
@cookies = @endpoint.cookiejar.get(@uri)
|
126
|
+
if cookie = head[COOKIE]
|
127
|
+
@cookies << encode_cookie(cookie)
|
128
|
+
end
|
129
|
+
head[COOKIE] = @cookies.compact.uniq.join("; ").squeeze(";") unless @cookies.empty?
|
130
|
+
|
131
|
+
# Set connection close unless keep-alive
|
132
|
+
if !@options[:keepalive]
|
133
|
+
head['connection'] = 'close'
|
134
|
+
end
|
135
|
+
|
136
|
+
# Set the Host header if it hasn't been specified already
|
137
|
+
head['host'] ||= encode_host(@endpoint.host, @endpoint.port)
|
138
|
+
|
139
|
+
# Set the User-Agent if it hasn't been specified
|
140
|
+
if !head.key?('user-agent')
|
141
|
+
head['user-agent'] = "UvRays HttpClient"
|
142
|
+
elsif head['user-agent'].nil?
|
143
|
+
head.delete('user-agent')
|
144
|
+
end
|
145
|
+
|
146
|
+
head
|
147
|
+
end
|
148
|
+
end
|
149
|
+
end
|
150
|
+
end
|
@@ -0,0 +1,119 @@
|
|
1
|
+
module UvRays
|
2
|
+
module Http
|
3
|
+
class Headers < Hash
|
4
|
+
# The HTTP version returned
|
5
|
+
attr_accessor :http_version
|
6
|
+
|
7
|
+
# The status code (as an integer)
|
8
|
+
attr_accessor :status
|
9
|
+
|
10
|
+
# Cookies at the time of the request
|
11
|
+
attr_accessor :cookies
|
12
|
+
|
13
|
+
attr_accessor :keep_alive
|
14
|
+
end
|
15
|
+
|
16
|
+
class Response
|
17
|
+
def initialize(requests)
|
18
|
+
@requests = requests
|
19
|
+
|
20
|
+
@parser = ::HttpParser::Parser.new(self)
|
21
|
+
@state = ::HttpParser::Parser.new_instance
|
22
|
+
@state.type = :response
|
23
|
+
end
|
24
|
+
|
25
|
+
# Called on connection disconnect
|
26
|
+
def reset!
|
27
|
+
@state.reset!
|
28
|
+
end
|
29
|
+
|
30
|
+
# Socket level data is forwarded as it arrives
|
31
|
+
def receive(data)
|
32
|
+
# Returns true if error
|
33
|
+
if @parser.parse(@state, data)
|
34
|
+
if @request
|
35
|
+
@request.reject(@state.error)
|
36
|
+
return true
|
37
|
+
#else # silently fail here
|
38
|
+
# p 'parse error and no request..'
|
39
|
+
# p @state.error
|
40
|
+
end
|
41
|
+
end
|
42
|
+
|
43
|
+
false
|
44
|
+
end
|
45
|
+
|
46
|
+
##
|
47
|
+
# Parser Callbacks:
|
48
|
+
def on_message_begin(parser)
|
49
|
+
@request = @requests.shift
|
50
|
+
@headers = Headers.new
|
51
|
+
@body = ''
|
52
|
+
@chunked = false
|
53
|
+
end
|
54
|
+
|
55
|
+
def on_status_complete(parser)
|
56
|
+
# Different HTTP versions have different defaults
|
57
|
+
if @state.http_minor == 0
|
58
|
+
@close_connection = true
|
59
|
+
else
|
60
|
+
@close_connection = false
|
61
|
+
end
|
62
|
+
end
|
63
|
+
|
64
|
+
def on_header_field(parser, data)
|
65
|
+
@header = data.to_sym
|
66
|
+
end
|
67
|
+
|
68
|
+
def on_header_value(parser, data)
|
69
|
+
case @header
|
70
|
+
when :"Set-Cookie"
|
71
|
+
@request.set_cookie(data)
|
72
|
+
|
73
|
+
when :Connection
|
74
|
+
# Overwrite the default
|
75
|
+
@close_connection = data == 'close'
|
76
|
+
|
77
|
+
when :"Transfer-Encoding"
|
78
|
+
# If chunked we'll buffer streaming data for notification
|
79
|
+
@chunked = data == 'chunked'
|
80
|
+
|
81
|
+
else
|
82
|
+
@headers[@header] = data
|
83
|
+
end
|
84
|
+
end
|
85
|
+
|
86
|
+
def on_headers_complete(parser)
|
87
|
+
headers = @headers
|
88
|
+
@headers = nil
|
89
|
+
@header = nil
|
90
|
+
|
91
|
+
# https://github.com/joyent/http-parser indicates we should extract
|
92
|
+
# this information here
|
93
|
+
headers.http_version = @state.http_version
|
94
|
+
headers.status = @state.http_status
|
95
|
+
headers.cookies = @request.cookies_hash
|
96
|
+
headers.keep_alive = !@close_connection
|
97
|
+
|
98
|
+
# User code may throw an error
|
99
|
+
# Errors will halt the processing and return a PAUSED error
|
100
|
+
@request.set_headers(headers)
|
101
|
+
end
|
102
|
+
|
103
|
+
def on_body(parser, data)
|
104
|
+
@body << data
|
105
|
+
# TODO:: What if it is a chunked response body?
|
106
|
+
# We should buffer complete chunks
|
107
|
+
@request.notify(data)
|
108
|
+
end
|
109
|
+
|
110
|
+
def on_message_complete(parser)
|
111
|
+
@request.resolve(@body)
|
112
|
+
|
113
|
+
# Clean up memory
|
114
|
+
@request = nil
|
115
|
+
@body = nil
|
116
|
+
end
|
117
|
+
end
|
118
|
+
end
|
119
|
+
end
|
@@ -0,0 +1,253 @@
|
|
1
|
+
module UvRays
|
2
|
+
class CookieJar
|
3
|
+
def initialize
|
4
|
+
@jar = ::CookieJar::Jar.new
|
5
|
+
end
|
6
|
+
|
7
|
+
def set(string, uri)
|
8
|
+
@jar.set_cookie(uri, string) rescue nil # drop invalid cookies
|
9
|
+
end
|
10
|
+
|
11
|
+
def get(uri)
|
12
|
+
uri = URI.parse(uri) rescue nil
|
13
|
+
uri ? @jar.get_cookies(uri).map(&:to_s) : []
|
14
|
+
end
|
15
|
+
|
16
|
+
def get_hash(uri)
|
17
|
+
uri = URI.parse(uri) rescue nil
|
18
|
+
cookies = {}
|
19
|
+
if uri
|
20
|
+
@jar.get_cookies(uri).each do |cookie|
|
21
|
+
cookies[cookie.name.to_sym] = cookie.value
|
22
|
+
end
|
23
|
+
end
|
24
|
+
cookies
|
25
|
+
end
|
26
|
+
end # CookieJar
|
27
|
+
|
28
|
+
|
29
|
+
class HttpEndpoint < TcpConnection
|
30
|
+
TRANSFER_ENCODING="TRANSFER_ENCODING"
|
31
|
+
CONTENT_ENCODING="CONTENT_ENCODING"
|
32
|
+
CONTENT_LENGTH="CONTENT_LENGTH"
|
33
|
+
CONTENT_TYPE="CONTENT_TYPE"
|
34
|
+
LAST_MODIFIED="LAST_MODIFIED"
|
35
|
+
KEEP_ALIVE="CONNECTION"
|
36
|
+
LOCATION="LOCATION"
|
37
|
+
HOST="HOST"
|
38
|
+
ETAG="ETAG"
|
39
|
+
CRLF="\r\n"
|
40
|
+
|
41
|
+
|
42
|
+
@@defaults = {
|
43
|
+
:path => '/',
|
44
|
+
:keepalive => true
|
45
|
+
}
|
46
|
+
|
47
|
+
attr_reader :host, :port, :using_tls, :loop, :cookiejar
|
48
|
+
attr_reader :connect_timeout, :inactivity_timeout
|
49
|
+
|
50
|
+
def initialize(uri, options = {})
|
51
|
+
@connect_timeout = options[:connect_timeout] ||= 5 # default connection setup timeout
|
52
|
+
@inactivity_timeout = options[:inactivity_timeout] ||= 10 # default connection inactivity (post-setup) timeout
|
53
|
+
|
54
|
+
|
55
|
+
@using_tls = options[:tls] || options[:ssl] || false
|
56
|
+
@using_tls.delete(:server) unless @using_tls == false
|
57
|
+
|
58
|
+
uri = uri.kind_of?(Addressable::URI) ? uri : Addressable::URI::parse(uri.to_s)
|
59
|
+
@https = uri.scheme == "https"
|
60
|
+
uri.port ||= (@https ? 443 : 80)
|
61
|
+
|
62
|
+
|
63
|
+
@loop = Libuv::Loop.current
|
64
|
+
@host = uri.host
|
65
|
+
@port = uri.port
|
66
|
+
#@transport = @loop.tcp
|
67
|
+
|
68
|
+
# State flags
|
69
|
+
@ready = false
|
70
|
+
@connecting = false
|
71
|
+
|
72
|
+
# Current requests
|
73
|
+
@pending_requests = []
|
74
|
+
@pending_responses = []
|
75
|
+
@connection_pending = []
|
76
|
+
@cookiejar = CookieJar.new
|
77
|
+
|
78
|
+
# Callback methods
|
79
|
+
@connection_method = method(:get_connection)
|
80
|
+
@next_request_method = method(:next_request)
|
81
|
+
@idle_timeout_method = method(:idle_timeout)
|
82
|
+
@connect_timeout_method = method(:connect_timeout)
|
83
|
+
|
84
|
+
# Used to indicate when we can start the next send
|
85
|
+
@breakpoint = ::Libuv::Q::ResolvedPromise.new(@loop, true)
|
86
|
+
|
87
|
+
# Manages the tokenising of response from the input stream
|
88
|
+
@response = Http::Response.new(@pending_responses)
|
89
|
+
|
90
|
+
# Timeout timer
|
91
|
+
if @connect_timeout || @inactivity_timeout
|
92
|
+
@timer = @loop.timer
|
93
|
+
end
|
94
|
+
end
|
95
|
+
|
96
|
+
|
97
|
+
def get options = {}, &blk; request(:get, options, &blk); end
|
98
|
+
def head options = {}, &blk; request(:head, options, &blk); end
|
99
|
+
def delete options = {}, &blk; request(:delete, options, &blk); end
|
100
|
+
def put options = {}, &blk; request(:put, options, &blk); end
|
101
|
+
def post options = {}, &blk; request(:post, options, &blk); end
|
102
|
+
def patch options = {}, &blk; request(:patch, options, &blk); end
|
103
|
+
def options options = {}, &blk; request(:options, options, &blk); end
|
104
|
+
|
105
|
+
|
106
|
+
def request(method, options = {}, &blk)
|
107
|
+
options = @@defaults.merge(options)
|
108
|
+
options[:method] = method
|
109
|
+
|
110
|
+
# Setup the request with callbacks
|
111
|
+
request = Http::Request.new(self, options)
|
112
|
+
request.then proc { |result|
|
113
|
+
if !result[:headers].keep_alive
|
114
|
+
@transport.close
|
115
|
+
end
|
116
|
+
result
|
117
|
+
}
|
118
|
+
|
119
|
+
##
|
120
|
+
# TODO:: Add middleware here
|
121
|
+
request.then blk if blk
|
122
|
+
|
123
|
+
# Add to pending requests and schedule using the breakpoint
|
124
|
+
@pending_requests << request
|
125
|
+
@breakpoint.finally @next_request_method
|
126
|
+
if options[:pipeline] == true
|
127
|
+
options[:keepalive] = true
|
128
|
+
else
|
129
|
+
@breakpoint = request
|
130
|
+
end
|
131
|
+
|
132
|
+
# return the request
|
133
|
+
request
|
134
|
+
end
|
135
|
+
|
136
|
+
def middleware
|
137
|
+
# TODO:: allow for middle ware
|
138
|
+
[]
|
139
|
+
end
|
140
|
+
|
141
|
+
def on_read(data, *args)
|
142
|
+
@timer.again if @inactivity_timeout > 0
|
143
|
+
# returns true on error
|
144
|
+
# Response rejects the request
|
145
|
+
if @response.receive(data)
|
146
|
+
@transport.close
|
147
|
+
end
|
148
|
+
end
|
149
|
+
|
150
|
+
def on_close
|
151
|
+
@ready = false
|
152
|
+
@connecting = false
|
153
|
+
stop_timer
|
154
|
+
|
155
|
+
# Reject any requests waiting on a response
|
156
|
+
@pending_responses.each do |request|
|
157
|
+
request.reject(:disconnected)
|
158
|
+
end
|
159
|
+
@pending_responses.clear
|
160
|
+
|
161
|
+
# Re-connect if there are pending requests
|
162
|
+
if not @connection_pending.empty?
|
163
|
+
do_connect
|
164
|
+
end
|
165
|
+
end
|
166
|
+
|
167
|
+
def on_connect(transport)
|
168
|
+
@connecting = false
|
169
|
+
@ready = true
|
170
|
+
|
171
|
+
# Update timeouts
|
172
|
+
stop_timer
|
173
|
+
if @inactivity_timeout > 0
|
174
|
+
@timer.progress @idle_timeout_method
|
175
|
+
@timer.start @inactivity_timeout * 1000
|
176
|
+
end
|
177
|
+
|
178
|
+
# Kick off pending requests
|
179
|
+
@response.reset!
|
180
|
+
@connection_pending.each do |callback|
|
181
|
+
callback.call
|
182
|
+
end
|
183
|
+
@connection_pending.clear
|
184
|
+
end
|
185
|
+
|
186
|
+
|
187
|
+
protected
|
188
|
+
|
189
|
+
|
190
|
+
def do_connect
|
191
|
+
@transport = @loop.tcp
|
192
|
+
|
193
|
+
if @connect_timeout > 0
|
194
|
+
@timer.progress @connect_timeout_method
|
195
|
+
@timer.start @connect_timeout * 1000
|
196
|
+
end
|
197
|
+
|
198
|
+
@connecting = true
|
199
|
+
::UvRays.try_connect(@transport, self, @host, @port)
|
200
|
+
end
|
201
|
+
|
202
|
+
def get_connection(callback = nil, &blk)
|
203
|
+
callback ||= blk
|
204
|
+
|
205
|
+
if @connecting
|
206
|
+
@connection_pending << callback
|
207
|
+
elsif !@ready
|
208
|
+
@connection_pending << callback
|
209
|
+
do_connect
|
210
|
+
elsif @transport.closing?
|
211
|
+
@connection_pending << callback
|
212
|
+
else
|
213
|
+
callback.call(@connection)
|
214
|
+
end
|
215
|
+
end
|
216
|
+
|
217
|
+
def connect_timeout
|
218
|
+
@timer.stop
|
219
|
+
@transport.close
|
220
|
+
@connection_pending.each do |request|
|
221
|
+
request.reject(:connection_timeout)
|
222
|
+
end
|
223
|
+
end
|
224
|
+
|
225
|
+
def idle_timeout
|
226
|
+
@timer.stop
|
227
|
+
@transport.close
|
228
|
+
@pending_responses.each do |request|
|
229
|
+
request.reject(:idle_timeout)
|
230
|
+
end
|
231
|
+
end
|
232
|
+
|
233
|
+
def next_request
|
234
|
+
request = @pending_requests.shift
|
235
|
+
|
236
|
+
get_connection do
|
237
|
+
@pending_responses << request
|
238
|
+
|
239
|
+
@timer.again if @inactivity_timeout > 0
|
240
|
+
|
241
|
+
# TODO:: have request deal with the error internally
|
242
|
+
request.send(@transport, proc { |err|
|
243
|
+
@transport.close
|
244
|
+
request.reject(err)
|
245
|
+
})
|
246
|
+
end
|
247
|
+
end
|
248
|
+
|
249
|
+
def stop_timer
|
250
|
+
@timer.stop unless @timer.nil?
|
251
|
+
end
|
252
|
+
end
|
253
|
+
end
|