iodine 0.0.1 → 0.0.2

Sign up to get free protection for your applications and to get access to all the features.

Potentially problematic release.


This version of iodine might be problematic. Click here for more details.

@@ -0,0 +1,355 @@
1
+ module Iodine
2
+
3
+ class Http < ::Iodine::Protocol
4
+
5
+ # this class handles Http responses.
6
+ #
7
+ # The response can be sent in stages but should complete within the scope of the connecton's message. Please notice that headers and status cannot be changed once the response started sending data.
8
+ class Response
9
+ # the response's status code
10
+ attr_accessor :status
11
+ # the response's headers
12
+ attr_reader :headers
13
+ # the flash cookie-jar (single-use cookies, that survive only one request).
14
+ attr_reader :flash
15
+ # the response's body buffer container (an array). This object is removed once the headers are sent and all write operations hang after that point.
16
+ attr_accessor :body
17
+ # the io through which the response will be sent.
18
+ attr_reader :io
19
+ # the request.
20
+ attr_accessor :request
21
+ # Logs the number of bytes written.
22
+ attr_accessor :bytes_written
23
+ # forces the connection to remain alive if this flag is set to `true` (otherwise follows Http and optimization guidelines).
24
+ attr_accessor :keep_alive
25
+
26
+ # the response object responds to a specific request on a specific io.
27
+ # hence, to initialize a response object, a request must be set.
28
+ #
29
+ # use, at the very least `HTTPResponse.new request`
30
+ def initialize request, status = 200, headers = {}, content = nil
31
+ @request = request
32
+ @status = status
33
+ @headers = headers
34
+ @body = content || []
35
+ @request.cookies.set_response self
36
+ @cookies = {}
37
+ @io = request.io
38
+ @bytes_written = 0
39
+ @keep_alive = @http_sblocks_count = false
40
+ # propegate flash object
41
+ @flash = Hash.new do |hs,k|
42
+ hs["magic_flash_#{k.to_s}".to_sym] if hs.has_key? "magic_flash_#{k.to_s}".to_sym
43
+ end
44
+ request.cookies.each do |k,v|
45
+ @flash[k] = v if k.to_s.start_with? 'magic_flash_'
46
+ end
47
+ end
48
+
49
+ # returns true if headers were already sent
50
+ def headers_sent?
51
+ @headers.frozen?
52
+ end
53
+
54
+ # Creates a streaming block. Once all streaming blocks are done, the response will automatically finish.
55
+ #
56
+ # This avoids manualy handling {#start_streaming}, {#finish_streaming} and asynchronously tasking.
57
+ #
58
+ # Every time data is sent the timout is reset. Responses longer than timeout will not be sent (but they will be processed).
59
+ #
60
+ # Since Iodine is likely to be multi-threading (depending on your settings and architecture), it is important that
61
+ # streaming blocks are nested rather than chained. Chained streaming blocks might be executed in parallel and
62
+ # suffer frome race conditions that might lead to the response being corrupted.
63
+ #
64
+ # Accepts a required block. i.e.
65
+ #
66
+ # response.stream_async {sleep 1; response << "Hello Streaming"}
67
+ # # OR, you can nest (but not chain) the streaming calls
68
+ # response.stream_async do
69
+ # sleep 1
70
+ # response << "Hello Streaming"
71
+ # response.stream_async do
72
+ # sleep 1
73
+ # response << "\r\nGoodbye Streaming"
74
+ # end
75
+ # end
76
+ #
77
+ # @return [true, Exception] The method returns immidiatly with a value of true unless it is impossible to stream the response (an exception will be raised) or a block wasn't supplied.
78
+ def stream_async &block
79
+ raise "Block required." unless block
80
+ start_streaming unless @http_sblocks_count
81
+ @http_sblocks_count += 1
82
+ @stream_proc ||= Proc.new { |block| raise "IO closed. Streaming failed." if io.io.closed?; block.call; @http_sblocks_count -= 1; finish_streaming }
83
+ Iodine.run block, &@stream_proc
84
+ end
85
+
86
+ # Creates nested streaming blocks for an Array or another Enumerable object. Once all streaming blocks are done, the response will automatically finish.
87
+ #
88
+ # Since streaming blocks might run in parallel, nesting the streaming blocks is important...
89
+ #
90
+ # However, manually nesting hundreds of nesting blocks is time consuming and error prone.
91
+ #
92
+ # {.sream_enum} allows you to stream an enumerable knowing that Plezi will nest the streaming blocks dynamically.
93
+ #
94
+ # Accepts:
95
+ # enum:: an Enumerable or an object that answers to the `to_a` method (the array will be used to stream the )
96
+ #
97
+ # If an Array is passed to the enumerable, it will be changed and emptied as the streaming progresses.
98
+ # So, if preserving the array is important, please create a shallow copy of the array first using the `.dup` method.
99
+ #
100
+ # i.e.:
101
+ #
102
+ # data = "Hello world!".chars
103
+ # response.stream_enum(data.each_with_index) {|c, i| response << c; sleep i/10.0 }
104
+ #
105
+ #
106
+ # @return [true, Exception] The method returns immidiatly with a value of true unless it is impossible to stream the response (an exception will be raised) or a block wasn't supplied.
107
+ def stream_array enum, &block
108
+ enum = enum.to_a
109
+ return if enum.empty?
110
+ stream_async do
111
+ args = enum.shift
112
+ block.call(*args)
113
+ stream_array enum, &block
114
+ end
115
+ end
116
+
117
+ # Creates and returns the session storage object.
118
+ #
119
+ # By default and for security reasons, session id's created on a secure connection will NOT be available on a non secure connection (SSL/TLS).
120
+ #
121
+ # Since this method renews the session_id's cookie's validity (update's it's times-stump), it must be called for the first time BEFORE the headers are sent.
122
+ #
123
+ # After the session object was created using this method call, it should be safe to continue updating the session data even after the headers were sent and this method would act as an accessor for the already existing session object.
124
+ #
125
+ # @return [Hash like storage] creates and returns the session storage object with all the data from a previous connection.
126
+ def session
127
+ return @session if @session
128
+ id = request.cookies[::Iodine::Http.session_token.to_sym] || SecureRandom.uuid
129
+ set_cookie ::Iodine::Http.session_token, id, expires: (Time.now+86_400), secure: @request.ssl?
130
+ @request[:session] = @session = ::Iodine::Http::SessionManager.get(id)
131
+ end
132
+
133
+ # Returns a writable combined hash of the request's cookies and the response cookie values.
134
+ #
135
+ # Any cookies writen to this hash (`response.cookies[:name] = value` will be set using default values).
136
+ #
137
+ # It's also possible to use this combined hash to delete cookies, using: response.cookies[:name] = nil
138
+ def cookies
139
+ @request.cookies
140
+ end
141
+
142
+ # Returns the response's encoded cookie hash.
143
+ #
144
+ # This method allows direct editing of the cookies about to be set.
145
+ def raw_cookies
146
+ @cookies
147
+ end
148
+
149
+ # pushes data to the buffer of the response. this is the preferred way to add data to the response.
150
+ #
151
+ # If the headers were already sent, this will also send the data and hang until the data was sent.
152
+ def << str
153
+ ( @body ? @body.push(str) : ( (@body = str.dup) && @io.stream_response(self) ) ) if str
154
+ self
155
+ end
156
+
157
+ # returns a response header, if set.
158
+ def [] header
159
+ header.is_a?(String) ? (header.frozen? ? header : header.downcase!) : (header.is_a?(Symbol) ? (header = header.to_s.downcase) : (return false))
160
+ headers[header]
161
+ end
162
+
163
+ # Sets a response header. response headers should be a **downcase** String (not a symbol or any other object).
164
+ #
165
+ # this is the prefered to set a header.
166
+ #
167
+ # Be aware that HTTP/2 will treat a header name with an upper-case letter as an Error! (while HTTP/1.1 ignores the letter case)
168
+ #
169
+ # returns the value set for the header.
170
+ #
171
+ # see HTTP response headers for valid headers and values: http://en.wikipedia.org/wiki/List_of_HTTP_header_fields
172
+ def []= header, value
173
+ raise 'Cannot set headers after the headers had been sent.' if headers_sent?
174
+ return (@headers.delete(header) && nil) if header.nil?
175
+ header.is_a?(String) ? (header.frozen? ? header : header.downcase!) : (header.is_a?(Symbol) ? (header = header.to_s.downcase) : (return false))
176
+ headers[header] = value
177
+ end
178
+
179
+
180
+ COOKIE_NAME_REGEXP = /[\x00-\x20\(\)\<\>@,;:\\\"\/\[\]\?\=\{\}\s]/
181
+
182
+ # Sets/deletes cookies when headers are sent.
183
+ #
184
+ # Accepts:
185
+ # name:: the cookie's name
186
+ # value:: the cookie's value
187
+ # parameters:: a parameters Hash for cookie creation.
188
+ #
189
+ # Parameters accept any of the following Hash keys and values:
190
+ #
191
+ # expires:: a Time object with the expiration date. defaults to 10 years in the future.
192
+ # max_age:: a Max-Age HTTP cookie string.
193
+ # path:: the path from which the cookie is acessible. defaults to '/'.
194
+ # domain:: the domain for the cookie (best used to manage subdomains). defaults to the active domain (sub-domain limitations might apply).
195
+ # secure:: if set to `true`, the cookie will only be available over secure connections. defaults to false.
196
+ # http_only:: if true, the HttpOnly flag will be set (not accessible to javascript). defaults to false.
197
+ #
198
+ # Setting the request's coockies (`request.cookies[:name] = value`) will automatically call this method with default parameters.
199
+ #
200
+ def set_cookie name, value, params = {}
201
+ raise 'Cannot set cookies after the headers had been sent.' if headers_sent?
202
+ name = name.to_s
203
+ raise 'Illegal cookie name' if name =~ COOKIE_NAME_REGEXP
204
+ params[:expires] = (Time.now - 315360000) unless value
205
+ value ||= 'deleted'.freeze
206
+ params[:expires] ||= (Time.now + 315360000) unless params[:max_age]
207
+ params[:path] ||= '/'.freeze
208
+ value = Iodine::Http::Request.encode_url(value)
209
+ if params[:max_age]
210
+ value << ('; Max-Age=%s' % params[:max_age])
211
+ else
212
+ value << ('; Expires=%s' % params[:expires].httpdate)
213
+ end
214
+ value << "; Path=#{params[:path]}"
215
+ value << "; Domain=#{params[:domain]}" if params[:domain]
216
+ value << '; Secure'.freeze if params[:secure]
217
+ value << '; HttpOnly'.freeze if params[:http_only]
218
+ @cookies[name.to_sym] = value
219
+ end
220
+
221
+ # deletes a cookie (actually calls `set_cookie name, nil`)
222
+ def delete_cookie name
223
+ set_cookie name, nil
224
+ end
225
+
226
+ # clears the response object, unless headers were already sent (the response is already on it's way, at least in part).
227
+ #
228
+ # returns false if the response was already sent.
229
+ def clear
230
+ return false if @headers.frozen?
231
+ @status, @body, @headers, @cookies = 200, [], {}, {}
232
+ self
233
+ end
234
+
235
+ # attempts to write a non-streaming response to the IO. This can be done only once and will quitely fail subsequently.
236
+ def finish
237
+ @io.send_response self
238
+ end
239
+
240
+ # Returns the connection's UUID.
241
+ def uuid
242
+ io.id
243
+ end
244
+
245
+ # response status codes, as defined.
246
+ STATUS_CODES = {100=>"Continue".freeze,
247
+ 101=>"Switching Protocols".freeze,
248
+ 102=>"Processing".freeze,
249
+ 200=>"OK".freeze,
250
+ 201=>"Created".freeze,
251
+ 202=>"Accepted".freeze,
252
+ 203=>"Non-Authoritative Information".freeze,
253
+ 204=>"No Content".freeze,
254
+ 205=>"Reset Content".freeze,
255
+ 206=>"Partial Content".freeze,
256
+ 207=>"Multi-Status".freeze,
257
+ 208=>"Already Reported".freeze,
258
+ 226=>"IM Used".freeze,
259
+ 300=>"Multiple Choices".freeze,
260
+ 301=>"Moved Permanently".freeze,
261
+ 302=>"Found".freeze,
262
+ 303=>"See Other".freeze,
263
+ 304=>"Not Modified".freeze,
264
+ 305=>"Use Proxy".freeze,
265
+ 306=>"(Unused)".freeze,
266
+ 307=>"Temporary Redirect".freeze,
267
+ 308=>"Permanent Redirect".freeze,
268
+ 400=>"Bad Request".freeze,
269
+ 401=>"Unauthorized".freeze,
270
+ 402=>"Payment Required".freeze,
271
+ 403=>"Forbidden".freeze,
272
+ 404=>"Not Found".freeze,
273
+ 405=>"Method Not Allowed".freeze,
274
+ 406=>"Not Acceptable".freeze,
275
+ 407=>"Proxy Authentication Required".freeze,
276
+ 408=>"Request Timeout".freeze,
277
+ 409=>"Conflict".freeze,
278
+ 410=>"Gone".freeze,
279
+ 411=>"Length Required".freeze,
280
+ 412=>"Precondition Failed".freeze,
281
+ 413=>"Payload Too Large".freeze,
282
+ 414=>"URI Too Long".freeze,
283
+ 415=>"Unsupported Media Type".freeze,
284
+ 416=>"Range Not Satisfiable".freeze,
285
+ 417=>"Expectation Failed".freeze,
286
+ 422=>"Unprocessable Entity".freeze,
287
+ 423=>"Locked".freeze,
288
+ 424=>"Failed Dependency".freeze,
289
+ 426=>"Upgrade Required".freeze,
290
+ 428=>"Precondition Required".freeze,
291
+ 429=>"Too Many Requests".freeze,
292
+ 431=>"Request Header Fields Too Large".freeze,
293
+ 500=>"Internal Server Error".freeze,
294
+ 501=>"Not Implemented".freeze,
295
+ 502=>"Bad Gateway".freeze,
296
+ 503=>"Service Unavailable".freeze,
297
+ 504=>"Gateway Timeout".freeze,
298
+ 505=>"HTTP Version Not Supported".freeze,
299
+ 506=>"Variant Also Negotiates".freeze,
300
+ 507=>"Insufficient Storage".freeze,
301
+ 508=>"Loop Detected".freeze,
302
+ 510=>"Not Extended".freeze,
303
+ 511=>"Network Authentication Required".freeze
304
+ }
305
+
306
+ # This will return the Body object as a String... And set the body to `nil` (seeing as it was extracted from the response).
307
+ def extract_body
308
+ if @body.is_a?(Array)
309
+ return (@body = nil) if body.empty?
310
+ @body = @body.join
311
+ extract_body
312
+ elsif @body.is_a?(String)
313
+ return (@body = nil) if body.empty?
314
+ tmp = @body
315
+ @body = nil
316
+ tmp
317
+ elsif body.nil?
318
+ nil
319
+ elsif body.respond_to? :each
320
+ tmp = ''
321
+ body.each {|s| tmp << s}
322
+ body.close if body.respond_to? :close
323
+ @body = nil
324
+ return nil if tmp.empty?
325
+ tmp
326
+ end
327
+ end
328
+
329
+ protected
330
+
331
+ # Sets the http streaming flag and sends the responses headers, so that the response could be handled asynchronously.
332
+ #
333
+ # if this flag is not set, the response will try to automatically finish its job
334
+ # (send its data and maybe close the connection).
335
+ #
336
+ # NOTICE! :: If HTTP streaming is set, you will need to manually call `response.finish_streaming`
337
+ # or the connection will not close properly and the client will be left expecting more information.
338
+ def start_streaming
339
+ raise "Cannot start streaming after headers were sent!" if headers_sent?
340
+ @http_sblocks_count ||= 0
341
+ @io.stream_response self
342
+ end
343
+
344
+ # Sends the complete response signal for a streaming response.
345
+ #
346
+ # Careful - sending the completed response signal more than once might case disruption to the HTTP connection.
347
+ def finish_streaming
348
+ return unless @http_sblocks_count == 0
349
+ @io.stream_response self, true
350
+ end
351
+ end
352
+ end
353
+ end
354
+
355
+
@@ -0,0 +1,110 @@
1
+ module Iodine
2
+ module Base
3
+ module MemSessionStorage
4
+ @mem_storage = {}
5
+ def self.fetch key
6
+ @mem_storage[key] ||= {}
7
+ end
8
+ end
9
+ module FileSessionStorage
10
+ class SessionObject
11
+ # called by the Plezi framework to initiate a session with the id requested
12
+ def initialize id
13
+ @filename = File.join Dir.tmpdir, "iodine_#{Iodine::Http.session_token}_#{id}"
14
+ @data ||= {}
15
+ end
16
+ # Get a key from the session data store.
17
+ #
18
+ # Due to different considirations, all keys will be converted to strings, so that `"name" == :name` and `1234 == "1234"`.
19
+ # If you store two keys that evaluate as the same string, they WILL override each other.
20
+ def [] key
21
+ key = key.to_s
22
+ load
23
+ @data[key]
24
+ end
25
+ alias :fetch :[]
26
+
27
+ # Stores a key in the session's data store.
28
+ #
29
+ # Due to different considirations, all keys will be converted to strings, so that `"name" == :name` and `1234 == "1234"`.
30
+ # If you store two keys that evaluate as the same string, they WILL override each other.
31
+ def []= key, value
32
+ key = key.to_s
33
+ load
34
+ @data[key] = value
35
+ save
36
+ value
37
+ end
38
+ alias :store :[]=
39
+
40
+ # @return [Hash] returns a shallow copy of the current session data as a Hash.
41
+ def to_h
42
+ load
43
+ @data.dup
44
+ end
45
+
46
+ # Removes a key from the session's data store.
47
+ def delete key
48
+ load
49
+ ret = @data.delete key
50
+ save
51
+ ret
52
+ end
53
+
54
+ # Clears the session's data.
55
+ def clear
56
+ @data.clear
57
+ save
58
+ nil
59
+ end
60
+ protected
61
+ def save
62
+ # save data to tmp-file
63
+ IO.write @filename, @data.to_yaml
64
+ end
65
+ def load
66
+ @data = YAML.load IO.read(@filename) if File.exists?(@filename)
67
+ end
68
+ end
69
+ def self.fetch key
70
+ SessionObject.new key
71
+ end
72
+ end
73
+ end
74
+ class Http < Iodine::Protocol
75
+ module SessionManager
76
+ module_function
77
+ # returns a session object
78
+ def get id
79
+ storage.fetch(id)
80
+ end
81
+ # Sets the session storage system, to allow for different storage systems.
82
+ #
83
+ # A Session Storage system must answer only one methods:
84
+ # fetch(id):: returns a Hash like session object with all the session's data or a fresh session object if the session object did not exist before
85
+ #
86
+ # The Session Object should update itself in the storage whenever data is saved to the session Object.
87
+ # This is important also because websocket 'session' could exist simultaneously with other HTTP requests and the data should be kept updated at all times.
88
+ # If there are race conditions that apply for multi-threading / multi processing, the Session Object should manage them as well as possible.
89
+ def storage= session_storage = nil
90
+ case session_storage
91
+ when :file, nil
92
+ @storage = Iodine::Base::FileSessionStorage
93
+ when :mem
94
+ @storage = Iodine::Base::MemSessionStorage
95
+ else
96
+ @storage = session_storage
97
+ end
98
+ end
99
+
100
+ # returns the current session storage system.
101
+ def storage
102
+ @storage ||= Iodine::Base::FileSessionStorage
103
+ end
104
+ end
105
+ end
106
+ end
107
+ # A hash like interface for storing request session data.
108
+ # The store must implement: store(key, value) (aliased as []=);
109
+ # fetch(key, default = nil) (aliased as []);
110
+ # delete(key); clear;