anycable-core 1.1.0.pre1
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 +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
|