rocketio 0.1.0 → 0.2.0

Sign up to get free protection for your applications and to get access to all the features.
Files changed (50) hide show
  1. checksums.yaml +4 -4
  2. data/lib/rocketio.rb +6 -3
  3. data/lib/rocketio/application.rb +3 -7
  4. data/lib/rocketio/controller.rb +62 -90
  5. data/lib/rocketio/controller/authentication.rb +38 -44
  6. data/lib/rocketio/controller/authorization.rb +8 -4
  7. data/lib/rocketio/controller/error_handlers.rb +12 -8
  8. data/lib/rocketio/controller/filters.rb +14 -19
  9. data/lib/rocketio/controller/helpers.rb +1 -1
  10. data/lib/rocketio/controller/middleware.rb +1 -1
  11. data/lib/rocketio/controller/render/engine.rb +3 -3
  12. data/lib/rocketio/controller/render/layout.rb +1 -1
  13. data/lib/rocketio/controller/render/layouts.rb +6 -6
  14. data/lib/rocketio/controller/render/template_vars.rb +3 -3
  15. data/lib/rocketio/controller/render/templates.rb +6 -6
  16. data/lib/rocketio/controller/sessions.rb +1 -1
  17. data/lib/rocketio/error_templates/409.html +11 -7
  18. data/lib/rocketio/error_templates/501.html +4 -4
  19. data/lib/rocketio/router.rb +35 -21
  20. data/lib/rocketio/version.rb +1 -1
  21. data/rocketio.gemspec +2 -0
  22. data/test/aliases_test.rb +2 -2
  23. data/test/api_test.rb +24 -117
  24. data/test/authentication_test.rb +96 -60
  25. data/test/authorization_test.rb +28 -17
  26. data/test/cache_control_test.rb +12 -12
  27. data/test/content_type_test.rb +7 -7
  28. data/test/cookies_test.rb +4 -4
  29. data/test/error_handlers_test.rb +14 -12
  30. data/test/etag_test.rb +32 -32
  31. data/test/filters_test.rb +96 -79
  32. data/test/halt_test.rb +1 -1
  33. data/test/helpers_test.rb +6 -6
  34. data/test/middleware_test.rb +4 -4
  35. data/test/redirect_test.rb +6 -7
  36. data/test/render/{post.erb → b.erb} +0 -0
  37. data/test/render/{put.erb → c.erb} +0 -0
  38. data/test/render/engine_test.rb +5 -5
  39. data/test/render/{get.erb → index.erb} +0 -0
  40. data/test/render/layout_test.rb +21 -17
  41. data/test/render/layouts_test.rb +14 -14
  42. data/test/render/render_test.rb +17 -14
  43. data/test/render/template_vars_test.rb +9 -9
  44. data/test/render/templates_test.rb +16 -16
  45. data/test/response_test.rb +4 -4
  46. data/test/routes_test.rb +21 -42
  47. data/test/sendfile_test.rb +8 -8
  48. data/test/sessions_test.rb +27 -27
  49. data/test/setup.rb +2 -0
  50. metadata +34 -6
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA1:
3
- metadata.gz: e9540db8ccba375983a890602be457287f31f7e2
4
- data.tar.gz: aeeba39bb33341e1c015c099040793df15e1b396
3
+ metadata.gz: c81e2dd778eb2ae80019ccfdbd0a07da71685609
4
+ data.tar.gz: 18fb36dc539e788db0c8ca0a4143fbf8867eab23
5
5
  SHA512:
6
- metadata.gz: 135b3a8e35301383adf5b21ae1a3a80aa0639feb1d44d5cd96e82c4c4da453844890cffe4517aceb2b5642d664d9893bd8889c10d7d05a58026a4e7220c1544b
7
- data.tar.gz: b526fcf90b86d59f0e1a3cf6cd847c1d752919ef4e27a6c9b4e85a741b06795e1a3b7ed2554dfbfe300d3e5b3014390e224840939008a00b2ebf155d37c31873
6
+ metadata.gz: b5d4954032b4b5fedfa2d43cfff1b923f8a43fabab4eb41bf3e3fa5592ad7459fc836859553b2e37d84ec65812f04a4371ab7a276ddcbdde09892c2aa88c9ba4
7
+ data.tar.gz: 5d19a34c16c3f4aaf635940796a6f1d435d11d4a12de9c6e55b5dc543c830ad79d83a2db26b9e47e206cbf4db3a23b61a14999e285aa2dce8388b44955f09f24
data/lib/rocketio.rb CHANGED
@@ -47,13 +47,16 @@ module RocketIO
47
47
  HEAD => :head
48
48
  }.freeze
49
49
 
50
+ INDEX_METHOD = :index
51
+
50
52
  EMPTY_STRING = ''.freeze
51
53
  EMPTY_STRING_PROC = proc {RocketIO::EMPTY_STRING}
52
54
  EMPTY_ARRAY = [].freeze
53
55
  EMPTY_HASH = {}.freeze
54
56
 
55
- SLASH = '/'.freeze
56
- QUERY_PREFIX = '?'.freeze
57
+ SLASH = '/'.freeze
58
+ QUERY_PREFIX = '?'.freeze
59
+ PATH_SPLITTER = /[^\/]+/.freeze
57
60
 
58
61
  CONTENT_TYPE = 'Content-Type'.freeze
59
62
  DEFAULT_CONTENT_TYPE = 'text/html'.freeze
@@ -72,7 +75,7 @@ module RocketIO
72
75
  HTTP_AUTHORIZATION_KEYS = ['HTTP_AUTHORIZATION', 'X-HTTP_AUTHORIZATION', 'X_HTTP_AUTHORIZATION'].map(&:freeze).freeze
73
76
  HTTP_AUTHORIZATION_MOCKS = {
74
77
  basic: 'Basic Og=='.freeze,
75
- digest: 'Digest opaque="", qop="auth", uri="/"'.freeze
78
+ digest: 'Digest opaque="", qop="auth", uri="%s"'.freeze
76
79
  }.freeze
77
80
  HTTP_UPGRADE = 'HTTP_UPGRADE'.freeze
78
81
  UPGRADE = 'upgrade'.freeze
@@ -11,15 +11,11 @@ module RocketIO
11
11
 
12
12
  def call env
13
13
  catch :__response__ do # catch 404 errors and potential `halt` calls from middleware
14
- controller, path_params = @router.resolve_path(env[PATH_INFO])
14
+ controller, method, path_params = @router.resolve_path(env[PATH_INFO])
15
15
 
16
- unless controller
17
- controller = RocketIO::Controller.initialize_controller
18
- controller.env = env
19
- controller.error(404)
20
- end
16
+ controller || Controller.new(INDEX_METHOD, EMPTY_ARRAY, env).error(404)
21
17
 
22
- controller = controller.initialize_controller(REQUEST_METHODS[env[REQUEST_METHOD]], path_params)
18
+ controller = controller.new(method, path_params)
23
19
  chain = controller.middleware.reverse.inject(controller) {|app,ware| ware.call(app)}
24
20
  if controller.sessions
25
21
  chain = controller.sessions[0].new(chain, controller.sessions[1])
@@ -2,35 +2,36 @@ module RocketIO
2
2
  class Controller
3
3
  extend Forwardable
4
4
 
5
- def_delegators 'self.class', :url, :dirname, :parameters_policy
5
+ def_delegators :'self.class', :url, :api, :dirname, :parameters_policy
6
6
  def_delegators :request, :session, :request_method
7
- def_delegators RocketIO, :indifferent_params, :indifferent_hash, :mime_type, :engine_const, :engine_class
7
+ def_delegators RocketIO, :environment
8
+ def_delegators RocketIO, :indifferent_params, :indifferent_hash, :mime_type
9
+ def_delegators RocketIO, :engine_const, :engine_class
8
10
  def_delegators CGI, :escape_html
9
11
 
10
- def_delegators RocketIO, :environment
12
+ # defining instance helpers for querying current environment like
13
+ # development? production? etc.
11
14
  RocketIO::ENVIRONMENTS.each_key do |env|
12
15
  def_delegators RocketIO, :"#{env}?"
13
16
  end
14
17
 
15
- # defining get, post etc. methods that will be called
16
- # when a request matches current controller and appropriate request method used.
17
- #
18
- # by default all requests, except HEAD, will return a NotImplementedError.
19
- # override the methods you need to be handled by controller.
20
- #
21
- RocketIO::REQUEST_METHODS.each_value do |verb|
22
- define_method(verb) {|*| error(501)}
18
+ def initialize requested_method = RocketIO::INDEX_METHOD, path_params = RocketIO::EMPTY_ARRAY, env = nil
19
+ @__requested_method__ = requested_method
20
+ @__path_params_array__ = path_params.freeze
21
+ @__env__ = env if env
23
22
  end
24
- def head(*); end
25
23
 
26
- # call requested method.
27
- # also call #before, #around and #after filters.
24
+ # call requested method
28
25
  #
29
26
  # @param [Hash] env
30
27
  # @return [Rack::Response]
31
28
  #
32
29
  def call env
30
+ @__env__ = env
33
31
  catch :__response__ do
32
+
33
+ api[requested_method] || error(501)
34
+
34
35
  if error_handlers[500]
35
36
  begin
36
37
  __call__(env)
@@ -43,8 +44,9 @@ module RocketIO
43
44
  end
44
45
  end
45
46
 
46
- private def __call__ env
47
- self.env = env
47
+ private
48
+ def __call__ env
49
+
48
50
  validate_or_request_authentication_if_needed
49
51
  validate_or_request_authorization_if_needed
50
52
  validate_parameters
@@ -57,53 +59,51 @@ module RocketIO
57
59
  invoke_after_filter
58
60
  }
59
61
 
60
- response.body ||= RocketIO::EMPTY_ARRAY
61
- response.body = [] if head? # dropping body on HEAD requests
62
+ if head? # dropping body on HEAD requests
63
+ response.body = RocketIO::EMPTY_ARRAY
64
+ else
65
+ response.body ||= RocketIO::EMPTY_ARRAY
66
+ end
67
+
62
68
  response.finish
63
69
  end
64
70
 
65
- private def __run__ app
71
+ def __run__ app
66
72
  app.call
67
73
  end
68
74
 
69
- def env= env
70
- @__env__ = env
71
- end
72
-
73
75
  def env
74
76
  @__env__
75
77
  end
76
78
 
77
79
  def validate_parameters
78
- # enforce policy only if a method defined for current request method
79
- # cause stock REST methods accepting any number of arguments
80
- return unless policy = parameters_policy[requested_method]
81
80
 
82
- if path_params_array.size >= policy[:min]
83
- return if policy[:max] == :* || path_params_array.size <= policy[:max]
81
+ if path_params_array.size >= api[requested_method][:parameters_policy][:min]
82
+ return if api[requested_method][:parameters_policy][:max] == :* ||
83
+ path_params_array.size <= api[requested_method][:parameters_policy][:max]
84
84
  end
85
85
 
86
86
  error(409)
87
87
  end
88
88
 
89
- def path_params
90
- @__path_params__ ||= begin
91
- rangemap = self.class.path_params[requested_method] ||
92
- raise(StandardError, 'No path_params map found for %s method' % requested_method)
93
- indifferent_params(rangemap.each_with_object({}) {|(m,r),o|
94
- o[m] = if r.min && r.max
95
- r.min == r.max ? path_params_array[r.min] : path_params_array[r]
96
- else
97
- path_params_array[r]
98
- end
99
- }).freeze
100
- end
89
+ def requested_method
90
+ @__requested_method__
101
91
  end
102
92
 
103
93
  def path_params_array
104
94
  @__path_params_array__
105
95
  end
106
96
 
97
+ def path_params
98
+ @__path_params__ ||= api[requested_method][:path_params].each_with_object({}) {|(m,r),o|
99
+ o[m] = if r.min && r.max
100
+ r.min == r.max ? path_params_array[r.min] : path_params_array[r]
101
+ else
102
+ path_params_array[r]
103
+ end
104
+ }.freeze
105
+ end
106
+
107
107
  def params
108
108
  @__params__ ||= indifferent_params(request.params)
109
109
  end
@@ -115,30 +115,12 @@ module RocketIO
115
115
  def response
116
116
  @__response__ ||= RocketIO::Response.new
117
117
  end
118
-
119
- def requested_method
120
- @__requested_method__ ||= RocketIO::REQUEST_METHODS[request_method]
121
- end
122
118
  end
123
119
 
124
120
  class << Controller
125
121
 
126
- # only public non-inherited methods are included in public API directly.
127
122
  def api
128
- return [] if self == RocketIO::Controller
129
- public_instance_methods(false).concat(inherited_api) - private_api
130
- end
131
-
132
- # inherited methods are excluded from public API
133
- # but we still need a way to know what API methods was inherited
134
- def inherited_api
135
- @__inherited_api__
136
- end
137
-
138
- # used internally to keep a list of public methods
139
- # that should be excluded from public API
140
- def private_api
141
- @__private_api__
123
+ @__api__
142
124
  end
143
125
 
144
126
  # import some config from some controller
@@ -147,11 +129,10 @@ module RocketIO
147
129
  end
148
130
 
149
131
  def inherited base
150
- # registering new controller
132
+ # registering a new controller
151
133
  RocketIO.controllers.push(base)
152
134
 
153
- base.instance_variable_set(:@__private_api__, self.private_api.uniq)
154
- base.instance_variable_set(:@__inherited_api__, self.api.freeze)
135
+ base.instance_variable_set(:@__api__, self.api.dup)
155
136
 
156
137
  # new controller inherits all setups from superclass
157
138
  base.import :before, from: self
@@ -250,10 +231,11 @@ module RocketIO
250
231
  @__aliases__ ||= []
251
232
  end
252
233
 
253
- # build a URL from given chunks prefixing them with actual path
234
+ # build a URL from given chunks prefixing them with controller's baseurl
254
235
  #
255
236
  # @param *args [Array]
256
237
  # @return [String]
238
+ #
257
239
  def url *args
258
240
  return @__url__ if args.empty?
259
241
  query = if args.last.is_a?(Hash)
@@ -265,45 +247,35 @@ module RocketIO
265
247
  end
266
248
 
267
249
  def method_added meth
268
- parameters = instance_method(meth).parameters
269
- path_params[meth] = RocketIO.path_params(parameters).freeze
270
- if requested_method = RocketIO::REQUEST_METHODS.values.find {|verb| verb == meth}
271
- # REST methods should be called with a predetermined set of parameters.
272
- # setting an appropriate policy for just defined method based on its parameters.
273
- parameters_policy[requested_method] = RocketIO.parameters_policy(parameters).freeze
274
- end
275
- end
250
+ return if self == RocketIO::Controller
251
+ return unless public_instance_methods(false).include?(meth)
276
252
 
277
- # initializing the controller to process a HTTP request
278
- #
279
- # @param path_params [Array]
280
- # @return a RocketIO::Route instance
281
- def initialize_controller requested_method = nil, path_params_array = nil
282
- controller = allocate
283
- controller.instance_variable_set(:@__requested_method__, requested_method.to_sym) if requested_method
284
- controller.instance_variable_set(:@__path_params_array__, (path_params_array || []).freeze)
285
- controller
286
- end
287
-
288
- def parameters_policy
289
- @__parameters_policy__ ||= {}
290
- end
253
+ parameters = instance_method(meth).parameters
291
254
 
292
- def path_params
293
- @__path_params__ ||= {}
255
+ api[meth] = {
256
+ # api methods should be called with a predetermined set of parameters
257
+ # so setting an appropriate policy for just defined method based on its parameters.
258
+ path_params: RocketIO.path_params(parameters).freeze,
259
+ parameters_policy: RocketIO.parameters_policy(parameters).freeze
260
+ }
294
261
  end
295
262
 
296
263
  def dirname *args
297
264
  ::File.join(@__dirname__, args.map!(&:to_s))
298
265
  end
299
266
 
300
- # making controller to act as a Rack application
301
267
  def call env
302
- initialize_controller.call(env)
268
+ path_params = env[RocketIO::PATH_INFO].sub(url, RocketIO::EMPTY_STRING).scan(RocketIO::PATH_SPLITTER)
269
+ method = if path_params.any? && api[path_params[0].to_sym]
270
+ path_params.slice!(0).to_sym
271
+ else
272
+ RocketIO::INDEX_METHOD
273
+ end
274
+ new(method, path_params).call(env)
303
275
  end
304
276
  end
305
277
 
306
- Controller.instance_variable_set(:@__private_api__, [])
278
+ Controller.instance_variable_set(:@__api__, {})
307
279
  end
308
280
 
309
281
  require 'rocketio/controller/authentication'
@@ -3,37 +3,34 @@ module RocketIO
3
3
 
4
4
  # easily restrict access to controller using basic auth
5
5
  #
6
- # @example protect all request methods
6
+ # @example protect all methods on any request methods
7
7
  # basic_auth do |user,pass|
8
8
  # user == 'admin' && pass == 'super secret password'
9
9
  # end
10
10
  #
11
- # @example protect only POST, PUT and DELETE request methods
12
- # basic_auth :post, :put, :delete do |user,pass|
13
- # user == 'admin' && pass == 'super secret password'
11
+ # @example protect all methods only on POST, PUT and DELETE request methods
12
+ # basic_auth do |user,pass|
13
+ # post? || put? || delete? ?
14
+ # user == 'admin' && pass == 'super secret password' :
15
+ # true
14
16
  # end
15
17
  #
16
- # @example use different credentials for GET and POST
17
- # basic_auth :get do |user,pass|
18
+ # @example protect only :edit
19
+ # basic_auth :edit do |user,pass|
18
20
  # user == 'reader' && pass == 'readPass'
19
21
  # end
20
22
  #
21
- # basic_auth :post do |user,pass|
22
- # user == 'poster' && pass == 'writePass'
23
- # end
24
- #
25
- # @note authorization is composable, that's it, if superclass is protecting :get method
26
- # and current controller protects :post method,
27
- # both :get and :post will be protected in current controller
23
+ # @note authorization is composable, that's it, if superclass is protecting :a
24
+ # and current controller is protecting :b method,
25
+ # both :a and :b will be protected in current controller
28
26
  #
29
27
  # @params *args [Array]
30
28
  # @param block [Proc]
31
29
  #
32
30
  def self.basic_auth *args, &block
33
31
  opts = args.last.is_a?(Hash) ? args.pop : {}
34
- rqms = args.any? ? args.map!(&:to_sym) : RocketIO::REQUEST_METHODS.values
35
- rqms.each do |rm|
36
- (@__basic_auth__ ||= {})[rm] = {
32
+ (args.any? ? args.map(&:to_sym) : [:*]).each do |method|
33
+ (@__basic_auth__ ||= {})[method] = {
37
34
  class: Rack::Auth::Basic,
38
35
  arguments: [opts[:realm] || RocketIO::DEFAULT_AUTH_REALM].freeze,
39
36
  block: block,
@@ -44,20 +41,20 @@ module RocketIO
44
41
  end
45
42
 
46
43
  def self.define_basic_auth_methods source = self
47
- prompts = (source.instance_variable_get(:@__basic_auth__) || {}).each_with_object(allocate.basic_auth.dup) do |(rm,p),o|
48
- method = :"__basic_auth__#{rm}__"
49
- private_api << define_method(method, &p[:block])
50
- o[rm] = p.merge(method: method).freeze
44
+ prompts = (source.instance_variable_get(:@__basic_auth__) || {}).each_with_object(allocate.basic_auth.dup) do |(m,p),o|
45
+ method = :"__basic_auth__#{m}__"
46
+ api.delete define_method(method, &p[:block])
47
+ o[m] = p.merge(method: method).freeze
51
48
  end.freeze
52
49
  return if prompts.empty?
53
- private_api << define_method(:basic_auth) {prompts}
50
+ api.delete define_method(:basic_auth) {prompts}
54
51
  end
55
52
 
56
53
  def basic_auth; RocketIO::EMPTY_HASH end
57
54
 
58
55
  # easily restrict access to controller using digest auth
59
56
  #
60
- # @example protect all request methods using hashed passwords
57
+ # @example protect all methods using hashed passwords
61
58
  # # hash the password somewhere in irb:
62
59
  # # ::Digest::MD5.hexdigest 'admin:AccessRestricted:somePassword'
63
60
  # # username ^ realm ^ password ^
@@ -68,25 +65,16 @@ module RocketIO
68
65
  # {'admin' => '9d77d54decc22cdcfb670b7b79ee0ef0'}[user]
69
66
  # end
70
67
  #
71
- # @example protect all request methods using plain passwords
68
+ # @example protect all methods using plain passwords
72
69
  # digest_auth do |user|
73
70
  # {'admin' => 'password'}[user]
74
71
  # end
75
72
  #
76
- # @example protect only POST, PUT and DELETE request methods
77
- # digest_auth :post, :put, :delete do |user|
73
+ # @example protect only :create and :edit methods
74
+ # digest_auth :create, :edit do |user|
78
75
  # {'admin' => 'password'}[user]
79
76
  # end
80
77
  #
81
- # @example use different credentials for GET and POST
82
- # digest_auth :get do |user|
83
- # {'user' => 'readPass'}[user]
84
- # end
85
- #
86
- # digest_auth :post do |user|
87
- # {'poster' => 'writePass'}[user]
88
- # end
89
- #
90
78
  # @params *args [Array]
91
79
  # @param block [Proc]
92
80
  #
@@ -94,9 +82,8 @@ module RocketIO
94
82
  opts = args.last.is_a?(Hash) ? args.pop : {}
95
83
  opts[:realm] ||= RocketIO::DEFAULT_AUTH_REALM
96
84
  opts[:opaque] ||= opts[:realm]
97
- rqms = args.any? ? args.map!(&:to_sym) : RocketIO::REQUEST_METHODS.values
98
- rqms.each do |rm|
99
- (@__digest_auth__ ||= {})[rm] = {
85
+ (args.any? ? args.map(&:to_sym) : [:*]).each do |method|
86
+ (@__digest_auth__ ||= {})[method] = {
100
87
  class: Rack::Auth::Digest::MD5,
101
88
  arguments: [opts].freeze,
102
89
  block: block,
@@ -107,13 +94,13 @@ module RocketIO
107
94
  end
108
95
 
109
96
  def self.define_digest_auth_methods source = self
110
- prompts = (source.instance_variable_get(:@__digest_auth__) || {}).each_with_object(allocate.digest_auth.dup) do |(rm,p),o|
111
- method = :"__digest_auth__#{rm}__"
112
- private_api << define_method(method, &p[:block])
113
- o[rm] = p.merge(method: method).freeze
97
+ prompts = (source.instance_variable_get(:@__digest_auth__) || {}).each_with_object(allocate.digest_auth.dup) do |(m,p),o|
98
+ method = :"__digest_auth__#{m}__"
99
+ api.delete define_method(method, &p[:block])
100
+ o[m] = p.merge(method: method).freeze
114
101
  end.freeze
115
102
  return if prompts.empty?
116
- private_api << define_method(:digest_auth) {prompts}
103
+ api.delete define_method(:digest_auth) {prompts}
117
104
  end
118
105
 
119
106
  def digest_auth; RocketIO::EMPTY_HASH end
@@ -123,17 +110,24 @@ module RocketIO
123
110
  # checks whether authentication is required and
124
111
  # send an authorization request if credentials not present or invalid
125
112
  def validate_or_request_authentication_if_needed
126
- return unless auth = digest_auth[requested_method] || basic_auth[requested_method]
113
+ return unless auth = authentication_required?
127
114
  return unless prompt = auth[:class].new(proc {}, *auth[:arguments]) do |*a|
128
115
  self.__send__(auth[:method], *a)
129
116
  end.call(
130
117
  if RocketIO::HTTP_AUTHORIZATION_KEYS.detect {|key| env.has_key?(key)}
131
118
  env
132
119
  else
133
- env.merge(RocketIO::HTTP_AUTHORIZATION_KEYS.first => auth[:mock])
120
+ env.merge(RocketIO::HTTP_AUTHORIZATION_KEYS.first => auth[:mock] % env[RocketIO::PATH_INFO])
134
121
  end
135
122
  )
136
123
  throw(:__response__, prompt)
137
124
  end
125
+
126
+ def authentication_required?
127
+ digest_auth[requested_method] ||
128
+ basic_auth[requested_method] ||
129
+ digest_auth[:*] ||
130
+ basic_auth[:*]
131
+ end
138
132
  end
139
133
  end