litecable 0.4.0
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +7 -0
- data/.gitignore +40 -0
- data/.rubocop.yml +63 -0
- data/.travis.yml +7 -0
- data/CHANGELOG.md +7 -0
- data/Gemfile +4 -0
- data/LICENSE.txt +21 -0
- data/README.md +128 -0
- data/Rakefile +6 -0
- data/bin/console +14 -0
- data/bin/setup +8 -0
- data/circle.yml +8 -0
- data/examples/sinatra/Gemfile +16 -0
- data/examples/sinatra/Procfile +3 -0
- data/examples/sinatra/README.md +33 -0
- data/examples/sinatra/anycable +18 -0
- data/examples/sinatra/app.rb +52 -0
- data/examples/sinatra/assets/app.css +169 -0
- data/examples/sinatra/assets/cable.js +584 -0
- data/examples/sinatra/assets/reset.css +223 -0
- data/examples/sinatra/bin/anycable-go +0 -0
- data/examples/sinatra/chat.rb +39 -0
- data/examples/sinatra/config.ru +28 -0
- data/examples/sinatra/views/index.slim +8 -0
- data/examples/sinatra/views/layout.slim +15 -0
- data/examples/sinatra/views/login.slim +8 -0
- data/examples/sinatra/views/resetcss.slim +224 -0
- data/examples/sinatra/views/room.slim +68 -0
- data/lib/lite_cable.rb +29 -0
- data/lib/lite_cable/anycable.rb +62 -0
- data/lib/lite_cable/channel.rb +8 -0
- data/lib/lite_cable/channel/base.rb +165 -0
- data/lib/lite_cable/channel/registry.rb +34 -0
- data/lib/lite_cable/channel/streams.rb +56 -0
- data/lib/lite_cable/coders.rb +7 -0
- data/lib/lite_cable/coders/json.rb +19 -0
- data/lib/lite_cable/coders/raw.rb +15 -0
- data/lib/lite_cable/config.rb +18 -0
- data/lib/lite_cable/connection.rb +10 -0
- data/lib/lite_cable/connection/authorization.rb +13 -0
- data/lib/lite_cable/connection/base.rb +131 -0
- data/lib/lite_cable/connection/identification.rb +88 -0
- data/lib/lite_cable/connection/streams.rb +28 -0
- data/lib/lite_cable/connection/subscriptions.rb +108 -0
- data/lib/lite_cable/internal.rb +13 -0
- data/lib/lite_cable/logging.rb +28 -0
- data/lib/lite_cable/server.rb +27 -0
- data/lib/lite_cable/server/client_socket.rb +9 -0
- data/lib/lite_cable/server/client_socket/base.rb +163 -0
- data/lib/lite_cable/server/client_socket/subscriptions.rb +23 -0
- data/lib/lite_cable/server/heart_beat.rb +50 -0
- data/lib/lite_cable/server/middleware.rb +55 -0
- data/lib/lite_cable/server/subscribers_map.rb +67 -0
- data/lib/lite_cable/server/websocket_ext/protocols.rb +45 -0
- data/lib/lite_cable/version.rb +4 -0
- data/lib/litecable.rb +2 -0
- data/litecable.gemspec +33 -0
- metadata +256 -0
@@ -0,0 +1,27 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
module LiteCable
|
3
|
+
# Rack middleware to hijack sockets.
|
4
|
+
#
|
5
|
+
# Uses thread-per-connection model (thus recommended only for development and test usage).
|
6
|
+
#
|
7
|
+
# Inspired by https://github.com/ngauthier/tubesock/blob/master/lib/tubesock.rb
|
8
|
+
module Server
|
9
|
+
require "websocket"
|
10
|
+
require "lite_cable/server/subscribers_map"
|
11
|
+
require "lite_cable/server/client_socket"
|
12
|
+
require "lite_cable/server/heart_beat"
|
13
|
+
require "lite_cable/server/middleware"
|
14
|
+
|
15
|
+
class << self
|
16
|
+
attr_accessor :subscribers_map
|
17
|
+
|
18
|
+
# Broadcast encoded message to the stream
|
19
|
+
def broadcast(stream, message, coder: nil)
|
20
|
+
coder ||= LiteCable.config.coder
|
21
|
+
subscribers_map.broadcast stream, message, coder
|
22
|
+
end
|
23
|
+
end
|
24
|
+
|
25
|
+
self.subscribers_map = SubscribersMap.new
|
26
|
+
end
|
27
|
+
end
|
@@ -0,0 +1,163 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
module LiteCable
|
3
|
+
module Server
|
4
|
+
module ClientSocket
|
5
|
+
# Wrapper over web socket
|
6
|
+
# rubocop:disable Metrics/ClassLength
|
7
|
+
class Base
|
8
|
+
include Logging
|
9
|
+
include Subscriptions
|
10
|
+
|
11
|
+
attr_reader :version, :active
|
12
|
+
|
13
|
+
def initialize(env, socket, version)
|
14
|
+
log(:debug, "WebSocket version #{version}")
|
15
|
+
@env = env
|
16
|
+
@socket = socket
|
17
|
+
@version = version
|
18
|
+
@active = true
|
19
|
+
|
20
|
+
@open_handlers = []
|
21
|
+
@message_handlers = []
|
22
|
+
@close_handlers = []
|
23
|
+
@error_handlers = []
|
24
|
+
|
25
|
+
@close_on_error = true
|
26
|
+
end
|
27
|
+
|
28
|
+
def prevent_close_on_error
|
29
|
+
@close_on_error = false
|
30
|
+
end
|
31
|
+
|
32
|
+
def transmit(data, type: :text)
|
33
|
+
frame = WebSocket::Frame::Outgoing::Server.new(
|
34
|
+
version: version,
|
35
|
+
data: data,
|
36
|
+
type: type
|
37
|
+
)
|
38
|
+
socket.write frame.to_s
|
39
|
+
rescue IOError, Errno::EPIPE, Errno::ETIMEDOUT => e
|
40
|
+
log(:error, "Socket send failed: #{e}")
|
41
|
+
close
|
42
|
+
end
|
43
|
+
|
44
|
+
def request
|
45
|
+
@request ||= Rack::Request.new(@env)
|
46
|
+
end
|
47
|
+
|
48
|
+
def onopen(&block)
|
49
|
+
@open_handlers << block
|
50
|
+
end
|
51
|
+
|
52
|
+
def onmessage(&block)
|
53
|
+
@message_handlers << block
|
54
|
+
end
|
55
|
+
|
56
|
+
def onclose(&block)
|
57
|
+
@close_handlers << block
|
58
|
+
end
|
59
|
+
|
60
|
+
def onerror(&block)
|
61
|
+
@error_handlers << block
|
62
|
+
end
|
63
|
+
|
64
|
+
def listen
|
65
|
+
keepalive
|
66
|
+
Thread.new do
|
67
|
+
Thread.current.abort_on_exception = true
|
68
|
+
begin
|
69
|
+
@open_handlers.each(&:call)
|
70
|
+
each_frame do |data|
|
71
|
+
@message_handlers.each do |h|
|
72
|
+
begin
|
73
|
+
h.call(data)
|
74
|
+
rescue => e
|
75
|
+
log(:error, "Socket receive failed: #{e}")
|
76
|
+
@error_handlers.each { |eh| eh.call(e, data) }
|
77
|
+
close if close_on_error
|
78
|
+
end
|
79
|
+
end
|
80
|
+
end
|
81
|
+
ensure
|
82
|
+
close
|
83
|
+
end
|
84
|
+
end
|
85
|
+
end
|
86
|
+
|
87
|
+
def close
|
88
|
+
return unless @active
|
89
|
+
|
90
|
+
@close_handlers.each(&:call)
|
91
|
+
close!
|
92
|
+
|
93
|
+
@active = false
|
94
|
+
end
|
95
|
+
|
96
|
+
def closed?
|
97
|
+
@socket.closed?
|
98
|
+
end
|
99
|
+
|
100
|
+
private
|
101
|
+
|
102
|
+
attr_reader :socket, :close_on_error
|
103
|
+
|
104
|
+
def close!
|
105
|
+
if @socket.respond_to?(:closed?)
|
106
|
+
close_socket unless @socket.closed?
|
107
|
+
else
|
108
|
+
close_socket
|
109
|
+
end
|
110
|
+
end
|
111
|
+
|
112
|
+
def close_socket
|
113
|
+
frame = WebSocket::Frame::Outgoing::Server.new(version: version, type: :close, code: 1000)
|
114
|
+
@socket.write(frame.to_s) if frame.supported?
|
115
|
+
@socket.close
|
116
|
+
end
|
117
|
+
|
118
|
+
def keepalive
|
119
|
+
thread = Thread.new do
|
120
|
+
Thread.current.abort_on_exception = true
|
121
|
+
loop do
|
122
|
+
sleep 5
|
123
|
+
transmit nil, type: :ping
|
124
|
+
end
|
125
|
+
end
|
126
|
+
|
127
|
+
onclose do
|
128
|
+
thread.kill
|
129
|
+
end
|
130
|
+
end
|
131
|
+
|
132
|
+
# rubocop:disable Metrics/AbcSize
|
133
|
+
# rubocop:disable Metrics/CyclomaticComplexity
|
134
|
+
# rubocop:disable Metrics/PerceivedComplexity
|
135
|
+
# rubocop:disable Metrics/MethodLength
|
136
|
+
def each_frame
|
137
|
+
framebuffer = WebSocket::Frame::Incoming::Server.new(version: version)
|
138
|
+
|
139
|
+
while IO.select([socket])
|
140
|
+
if socket.respond_to?(:recvfrom)
|
141
|
+
data, _addrinfo = socket.recvfrom(2000)
|
142
|
+
else
|
143
|
+
data, _addrinfo = socket.readpartial(2000), socket.peeraddr
|
144
|
+
end
|
145
|
+
break if data.empty?
|
146
|
+
framebuffer << data
|
147
|
+
while frame = framebuffer.next
|
148
|
+
case frame.type
|
149
|
+
when :close
|
150
|
+
return
|
151
|
+
when :text, :binary
|
152
|
+
yield frame.data
|
153
|
+
end
|
154
|
+
end
|
155
|
+
end
|
156
|
+
rescue Errno::EHOSTUNREACH, Errno::ETIMEDOUT, Errno::ECONNRESET, IOError, Errno::EBADF => e
|
157
|
+
log(:debug, "Socket frame error: #{e}")
|
158
|
+
nil # client disconnected or timed out
|
159
|
+
end
|
160
|
+
end
|
161
|
+
end
|
162
|
+
end
|
163
|
+
end
|
@@ -0,0 +1,23 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
module LiteCable
|
3
|
+
module Server
|
4
|
+
module ClientSocket
|
5
|
+
# Handle socket subscriptions
|
6
|
+
module Subscriptions
|
7
|
+
def subscribe(channel, broadcasting)
|
8
|
+
LiteCable::Server.subscribers_map
|
9
|
+
.add_subscriber(broadcasting, self, channel)
|
10
|
+
end
|
11
|
+
|
12
|
+
def unsubscribe(channel, broadcasting)
|
13
|
+
LiteCable::Server.subscribers_map
|
14
|
+
.remove_subscriber(broadcasting, self, channel)
|
15
|
+
end
|
16
|
+
|
17
|
+
def unsubscribe_from_all(channel)
|
18
|
+
LiteCable::Server.subscribers_map.remove_socket(self, channel)
|
19
|
+
end
|
20
|
+
end
|
21
|
+
end
|
22
|
+
end
|
23
|
+
end
|
@@ -0,0 +1,50 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
module LiteCable
|
3
|
+
module Server
|
4
|
+
# Sends pings to sockets
|
5
|
+
class HeartBeat
|
6
|
+
BEAT_INTERVAL = 3
|
7
|
+
|
8
|
+
def initialize
|
9
|
+
@sockets = []
|
10
|
+
run
|
11
|
+
end
|
12
|
+
|
13
|
+
def add(socket)
|
14
|
+
@sockets << socket
|
15
|
+
end
|
16
|
+
|
17
|
+
def remove(socket)
|
18
|
+
@sockets.delete(socket)
|
19
|
+
end
|
20
|
+
|
21
|
+
def stop
|
22
|
+
@stopped = true
|
23
|
+
end
|
24
|
+
|
25
|
+
def run
|
26
|
+
Thread.new do
|
27
|
+
Thread.current.abort_on_exception = true
|
28
|
+
loop do
|
29
|
+
break if @stopped
|
30
|
+
|
31
|
+
unless @sockets.empty?
|
32
|
+
msg = ping_message Time.now.to_i
|
33
|
+
@sockets.each do |socket|
|
34
|
+
socket.transmit msg
|
35
|
+
end
|
36
|
+
end
|
37
|
+
|
38
|
+
sleep BEAT_INTERVAL
|
39
|
+
end
|
40
|
+
end
|
41
|
+
end
|
42
|
+
|
43
|
+
private
|
44
|
+
|
45
|
+
def ping_message(time)
|
46
|
+
{ type: LiteCable::INTERNAL[:message_types][:ping], message: time }.to_json
|
47
|
+
end
|
48
|
+
end
|
49
|
+
end
|
50
|
+
end
|
@@ -0,0 +1,55 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
module LiteCable
|
3
|
+
module Server
|
4
|
+
require "lite_cable/server/websocket_ext/protocols"
|
5
|
+
# Rack middleware to hijack the socket
|
6
|
+
class Middleware
|
7
|
+
class HijackNotAvailable < RuntimeError; end
|
8
|
+
|
9
|
+
def initialize(_app, connection_class:)
|
10
|
+
@connection_class = connection_class
|
11
|
+
@heart_beat = HeartBeat.new
|
12
|
+
end
|
13
|
+
|
14
|
+
def call(env)
|
15
|
+
return [404, { 'Content-Type' => 'text/plain' }, ['Not Found']] unless
|
16
|
+
env["HTTP_UPGRADE"] == 'websocket'
|
17
|
+
|
18
|
+
raise HijackNotAvailable unless env['rack.hijack']
|
19
|
+
|
20
|
+
env['rack.hijack'].call
|
21
|
+
handshake = send_handshake(env)
|
22
|
+
|
23
|
+
socket = ClientSocket::Base.new env, env['rack.hijack_io'], handshake.version
|
24
|
+
init_connection socket
|
25
|
+
init_heartbeat socket
|
26
|
+
socket.listen
|
27
|
+
[-1, {}, []]
|
28
|
+
end
|
29
|
+
|
30
|
+
private
|
31
|
+
|
32
|
+
def send_handshake(env)
|
33
|
+
handshake = WebSocket::Handshake::Server.new
|
34
|
+
handshake.from_rack env
|
35
|
+
handshake.protocols LiteCable::INTERNAL[:protocols]
|
36
|
+
|
37
|
+
env['rack.hijack_io'].write handshake.to_s
|
38
|
+
handshake
|
39
|
+
end
|
40
|
+
|
41
|
+
def init_connection(socket)
|
42
|
+
connection = @connection_class.new(socket)
|
43
|
+
|
44
|
+
socket.onopen { connection.handle_open }
|
45
|
+
socket.onclose { connection.handle_close }
|
46
|
+
socket.onmessage { |data| connection.handle_command(data) }
|
47
|
+
end
|
48
|
+
|
49
|
+
def init_heartbeat(socket)
|
50
|
+
@heart_beat.add(socket)
|
51
|
+
socket.onclose { @heart_beat.remove(socket) }
|
52
|
+
end
|
53
|
+
end
|
54
|
+
end
|
55
|
+
end
|
@@ -0,0 +1,67 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
module LiteCable
|
3
|
+
module Server
|
4
|
+
# From https://github.com/rails/rails/blob/v5.0.1/actioncable/lib/action_cable/subscription_adapter/subscriber_map.rb
|
5
|
+
class SubscribersMap
|
6
|
+
attr_reader :streams, :sockets
|
7
|
+
|
8
|
+
def initialize
|
9
|
+
@streams = Hash.new do |streams, stream_id|
|
10
|
+
streams[stream_id] = Hash.new { |channels, channel_id| channels[channel_id] = [] }
|
11
|
+
end
|
12
|
+
@sockets = Hash.new { |h, k| h[k] = [] }
|
13
|
+
@sync = Mutex.new
|
14
|
+
end
|
15
|
+
|
16
|
+
def add_subscriber(stream, socket, channel)
|
17
|
+
@sync.synchronize do
|
18
|
+
@streams[stream][channel] << socket
|
19
|
+
@sockets[socket] << [channel, stream]
|
20
|
+
end
|
21
|
+
end
|
22
|
+
|
23
|
+
def remove_subscriber(stream, socket, channel)
|
24
|
+
@sync.synchronize do
|
25
|
+
@streams[stream][channel].delete(socket)
|
26
|
+
@sockets[socket].delete([channel, stream])
|
27
|
+
cleanup stream, socket, channel
|
28
|
+
end
|
29
|
+
end
|
30
|
+
|
31
|
+
def remove_socket(socket, channel)
|
32
|
+
list = @sync.synchronize do
|
33
|
+
return unless @sockets.key?(socket)
|
34
|
+
@sockets[socket].dup
|
35
|
+
end
|
36
|
+
|
37
|
+
list.each do |(channel_id, stream)|
|
38
|
+
remove_subscriber(stream, socket, channel) if channel == channel_id
|
39
|
+
end
|
40
|
+
end
|
41
|
+
|
42
|
+
def broadcast(stream, message, coder)
|
43
|
+
list = @sync.synchronize do
|
44
|
+
return unless @streams.key?(stream)
|
45
|
+
@streams[stream].to_a
|
46
|
+
end
|
47
|
+
|
48
|
+
list.each do |(channel_id, sockets)|
|
49
|
+
cmessage = channel_message(channel_id, message, coder)
|
50
|
+
sockets.each { |s| s.transmit cmessage }
|
51
|
+
end
|
52
|
+
end
|
53
|
+
|
54
|
+
private
|
55
|
+
|
56
|
+
def cleanup(stream, socket, channel)
|
57
|
+
@streams[stream].delete(channel) if @streams[stream][channel].empty?
|
58
|
+
@streams.delete(stream) if @streams[stream].empty?
|
59
|
+
@sockets.delete(socket) if @sockets[socket].empty?
|
60
|
+
end
|
61
|
+
|
62
|
+
def channel_message(channel_id, message, coder)
|
63
|
+
coder.encode(identifier: channel_id, message: message)
|
64
|
+
end
|
65
|
+
end
|
66
|
+
end
|
67
|
+
end
|
@@ -0,0 +1,45 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
# Add missing protocols support to websocket-ruby
|
3
|
+
module WebSocketExt
|
4
|
+
module Protocols # :nodoc:
|
5
|
+
module Handshake # :nodoc:
|
6
|
+
# Specify server protocols
|
7
|
+
def protocols(values)
|
8
|
+
@protocols = values
|
9
|
+
end
|
10
|
+
|
11
|
+
# Return matching protocol
|
12
|
+
def protocol
|
13
|
+
return @protocol if instance_variable_defined?(:@protocol)
|
14
|
+
protos = @headers['sec-websocket-protocol']
|
15
|
+
|
16
|
+
return @protocol = nil unless protos
|
17
|
+
@protocol = begin
|
18
|
+
protos = protos.split(/ *, */) if protos.is_a?(String)
|
19
|
+
protos.find { |p| @protocols.include?(p) }
|
20
|
+
end
|
21
|
+
end
|
22
|
+
end
|
23
|
+
|
24
|
+
module Handler # :nodoc:
|
25
|
+
def handshake_keys
|
26
|
+
return super unless @handshake.protocol
|
27
|
+
super + [
|
28
|
+
[
|
29
|
+
'Sec-WebSocket-Protocol',
|
30
|
+
@handshake.protocol
|
31
|
+
]
|
32
|
+
]
|
33
|
+
end
|
34
|
+
end
|
35
|
+
end
|
36
|
+
end
|
37
|
+
|
38
|
+
WebSocket::Handshake::Server.include WebSocketExt::Protocols::Handshake
|
39
|
+
[
|
40
|
+
WebSocket::Handshake::Handler::Server04,
|
41
|
+
WebSocket::Handshake::Handler::Server75,
|
42
|
+
WebSocket::Handshake::Handler::Server76
|
43
|
+
].each do |handler|
|
44
|
+
handler.prepend WebSocketExt::Protocols::Handler
|
45
|
+
end
|