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,15 @@
1
+ # frozen_string_literal: true
2
+ module LiteCable
3
+ module Coders
4
+ # No-op coder
5
+ module Raw
6
+ class << self
7
+ def decode(val)
8
+ val
9
+ end
10
+
11
+ alias encode decode
12
+ end
13
+ end
14
+ end
15
+ end
@@ -0,0 +1,18 @@
1
+ # frozen_string_literal: true
2
+ require "anyway"
3
+ require 'logger'
4
+
5
+ module LiteCable
6
+ # Anycable configuration
7
+ class Config < Anyway::Config
8
+ require "lite_cable/coders/json"
9
+ require "lite_cable/coders/raw"
10
+
11
+ config_name :litecable
12
+
13
+ attr_config :logger,
14
+ coder: Coders::JSON,
15
+ identifier_coder: Coders::Raw,
16
+ log_level: Logger::INFO
17
+ end
18
+ end
@@ -0,0 +1,10 @@
1
+ # frozen_string_literal: true
2
+ module LiteCable
3
+ module Connection # :nodoc:
4
+ require "lite_cable/connection/authorization"
5
+ require "lite_cable/connection/identification"
6
+ require "lite_cable/connection/base"
7
+ require "lite_cable/connection/streams"
8
+ require "lite_cable/connection/subscriptions"
9
+ end
10
+ end
@@ -0,0 +1,13 @@
1
+ # frozen_string_literal: true
2
+ module LiteCable
3
+ module Connection
4
+ class UnauthorizedError < StandardError; end
5
+
6
+ # Include methods to control authorization flow
7
+ module Authorization
8
+ def reject_unauthorized_connection
9
+ raise UnauthorizedError
10
+ end
11
+ end
12
+ end
13
+ end
@@ -0,0 +1,131 @@
1
+ # frozen_string_literal: true
2
+ module LiteCable
3
+ # rubocop:disable Metrics/LineLength
4
+ module Connection
5
+ # A Connection object represents a client "connected" to the application.
6
+ # It contains all of the channel subscriptions. Incoming messages are then routed to these channel subscriptions
7
+ # based on an identifier sent by the consumer.
8
+ # The Connection itself does not deal with any specific application logic beyond authentication and authorization.
9
+ #
10
+ # Here's a basic example:
11
+ #
12
+ # module MyApplication
13
+ # class Connection < LiteCable::Connection::Base
14
+ # identified_by :current_user
15
+ #
16
+ # def connect
17
+ # self.current_user = find_verified_user
18
+ # end
19
+ #
20
+ # def disconnect
21
+ # # Any cleanup work needed when the cable connection is cut.
22
+ # end
23
+ #
24
+ # private
25
+ # def find_verified_user
26
+ # User.find_by_identity(cookies[:identity]) ||
27
+ # reject_unauthorized_connection
28
+ # end
29
+ # end
30
+ # end
31
+ #
32
+ # First, we declare that this connection can be identified by its current_user. This allows us to later be able to find all connections
33
+ # established for that current_user (and potentially disconnect them). You can declare as many
34
+ # identification indexes as you like. Declaring an identification means that an attr_accessor is automatically set for that key.
35
+ #
36
+ # Second, we rely on the fact that the connection is established with the cookies from the domain being sent along. This makes
37
+ # it easy to use cookies that were set when logging in via a web interface to authorize the connection.
38
+ #
39
+ class Base
40
+ # rubocop:enable Metrics/LineLength
41
+ include Authorization
42
+ prepend Identification
43
+ include Logging
44
+
45
+ attr_reader :subscriptions, :streams, :coder
46
+
47
+ def initialize(socket, coder: nil)
48
+ @socket = socket
49
+ @coder = coder || LiteCable.config.coder
50
+
51
+ @subscriptions = Subscriptions.new(self)
52
+ @streams = Streams.new(socket)
53
+ end
54
+
55
+ def handle_open
56
+ connect if respond_to?(:connect)
57
+ send_welcome_message
58
+ log(:debug) { log_fmt("Opened") }
59
+ rescue UnauthorizedError
60
+ log(:debug) { log_fmt("Authorization failed") }
61
+ close
62
+ end
63
+
64
+ def handle_close
65
+ disconnected!
66
+ subscriptions.remove_all
67
+
68
+ disconnect if respond_to?(:disconnect)
69
+ log(:debug) { log_fmt("Closed") }
70
+ end
71
+
72
+ def handle_command(websocket_message)
73
+ command = decode(websocket_message)
74
+ subscriptions.execute_command command
75
+ rescue Subscriptions::Error, Channel::Error, Channel::Registry::Error => e
76
+ log(:error, log_fmt("Connection command failed: #{e}"))
77
+ close
78
+ end
79
+
80
+ def transmit(cable_message)
81
+ return if disconnected?
82
+ socket.transmit encode(cable_message)
83
+ end
84
+
85
+ def close
86
+ socket.close
87
+ end
88
+
89
+ # Rack::Request instance of underlying socket
90
+ def request
91
+ socket.request
92
+ end
93
+
94
+ # Request cookies
95
+ def cookies
96
+ request.cookies
97
+ end
98
+
99
+ def disconnected?
100
+ @_disconnected == true
101
+ end
102
+
103
+ private
104
+
105
+ attr_reader :socket
106
+
107
+ def disconnected!
108
+ @_disconnected = true
109
+ end
110
+
111
+ def send_welcome_message
112
+ # Send welcome message to the internal connection monitor channel.
113
+ # This ensures the connection monitor state is reset after a successful
114
+ # websocket connection.
115
+ transmit type: LiteCable::INTERNAL[:message_types][:welcome]
116
+ end
117
+
118
+ def encode(cable_message)
119
+ coder.encode cable_message
120
+ end
121
+
122
+ def decode(websocket_message)
123
+ coder.decode websocket_message
124
+ end
125
+
126
+ def log_fmt(msg)
127
+ "[connection:#{identifier}] #{msg}"
128
+ end
129
+ end
130
+ end
131
+ end
@@ -0,0 +1,88 @@
1
+ # frozen_string_literal: true
2
+ require "set"
3
+
4
+ module LiteCable
5
+ module Connection
6
+ module Identification # :nodoc:
7
+ module ClassMethods # :nodoc:
8
+ # Mark a key as being a connection identifier index
9
+ # that can then be used to find the specific connection again later.
10
+ def identified_by(*identifiers)
11
+ Array(identifiers).each do |identifier|
12
+ attr_writer identifier
13
+ define_method(identifier) do
14
+ return instance_variable_get(:"@#{identifier}") if
15
+ instance_variable_defined?(:"@#{identifier}")
16
+ fetch_identifier(identifier.to_s)
17
+ end
18
+ end
19
+
20
+ self.identifiers += identifiers
21
+ end
22
+ end
23
+
24
+ def self.prepended(base)
25
+ base.class_eval do
26
+ class << self
27
+ attr_writer :identifiers
28
+
29
+ def identifiers
30
+ @identifiers ||= Set.new
31
+ end
32
+
33
+ include ClassMethods
34
+ end
35
+ end
36
+ end
37
+
38
+ def initialize(socket, identifiers: nil, **hargs)
39
+ @encoded_ids = identifiers ? JSON.parse(identifiers) : {}
40
+ super socket, **hargs
41
+ end
42
+
43
+ def identifiers
44
+ self.class.identifiers
45
+ end
46
+
47
+ # Return a single connection identifier
48
+ # that combines the value of all the registered identifiers into a single id.
49
+ #
50
+ # You can specify a custom identifier_coder in config
51
+ # to implement specific logic of encoding/decoding
52
+ # custom classes to identifiers.
53
+ #
54
+ # By default uses Raw coder.
55
+ def identifier
56
+ unless defined? @identifier
57
+ values = identifiers_hash.values.compact
58
+ @identifier = values.empty? ? nil : values.map(&:to_s).sort.join(":")
59
+ end
60
+
61
+ @identifier
62
+ end
63
+
64
+ # Generate identifiers info as hash.
65
+ def identifiers_hash
66
+ identifiers.each_with_object({}) do |id, acc|
67
+ obj = instance_variable_get("@#{id}")
68
+ next unless obj
69
+ acc[id.to_s] = LiteCable.config.identifier_coder.encode(obj)
70
+ end
71
+ end
72
+
73
+ def identifiers_json
74
+ identifiers_hash.to_json
75
+ end
76
+
77
+ # Fetch identifier and deserialize if neccessary
78
+ def fetch_identifier(name)
79
+ val = @encoded_ids[name]
80
+ val = LiteCable.config.identifier_coder.decode(val) unless val.nil?
81
+ instance_variable_set(
82
+ :"@#{name}",
83
+ val
84
+ )
85
+ end
86
+ end
87
+ end
88
+ end
@@ -0,0 +1,28 @@
1
+ # frozen_string_literal: true
2
+ module LiteCable
3
+ module Connection
4
+ # Manage the connection streams
5
+ class Streams
6
+ attr_reader :socket
7
+
8
+ def initialize(socket)
9
+ @socket = socket
10
+ end
11
+
12
+ # Start streaming from broadcasting to the channel.
13
+ def add(channel_id, broadcasting)
14
+ socket.subscribe(channel_id, broadcasting)
15
+ end
16
+
17
+ # Stop streaming from broadcasting to the channel.
18
+ def remove(channel_id, broadcasting)
19
+ socket.unsubscribe(channel_id, broadcasting)
20
+ end
21
+
22
+ # Stop all streams for the channel
23
+ def remove_all(channel_id)
24
+ socket.unsubscribe_from_all(channel_id)
25
+ end
26
+ end
27
+ end
28
+ end
@@ -0,0 +1,108 @@
1
+ # frozen_string_literal: true
2
+ module LiteCable
3
+ module Connection
4
+ # Manage the connection channels and route messages
5
+ class Subscriptions
6
+ class Error < StandardError; end
7
+ class AlreadySubscribedError < Error; end
8
+ class UnknownCommandError < Error; end
9
+ class ChannelNotFoundError < Error; end
10
+
11
+ include Logging
12
+
13
+ def initialize(connection)
14
+ @connection = connection
15
+ @subscriptions = {}
16
+ end
17
+
18
+ def identifiers
19
+ subscriptions.keys
20
+ end
21
+
22
+ def add(identifier, subscribe = true)
23
+ raise AlreadySubscribedError if find(identifier)
24
+
25
+ params = connection.coder.decode(identifier)
26
+
27
+ channel_id = params.delete("channel")
28
+
29
+ channel_class = Channel::Registry.find!(channel_id)
30
+
31
+ subscriptions[identifier] = channel_class.new(connection, identifier, params)
32
+ subscribe ? subscribe_channel(subscriptions[identifier]) : subscriptions[identifier]
33
+ end
34
+
35
+ def remove(identifier)
36
+ channel = find!(identifier)
37
+ subscriptions.delete(identifier)
38
+ channel.handle_unsubscribe
39
+ log(:debug) { log_fmt("Unsubscribed from channel #{channel.class.id}") }
40
+ transmit_subscription_cancel(channel.identifier)
41
+ end
42
+
43
+ def remove_all
44
+ subscriptions.keys.each(&method(:remove))
45
+ end
46
+
47
+ def perform_action(identifier, data)
48
+ channel = find!(identifier)
49
+ channel.handle_action data
50
+ end
51
+
52
+ def execute_command(data)
53
+ command = data.delete("command")
54
+ case command
55
+ when "subscribe" then add(data["identifier"])
56
+ when "unsubscribe" then remove(data["identifier"])
57
+ when "message" then perform_action(data["identifier"], data["data"])
58
+ else
59
+ raise UnknownCommandError
60
+ end
61
+ end
62
+
63
+ def find(identifier)
64
+ subscriptions[identifier]
65
+ end
66
+
67
+ def find!(identifier)
68
+ channel = find(identifier)
69
+ raise ChannelNotFoundError unless channel
70
+ channel
71
+ end
72
+
73
+ private
74
+
75
+ attr_reader :connection, :subscriptions
76
+
77
+ def subscribe_channel(channel)
78
+ channel.handle_subscribe
79
+ log(:debug) { log_fmt("Subscribed to channel #{channel.class.id}") }
80
+ transmit_subscription_confirmation(channel.identifier)
81
+ channel
82
+ rescue Channel::RejectedError
83
+ subscriptions.delete(channel.identifier)
84
+ transmit_subscription_rejection(channel.identifier)
85
+ nil
86
+ end
87
+
88
+ def transmit_subscription_confirmation(identifier)
89
+ connection.transmit identifier: identifier,
90
+ type: LiteCable::INTERNAL[:message_types][:confirmation]
91
+ end
92
+
93
+ def transmit_subscription_rejection(identifier)
94
+ connection.transmit identifier: identifier,
95
+ type: LiteCable::INTERNAL[:message_types][:rejection]
96
+ end
97
+
98
+ def transmit_subscription_cancel(identifier)
99
+ connection.transmit identifier: identifier,
100
+ type: LiteCable::INTERNAL[:message_types][:cancel]
101
+ end
102
+
103
+ def log_fmt(msg)
104
+ "[connection:#{connection.identifier}] #{msg}"
105
+ end
106
+ end
107
+ end
108
+ end
@@ -0,0 +1,13 @@
1
+ # frozen_string_literal: true
2
+ module LiteCable
3
+ INTERNAL = {
4
+ message_types: {
5
+ welcome: "welcome",
6
+ ping: "ping",
7
+ confirmation: "confirm_subscription",
8
+ rejection: "reject_subscription",
9
+ cancel: "cancel_subscription"
10
+ }.freeze,
11
+ protocols: ["actioncable-v1-json", "actioncable-unsupported"].freeze
12
+ }.freeze
13
+ end
@@ -0,0 +1,28 @@
1
+ # frozen_string_literal: true
2
+ require 'logger'
3
+
4
+ module LiteCable
5
+ module Logging # :nodoc:
6
+ PREFIX = "LiteCable"
7
+
8
+ class << self
9
+ def logger
10
+ return @logger if instance_variable_defined?(:@logger)
11
+
12
+ @logger = LiteCable.config.logger
13
+ return if @logger == false
14
+
15
+ @logger ||= ::Logger.new(STDERR).tap do |logger|
16
+ logger.level = LiteCable.config.log_level
17
+ end
18
+ end
19
+ end
20
+
21
+ private
22
+
23
+ def log(level, message = nil)
24
+ return unless LiteCable::Logging.logger
25
+ LiteCable::Logging.logger.send(level, PREFIX) { message || yield }
26
+ end
27
+ end
28
+ end