viaduct-webpush 1.0.3 → 1.0.4

Sign up to get free protection for your applications and to get access to all the features.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA1:
3
- metadata.gz: 5bd894fea39bb8ad5d5f82a3fbf241fa44f2d1b7
4
- data.tar.gz: 3cd260918918a5f47716bdf2fce2a0e7bcbfd26b
3
+ metadata.gz: 79f4047fad65b0886d126655ce6ed34a0570028f
4
+ data.tar.gz: e375862818d3424875da28e003686396902de8c3
5
5
  SHA512:
6
- metadata.gz: 305429ca69ea43580da39e84854414c1bdeae1d212f6d6933d696b15de29913b02b89584bd7822ec565d0900e8bf49b5982cf33e8e72402706e19bc90d676245
7
- data.tar.gz: 10d37a1a0e1743474b9dcc06d38e19cd4ec2a37d22fb20607c1d644f529578bba3d5ce03b057da3c73356af1e11cba600843921415487555efd54ad08fd55c21
6
+ metadata.gz: 6e0ed7ae59ed0895f75c92f1ec7c97a8c0d1ab404acc215ad042358ed7f3bc48a2a652af3148b814b2a7fb272928621a8fab3ae0e18612301975aaea2b961f1c
7
+ data.tar.gz: be83004eff45e0b50a89b6b696093c8c982051371ba8425872edef4ded2924fcb2d1c5571e817fecb8be11add2a8d5d0abc4a463970019d5f4eb5da82e912bb0
data/Gemfile ADDED
@@ -0,0 +1,3 @@
1
+ source 'https://rubygems.org'
2
+
3
+ gemspec
data/README.md CHANGED
@@ -14,12 +14,24 @@ gem 'viaduct-webpush', :require => 'viaduct/webpush'
14
14
 
15
15
  ## Usage
16
16
 
17
+ ### Configuration
18
+
17
19
  ```ruby
18
20
  require 'viaduct/webpush'
19
21
 
20
22
  Viaduct::WebPush.token = 'your-token'
21
23
  Viaduct::WebPush.secret = 'your-secret'
22
24
 
25
+ # Additional configuration for websockets
26
+ require 'viaduct/webpush/websocket'
27
+ Viaduct::WebPush::WebSocket.logger = Your.logger # optional
28
+ ```
29
+
30
+ ### Sending Messages via the HTTP API
31
+
32
+ Sending messages via the HTTP API is ideal if you're sending low-frequency messages and when you don't need to receive any messages in return.
33
+
34
+ ```ruby
23
35
  # Sending a single message
24
36
  Viaduct::WebPush['test-channel'].trigger('say-hello', {
25
37
  :name => 'Adam'
@@ -33,3 +45,51 @@ Viaduct::WebPush::Channel.multi_trigger(['channel1', 'channel2'], 'say-hello', {
33
45
  # Generating a signature for private channels
34
46
  Viaduct::WebPush::Channel.generate_signature(session_id, channel_name)
35
47
  ```
48
+
49
+ ### Sending and Receiving via the Websockets API
50
+
51
+ If you want to receive data from Viaduct WebPush, or you plan on sending messages very frequently, you'll want to use the websockets API.
52
+
53
+ ```ruby
54
+ # Ensure you've included the websockets classes
55
+ require 'viaduct/webpush/websocket'
56
+
57
+ # Connect to the VWP service
58
+ connection = Viaduct::WebPush::WebSocket.connection
59
+
60
+ # Subscribe to any channels you want to send/recieve on
61
+ channel1 = connection.subscribe('channel1')
62
+
63
+ # Bind a handler to a specific event that comes in on a channel
64
+ channel1.bind 'say-hello' do |received_data|
65
+ puts received_data # deserialized JSON
66
+ end
67
+
68
+ # Send messages out on a channel
69
+ channel1.trigger('say-hello', {
70
+ :name => "Adam"
71
+ })
72
+
73
+ # Disconnect from VWP when you're done
74
+ connection.disconnect
75
+
76
+ # Reconnect if you need to later
77
+ connection.connect
78
+ ```
79
+
80
+ You can also choose not authenticate if you only want to receive messages, or not to automatically connect if you want to set up all of your bindings before connecting.
81
+
82
+ ```ruby
83
+ # Do not authenticate, gives you a receive-only connection
84
+ connection = Viaduct::WebPush::WebSocket.connection(:authenticate => false)
85
+
86
+ # Do not automatically connect, set up your bindings first
87
+ connection = Viaduct::WebPush::WebSocket.connection(:autoconnect => false)
88
+
89
+ channel1 = connection.subscribe('channel1')
90
+ channel1.bind 'say-hello' do |received_data|
91
+ puts received_data
92
+ end
93
+
94
+ connection.connect
95
+ ```
data/Rakefile ADDED
@@ -0,0 +1 @@
1
+ require "bundler/gem_tasks"
@@ -1,9 +1,23 @@
1
+ # Class for sending messages to viaduct via the HTTP API
2
+
1
3
  module Viaduct
2
4
  module WebPush
3
5
  class Channel
4
6
 
5
- def initialize(name)
7
+ attr_accessor :subscribed, :name, :bindings
8
+
9
+ def initialize(name, connection)
6
10
  @name = name
11
+ @connection = connection
12
+ @bindings = Hash.new([])
13
+ @subscribed = false
14
+ end
15
+
16
+ #
17
+ # Blank name indicates global channel
18
+ #
19
+ def global?
20
+ @name.nil?
7
21
  end
8
22
 
9
23
  #
@@ -22,6 +36,31 @@ module Viaduct
22
36
  end
23
37
  end
24
38
 
39
+ #
40
+ # Bind some code to an incoming message on this channel
41
+ #
42
+ def bind(event, &block)
43
+ @bindings[event] += [block]
44
+ end
45
+
46
+ #
47
+ # Run the bindings for an event
48
+ #
49
+ def dispatch(event, data)
50
+ @bindings[event].each do |binding|
51
+ binding.call(data)
52
+ end
53
+ end
54
+
55
+ #
56
+ # Send request subscription for this channel from VWP
57
+ #
58
+ def register
59
+ return if @subscibed || global?
60
+ signature = self.class.generate_signature(@connection.session_id, @name)
61
+ @connection.register_subscription(@name, signature)
62
+ end
63
+
25
64
  #
26
65
  # Trigger a single even on a given channel
27
66
  #
@@ -29,7 +68,6 @@ module Viaduct
29
68
  WebPush.request('trigger', {:channel => channel, :event => event, :data => data.to_json})
30
69
  end
31
70
 
32
-
33
71
  #
34
72
  # Trigger an event on multiple channels simultaneously
35
73
  #
@@ -0,0 +1,80 @@
1
+ module Viaduct
2
+ module WebPush
3
+ module WebSocket
4
+ class Channel
5
+
6
+ attr_accessor :subscribed, :name, :bindings
7
+
8
+ def initialize(name, connection)
9
+ @name = name
10
+ @connection = connection
11
+ @bindings = Hash.new([])
12
+ @subscribed = false
13
+ end
14
+
15
+ #
16
+ # Blank name indicates global channel
17
+ #
18
+ def global?
19
+ @name.nil?
20
+ end
21
+
22
+ #
23
+ # Bind some code to an incoming message on this channel
24
+ #
25
+ def bind(event, &block)
26
+ @bindings[event] += [block]
27
+ end
28
+
29
+ #
30
+ # Run the bindings for an event
31
+ #
32
+ def dispatch(event, data)
33
+ @bindings[event].each do |binding|
34
+ binding.call(data)
35
+ end
36
+ end
37
+
38
+ #
39
+ # Send request subscription for this channel from VWP
40
+ #
41
+ def register
42
+ return if @subscibed || global?
43
+
44
+ if @name =~ /^private-/
45
+ signature = self.class.generate_signature(@connection.session_id, @name)
46
+ else
47
+ signature = nil
48
+ end
49
+
50
+ @connection.register_subscription(@name, signature)
51
+ end
52
+
53
+ #
54
+ # Trigger an event on this channel
55
+ #
56
+ def trigger(event, data = {})
57
+ @connection.trigger(@name, event, data)
58
+ end
59
+
60
+ def inspect
61
+ String.new.tap do |s|
62
+ s << "#<#{self.class.name}:#{self.object_id} "
63
+ s << [:name, :subscribed].map do |attrib|
64
+ "#{attrib}: #{self.send(attrib).inspect}"
65
+ end.join(', ')
66
+ s << ">"
67
+ end
68
+ end
69
+
70
+ #
71
+ # Generate a HMAC signature for private channels
72
+ #
73
+ def self.generate_signature(session_id, channel)
74
+ OpenSSL::HMAC.hexdigest(OpenSSL::Digest::SHA256.new, WebPush.secret, "#{session_id}:#{channel}")
75
+ end
76
+
77
+ end
78
+ end
79
+ end
80
+ end
@@ -0,0 +1,190 @@
1
+ module Viaduct
2
+ module WebPush
3
+ module WebSocket
4
+ class Connection
5
+
6
+ attr_reader :connected, :session_id, :channels, :authenticated
7
+
8
+ DEFAULT_OPTIONS = {
9
+ :authenticate => true,
10
+ :autoconnect => true
11
+ }
12
+
13
+ def initialize(options={})
14
+ @options = DEFAULT_OPTIONS.merge(options)
15
+
16
+ @connected = false
17
+ @authenticated = false
18
+ @session_id = nil
19
+ @channels = {}
20
+ @logger = WebSocket.logger
21
+
22
+ # Set up global vwp events
23
+ @global_channel = subscribe(nil)
24
+
25
+ # On connect, store data from the payload, send vwp:subscribe events
26
+ # for each channel that's already been addded
27
+ @global_channel.bind 'vwp:connected' do |data|
28
+ @logger.info "Connected to vwp"
29
+
30
+ @global_channel.subscribed = true
31
+ @session_id = data["session_id"]
32
+ @connected = true
33
+
34
+ register_subscriptions
35
+ end
36
+
37
+ # If we're sending messages we need to authenticate after we've connected to vwp
38
+ if @options[:authenticate]
39
+ @global_channel.bind 'vwp:connected' do
40
+ authenticate
41
+ end
42
+ @global_channel.bind 'vwp:authenticated' do
43
+ @logger.info "Authenticated with vwp"
44
+ @authenticated = true
45
+ end
46
+ end
47
+
48
+ # When we've successfully subscribed to a channel set the subscribed bit to true on the channel
49
+ @global_channel.bind 'vwp:subscribed' do |data|
50
+ channel_id = data['channel']
51
+ @channels[channel_id].subscribed = true
52
+ @logger.info "Subscribed to vwp #{data['channel_name']}"
53
+ end
54
+
55
+ connect if @options[:autoconnect]
56
+ end
57
+
58
+ def [](channel_name)
59
+ @channels[channel_name]
60
+ end
61
+
62
+ #
63
+ # Create a new channel object and send a vwp:subscribe event if we're
64
+ # already connected to vwp
65
+ #
66
+ def subscribe(channel_name)
67
+ @channels[channel_name] ||= Channel.new(channel_name, self)
68
+ @channels[channel_name].register if @connected
69
+ @channels[channel_name]
70
+ end
71
+
72
+ #
73
+ # Create a new thread and connect to vwp in it.
74
+ # Create an event loop for the thread which will dispatch incoming messages
75
+ #
76
+ def connect
77
+ return if @websocket
78
+ @logger.debug "Connecting..."
79
+
80
+ Thread.abort_on_exception = true
81
+ @websocket_thread = Thread.new do
82
+ @websocket = RawSocket.new
83
+ loop do
84
+ @websocket.receive.each do |message|
85
+ data = JSON.parse(message)
86
+ @logger.debug data
87
+ dispatch(data)
88
+ end
89
+ end
90
+ end
91
+
92
+ self
93
+ end
94
+
95
+ #
96
+ # Disconnect the websocket server and reset all variables, set all channels
97
+ # as unsubscribed
98
+ #
99
+ def disconnect
100
+ @websocket.disconnect
101
+ @websocket = nil
102
+ @websocket_thread.kill
103
+ @websocket_thread = nil
104
+ @connected = false
105
+ @session_id = nil
106
+ @channels.each {|name, chan| chan.subscribed = false }
107
+ @logger.info "Disconnected from vwp"
108
+ end
109
+
110
+ #
111
+ # Build a vwp message and send it via the websocket
112
+ #
113
+ def trigger(channel_name, event, data)
114
+ return false unless @authenticated
115
+
116
+ payload = JSON.generate({
117
+ "event" => "vwp:send",
118
+ "data" => {
119
+ "channel" => channel_name,
120
+ "event" => event,
121
+ "data" => data
122
+ }
123
+ })
124
+
125
+ @websocket.send_data(payload)
126
+ true
127
+ end
128
+
129
+ #
130
+ # Send a vwp:subscribe message via the websocket. The socket name and signature
131
+ # are calculated by the Channel
132
+ #
133
+ def register_subscription(channel_name, signature)
134
+ payload = JSON.generate({"event" => "vwp:subscribe", "data" => {
135
+ "channel" => channel_name,
136
+ "signature" => signature
137
+ }})
138
+ @websocket.send_data(payload)
139
+ end
140
+
141
+ def inspect
142
+ String.new.tap do |s|
143
+ s << "#<#{self.class.name}:#{self.object_id} "
144
+ s << [:connected, :session_id, :channels].map do |attrib|
145
+ "#{attrib}: #{self.send(attrib).inspect}"
146
+ end.join(', ')
147
+ s << ">"
148
+ end
149
+ end
150
+
151
+ protected
152
+
153
+ #
154
+ # Process some payload data and dispatch the message to the relevant
155
+ # channel. The channel will then dispatch the data to the correct binding.
156
+ #
157
+ def dispatch(payload_data)
158
+ event = payload_data['event']
159
+ channel = payload_data['channel']
160
+ data = payload_data['data']
161
+
162
+ if @channels[channel]
163
+ @channels[channel].dispatch(event, data)
164
+ end
165
+ end
166
+
167
+ # Send a vwp:subscribe message for every channel
168
+ def register_subscriptions
169
+ @channels.each do |name, chan|
170
+ chan.register
171
+ end
172
+ end
173
+
174
+ #
175
+ # Send a vwp:authenticate message that we need if we're going to be sending data
176
+ # on the websocket.
177
+ #
178
+ def authenticate
179
+ @logger.debug "Authenticating..."
180
+
181
+ payload = JSON.generate({"event" => "vwp:authenticate", "data" => {
182
+ "secret" => Viaduct::WebPush.secret
183
+ }})
184
+ @websocket.send_data(payload)
185
+ end
186
+
187
+ end
188
+ end
189
+ end
190
+ end
@@ -0,0 +1,77 @@
1
+ module Viaduct
2
+ module WebPush
3
+ module WebSocket
4
+ class RawSocket
5
+
6
+ WAIT_EXCEPTIONS = [Errno::EWOULDBLOCK, Errno::EAGAIN, IO::WaitReadable]
7
+
8
+ #
9
+ # Open an SSL connection and perform the HTTP upgrade/websockets handhsake procedure
10
+ #
11
+ def initialize
12
+ @handshake = ::WebSocket::Handshake::Client.new(:url => Viaduct::WebPush::WebSocket.endpoint)
13
+ @connection = TCPSocket.new(@handshake.host, @handshake.port || 443)
14
+
15
+ ssl_ctx = OpenSSL::SSL::SSLContext.new
16
+ ssl_ctx.verify_mode = OpenSSL::SSL::VERIFY_PEER
17
+ ssl_ctx.cert_store = OpenSSL::SSL::SSLContext::DEFAULT_CERT_STORE
18
+
19
+ ssl = OpenSSL::SSL::SSLSocket.new(@connection, ssl_ctx)
20
+ ssl.sync_close = true
21
+ ssl.connect
22
+
23
+ @connection = ssl
24
+
25
+ @connection.write @handshake.to_s
26
+ @connection.flush
27
+
28
+ while line = @connection.gets
29
+ @handshake << line
30
+ break if @handshake.finished?
31
+ end
32
+
33
+ raise HandshakeError unless @handshake.valid?
34
+ end
35
+
36
+ #
37
+ # Send a websocket frame out on the connection
38
+ #
39
+ def send_data(message, type=:text)
40
+ frame = ::WebSocket::Frame::Outgoing::Client.new(:version => @handshake.version, :data => message, :type => type)
41
+ @connection.write frame.to_s
42
+ @connection.flush
43
+ end
44
+
45
+ #
46
+ # Read data from the socket and wait with IO#select
47
+ #
48
+ def receive
49
+ frame = ::WebSocket::Frame::Incoming::Server.new(:version => @handshake.version)
50
+
51
+ begin
52
+ data = @connection.read_nonblock(1024)
53
+ rescue *WAIT_EXCEPTIONS
54
+ IO.select([@connection])
55
+ retry
56
+ end
57
+ frame << data
58
+
59
+ messages = []
60
+ while message = frame.next
61
+ messages << message.to_s
62
+ end
63
+
64
+ messages
65
+ end
66
+
67
+ #
68
+ # Close the connection
69
+ #
70
+ def disconnect
71
+ @connection.close
72
+ end
73
+ end
74
+
75
+ end
76
+ end
77
+ end
@@ -0,0 +1,40 @@
1
+ require 'logger'
2
+ require 'websocket'
3
+ require 'viaduct/web_push/web_socket/raw_socket'
4
+ require 'viaduct/web_push/web_socket/connection'
5
+ require 'viaduct/web_push/web_socket/channel'
6
+
7
+ module Viaduct
8
+ module WebPush
9
+ module WebSocket
10
+
11
+ class HandshakeError < StandardError; end
12
+
13
+ class << self
14
+
15
+ attr_writer :logger
16
+ attr_writer :endpoint
17
+
18
+ #
19
+ # Initialize a websocket connection for sending and receiving messages
20
+ #
21
+ def connection(options={})
22
+ @connection ||= Connection.new(options)
23
+ end
24
+
25
+ #
26
+ # Return the endpoint for the websocket server
27
+ #
28
+ def endpoint
29
+ @endpoint ||= "wss://#{Viaduct::WebPush.webpush_host}/vwp/socket/#{Viaduct::WebPush.token}"
30
+ end
31
+
32
+
33
+ def logger
34
+ @logger ||= Logger.new(STDOUT)
35
+ end
36
+
37
+ end
38
+ end
39
+ end
40
+ end
@@ -10,11 +10,18 @@ module Viaduct
10
10
 
11
11
  class << self
12
12
 
13
+ #
14
+ # Return the host for the VWP service
15
+ #
16
+ def webpush_host
17
+ @webpush_host ||= 'webpush.viaduct.io'
18
+ end
19
+
13
20
  #
14
21
  # Return the endpoint for API request
15
22
  #
16
23
  def endpoint
17
- @endpoint ||= 'https://webpush.viaduct.io/vwp/api'
24
+ @endpoint ||= "https://#{self.webpush_host}/vwp/api"
18
25
  end
19
26
 
20
27
  #
@@ -34,6 +41,7 @@ module Viaduct
34
41
  #
35
42
  # Allow some configuration to be overridden/set
36
43
  #
44
+ attr_writer :webpush_host
37
45
  attr_writer :endpoint
38
46
  attr_writer :token
39
47
  attr_writer :secret
@@ -46,6 +54,7 @@ module Viaduct
46
54
  @channels[name] ||= Channel.new(name)
47
55
  end
48
56
 
57
+
49
58
  #
50
59
  # Make an HTTP request to the WebPush API
51
60
  #
@@ -0,0 +1 @@
1
+ require 'viaduct/web_push/web_socket'
metadata CHANGED
@@ -1,15 +1,43 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: viaduct-webpush
3
3
  version: !ruby/object:Gem::Version
4
- version: 1.0.3
4
+ version: 1.0.4
5
5
  platform: ruby
6
6
  authors:
7
7
  - Adam Cooke
8
8
  autorequire:
9
9
  bindir: bin
10
10
  cert_chain: []
11
- date: 2014-12-09 00:00:00.000000000 Z
11
+ date: 2015-01-12 00:00:00.000000000 Z
12
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.6'
20
+ type: :development
21
+ prerelease: false
22
+ version_requirements: !ruby/object:Gem::Requirement
23
+ requirements:
24
+ - - "~>"
25
+ - !ruby/object:Gem::Version
26
+ version: '1.6'
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'
13
41
  - !ruby/object:Gem::Dependency
14
42
  name: json
15
43
  requirement: !ruby/object:Gem::Requirement
@@ -30,6 +58,20 @@ dependencies:
30
58
  - - "<"
31
59
  - !ruby/object:Gem::Version
32
60
  version: '2'
61
+ - !ruby/object:Gem::Dependency
62
+ name: websocket
63
+ requirement: !ruby/object:Gem::Requirement
64
+ requirements:
65
+ - - "~>"
66
+ - !ruby/object:Gem::Version
67
+ version: '1.2'
68
+ type: :runtime
69
+ prerelease: false
70
+ version_requirements: !ruby/object:Gem::Requirement
71
+ requirements:
72
+ - - "~>"
73
+ - !ruby/object:Gem::Version
74
+ version: '1.2'
33
75
  description: A client library allows messages to be sent to the WebPush API.
34
76
  email:
35
77
  - adam@viaduct.io
@@ -37,10 +79,17 @@ executables: []
37
79
  extensions: []
38
80
  extra_rdoc_files: []
39
81
  files:
82
+ - Gemfile
40
83
  - README.md
84
+ - Rakefile
41
85
  - lib/viaduct/web_push.rb
42
86
  - lib/viaduct/web_push/channel.rb
87
+ - lib/viaduct/web_push/web_socket.rb
88
+ - lib/viaduct/web_push/web_socket/channel.rb
89
+ - lib/viaduct/web_push/web_socket/connection.rb
90
+ - lib/viaduct/web_push/web_socket/raw_socket.rb
43
91
  - lib/viaduct/webpush.rb
92
+ - lib/viaduct/webpush/websocket.rb
44
93
  homepage: http://viaduct.io
45
94
  licenses:
46
95
  - MIT
@@ -66,4 +115,3 @@ signing_key:
66
115
  specification_version: 4
67
116
  summary: A client library for the Viaduct WebPush API.
68
117
  test_files: []
69
- has_rdoc: