rage-rb 1.7.0 → 1.8.0
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +4 -4
- data/CHANGELOG.md +6 -0
- data/Gemfile +1 -0
- data/OVERVIEW.md +1 -1
- data/README.md +4 -2
- data/lib/rage/all.rb +1 -0
- data/lib/rage/cable/cable.rb +130 -0
- data/lib/rage/cable/channel.rb +452 -0
- data/lib/rage/cable/connection.rb +78 -0
- data/lib/rage/cable/protocol/actioncable_v1_json.rb +167 -0
- data/lib/rage/cable/router.rb +138 -0
- data/lib/rage/code_loader.rb +9 -0
- data/lib/rage/configuration.rb +53 -0
- data/lib/rage/controller/api.rb +8 -9
- data/lib/rage/cookies.rb +2 -2
- data/lib/rage/middleware/fiber_wrapper.rb +3 -1
- data/lib/rage/middleware/origin_validator.rb +38 -0
- data/lib/rage/router/dsl.rb +1 -1
- data/lib/rage/session.rb +2 -2
- data/lib/rage/version.rb +1 -1
- data/lib/rage-rb.rb +23 -15
- metadata +8 -2
@@ -0,0 +1,78 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
class Rage::Cable::Connection
|
4
|
+
# @private
|
5
|
+
attr_reader :__identified_by_map
|
6
|
+
|
7
|
+
# Mark a key as being a connection identifier index that can then be used to find the specific connection again later.
|
8
|
+
# Common identifiers are `current_user` and `current_account`, but could be anything.
|
9
|
+
#
|
10
|
+
# @param identifiers [Symbol,Array<Symbol>]
|
11
|
+
def self.identified_by(*identifiers)
|
12
|
+
identifiers.each do |method_name|
|
13
|
+
define_method(method_name) do
|
14
|
+
@__identified_by_map[method_name]
|
15
|
+
end
|
16
|
+
|
17
|
+
define_method("#{method_name}=") do |data|
|
18
|
+
@__identified_by_map[method_name] = data
|
19
|
+
end
|
20
|
+
|
21
|
+
Rage::Cable::Channel.__prepare_id_method(method_name)
|
22
|
+
end
|
23
|
+
end
|
24
|
+
|
25
|
+
# @private
|
26
|
+
def initialize(env, identified_by = {})
|
27
|
+
@__env = env
|
28
|
+
@__identified_by_map = identified_by
|
29
|
+
end
|
30
|
+
|
31
|
+
# @private
|
32
|
+
def connect
|
33
|
+
end
|
34
|
+
|
35
|
+
# Reject the WebSocket connection.
|
36
|
+
def reject_unauthorized_connection
|
37
|
+
@rejected = true
|
38
|
+
end
|
39
|
+
|
40
|
+
def rejected?
|
41
|
+
!!@rejected
|
42
|
+
end
|
43
|
+
|
44
|
+
# Get the request object. See {Rage::Request}.
|
45
|
+
#
|
46
|
+
# @return [Rage::Request]
|
47
|
+
def request
|
48
|
+
@__request ||= Rage::Request.new(@__env)
|
49
|
+
end
|
50
|
+
|
51
|
+
# Get the cookie object. See {Rage::Cookies}.
|
52
|
+
#
|
53
|
+
# @return [Rage::Cookies]
|
54
|
+
def cookies
|
55
|
+
@__cookies ||= Rage::Cookies.new(@__env, ReadOnlyHash.new)
|
56
|
+
end
|
57
|
+
|
58
|
+
# Get the session object. See {Rage::Session}.
|
59
|
+
#
|
60
|
+
# @return [Rage::Session]
|
61
|
+
def session
|
62
|
+
@__session ||= Rage::Session.new(cookies)
|
63
|
+
end
|
64
|
+
|
65
|
+
# Get URL query parameters.
|
66
|
+
#
|
67
|
+
# @return [Hash{Symbol=>String,Array,Hash}]
|
68
|
+
def params
|
69
|
+
@__params ||= Iodine::Rack::Utils.parse_nested_query(@__env["QUERY_STRING"])
|
70
|
+
end
|
71
|
+
|
72
|
+
# @private
|
73
|
+
class ReadOnlyHash < Hash
|
74
|
+
def []=(_, _)
|
75
|
+
raise "Cookies cannot be set for WebSocket clients"
|
76
|
+
end
|
77
|
+
end
|
78
|
+
end
|
@@ -0,0 +1,167 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
##
|
4
|
+
# A protocol defines the structure, rules and semantics for exchanging data between the client and the server.
|
5
|
+
# The class that defines a protocol should respond to the following methods:
|
6
|
+
#
|
7
|
+
# * `protocol_definition`
|
8
|
+
# * `init`
|
9
|
+
# * `on_open`
|
10
|
+
# * `on_message`
|
11
|
+
# * `serialize`
|
12
|
+
# * `subscribe`
|
13
|
+
# * `broadcast`
|
14
|
+
#
|
15
|
+
# The two optional methods are:
|
16
|
+
#
|
17
|
+
# * `on_shutdown`
|
18
|
+
# * `on_close`
|
19
|
+
#
|
20
|
+
class Rage::Cable::Protocol::ActioncableV1Json
|
21
|
+
module TYPE
|
22
|
+
WELCOME = "welcome"
|
23
|
+
DISCONNECT = "disconnect"
|
24
|
+
PING = "ping"
|
25
|
+
CONFIRM = "confirm_subscription"
|
26
|
+
REJECT = "reject_subscription"
|
27
|
+
end
|
28
|
+
|
29
|
+
module REASON
|
30
|
+
UNAUTHORIZED = "unauthorized"
|
31
|
+
INVALID = "invalid_request"
|
32
|
+
end
|
33
|
+
|
34
|
+
module COMMAND
|
35
|
+
SUBSCRIBE = "subscribe"
|
36
|
+
MESSAGE = "message"
|
37
|
+
end
|
38
|
+
|
39
|
+
module MESSAGES
|
40
|
+
WELCOME = { type: TYPE::WELCOME }.to_json
|
41
|
+
UNAUTHORIZED = { type: TYPE::DISCONNECT, reason: REASON::UNAUTHORIZED, reconnect: false }.to_json
|
42
|
+
INVALID = { type: TYPE::DISCONNECT, reason: REASON::INVALID, reconnect: true }.to_json
|
43
|
+
end
|
44
|
+
|
45
|
+
HANDSHAKE_HEADERS = { "Sec-WebSocket-Protocol" => "actioncable-v1-json" }
|
46
|
+
|
47
|
+
# The method defines the headers to send to the client after the handshake process.
|
48
|
+
def self.protocol_definition
|
49
|
+
HANDSHAKE_HEADERS
|
50
|
+
end
|
51
|
+
|
52
|
+
# This method serves as a constructor to prepare the object or set up recurring tasks (e.g. heartbeats).
|
53
|
+
#
|
54
|
+
# @param router [Rage::Cable::Router]
|
55
|
+
def self.init(router)
|
56
|
+
@router = router
|
57
|
+
|
58
|
+
ping_counter = Time.now.to_i
|
59
|
+
::Iodine.run_every(3000) do
|
60
|
+
ping_counter += 1
|
61
|
+
::Iodine.publish("cable:ping", { type: TYPE::PING, message: ping_counter }.to_json)
|
62
|
+
end
|
63
|
+
|
64
|
+
# Hash<String(stream name) => Array<Hash>(subscription params)>
|
65
|
+
@subscription_identifiers = Hash.new { |hash, key| hash[key] = [] }
|
66
|
+
end
|
67
|
+
|
68
|
+
# The method is called any time a new WebSocket connection is established.
|
69
|
+
# It is expected to call {Rage::Cable::Router#process_connection} and handle its return value.
|
70
|
+
#
|
71
|
+
# @param connection [Rage::Cable::WebSocketConnection] the connection object
|
72
|
+
# @see Rage::Cable::Router
|
73
|
+
def self.on_open(connection)
|
74
|
+
accepted = @router.process_connection(connection)
|
75
|
+
|
76
|
+
if accepted
|
77
|
+
connection.subscribe("cable:ping")
|
78
|
+
connection.write(MESSAGES::WELCOME)
|
79
|
+
else
|
80
|
+
connection.write(MESSAGES::UNAUTHORIZED)
|
81
|
+
connection.close
|
82
|
+
end
|
83
|
+
end
|
84
|
+
|
85
|
+
# The method processes messages from existing connections. It should parse the message, call either
|
86
|
+
# {Rage::Cable::Router#process_subscription} or {Rage::Cable::Router#process_message}, and handle its return value.
|
87
|
+
#
|
88
|
+
# @param connection [Rage::Cable::WebSocketConnection] the connection object
|
89
|
+
# @param raw_data [String] the message body
|
90
|
+
# @see Rage::Cable::Router
|
91
|
+
def self.on_message(connection, raw_data)
|
92
|
+
parsed_data = Rage::ParamsParser.json_parse(raw_data)
|
93
|
+
|
94
|
+
command, identifier = parsed_data[:command], parsed_data[:identifier]
|
95
|
+
params = Rage::ParamsParser.json_parse(identifier)
|
96
|
+
|
97
|
+
# process subscription messages
|
98
|
+
if command == COMMAND::SUBSCRIBE
|
99
|
+
status = @router.process_subscription(connection, identifier, params[:channel], params)
|
100
|
+
if status == :subscribed
|
101
|
+
connection.write({ identifier: identifier, type: TYPE::CONFIRM }.to_json)
|
102
|
+
elsif status == :rejected
|
103
|
+
connection.write({ identifier: identifier, type: TYPE::REJECT }.to_json)
|
104
|
+
elsif status == :invalid
|
105
|
+
connection.write(MESSAGES::INVALID)
|
106
|
+
end
|
107
|
+
|
108
|
+
return
|
109
|
+
end
|
110
|
+
|
111
|
+
# process data messages;
|
112
|
+
# plain `JSON` is used here to conform with the ActionCable API that passes `data` as a Hash with string keys;
|
113
|
+
data = JSON.parse(parsed_data[:data])
|
114
|
+
|
115
|
+
message_status = if command == COMMAND::MESSAGE && data.has_key?("action")
|
116
|
+
@router.process_message(connection, identifier, data["action"].to_sym, data)
|
117
|
+
|
118
|
+
elsif command == COMMAND::MESSAGE
|
119
|
+
@router.process_message(connection, identifier, :receive, data)
|
120
|
+
end
|
121
|
+
|
122
|
+
unless message_status == :processed
|
123
|
+
connection.write(MESSAGES::INVALID)
|
124
|
+
end
|
125
|
+
end
|
126
|
+
|
127
|
+
# The method should process client disconnections and call {Rage::Cable::Router#process_message}.
|
128
|
+
#
|
129
|
+
# @note This method is optional.
|
130
|
+
# @param connection [Rage::Cable::WebSocketConnection] the connection object
|
131
|
+
# @see Rage::Cable::Router
|
132
|
+
def self.on_close(connection)
|
133
|
+
@router.process_disconnection(connection)
|
134
|
+
end
|
135
|
+
|
136
|
+
# Serialize a Ruby object into the format the client would understand.
|
137
|
+
#
|
138
|
+
# @param params [Hash] parameters associated with the client
|
139
|
+
# @param data [Object] the object to serialize
|
140
|
+
def self.serialize(params, data)
|
141
|
+
{ identifier: params.to_json, message: data }.to_json
|
142
|
+
end
|
143
|
+
|
144
|
+
# Subscribe to a stream.
|
145
|
+
#
|
146
|
+
# @param connection [Rage::Cable::WebSocketConnection] the connection object
|
147
|
+
# @param name [String] the stream name
|
148
|
+
# @param params [Hash] parameters associated with the client
|
149
|
+
def self.subscribe(connection, name, params)
|
150
|
+
connection.subscribe("cable:#{name}:#{params.hash}")
|
151
|
+
@subscription_identifiers[name] << params unless @subscription_identifiers[name].include?(params)
|
152
|
+
end
|
153
|
+
|
154
|
+
# Broadcast data to all clients connected to a stream.
|
155
|
+
#
|
156
|
+
# @param name [String] the stream name
|
157
|
+
# @param data [Object] the data to send
|
158
|
+
def self.broadcast(name, data)
|
159
|
+
i, identifiers = 0, @subscription_identifiers[name]
|
160
|
+
|
161
|
+
while i < identifiers.length
|
162
|
+
params = identifiers[i]
|
163
|
+
::Iodine.publish("cable:#{name}:#{params.hash}", serialize(params, data))
|
164
|
+
i += 1
|
165
|
+
end
|
166
|
+
end
|
167
|
+
end
|
@@ -0,0 +1,138 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
class Rage::Cable::Router
|
4
|
+
# @private
|
5
|
+
def initialize
|
6
|
+
# Hash<String(channel name) => Proc(new channel instance)>
|
7
|
+
@channels_map = {}
|
8
|
+
init_connection_class
|
9
|
+
end
|
10
|
+
|
11
|
+
# Calls the `connect` method on the `Connection` class to handle authentication.
|
12
|
+
#
|
13
|
+
# @param connection [Rage::Cable::WebSocketConnection] the connection object
|
14
|
+
# @return [true] if the connection was accepted
|
15
|
+
# @return [false] if the connection was rejected
|
16
|
+
def process_connection(connection)
|
17
|
+
cable_connection = @connection_class.new(connection.env)
|
18
|
+
cable_connection.connect
|
19
|
+
|
20
|
+
if cable_connection.rejected?
|
21
|
+
Rage.logger.debug { "An unauthorized connection attempt was rejected" }
|
22
|
+
else
|
23
|
+
connection.env["rage.identified_by"] = cable_connection.__identified_by_map
|
24
|
+
connection.env["rage.cable"] = {}
|
25
|
+
end
|
26
|
+
|
27
|
+
!cable_connection.rejected?
|
28
|
+
end
|
29
|
+
|
30
|
+
# Calls the `subscribed` method on the specified channel.
|
31
|
+
#
|
32
|
+
# @param connection [Rage::Cable::WebSocketConnection] the connection object
|
33
|
+
# @param identifier [String] the identifier of the subscription
|
34
|
+
# @param channel_name [String] the name of the channel class
|
35
|
+
# @param params [Hash] the params hash associated with the subscription
|
36
|
+
#
|
37
|
+
# @return [:invalid] if the subscription class does not exist
|
38
|
+
# @return [:rejected] if the subscription was rejected
|
39
|
+
# @return [:subscribed] if the subscription was accepted
|
40
|
+
def process_subscription(connection, identifier, channel_name, params)
|
41
|
+
channel_class = @channels_map[channel_name] || begin
|
42
|
+
begin
|
43
|
+
klass = Object.const_get(channel_name)
|
44
|
+
rescue NameError
|
45
|
+
nil
|
46
|
+
end
|
47
|
+
|
48
|
+
if klass.nil? || !klass.ancestors.include?(Rage::Cable::Channel)
|
49
|
+
Rage.logger.debug { "Subscription class not found: #{channel_name}" }
|
50
|
+
return :invalid
|
51
|
+
end
|
52
|
+
|
53
|
+
klass.__register_actions.tap do |available_actions|
|
54
|
+
Rage.logger.debug { "Compiled #{channel_name}. Available remote actions: #{available_actions}." }
|
55
|
+
end
|
56
|
+
|
57
|
+
@channels_map[channel_name] = klass
|
58
|
+
end
|
59
|
+
|
60
|
+
channel = channel_class.new(connection, params, connection.env["rage.identified_by"])
|
61
|
+
channel.__run_action(:subscribed)
|
62
|
+
|
63
|
+
if channel.subscription_rejected?
|
64
|
+
Rage.logger.debug { "#{channel_name} is transmitting the subscription rejection" }
|
65
|
+
# if the subscription is rejected in the `subscribed` method, ActionCable will additionally run
|
66
|
+
# the `unsubscribed` method; this makes little sense to me as the client was never subscribed in
|
67
|
+
# the first place; additionally, I don't think this behaviour is documented anywhere;
|
68
|
+
# so, I'm going to leave this line commented out for now;
|
69
|
+
# channel.__run_action(:unsubscribed)
|
70
|
+
:rejected
|
71
|
+
else
|
72
|
+
Rage.logger.debug { "#{channel_name} is transmitting the subscription confirmation" }
|
73
|
+
connection.env["rage.cable"][identifier] = channel
|
74
|
+
:subscribed
|
75
|
+
end
|
76
|
+
end
|
77
|
+
|
78
|
+
# Calls the handler method on the specified channel.
|
79
|
+
#
|
80
|
+
# @param connection [Rage::Cable::WebSocketConnection] the connection object
|
81
|
+
# @param identifier [String] the identifier of the subscription
|
82
|
+
# @param action_name [Symbol] the name of the handler method
|
83
|
+
# @param data [Object] the data sent by the client
|
84
|
+
#
|
85
|
+
# @return [:no_subscription] if the client is not subscribed to the specified channel
|
86
|
+
# @return [:unknown_action] if the action does not exist on the specified channel
|
87
|
+
# @return [:processed] if the message has been successfully processed
|
88
|
+
def process_message(connection, identifier, action_name, data)
|
89
|
+
channel = connection.env["rage.cable"][identifier]
|
90
|
+
unless channel
|
91
|
+
Rage.logger.debug { "Unable to find the subscription" }
|
92
|
+
return :no_subscription
|
93
|
+
end
|
94
|
+
|
95
|
+
if channel.__has_action?(action_name)
|
96
|
+
channel.__run_action(action_name, data)
|
97
|
+
:processed
|
98
|
+
else
|
99
|
+
Rage.logger.debug { "Unable to process #{channel.class.name}##{action_name}" }
|
100
|
+
:unknown_action
|
101
|
+
end
|
102
|
+
end
|
103
|
+
|
104
|
+
# Runs the `unsubscribed` methods on all the channels the client is subscribed to.
|
105
|
+
#
|
106
|
+
# @param connection [Rage::Cable::WebSocketConnection] the connection object
|
107
|
+
def process_disconnection(connection)
|
108
|
+
connection.env["rage.cable"]&.each do |_, channel|
|
109
|
+
channel.__run_action(:unsubscribed)
|
110
|
+
end
|
111
|
+
|
112
|
+
if @connection_can_disconnect
|
113
|
+
cable_connection = @connection_class.new(connection.env, connection.env["rage.identified_by"])
|
114
|
+
cable_connection.disconnect
|
115
|
+
end
|
116
|
+
end
|
117
|
+
|
118
|
+
# @private
|
119
|
+
def reset
|
120
|
+
@channels_map.clear
|
121
|
+
init_connection_class
|
122
|
+
end
|
123
|
+
|
124
|
+
private
|
125
|
+
|
126
|
+
def init_connection_class
|
127
|
+
@connection_class = if Object.const_defined?("RageCable::Connection")
|
128
|
+
RageCable::Connection
|
129
|
+
elsif Object.const_defined?("ApplicationCable::Connection")
|
130
|
+
ApplicationCable::Connection
|
131
|
+
else
|
132
|
+
puts "WARNING: Could not find the RageCable connection class! All connections will be accepted by default."
|
133
|
+
Rage::Cable::Connection
|
134
|
+
end
|
135
|
+
|
136
|
+
@connection_can_disconnect = @connection_class.method_defined?(:disconnect)
|
137
|
+
end
|
138
|
+
end
|
data/lib/rage/code_loader.rb
CHANGED
@@ -30,8 +30,13 @@ class Rage::CodeLoader
|
|
30
30
|
|
31
31
|
@reloading = true
|
32
32
|
@loader.reload
|
33
|
+
|
33
34
|
Rage.__router.reset_routes
|
34
35
|
load("#{Rage.root}/config/routes.rb")
|
36
|
+
|
37
|
+
unless Rage.autoload?(:Cable) # the `Cable` component is loaded
|
38
|
+
Rage::Cable.__router.reset
|
39
|
+
end
|
35
40
|
end
|
36
41
|
|
37
42
|
# in Rails mode - reset the routes; everything else will be done by Rails
|
@@ -40,6 +45,10 @@ class Rage::CodeLoader
|
|
40
45
|
|
41
46
|
@reloading = true
|
42
47
|
Rage.__router.reset_routes
|
48
|
+
|
49
|
+
unless Rage.autoload?(:Cable) # the `Cable` component is loaded
|
50
|
+
Rage::Cable.__router.reset
|
51
|
+
end
|
43
52
|
end
|
44
53
|
|
45
54
|
def reloading?
|
data/lib/rage/configuration.rb
CHANGED
@@ -1,6 +1,15 @@
|
|
1
1
|
# frozen_string_literal: true
|
2
2
|
|
3
3
|
##
|
4
|
+
# `Rage.configure` can be used to adjust the behavior of your Rage application:
|
5
|
+
#
|
6
|
+
# ```ruby
|
7
|
+
# Rage.configure do
|
8
|
+
# config.logger = Rage::Logger.new(STDOUT)
|
9
|
+
# config.server.workers_count = 2
|
10
|
+
# end
|
11
|
+
# ```
|
12
|
+
#
|
4
13
|
# # General Configuration
|
5
14
|
#
|
6
15
|
# • _config.logger_
|
@@ -93,6 +102,20 @@
|
|
93
102
|
#
|
94
103
|
# > Specifies connection timeout.
|
95
104
|
#
|
105
|
+
# # Cable Configuration
|
106
|
+
#
|
107
|
+
# • _config.cable.protocol_
|
108
|
+
#
|
109
|
+
# > Specifies the protocol the server will use. The only value currently supported is `Rage::Cable::Protocol::ActioncableV1Json`. The client application will need to use [@rails/actioncable](https://www.npmjs.com/package/@rails/actioncable) to talk to the server.
|
110
|
+
#
|
111
|
+
# • _config.cable.allowed_request_origins_
|
112
|
+
#
|
113
|
+
# > Restricts the server to only accept requests from specified origins. The origins can be instances of strings or regular expressions, against which a check for the match will be performed.
|
114
|
+
#
|
115
|
+
# • _config.cable.disable_request_forgery_protection_
|
116
|
+
#
|
117
|
+
# > Allows requests from any origin.
|
118
|
+
#
|
96
119
|
# # Transient Settings
|
97
120
|
#
|
98
121
|
# The settings described in this section should be configured using **environment variables** and are either temporary or will become the default in the future.
|
@@ -138,6 +161,10 @@ class Rage::Configuration
|
|
138
161
|
@middleware ||= Middleware.new
|
139
162
|
end
|
140
163
|
|
164
|
+
def cable
|
165
|
+
@cable ||= Cable.new
|
166
|
+
end
|
167
|
+
|
141
168
|
def internal
|
142
169
|
@internal ||= Internal.new
|
143
170
|
end
|
@@ -193,6 +220,32 @@ class Rage::Configuration
|
|
193
220
|
end
|
194
221
|
end
|
195
222
|
|
223
|
+
class Cable
|
224
|
+
attr_accessor :protocol, :allowed_request_origins, :disable_request_forgery_protection
|
225
|
+
|
226
|
+
def initialize
|
227
|
+
@protocol = Rage::Cable::Protocol::ActioncableV1Json
|
228
|
+
@allowed_request_origins = if Rage.env.development? || Rage.env.test?
|
229
|
+
/localhost/
|
230
|
+
end
|
231
|
+
end
|
232
|
+
|
233
|
+
# @private
|
234
|
+
def middlewares
|
235
|
+
@middlewares ||= begin
|
236
|
+
origin_middleware = if @disable_request_forgery_protection
|
237
|
+
[]
|
238
|
+
else
|
239
|
+
[[Rage::OriginValidator, Array(@allowed_request_origins), nil]]
|
240
|
+
end
|
241
|
+
|
242
|
+
origin_middleware + Rage.config.middleware.middlewares.reject do |middleware, _, _|
|
243
|
+
middleware == Rage::FiberWrapper
|
244
|
+
end
|
245
|
+
end
|
246
|
+
end
|
247
|
+
end
|
248
|
+
|
196
249
|
# @private
|
197
250
|
class Internal
|
198
251
|
attr_accessor :rails_mode
|
data/lib/rage/controller/api.rb
CHANGED
@@ -142,6 +142,7 @@ class RageController::API
|
|
142
142
|
|
143
143
|
# @private
|
144
144
|
attr_writer :__before_actions, :__after_actions, :__rescue_handlers
|
145
|
+
# @private
|
145
146
|
attr_accessor :__wrap_parameters_key, :__wrap_parameters_options
|
146
147
|
|
147
148
|
# @private
|
@@ -299,14 +300,12 @@ class RageController::API
|
|
299
300
|
@__before_actions[i] = action
|
300
301
|
end
|
301
302
|
|
302
|
-
#
|
303
|
-
#
|
304
|
-
# Params get wrapped only if the CONTENT_TYPE header is present and params hash doesn't contain a param that
|
305
|
-
# has the same name as the wrapper key.
|
303
|
+
# Wraps the parameters hash into a nested hash. This will allow clients to submit requests without having to specify any root elements.
|
304
|
+
# Params get wrapped only if the `Content-Type` header is present and the `params` hash doesn't contain a param with the same name as the wrapper key.
|
306
305
|
#
|
307
|
-
# @param key [Symbol]
|
308
|
-
# @param include [Array]
|
309
|
-
# @param exclude [Array]
|
306
|
+
# @param key [Symbol] the wrapper key
|
307
|
+
# @param include [Symbol, Array<Symbol>] the list of attribute names which parameters wrapper will wrap into a nested hash
|
308
|
+
# @param exclude [Symbol, Array<Symbol>] the list of attribute names which parameters wrapper will exclude from a nested hash
|
310
309
|
# @example
|
311
310
|
# wrap_parameters :user, include: %i[name age]
|
312
311
|
# @example
|
@@ -366,13 +365,13 @@ class RageController::API
|
|
366
365
|
# Get the cookie object. See {Rage::Cookies}.
|
367
366
|
# @return [Rage::Cookies]
|
368
367
|
def cookies
|
369
|
-
@cookies ||= Rage::Cookies.new(@__env,
|
368
|
+
@cookies ||= Rage::Cookies.new(@__env, @__headers)
|
370
369
|
end
|
371
370
|
|
372
371
|
# Get the session object. See {Rage::Session}.
|
373
372
|
# @return [Rage::Session]
|
374
373
|
def session
|
375
|
-
@session ||= Rage::Session.new(
|
374
|
+
@session ||= Rage::Session.new(cookies)
|
376
375
|
end
|
377
376
|
|
378
377
|
# Send a response to the client.
|
data/lib/rage/cookies.rb
CHANGED
@@ -0,0 +1,38 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
class Rage::OriginValidator
|
4
|
+
def initialize(app, *allowed_origins)
|
5
|
+
@app = app
|
6
|
+
@validator = build_validator(allowed_origins)
|
7
|
+
end
|
8
|
+
|
9
|
+
def call(env)
|
10
|
+
if @validator.call(env)
|
11
|
+
@app.call(env)
|
12
|
+
else
|
13
|
+
Rage.logger.error("Request origin not allowed: #{env["HTTP_ORIGIN"]}")
|
14
|
+
[404, {}, ["Not Found"]]
|
15
|
+
end
|
16
|
+
end
|
17
|
+
|
18
|
+
private
|
19
|
+
|
20
|
+
def build_validator(allowed_origins)
|
21
|
+
if allowed_origins.empty?
|
22
|
+
->(env) { false }
|
23
|
+
else
|
24
|
+
origins_eval = allowed_origins.map { |origin|
|
25
|
+
origin.is_a?(Regexp) ?
|
26
|
+
"origin =~ /#{origin.source}/.freeze" :
|
27
|
+
"origin == '#{origin}'.freeze"
|
28
|
+
}.join(" || ")
|
29
|
+
|
30
|
+
eval <<-RUBY
|
31
|
+
->(env) do
|
32
|
+
origin = env["HTTP_ORIGIN".freeze]
|
33
|
+
#{origins_eval}
|
34
|
+
end
|
35
|
+
RUBY
|
36
|
+
end
|
37
|
+
end
|
38
|
+
end
|
data/lib/rage/router/dsl.rb
CHANGED
@@ -17,7 +17,7 @@ class Rage::Router::DSL
|
|
17
17
|
end
|
18
18
|
|
19
19
|
##
|
20
|
-
# This class implements routing logic for your application, providing API similar to Rails.
|
20
|
+
# This class implements routing logic for your application, providing an API similar to Rails.
|
21
21
|
#
|
22
22
|
# Compared to the Rails router, the most notable difference is that a wildcard segment can only be in the last section of the path and cannot be named.
|
23
23
|
# Example:
|
data/lib/rage/session.rb
CHANGED
data/lib/rage/version.rb
CHANGED
data/lib/rage-rb.rb
CHANGED
@@ -7,27 +7,17 @@ require "pathname"
|
|
7
7
|
|
8
8
|
module Rage
|
9
9
|
def self.application
|
10
|
-
|
11
|
-
|
12
|
-
config.middleware.middlewares.reverse.inject(app) do |next_in_chain, (middleware, args, block)|
|
13
|
-
# in Rails compatibility mode we first check if the middleware is a part of the Rails middleware stack;
|
14
|
-
# if it is - it is expected to be built using `ActionDispatch::MiddlewareStack::Middleware#build`
|
15
|
-
if Rage.config.internal.rails_mode
|
16
|
-
rails_middleware = Rails.application.config.middleware.middlewares.find { |m| m.name == middleware.name }
|
17
|
-
end
|
18
|
-
|
19
|
-
if rails_middleware
|
20
|
-
rails_middleware.build(next_in_chain)
|
21
|
-
else
|
22
|
-
middleware.new(next_in_chain, *args, &block)
|
23
|
-
end
|
24
|
-
end
|
10
|
+
with_middlewares(Application.new(__router), config.middleware.middlewares)
|
25
11
|
end
|
26
12
|
|
27
13
|
def self.multi_application
|
28
14
|
Rage::Router::Util::Cascade.new(application, Rails.application)
|
29
15
|
end
|
30
16
|
|
17
|
+
def self.cable
|
18
|
+
Rage::Cable
|
19
|
+
end
|
20
|
+
|
31
21
|
def self.routes
|
32
22
|
Rage::Router::DSL.new(__router)
|
33
23
|
end
|
@@ -90,6 +80,23 @@ module Rage
|
|
90
80
|
end
|
91
81
|
end
|
92
82
|
|
83
|
+
# @private
|
84
|
+
def self.with_middlewares(app, middlewares)
|
85
|
+
middlewares.reverse.inject(app) do |next_in_chain, (middleware, args, block)|
|
86
|
+
# in Rails compatibility mode we first check if the middleware is a part of the Rails middleware stack;
|
87
|
+
# if it is - it is expected to be built using `ActionDispatch::MiddlewareStack::Middleware#build`
|
88
|
+
if Rage.config.internal.rails_mode
|
89
|
+
rails_middleware = Rails.application.config.middleware.middlewares.find { |m| m.name == middleware.name }
|
90
|
+
end
|
91
|
+
|
92
|
+
if rails_middleware
|
93
|
+
rails_middleware.build(next_in_chain)
|
94
|
+
else
|
95
|
+
middleware.new(next_in_chain, *args, &block)
|
96
|
+
end
|
97
|
+
end
|
98
|
+
end
|
99
|
+
|
93
100
|
module Router
|
94
101
|
module Strategies
|
95
102
|
end
|
@@ -106,6 +113,7 @@ module Rage
|
|
106
113
|
|
107
114
|
autoload :Cookies, "rage/cookies"
|
108
115
|
autoload :Session, "rage/session"
|
116
|
+
autoload :Cable, "rage/cable/cable"
|
109
117
|
end
|
110
118
|
|
111
119
|
module RageController
|