tightrope 0.1.1
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/lib/tightrope.rb +7 -0
- data/lib/tightrope/errors.rb +12 -0
- data/lib/tightrope/version.rb +3 -0
- data/lib/tightrope/websocket/action_cable_auth_client.rb +50 -0
- data/lib/tightrope/websocket/action_cable_client.rb +148 -0
- data/lib/tightrope/websocket/filter_client.rb +87 -0
- metadata +135 -0
checksums.yaml
ADDED
@@ -0,0 +1,7 @@
|
|
1
|
+
---
|
2
|
+
SHA256:
|
3
|
+
metadata.gz: 15a7366d3564b2186c12e87fd4b1f23c3fb92f288ea868e3d39cb21d97a41874
|
4
|
+
data.tar.gz: 219c19c58e24e601c4754cfdb782133156bbe947808612bc4a7eb9f5c0a13be9
|
5
|
+
SHA512:
|
6
|
+
metadata.gz: e70a010a40b5c1d1bdb0a0c66487acd72b650724a144e1dc32f71405a9c698f6bc8bdabebe6eab3b4101dfd12bc5dc7a08e47cc0a96ba3eb126d48981d263158
|
7
|
+
data.tar.gz: 420fbb1d5d74e583a9169b6d33e5ddcdd36f5b255feef928b76b2368c4dbe7ca7d5c8b874449eea93b1ea9ab7c033a860e4fbbe3cbe3cc234ae792a2b1907917
|
data/lib/tightrope.rb
ADDED
@@ -0,0 +1,7 @@
|
|
1
|
+
require "websocket-eventmachine-client"
|
2
|
+
|
3
|
+
require_relative 'tightrope/version'
|
4
|
+
require_relative 'tightrope/errors'
|
5
|
+
require_relative 'tightrope/websocket/filter_client'
|
6
|
+
require_relative 'tightrope/websocket/action_cable_client'
|
7
|
+
require_relative 'tightrope/websocket/action_cable_auth_client'
|
@@ -0,0 +1,12 @@
|
|
1
|
+
module Tightrope
|
2
|
+
ERROR_OK = 0
|
3
|
+
ERROR_PARAM_CHANNEL_MISSING = 1
|
4
|
+
ERROR_PARAM_MESSAGE_MISSING = 2
|
5
|
+
ERROR_PARAM_ACTION_MISSING = 3
|
6
|
+
ERROR_CHANNEL_ALREADY_SUBSCRIBED = 4
|
7
|
+
ERROR_CLIENT_HAS_NOTSUBSCRIBED = 5
|
8
|
+
ERROR_PARAM_CHANNEL_NOT_UNIQUE = 6
|
9
|
+
|
10
|
+
class LoginError < Exception
|
11
|
+
end
|
12
|
+
end
|
@@ -0,0 +1,50 @@
|
|
1
|
+
require 'http'
|
2
|
+
|
3
|
+
module Tightrope
|
4
|
+
module WebSocket
|
5
|
+
class ActionCableAuthClient < ::Tightrope::WebSocket::ActionCableClient
|
6
|
+
def self.connect(args)
|
7
|
+
local_args = prepare_args(args)
|
8
|
+
uri = URI.parse(local_args[:uri])
|
9
|
+
|
10
|
+
host = uri.host
|
11
|
+
port = uri.port
|
12
|
+
|
13
|
+
if args[:auth_uri]
|
14
|
+
auth_uri = URI.parse(local_args[:auth_uri])
|
15
|
+
host = auth_uri.host
|
16
|
+
port = auth_uri.port
|
17
|
+
local_args[:ssl] = true if auth_uri.scheme == 'https'
|
18
|
+
else
|
19
|
+
auth_path = local_args[:auth_path].sub(/^\//, '') if local_args[:auth_path]
|
20
|
+
local_args[:auth_uri] = (args[:ssl] ? 'https' : 'http') + "://#{host}"
|
21
|
+
local_args[:auth_uri] << ":#{port}" if port
|
22
|
+
local_args[:auth_uri] << "/#{auth_path}" if auth_path
|
23
|
+
end
|
24
|
+
|
25
|
+
local_args.delete(:auth_path)
|
26
|
+
|
27
|
+
check_arg(local_args, :user)
|
28
|
+
check_arg(local_args, :password)
|
29
|
+
local_args[:headers] = login(local_args)
|
30
|
+
|
31
|
+
super(local_args)
|
32
|
+
end
|
33
|
+
|
34
|
+
def self.login(args)
|
35
|
+
res = HTTP.post(args[:auth_uri], json: {email: args[:user], password: args[:password]})
|
36
|
+
raise Tightrope::LoginError.new("Login failed for user #{args[:user]}") if res.code != 200
|
37
|
+
|
38
|
+
{'access-token' => res.headers['access-token'],
|
39
|
+
'expiry' => res.headers['expiry'],
|
40
|
+
'uid' => res.headers['uid'],
|
41
|
+
'client' => res.headers['client'],
|
42
|
+
'token-type' => 'Bearer'}
|
43
|
+
end
|
44
|
+
|
45
|
+
def self.check_arg(args, key)
|
46
|
+
raise ArgumentError.new("Argument ':#{key.to_s}' is missing") unless args[key]
|
47
|
+
end
|
48
|
+
end
|
49
|
+
end
|
50
|
+
end
|
@@ -0,0 +1,148 @@
|
|
1
|
+
require 'cgi'
|
2
|
+
require 'json'
|
3
|
+
|
4
|
+
module Tightrope
|
5
|
+
module WebSocket
|
6
|
+
class ActionCableClient < Tightrope::WebSocket::FilterClient
|
7
|
+
attr_reader :channel
|
8
|
+
|
9
|
+
def initialize(args)
|
10
|
+
@channel = args[:channel] if args[:channel]
|
11
|
+
@channel_subscribed = false
|
12
|
+
super(args)
|
13
|
+
end
|
14
|
+
|
15
|
+
def channel_subscribed?
|
16
|
+
@channel_subscribed
|
17
|
+
end
|
18
|
+
|
19
|
+
# Connect to an ActionCable websocket server
|
20
|
+
# @param args [Hash] The request arguments
|
21
|
+
# @option args [String] :host The host IP/DNS name
|
22
|
+
# @option args [Integer] :port The port to connect too(default = 80)
|
23
|
+
# @option args [String] :uri Full URI for server(optional - use instead of host/port combination)
|
24
|
+
# @option args [Integer] :version Version of protocol to use(default = 13)
|
25
|
+
# @option args [Hash] :headers HTTP headers to use in the handshake
|
26
|
+
# @option args [Boolean] :ssl Force SSL/TLS connection
|
27
|
+
def self.connect(args = {})
|
28
|
+
super(prepare_args(args))
|
29
|
+
end
|
30
|
+
|
31
|
+
def self.prepare_args(args = {})
|
32
|
+
host = nil
|
33
|
+
port = nil
|
34
|
+
cable_path = nil
|
35
|
+
query = nil
|
36
|
+
|
37
|
+
if args[:uri]
|
38
|
+
uri = URI.parse(args[:uri])
|
39
|
+
host = uri.host
|
40
|
+
port = uri.port
|
41
|
+
args[:ssl] = true if uri.scheme == 'wss'
|
42
|
+
else
|
43
|
+
host = args[:host] if args[:host]
|
44
|
+
port = args[:port] if args[:port]
|
45
|
+
cable_path = args[:cable_path].sub(/^\//, '') if args[:cable_path]
|
46
|
+
|
47
|
+
args[:uri] = (args[:ssl] ? 'wss' : 'ws') + '://' + host
|
48
|
+
args[:uri] << ":#{port}" if port
|
49
|
+
args[:uri] << "/#{cable_path}" if cable_path
|
50
|
+
if args[:query]
|
51
|
+
args[:uri] << '?'
|
52
|
+
args[:query].each {|k,v| args[:uri] << ::CGI::escape(k.to_s) + '=' + ::CGI::escape(v.to_s) + '&'}
|
53
|
+
args[:uri].sub!(/&$/, '')
|
54
|
+
end
|
55
|
+
|
56
|
+
[:host, :port, :cable_path, :query].each {|k| args.delete(k) }
|
57
|
+
end
|
58
|
+
|
59
|
+
args
|
60
|
+
end
|
61
|
+
|
62
|
+
def subscribe_to_channel(channel = nil, options = {})
|
63
|
+
if channel_subscribed?
|
64
|
+
error(__method__, Tightrope::ERROR_CHANNEL_ALREADY_SUBSCRIBED)
|
65
|
+
return false
|
66
|
+
end
|
67
|
+
|
68
|
+
if @channel.nil? && channel.nil?
|
69
|
+
error(__method__, Tightrope::ERROR_PARAM_CHANNEL_MISSING)
|
70
|
+
return false
|
71
|
+
end
|
72
|
+
|
73
|
+
if @channel && channel && @channel != channel
|
74
|
+
error(__method__, Tightrope::ERROR_PARAM_CHANNEL_NOT_UNIQUE)
|
75
|
+
return false
|
76
|
+
end
|
77
|
+
|
78
|
+
@channel = channel if @channel.nil? && channel
|
79
|
+
|
80
|
+
send_message(command: 'subscribe', channel: @channel, opts: options)
|
81
|
+
end
|
82
|
+
|
83
|
+
def send_message(options = {})
|
84
|
+
channel = options[:channel]
|
85
|
+
unless channel
|
86
|
+
error(__method__, Tightrope::ERROR_PARAM_CHANNEL_MISSING)
|
87
|
+
return false
|
88
|
+
end
|
89
|
+
|
90
|
+
identifier = {channel: channel}
|
91
|
+
identifier.merge!(options[:opts]) if options[:opts]
|
92
|
+
msg = {command: options[:command] || 'message', identifier: identifier.to_json}
|
93
|
+
|
94
|
+
if options[:message] || options[:action]
|
95
|
+
unless options[:message]
|
96
|
+
error(__method__, Tightrope::ERROR_PARAM_MESSAGE_MISSING)
|
97
|
+
return false
|
98
|
+
end
|
99
|
+
unless options[:action]
|
100
|
+
error(__method__, Tightrope::ERROR_PARAM_ACTION_MISSING)
|
101
|
+
return false
|
102
|
+
end
|
103
|
+
msg[:data] = {message: options[:message], action: options[:action]}.to_json
|
104
|
+
end
|
105
|
+
|
106
|
+
send(msg.to_json, type: options['type'] || 'text')
|
107
|
+
end
|
108
|
+
|
109
|
+
def send_to_channel(options = {})
|
110
|
+
unless channel_subscribed?
|
111
|
+
error(__method__, Tightrope::ERROR_CLIENT_HAS_NOTSUBSCRIBED)
|
112
|
+
return false
|
113
|
+
end
|
114
|
+
|
115
|
+
send_message(options.merge(channel: @channel))
|
116
|
+
end
|
117
|
+
|
118
|
+
def trigger_onsubscription(channel)
|
119
|
+
@onsubscription.call(channel) if @onsubscription
|
120
|
+
end
|
121
|
+
|
122
|
+
#
|
123
|
+
# is called when the subscription confirmation is sent
|
124
|
+
#
|
125
|
+
def onsubscription(&blk)
|
126
|
+
@onsubscription = blk
|
127
|
+
end
|
128
|
+
|
129
|
+
#
|
130
|
+
# triggers event 'onsubscribtion' when subscription confirmation is sent
|
131
|
+
#
|
132
|
+
def filter_text(frame)
|
133
|
+
unless channel_subscribed?
|
134
|
+
confirmation = JSON.parse(frame.to_s)
|
135
|
+
if confirmation['type'] == 'confirm_subscription'
|
136
|
+
identifier = JSON.parse(confirmation['identifier'])
|
137
|
+
@channel_subscribed = @channel && identifier['channel'] == @channel
|
138
|
+
trigger_onsubscription(identifier['channel']) if @channel_subscribed
|
139
|
+
return @channel_subscribed
|
140
|
+
end
|
141
|
+
end
|
142
|
+
false
|
143
|
+
end
|
144
|
+
|
145
|
+
end
|
146
|
+
end # ActionCableClient
|
147
|
+
|
148
|
+
end
|
@@ -0,0 +1,87 @@
|
|
1
|
+
require 'json'
|
2
|
+
|
3
|
+
module Tightrope
|
4
|
+
module WebSocket
|
5
|
+
# https://tools.ietf.org/html/rfc6455
|
6
|
+
|
7
|
+
class FilterClient < ::WebSocket::EventMachine::Client
|
8
|
+
#
|
9
|
+
# Websocket client for filtering frames send by websocket events.
|
10
|
+
# You can customize the methods
|
11
|
+
# filter_text, filter_binary, filter_ping, filter_pong and filter_close
|
12
|
+
#
|
13
|
+
def initialize(args)
|
14
|
+
create_methods
|
15
|
+
super(args)
|
16
|
+
end
|
17
|
+
|
18
|
+
[:text, :binary, :ping, :pong, :close].each do |type|
|
19
|
+
define_method("filter_#{type.to_s}") { |arg| false }
|
20
|
+
end
|
21
|
+
|
22
|
+
def args
|
23
|
+
instance_variable_get("@args")
|
24
|
+
end
|
25
|
+
|
26
|
+
def error(method, code, message = '')
|
27
|
+
e = {method: method, code: code}
|
28
|
+
e[:message] = message if message && message != ''
|
29
|
+
trigger_onerror(e.to_json)
|
30
|
+
end
|
31
|
+
|
32
|
+
private
|
33
|
+
|
34
|
+
def create_method(name, &block)
|
35
|
+
self.class.send(:define_method, name, &block)
|
36
|
+
end
|
37
|
+
|
38
|
+
def create_methods
|
39
|
+
[:onopen, :onping, :onpong, :onerror, :onmessage, :onclose].each do |event|
|
40
|
+
create_method("trigger_#{event}".to_sym) do |data,type=nil|
|
41
|
+
ivar = instance_variable_get("@#{event}")
|
42
|
+
ivar.call(data,type) if ivar
|
43
|
+
end
|
44
|
+
end
|
45
|
+
|
46
|
+
create_method(:handle_open) do |data|
|
47
|
+
frame_super = instance_variable_get("@frame")
|
48
|
+
frame_super << data
|
49
|
+
instance_variable_set("@frame", frame_super)
|
50
|
+
while frame = frame_super.next
|
51
|
+
state = instance_variable_get("@state")
|
52
|
+
if state == :open
|
53
|
+
case frame.type
|
54
|
+
when :close
|
55
|
+
state = :closing
|
56
|
+
instance_variable_set("@state", :closing)
|
57
|
+
close
|
58
|
+
trigger_onclose(frame.code, frame.data) unless filter_close(frame)
|
59
|
+
when :ping
|
60
|
+
pong(frame.to_s)
|
61
|
+
trigger_onping(frame.to_s) unless filter_ping(frame)
|
62
|
+
when :pong
|
63
|
+
trigger_onpong(frame.to_s) unless filter_pong(frame)
|
64
|
+
when :text
|
65
|
+
trigger_onmessage(frame.to_s, :text) unless filter_text(frame)
|
66
|
+
when :binary
|
67
|
+
trigger_onmessage(frame.to_s, :binary) unless filter_binary(frame)
|
68
|
+
end
|
69
|
+
else
|
70
|
+
break
|
71
|
+
end
|
72
|
+
end
|
73
|
+
|
74
|
+
if frame_super.error?
|
75
|
+
error_code = case error
|
76
|
+
when :invalid_payload_encoding then 1007
|
77
|
+
else 1002
|
78
|
+
end
|
79
|
+
trigger_onerror(error.to_s)
|
80
|
+
close(error_code)
|
81
|
+
unbind
|
82
|
+
end
|
83
|
+
end
|
84
|
+
end
|
85
|
+
end
|
86
|
+
end
|
87
|
+
end
|
metadata
ADDED
@@ -0,0 +1,135 @@
|
|
1
|
+
--- !ruby/object:Gem::Specification
|
2
|
+
name: tightrope
|
3
|
+
version: !ruby/object:Gem::Version
|
4
|
+
version: 0.1.1
|
5
|
+
platform: ruby
|
6
|
+
authors:
|
7
|
+
- Andreas Leipelt
|
8
|
+
autorequire:
|
9
|
+
bindir: exe
|
10
|
+
cert_chain: []
|
11
|
+
date: 2018-04-27 00:00:00.000000000 Z
|
12
|
+
dependencies:
|
13
|
+
- !ruby/object:Gem::Dependency
|
14
|
+
name: bundler
|
15
|
+
requirement: !ruby/object:Gem::Requirement
|
16
|
+
requirements:
|
17
|
+
- - "~>"
|
18
|
+
- !ruby/object:Gem::Version
|
19
|
+
version: '1.16'
|
20
|
+
type: :development
|
21
|
+
prerelease: false
|
22
|
+
version_requirements: !ruby/object:Gem::Requirement
|
23
|
+
requirements:
|
24
|
+
- - "~>"
|
25
|
+
- !ruby/object:Gem::Version
|
26
|
+
version: '1.16'
|
27
|
+
- !ruby/object:Gem::Dependency
|
28
|
+
name: rake
|
29
|
+
requirement: !ruby/object:Gem::Requirement
|
30
|
+
requirements:
|
31
|
+
- - "~>"
|
32
|
+
- !ruby/object:Gem::Version
|
33
|
+
version: '10.0'
|
34
|
+
type: :development
|
35
|
+
prerelease: false
|
36
|
+
version_requirements: !ruby/object:Gem::Requirement
|
37
|
+
requirements:
|
38
|
+
- - "~>"
|
39
|
+
- !ruby/object:Gem::Version
|
40
|
+
version: '10.0'
|
41
|
+
- !ruby/object:Gem::Dependency
|
42
|
+
name: rspec
|
43
|
+
requirement: !ruby/object:Gem::Requirement
|
44
|
+
requirements:
|
45
|
+
- - "~>"
|
46
|
+
- !ruby/object:Gem::Version
|
47
|
+
version: '3.0'
|
48
|
+
type: :development
|
49
|
+
prerelease: false
|
50
|
+
version_requirements: !ruby/object:Gem::Requirement
|
51
|
+
requirements:
|
52
|
+
- - "~>"
|
53
|
+
- !ruby/object:Gem::Version
|
54
|
+
version: '3.0'
|
55
|
+
- !ruby/object:Gem::Dependency
|
56
|
+
name: websocket-eventmachine-client
|
57
|
+
requirement: !ruby/object:Gem::Requirement
|
58
|
+
requirements:
|
59
|
+
- - "~>"
|
60
|
+
- !ruby/object:Gem::Version
|
61
|
+
version: '1.2'
|
62
|
+
type: :runtime
|
63
|
+
prerelease: false
|
64
|
+
version_requirements: !ruby/object:Gem::Requirement
|
65
|
+
requirements:
|
66
|
+
- - "~>"
|
67
|
+
- !ruby/object:Gem::Version
|
68
|
+
version: '1.2'
|
69
|
+
- !ruby/object:Gem::Dependency
|
70
|
+
name: http
|
71
|
+
requirement: !ruby/object:Gem::Requirement
|
72
|
+
requirements:
|
73
|
+
- - "~>"
|
74
|
+
- !ruby/object:Gem::Version
|
75
|
+
version: '3.0'
|
76
|
+
type: :runtime
|
77
|
+
prerelease: false
|
78
|
+
version_requirements: !ruby/object:Gem::Requirement
|
79
|
+
requirements:
|
80
|
+
- - "~>"
|
81
|
+
- !ruby/object:Gem::Version
|
82
|
+
version: '3.0'
|
83
|
+
- !ruby/object:Gem::Dependency
|
84
|
+
name: json
|
85
|
+
requirement: !ruby/object:Gem::Requirement
|
86
|
+
requirements:
|
87
|
+
- - ">="
|
88
|
+
- !ruby/object:Gem::Version
|
89
|
+
version: '0'
|
90
|
+
type: :runtime
|
91
|
+
prerelease: false
|
92
|
+
version_requirements: !ruby/object:Gem::Requirement
|
93
|
+
requirements:
|
94
|
+
- - ">="
|
95
|
+
- !ruby/object:Gem::Version
|
96
|
+
version: '0'
|
97
|
+
description: tigthrope
|
98
|
+
email:
|
99
|
+
- andreas@leipelt-hamburg.de
|
100
|
+
executables: []
|
101
|
+
extensions: []
|
102
|
+
extra_rdoc_files: []
|
103
|
+
files:
|
104
|
+
- lib/tightrope.rb
|
105
|
+
- lib/tightrope/errors.rb
|
106
|
+
- lib/tightrope/version.rb
|
107
|
+
- lib/tightrope/websocket/action_cable_auth_client.rb
|
108
|
+
- lib/tightrope/websocket/action_cable_client.rb
|
109
|
+
- lib/tightrope/websocket/filter_client.rb
|
110
|
+
homepage: https://github.com/briareos62/tightrope
|
111
|
+
licenses:
|
112
|
+
- MIT
|
113
|
+
metadata: {}
|
114
|
+
post_install_message:
|
115
|
+
rdoc_options: []
|
116
|
+
require_paths:
|
117
|
+
- lib
|
118
|
+
required_ruby_version: !ruby/object:Gem::Requirement
|
119
|
+
requirements:
|
120
|
+
- - ">="
|
121
|
+
- !ruby/object:Gem::Version
|
122
|
+
version: '0'
|
123
|
+
required_rubygems_version: !ruby/object:Gem::Requirement
|
124
|
+
requirements:
|
125
|
+
- - ">="
|
126
|
+
- !ruby/object:Gem::Version
|
127
|
+
version: '0'
|
128
|
+
requirements: []
|
129
|
+
rubyforge_project:
|
130
|
+
rubygems_version: 2.7.6
|
131
|
+
signing_key:
|
132
|
+
specification_version: 4
|
133
|
+
summary: Rails ActionCable client library for use in non browser clients. Based on
|
134
|
+
Eventmachine.
|
135
|
+
test_files: []
|