nio4r-websocket 0.6.0
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +7 -0
- data/.gitignore +15 -0
- data/.rspec +3 -0
- data/.travis.yml +27 -0
- data/Gemfile +3 -0
- data/LICENSE.txt +22 -0
- data/README.md +84 -0
- data/Rakefile +5 -0
- data/Vagrantfile +27 -0
- data/bin/console +7 -0
- data/bin/setup +6 -0
- data/lib/nio/websocket/adapter/client.rb +18 -0
- data/lib/nio/websocket/adapter/server.rb +20 -0
- data/lib/nio/websocket/adapter.rb +107 -0
- data/lib/nio/websocket/reactor.rb +72 -0
- data/lib/nio/websocket/version.rb +5 -0
- data/lib/nio/websocket.rb +178 -0
- data/nio4r-websocket.gemspec +30 -0
- metadata +151 -0
checksums.yaml
ADDED
@@ -0,0 +1,7 @@
|
|
1
|
+
---
|
2
|
+
SHA1:
|
3
|
+
metadata.gz: 1d236ea0ed2486632c8d4deab40592bbc49d2e88
|
4
|
+
data.tar.gz: b13b3d9545f08e05f7387eb21f38d39245d21f57
|
5
|
+
SHA512:
|
6
|
+
metadata.gz: 2e1b4039e7f6712d08be30096c3edce281c53b8b2768b5ac284bd779906ca129b39148b238edbd21b0bb149ea3e9b9988682fb6609114c2125ac270175dd7327
|
7
|
+
data.tar.gz: '0139edfb59b125dd4d8ba09c967d9e1f5a75da90d9bcfe22c1e619b0258be3fa1fb4c0a26b8f963ac97ca476f3126a3707c991a8e4136536c19a3249fe4cbb26'
|
data/.gitignore
ADDED
data/.rspec
ADDED
data/.travis.yml
ADDED
@@ -0,0 +1,27 @@
|
|
1
|
+
sudo: false
|
2
|
+
language: ruby
|
3
|
+
rvm:
|
4
|
+
- 2.1.0
|
5
|
+
- 2.2.2
|
6
|
+
- 2.3.1
|
7
|
+
- ruby-head
|
8
|
+
matrix:
|
9
|
+
allow_failures:
|
10
|
+
- rvm: ruby-head
|
11
|
+
before_script:
|
12
|
+
- curl -L https://codeclimate.com/downloads/test-reporter/test-reporter-latest-linux-amd64 > ./cc-test-reporter
|
13
|
+
- chmod +x ./cc-test-reporter
|
14
|
+
- "./cc-test-reporter before-build"
|
15
|
+
- ip a
|
16
|
+
- cat /etc/hosts
|
17
|
+
after_script:
|
18
|
+
- cat Gemfile.lock
|
19
|
+
- "./cc-test-reporter after-build --exit-code $TRAVIS_TEST_RESULT"
|
20
|
+
deploy:
|
21
|
+
provider: rubygems
|
22
|
+
api_key:
|
23
|
+
secure: TcdJ+iGasCJJFqG9BqWBwsZrKKMTbvWJ+CNMeSYLLCuFOSW74m/A0DBkVddgaADlY1EYIkNkAAaFEaPgQsPunsll8wvAwbddXX5LOsrUaIk3gaYk3V8sudnn437HGsFIvMVQ8yg8fc25p2MMz/pCVPyb1JcMfRBHVDB4I39LUHthi24aGJJn2EZPgiarXYFbsiO/aMId22/Yxw32gG3+ULd2GAhxQ55zZzxfTzvwRian3U9kY4uO79a3rjKH9beSGXYfH3hecp0NEt65vEjwUnfohx7M4We9SHYLm3bEpPtLjUrI2eaaMbeY9bp3UkwVetsL3Ms4METU49sVbYwDrjt+H5s7GvSKdZt/ybmgJWu/Z7llUVrZzJLXRIpMTEGOYh2l+pgzOon+0fmKkcjSbveM6BCiQbMkt+kHHEGlm01VrZPlBeuhnl0ORAxTvAb9PZjs5/myrMrd7C+MAFp9xT+kJ3BodnhV8oPP8imHLqZiu3eAQBk6YtjEOJ3VPlk4VZzL2risXWdKpOeTXU/5WAJJLJmUWNFtfWDsd+EkxprTpNHzymNGUxR2m3OVdqlEwUELCggrkaSNVrYgBFVkrPz0Th23ALsd5YbN16p5eRVbozJP35SxHWQzmCj8Ye7vRgeIN9AyT6FSmuNTbJuPFFGPQaFDmpFkH/CreOXJFNU=
|
24
|
+
gem: nio4r-websocket
|
25
|
+
on:
|
26
|
+
tags: true
|
27
|
+
rvm: 2.3.1
|
data/Gemfile
ADDED
data/LICENSE.txt
ADDED
@@ -0,0 +1,22 @@
|
|
1
|
+
The MIT License (MIT)
|
2
|
+
|
3
|
+
Copyright (c) 2017 Sean Zachariasen
|
4
|
+
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
7
|
+
in the Software without restriction, including without limitation the rights
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
10
|
+
furnished to do so, subject to the following conditions:
|
11
|
+
|
12
|
+
The above copyright notice and this permission notice shall be included in
|
13
|
+
all copies or substantial portions of the Software.
|
14
|
+
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
|
21
|
+
THE SOFTWARE.
|
22
|
+
|
data/README.md
ADDED
@@ -0,0 +1,84 @@
|
|
1
|
+
# NIO::WebSocket [![Build Status](https://travis-ci.org/NexusSW/nio4r-websocket.svg?branch=master)](https://travis-ci.org/NexusSW/nio4r-websocket) [![Dependency Status](https://gemnasium.com/badges/github.com/NexusSW/nio4r-websocket.svg)](https://gemnasium.com/github.com/NexusSW/nio4r-websocket)
|
2
|
+
|
3
|
+
[![Maintainability](https://api.codeclimate.com/v1/badges/cce01221d575804b09f5/maintainability)](https://codeclimate.com/github/NexusSW/nio4r-websocket/maintainability) [![Test Coverage](https://api.codeclimate.com/v1/badges/cce01221d575804b09f5/test_coverage)](https://codeclimate.com/github/NexusSW/nio4r-websocket/test_coverage) [![Gem Version](https://badge.fury.io/rb/nio4r-websocket.svg)](https://badge.fury.io/rb/nio4r-websocket)
|
4
|
+
|
5
|
+
This gem ties websocket-driver, a transport agnostic WebSockets library, together with a nio4r driven socket implementation.
|
6
|
+
|
7
|
+
## Installation
|
8
|
+
|
9
|
+
Add this line to your application's Gemfile:
|
10
|
+
|
11
|
+
```ruby
|
12
|
+
gem 'nio4r-websocket'
|
13
|
+
```
|
14
|
+
|
15
|
+
And then execute:
|
16
|
+
|
17
|
+
bundle
|
18
|
+
|
19
|
+
Or install it yourself as:
|
20
|
+
|
21
|
+
gem install nio4r-websocket
|
22
|
+
|
23
|
+
## Usage
|
24
|
+
|
25
|
+
[YARD Documentation](http://www.rubydoc.info/gems/nio4r-websocket/)
|
26
|
+
|
27
|
+
The only usage patterns introduced by this module are in how to instantiate 'websocket-driver' objects. Please refer to their documentation at <https://github.com/faye/websocket-driver-ruby#driver-api> on how to use them.
|
28
|
+
|
29
|
+
Additionally, the WebSocket driver object will emit an `:io_error` event. In the case that the underlying IO object gets disconnected, or otherwise closed without completing the `WebSocket::Driver#close` mechanism, you will be notified via subscribing to `:io_error` like:
|
30
|
+
|
31
|
+
```ruby
|
32
|
+
driver.on :io_error do
|
33
|
+
# some cleanup logic
|
34
|
+
# `driver.on :close` may or may not be called - likely not
|
35
|
+
end
|
36
|
+
```
|
37
|
+
|
38
|
+
### Examples
|
39
|
+
|
40
|
+
`require 'nio/websocket'`
|
41
|
+
|
42
|
+
Client:
|
43
|
+
|
44
|
+
```ruby
|
45
|
+
NIO::WebSocket.connect 'wss://example.com/' do |driver|
|
46
|
+
driver.on :message do |event|
|
47
|
+
puts event.data
|
48
|
+
end
|
49
|
+
... other wireup code (refer to 'websocket-driver' documentation)
|
50
|
+
end
|
51
|
+
```
|
52
|
+
|
53
|
+
Server:
|
54
|
+
|
55
|
+
```ruby
|
56
|
+
NIO::WebSocket.listen port:443, ssl_context: { key: openssl_pkey_rsa_obj, cert: x509_cert_obj } do |driver|
|
57
|
+
driver.on :message do |event|
|
58
|
+
puts event.data
|
59
|
+
end
|
60
|
+
... other wireup code (refer to 'websocket-driver' documentation)
|
61
|
+
end
|
62
|
+
```
|
63
|
+
|
64
|
+
> Note: The above server block (`listen`) is executed on a per-connection basis
|
65
|
+
|
66
|
+
### Options
|
67
|
+
|
68
|
+
`NIO::WebSocket.listen` accepts `port:` and `address:` options. Port is required, but address is optional for if you care to bind to a specific IP address on your host.
|
69
|
+
|
70
|
+
Both `listen` and `NIO::WebSocket.connect` accept `websocket_options:` which is passed to the corresponding 'websocket-driver' calls. Additionally, `ssl_context:` is available if you care to enable and customize your SSL experience.
|
71
|
+
|
72
|
+
## Development
|
73
|
+
|
74
|
+
After checking out the repo, run `bin/setup` to install dependencies. Then, run `rake spec` to run the tests. You can also run `bin/console` for an interactive prompt that will allow you to experiment.
|
75
|
+
|
76
|
+
To install this gem onto your local machine, run `bundle exec rake install`.
|
77
|
+
|
78
|
+
## Contributing
|
79
|
+
|
80
|
+
Bug reports and pull requests are welcome on GitHub at <https://github.com/NexusSW/nio4r-websocket>. Ensure that you sign off on all of your commits.
|
81
|
+
|
82
|
+
## License
|
83
|
+
|
84
|
+
The gem is available as open source under the terms of the [MIT License](http://opensource.org/licenses/MIT).
|
data/Rakefile
ADDED
data/Vagrantfile
ADDED
@@ -0,0 +1,27 @@
|
|
1
|
+
Vagrant.configure('2') do |config|
|
2
|
+
config.vm.box = 'ubuntu/trusty64'
|
3
|
+
config.vm.provider 'virtualbox' do |vb|
|
4
|
+
vb.memory = '512'
|
5
|
+
end
|
6
|
+
config.vm.provision 'chef_apply' do |chef|
|
7
|
+
chef.recipe = <<-RECIPE
|
8
|
+
apt_update 'update' do
|
9
|
+
action :nothing
|
10
|
+
end
|
11
|
+
apt_repository 'ruby-ng' do
|
12
|
+
uri 'ppa:brightbox/ruby-ng'
|
13
|
+
distribution node['lsb']['codename']
|
14
|
+
only_if { node['lsb']['codename'] == 'trusty' }
|
15
|
+
notifies :update, 'apt_update[update]', :immediately
|
16
|
+
end
|
17
|
+
package %w(git)
|
18
|
+
package %w(ruby2.1 ruby2.1-dev) do # raise/lower this if our minimum version ever changes - only affects local testing
|
19
|
+
only_if { node['lsb']['codename'] == 'trusty' }
|
20
|
+
end
|
21
|
+
gem_package 'bundler'
|
22
|
+
execute 'bundle install' do
|
23
|
+
cwd '/vagrant'
|
24
|
+
end
|
25
|
+
RECIPE
|
26
|
+
end
|
27
|
+
end
|
data/bin/console
ADDED
data/bin/setup
ADDED
@@ -0,0 +1,18 @@
|
|
1
|
+
require 'nio/websocket/adapter'
|
2
|
+
|
3
|
+
module NIO
|
4
|
+
module WebSocket
|
5
|
+
class Adapter
|
6
|
+
class Client < Adapter
|
7
|
+
def initialize(url, io, options)
|
8
|
+
@url = url
|
9
|
+
driver = ::WebSocket::Driver.client(self, options[:websocket_options] || {})
|
10
|
+
super io, driver, options
|
11
|
+
WebSocket.logger.debug "Initiating handshake on #{io}"
|
12
|
+
driver.start
|
13
|
+
end
|
14
|
+
attr_reader :url
|
15
|
+
end
|
16
|
+
end
|
17
|
+
end
|
18
|
+
end
|
@@ -0,0 +1,20 @@
|
|
1
|
+
require 'nio/websocket/adapter'
|
2
|
+
|
3
|
+
module NIO
|
4
|
+
module WebSocket
|
5
|
+
class Adapter
|
6
|
+
class Server < Adapter
|
7
|
+
def initialize(io, options)
|
8
|
+
driver = ::WebSocket::Driver.server(self, options[:websocket_options] || {})
|
9
|
+
driver.on :connect do
|
10
|
+
if ::WebSocket::Driver.websocket? driver.env
|
11
|
+
driver.start
|
12
|
+
WebSocket.logger.debug 'driver connected'
|
13
|
+
end
|
14
|
+
end
|
15
|
+
super io, driver, options
|
16
|
+
end
|
17
|
+
end
|
18
|
+
end
|
19
|
+
end
|
20
|
+
end
|
@@ -0,0 +1,107 @@
|
|
1
|
+
module NIO
|
2
|
+
module WebSocket
|
3
|
+
class Adapter
|
4
|
+
def initialize(io, driver, options)
|
5
|
+
@inner = io
|
6
|
+
@options = options
|
7
|
+
@driver = driver
|
8
|
+
@buffer = ''
|
9
|
+
@mutex = Mutex.new
|
10
|
+
|
11
|
+
driver.on :close do |ev|
|
12
|
+
WebSocket.logger.info "Driver initiated #{inner} close (code #{ev.code}): #{ev.reason}"
|
13
|
+
close :driver
|
14
|
+
end
|
15
|
+
driver.on :error do |ev|
|
16
|
+
WebSocket.logger.error "Driver reports error on #{inner}: #{ev.message}"
|
17
|
+
close :driver
|
18
|
+
end
|
19
|
+
end
|
20
|
+
attr_reader :inner, :options, :driver, :monitor
|
21
|
+
|
22
|
+
def teardown
|
23
|
+
@driver = nil # circular reference
|
24
|
+
monitor.close
|
25
|
+
inner.close
|
26
|
+
end
|
27
|
+
|
28
|
+
def close(from = nil)
|
29
|
+
return false if @closing
|
30
|
+
|
31
|
+
driver.close if from.nil?
|
32
|
+
@closing = true
|
33
|
+
monitor.interests = :rw
|
34
|
+
Reactor.selector.wakeup
|
35
|
+
true
|
36
|
+
end
|
37
|
+
|
38
|
+
def add_to_reactor
|
39
|
+
@monitor = Reactor.selector.register(inner, :rw) # This can block if this is the main thread and the reactor is busy
|
40
|
+
monitor.value = proc do
|
41
|
+
begin
|
42
|
+
read if monitor.readable?
|
43
|
+
pump_buffer if monitor.writable?
|
44
|
+
rescue Errno::ECONNRESET, EOFError
|
45
|
+
driver.force_state :closed
|
46
|
+
driver.emit :io_error
|
47
|
+
teardown
|
48
|
+
WebSocket.logger.info "#{inner} socket closed"
|
49
|
+
rescue IO::WaitReadable # rubocop:disable Lint/HandleExceptions
|
50
|
+
rescue IO::WaitWritable
|
51
|
+
monitor.interests = :rw
|
52
|
+
end
|
53
|
+
if @closing
|
54
|
+
if !monitor.readable? && @buffer.empty?
|
55
|
+
teardown
|
56
|
+
WebSocket.logger.info "#{inner} closed"
|
57
|
+
else
|
58
|
+
monitor.interests = :rw unless monitor.closed? # keep the :w interest so that our block runs each time
|
59
|
+
# edge case: if monitor was readable this time, and the write buffer is empty, if we emptied the read buffer this time our block wouldn't run again
|
60
|
+
end
|
61
|
+
end
|
62
|
+
end
|
63
|
+
end
|
64
|
+
|
65
|
+
def read
|
66
|
+
data = inner.read_nonblock(16384)
|
67
|
+
if data
|
68
|
+
WebSocket.logger.debug { "Incoming data on #{inner}:\n#{data}" } if WebSocket.log_traffic?
|
69
|
+
driver.parse data
|
70
|
+
end
|
71
|
+
data
|
72
|
+
end
|
73
|
+
|
74
|
+
def write(data)
|
75
|
+
@mutex.synchronize do
|
76
|
+
@buffer << data
|
77
|
+
end
|
78
|
+
return unless monitor
|
79
|
+
pump_buffer
|
80
|
+
Reactor.selector.wakeup unless monitor.interests == :r
|
81
|
+
end
|
82
|
+
|
83
|
+
def pump_buffer
|
84
|
+
@mutex.synchronize do
|
85
|
+
written = 0
|
86
|
+
begin
|
87
|
+
written = inner.write_nonblock @buffer unless @buffer.empty?
|
88
|
+
WebSocket.logger.debug { "Pumped #{written} bytes of data from buffer to #{inner}:\n#{@buffer}" } unless @buffer.empty? || !WebSocket.log_traffic?
|
89
|
+
@buffer = @buffer.byteslice(written..-1) if written > 0
|
90
|
+
WebSocket.logger.debug { "The buffer is now:\n#{@buffer}" } unless @buffer.empty? || !WebSocket.log_traffic?
|
91
|
+
rescue IO::WaitWritable, IO::WaitReadable
|
92
|
+
return written
|
93
|
+
ensure
|
94
|
+
monitor.interests = @buffer.empty? ? :r : :rw
|
95
|
+
end
|
96
|
+
written
|
97
|
+
end
|
98
|
+
end
|
99
|
+
end
|
100
|
+
end
|
101
|
+
end
|
102
|
+
|
103
|
+
class ::WebSocket::Driver
|
104
|
+
def force_state(newstate)
|
105
|
+
@ready_state = STATES.index newstate
|
106
|
+
end
|
107
|
+
end
|
@@ -0,0 +1,72 @@
|
|
1
|
+
require 'nio'
|
2
|
+
|
3
|
+
module NIO
|
4
|
+
module WebSocket
|
5
|
+
class Reactor
|
6
|
+
class << self
|
7
|
+
def queue_task(&blk)
|
8
|
+
return unless block_given?
|
9
|
+
task_mutex.synchronize do
|
10
|
+
@task_queue ||= []
|
11
|
+
@task_queue << blk
|
12
|
+
end
|
13
|
+
selector.wakeup
|
14
|
+
end
|
15
|
+
|
16
|
+
def selector
|
17
|
+
@selector ||= NIO::Selector.new
|
18
|
+
end
|
19
|
+
|
20
|
+
def reset
|
21
|
+
@reactor.exit if @reactor
|
22
|
+
@selector = nil
|
23
|
+
@reactor = nil
|
24
|
+
@task_queue = nil
|
25
|
+
@task_mutex = nil
|
26
|
+
end
|
27
|
+
|
28
|
+
def start
|
29
|
+
WebSocket.logger.debug 'Starting reactor' unless @reactor
|
30
|
+
@reactor ||= Thread.start do
|
31
|
+
Thread.current.abort_on_exception = true
|
32
|
+
WebSocket.logger.info 'Reactor started'
|
33
|
+
begin
|
34
|
+
loop do
|
35
|
+
queue = []
|
36
|
+
task_mutex.synchronize do
|
37
|
+
queue = @task_queue || []
|
38
|
+
@task_queue = []
|
39
|
+
end
|
40
|
+
# If something queues up while this runs, then the selector will also be awoken & won't block
|
41
|
+
queue.each(&:call)
|
42
|
+
|
43
|
+
selector.select 1 do |monitor|
|
44
|
+
begin
|
45
|
+
monitor.value.call if monitor.value.respond_to? :call
|
46
|
+
rescue => e
|
47
|
+
WebSocket.logger.error "Error occured in callback on socket #{monitor.io}. No longer handling this connection."
|
48
|
+
WebSocket.logger.error "#{e.class}: #{e.message}"
|
49
|
+
e.backtrace.map { |s| WebSocket.logger.error "\t#{s}" }
|
50
|
+
monitor.close # protect global loop from being crashed by a misbehaving driver, or a sloppy disconnect
|
51
|
+
end
|
52
|
+
end
|
53
|
+
Thread.pass # give other threads a chance at manipulating our selector (e.g. a new connection on the main thread trying to register)
|
54
|
+
end
|
55
|
+
rescue => e
|
56
|
+
WebSocket.logger.fatal 'Error occured in reactor subsystem.'
|
57
|
+
WebSocket.logger.fatal "#{e.class}: #{e.message}"
|
58
|
+
e.backtrace.map { |s| WebSocket.logger.fatal "\t#{s}" }
|
59
|
+
raise
|
60
|
+
end
|
61
|
+
end
|
62
|
+
end
|
63
|
+
|
64
|
+
private
|
65
|
+
|
66
|
+
def task_mutex
|
67
|
+
@task_mutex ||= Mutex.new
|
68
|
+
end
|
69
|
+
end
|
70
|
+
end
|
71
|
+
end
|
72
|
+
end
|
@@ -0,0 +1,178 @@
|
|
1
|
+
require 'nio/websocket/version'
|
2
|
+
require 'websocket/driver'
|
3
|
+
require 'nio'
|
4
|
+
require 'socket'
|
5
|
+
require 'uri'
|
6
|
+
require 'openssl'
|
7
|
+
require 'logger'
|
8
|
+
require 'nio/websocket/reactor'
|
9
|
+
require 'nio/websocket/adapter/client'
|
10
|
+
require 'nio/websocket/adapter/server'
|
11
|
+
|
12
|
+
module NIO
|
13
|
+
module WebSocket
|
14
|
+
class << self
|
15
|
+
# Returns the current logger, or creates one at level ERROR if one has not been assigned
|
16
|
+
# @return [Logger] the current logger instance
|
17
|
+
def logger
|
18
|
+
@logger ||= begin
|
19
|
+
logger = Logger.new(STDERR, progname: 'WebSocket', level: Logger::ERROR)
|
20
|
+
logger.level = Logger::ERROR
|
21
|
+
logger
|
22
|
+
end
|
23
|
+
end
|
24
|
+
|
25
|
+
attr_writer :logger
|
26
|
+
|
27
|
+
# Should raw traffic be logged through the logger? Disabled by default for security reasons
|
28
|
+
# @param enable [Boolean]
|
29
|
+
def log_traffic=(enable)
|
30
|
+
@log_traffic = enable
|
31
|
+
logger.level = Logger::DEBUG if enable
|
32
|
+
enable
|
33
|
+
end
|
34
|
+
|
35
|
+
# Should raw traffic be logged through the logger? Disabled by default for security reasons
|
36
|
+
def log_traffic?
|
37
|
+
@log_traffic
|
38
|
+
end
|
39
|
+
|
40
|
+
# Create and return a websocket client that communicates either over the given IO object (upgrades the connection),
|
41
|
+
# or we'll create a new connection to url if io is not supplied
|
42
|
+
# @param [String] url ws:// or wss:// location to connect
|
43
|
+
# @param [Hash] options
|
44
|
+
# @param [IO] io (DI) raw IO object to use in lieu of opening a new connection to url
|
45
|
+
# @option options [Hash] :websocket_options Hash to pass to the ::WebSocket::Driver.client
|
46
|
+
# @option options [Hash] :ssl_context Hash from which to create the OpenSSL::SSL::SSLContext object
|
47
|
+
# @yield [::WebSocket::Driver]
|
48
|
+
# @return [::WebSocket::Driver]
|
49
|
+
def connect(url, options = {}, io = nil)
|
50
|
+
io ||= open_socket(url, options)
|
51
|
+
adapter = CLIENT_ADAPTER.new(url, io, options)
|
52
|
+
yield(adapter.driver, adapter) if block_given?
|
53
|
+
Reactor.queue_task do
|
54
|
+
adapter.add_to_reactor
|
55
|
+
end
|
56
|
+
Reactor.start
|
57
|
+
logger.info "Client #{io} connected to #{url}"
|
58
|
+
adapter.driver
|
59
|
+
end
|
60
|
+
|
61
|
+
# Start handling new connections, passing each through the supplied block
|
62
|
+
# @param [Hash] options
|
63
|
+
# @param server [TCPServer] (DI) TCPServer-like object to use in lieu of starting a new server
|
64
|
+
# @option options [Integer] :port required: Port on which to listen for incoming connections
|
65
|
+
# @option options [String] :address optional: Specific Address on which to bind the TCPServer
|
66
|
+
# @option options [Hash] :websocket_options Hash to pass to the ::WebSocket::Driver.server
|
67
|
+
# @option options [Hash] :ssl_context Hash from which to create the OpenSSL::SSL::SSLContext object
|
68
|
+
# @yield [::WebSocket::Driver]
|
69
|
+
# @return server, as passed in, or a new TCPServer if no server was specified
|
70
|
+
def listen(options = {}, server = nil)
|
71
|
+
server ||= create_server(options)
|
72
|
+
Reactor.queue_task do
|
73
|
+
monitor = Reactor.selector.register(server, :r)
|
74
|
+
monitor.value = proc do
|
75
|
+
accept_socket server, options do |io| # this next block won't run until ssl (if enabled) has started
|
76
|
+
adapter = SERVER_ADAPTER.new(io, options)
|
77
|
+
yield(adapter.driver, adapter) if block_given?
|
78
|
+
Reactor.queue_task do
|
79
|
+
adapter.add_to_reactor
|
80
|
+
end
|
81
|
+
logger.info "Host accepted client connection #{io} on port #{options[:port]}"
|
82
|
+
end
|
83
|
+
end
|
84
|
+
end
|
85
|
+
Reactor.start
|
86
|
+
logger.info 'Host listening for new connections on port ' + options[:port].to_s
|
87
|
+
server
|
88
|
+
end
|
89
|
+
|
90
|
+
SERVER_ADAPTER = NIO::WebSocket::Adapter::Server
|
91
|
+
CLIENT_ADAPTER = NIO::WebSocket::Adapter::Client
|
92
|
+
|
93
|
+
# Resets this API to a fresh state
|
94
|
+
def reset
|
95
|
+
logger.info 'Resetting reactor subsystem'
|
96
|
+
Reactor.reset
|
97
|
+
end
|
98
|
+
|
99
|
+
# @!endgroup
|
100
|
+
|
101
|
+
private
|
102
|
+
|
103
|
+
# return an open socket given the url and options
|
104
|
+
def open_socket(url, options)
|
105
|
+
uri = URI(url)
|
106
|
+
port = uri.port || (uri.scheme == 'wss' ? 443 : 80) # redundant? test uri.port if port is unspecified but because ws: & wss: aren't default protocols we'll maybe still need this(?)
|
107
|
+
logger.debug "Opening Connection to #{uri.hostname} on port #{port}"
|
108
|
+
io = TCPSocket.new uri.hostname, port
|
109
|
+
return io unless uri.scheme == 'wss'
|
110
|
+
logger.debug "Upgrading Connection #{io} to ssl"
|
111
|
+
ssl = upgrade_to_ssl(io, options).connect
|
112
|
+
logger.info "Connection #{io} upgraded to #{ssl}"
|
113
|
+
ssl
|
114
|
+
end
|
115
|
+
|
116
|
+
def create_server(options)
|
117
|
+
options[:address] ? TCPServer.new(options[:address], options[:port]) : TCPServer.new(options[:port])
|
118
|
+
end
|
119
|
+
|
120
|
+
# supply a block to run after protocol negotiation
|
121
|
+
def accept_socket(server, options)
|
122
|
+
waiting = accept_nonblock server
|
123
|
+
if [:r, :w].include? waiting
|
124
|
+
logger.warn 'Expected to receive new connection, but the server is not quite ready'
|
125
|
+
return
|
126
|
+
end
|
127
|
+
logger.debug "Receiving new connection #{waiting} on port #{options[:port]}"
|
128
|
+
if options[:ssl_context]
|
129
|
+
logger.debug "Upgrading Connection #{waiting} to ssl"
|
130
|
+
ssl = upgrade_to_ssl(waiting, options)
|
131
|
+
try_accept_nonblock ssl do
|
132
|
+
logger.info "Incoming connection #{waiting} upgraded to #{ssl}"
|
133
|
+
yield ssl
|
134
|
+
end
|
135
|
+
else
|
136
|
+
yield waiting
|
137
|
+
end
|
138
|
+
end
|
139
|
+
|
140
|
+
def try_accept_nonblock(io)
|
141
|
+
waiting = accept_nonblock io
|
142
|
+
if [:r, :w].include? waiting
|
143
|
+
# Only happens on server side ssl negotiation
|
144
|
+
Reactor.queue_task do
|
145
|
+
monitor = Reactor.selector.register(io, :rw)
|
146
|
+
monitor.value = proc do
|
147
|
+
waiting = accept_nonblock io
|
148
|
+
unless [:r, :w].include? waiting
|
149
|
+
monitor.close
|
150
|
+
yield waiting
|
151
|
+
end
|
152
|
+
end
|
153
|
+
end
|
154
|
+
else
|
155
|
+
yield waiting
|
156
|
+
end
|
157
|
+
end
|
158
|
+
|
159
|
+
def accept_nonblock(io)
|
160
|
+
return io.accept_nonblock
|
161
|
+
rescue IO::WaitReadable
|
162
|
+
return :r
|
163
|
+
rescue IO::WaitWritable
|
164
|
+
return :w
|
165
|
+
end
|
166
|
+
|
167
|
+
def upgrade_to_ssl(io, options)
|
168
|
+
store = OpenSSL::X509::Store.new
|
169
|
+
store.set_default_paths
|
170
|
+
ctx = OpenSSL::SSL::SSLContext.new
|
171
|
+
{ cert_store: store, verify_mode: OpenSSL::SSL::VERIFY_PEER }.merge(options[:ssl_context] || {}).each do |k, v|
|
172
|
+
ctx.send "#{k}=", v if ctx.respond_to? k
|
173
|
+
end
|
174
|
+
OpenSSL::SSL::SSLSocket.new(io, ctx)
|
175
|
+
end
|
176
|
+
end
|
177
|
+
end
|
178
|
+
end
|
@@ -0,0 +1,30 @@
|
|
1
|
+
# coding: utf-8
|
2
|
+
lib = File.expand_path('../lib', __FILE__)
|
3
|
+
$LOAD_PATH.unshift(lib) unless $LOAD_PATH.include?(lib)
|
4
|
+
require 'nio/websocket/version'
|
5
|
+
|
6
|
+
Gem::Specification.new do |spec|
|
7
|
+
spec.name = 'nio4r-websocket'
|
8
|
+
spec.version = NIO::WebSocket::VERSION
|
9
|
+
spec.authors = ['Sean Zachariasen']
|
10
|
+
spec.email = ['thewyzard@hotmail.com']
|
11
|
+
|
12
|
+
spec.summary = 'websocket-driver implementation built over nio4r'
|
13
|
+
spec.homepage = 'https://github.com/nexussw/nio4r-websocket'
|
14
|
+
spec.license = 'MIT'
|
15
|
+
|
16
|
+
spec.files = `git ls-files -z`.split("\x0").reject do |f|
|
17
|
+
f.match(%r{^(test|spec|features)/})
|
18
|
+
end
|
19
|
+
spec.bindir = 'exe'
|
20
|
+
spec.executables = spec.files.grep(%r{^exe/}) { |f| File.basename(f) }
|
21
|
+
spec.require_paths = ['lib']
|
22
|
+
|
23
|
+
spec.add_dependency 'nio4r', '>= 1.2.1', '< 3.0' # Allow older nio4r, if possible, so as to not lock our ruby version to 2.2.2
|
24
|
+
spec.add_dependency 'websocket-driver', '~> 0.7'
|
25
|
+
|
26
|
+
spec.add_development_dependency 'bundler'
|
27
|
+
spec.add_development_dependency 'rake'
|
28
|
+
spec.add_development_dependency 'rspec'
|
29
|
+
spec.add_development_dependency 'simplecov'
|
30
|
+
end
|
metadata
ADDED
@@ -0,0 +1,151 @@
|
|
1
|
+
--- !ruby/object:Gem::Specification
|
2
|
+
name: nio4r-websocket
|
3
|
+
version: !ruby/object:Gem::Version
|
4
|
+
version: 0.6.0
|
5
|
+
platform: ruby
|
6
|
+
authors:
|
7
|
+
- Sean Zachariasen
|
8
|
+
autorequire:
|
9
|
+
bindir: exe
|
10
|
+
cert_chain: []
|
11
|
+
date: 2017-10-22 00:00:00.000000000 Z
|
12
|
+
dependencies:
|
13
|
+
- !ruby/object:Gem::Dependency
|
14
|
+
name: nio4r
|
15
|
+
requirement: !ruby/object:Gem::Requirement
|
16
|
+
requirements:
|
17
|
+
- - ">="
|
18
|
+
- !ruby/object:Gem::Version
|
19
|
+
version: 1.2.1
|
20
|
+
- - "<"
|
21
|
+
- !ruby/object:Gem::Version
|
22
|
+
version: '3.0'
|
23
|
+
type: :runtime
|
24
|
+
prerelease: false
|
25
|
+
version_requirements: !ruby/object:Gem::Requirement
|
26
|
+
requirements:
|
27
|
+
- - ">="
|
28
|
+
- !ruby/object:Gem::Version
|
29
|
+
version: 1.2.1
|
30
|
+
- - "<"
|
31
|
+
- !ruby/object:Gem::Version
|
32
|
+
version: '3.0'
|
33
|
+
- !ruby/object:Gem::Dependency
|
34
|
+
name: websocket-driver
|
35
|
+
requirement: !ruby/object:Gem::Requirement
|
36
|
+
requirements:
|
37
|
+
- - "~>"
|
38
|
+
- !ruby/object:Gem::Version
|
39
|
+
version: '0.7'
|
40
|
+
type: :runtime
|
41
|
+
prerelease: false
|
42
|
+
version_requirements: !ruby/object:Gem::Requirement
|
43
|
+
requirements:
|
44
|
+
- - "~>"
|
45
|
+
- !ruby/object:Gem::Version
|
46
|
+
version: '0.7'
|
47
|
+
- !ruby/object:Gem::Dependency
|
48
|
+
name: bundler
|
49
|
+
requirement: !ruby/object:Gem::Requirement
|
50
|
+
requirements:
|
51
|
+
- - ">="
|
52
|
+
- !ruby/object:Gem::Version
|
53
|
+
version: '0'
|
54
|
+
type: :development
|
55
|
+
prerelease: false
|
56
|
+
version_requirements: !ruby/object:Gem::Requirement
|
57
|
+
requirements:
|
58
|
+
- - ">="
|
59
|
+
- !ruby/object:Gem::Version
|
60
|
+
version: '0'
|
61
|
+
- !ruby/object:Gem::Dependency
|
62
|
+
name: rake
|
63
|
+
requirement: !ruby/object:Gem::Requirement
|
64
|
+
requirements:
|
65
|
+
- - ">="
|
66
|
+
- !ruby/object:Gem::Version
|
67
|
+
version: '0'
|
68
|
+
type: :development
|
69
|
+
prerelease: false
|
70
|
+
version_requirements: !ruby/object:Gem::Requirement
|
71
|
+
requirements:
|
72
|
+
- - ">="
|
73
|
+
- !ruby/object:Gem::Version
|
74
|
+
version: '0'
|
75
|
+
- !ruby/object:Gem::Dependency
|
76
|
+
name: rspec
|
77
|
+
requirement: !ruby/object:Gem::Requirement
|
78
|
+
requirements:
|
79
|
+
- - ">="
|
80
|
+
- !ruby/object:Gem::Version
|
81
|
+
version: '0'
|
82
|
+
type: :development
|
83
|
+
prerelease: false
|
84
|
+
version_requirements: !ruby/object:Gem::Requirement
|
85
|
+
requirements:
|
86
|
+
- - ">="
|
87
|
+
- !ruby/object:Gem::Version
|
88
|
+
version: '0'
|
89
|
+
- !ruby/object:Gem::Dependency
|
90
|
+
name: simplecov
|
91
|
+
requirement: !ruby/object:Gem::Requirement
|
92
|
+
requirements:
|
93
|
+
- - ">="
|
94
|
+
- !ruby/object:Gem::Version
|
95
|
+
version: '0'
|
96
|
+
type: :development
|
97
|
+
prerelease: false
|
98
|
+
version_requirements: !ruby/object:Gem::Requirement
|
99
|
+
requirements:
|
100
|
+
- - ">="
|
101
|
+
- !ruby/object:Gem::Version
|
102
|
+
version: '0'
|
103
|
+
description:
|
104
|
+
email:
|
105
|
+
- thewyzard@hotmail.com
|
106
|
+
executables: []
|
107
|
+
extensions: []
|
108
|
+
extra_rdoc_files: []
|
109
|
+
files:
|
110
|
+
- ".gitignore"
|
111
|
+
- ".rspec"
|
112
|
+
- ".travis.yml"
|
113
|
+
- Gemfile
|
114
|
+
- LICENSE.txt
|
115
|
+
- README.md
|
116
|
+
- Rakefile
|
117
|
+
- Vagrantfile
|
118
|
+
- bin/console
|
119
|
+
- bin/setup
|
120
|
+
- lib/nio/websocket.rb
|
121
|
+
- lib/nio/websocket/adapter.rb
|
122
|
+
- lib/nio/websocket/adapter/client.rb
|
123
|
+
- lib/nio/websocket/adapter/server.rb
|
124
|
+
- lib/nio/websocket/reactor.rb
|
125
|
+
- lib/nio/websocket/version.rb
|
126
|
+
- nio4r-websocket.gemspec
|
127
|
+
homepage: https://github.com/nexussw/nio4r-websocket
|
128
|
+
licenses:
|
129
|
+
- MIT
|
130
|
+
metadata: {}
|
131
|
+
post_install_message:
|
132
|
+
rdoc_options: []
|
133
|
+
require_paths:
|
134
|
+
- lib
|
135
|
+
required_ruby_version: !ruby/object:Gem::Requirement
|
136
|
+
requirements:
|
137
|
+
- - ">="
|
138
|
+
- !ruby/object:Gem::Version
|
139
|
+
version: '0'
|
140
|
+
required_rubygems_version: !ruby/object:Gem::Requirement
|
141
|
+
requirements:
|
142
|
+
- - ">="
|
143
|
+
- !ruby/object:Gem::Version
|
144
|
+
version: '0'
|
145
|
+
requirements: []
|
146
|
+
rubyforge_project:
|
147
|
+
rubygems_version: 2.6.13
|
148
|
+
signing_key:
|
149
|
+
specification_version: 4
|
150
|
+
summary: websocket-driver implementation built over nio4r
|
151
|
+
test_files: []
|