rocketio 0.0.0 → 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.
Files changed (75) hide show
  1. checksums.yaml +4 -4
  2. data/.gitignore +2 -5
  3. data/.pryrc +2 -0
  4. data/.travis.yml +3 -0
  5. data/README.md +22 -5
  6. data/Rakefile +7 -1
  7. data/bin/console +14 -0
  8. data/bin/setup +7 -0
  9. data/lib/rocketio.rb +131 -3
  10. data/lib/rocketio/application.rb +31 -0
  11. data/lib/rocketio/controller.rb +288 -0
  12. data/lib/rocketio/controller/authentication.rb +141 -0
  13. data/lib/rocketio/controller/authorization.rb +53 -0
  14. data/lib/rocketio/controller/cookies.rb +59 -0
  15. data/lib/rocketio/controller/error_handlers.rb +89 -0
  16. data/lib/rocketio/controller/filters.rb +119 -0
  17. data/lib/rocketio/controller/flash.rb +21 -0
  18. data/lib/rocketio/controller/helpers.rb +438 -0
  19. data/lib/rocketio/controller/middleware.rb +32 -0
  20. data/lib/rocketio/controller/render.rb +148 -0
  21. data/lib/rocketio/controller/render/engine.rb +76 -0
  22. data/lib/rocketio/controller/render/layout.rb +27 -0
  23. data/lib/rocketio/controller/render/layouts.rb +85 -0
  24. data/lib/rocketio/controller/render/templates.rb +83 -0
  25. data/lib/rocketio/controller/request.rb +115 -0
  26. data/lib/rocketio/controller/response.rb +84 -0
  27. data/lib/rocketio/controller/sessions.rb +64 -0
  28. data/lib/rocketio/controller/token_auth.rb +118 -0
  29. data/lib/rocketio/controller/websocket.rb +21 -0
  30. data/lib/rocketio/error_templates/404.html +3 -0
  31. data/lib/rocketio/error_templates/409.html +7 -0
  32. data/lib/rocketio/error_templates/500.html +3 -0
  33. data/lib/rocketio/error_templates/501.html +6 -0
  34. data/lib/rocketio/error_templates/layout.html +1 -0
  35. data/lib/rocketio/exceptions.rb +4 -0
  36. data/lib/rocketio/router.rb +65 -0
  37. data/lib/rocketio/util.rb +122 -0
  38. data/lib/rocketio/version.rb +2 -2
  39. data/rocketio.gemspec +21 -17
  40. data/test/aliases_test.rb +54 -0
  41. data/test/authentication_test.rb +307 -0
  42. data/test/authorization_test.rb +91 -0
  43. data/test/cache_control_test.rb +268 -0
  44. data/test/content_type_test.rb +124 -0
  45. data/test/cookies_test.rb +49 -0
  46. data/test/error_handlers_test.rb +125 -0
  47. data/test/etag_test.rb +445 -0
  48. data/test/filters_test.rb +177 -0
  49. data/test/halt_test.rb +73 -0
  50. data/test/helpers_test.rb +171 -0
  51. data/test/middleware_test.rb +57 -0
  52. data/test/redirect_test.rb +135 -0
  53. data/test/render/engine_test.rb +71 -0
  54. data/test/render/get.erb +1 -0
  55. data/test/render/items.erb +1 -0
  56. data/test/render/layout.erb +1 -0
  57. data/test/render/layout_test.rb +104 -0
  58. data/test/render/layouts/master.erb +1 -0
  59. data/test/render/layouts_test.rb +145 -0
  60. data/test/render/master.erb +1 -0
  61. data/test/render/post.erb +1 -0
  62. data/test/render/put.erb +1 -0
  63. data/test/render/render_test.rb +101 -0
  64. data/test/render/setup.rb +14 -0
  65. data/test/render/templates/a/get.erb +1 -0
  66. data/test/render/templates/master.erb +1 -0
  67. data/test/render/templates_test.rb +146 -0
  68. data/test/request_test.rb +105 -0
  69. data/test/response_test.rb +119 -0
  70. data/test/routes_test.rb +70 -0
  71. data/test/sendfile_test.rb +209 -0
  72. data/test/sessions_test.rb +176 -0
  73. data/test/setup.rb +59 -0
  74. metadata +144 -9
  75. data/LICENSE.txt +0 -22
@@ -0,0 +1,21 @@
1
+ module RocketIO
2
+ class Flash
3
+ KEY_FORMAT = '__session__flash__%s'.freeze
4
+
5
+ def initialize session = {}
6
+ @session = session
7
+ end
8
+
9
+ def []= key, val
10
+ @session[key(key)] = val
11
+ end
12
+
13
+ def [] key
14
+ @session.delete(key(key))
15
+ end
16
+
17
+ def key key
18
+ KEY_FORMAT % key.to_s
19
+ end
20
+ end
21
+ end
@@ -0,0 +1,438 @@
1
+ # Copyright (c) 2007, 2008, 2009 Blake Mizerany
2
+ # Copyright (c) 2010, 2011, 2012, 2013, 2014 Konstantin Haase
3
+ # Copyright (c) 2015 Slee Woo
4
+ #
5
+ # Permission is hereby granted, free of charge, to any person
6
+ # obtaining a copy of this software and associated documentation
7
+ # files (the "Software"), to deal in the Software without
8
+ # restriction, including without limitation the rights to use,
9
+ # copy, modify, merge, publish, distribute, sublicense, and/or sell
10
+ # copies of the Software, and to permit persons to whom the
11
+ # Software is furnished to do so, subject to the following
12
+ # conditions:
13
+ #
14
+ # The above copyright notice and this permission notice shall be
15
+ # included in all copies or substantial portions of the Software.
16
+ #
17
+ # THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
18
+ # EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES
19
+ # OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
20
+ # NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT
21
+ # HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY,
22
+ # WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
23
+ # FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR
24
+ # OTHER DEALINGS IN THE SOFTWARE.
25
+
26
+ module RocketIO
27
+ class Controller
28
+
29
+ # helpers to determine actual request method
30
+ # `get?` returns true for GET, `post?` for POST etc.
31
+ RocketIO::REQUEST_METHODS.each_key do |request_method|
32
+ define_method request_method.downcase + '?' do
33
+ env[RocketIO::REQUEST_METHOD] == request_method
34
+ end
35
+ end
36
+
37
+ # whether or not the status is set to 1xx
38
+ def informational?
39
+ response.status.between? 100, 199
40
+ end
41
+
42
+ # whether or not the status is set to 2xx
43
+ def success?
44
+ response.status.between? 200, 299
45
+ end
46
+
47
+ # whether or not the status is set to 3xx
48
+ def redirect?
49
+ response.status.between? 300, 399
50
+ end
51
+
52
+ # whether or not the status is set to 4xx
53
+ def client_error?
54
+ response.status.between? 400, 499
55
+ end
56
+
57
+ # whether or not the status is set to 5xx
58
+ def server_error?
59
+ response.status.between? 500, 599
60
+ end
61
+
62
+ # whether or not the status is set to 404
63
+ def not_found?
64
+ response.status == 404
65
+ end
66
+
67
+ # returns true for HTTP/1.1 requests
68
+ def http_1_1?
69
+ env[RocketIO::HTTP_VERSION] == RocketIO::HTTP_1_1
70
+ end
71
+
72
+ def xhr?
73
+ env[RocketIO::HTTP_X_REQUESTED_WITH] == RocketIO::XML_HTTP_REQUEST
74
+ end
75
+
76
+ # switch controller and halt with returned response.
77
+ # any arguments will be passed to requested method.
78
+ #
79
+ # @example pass control to User controller, call requested method without arguments
80
+ # pass User
81
+ #
82
+ # @example pass control to User controller, call requested method with [:bob, :bobsen] arguments
83
+ # pass User, :bob, :bobsen
84
+ #
85
+ def pass controller, *args
86
+ halt controller.initialize_controller(RocketIO::REQUEST_METHODS[request_method], args).call(env)
87
+ end
88
+
89
+ # stop executing any code and send response to browser.
90
+ #
91
+ # accepts an arbitrary number of arguments.
92
+ # if first argument is a Rack::Response,
93
+ # halting right away using the first arg as response and ignore other args.
94
+ #
95
+ # if first arg is a Array, updating current response
96
+ # using first array element as status, second to update headers and 3rd as body
97
+ #
98
+ # if some arg is an Integer, it will be used as status code.
99
+ # if some arg is a Hash, it is treated as headers.
100
+ # any other args are treated as body.
101
+ #
102
+ # if no args given it halts with current response.
103
+ #
104
+ # @example returning "Well Done" body with 200 status code
105
+ # halt 'Well Done'
106
+ #
107
+ # @example halting with current response without alter it in any way
108
+ # halt
109
+ #
110
+ # @example returning a error message with 500 code:
111
+ # halt 500, 'Sorry, some fatal error occurred'
112
+ #
113
+ # @example custom content type
114
+ # halt File.read('/path/to/theme.css'), 'Content-Type' => mime_type('.css')
115
+ #
116
+ # @example sending custom Rack response
117
+ # halt [200, {'Content-Disposition' => "attachment; filename=some-file"}, some_IO_instance]
118
+ #
119
+ # @param [Array] *args
120
+ #
121
+ def halt *args
122
+ args.each do |a|
123
+ case a
124
+ when Rack::Response
125
+ throw(:__response__, a.finish)
126
+ when Fixnum
127
+ response.status = a
128
+ when Array
129
+ if a.size == 3
130
+ response.status = a[0]
131
+ response.headers.update(a[1])
132
+ response.body = a[2]
133
+ break
134
+ else
135
+ response.body = a
136
+ end
137
+ when Hash
138
+ response.headers.update(a)
139
+ else
140
+ response.body = a
141
+ end
142
+ end
143
+ throw(:__response__, response.finish)
144
+ end
145
+
146
+ # @example
147
+ # flash[:alert] = 'some secret'
148
+ # p flash[:alert] #=> "some secret"
149
+ # p flash[:alert] #=> nil
150
+ def flash
151
+ @__flash_proxy__ ||= RocketIO::Flash.new(session)
152
+ end
153
+
154
+ # Halt processing and redirect to the URI provided.
155
+ def redirect uri
156
+ response.status = http_1_1? && !get? ? 303 : 302
157
+
158
+ # According to RFC 2616 section 14.30, "the field value consists of a
159
+ # single absolute URI"
160
+ response[RocketIO::LOCATION] = uri(uri.to_s)
161
+ halt
162
+ end
163
+
164
+ def permanent_redirect uri
165
+ response.status = 301
166
+ response[RocketIO::LOCATION] = uri(uri.to_s)
167
+ halt
168
+ end
169
+
170
+ # Generates the absolute URI for a given path in the app.
171
+ # Takes Rack routers and reverse proxies into account.
172
+ def uri addr = nil, absolute = true, add_script_name = true
173
+ return addr if addr && addr =~ /\A[A-z][A-z0-9\+\.\-]*:/
174
+ uri = [host = ""]
175
+ if absolute
176
+ host << "http#{'s' if request.secure?}://"
177
+ if request.forwarded? or request.port != (request.secure? ? 443 : 80)
178
+ host << request.host_with_port
179
+ else
180
+ host << request.host
181
+ end
182
+ end
183
+ uri << request.script_name.to_s if add_script_name
184
+ uri << (addr ? addr : request.path_info).to_s
185
+ File.join(uri)
186
+ end
187
+
188
+ # Set multiple response headers with Hash.
189
+ def headers hash = nil
190
+ response.headers.merge!(hash) if hash
191
+ response.headers
192
+ end
193
+
194
+ # shorthand for content_type(charset: 'something')
195
+ def charset charset
196
+ content_type(charset: charset)
197
+ end
198
+
199
+ # returns, set or update content type.
200
+ # if called without args it will return current content type.
201
+ # if called with a single argument, given argument will be set as content type.
202
+ # if a type and hash given it will set brand new content type composed of given type and opts.
203
+ # if only a hash given it will update current content type with given opts.
204
+ # if no content type is set it will use default one + given opts.
205
+ #
206
+ # @example set content type
207
+ # content_type '.json'
208
+ #
209
+ # @example set content type with some params
210
+ # content_type '.json', level: 1
211
+ #
212
+ # @example add params to current content type
213
+ # content_type comment: 'Boo!'
214
+ #
215
+ def content_type *args
216
+ return response[RocketIO::CONTENT_TYPE] if args.empty?
217
+ params = args.last.is_a?(Hash) ? args.pop.map {|kv| kv.map!(&:to_s)}.to_h : {}
218
+ default = params.delete('default')
219
+
220
+ if type = args.first
221
+ mime_type = mime_type(type) || default || raise(ArgumentError, "Unknown media type: %p" % type)
222
+ else
223
+ mime_type = response[RocketIO::CONTENT_TYPE]
224
+ end
225
+ mime_type ||= RocketIO::DEFAULT_CONTENT_TYPE
226
+
227
+ mime_type, _params = mime_type.split(';')
228
+ if _params
229
+ params = _params.split(',').map! {|o| o.strip.split('=')}.to_h.merge!(params)
230
+ end
231
+
232
+ if params.any?
233
+ mime_type << '; '
234
+ mime_type << params.map do |key, val|
235
+ val = val.inspect if val =~ /[";,]/
236
+ [key, val]*'='
237
+ end.join(', ')
238
+ end
239
+ response[RocketIO::CONTENT_TYPE] = mime_type
240
+ end
241
+
242
+ # Set the Content-Disposition to "attachment" with the specified filename,
243
+ # instructing the user agents to prompt to save.
244
+ def attachment(filename = nil, disposition = 'attachment')
245
+ response[RocketIO::CONTENT_DISPOSITION] = disposition.to_s
246
+ if filename
247
+ response[RocketIO::CONTENT_DISPOSITION] << ('; filename="%s"' % File.basename(filename))
248
+ ext = File.extname(filename)
249
+ content_type(ext) unless response[RocketIO::CONTENT_TYPE] or ext.empty?
250
+ end
251
+ end
252
+
253
+ # Use the contents of the file at +path+ as the response body.
254
+ def send_file path, opts = {}
255
+ if opts[:type] or not response[RocketIO::CONTENT_TYPE]
256
+ content_type(opts[:type] || File.extname(path), default: RocketIO::APPLICATION_OCTET_STREAM)
257
+ end
258
+
259
+ disposition = opts[:disposition]
260
+ filename = opts[:filename]
261
+ disposition = 'attachment' if disposition.nil? and filename
262
+ filename = path if filename.nil?
263
+ attachment(filename, disposition) if disposition
264
+
265
+ last_modified(opts[:last_modified]) if opts[:last_modified]
266
+
267
+ file = Rack::File.new(nil)
268
+ file.path = path
269
+ result = file.serving(env)
270
+ result[1].each { |k,v| headers[k] ||= v }
271
+ headers[RocketIO::CONTENT_LENGTH] = result[1][RocketIO::CONTENT_LENGTH]
272
+ opts[:status] &&= Integer(opts[:status])
273
+ response.status = opts[:status] || result[0]
274
+ response.body = result[2]
275
+ halt
276
+ rescue Errno::ENOENT
277
+ error(404)
278
+ end
279
+
280
+ # Specify response freshness policy for HTTP caches (Cache-Control header).
281
+ # Any number of non-value directives (:public, :private, :no_cache,
282
+ # :no_store, :must_revalidate, :proxy_revalidate) may be passed along with
283
+ # a Hash of value directives (:max_age, :min_stale, :s_max_age).
284
+ #
285
+ # cache_control :public, :must_revalidate, :max_age => 60
286
+ # => Cache-Control: public, must-revalidate, max-age=60
287
+ #
288
+ # See RFC 2616 / 14.9 for more on standard cache control directives:
289
+ # http://tools.ietf.org/html/rfc2616#section-14.9.1
290
+ def cache_control *values
291
+ if values.last.kind_of?(Hash)
292
+ hash = values.pop
293
+ hash.reject! { |k,v| v == false }
294
+ hash.reject! { |k,v| values << k if v == true }
295
+ else
296
+ hash = {}
297
+ end
298
+
299
+ values.map! { |value| value.to_s.tr('_','-') }
300
+ hash.each do |key, value|
301
+ key = key.to_s.tr('_', '-')
302
+ value = value.to_i if key == "max-age"
303
+ values << "#{key}=#{value}"
304
+ end
305
+
306
+ response[RocketIO::CACHE_CONTROL] = values.join(', ') if values.any?
307
+ end
308
+
309
+ # Set the Expires header and Cache-Control/max-age directive. Amount
310
+ # can be an integer number of seconds in the future or a Time object
311
+ # indicating when the response should be considered "stale". The remaining
312
+ # "values" arguments are passed to the #cache_control helper:
313
+ #
314
+ # expires 500, :public, :must_revalidate
315
+ # => Cache-Control: public, must-revalidate, max-age=60
316
+ # => Expires: Mon, 08 Jun 2009 08:50:17 GMT
317
+ #
318
+ def expires amount, *values
319
+ values << {} unless values.last.kind_of?(Hash)
320
+
321
+ if amount.is_a?(Integer)
322
+ time = Time.now + amount.to_i
323
+ max_age = amount
324
+ else
325
+ time = time_for amount
326
+ max_age = time - Time.now
327
+ end
328
+
329
+ values.last.merge!(:max_age => max_age)
330
+ cache_control(*values)
331
+
332
+ response[RocketIO::EXPIRES] = time.httpdate
333
+ end
334
+
335
+ # Set the last modified time of the resource (HTTP 'Last-Modified' header)
336
+ # and halt if conditional GET matches. The +time+ argument is a Time,
337
+ # DateTime, or other object that responds to +to_time+.
338
+ #
339
+ # When the current request includes an 'If-Modified-Since' header that is
340
+ # equal or later than the time specified, execution is immediately halted
341
+ # with a '304 Not Modified' response.
342
+ def last_modified time
343
+ return unless time
344
+ time = time_for(time)
345
+ response[RocketIO::LAST_MODIFIED] = time.httpdate
346
+ return if env[RocketIO::HTTP_IF_NONE_MATCH]
347
+
348
+ if response.ok? && env[RocketIO::HTTP_IF_MODIFIED_SINCE]
349
+ # compare based on seconds since epoch
350
+ since = Time.httpdate(env[RocketIO::HTTP_IF_MODIFIED_SINCE]).to_i
351
+ halt(304) if since >= time.to_i
352
+ end
353
+
354
+ if (response.successful? || response.precondition_failed?) && env[RocketIO::HTTP_IF_UNMODIFIED_SINCE]
355
+ # compare based on seconds since epoch
356
+ since = Time.httpdate(env[RocketIO::HTTP_IF_UNMODIFIED_SINCE]).to_i
357
+ halt(412) if since < time.to_i
358
+ end
359
+ rescue ArgumentError
360
+ end
361
+
362
+ # Set the response entity tag (HTTP 'ETag' header) and halt if conditional
363
+ # GET matches. The +value+ argument is an identifier that uniquely
364
+ # identifies the current version of the resource. The +kind+ argument
365
+ # indicates whether the etag should be used as a :strong (default) or :weak
366
+ # cache validator.
367
+ #
368
+ # When the current request includes an 'If-None-Match' header with a
369
+ # matching etag, execution is immediately halted. If the request method is
370
+ # GET or HEAD, a '304 Not Modified' response is sent.
371
+ def etag value, options = {}
372
+ # Before touching this code, please double check RFC 2616 14.24 and 14.26.
373
+ options = {:kind => options} unless Hash === options
374
+ kind = options[:kind] || :strong
375
+ new_resource = options.fetch(:new_resource) { request.post? }
376
+
377
+ unless RocketIO::ETAG_KINDS.include?(kind)
378
+ raise ArgumentError, ":strong or :weak expected"
379
+ end
380
+
381
+ value = '"%s"' % value
382
+ value = "W/#{value}" if kind == :weak
383
+ response[RocketIO::ETAG] = value
384
+
385
+ if response.successful? || response.not_modified?
386
+ if etag_matches?(env[RocketIO::HTTP_IF_NONE_MATCH], new_resource)
387
+ response.status = request.safe? ? 304 : 412
388
+ halt
389
+ end
390
+
391
+ if env[RocketIO::HTTP_IF_MATCH]
392
+ unless etag_matches?(env[RocketIO::HTTP_IF_MATCH], new_resource)
393
+ response.status = 412
394
+ halt
395
+ end
396
+ end
397
+ end
398
+ end
399
+
400
+ # Sugar for redirect (example: redirect back)
401
+ def back
402
+ request.referer
403
+ end
404
+
405
+ # Generates a Time object from the given value.
406
+ # Used by #expires and #last_modified.
407
+ def time_for value
408
+ if value.respond_to?(:to_time)
409
+ value.to_time
410
+ elsif value.is_a?(Time)
411
+ value
412
+ elsif value.respond_to?(:new_offset)
413
+ # DateTime#to_time does the same on 1.9
414
+ d = value.new_offset 0
415
+ t = Time.utc(d.year, d.mon, d.mday, d.hour, d.min, d.sec + d.sec_fraction)
416
+ t.getlocal
417
+ elsif value.respond_to?(:mday)
418
+ # Date#to_time does the same on 1.9
419
+ Time.local(value.year, value.mon, value.mday)
420
+ elsif value.is_a? Numeric
421
+ Time.at value
422
+ else
423
+ Time.parse value.to_s
424
+ end
425
+ rescue ArgumentError => boom
426
+ raise boom
427
+ rescue Exception
428
+ raise ArgumentError, "unable to convert #{value.inspect} to a Time object"
429
+ end
430
+
431
+ private
432
+ # Helper method checking if a ETag value list includes the current ETag.
433
+ def etag_matches? list, new_resource = request.post?
434
+ return !new_resource if list == '*'
435
+ list.to_s.split(/\s*,\s*/).include? response[RocketIO::ETAG]
436
+ end
437
+ end
438
+ end