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.
- 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,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
|