anycable-rack-server 0.0.1

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.
@@ -0,0 +1,7 @@
1
+ ---
2
+ SHA256:
3
+ metadata.gz: 416a34b5853c2b0f81f4693e59fe4c9b10021b41fc8eb2e710b8fd82b5a67c04
4
+ data.tar.gz: fcb0f8b569adc737b704025393237b573b7de616ea4e5deedaa77e442a51bc09
5
+ SHA512:
6
+ metadata.gz: 7cd8bcd7b5afda208b3df2e476b0729faf9fbfbfd9d6fb36735f26cba48b9589e3a4704a9dbc69c6743bdf4f209c2ee9e6d44f8521a5fc89036a42859f0f9090
7
+ data.tar.gz: 68f4b98948bb3ec8fb330947faed0f933ee7dd12dab2f1ca6abbb35af41ab3f7b7f3ad720ea74be82af486f309b250c340603e8e10716478a03975704c3ba3f5
data/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2018 Yulia Oletskaya
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
@@ -0,0 +1,37 @@
1
+ # anycable-rack-server
2
+
3
+ AnyCable-compatible Rack hijack based Ruby Web Socket server middleware designed for development and testing purposes.
4
+
5
+ ## Usage
6
+
7
+ Mount the rack middleware
8
+ ```ruby
9
+ # config/routes.rb
10
+ Rails.application.routes.draw do
11
+ mount AnyCable::Rack.new => '/cable'
12
+ end
13
+ ```
14
+
15
+ ## Settings
16
+
17
+ Customizable options: headers being sent with each gRPC request. The gem uses AnyCable config for redis and gRPC host settings.
18
+
19
+ Default headers: `'cookie', 'x-api-token'`.
20
+
21
+ Can be specified via env variable
22
+ ```
23
+ ANYCABLE_HEADERS=cookie,x-api-token,origin
24
+ ```
25
+
26
+ Or
27
+
28
+ ```ruby
29
+ options = { headers: ['cookie', 'origin'] }
30
+ AnyCable::Rack.new(nil, options)
31
+ ```
32
+
33
+ ## Testing
34
+
35
+ Run units with `rake`.
36
+
37
+ Instructions for testing with [anyt](https://github.com/anycable/anyt) (anycable conformance testing) can be found [here](https://github.com/tuwukee/anycable-rack-server/tree/master/test/support).
@@ -0,0 +1,3 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'anycable/rack-server'
@@ -0,0 +1,107 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'set'
4
+ require 'json'
5
+ require 'anycable'
6
+ require 'websocket'
7
+ require 'securerandom'
8
+ require 'anycable/rack-server/hub'
9
+ require 'anycable/rack-server/pinger'
10
+ require 'anycable/rack-server/errors'
11
+ require 'anycable/rack-server/middleware'
12
+ require 'anycable/rack-server/broadcast_subscribers/redis_subscriber'
13
+ require 'anycable/rack-server/coders/json'
14
+
15
+ module AnyCable
16
+ module RackServer
17
+ DEFAULT_OPTIONS = {
18
+ headers: ['cookie', 'x-api-token']
19
+ }.freeze
20
+
21
+ class << self
22
+ attr_reader :broadcast_subscriber,
23
+ :coder,
24
+ :hub,
25
+ :middleware,
26
+ :pinger,
27
+ :server_id
28
+
29
+ def start(options = {})
30
+ options = DEFAULT_OPTIONS.merge(options)
31
+ @hub = Hub.new
32
+ @pinger = Pinger.new
33
+ @coder = Coders::JSON
34
+
35
+ rpc_host = unpack_host(AnyCable.config.rpc_host)
36
+ headers = parse_env_headers || options[:headers]
37
+
38
+ @server_id = "anycable-rack-server-#{SecureRandom.hex}"
39
+ @middleware = Middleware.new(
40
+ nil,
41
+ pinger: pinger,
42
+ hub: hub,
43
+ coder: coder,
44
+ rpc_host: rpc_host,
45
+ headers: headers,
46
+ server_id: server_id
47
+ )
48
+
49
+ broadcast_subscribe
50
+
51
+ @_started = true
52
+ end
53
+
54
+ def started?
55
+ @_started == true
56
+ end
57
+
58
+ def stop
59
+ return unless started?
60
+
61
+ @_started = false
62
+ broadcast_subscriber.unsubscribe(@_redis_channel)
63
+ pinger.stop
64
+
65
+ hub.sockets.each do |socket|
66
+ hub.remove_socket(socket)
67
+ socket.close
68
+ end
69
+ end
70
+
71
+ private
72
+
73
+ def broadcast_subscribe
74
+ @_redis_params = AnyCable.config.to_redis_params
75
+ @_redis_channel = AnyCable.config.redis_channel
76
+
77
+ @broadcast_subscriber = BroadcastSubscribers::RedisSubscriber.new(
78
+ hub: @hub,
79
+ coder: @coder,
80
+ options: @_redis_params
81
+ )
82
+
83
+ @broadcast_subscriber.subscribe(@_redis_channel)
84
+ end
85
+
86
+ def parse_env_headers
87
+ headers = ENV['ANYCABLE_HEADERS'].to_s.split(',')
88
+ return nil if headers.empty?
89
+ headers
90
+ end
91
+
92
+ def unpack_host(str)
93
+ str.gsub('[::]', '0.0.0.0')
94
+ end
95
+ end
96
+ end
97
+
98
+ class Rack
99
+ def initialize(_app = nil, options = {})
100
+ AnyCable::RackServer.start(options)
101
+ end
102
+
103
+ def call(env)
104
+ AnyCable::RackServer.middleware.call(env)
105
+ end
106
+ end
107
+ end
@@ -0,0 +1,40 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'redis'
4
+
5
+ module AnyCable
6
+ module RackServer
7
+ module BroadcastSubscribers
8
+ class RedisSubscriber
9
+ attr_reader :hub, :coder, :redis_conn, :threads
10
+
11
+ def initialize(hub:, coder:, options:)
12
+ @hub = hub
13
+ @coder = coder
14
+ @redis_conn = ::Redis.new(options)
15
+ @threads = {}
16
+ end
17
+
18
+ def subscribe(channel)
19
+ @threads[channel] = Thread.new do
20
+ redis_conn.subscribe(channel) do |on|
21
+ on.message { |_channel, msg| handle_message(msg) }
22
+ end
23
+ end
24
+ end
25
+
26
+ def unsubscribe(channel)
27
+ @threads[channel].terminate unless @threads[channel].nil?
28
+ @threads.delete(channel)
29
+ end
30
+
31
+ private
32
+
33
+ def handle_message(msg)
34
+ data = JSON.parse(msg)
35
+ hub.broadcast(data['stream'], data['data'], coder)
36
+ end
37
+ end
38
+ end
39
+ end
40
+ end
@@ -0,0 +1,19 @@
1
+ # frozen_string_literal: true
2
+
3
+ module AnyCable
4
+ module RackServer
5
+ module Coders
6
+ module JSON
7
+ class << self
8
+ def decode(json_str)
9
+ ::JSON.parse(json_str)
10
+ end
11
+
12
+ def encode(ruby_obj)
13
+ ruby_obj.to_json
14
+ end
15
+ end
16
+ end
17
+ end
18
+ end
19
+ end
@@ -0,0 +1,189 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'anycable/rack-server/rpc/client'
4
+ require 'anycable/rack-server/logging'
5
+ require 'anycable/rack-server/errors'
6
+
7
+ module AnyCable
8
+ # rubocop:disable Metrics/LineLength
9
+ module RackServer
10
+ class Connection
11
+ # rubocop:enable Metrics/LineLength
12
+ include Logging
13
+
14
+ attr_reader :coder,
15
+ :header_names,
16
+ :hub,
17
+ :socket,
18
+ :rpc_client,
19
+ :server_id
20
+
21
+ def initialize(socket, hub, coder, host, header_names, server_id)
22
+ @socket = socket
23
+ @coder = coder
24
+ @hub = hub
25
+ @header_names = header_names
26
+ @server_id = server_id
27
+
28
+ @rpc_client = RPC::Client.new(host)
29
+
30
+ @_identifiers = '{}'
31
+ @_subscriptions = Set.new
32
+ end
33
+
34
+ def handle_open
35
+ response = rpc_connect
36
+ process_open(response)
37
+ end
38
+
39
+ def handle_close
40
+ response = rpc_disconnect
41
+ process_close(response)
42
+ reset_connection
43
+ end
44
+
45
+ def handle_command(websocket_message)
46
+ decoded = decode(websocket_message)
47
+ command = decoded.delete('command')
48
+
49
+ channel_identifier = decoded['identifier']
50
+
51
+ case command
52
+ when 'subscribe' then subscribe(channel_identifier)
53
+ when 'unsubscribe' then unsubscribe(channel_identifier)
54
+ when 'message' then send_message(channel_identifier, decoded['data'])
55
+ else
56
+ raise Errors::UnknownCommand, "Command not found #{command}"
57
+ end
58
+ end
59
+
60
+ private
61
+
62
+ def transmit(cable_message)
63
+ socket.transmit(encode(cable_message))
64
+ end
65
+
66
+ def close
67
+ socket.close
68
+ end
69
+
70
+ def request
71
+ socket.request
72
+ end
73
+
74
+ def request_path
75
+ request.fullpath
76
+ end
77
+
78
+ def rpc_connect
79
+ rpc_client.connect(headers: headers, path: request_path)
80
+ end
81
+
82
+ def rpc_disconnect
83
+ rpc_client.disconnect(
84
+ identifiers: @_identifiers,
85
+ subscriptions: @_subscriptions.to_a,
86
+ headers: headers,
87
+ path: request_path
88
+ )
89
+ end
90
+
91
+ def rpc_command(command, identifier, data = '')
92
+ rpc_client.command(
93
+ command: command,
94
+ identifier: identifier,
95
+ connection_identifiers: @_identifiers,
96
+ data: data
97
+ )
98
+ end
99
+
100
+ def subscribe(identifier)
101
+ response = rpc_command('subscribe', identifier)
102
+ if response.status == :SUCCESS
103
+ @_subscriptions.add(identifier)
104
+ else
105
+ log(:debug, log_fmt("RPC subscribe command failed: #{response.inspect}"))
106
+ end
107
+ process_command(response, identifier)
108
+ end
109
+
110
+ def unsubscribe(identifier)
111
+ response = rpc_command('unsubscribe', identifier)
112
+ if response.status == :SUCCESS
113
+ @_subscriptions.delete(identifier)
114
+ else
115
+ log(:debug, log_fmt("RPC unsubscribe command failed: #{response.inspect}"))
116
+ end
117
+ process_command(response, identifier)
118
+ end
119
+
120
+ def send_message(identifier, data)
121
+ response = rpc_command('message', identifier, data)
122
+ unless response.status == :SUCCESS
123
+ log(:debug, log_fmt("RPC message command failed: #{response.inspect}"))
124
+ end
125
+ process_command(response, identifier)
126
+ end
127
+
128
+ def headers
129
+ @headers ||= begin
130
+ header_names.inject({}) do |acc, name|
131
+ header_val = request.env["HTTP_#{name.gsub(/-/,'_').upcase}"]
132
+ acc[name] = header_val unless header_val.nil? || header_val.empty?
133
+ acc
134
+ end
135
+ end
136
+ end
137
+
138
+ def process_command(response, identifier)
139
+ response.transmissions.each { |transmission| transmit(decode(transmission)) }
140
+ hub.remove_channel(socket, identifier) if response.stop_streams
141
+ response.streams.each { |stream| hub.add_subscriber(stream, socket, identifier) }
142
+ close_connection if response.disconnect
143
+ end
144
+
145
+ def process_open(response)
146
+ if response.status == :SUCCESS
147
+ @_identifiers = response.identifiers
148
+ response.transmissions.each { |transmission| transmit(decode(transmission)) }
149
+ log(:debug) { log_fmt('Opened') }
150
+ else
151
+ log(:error, log_fmt("RPC connection command failed: #{response.inspect}"))
152
+ close_connection
153
+ end
154
+ end
155
+
156
+ def process_close(response)
157
+ if response.status == :SUCCESS
158
+ log(:debug) { log_fmt('Closed') }
159
+ else
160
+ log(:error, log_fmt("RPC disconnection command failed: #{response.inspect}"))
161
+ end
162
+ end
163
+
164
+ def reset_connection
165
+ @_identifiers = '{}'
166
+ @_subscriptions = []
167
+
168
+ hub.remove_socket(socket)
169
+ end
170
+
171
+ def close_connection
172
+ reset_connection
173
+ close
174
+ end
175
+
176
+ def encode(cable_message)
177
+ coder.encode(cable_message)
178
+ end
179
+
180
+ def decode(websocket_message)
181
+ coder.decode(websocket_message)
182
+ end
183
+
184
+ def log_fmt(msg)
185
+ "[connection:#{server_id}] #{msg}"
186
+ end
187
+ end
188
+ end
189
+ end
@@ -0,0 +1,11 @@
1
+ # frozen_string_literal: true
2
+
3
+ module AnyCable
4
+ module RackServer
5
+ module Errors
6
+ class HijackNotAvailable < RuntimeError; end
7
+ class UnknownCommand < StandardError; end
8
+ class MiddlewareSetup < StandardError; end
9
+ end
10
+ end
11
+ end
@@ -0,0 +1,83 @@
1
+ # frozen_string_literal: true
2
+
3
+ module AnyCable
4
+ module RackServer
5
+ # From https://github.com/rails/rails/blob/v5.0.1/actioncable/lib/action_cable/subscription_adapter/subscriber_map.rb
6
+ class Hub
7
+ attr_reader :streams, :sockets
8
+
9
+ def initialize
10
+ @streams = Hash.new do |streams, stream_id|
11
+ streams[stream_id] = Hash.new { |channels, channel_id| channels[channel_id] = Set.new }
12
+ end
13
+ @sockets = Hash.new { |h, k| h[k] = Set.new }
14
+ @sync = Mutex.new
15
+ end
16
+
17
+ def add_subscriber(stream, socket, channel)
18
+ @sync.synchronize do
19
+ @streams[stream][channel] << socket
20
+ @sockets[socket] << [channel, stream]
21
+ end
22
+ end
23
+
24
+ def remove_subscriber(stream, socket, channel)
25
+ @sync.synchronize do
26
+ @streams[stream][channel].delete(socket)
27
+ @sockets[socket].delete([channel, stream])
28
+ cleanup stream, socket, channel
29
+ end
30
+ end
31
+
32
+ def remove_channel(socket, channel)
33
+ list = @sync.synchronize do
34
+ return unless @sockets.key?(socket)
35
+
36
+ @sockets[socket].dup
37
+ end
38
+
39
+ list.each do |(channel_id, stream)|
40
+ remove_subscriber(stream, socket, channel) if channel == channel_id
41
+ end
42
+ end
43
+
44
+ def remove_socket(socket)
45
+ list = @sync.synchronize do
46
+ return unless @sockets.key?(socket)
47
+
48
+ @sockets[socket].dup
49
+ end
50
+
51
+ list.each do |(channel_id, stream)|
52
+ remove_subscriber(stream, socket, channel_id)
53
+ end
54
+ end
55
+
56
+ def broadcast(stream, message, coder)
57
+ list = @sync.synchronize do
58
+ return unless @streams.key?(stream)
59
+
60
+ @streams[stream].to_a
61
+ end
62
+
63
+ list.each do |(channel_id, sockets)|
64
+ decoded = coder.decode(message)
65
+ cmessage = channel_message(channel_id, decoded, coder)
66
+ sockets.each { |socket| socket.transmit(cmessage) }
67
+ end
68
+ end
69
+
70
+ private
71
+
72
+ def cleanup(stream, socket, channel)
73
+ @streams[stream].delete(channel) if @streams[stream][channel].empty?
74
+ @streams.delete(stream) if @streams[stream].empty?
75
+ @sockets.delete(socket) if @sockets[socket].empty?
76
+ end
77
+
78
+ def channel_message(channel_id, message, coder)
79
+ coder.encode(identifier: channel_id, message: message)
80
+ end
81
+ end
82
+ end
83
+ end
@@ -0,0 +1,15 @@
1
+ # frozen_string_literal: true
2
+
3
+ module AnyCable
4
+ module RackServer
5
+ module Logging # :nodoc:
6
+ PREFIX = 'AnycableRackServer'
7
+
8
+ private
9
+
10
+ def log(level, message = nil, logger = AnyCable.logger)
11
+ logger.send(level, PREFIX) { message || yield }
12
+ end
13
+ end
14
+ end
15
+ end
@@ -0,0 +1,82 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'anycable/rack-server/connection'
4
+ require 'anycable/rack-server/errors'
5
+ require 'anycable/rack-server/socket'
6
+
7
+ module AnyCable
8
+ module RackServer
9
+ class Middleware
10
+ PROTOCOLS = ['actioncable-v1-json', 'actioncable-unsupported'].freeze
11
+ attr_reader :pinger,
12
+ :hub,
13
+ :coder,
14
+ :rpc_host,
15
+ :headers,
16
+ :server_id
17
+
18
+ def initialize(_app, pinger:, hub:, coder:, rpc_host:, headers:, server_id:)
19
+ @pinger = pinger
20
+ @hub = hub
21
+ @coder = coder
22
+ @rpc_host = rpc_host
23
+ @headers = headers
24
+ @server_id = server_id
25
+ end
26
+
27
+ def call(env)
28
+ return not_found unless websocket?(env)
29
+
30
+ rack_hijack(env)
31
+ listen_socket(env)
32
+
33
+ [-1, {}, []]
34
+ end
35
+
36
+ private
37
+
38
+ def handshake
39
+ @handshake ||= WebSocket::Handshake::Server.new(protocols: PROTOCOLS)
40
+ end
41
+
42
+ def rack_hijack(env)
43
+ raise Errors::HijackNotAvailable unless env['rack.hijack']
44
+
45
+ env['rack.hijack'].call
46
+ send_handshake(env)
47
+ end
48
+
49
+ def send_handshake(env)
50
+ handshake.from_rack(env)
51
+ env['rack.hijack_io'].write(handshake.to_s)
52
+ end
53
+
54
+ def listen_socket(env)
55
+ socket = Socket.new(env, env['rack.hijack_io'], handshake.version)
56
+ init_connection(socket)
57
+ init_pinger(socket)
58
+ socket.listen
59
+ end
60
+
61
+ def not_found
62
+ [404, { 'Content-Type' => 'text/plain' }, ['Not Found']]
63
+ end
64
+
65
+ def websocket?(env)
66
+ env['HTTP_UPGRADE'] == 'websocket'
67
+ end
68
+
69
+ def init_connection(socket)
70
+ connection = Connection.new(socket, hub, coder, rpc_host, headers, server_id)
71
+ socket.onopen { connection.handle_open }
72
+ socket.onclose { connection.handle_close }
73
+ socket.onmessage { |data| connection.handle_command(data) }
74
+ end
75
+
76
+ def init_pinger(socket)
77
+ pinger.add(socket)
78
+ socket.onclose { pinger.remove(socket) }
79
+ end
80
+ end
81
+ end
82
+ end
@@ -0,0 +1,53 @@
1
+ # frozen_string_literal: true
2
+
3
+ module AnyCable
4
+ module RackServer
5
+ # Sends pings to sockets
6
+ class Pinger
7
+ INTERVAL = 3
8
+
9
+ def initialize
10
+ @_sockets = []
11
+ @_stopped = false
12
+ run
13
+ end
14
+
15
+ def add(socket)
16
+ @_sockets << socket
17
+ end
18
+
19
+ def remove(socket)
20
+ @_sockets.delete(socket)
21
+ end
22
+
23
+ def stop
24
+ @_stopped = true
25
+ end
26
+
27
+ # rubocop: disable Metrics/MethodLength
28
+ def run
29
+ Thread.new do
30
+ loop do
31
+ break if @_stopped
32
+
33
+ unless @_sockets.empty?
34
+ msg = ping_message(Time.now.to_i)
35
+ @_sockets.each do |socket|
36
+ socket.transmit(msg)
37
+ end
38
+ end
39
+
40
+ sleep(INTERVAL)
41
+ end
42
+ end
43
+ end
44
+ # rubocop: enable Metrics/MethodLength
45
+
46
+ private
47
+
48
+ def ping_message(time)
49
+ { type: :ping, message: time }.to_json
50
+ end
51
+ end
52
+ end
53
+ end
@@ -0,0 +1,42 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'grpc'
4
+
5
+ module AnyCable
6
+ module RackServer
7
+ module RPC
8
+ class Client
9
+ attr_reader :stub
10
+
11
+ def initialize(host)
12
+ @stub = AnyCable::RPC::Service.rpc_stub_class.new(host, :this_channel_is_insecure)
13
+ end
14
+
15
+ def connect(headers:, path:)
16
+ request = ConnectionRequest.new(headers: headers, path: path)
17
+ stub.connect(request)
18
+ end
19
+
20
+ def command(command:, identifier:, connection_identifiers:, data:)
21
+ message = CommandMessage.new(
22
+ command: command,
23
+ identifier: identifier,
24
+ connection_identifiers: connection_identifiers,
25
+ data: data
26
+ )
27
+ stub.command(message)
28
+ end
29
+
30
+ def disconnect(identifiers:, subscriptions:, headers:, path:)
31
+ request = DisconnectRequest.new(
32
+ identifiers: identifiers,
33
+ subscriptions: subscriptions,
34
+ headers: headers,
35
+ path: path
36
+ )
37
+ stub.disconnect(request)
38
+ end
39
+ end
40
+ end
41
+ end
42
+ end
@@ -0,0 +1,55 @@
1
+ syntax = "proto3";
2
+
3
+ package anycable;
4
+
5
+ service RPC {
6
+ rpc Connect (ConnectionRequest) returns (ConnectionResponse) {}
7
+ rpc Command (CommandMessage) returns (CommandResponse) {}
8
+ rpc Disconnect (DisconnectRequest) returns (DisconnectResponse) {}
9
+ }
10
+
11
+ enum Status {
12
+ ERROR = 0;
13
+ SUCCESS = 1;
14
+ FAILURE = 2;
15
+ }
16
+
17
+ message ConnectionRequest {
18
+ string path = 1;
19
+ map<string,string> headers = 2;
20
+ }
21
+
22
+ message ConnectionResponse {
23
+ Status status = 1;
24
+ string identifiers = 2;
25
+ repeated string transmissions = 3;
26
+ string error_msg = 4;
27
+ }
28
+
29
+ message CommandMessage {
30
+ string command = 1;
31
+ string identifier = 2;
32
+ string connection_identifiers = 3;
33
+ string data = 4;
34
+ }
35
+
36
+ message CommandResponse {
37
+ Status status = 1;
38
+ bool disconnect = 2;
39
+ bool stop_streams = 3;
40
+ repeated string streams = 4;
41
+ repeated string transmissions = 5;
42
+ string error_msg = 6;
43
+ }
44
+
45
+ message DisconnectRequest {
46
+ string identifiers = 1;
47
+ repeated string subscriptions = 2;
48
+ string path = 3;
49
+ map<string,string> headers = 4;
50
+ }
51
+
52
+ message DisconnectResponse {
53
+ Status status = 1;
54
+ string error_msg = 2;
55
+ }
@@ -0,0 +1,159 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'anycable/rack-server/logging'
4
+
5
+ module AnyCable
6
+ module RackServer
7
+ class Socket
8
+ include Logging
9
+ attr_reader :version, :socket
10
+
11
+ def initialize(env, socket, version)
12
+ log(:debug, "WebSocket version #{version}")
13
+ @env = env
14
+ @socket = socket
15
+ @version = version
16
+
17
+ @_open_handlers = []
18
+ @_message_handlers = []
19
+ @_close_handlers = []
20
+ @_error_handlers = []
21
+ @_active = true
22
+ end
23
+
24
+ def transmit(data, type: :text)
25
+ frame = WebSocket::Frame::Outgoing::Server.new(
26
+ version: version,
27
+ data: data,
28
+ type: type
29
+ )
30
+ socket.write(frame.to_s)
31
+ rescue IOError, Errno::EPIPE, Errno::ETIMEDOUT => e
32
+ log(:error, "Socket send failed: #{e}")
33
+ close
34
+ end
35
+
36
+ def request
37
+ @_request ||= ::Rack::Request.new(@env)
38
+ end
39
+
40
+ def onopen(&block)
41
+ @_open_handlers << block
42
+ end
43
+
44
+ def onmessage(&block)
45
+ @_message_handlers << block
46
+ end
47
+
48
+ def onclose(&block)
49
+ @_close_handlers << block
50
+ end
51
+
52
+ def onerror(&block)
53
+ @_error_handlers << block
54
+ end
55
+
56
+ # rubocop: disable Metrics/MethodLength
57
+ def listen
58
+ keepalive
59
+ Thread.new do
60
+ Thread.current.abort_on_exception = true
61
+ begin
62
+ @_open_handlers.each(&:call)
63
+ each_frame do |data|
64
+ @_message_handlers.each do |handler|
65
+ begin
66
+ handler.call(data)
67
+ rescue => e # rubocop: disable Style/RescueStandardError
68
+ log(:error, "Socket receive failed: #{e}")
69
+ @_error_handlers.each { |eh| eh.call(e, data) }
70
+ close
71
+ end
72
+ end
73
+ end
74
+ ensure
75
+ close
76
+ end
77
+ end
78
+ end
79
+ # rubocop: enable Metrics/MethodLength
80
+
81
+ def close
82
+ return unless @_active
83
+
84
+ @_close_handlers.each(&:call)
85
+ close!
86
+
87
+ @_active = false
88
+ end
89
+
90
+ def closed?
91
+ socket.closed?
92
+ end
93
+
94
+ private
95
+
96
+ def close!
97
+ if socket.respond_to?(:closed?)
98
+ close_socket unless @socket.closed?
99
+ else
100
+ close_socket
101
+ end
102
+ end
103
+
104
+ def close_socket
105
+ frame = WebSocket::Frame::Outgoing::Server.new(version: version, type: :close, code: 1000)
106
+ socket.write(frame.to_s) if frame.supported?
107
+ socket.close
108
+ rescue IOError, Errno::EPIPE, Errno::ETIMEDOUT # rubocop:disable Lint/HandleExceptions
109
+ # already closed
110
+ end
111
+
112
+ def keepalive
113
+ thread = Thread.new do
114
+ Thread.current.abort_on_exception = true
115
+ loop do
116
+ sleep 5
117
+ time = Time.now.to_i
118
+ transmit({ message: time, type: :ping }.to_json)
119
+ end
120
+ end
121
+
122
+ onclose do
123
+ thread.kill
124
+ end
125
+ end
126
+
127
+ # rubocop:disable Metrics/AbcSize
128
+ # rubocop:disable Metrics/CyclomaticComplexity
129
+ # rubocop:disable Metrics/PerceivedComplexity
130
+ # rubocop:disable Metrics/MethodLength
131
+ def each_frame
132
+ framebuffer = WebSocket::Frame::Incoming::Server.new(version: version)
133
+ while IO.select([socket])
134
+ if socket.respond_to?(:recvfrom)
135
+ data, _addrinfo = socket.recvfrom(2000)
136
+ else
137
+ data, _addrinfo = socket.readpartial(2000), socket.peeraddr
138
+ end
139
+
140
+ break if data.empty?
141
+
142
+ framebuffer << data
143
+
144
+ while frame = framebuffer.next
145
+ case frame.type
146
+ when :close
147
+ return
148
+ when :text, :binary
149
+ yield frame.data
150
+ end
151
+ end
152
+ end
153
+ rescue Errno::EHOSTUNREACH, Errno::ETIMEDOUT, Errno::ECONNRESET, IOError, Errno::EBADF => e
154
+ log(:error, "Socket frame error: #{e}")
155
+ nil # client disconnected or timed out
156
+ end
157
+ end
158
+ end
159
+ end
@@ -0,0 +1,7 @@
1
+ # frozen_string_literal: true
2
+
3
+ module AnyCable
4
+ module RackServer
5
+ VERSION = '0.0.1'
6
+ end
7
+ end
metadata ADDED
@@ -0,0 +1,143 @@
1
+ --- !ruby/object:Gem::Specification
2
+ name: anycable-rack-server
3
+ version: !ruby/object:Gem::Version
4
+ version: 0.0.1
5
+ platform: ruby
6
+ authors:
7
+ - Yulia Oletskaya
8
+ autorequire:
9
+ bindir: bin
10
+ cert_chain: []
11
+ date: 2019-01-06 00:00:00.000000000 Z
12
+ dependencies:
13
+ - !ruby/object:Gem::Dependency
14
+ name: anycable
15
+ requirement: !ruby/object:Gem::Requirement
16
+ requirements:
17
+ - - "~>"
18
+ - !ruby/object:Gem::Version
19
+ version: '0.6'
20
+ type: :runtime
21
+ prerelease: false
22
+ version_requirements: !ruby/object:Gem::Requirement
23
+ requirements:
24
+ - - "~>"
25
+ - !ruby/object:Gem::Version
26
+ version: '0.6'
27
+ - !ruby/object:Gem::Dependency
28
+ name: websocket
29
+ requirement: !ruby/object:Gem::Requirement
30
+ requirements:
31
+ - - "~>"
32
+ - !ruby/object:Gem::Version
33
+ version: '1.2'
34
+ type: :runtime
35
+ prerelease: false
36
+ version_requirements: !ruby/object:Gem::Requirement
37
+ requirements:
38
+ - - "~>"
39
+ - !ruby/object:Gem::Version
40
+ version: '1.2'
41
+ - !ruby/object:Gem::Dependency
42
+ name: redis
43
+ requirement: !ruby/object:Gem::Requirement
44
+ requirements:
45
+ - - "~>"
46
+ - !ruby/object:Gem::Version
47
+ version: '4'
48
+ type: :runtime
49
+ prerelease: false
50
+ version_requirements: !ruby/object:Gem::Requirement
51
+ requirements:
52
+ - - "~>"
53
+ - !ruby/object:Gem::Version
54
+ version: '4'
55
+ - !ruby/object:Gem::Dependency
56
+ name: anyt
57
+ requirement: !ruby/object:Gem::Requirement
58
+ requirements:
59
+ - - "~>"
60
+ - !ruby/object:Gem::Version
61
+ version: '0.8'
62
+ type: :development
63
+ prerelease: false
64
+ version_requirements: !ruby/object:Gem::Requirement
65
+ requirements:
66
+ - - "~>"
67
+ - !ruby/object:Gem::Version
68
+ version: '0.8'
69
+ - !ruby/object:Gem::Dependency
70
+ name: minitest
71
+ requirement: !ruby/object:Gem::Requirement
72
+ requirements:
73
+ - - "~>"
74
+ - !ruby/object:Gem::Version
75
+ version: '5.11'
76
+ type: :development
77
+ prerelease: false
78
+ version_requirements: !ruby/object:Gem::Requirement
79
+ requirements:
80
+ - - "~>"
81
+ - !ruby/object:Gem::Version
82
+ version: '5.11'
83
+ - !ruby/object:Gem::Dependency
84
+ name: rake
85
+ requirement: !ruby/object:Gem::Requirement
86
+ requirements:
87
+ - - "~>"
88
+ - !ruby/object:Gem::Version
89
+ version: '12.3'
90
+ type: :development
91
+ prerelease: false
92
+ version_requirements: !ruby/object:Gem::Requirement
93
+ requirements:
94
+ - - "~>"
95
+ - !ruby/object:Gem::Version
96
+ version: '12.3'
97
+ description: AnyCable-compatible Ruby Rack middleware
98
+ email: yulia.oletskaya@gmail.com
99
+ executables: []
100
+ extensions: []
101
+ extra_rdoc_files: []
102
+ files:
103
+ - LICENSE
104
+ - README.md
105
+ - lib/anycable-rack-server.rb
106
+ - lib/anycable/rack-server.rb
107
+ - lib/anycable/rack-server/broadcast_subscribers/redis_subscriber.rb
108
+ - lib/anycable/rack-server/coders/json.rb
109
+ - lib/anycable/rack-server/connection.rb
110
+ - lib/anycable/rack-server/errors.rb
111
+ - lib/anycable/rack-server/hub.rb
112
+ - lib/anycable/rack-server/logging.rb
113
+ - lib/anycable/rack-server/middleware.rb
114
+ - lib/anycable/rack-server/pinger.rb
115
+ - lib/anycable/rack-server/rpc/client.rb
116
+ - lib/anycable/rack-server/rpc/rpc.proto
117
+ - lib/anycable/rack-server/socket.rb
118
+ - lib/anycable/rack-server/version.rb
119
+ homepage:
120
+ licenses:
121
+ - MIT
122
+ metadata: {}
123
+ post_install_message:
124
+ rdoc_options: []
125
+ require_paths:
126
+ - lib
127
+ required_ruby_version: !ruby/object:Gem::Requirement
128
+ requirements:
129
+ - - ">="
130
+ - !ruby/object:Gem::Version
131
+ version: '0'
132
+ required_rubygems_version: !ruby/object:Gem::Requirement
133
+ requirements:
134
+ - - ">="
135
+ - !ruby/object:Gem::Version
136
+ version: '0'
137
+ requirements: []
138
+ rubyforge_project:
139
+ rubygems_version: 2.7.6
140
+ signing_key:
141
+ specification_version: 4
142
+ summary: Anycable Rack Server
143
+ test_files: []