uv-rays 0.0.1

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