litecable 0.4.0

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.
Files changed (58) hide show
  1. checksums.yaml +7 -0
  2. data/.gitignore +40 -0
  3. data/.rubocop.yml +63 -0
  4. data/.travis.yml +7 -0
  5. data/CHANGELOG.md +7 -0
  6. data/Gemfile +4 -0
  7. data/LICENSE.txt +21 -0
  8. data/README.md +128 -0
  9. data/Rakefile +6 -0
  10. data/bin/console +14 -0
  11. data/bin/setup +8 -0
  12. data/circle.yml +8 -0
  13. data/examples/sinatra/Gemfile +16 -0
  14. data/examples/sinatra/Procfile +3 -0
  15. data/examples/sinatra/README.md +33 -0
  16. data/examples/sinatra/anycable +18 -0
  17. data/examples/sinatra/app.rb +52 -0
  18. data/examples/sinatra/assets/app.css +169 -0
  19. data/examples/sinatra/assets/cable.js +584 -0
  20. data/examples/sinatra/assets/reset.css +223 -0
  21. data/examples/sinatra/bin/anycable-go +0 -0
  22. data/examples/sinatra/chat.rb +39 -0
  23. data/examples/sinatra/config.ru +28 -0
  24. data/examples/sinatra/views/index.slim +8 -0
  25. data/examples/sinatra/views/layout.slim +15 -0
  26. data/examples/sinatra/views/login.slim +8 -0
  27. data/examples/sinatra/views/resetcss.slim +224 -0
  28. data/examples/sinatra/views/room.slim +68 -0
  29. data/lib/lite_cable.rb +29 -0
  30. data/lib/lite_cable/anycable.rb +62 -0
  31. data/lib/lite_cable/channel.rb +8 -0
  32. data/lib/lite_cable/channel/base.rb +165 -0
  33. data/lib/lite_cable/channel/registry.rb +34 -0
  34. data/lib/lite_cable/channel/streams.rb +56 -0
  35. data/lib/lite_cable/coders.rb +7 -0
  36. data/lib/lite_cable/coders/json.rb +19 -0
  37. data/lib/lite_cable/coders/raw.rb +15 -0
  38. data/lib/lite_cable/config.rb +18 -0
  39. data/lib/lite_cable/connection.rb +10 -0
  40. data/lib/lite_cable/connection/authorization.rb +13 -0
  41. data/lib/lite_cable/connection/base.rb +131 -0
  42. data/lib/lite_cable/connection/identification.rb +88 -0
  43. data/lib/lite_cable/connection/streams.rb +28 -0
  44. data/lib/lite_cable/connection/subscriptions.rb +108 -0
  45. data/lib/lite_cable/internal.rb +13 -0
  46. data/lib/lite_cable/logging.rb +28 -0
  47. data/lib/lite_cable/server.rb +27 -0
  48. data/lib/lite_cable/server/client_socket.rb +9 -0
  49. data/lib/lite_cable/server/client_socket/base.rb +163 -0
  50. data/lib/lite_cable/server/client_socket/subscriptions.rb +23 -0
  51. data/lib/lite_cable/server/heart_beat.rb +50 -0
  52. data/lib/lite_cable/server/middleware.rb +55 -0
  53. data/lib/lite_cable/server/subscribers_map.rb +67 -0
  54. data/lib/lite_cable/server/websocket_ext/protocols.rb +45 -0
  55. data/lib/lite_cable/version.rb +4 -0
  56. data/lib/litecable.rb +2 -0
  57. data/litecable.gemspec +33 -0
  58. 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,9 @@
1
+ # frozen_string_literal: true
2
+ module LiteCable
3
+ module Server
4
+ module ClientSocket # :nodoc:
5
+ require "lite_cable/server/client_socket/subscriptions"
6
+ require "lite_cable/server/client_socket/base"
7
+ end
8
+ end
9
+ 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