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 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.