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,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