rocketio 0.0.0.pre.alpha

Sign up to get free protection for your applications and to get access to all the features.
Files changed (74) hide show
  1. checksums.yaml +7 -0
  2. data/.gitignore +11 -0
  3. data/.travis.yml +3 -0
  4. data/Gemfile +4 -0
  5. data/README.md +48 -0
  6. data/Rakefile +8 -0
  7. data/bin/console +14 -0
  8. data/bin/setup +7 -0
  9. data/lib/rocketio.rb +133 -0
  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 +3 -0
  39. data/rocketio.gemspec +27 -0
  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 +215 -0
@@ -0,0 +1,141 @@
1
+ module RocketIO
2
+ class Controller
3
+
4
+ # easily restrict access to controller using basic auth
5
+ #
6
+ # @example protect all request methods
7
+ # basic_auth do |user,pass|
8
+ # user == 'admin' && pass == 'super secret password'
9
+ # end
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'
14
+ # end
15
+ #
16
+ # @example use different credentials for GET and POST
17
+ # basic_auth :get do |user,pass|
18
+ # user == 'reader' && pass == 'readPass'
19
+ # end
20
+ #
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
28
+ #
29
+ # @params *args [Array]
30
+ # @param block [Proc]
31
+ #
32
+ def self.basic_auth *args, &block
33
+ 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] = {
37
+ class: Rack::Auth::Basic,
38
+ arguments: [opts[:realm] || RocketIO::DEFAULT_AUTH_REALM].freeze,
39
+ block: block,
40
+ mock: RocketIO::HTTP_AUTHORIZATION_MOCKS[:basic]
41
+ }.freeze
42
+ end
43
+ define_basic_auth_methods
44
+ end
45
+
46
+ 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
+ define_method(method, &p[:block])
50
+ o[rm] = p.merge(method: method).freeze
51
+ end.freeze
52
+ return if prompts.empty?
53
+ define_method(:basic_auth) {prompts}
54
+ end
55
+
56
+ def basic_auth; RocketIO::EMPTY_HASH end
57
+
58
+ # easily restrict access to controller using digest auth
59
+ #
60
+ # @example protect all request methods using hashed passwords
61
+ # # hash the password somewhere in irb:
62
+ # # ::Digest::MD5.hexdigest 'admin:AccessRestricted:somePassword'
63
+ # # username ^ realm ^ password ^
64
+ #
65
+ # #=> 9d77d54decc22cdcfb670b7b79ee0ef0
66
+ #
67
+ # digest_auth :passwords_hashed => true, :realm => 'AccessRestricted' do |user|
68
+ # {'admin' => '9d77d54decc22cdcfb670b7b79ee0ef0'}[user]
69
+ # end
70
+ #
71
+ # @example protect all request methods using plain passwords
72
+ # digest_auth do |user|
73
+ # {'admin' => 'password'}[user]
74
+ # end
75
+ #
76
+ # @example protect only POST, PUT and DELETE request methods
77
+ # digest_auth :post, :put, :delete do |user|
78
+ # {'admin' => 'password'}[user]
79
+ # end
80
+ #
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
+ # @params *args [Array]
91
+ # @param block [Proc]
92
+ #
93
+ def self.digest_auth *args, &block
94
+ opts = args.last.is_a?(Hash) ? args.pop : {}
95
+ opts[:realm] ||= RocketIO::DEFAULT_AUTH_REALM
96
+ 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] = {
100
+ class: Rack::Auth::Digest::MD5,
101
+ arguments: [opts].freeze,
102
+ block: block,
103
+ mock: RocketIO::HTTP_AUTHORIZATION_MOCKS[:digest]
104
+ }.freeze
105
+ end
106
+ define_digest_auth_methods
107
+ end
108
+
109
+ 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
+ define_method(method, &p[:block])
113
+ o[rm] = p.merge(method: method).freeze
114
+ end.freeze
115
+ return if prompts.empty?
116
+ define_method(:digest_auth) {prompts}
117
+ end
118
+
119
+ def digest_auth; RocketIO::EMPTY_HASH end
120
+
121
+ def user?
122
+ env[RocketIO::REMOTE_USER]
123
+ end
124
+
125
+ # checks whether authentication is required and
126
+ # send an authorization request if credentials not present or invalid
127
+ def validate_or_request_authentication_if_needed
128
+ return unless auth = digest_auth[requested_method] || basic_auth[requested_method]
129
+ return unless prompt = auth[:class].new(proc {}, *auth[:arguments]) do |*a|
130
+ self.__send__(auth[:method], *a)
131
+ end.call(
132
+ if RocketIO::HTTP_AUTHORIZATION_KEYS.detect {|key| env.has_key?(key)}
133
+ env
134
+ else
135
+ env.merge(RocketIO::HTTP_AUTHORIZATION_KEYS.first => auth[:mock])
136
+ end
137
+ )
138
+ throw(:__response__, prompt)
139
+ end
140
+ end
141
+ end
@@ -0,0 +1,53 @@
1
+ require 'rocketio/controller/token_auth'
2
+
3
+ module RocketIO
4
+ class Controller
5
+
6
+ # easily restrict access to controller using token auth
7
+ #
8
+ # @example simple Token example
9
+ #
10
+ # class User < RocketIO::Controller
11
+ # token_auth { |token| token == 'secret' }
12
+ # end
13
+ #
14
+ def self.token_auth *args, &block
15
+ opts = args.last.is_a?(Hash) ? args.pop : {}
16
+ (args.any? ? args.map!(&:to_sym) : RocketIO::REQUEST_METHODS.values).each do |rm|
17
+ (@__token_auth__ ||= {})[rm] = {
18
+ realm: opts[:realm] || RocketIO::DEFAULT_TOKEN_AUTH_REALM.freeze,
19
+ block: block
20
+ }
21
+ end
22
+ define_token_auth_methods
23
+ end
24
+
25
+ def self.define_token_auth_methods source = self
26
+ prompts = allocate.token_auth.merge(source.instance_variable_get(:@__token_auth__) || {}).freeze
27
+ return if prompts.empty?
28
+ define_method(:token_auth) {prompts}
29
+ end
30
+
31
+ def token_auth; RocketIO::EMPTY_HASH end
32
+
33
+ def validate_or_request_authorization_if_needed
34
+ return unless auth = token_auth[requested_method]
35
+ return if validate_token_auth(&auth[:block])
36
+ throw(:__response__, request_token_auth(auth[:realm]))
37
+ end
38
+
39
+ def validate_or_request_token_auth realm = RocketIO::DEFAULT_TOKEN_AUTH_REALM, &block
40
+ validate_token_auth(&block) || request_token_auth(realm)
41
+ end
42
+
43
+ def validate_token_auth &block
44
+ RocketIO::TokenAuth.authenticate(env, &block)
45
+ end
46
+ alias valid_token_auth? validate_token_auth
47
+
48
+ def request_token_auth realm = RocketIO::DEFAULT_TOKEN_AUTH_REALM
49
+ RocketIO::TokenAuth.authentication_request(realm)
50
+ end
51
+ alias request_token_auth! request_token_auth
52
+ end
53
+ end
@@ -0,0 +1,59 @@
1
+ module RocketIO
2
+ class Controller
3
+
4
+ # shorthand for `request.cookies`, `response.set_cookie` and `response.delete_cookie`
5
+ #
6
+ # @example Setting a cookie
7
+ # cookies['cookie-name'] = 'value'
8
+ #
9
+ # @example Reading a cookie
10
+ # cookies['cookie-name']
11
+ #
12
+ # @example Setting a cookie with custom options
13
+ # cookies['question_of_the_day'] = {
14
+ # value: 'who is not who?',
15
+ # expires: Date.today + 1,
16
+ # secure: true
17
+ # }
18
+ #
19
+ # @example Deleting a cookie
20
+ # cookies.delete('cookie-name')
21
+ #
22
+ def cookies
23
+ @__cookies__ ||= RocketIO::Cookies.new(request.cookies, response)
24
+ end
25
+ end
26
+ end
27
+
28
+ module RocketIO
29
+ class Cookies
30
+
31
+ def initialize cookies, response
32
+ @cookies = RocketIO.indifferent_params(cookies)
33
+ @response = response
34
+ end
35
+
36
+ # set cookie header
37
+ #
38
+ # @param [String, Symbol] key
39
+ # @param [String, Hash] val
40
+ #
41
+ def []= key, val
42
+ @response.set_cookie(key, val)
43
+ end
44
+
45
+ # get cookie by key
46
+ def [] key
47
+ @cookies[key]
48
+ end
49
+
50
+ # instruct browser to delete a cookie
51
+ #
52
+ # @param [String, Symbol] key
53
+ # @param [Hash] opts
54
+ #
55
+ def delete key, opts ={}
56
+ @response.delete_cookie(key, opts)
57
+ end
58
+ end
59
+ end
@@ -0,0 +1,89 @@
1
+ module RocketIO
2
+ class Controller
3
+
4
+ # define error handlers
5
+ #
6
+ # @example define a handler that will process 404 errors
7
+ # class Pages < RocketIO::Controller
8
+ #
9
+ # error 404 do |id|
10
+ # "Sorry, looks like item with ID #{id.to_i} does not exists"
11
+ # end
12
+ #
13
+ # def get id
14
+ # item = Item.find_by(id: id) || error(404, id)
15
+ # end
16
+ # end
17
+ #
18
+ # @example define a handler that will process fatal errors
19
+ # class Pages < RocketIO::Controller
20
+ #
21
+ # error 500 do |exception|
22
+ # "Fatal error occurred: " + html_escape(exception.message)
23
+ # end
24
+ #
25
+ # def get
26
+ # # any exception raised here will be handled by the handler above
27
+ # end
28
+ # end
29
+ #
30
+ def self.error code, &block
31
+ code = code.to_i
32
+ code > 0 || raise(ArgumentError, 'Error code should be a number')
33
+ block || raise(ArgumentError, 'block missing')
34
+ (@__error_handlers__ ||= {})[code] = block
35
+ define_error_handlers_methods
36
+ end
37
+
38
+ def self.define_error_handlers_methods source = self
39
+ handlers = (source.instance_variable_get(:@__error_handlers__) || {}).each_with_object({}) do |(code,proc),o|
40
+ o[code] = :"__#{code}_error_handler__"
41
+ define_method(o[code], &proc)
42
+ end
43
+ handlers.update(allocate.error_handlers)
44
+ return if handlers.empty?
45
+ handlers.freeze
46
+ define_method(:error_handlers) {handlers}
47
+ end
48
+
49
+ def error_handlers; RocketIO::EMPTY_HASH end
50
+
51
+ # if there is a handler defined for given code it will be executed and the result used as body.
52
+ # otherwise the `error` behaves exactly as `halt`.
53
+ #
54
+ # given args will be passed either to handler(if any defined) or to `halt`
55
+ #
56
+ def error code, *args
57
+ error_handlers[code] || halt(code, *args)
58
+ halt(code, __send__(error_handlers[code], *args))
59
+ end
60
+ alias error! error
61
+
62
+ # 404: Not Found
63
+ error 404 do
64
+ RocketIO.error_renderer(404, xhr?, env: env)
65
+ end
66
+
67
+ # 409: Wrong number of arguments received
68
+ error 409 do
69
+ RocketIO.error_renderer(409, xhr?, {
70
+ env: env,
71
+ controller: self.class,
72
+ resolved_path: url,
73
+ expected_parameters: parameters_policy[env[RocketIO::REQUEST_METHOD]],
74
+ received_parameters: path_params
75
+ })
76
+ end
77
+
78
+ # 500: Fatal Error
79
+ error 500 do |error|
80
+ error = StandardError.new(error) unless Exception === error
81
+ RocketIO.error_renderer(500, xhr?, env: env, error: error)
82
+ end
83
+
84
+ # 501: Not Implemented
85
+ error 501 do
86
+ RocketIO.error_renderer(501, xhr?, env: env)
87
+ end
88
+ end
89
+ end
@@ -0,0 +1,119 @@
1
+ module RocketIO
2
+ class Controller
3
+
4
+ # define blocks to run before, around, after called method.
5
+ # if no methods given, given block will run on any called method.
6
+ #
7
+ # @note call it without a block to define a void filter.
8
+ # useful to override inherited filters.
9
+ #
10
+ # @note sub-controllers will inherit all filters from parent controller
11
+ # and can override them selectively, by name
12
+ #
13
+ # @note wildcard filters will run before/around/after any methods,
14
+ # that's it, if defining `before {}` and `before(:get) {}` filters
15
+ # `get` method will run `before` filter then `before(:get)`
16
+ #
17
+ # @example run before any requested method, being it REST or websocket
18
+ # before do
19
+ # # some logic here
20
+ # end
21
+ #
22
+ # @example run only before GET
23
+ # before :get do
24
+ # # some logic here
25
+ # end
26
+ #
27
+ # @example run only around PUT and POST
28
+ # around :put, :post do |app|
29
+ # # some logic here
30
+ # app.call
31
+ # end
32
+ #
33
+ # @example run only after :register websocket call
34
+ # after :register do
35
+ # # some logic here
36
+ # end
37
+ #
38
+ # @example define a filter that does nothing. useful to override inherited filters.
39
+ # before :get
40
+ #
41
+ # @example run 2 blocks before GET
42
+ # before do # wildcard filter, will run before any method
43
+ # @user = User.find...
44
+ # end
45
+ #
46
+ # before :get do # named filter, will run only before `get`
47
+ # # wildcard filter already executed so we have `@user` variable here
48
+ # @photo = @user.photos.find...
49
+ # end
50
+ #
51
+ # # before calling `get` two filters will be executed:
52
+ # # - wildcard one
53
+ # # - named one
54
+ # def get
55
+ # # both @user and @photo variables available here
56
+ # end
57
+ #
58
+ {
59
+ before: proc {},
60
+ around: proc {|app| app.call},
61
+ after: proc {}
62
+ }.each_pair do |filter,default_block|
63
+ define_methods = :"define_#{filter}_methods"
64
+ var = :"@__#{filter}_filters__"
65
+
66
+ define_singleton_method filter do |*methods,&block|
67
+ instance_variable_get(var) || instance_variable_set(var, {})
68
+ methods = [:*] if methods.empty?
69
+ methods.each do |meth|
70
+ instance_variable_get(var)[meth.to_sym] = block || default_block
71
+ end
72
+ __send__(define_methods)
73
+ end
74
+
75
+ define_singleton_method define_methods do |source = self|
76
+ filters = (source.instance_variable_get(var) || {}).each_with_object({}) do |(meth,proc),o|
77
+ o[meth] = :"__#{filter}_#{meth}__"
78
+ define_method(o[meth], &proc)
79
+ end
80
+ filters.update(allocate.__send__(filter))
81
+ return unless filters.any?
82
+ filters.freeze
83
+ define_method(filter) {filters}
84
+ end
85
+ end
86
+
87
+ def before; RocketIO::EMPTY_HASH end
88
+ def around; RocketIO::EMPTY_HASH end
89
+ def after; RocketIO::EMPTY_HASH end
90
+
91
+ def invoke_before_filter method = requested_method
92
+ __send__(before[:*]) if before[:*]
93
+ __send__(before[method]) if before[method]
94
+ end
95
+
96
+ # passing blocks somehow tends to add some overhead
97
+ # so passing the proc as a common argument
98
+ def invoke_around_filter method = requested_method, block
99
+ if around[:*]
100
+ __send__ around[:*], proc {
101
+ if around[method]
102
+ __send__(around[method], block)
103
+ else
104
+ block.call
105
+ end
106
+ }
107
+ elsif around[method]
108
+ __send__(around[method], block)
109
+ else
110
+ block.call
111
+ end
112
+ end
113
+
114
+ def invoke_after_filter method = requested_method
115
+ __send__(after[:*]) if after[:*]
116
+ __send__(after[method]) if after[method]
117
+ end
118
+ end
119
+ end