rage-rb 1.7.0 → 1.9.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 +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
|