async-websocket 0.8.0 → 0.9.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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: a501ecd90b68be17ff1a832c672ed917da9edd521d478cb0da3565f596c8f9d6
4
- data.tar.gz: 7e3e8cb233c53198506f2ced5a2771f150248c4de916921741f0d60ea07c79e0
3
+ metadata.gz: 0ba6df1f81d1727db806e398e804b402a821f138e46298fe501c49462a68d5fd
4
+ data.tar.gz: 898bd7a5f9dc2ca60b4d0b4f3ca94a8e5488a18161543002a0cbf5b397d5d7d0
5
5
  SHA512:
6
- metadata.gz: 0b83359dd959354e0d31463065782e7b0b86a63b900930ca44252ca228366e99ac1fb69251aa605c72ae3e8ab281add7eb522f9ee01949d2c497b5500a300e77
7
- data.tar.gz: 40912574da9db4d7bde1e8d6854ac33a35c193c0a662eed6b0426a1a5152127e7275e22279ee72f97b7fea2613acd90389b7af449a1fa4d417d2c727dfcd7d54
6
+ metadata.gz: bc6798196531c1994e077f41c090851c7e3de09347672f26fd913a5a4e6ebf97986d3113d0caa4a49de56060104ebbbe4528854fa77b4dfcf6d72a5d1d27a6be
7
+ data.tar.gz: 29693d7fe7ae34a21a5105d4231512181f611bfd51ff25e7a66b2feba53a309aee952bc5e6bf9d95b9d01a51ecc83b873275911a096786032dd6e090a170e637
@@ -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=BriefSummary,Coveralls
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
@@ -6,3 +6,7 @@ group :test do
6
6
  gem 'rack-test'
7
7
  gem 'pry'
8
8
  end
9
+
10
+ group :development do
11
+ gem 'tty-progressbar'
12
+ end
data/README.md CHANGED
@@ -69,7 +69,9 @@ require 'async/websocket/server'
69
69
  $connections = []
70
70
 
71
71
  run lambda {|env|
72
- Async::WebSocket::Server.open(env) do |connection|
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
@@ -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 "websocket-driver", "~> 0.7.0"
19
-
20
- spec.add_dependency "async-io"
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.17"
23
+ spec.add_development_dependency "falcon", "~> 0.30"
24
24
 
25
25
  spec.add_development_dependency "covered"
26
26
  spec.add_development_dependency "bundler"
@@ -3,39 +3,30 @@
3
3
  require 'async'
4
4
  require 'async/io/stream'
5
5
  require 'async/http/url_endpoint'
6
- require 'async/websocket/client'
6
+ require_relative '../../lib/async/websocket/client'
7
7
 
8
8
  USER = ARGV.pop || "anonymous"
9
- URL = ARGV.pop || "ws://localhost:9292"
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
- endpoint = Async::HTTP::URLEndpoint.parse(URL)
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
- puts "Sending text: #{line}"
30
- connection.send_message({
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 "From server: #{message.inspect}"
27
+ puts "> #{message.inspect}"
39
28
  end
29
+ ensure
30
+ input_task&.stop
40
31
  end
41
32
  end
@@ -1,15 +1,18 @@
1
- #!/usr/bin/env falcon serve --concurrency 1 -c
1
+ #!/usr/bin/env -S falcon serve --bind http://127.0.0.1:8080 --count 1 -c
2
2
 
3
- require 'async/websocket/server'
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
- bus = Async::Actor::Bus::Redis.new
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
- Async::WebSocket::Server.open(env) do |connection|
36
- begin
37
- room.connect(connection)
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
- while message = connection.next_message
40
- room.each do |connection|
41
- connection.send_message(message)
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
- Async::Task.current.sleep(0.1)
50
- [200, {}, ["Hello World"]]
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 < Connection
27
- def initialize(socket, url = "ws://.", **options)
28
- @url = url
29
-
30
- super socket, build_client(**options)
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
- def build_client(headers: nil, **options)
34
- ::WebSocket::Driver.client(self, options).tap do |client|
35
- headers&.each do |key, value|
36
- client.set_header(key, value)
37
- end
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/driver'
22
- require 'json'
21
+ require 'protocol/websocket/connection'
23
22
 
24
- require 'async/io/stream'
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
- BLOCK_SIZE = Async::IO::Stream::BLOCK_SIZE
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
- @driver.start
35
+ @protocol = protocol
36
+ @mask = mask
37
+ @format = format
51
38
  end
52
39
 
53
- attr :driver
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
- while event = next_event
76
- if event.is_a? ::WebSocket::Driver::MessageEvent
77
- return JSON.parse(event.data)
78
- elsif event.is_a? ::WebSocket::Driver::CloseEvent
79
- return nil
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 send_text(text)
85
- @driver.text(text)
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
- class Server < Connection
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
@@ -20,6 +20,6 @@
20
20
 
21
21
  module Async
22
22
  module WebSocket
23
- VERSION = "0.8.0"
23
+ VERSION = "0.9.0"
24
24
  end
25
25
  end
@@ -21,32 +21,16 @@
21
21
  require "async/websocket/client"
22
22
 
23
23
  RSpec.describe Async::WebSocket::Client do
24
- let(:client_double) {
25
- instance_double(WebSocket::Driver::Client, on: nil, start: nil)
26
- }
27
-
28
- before do
29
- allow(WebSocket::Driver).to receive(:client).and_return(client_double)
30
- end
31
-
32
- it "sets headers on the driver" do
33
- headers = {
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.connect do |socket|
46
- client = Async::WebSocket::Client.new(socket)
47
-
48
- while event = client.next_event
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
- client.close # optional
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 send back Sec-WebSocket-Protocol header" do
58
+ it "should negotiate protocol" do
61
59
  server_task = reactor.async do
62
60
  server.run
63
61
  end
64
62
 
65
- # Should send and receive Sec-WebSocket-Protocol header as
66
- # `ws`
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
- result = Async::WebSocket::Server.open(env, protocols: ['ws']) do |server|
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
- server.send_text(line)
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.8.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-03-04 00:00:00.000000000 Z
11
+ date: 2019-05-10 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
- name: websocket-driver
14
+ name: async-io
15
15
  requirement: !ruby/object:Gem::Requirement
16
16
  requirements:
17
17
  - - "~>"
18
18
  - !ruby/object:Gem::Version
19
- version: 0.7.0
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: 0.7.0
26
+ version: '1.23'
27
27
  - !ruby/object:Gem::Dependency
28
- name: async-io
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.17'
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.17'
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.2
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.