litecable 0.4.0

Sign up to get free protection for your applications and to get access to all the features.
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