async-websocket 0.8.0 → 0.9.0
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +4 -4
- data/.travis.yml +5 -9
- data/Gemfile +4 -0
- data/README.md +3 -1
- data/async-websocket.gemspec +5 -5
- data/examples/chat/client.rb +11 -20
- data/examples/chat/config.ru +84 -23
- data/examples/chat/multi-client.rb +81 -0
- data/examples/middleware/client.rb +1 -1
- data/lib/async/websocket/client.rb +85 -11
- data/lib/async/websocket/connection.rb +21 -63
- data/lib/async/websocket/error.rb +28 -0
- data/lib/async/websocket/server.rb +1 -38
- data/lib/async/websocket/server/rack.rb +96 -0
- data/lib/async/websocket/version.rb +1 -1
- data/spec/async/websocket/client_spec.rb +10 -26
- data/spec/async/websocket/connection_spec.rb +8 -17
- data/spec/async/websocket/upgrade.rb +5 -3
- metadata +30 -13
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA256:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: 0ba6df1f81d1727db806e398e804b402a821f138e46298fe501c49462a68d5fd
|
4
|
+
data.tar.gz: 898bd7a5f9dc2ca60b4d0b4f3ca94a8e5488a18161543002a0cbf5b397d5d7d0
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: bc6798196531c1994e077f41c090851c7e3de09347672f26fd913a5a4e6ebf97986d3113d0caa4a49de56060104ebbbe4528854fa77b4dfcf6d72a5d1d27a6be
|
7
|
+
data.tar.gz: 29693d7fe7ae34a21a5105d4231512181f611bfd51ff25e7a66b2feba53a309aee952bc5e6bf9d95b9d01a51ecc83b873275911a096786032dd6e090a170e637
|
data/.travis.yml
CHANGED
@@ -1,23 +1,19 @@
|
|
1
1
|
language: ruby
|
2
|
+
dist: xenial
|
2
3
|
cache: bundler
|
3
4
|
|
4
|
-
before_script:
|
5
|
-
- gem update --system
|
6
|
-
- gem install bundler
|
7
|
-
|
8
5
|
matrix:
|
9
6
|
include:
|
10
|
-
- rvm: 2.3
|
11
7
|
- rvm: 2.4
|
12
8
|
- rvm: 2.5
|
13
9
|
- rvm: 2.6
|
14
10
|
- rvm: 2.6
|
15
|
-
env: COVERAGE=
|
11
|
+
env: COVERAGE=PartialSummary,Coveralls
|
12
|
+
- rvm: truffleruby
|
16
13
|
- rvm: jruby-head
|
17
14
|
env: JRUBY_OPTS="--debug -X+O"
|
18
|
-
- rvm: truffleruby
|
19
15
|
- rvm: ruby-head
|
20
16
|
allow_failures:
|
21
|
-
- rvm: jruby-head
|
22
17
|
- rvm: truffleruby
|
23
|
-
- rvm: ruby-head
|
18
|
+
- rvm: ruby-head
|
19
|
+
- rvm: jruby-head
|
data/Gemfile
CHANGED
data/README.md
CHANGED
@@ -69,7 +69,9 @@ require 'async/websocket/server'
|
|
69
69
|
$connections = []
|
70
70
|
|
71
71
|
run lambda {|env|
|
72
|
-
|
72
|
+
# Options for websocket-driver-ruby can be passed as second argument to open
|
73
|
+
# Supported options here: https://github.com/faye/websocket-driver-ruby#driver-api
|
74
|
+
Async::WebSocket::Server.open(env, protocols: ['ws']) do |connection|
|
73
75
|
$connections << connection
|
74
76
|
|
75
77
|
while message = connection.next_message
|
data/async-websocket.gemspec
CHANGED
@@ -14,13 +14,13 @@ Gem::Specification.new do |spec|
|
|
14
14
|
spec.executables = spec.files.grep(%r{^bin/}) { |f| File.basename(f) }
|
15
15
|
spec.test_files = spec.files.grep(%r{^(test|spec|features)/})
|
16
16
|
spec.require_paths = ["lib"]
|
17
|
-
|
18
|
-
spec.add_dependency "
|
19
|
-
|
20
|
-
spec.add_dependency "
|
17
|
+
|
18
|
+
spec.add_dependency "async-io", "~> 1.23"
|
19
|
+
spec.add_dependency "protocol-http1", "~> 0.1"
|
20
|
+
spec.add_dependency "protocol-websocket", "~> 0.3"
|
21
21
|
|
22
22
|
spec.add_development_dependency "async-rspec"
|
23
|
-
spec.add_development_dependency "falcon", "~> 0.
|
23
|
+
spec.add_development_dependency "falcon", "~> 0.30"
|
24
24
|
|
25
25
|
spec.add_development_dependency "covered"
|
26
26
|
spec.add_development_dependency "bundler"
|
data/examples/chat/client.rb
CHANGED
@@ -3,39 +3,30 @@
|
|
3
3
|
require 'async'
|
4
4
|
require 'async/io/stream'
|
5
5
|
require 'async/http/url_endpoint'
|
6
|
-
|
6
|
+
require_relative '../../lib/async/websocket/client'
|
7
7
|
|
8
8
|
USER = ARGV.pop || "anonymous"
|
9
|
-
URL = ARGV.pop || "
|
9
|
+
URL = ARGV.pop || "http://127.0.0.1:8080"
|
10
|
+
ENDPOINT = Async::HTTP::URLEndpoint.parse(URL)
|
10
11
|
|
11
12
|
Async do |task|
|
12
13
|
stdin = Async::IO::Stream.new(
|
13
14
|
Async::IO::Generic.new($stdin)
|
14
15
|
)
|
15
16
|
|
16
|
-
|
17
|
-
|
18
|
-
endpoint.connect do |socket|
|
19
|
-
connection = Async::WebSocket::Client.new(socket, URL)
|
20
|
-
|
21
|
-
connection.send_message({
|
22
|
-
user: USER,
|
23
|
-
status: "connected",
|
24
|
-
})
|
25
|
-
|
26
|
-
task.async do
|
27
|
-
puts "Waiting for input..."
|
17
|
+
Async::WebSocket::Client.open(ENDPOINT) do |connection|
|
18
|
+
input_task = task.async do
|
28
19
|
while line = stdin.read_until("\n")
|
29
|
-
|
30
|
-
connection.
|
31
|
-
user: USER,
|
32
|
-
text: line,
|
33
|
-
})
|
20
|
+
connection.send_message({text: line})
|
21
|
+
connection.flush
|
34
22
|
end
|
35
23
|
end
|
36
24
|
|
25
|
+
puts "Connected..."
|
37
26
|
while message = connection.next_message
|
38
|
-
puts "
|
27
|
+
puts "> #{message.inspect}"
|
39
28
|
end
|
29
|
+
ensure
|
30
|
+
input_task&.stop
|
40
31
|
end
|
41
32
|
end
|
data/examples/chat/config.ru
CHANGED
@@ -1,15 +1,18 @@
|
|
1
|
-
#!/usr/bin/env falcon serve --
|
1
|
+
#!/usr/bin/env -S falcon serve --bind http://127.0.0.1:8080 --count 1 -c
|
2
2
|
|
3
|
-
|
3
|
+
require_relative '../../lib/async/websocket/server/rack'
|
4
|
+
require 'async/clock'
|
5
|
+
require 'async/semaphore'
|
6
|
+
require 'async/logger'
|
4
7
|
|
5
|
-
require 'async/actor'
|
6
8
|
require 'set'
|
7
9
|
|
8
|
-
|
10
|
+
GC.disable
|
9
11
|
|
10
12
|
class Room
|
11
13
|
def initialize
|
12
14
|
@connections = Set.new
|
15
|
+
@semaphore = Async::Semaphore.new(512)
|
13
16
|
end
|
14
17
|
|
15
18
|
def connect connection
|
@@ -23,29 +26,87 @@ class Room
|
|
23
26
|
def each(&block)
|
24
27
|
@connections.each(&block)
|
25
28
|
end
|
26
|
-
end
|
27
|
-
|
28
|
-
bus.supervise(:room) do
|
29
|
-
Room.new
|
30
|
-
end
|
31
|
-
|
32
|
-
run lambda {|env|
|
33
|
-
room = bus[:room]
|
34
29
|
|
35
|
-
|
36
|
-
|
37
|
-
|
30
|
+
def allocations
|
31
|
+
counts = Hash.new{|h,k| h[k] = 0}
|
32
|
+
|
33
|
+
ObjectSpace.each_object do |object|
|
34
|
+
counts[object.class] += 1
|
35
|
+
end
|
36
|
+
|
37
|
+
return counts
|
38
|
+
end
|
39
|
+
|
40
|
+
def show_allocations(key, limit = 1000)
|
41
|
+
Async.logger.info(self) do |buffer|
|
42
|
+
ObjectSpace.each_object(key).each do |object|
|
43
|
+
buffer.puts object
|
44
|
+
end
|
45
|
+
end
|
46
|
+
end
|
47
|
+
|
48
|
+
def print_allocations(minimum = @connections.count)
|
49
|
+
count = 0
|
50
|
+
|
51
|
+
Async.logger.info(self) do |buffer|
|
52
|
+
allocations.select{|k,v| v >= minimum}.sort_by{|k,v| -v}.each do |key, value|
|
53
|
+
count += value
|
54
|
+
buffer.puts "#{key}: #{value} allocations"
|
55
|
+
end
|
38
56
|
|
39
|
-
|
40
|
-
|
41
|
-
|
57
|
+
buffer.puts "** #{count.to_f / @connections.count} objects per connection."
|
58
|
+
end
|
59
|
+
end
|
60
|
+
|
61
|
+
def command(code)
|
62
|
+
Async.logger.warn self, "eval(#{code})"
|
63
|
+
|
64
|
+
eval(code)
|
65
|
+
end
|
66
|
+
|
67
|
+
def broadcast(message)
|
68
|
+
Async.logger.info "Broadcast: #{message.inspect}"
|
69
|
+
start_time = Async::Clock.now
|
70
|
+
|
71
|
+
@connections.each do |connection|
|
72
|
+
@semaphore.async do
|
73
|
+
connection.send_message(message)
|
74
|
+
end
|
75
|
+
end
|
76
|
+
|
77
|
+
end_time = Async::Clock.now
|
78
|
+
Async.logger.info "Duration: #{(end_time - start_time).round(3)}s for #{@connections.count} connected clients."
|
79
|
+
end
|
80
|
+
|
81
|
+
def open(connection)
|
82
|
+
self.connect(connection)
|
83
|
+
|
84
|
+
while message = connection.next_message
|
85
|
+
if message["text"] =~ /^\/(.*?)$/
|
86
|
+
begin
|
87
|
+
result = self.command($1)
|
88
|
+
|
89
|
+
if result.is_a? Hash
|
90
|
+
connection.send_message(result)
|
91
|
+
else
|
92
|
+
connection.send_message({result: result.inspect})
|
93
|
+
end
|
94
|
+
rescue
|
95
|
+
connection.send_message({error: $!.inspect})
|
42
96
|
end
|
97
|
+
else
|
98
|
+
self.broadcast(message)
|
43
99
|
end
|
44
|
-
rescue
|
45
|
-
room.disconnect(connection)
|
46
100
|
end
|
101
|
+
|
102
|
+
connection.close
|
103
|
+
ensure
|
104
|
+
self.disconnect(connection)
|
47
105
|
end
|
48
106
|
|
49
|
-
|
50
|
-
|
51
|
-
|
107
|
+
def call(env)
|
108
|
+
Async::WebSocket::Server::Rack.open(env, &self.method(:open))
|
109
|
+
end
|
110
|
+
end
|
111
|
+
|
112
|
+
run Room.new
|
@@ -0,0 +1,81 @@
|
|
1
|
+
#!/usr/bin/env ruby
|
2
|
+
|
3
|
+
require 'async'
|
4
|
+
require 'async/semaphore'
|
5
|
+
require 'async/clock'
|
6
|
+
require 'async/io/stream'
|
7
|
+
require 'async/http/url_endpoint'
|
8
|
+
require_relative '../../lib/async/websocket/client'
|
9
|
+
|
10
|
+
require 'samovar'
|
11
|
+
require 'ruby-prof'
|
12
|
+
|
13
|
+
require 'tty/progressbar'
|
14
|
+
|
15
|
+
GC.disable
|
16
|
+
|
17
|
+
class Command < Samovar::Command
|
18
|
+
options do
|
19
|
+
option "-c/--count <integer>", "The total number of connections to make.", default: 1000, type: Integer
|
20
|
+
option "--bind <address>", "The local address to bind to before making a connection."
|
21
|
+
option "--connect <string>", "The remote server to connect to.", default: "http://127.0.0.1:8080"
|
22
|
+
|
23
|
+
option "-s/--semaphore <integer>", "The number of simultaneous connections to perform."
|
24
|
+
end
|
25
|
+
|
26
|
+
def local_address
|
27
|
+
if bind = @options[:bind]
|
28
|
+
Async::IO::Address.tcp(bind, 0)
|
29
|
+
end
|
30
|
+
end
|
31
|
+
|
32
|
+
def call
|
33
|
+
endpoint = Async::HTTP::URLEndpoint.parse(@options[:connect], local_address: self.local_address)
|
34
|
+
count = @options[:count]
|
35
|
+
|
36
|
+
connections = Async::Queue.new
|
37
|
+
progress = TTY::ProgressBar.new(":rate connection/s [:bar] :current/:total (:eta/:elapsed)", total: count)
|
38
|
+
|
39
|
+
# profile = RubyProf::Profile.new(merge_fibers: true)
|
40
|
+
# profile.start
|
41
|
+
|
42
|
+
Async do |task|
|
43
|
+
task.logger.info!
|
44
|
+
|
45
|
+
task.async do |subtask|
|
46
|
+
while connection = connections.dequeue
|
47
|
+
subtask.async(connection) do |subtask, connection|
|
48
|
+
pp connection.next_message
|
49
|
+
|
50
|
+
while message = connection.next_message
|
51
|
+
pp message
|
52
|
+
end
|
53
|
+
ensure
|
54
|
+
connection.close
|
55
|
+
end
|
56
|
+
end
|
57
|
+
|
58
|
+
# subtask.children.each(&:stop)
|
59
|
+
end
|
60
|
+
|
61
|
+
client = Async::WebSocket::Client.new(endpoint)
|
62
|
+
|
63
|
+
count.times do |i|
|
64
|
+
connections.enqueue(client.get)
|
65
|
+
progress.advance(1)
|
66
|
+
end
|
67
|
+
|
68
|
+
connections.enqueue(nil)
|
69
|
+
end
|
70
|
+
|
71
|
+
# ensure
|
72
|
+
# result = profile.stop
|
73
|
+
# printer = RubyProf::FlatPrinter.new(result)
|
74
|
+
# printer.print(STDOUT, min_percent: 0.5)
|
75
|
+
#
|
76
|
+
# printer = RubyProf::GraphPrinter.new(result)
|
77
|
+
# printer.print(STDOUT, min_percent: 0.5)
|
78
|
+
end
|
79
|
+
end
|
80
|
+
|
81
|
+
Command.call
|
@@ -15,7 +15,7 @@ Async do |task|
|
|
15
15
|
|
16
16
|
endpoint = Async::HTTP::URLEndpoint.parse(URL)
|
17
17
|
|
18
|
-
endpoint.connect do |socket|
|
18
|
+
endpoint.with(local_address: local_address).connect do |socket|
|
19
19
|
connection = Async::WebSocket::Client.new(socket, URL)
|
20
20
|
|
21
21
|
connection.send_message({
|
@@ -1,3 +1,5 @@
|
|
1
|
+
# frozen_string_literals: true
|
2
|
+
#
|
1
3
|
# Copyright, 2015, by Samuel G. D. Williams. <http://www.codeotaku.com>
|
2
4
|
#
|
3
5
|
# Permission is hereby granted, free of charge, to any person obtaining a copy
|
@@ -18,23 +20,95 @@
|
|
18
20
|
# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
|
19
21
|
# THE SOFTWARE.
|
20
22
|
|
23
|
+
require 'protocol/http1/connection'
|
24
|
+
require 'protocol/websocket/digest'
|
25
|
+
|
21
26
|
require_relative 'connection'
|
27
|
+
require_relative 'error'
|
22
28
|
|
23
29
|
module Async
|
24
30
|
module WebSocket
|
25
31
|
# This is a basic synchronous websocket client:
|
26
|
-
class Client
|
27
|
-
def
|
28
|
-
|
29
|
-
|
30
|
-
|
32
|
+
class Client
|
33
|
+
def self.open(endpoint, **options, &block)
|
34
|
+
# endpoint = Async::HTTP::URLEndpoint.parse(url)
|
35
|
+
client = self.new(endpoint, **options)
|
36
|
+
|
37
|
+
return client unless block_given?
|
38
|
+
|
39
|
+
client.get(endpoint.path, &block)
|
31
40
|
end
|
32
|
-
|
33
|
-
|
34
|
-
|
35
|
-
|
36
|
-
|
37
|
-
|
41
|
+
|
42
|
+
# @option protocols [Array] a list of supported sub-protocols to negotiate with the server.
|
43
|
+
def initialize(endpoint, headers: [], protocols: [], version: 13, key: SecureRandom.base64(16))
|
44
|
+
@endpoint = endpoint
|
45
|
+
@version = version
|
46
|
+
@headers = headers
|
47
|
+
|
48
|
+
@protocols = protocols
|
49
|
+
|
50
|
+
@key = key
|
51
|
+
end
|
52
|
+
|
53
|
+
attr :headers
|
54
|
+
|
55
|
+
def connect
|
56
|
+
peer = @endpoint.connect
|
57
|
+
|
58
|
+
return ::Protocol::HTTP1::Connection.new(IO::Stream.new(peer), false)
|
59
|
+
end
|
60
|
+
|
61
|
+
def request_headers
|
62
|
+
headers = [
|
63
|
+
['sec-websocket-key', @key],
|
64
|
+
['sec-websocket-version', @version]
|
65
|
+
] + @headers.to_a
|
66
|
+
|
67
|
+
if @protocols.any?
|
68
|
+
headers << ['sec-websocket-protocol', @protocols.join(',')]
|
69
|
+
end
|
70
|
+
|
71
|
+
return headers
|
72
|
+
end
|
73
|
+
|
74
|
+
def get(path = '/', &block)
|
75
|
+
self.call('GET', path, &block)
|
76
|
+
end
|
77
|
+
|
78
|
+
HTTP_VERSION = 'HTTP/1.0'.freeze
|
79
|
+
|
80
|
+
def make_connection(stream, headers)
|
81
|
+
protocol = headers['sec-websocket-protocol']&.first
|
82
|
+
|
83
|
+
framer = Protocol::WebSocket::Framer.new(stream)
|
84
|
+
|
85
|
+
return Connection.new(framer, protocol)
|
86
|
+
end
|
87
|
+
|
88
|
+
def call(method, path)
|
89
|
+
client = connect
|
90
|
+
client.upgrade!("websocket")
|
91
|
+
|
92
|
+
client.write_request(@endpoint.authority, method, @endpoint.path, HTTP_VERSION, self.request_headers)
|
93
|
+
stream = client.write_upgrade_body
|
94
|
+
|
95
|
+
version, status, reason, headers, body = client.read_response(method)
|
96
|
+
|
97
|
+
raise ProtocolError, "Expected status 101, got #{status}!" unless status == 101
|
98
|
+
|
99
|
+
accept_digest = headers['sec-websocket-accept'].first
|
100
|
+
if accept_digest.nil? or accept_digest != ::Protocol::WebSocket.accept_digest(@key)
|
101
|
+
raise ProtocolError, "Invalid accept header, got #{accept_digest.inspect}!"
|
102
|
+
end
|
103
|
+
|
104
|
+
connection = make_connection(stream, headers)
|
105
|
+
|
106
|
+
return connection unless block_given?
|
107
|
+
|
108
|
+
begin
|
109
|
+
yield connection
|
110
|
+
ensure
|
111
|
+
connection.close
|
38
112
|
end
|
39
113
|
end
|
40
114
|
end
|
@@ -18,83 +18,41 @@
|
|
18
18
|
# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
|
19
19
|
# THE SOFTWARE.
|
20
20
|
|
21
|
-
require 'websocket/
|
22
|
-
require 'json'
|
21
|
+
require 'protocol/websocket/connection'
|
23
22
|
|
24
|
-
require '
|
23
|
+
require 'json'
|
24
|
+
require 'securerandom'
|
25
25
|
|
26
26
|
module Async
|
27
27
|
module WebSocket
|
28
|
+
Frame = ::Protocol::WebSocket::Frame
|
29
|
+
|
28
30
|
# This is a basic synchronous websocket client:
|
29
|
-
class Connection
|
30
|
-
|
31
|
-
|
32
|
-
EVENTS = [:open, :message, :close]
|
33
|
-
|
34
|
-
def initialize(socket, driver)
|
35
|
-
@socket = socket
|
36
|
-
@driver = driver
|
37
|
-
|
38
|
-
@queue = []
|
39
|
-
|
40
|
-
@driver.on(:error) do |error|
|
41
|
-
raise error
|
42
|
-
end
|
43
|
-
|
44
|
-
EVENTS.each do |event|
|
45
|
-
@driver.on(event) do |data|
|
46
|
-
@queue.push(data)
|
47
|
-
end
|
48
|
-
end
|
31
|
+
class Connection < ::Protocol::WebSocket::Connection
|
32
|
+
def initialize(framer, protocol, mask: SecureRandom.bytes(4), format: JSON)
|
33
|
+
super(framer)
|
49
34
|
|
50
|
-
@
|
35
|
+
@protocol = protocol
|
36
|
+
@mask = mask
|
37
|
+
@format = format
|
51
38
|
end
|
52
39
|
|
53
|
-
attr :
|
54
|
-
attr :url
|
55
|
-
|
56
|
-
def next_event
|
57
|
-
@socket.flush
|
58
|
-
|
59
|
-
while @queue.empty?
|
60
|
-
data = @socket.readpartial(BLOCK_SIZE)
|
61
|
-
|
62
|
-
if data and !data.empty?
|
63
|
-
@driver.parse(data)
|
64
|
-
else
|
65
|
-
return nil
|
66
|
-
end
|
67
|
-
end
|
68
|
-
|
69
|
-
@queue.shift
|
70
|
-
rescue EOFError, Errno::ECONNRESET
|
71
|
-
return nil
|
72
|
-
end
|
40
|
+
attr :protocol
|
73
41
|
|
74
42
|
def next_message
|
75
|
-
|
76
|
-
if
|
77
|
-
|
78
|
-
|
79
|
-
return
|
43
|
+
if frames = super
|
44
|
+
if frames.first.is_a? Protocol::WebSocket::TextFrame
|
45
|
+
buffer = frames.collect(&:unpack).join
|
46
|
+
|
47
|
+
return @format.load(buffer)
|
48
|
+
else
|
49
|
+
return frames
|
80
50
|
end
|
81
51
|
end
|
82
52
|
end
|
83
53
|
|
84
|
-
def
|
85
|
-
@
|
86
|
-
end
|
87
|
-
|
88
|
-
def send_message(message)
|
89
|
-
@driver.text(JSON.dump(message))
|
90
|
-
end
|
91
|
-
|
92
|
-
def write(data)
|
93
|
-
@socket.write(data)
|
94
|
-
end
|
95
|
-
|
96
|
-
def close
|
97
|
-
@driver.close
|
54
|
+
def send_message(data)
|
55
|
+
send_text(@format.dump(data))
|
98
56
|
end
|
99
57
|
end
|
100
58
|
end
|
@@ -0,0 +1,28 @@
|
|
1
|
+
# Copyright, 2019, by Samuel G. D. Williams. <http://www.codeotaku.com>
|
2
|
+
#
|
3
|
+
# Permission is hereby granted, free of charge, to any person obtaining a copy
|
4
|
+
# of this software and associated documentation files (the "Software"), to deal
|
5
|
+
# in the Software without restriction, including without limitation the rights
|
6
|
+
# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
7
|
+
# copies of the Software, and to permit persons to whom the Software is
|
8
|
+
# furnished to do so, subject to the following conditions:
|
9
|
+
#
|
10
|
+
# The above copyright notice and this permission notice shall be included in
|
11
|
+
# all copies or substantial portions of the Software.
|
12
|
+
#
|
13
|
+
# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
14
|
+
# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
15
|
+
# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
16
|
+
# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
17
|
+
# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
18
|
+
# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
|
19
|
+
# THE SOFTWARE.
|
20
|
+
|
21
|
+
require 'protocol/websocket/error'
|
22
|
+
|
23
|
+
module Async
|
24
|
+
module WebSocket
|
25
|
+
class ProtocolError < ::Protocol::WebSocket::ProtocolError
|
26
|
+
end
|
27
|
+
end
|
28
|
+
end
|
@@ -22,44 +22,7 @@ require_relative 'connection'
|
|
22
22
|
|
23
23
|
module Async
|
24
24
|
module WebSocket
|
25
|
-
|
26
|
-
def initialize(env, socket, **options)
|
27
|
-
scheme = env['rack.url_scheme'] == 'https' ? 'wss' : 'ws'
|
28
|
-
|
29
|
-
@url = "#{scheme}://#{env['HTTP_HOST']}#{env['REQUEST_URI']}"
|
30
|
-
@env = env
|
31
|
-
|
32
|
-
super(socket, ::WebSocket::Driver.rack(self, options))
|
33
|
-
end
|
34
|
-
|
35
|
-
attr :env
|
36
|
-
attr :url
|
37
|
-
|
38
|
-
HIJACK_RESPONSE = [-1, {}, []].freeze
|
39
|
-
|
40
|
-
def self.open(env, **options)
|
41
|
-
if ::WebSocket::Driver.websocket?(env)
|
42
|
-
return nil unless env['rack.hijack?']
|
43
|
-
|
44
|
-
# https://github.com/rack/rack/blob/master/SPEC#L89-L93
|
45
|
-
peer = Async::IO.try_convert(
|
46
|
-
env['rack.hijack'].call
|
47
|
-
)
|
48
|
-
|
49
|
-
connection = self.new(env, peer, options)
|
50
|
-
|
51
|
-
return connection unless block_given?
|
52
|
-
|
53
|
-
begin
|
54
|
-
yield(connection)
|
55
|
-
|
56
|
-
return HIJACK_RESPONSE
|
57
|
-
ensure
|
58
|
-
connection.close
|
59
|
-
peer.close
|
60
|
-
end
|
61
|
-
end
|
62
|
-
end
|
25
|
+
module Server
|
63
26
|
end
|
64
27
|
end
|
65
28
|
end
|
@@ -0,0 +1,96 @@
|
|
1
|
+
# frozen_string_literals: true
|
2
|
+
#
|
3
|
+
# Copyright, 2019, by Samuel G. D. Williams. <http://www.codeotaku.com>
|
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
|
+
|
23
|
+
require 'protocol/websocket/digest'
|
24
|
+
|
25
|
+
require_relative '../connection'
|
26
|
+
|
27
|
+
module Async
|
28
|
+
module WebSocket
|
29
|
+
module Server
|
30
|
+
class Rack
|
31
|
+
def self.open(env, **options, &block)
|
32
|
+
return nil unless env['rack.hijack?']
|
33
|
+
|
34
|
+
connection = self.new(env, **options)
|
35
|
+
|
36
|
+
if connection.supported?
|
37
|
+
return connection.response(&block)
|
38
|
+
else
|
39
|
+
return nil
|
40
|
+
end
|
41
|
+
end
|
42
|
+
|
43
|
+
def initialize(env, supported_protocols: [], **options)
|
44
|
+
scheme = env['rack.url_scheme'] == 'https' ? 'wss' : 'ws'
|
45
|
+
@url = "#{scheme}://#{env['HTTP_HOST']}#{env['REQUEST_URI']}"
|
46
|
+
|
47
|
+
@key = env['HTTP_SEC_WEBSOCKET_KEY']
|
48
|
+
@version = Integer(env['HTTP_SEC_WEBSOCKET_VERSION'])
|
49
|
+
|
50
|
+
@protocol = negotiate_protocol(env, supported_protocols)
|
51
|
+
end
|
52
|
+
|
53
|
+
def negotiate_protocol(env, supported_protocols)
|
54
|
+
if supported_protocols and client_protocols = env['HTTP_SEC_WEBSOCKET_PROTOCOL']
|
55
|
+
return (supported_protocols & client_protocols.split(/\s*,\s/)).first
|
56
|
+
end
|
57
|
+
end
|
58
|
+
|
59
|
+
attr :protocol
|
60
|
+
|
61
|
+
def supported?
|
62
|
+
@key and @version == 13
|
63
|
+
end
|
64
|
+
|
65
|
+
def make_connection(stream)
|
66
|
+
framer = Protocol::WebSocket::Framer.new(stream)
|
67
|
+
|
68
|
+
Connection.new(framer, @protocol)
|
69
|
+
end
|
70
|
+
|
71
|
+
def response_headers
|
72
|
+
headers = [
|
73
|
+
['connection', 'upgrade'],
|
74
|
+
['upgrade', 'websocket'],
|
75
|
+
['sec-websocket-accept', ::Protocol::WebSocket.accept_digest(@key)],
|
76
|
+
]
|
77
|
+
|
78
|
+
if @protocol
|
79
|
+
headers << ['sec-websocket-protocol', @protocol]
|
80
|
+
end
|
81
|
+
|
82
|
+
return headers
|
83
|
+
end
|
84
|
+
|
85
|
+
def response(&block)
|
86
|
+
headers = [
|
87
|
+
['rack.hijack', ->(stream){block.call(make_connection(stream))}]
|
88
|
+
]
|
89
|
+
|
90
|
+
# https://stackoverflow.com/questions/13545453/http-response-code-when-requested-websocket-subprotocol-isnt-supported-recogniz
|
91
|
+
return [101, response_headers + headers, nil]
|
92
|
+
end
|
93
|
+
end
|
94
|
+
end
|
95
|
+
end
|
96
|
+
end
|
@@ -21,32 +21,16 @@
|
|
21
21
|
require "async/websocket/client"
|
22
22
|
|
23
23
|
RSpec.describe Async::WebSocket::Client do
|
24
|
-
|
25
|
-
|
26
|
-
|
27
|
-
|
28
|
-
|
29
|
-
|
30
|
-
|
31
|
-
|
32
|
-
|
33
|
-
|
34
|
-
"Foo" => "Bar",
|
35
|
-
"Baz" => "Qux"
|
36
|
-
}
|
37
|
-
|
38
|
-
headers.each do |key, value|
|
39
|
-
expect(client_double).to receive(:set_header).with(key, value)
|
40
|
-
end
|
41
|
-
|
42
|
-
described_class.new(double(write: nil), "", headers: headers)
|
43
|
-
end
|
44
|
-
|
45
|
-
context "without passing headers" do
|
46
|
-
it "does not fail" do
|
47
|
-
expect {
|
48
|
-
described_class.new(double(write: nil))
|
49
|
-
}.not_to raise_error
|
24
|
+
context "headers:" do
|
25
|
+
let(:headers) {[
|
26
|
+
["Foo", "Bar"],
|
27
|
+
["Baz", "Qux"]
|
28
|
+
]}
|
29
|
+
|
30
|
+
subject {described_class.new(nil, headers: headers)}
|
31
|
+
|
32
|
+
it "sets client request headers" do
|
33
|
+
expect(subject.request_headers.to_h).to include(headers.to_h)
|
50
34
|
end
|
51
35
|
end
|
52
36
|
end
|
@@ -26,8 +26,6 @@ require 'falcon/server'
|
|
26
26
|
require 'falcon/adapters/rack'
|
27
27
|
require 'async/http/url_endpoint'
|
28
28
|
|
29
|
-
require 'pry'
|
30
|
-
|
31
29
|
RSpec.describe Async::WebSocket::Connection, timeout: nil do
|
32
30
|
include_context Async::RSpec::Reactor
|
33
31
|
|
@@ -42,14 +40,14 @@ RSpec.describe Async::WebSocket::Connection, timeout: nil do
|
|
42
40
|
|
43
41
|
events = []
|
44
42
|
|
45
|
-
server_address
|
46
|
-
|
47
|
-
|
48
|
-
|
43
|
+
Async::WebSocket::Client.open(server_address) do |connection|
|
44
|
+
while event = connection.next_message
|
45
|
+
expect(event).to include("line")
|
46
|
+
|
49
47
|
events << event
|
50
48
|
end
|
51
49
|
|
52
|
-
|
50
|
+
connection.close # optional
|
53
51
|
end
|
54
52
|
|
55
53
|
expect(events.size).to be > 0
|
@@ -57,20 +55,13 @@ RSpec.describe Async::WebSocket::Connection, timeout: nil do
|
|
57
55
|
server_task.stop
|
58
56
|
end
|
59
57
|
|
60
|
-
it "should
|
58
|
+
it "should negotiate protocol" do
|
61
59
|
server_task = reactor.async do
|
62
60
|
server.run
|
63
61
|
end
|
64
62
|
|
65
|
-
|
66
|
-
|
67
|
-
|
68
|
-
server_address.connect do |socket|
|
69
|
-
client = Async::WebSocket::Client.new(socket, protocols: ['ws'])
|
70
|
-
|
71
|
-
expect(client.next_event).to be_kind_of(WebSocket::Driver::OpenEvent)
|
72
|
-
|
73
|
-
expect(client.driver.protocol).to be == 'ws'
|
63
|
+
Async::WebSocket::Client.open(server_address, protocols: ['ws']) do |connection|
|
64
|
+
expect(connection.protocol).to be == 'ws'
|
74
65
|
end
|
75
66
|
|
76
67
|
server_task.stop
|
@@ -18,7 +18,7 @@
|
|
18
18
|
# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
|
19
19
|
# THE SOFTWARE.
|
20
20
|
|
21
|
-
require 'async/websocket/server'
|
21
|
+
require 'async/websocket/server/rack'
|
22
22
|
|
23
23
|
class Upgrade
|
24
24
|
def initialize(app)
|
@@ -26,16 +26,18 @@ class Upgrade
|
|
26
26
|
end
|
27
27
|
|
28
28
|
def call(env)
|
29
|
-
|
29
|
+
Async::WebSocket::Server::Rack.open(env, supported_protocols: ['ws']) do |connection|
|
30
30
|
read, write = IO.pipe
|
31
31
|
|
32
32
|
Process.spawn("ls -lah", :out => write)
|
33
33
|
write.close
|
34
34
|
|
35
35
|
read.each_line do |line|
|
36
|
-
|
36
|
+
connection.send_message({line: line})
|
37
37
|
end
|
38
38
|
|
39
|
+
# Gracefully close the connection:
|
40
|
+
connection.close
|
39
41
|
end or @app.call(env)
|
40
42
|
end
|
41
43
|
end
|
metadata
CHANGED
@@ -1,43 +1,57 @@
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
2
2
|
name: async-websocket
|
3
3
|
version: !ruby/object:Gem::Version
|
4
|
-
version: 0.
|
4
|
+
version: 0.9.0
|
5
5
|
platform: ruby
|
6
6
|
authors:
|
7
7
|
- Samuel Williams
|
8
8
|
autorequire:
|
9
9
|
bindir: bin
|
10
10
|
cert_chain: []
|
11
|
-
date: 2019-
|
11
|
+
date: 2019-05-10 00:00:00.000000000 Z
|
12
12
|
dependencies:
|
13
13
|
- !ruby/object:Gem::Dependency
|
14
|
-
name:
|
14
|
+
name: async-io
|
15
15
|
requirement: !ruby/object:Gem::Requirement
|
16
16
|
requirements:
|
17
17
|
- - "~>"
|
18
18
|
- !ruby/object:Gem::Version
|
19
|
-
version:
|
19
|
+
version: '1.23'
|
20
20
|
type: :runtime
|
21
21
|
prerelease: false
|
22
22
|
version_requirements: !ruby/object:Gem::Requirement
|
23
23
|
requirements:
|
24
24
|
- - "~>"
|
25
25
|
- !ruby/object:Gem::Version
|
26
|
-
version:
|
26
|
+
version: '1.23'
|
27
27
|
- !ruby/object:Gem::Dependency
|
28
|
-
name:
|
28
|
+
name: protocol-http1
|
29
29
|
requirement: !ruby/object:Gem::Requirement
|
30
30
|
requirements:
|
31
|
-
- - "
|
31
|
+
- - "~>"
|
32
32
|
- !ruby/object:Gem::Version
|
33
|
-
version: '0'
|
33
|
+
version: '0.1'
|
34
34
|
type: :runtime
|
35
35
|
prerelease: false
|
36
36
|
version_requirements: !ruby/object:Gem::Requirement
|
37
37
|
requirements:
|
38
|
-
- - "
|
38
|
+
- - "~>"
|
39
39
|
- !ruby/object:Gem::Version
|
40
|
-
version: '0'
|
40
|
+
version: '0.1'
|
41
|
+
- !ruby/object:Gem::Dependency
|
42
|
+
name: protocol-websocket
|
43
|
+
requirement: !ruby/object:Gem::Requirement
|
44
|
+
requirements:
|
45
|
+
- - "~>"
|
46
|
+
- !ruby/object:Gem::Version
|
47
|
+
version: '0.3'
|
48
|
+
type: :runtime
|
49
|
+
prerelease: false
|
50
|
+
version_requirements: !ruby/object:Gem::Requirement
|
51
|
+
requirements:
|
52
|
+
- - "~>"
|
53
|
+
- !ruby/object:Gem::Version
|
54
|
+
version: '0.3'
|
41
55
|
- !ruby/object:Gem::Dependency
|
42
56
|
name: async-rspec
|
43
57
|
requirement: !ruby/object:Gem::Requirement
|
@@ -58,14 +72,14 @@ dependencies:
|
|
58
72
|
requirements:
|
59
73
|
- - "~>"
|
60
74
|
- !ruby/object:Gem::Version
|
61
|
-
version: '0.
|
75
|
+
version: '0.30'
|
62
76
|
type: :development
|
63
77
|
prerelease: false
|
64
78
|
version_requirements: !ruby/object:Gem::Requirement
|
65
79
|
requirements:
|
66
80
|
- - "~>"
|
67
81
|
- !ruby/object:Gem::Version
|
68
|
-
version: '0.
|
82
|
+
version: '0.30'
|
69
83
|
- !ruby/object:Gem::Dependency
|
70
84
|
name: covered
|
71
85
|
requirement: !ruby/object:Gem::Requirement
|
@@ -139,6 +153,7 @@ files:
|
|
139
153
|
- async-websocket.gemspec
|
140
154
|
- examples/chat/client.rb
|
141
155
|
- examples/chat/config.ru
|
156
|
+
- examples/chat/multi-client.rb
|
142
157
|
- examples/middleware/client.rb
|
143
158
|
- examples/middleware/config.ru
|
144
159
|
- examples/utopia/.bowerrc
|
@@ -178,7 +193,9 @@ files:
|
|
178
193
|
- lib/async/websocket.rb
|
179
194
|
- lib/async/websocket/client.rb
|
180
195
|
- lib/async/websocket/connection.rb
|
196
|
+
- lib/async/websocket/error.rb
|
181
197
|
- lib/async/websocket/server.rb
|
198
|
+
- lib/async/websocket/server/rack.rb
|
182
199
|
- lib/async/websocket/version.rb
|
183
200
|
- spec/async/websocket/client_spec.rb
|
184
201
|
- spec/async/websocket/connection_spec.rb
|
@@ -204,7 +221,7 @@ required_rubygems_version: !ruby/object:Gem::Requirement
|
|
204
221
|
- !ruby/object:Gem::Version
|
205
222
|
version: '0'
|
206
223
|
requirements: []
|
207
|
-
rubygems_version: 3.0.
|
224
|
+
rubygems_version: 3.0.3
|
208
225
|
signing_key:
|
209
226
|
specification_version: 4
|
210
227
|
summary: An async websocket library on top of websocket-driver.
|