rocketio 0.0.0 → 0.0.1

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