nio4r-websocket 0.6.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 +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 [](https://travis-ci.org/NexusSW/nio4r-websocket) [](https://gemnasium.com/github.com/NexusSW/nio4r-websocket)
|
2
|
+
|
3
|
+
[](https://codeclimate.com/github/NexusSW/nio4r-websocket/maintainability) [](https://codeclimate.com/github/NexusSW/nio4r-websocket/test_coverage) [](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: []
|