anycable-core 1.1.0.pre1
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +7 -0
- data/CHANGELOG.md +84 -0
- data/MIT-LICENSE +20 -0
- data/README.md +78 -0
- data/bin/anycable +13 -0
- data/bin/anycabled +30 -0
- data/bin/console +7 -0
- data/bin/setup +6 -0
- data/lib/anycable.rb +114 -0
- data/lib/anycable/broadcast_adapters.rb +34 -0
- data/lib/anycable/broadcast_adapters/base.rb +29 -0
- data/lib/anycable/broadcast_adapters/http.rb +131 -0
- data/lib/anycable/broadcast_adapters/redis.rb +46 -0
- data/lib/anycable/cli.rb +319 -0
- data/lib/anycable/config.rb +127 -0
- data/lib/anycable/exceptions_handling.rb +35 -0
- data/lib/anycable/grpc.rb +30 -0
- data/lib/anycable/grpc/check_version.rb +33 -0
- data/lib/anycable/grpc/config.rb +53 -0
- data/lib/anycable/grpc/handler.rb +25 -0
- data/lib/anycable/grpc/rpc_services_pb.rb +24 -0
- data/lib/anycable/grpc/server.rb +103 -0
- data/lib/anycable/health_server.rb +73 -0
- data/lib/anycable/middleware.rb +10 -0
- data/lib/anycable/middleware_chain.rb +74 -0
- data/lib/anycable/middlewares/exceptions.rb +35 -0
- data/lib/anycable/protos/rpc_pb.rb +74 -0
- data/lib/anycable/rpc.rb +91 -0
- data/lib/anycable/rpc/handler.rb +50 -0
- data/lib/anycable/rpc/handlers/command.rb +36 -0
- data/lib/anycable/rpc/handlers/connect.rb +33 -0
- data/lib/anycable/rpc/handlers/disconnect.rb +27 -0
- data/lib/anycable/rspec.rb +4 -0
- data/lib/anycable/rspec/rpc_command_context.rb +21 -0
- data/lib/anycable/socket.rb +169 -0
- data/lib/anycable/version.rb +5 -0
- data/sig/anycable.rbs +37 -0
- data/sig/anycable/broadcast_adapters.rbs +5 -0
- data/sig/anycable/cli.rbs +40 -0
- data/sig/anycable/config.rbs +46 -0
- data/sig/anycable/exceptions_handling.rbs +14 -0
- data/sig/anycable/health_server.rbs +21 -0
- data/sig/anycable/middleware.rbs +5 -0
- data/sig/anycable/middleware_chain.rbs +22 -0
- data/sig/anycable/rpc.rbs +143 -0
- data/sig/anycable/socket.rbs +40 -0
- data/sig/anycable/version.rbs +3 -0
- metadata +237 -0
@@ -0,0 +1,35 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module AnyCable
|
4
|
+
module Middlewares
|
5
|
+
class Exceptions < AnyCable::Middleware
|
6
|
+
def call(method_name, request)
|
7
|
+
yield
|
8
|
+
rescue => exp
|
9
|
+
notify_exception(exp, method_name, request)
|
10
|
+
|
11
|
+
response_class(method_name).new(
|
12
|
+
status: AnyCable::Status::ERROR,
|
13
|
+
error_msg: exp.message
|
14
|
+
)
|
15
|
+
end
|
16
|
+
|
17
|
+
private
|
18
|
+
|
19
|
+
def notify_exception(exp, method_name, message)
|
20
|
+
AnyCable::ExceptionsHandling.notify(exp, method_name.to_s, message.to_h)
|
21
|
+
end
|
22
|
+
|
23
|
+
def response_class(method_name)
|
24
|
+
case method_name
|
25
|
+
when :connect
|
26
|
+
AnyCable::ConnectionResponse
|
27
|
+
when :disconnect
|
28
|
+
AnyCable::DisconnectResponse
|
29
|
+
else
|
30
|
+
AnyCable::CommandResponse
|
31
|
+
end
|
32
|
+
end
|
33
|
+
end
|
34
|
+
end
|
35
|
+
end
|
@@ -0,0 +1,74 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
# Generated by the protocol buffer compiler. DO NOT EDIT!
|
4
|
+
# source: rpc.proto
|
5
|
+
|
6
|
+
require "google/protobuf"
|
7
|
+
|
8
|
+
Google::Protobuf::DescriptorPool.generated_pool.build do
|
9
|
+
add_file("rpc.proto", syntax: :proto3) do
|
10
|
+
add_message "anycable.Env" do
|
11
|
+
optional :url, :string, 1
|
12
|
+
map :headers, :string, :string, 2
|
13
|
+
map :cstate, :string, :string, 3
|
14
|
+
map :istate, :string, :string, 4
|
15
|
+
end
|
16
|
+
add_message "anycable.EnvResponse" do
|
17
|
+
map :cstate, :string, :string, 1
|
18
|
+
map :istate, :string, :string, 2
|
19
|
+
end
|
20
|
+
add_message "anycable.ConnectionRequest" do
|
21
|
+
optional :env, :message, 3, "anycable.Env"
|
22
|
+
end
|
23
|
+
add_message "anycable.ConnectionResponse" do
|
24
|
+
optional :status, :enum, 1, "anycable.Status"
|
25
|
+
optional :identifiers, :string, 2
|
26
|
+
repeated :transmissions, :string, 3
|
27
|
+
optional :error_msg, :string, 4
|
28
|
+
optional :env, :message, 5, "anycable.EnvResponse"
|
29
|
+
end
|
30
|
+
add_message "anycable.CommandMessage" do
|
31
|
+
optional :command, :string, 1
|
32
|
+
optional :identifier, :string, 2
|
33
|
+
optional :connection_identifiers, :string, 3
|
34
|
+
optional :data, :string, 4
|
35
|
+
optional :env, :message, 5, "anycable.Env"
|
36
|
+
end
|
37
|
+
add_message "anycable.CommandResponse" do
|
38
|
+
optional :status, :enum, 1, "anycable.Status"
|
39
|
+
optional :disconnect, :bool, 2
|
40
|
+
optional :stop_streams, :bool, 3
|
41
|
+
repeated :streams, :string, 4
|
42
|
+
repeated :transmissions, :string, 5
|
43
|
+
optional :error_msg, :string, 6
|
44
|
+
optional :env, :message, 7, "anycable.EnvResponse"
|
45
|
+
repeated :stopped_streams, :string, 8
|
46
|
+
end
|
47
|
+
add_message "anycable.DisconnectRequest" do
|
48
|
+
optional :identifiers, :string, 1
|
49
|
+
repeated :subscriptions, :string, 2
|
50
|
+
optional :env, :message, 5, "anycable.Env"
|
51
|
+
end
|
52
|
+
add_message "anycable.DisconnectResponse" do
|
53
|
+
optional :status, :enum, 1, "anycable.Status"
|
54
|
+
optional :error_msg, :string, 2
|
55
|
+
end
|
56
|
+
add_enum "anycable.Status" do
|
57
|
+
value :ERROR, 0
|
58
|
+
value :SUCCESS, 1
|
59
|
+
value :FAILURE, 2
|
60
|
+
end
|
61
|
+
end
|
62
|
+
end
|
63
|
+
|
64
|
+
module AnyCable
|
65
|
+
Env = ::Google::Protobuf::DescriptorPool.generated_pool.lookup("anycable.Env").msgclass
|
66
|
+
EnvResponse = ::Google::Protobuf::DescriptorPool.generated_pool.lookup("anycable.EnvResponse").msgclass
|
67
|
+
ConnectionRequest = ::Google::Protobuf::DescriptorPool.generated_pool.lookup("anycable.ConnectionRequest").msgclass
|
68
|
+
ConnectionResponse = ::Google::Protobuf::DescriptorPool.generated_pool.lookup("anycable.ConnectionResponse").msgclass
|
69
|
+
CommandMessage = ::Google::Protobuf::DescriptorPool.generated_pool.lookup("anycable.CommandMessage").msgclass
|
70
|
+
CommandResponse = ::Google::Protobuf::DescriptorPool.generated_pool.lookup("anycable.CommandResponse").msgclass
|
71
|
+
DisconnectRequest = ::Google::Protobuf::DescriptorPool.generated_pool.lookup("anycable.DisconnectRequest").msgclass
|
72
|
+
DisconnectResponse = ::Google::Protobuf::DescriptorPool.generated_pool.lookup("anycable.DisconnectResponse").msgclass
|
73
|
+
Status = ::Google::Protobuf::DescriptorPool.generated_pool.lookup("anycable.Status").enummodule
|
74
|
+
end
|
data/lib/anycable/rpc.rb
ADDED
@@ -0,0 +1,91 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require "anycable/protos/rpc_pb"
|
4
|
+
|
5
|
+
require "anycable/rpc/handler"
|
6
|
+
|
7
|
+
# Extend some PB auto-generated classes
|
8
|
+
module AnyCable
|
9
|
+
# Current RPC proto version (used for compatibility checks)
|
10
|
+
PROTO_VERSION = "v1"
|
11
|
+
SESSION_KEY = "_s_"
|
12
|
+
|
13
|
+
# Add setters/getter for cstate field
|
14
|
+
module WithConnectionState
|
15
|
+
def initialize(session: nil, **other)
|
16
|
+
if session
|
17
|
+
other[:cstate] ||= {}
|
18
|
+
other[:cstate][SESSION_KEY] = session
|
19
|
+
end
|
20
|
+
super(**other)
|
21
|
+
end
|
22
|
+
|
23
|
+
def session=(val)
|
24
|
+
self.cstate = {} unless cstate
|
25
|
+
state_ = cstate
|
26
|
+
if state_
|
27
|
+
state_[SESSION_KEY] = val
|
28
|
+
end
|
29
|
+
end
|
30
|
+
|
31
|
+
def session
|
32
|
+
state_ = cstate
|
33
|
+
if state_
|
34
|
+
state_[SESSION_KEY]
|
35
|
+
end
|
36
|
+
end
|
37
|
+
|
38
|
+
def cstate
|
39
|
+
env.cstate
|
40
|
+
end
|
41
|
+
|
42
|
+
def cstate=(val)
|
43
|
+
env.cstate = val
|
44
|
+
end
|
45
|
+
|
46
|
+
def istate
|
47
|
+
env.istate
|
48
|
+
end
|
49
|
+
|
50
|
+
def istate=(val)
|
51
|
+
env.istate = val
|
52
|
+
end
|
53
|
+
end
|
54
|
+
|
55
|
+
# Status predicates
|
56
|
+
module StatusPredicates
|
57
|
+
def success?
|
58
|
+
status == :SUCCESS
|
59
|
+
end
|
60
|
+
|
61
|
+
def failure?
|
62
|
+
status == :FAILURE
|
63
|
+
end
|
64
|
+
|
65
|
+
def error?
|
66
|
+
status == :ERROR
|
67
|
+
end
|
68
|
+
end
|
69
|
+
|
70
|
+
class ConnectionResponse
|
71
|
+
prepend WithConnectionState
|
72
|
+
include StatusPredicates
|
73
|
+
end
|
74
|
+
|
75
|
+
class CommandMessage
|
76
|
+
prepend WithConnectionState
|
77
|
+
end
|
78
|
+
|
79
|
+
class CommandResponse
|
80
|
+
prepend WithConnectionState
|
81
|
+
include StatusPredicates
|
82
|
+
end
|
83
|
+
|
84
|
+
class DisconnectRequest
|
85
|
+
prepend WithConnectionState
|
86
|
+
end
|
87
|
+
|
88
|
+
class DisconnectResponse
|
89
|
+
include StatusPredicates
|
90
|
+
end
|
91
|
+
end
|
@@ -0,0 +1,50 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require "anycable/rpc/handlers/connect"
|
4
|
+
require "anycable/rpc/handlers/disconnect"
|
5
|
+
require "anycable/rpc/handlers/command"
|
6
|
+
|
7
|
+
module AnyCable
|
8
|
+
module RPC
|
9
|
+
# Generic RPC handler
|
10
|
+
class Handler
|
11
|
+
include Handlers::Connect
|
12
|
+
include Handlers::Disconnect
|
13
|
+
include Handlers::Command
|
14
|
+
|
15
|
+
def initialize(middleware: AnyCable.middleware)
|
16
|
+
@middleware = middleware
|
17
|
+
@commands = {}
|
18
|
+
end
|
19
|
+
|
20
|
+
def handle(cmd, data)
|
21
|
+
middleware.call(cmd, data) do
|
22
|
+
send(cmd, data)
|
23
|
+
end
|
24
|
+
end
|
25
|
+
|
26
|
+
private
|
27
|
+
|
28
|
+
attr_reader :commands, :middleware
|
29
|
+
|
30
|
+
def build_socket(env:)
|
31
|
+
AnyCable::Socket.new(env: env)
|
32
|
+
end
|
33
|
+
|
34
|
+
def build_env_response(socket)
|
35
|
+
AnyCable::EnvResponse.new(
|
36
|
+
cstate: socket.cstate.changed_fields,
|
37
|
+
istate: socket.istate.changed_fields
|
38
|
+
)
|
39
|
+
end
|
40
|
+
|
41
|
+
def logger
|
42
|
+
AnyCable.logger
|
43
|
+
end
|
44
|
+
|
45
|
+
def factory
|
46
|
+
AnyCable.connection_factory
|
47
|
+
end
|
48
|
+
end
|
49
|
+
end
|
50
|
+
end
|
@@ -0,0 +1,36 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module AnyCable
|
4
|
+
module RPC
|
5
|
+
module Handlers
|
6
|
+
module Command
|
7
|
+
def command(message)
|
8
|
+
logger.debug("RPC Command: #{message.inspect}")
|
9
|
+
|
10
|
+
socket = build_socket(env: message.env)
|
11
|
+
|
12
|
+
connection = factory.call(
|
13
|
+
socket,
|
14
|
+
identifiers: message.connection_identifiers
|
15
|
+
)
|
16
|
+
|
17
|
+
result = connection.handle_channel_command(
|
18
|
+
message.identifier,
|
19
|
+
message.command,
|
20
|
+
message.data
|
21
|
+
)
|
22
|
+
|
23
|
+
AnyCable::CommandResponse.new(
|
24
|
+
status: result ? AnyCable::Status::SUCCESS : AnyCable::Status::FAILURE,
|
25
|
+
disconnect: socket.closed?,
|
26
|
+
stop_streams: socket.stop_streams?,
|
27
|
+
streams: socket.streams[:start],
|
28
|
+
stopped_streams: socket.streams[:stop],
|
29
|
+
transmissions: socket.transmissions,
|
30
|
+
env: build_env_response(socket)
|
31
|
+
)
|
32
|
+
end
|
33
|
+
end
|
34
|
+
end
|
35
|
+
end
|
36
|
+
end
|
@@ -0,0 +1,33 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module AnyCable
|
4
|
+
module RPC
|
5
|
+
module Handlers
|
6
|
+
module Connect
|
7
|
+
def connect(request)
|
8
|
+
logger.debug("RPC Connect: #{request.inspect}")
|
9
|
+
|
10
|
+
socket = build_socket(env: request.env)
|
11
|
+
|
12
|
+
connection = factory.call(socket)
|
13
|
+
|
14
|
+
connection.handle_open
|
15
|
+
|
16
|
+
if socket.closed?
|
17
|
+
AnyCable::ConnectionResponse.new(
|
18
|
+
status: AnyCable::Status::FAILURE,
|
19
|
+
transmissions: socket.transmissions
|
20
|
+
)
|
21
|
+
else
|
22
|
+
AnyCable::ConnectionResponse.new(
|
23
|
+
status: AnyCable::Status::SUCCESS,
|
24
|
+
identifiers: connection.identifiers_json,
|
25
|
+
transmissions: socket.transmissions,
|
26
|
+
env: build_env_response(socket)
|
27
|
+
)
|
28
|
+
end
|
29
|
+
end
|
30
|
+
end
|
31
|
+
end
|
32
|
+
end
|
33
|
+
end
|
@@ -0,0 +1,27 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module AnyCable
|
4
|
+
module RPC
|
5
|
+
module Handlers
|
6
|
+
module Disconnect
|
7
|
+
def disconnect(request)
|
8
|
+
logger.debug("RPC Disconnect: #{request.inspect}")
|
9
|
+
|
10
|
+
socket = build_socket(env: request.env)
|
11
|
+
|
12
|
+
connection = factory.call(
|
13
|
+
socket,
|
14
|
+
identifiers: request.identifiers,
|
15
|
+
subscriptions: request.subscriptions
|
16
|
+
)
|
17
|
+
|
18
|
+
if connection.handle_close
|
19
|
+
AnyCable::DisconnectResponse.new(status: AnyCable::Status::SUCCESS)
|
20
|
+
else
|
21
|
+
AnyCable::DisconnectResponse.new(status: AnyCable::Status::FAILURE)
|
22
|
+
end
|
23
|
+
end
|
24
|
+
end
|
25
|
+
end
|
26
|
+
end
|
27
|
+
end
|
@@ -0,0 +1,21 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
RSpec.shared_context "anycable:rpc:command" do
|
4
|
+
let(:url) { "ws://example.anycable.com/cable" }
|
5
|
+
let(:headers) { {} }
|
6
|
+
let(:env) { AnyCable::Env.new(url: url, headers: headers) }
|
7
|
+
let(:command) { "" }
|
8
|
+
let(:channel_id) { "" }
|
9
|
+
let(:identifiers) { {} }
|
10
|
+
let(:data) { {} }
|
11
|
+
|
12
|
+
let(:request) do
|
13
|
+
AnyCable::CommandMessage.new(
|
14
|
+
command: command,
|
15
|
+
identifier: channel_id,
|
16
|
+
connection_identifiers: identifiers.to_json,
|
17
|
+
data: data.to_json,
|
18
|
+
env: env
|
19
|
+
)
|
20
|
+
end
|
21
|
+
end
|
@@ -0,0 +1,169 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module AnyCable
|
4
|
+
# Socket mock to be used with application connection
|
5
|
+
class Socket
|
6
|
+
# Represents the per-connection store
|
7
|
+
# (for example, used to keep session beetween RPC calls)
|
8
|
+
class State
|
9
|
+
attr_reader :dirty_keys, :source
|
10
|
+
|
11
|
+
def initialize(from)
|
12
|
+
@source = from
|
13
|
+
@dirty_keys = nil
|
14
|
+
end
|
15
|
+
|
16
|
+
def read(key)
|
17
|
+
source&.[](key)
|
18
|
+
end
|
19
|
+
|
20
|
+
alias_method :[], :read
|
21
|
+
|
22
|
+
def write(key, val)
|
23
|
+
return if source&.[](key) == val
|
24
|
+
|
25
|
+
@source ||= {}
|
26
|
+
|
27
|
+
keys = (@dirty_keys ||= [])
|
28
|
+
keys << key
|
29
|
+
|
30
|
+
source[key] = val
|
31
|
+
end
|
32
|
+
|
33
|
+
alias_method :[]=, :write
|
34
|
+
|
35
|
+
def changed_fields
|
36
|
+
return unless source
|
37
|
+
|
38
|
+
keys = dirty_keys
|
39
|
+
return if keys.nil?
|
40
|
+
|
41
|
+
source.slice(*keys)
|
42
|
+
end
|
43
|
+
end
|
44
|
+
|
45
|
+
attr_reader :transmissions
|
46
|
+
|
47
|
+
def initialize(env:)
|
48
|
+
@transmissions = []
|
49
|
+
@request_env = env
|
50
|
+
end
|
51
|
+
|
52
|
+
def transmit(websocket_message)
|
53
|
+
transmissions << websocket_message
|
54
|
+
end
|
55
|
+
|
56
|
+
def subscribe(_channel, broadcasting)
|
57
|
+
streams[:start] << broadcasting
|
58
|
+
end
|
59
|
+
|
60
|
+
def unsubscribe(_channel, broadcasting)
|
61
|
+
streams[:stop] << broadcasting
|
62
|
+
end
|
63
|
+
|
64
|
+
def unsubscribe_from_all(_channel)
|
65
|
+
@stop_all_streams = true
|
66
|
+
end
|
67
|
+
|
68
|
+
def streams
|
69
|
+
@streams ||= {start: [], stop: []}
|
70
|
+
end
|
71
|
+
|
72
|
+
def close
|
73
|
+
@closed = true
|
74
|
+
@streams&.clear
|
75
|
+
@stop_all_streams = true
|
76
|
+
end
|
77
|
+
|
78
|
+
def closed?
|
79
|
+
@closed == true
|
80
|
+
end
|
81
|
+
|
82
|
+
def stop_streams?
|
83
|
+
@stop_all_streams == true
|
84
|
+
end
|
85
|
+
|
86
|
+
def session
|
87
|
+
cstate.read(SESSION_KEY)
|
88
|
+
end
|
89
|
+
|
90
|
+
def session=(val)
|
91
|
+
cstate.write(SESSION_KEY, val)
|
92
|
+
end
|
93
|
+
|
94
|
+
def env
|
95
|
+
return @env if defined?(@env)
|
96
|
+
|
97
|
+
@env = build_rack_env
|
98
|
+
end
|
99
|
+
|
100
|
+
def istate
|
101
|
+
return @istate if defined?(@istate)
|
102
|
+
|
103
|
+
@istate = env["anycable.istate"] = State.new(env["anycable.raw_istate"])
|
104
|
+
end
|
105
|
+
|
106
|
+
def cstate
|
107
|
+
return @cstate if defined?(@cstate)
|
108
|
+
|
109
|
+
@cstate = env["anycable.cstate"] = State.new(env["anycable.raw_cstate"])
|
110
|
+
end
|
111
|
+
|
112
|
+
private
|
113
|
+
|
114
|
+
attr_reader :request_env
|
115
|
+
|
116
|
+
# Build Rack env from request
|
117
|
+
def build_rack_env
|
118
|
+
uri = URI.parse(request_env.url)
|
119
|
+
|
120
|
+
env = base_rack_env
|
121
|
+
env.merge!({
|
122
|
+
"PATH_INFO" => uri.path,
|
123
|
+
"QUERY_STRING" => uri.query,
|
124
|
+
"SERVER_NAME" => uri.host,
|
125
|
+
"SERVER_PORT" => uri.port,
|
126
|
+
"HTTP_HOST" => uri.host,
|
127
|
+
"REMOTE_ADDR" => request_env.headers.delete("REMOTE_ADDR"),
|
128
|
+
"rack.url_scheme" => uri.scheme&.sub(/^ws/, "http"),
|
129
|
+
# AnyCable specific fields
|
130
|
+
"anycable.raw_cstate" => request_env.cstate&.to_h,
|
131
|
+
"anycable.raw_istate" => request_env.istate&.to_h
|
132
|
+
}.delete_if { |_k, v| v.nil? })
|
133
|
+
|
134
|
+
env.merge!(build_headers(request_env.headers))
|
135
|
+
end
|
136
|
+
|
137
|
+
def base_rack_env
|
138
|
+
# Minimum required variables according to Rack Spec
|
139
|
+
# (not all of them though, just those enough for Action Cable to work)
|
140
|
+
# See https://rubydoc.info/github/rack/rack/master/file/SPEC
|
141
|
+
# and https://github.com/rack/rack/blob/master/lib/rack/lint.rb
|
142
|
+
{
|
143
|
+
"REQUEST_METHOD" => "GET",
|
144
|
+
"SCRIPT_NAME" => "",
|
145
|
+
"PATH_INFO" => "/",
|
146
|
+
"QUERY_STRING" => "",
|
147
|
+
"SERVER_NAME" => "",
|
148
|
+
"SERVER_PORT" => "80",
|
149
|
+
"rack.url_scheme" => "http",
|
150
|
+
"rack.input" => StringIO.new("", "r").tap { |io| io.set_encoding(Encoding::ASCII_8BIT) },
|
151
|
+
"rack.version" => ::Rack::VERSION,
|
152
|
+
"rack.errors" => StringIO.new("").tap { |io| io.set_encoding(Encoding::ASCII_8BIT) },
|
153
|
+
"rack.multithread" => true,
|
154
|
+
"rack.multiprocess" => false,
|
155
|
+
"rack.run_once" => false,
|
156
|
+
"rack.hijack?" => false
|
157
|
+
}
|
158
|
+
end
|
159
|
+
|
160
|
+
def build_headers(headers)
|
161
|
+
headers.each_with_object({}) do |header, obj|
|
162
|
+
k, v = *header
|
163
|
+
k = k.upcase
|
164
|
+
k.tr!("-", "_")
|
165
|
+
obj["HTTP_#{k}"] = v
|
166
|
+
end
|
167
|
+
end
|
168
|
+
end
|
169
|
+
end
|