rage-rb 1.7.0 → 1.9.0
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.
- checksums.yaml +4 -4
- data/CHANGELOG.md +17 -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/cli.rb +12 -5
- data/lib/rage/code_loader.rb +9 -0
- data/lib/rage/configuration.rb +67 -0
- data/lib/rage/controller/api.rb +8 -9
- data/lib/rage/cookies.rb +2 -2
- data/lib/rage/ext/active_record/connection_pool.rb +11 -3
- data/lib/rage/ext/setup.rb +1 -1
- data/lib/rage/middleware/fiber_wrapper.rb +3 -1
- data/lib/rage/middleware/origin_validator.rb +38 -0
- data/lib/rage/rails.rb +4 -1
- data/lib/rage/request.rb +39 -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/cli.rb
CHANGED
@@ -29,12 +29,15 @@ module Rage
|
|
29
29
|
app = ::Rack::Builder.parse_file(options[:config] || "config.ru")
|
30
30
|
app = app[0] if app.is_a?(Array)
|
31
31
|
|
32
|
-
|
33
|
-
address = options[:binding] || (Rage.env.production? ? "0.0.0.0" : "localhost")
|
34
|
-
timeout = Rage.config.server.timeout
|
35
|
-
max_clients = Rage.config.server.max_clients
|
32
|
+
server_options = { service: :http, handler: app }
|
36
33
|
|
37
|
-
|
34
|
+
server_options[:port] = options[:port] || Rage.config.server.port
|
35
|
+
server_options[:address] = options[:binding] || (Rage.env.production? ? "0.0.0.0" : "localhost")
|
36
|
+
server_options[:timeout] = Rage.config.server.timeout
|
37
|
+
server_options[:max_clients] = Rage.config.server.max_clients
|
38
|
+
server_options[:public] = Rage.config.public_file_server.enabled ? Rage.root.join("public").to_s : nil
|
39
|
+
|
40
|
+
::Iodine.listen(**server_options)
|
38
41
|
::Iodine.threads = Rage.config.server.threads_count
|
39
42
|
::Iodine.workers = Rage.config.server.workers_count
|
40
43
|
|
@@ -124,6 +127,10 @@ module Rage
|
|
124
127
|
|
125
128
|
def set_env(options)
|
126
129
|
ENV["RAGE_ENV"] = options[:environment] if options[:environment]
|
130
|
+
|
131
|
+
# at this point we don't know whether the app is running in standalone or Rails mode;
|
132
|
+
# we set both variables to make sure applications are running in the same environment;
|
133
|
+
ENV["RAILS_ENV"] = ENV["RAGE_ENV"] if ENV["RAGE_ENV"] && ENV["RAILS_ENV"] != ENV["RAGE_ENV"]
|
127
134
|
end
|
128
135
|
end
|
129
136
|
|
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,26 @@
|
|
93
102
|
#
|
94
103
|
# > Specifies connection timeout.
|
95
104
|
#
|
105
|
+
# # Static file server
|
106
|
+
#
|
107
|
+
# • _config.public_file_server.enabled_
|
108
|
+
#
|
109
|
+
# > Configures whether Rage should serve static files from the public directory. Defaults to `false`.
|
110
|
+
#
|
111
|
+
# # Cable Configuration
|
112
|
+
#
|
113
|
+
# • _config.cable.protocol_
|
114
|
+
#
|
115
|
+
# > 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.
|
116
|
+
#
|
117
|
+
# • _config.cable.allowed_request_origins_
|
118
|
+
#
|
119
|
+
# > 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.
|
120
|
+
#
|
121
|
+
# • _config.cable.disable_request_forgery_protection_
|
122
|
+
#
|
123
|
+
# > Allows requests from any origin.
|
124
|
+
#
|
96
125
|
# # Transient Settings
|
97
126
|
#
|
98
127
|
# 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 +167,14 @@ class Rage::Configuration
|
|
138
167
|
@middleware ||= Middleware.new
|
139
168
|
end
|
140
169
|
|
170
|
+
def cable
|
171
|
+
@cable ||= Cable.new
|
172
|
+
end
|
173
|
+
|
174
|
+
def public_file_server
|
175
|
+
@public_file_server ||= PublicFileServer.new
|
176
|
+
end
|
177
|
+
|
141
178
|
def internal
|
142
179
|
@internal ||= Internal.new
|
143
180
|
end
|
@@ -193,6 +230,36 @@ class Rage::Configuration
|
|
193
230
|
end
|
194
231
|
end
|
195
232
|
|
233
|
+
class Cable
|
234
|
+
attr_accessor :protocol, :allowed_request_origins, :disable_request_forgery_protection
|
235
|
+
|
236
|
+
def initialize
|
237
|
+
@protocol = Rage::Cable::Protocol::ActioncableV1Json
|
238
|
+
@allowed_request_origins = if Rage.env.development? || Rage.env.test?
|
239
|
+
/localhost/
|
240
|
+
end
|
241
|
+
end
|
242
|
+
|
243
|
+
# @private
|
244
|
+
def middlewares
|
245
|
+
@middlewares ||= begin
|
246
|
+
origin_middleware = if @disable_request_forgery_protection
|
247
|
+
[]
|
248
|
+
else
|
249
|
+
[[Rage::OriginValidator, Array(@allowed_request_origins), nil]]
|
250
|
+
end
|
251
|
+
|
252
|
+
origin_middleware + Rage.config.middleware.middlewares.reject do |middleware, _, _|
|
253
|
+
middleware == Rage::FiberWrapper
|
254
|
+
end
|
255
|
+
end
|
256
|
+
end
|
257
|
+
end
|
258
|
+
|
259
|
+
class PublicFileServer
|
260
|
+
attr_accessor :enabled
|
261
|
+
end
|
262
|
+
|
196
263
|
# @private
|
197
264
|
class Internal
|
198
265
|
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
@@ -148,10 +148,14 @@ module Rage::Ext::ActiveRecord::ConnectionPool
|
|
148
148
|
end
|
149
149
|
|
150
150
|
# Yields a connection from the connection pool to the block.
|
151
|
-
def with_connection
|
152
|
-
|
151
|
+
def with_connection(_ = nil)
|
152
|
+
unless (conn = @__in_use[Fiber.current])
|
153
|
+
conn = connection
|
154
|
+
fresh_connection = true
|
155
|
+
end
|
156
|
+
yield conn
|
153
157
|
ensure
|
154
|
-
release_connection
|
158
|
+
release_connection if fresh_connection
|
155
159
|
end
|
156
160
|
|
157
161
|
# Returns an array containing the connections currently in the pool.
|
@@ -230,6 +234,10 @@ module Rage::Ext::ActiveRecord::ConnectionPool
|
|
230
234
|
connection
|
231
235
|
end
|
232
236
|
|
237
|
+
def lease_connection
|
238
|
+
connection
|
239
|
+
end
|
240
|
+
|
233
241
|
# Check in a database connection back into the pool, indicating that you no longer need this connection.
|
234
242
|
def checkin(conn)
|
235
243
|
fiber = @__in_use.key(conn)
|
data/lib/rage/ext/setup.rb
CHANGED
@@ -31,6 +31,6 @@ if defined?(ActiveRecord::ConnectionAdapters::ConnectionPool)
|
|
31
31
|
end
|
32
32
|
|
33
33
|
# patch `ActiveRecord::ConnectionPool`
|
34
|
-
if defined?(ActiveRecord) && ENV["RAGE_PATCH_AR_POOL"]
|
34
|
+
if defined?(ActiveRecord) && ENV["RAGE_PATCH_AR_POOL"] && !Rage.env.test?
|
35
35
|
Rage.patch_active_record_connection_pool
|
36
36
|
end
|
@@ -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/rails.rb
CHANGED
@@ -44,7 +44,10 @@ end
|
|
44
44
|
# clone Rails logger
|
45
45
|
Rails.configuration.after_initialize do
|
46
46
|
if Rails.logger && !Rage.logger
|
47
|
-
rails_logdev = Rails.logger.
|
47
|
+
rails_logdev = Rails.logger.yield_self { |logger|
|
48
|
+
logger.respond_to?(:broadcasts) ? logger.broadcasts.last : logger
|
49
|
+
}.instance_variable_get(:@logdev)
|
50
|
+
|
48
51
|
Rage.configure do
|
49
52
|
config.logger = Rage::Logger.new(rails_logdev.dev) if rails_logdev.is_a?(Logger::LogDevice)
|
50
53
|
end
|