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