async-websocket 0.10.0 → 0.11.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: 43a7d40f381a9a3874e3fda412627762d30d2354975e94b6550bda29f75fd26d
4
- data.tar.gz: 197f5ba2209b7bcc8d2eec2e600265ca1b5e59b264967a64da7f416761b9c6a4
3
+ metadata.gz: 0d90af5dc72de1e262ea96046aac6d5aea83dd2dba9d7a6f98684da3107d152d
4
+ data.tar.gz: 9544351dc684062ea0a78b50d15bf9441d18f401fd23fc30eae768a8ef45cd86
5
5
  SHA512:
6
- metadata.gz: 26a221435af1324568f92f7ab2317c16add07196a8eeca36108d226cfccf79cc5f8ad264c3a22b41cf245725e3bb20c148b37e3d71b644bd8ea4ff1373e149e0
7
- data.tar.gz: 982f6ca5dce50b3e230850d298f23cf1410c7c084a503ca52da60b6edcde2efc0f2e9143a500a165de482b72fb32cdfd1b6c71434e44be07b36c6669001195e1
6
+ metadata.gz: 22130d886d22511b6c9478f5035af01aed0dd00225582857d0bc28b864470f47568e0c38a62afb8a46227285009a227f9f2b222b0b5383c0ccaef455beede1bf
7
+ data.tar.gz: 3b9517e0cdaec509d72246776784024a171ba371b7284213e6064cb679b762417560556e00a6f76d164e0aa0c9e5272daa4ee076b31796834f468e30011b14a5
data/README.md CHANGED
@@ -35,7 +35,7 @@ There are [examples](examples/) which include:
35
35
 
36
36
  require 'async'
37
37
  require 'async/io/stream'
38
- require 'async/http/url_endpoint'
38
+ require 'async/http/endpoint'
39
39
  require 'async/websocket/client'
40
40
 
41
41
  USER = ARGV.pop || "anonymous"
@@ -46,7 +46,7 @@ Async do |task|
46
46
  Async::IO::Generic.new($stdin)
47
47
  )
48
48
 
49
- endpoint = Async::HTTP::URLEndpoint.parse(URL)
49
+ endpoint = Async::HTTP::Endpoint.parse(URL)
50
50
 
51
51
  Async::WebSocket::Client.open(endpoint) do |connection|
52
52
  input_task = task.async do
@@ -16,7 +16,7 @@ Gem::Specification.new do |spec|
16
16
  spec.require_paths = ["lib"]
17
17
 
18
18
  spec.add_dependency "async-io", "~> 1.23"
19
- spec.add_dependency "protocol-http1", "~> 0.4"
19
+ spec.add_dependency "async-http", "~> 0.41"
20
20
  spec.add_dependency "protocol-websocket", "~> 0.5.0"
21
21
 
22
22
  spec.add_development_dependency "async-rspec"
@@ -2,12 +2,12 @@
2
2
 
3
3
  require 'async'
4
4
  require 'async/io/stream'
5
- require 'async/http/url_endpoint'
5
+ require 'async/http/endpoint'
6
6
  require_relative '../../lib/async/websocket/client'
7
7
 
8
8
  USER = ARGV.pop || "anonymous"
9
9
  URL = ARGV.pop || "http://127.0.0.1:8080"
10
- ENDPOINT = Async::HTTP::URLEndpoint.parse(URL)
10
+ ENDPOINT = Async::HTTP::Endpoint.parse(URL)
11
11
 
12
12
  Async do |task|
13
13
  stdin = Async::IO::Stream.new(
@@ -4,7 +4,7 @@ require 'async'
4
4
  require 'async/semaphore'
5
5
  require 'async/clock'
6
6
  require 'async/io/stream'
7
- require 'async/http/url_endpoint'
7
+ require 'async/http/endpoint'
8
8
  require_relative '../../lib/async/websocket/client'
9
9
 
10
10
  require 'samovar'
@@ -30,7 +30,7 @@ class Command < Samovar::Command
30
30
  end
31
31
 
32
32
  def call
33
- endpoint = Async::HTTP::URLEndpoint.parse(@options[:connect], local_address: self.local_address)
33
+ endpoint = Async::HTTP::Endpoint.parse(@options[:connect], local_address: self.local_address)
34
34
  count = @options[:count]
35
35
 
36
36
  connections = Async::Queue.new
@@ -2,7 +2,7 @@
2
2
 
3
3
  require 'async'
4
4
  require 'async/io/stream'
5
- require 'async/http/url_endpoint'
5
+ require 'async/http/endpoint'
6
6
  require 'async/websocket/client'
7
7
 
8
8
  USER = ARGV.pop || "anonymous"
@@ -13,7 +13,7 @@ Async do |task|
13
13
  Async::IO::Generic.new($stdin)
14
14
  )
15
15
 
16
- endpoint = Async::HTTP::URLEndpoint.parse(URL)
16
+ endpoint = Async::HTTP::Endpoint.parse(URL)
17
17
 
18
18
  Async::WebSocket::Client.open(endpoint) do |connection|
19
19
  task.async do
@@ -1,9 +1,7 @@
1
1
 
2
2
  source "https://rubygems.org"
3
3
 
4
- gem "utopia", "~> 2.3.0"
5
- # gem "utopia-gallery"
6
- # gem "utopia-analytics"
4
+ gem "utopia", "~> 2.9.0"
7
5
 
8
6
  gem "rake"
9
7
  gem "bundler"
@@ -1,12 +1,13 @@
1
1
 
2
2
  prepend Actions
3
3
 
4
- require 'async/websocket/server'
4
+ require 'async/websocket/server/rack'
5
+ require 'set'
5
6
 
6
- $connections = []
7
+ $connections = Set.new
7
8
 
8
9
  on 'connect' do |request|
9
- respond? Async::WebSocket::Server::Rack.open(request.env) do |connection|
10
+ response = Async::WebSocket::Server::Rack.open(request.env) do |connection|
10
11
  $connections << connection
11
12
 
12
13
  while message = connection.read
@@ -15,5 +16,11 @@ on 'connect' do |request|
15
16
  connection.write(message)
16
17
  end
17
18
  end
19
+ ensure
20
+ $connections.delete(connection)
18
21
  end
22
+
23
+ Async.logger.info(self, request, response)
24
+
25
+ respond?(response)
19
26
  end
@@ -4,7 +4,7 @@ require_relative 'website_context'
4
4
  require 'falcon/server'
5
5
  require 'falcon/adapters/rack'
6
6
 
7
- require 'async/http/url_endpoint'
7
+ require 'async/http/endpoint'
8
8
  require 'async/websocket/client'
9
9
 
10
10
  # Learn about best practice specs from http://betterspecs.org
@@ -22,7 +22,7 @@ RSpec.describe "my website" do
22
22
  context "websockets" do
23
23
  include_context Async::RSpec::Reactor
24
24
 
25
- let(:endpoint) {Async::HTTP::URLEndpoint.parse("http://localhost:9282")}
25
+ let(:endpoint) {Async::HTTP::Endpoint.parse("http://localhost:9282")}
26
26
  let(:server) {Falcon::Server.new(Falcon::Adapters::Rack.new(app), endpoint)}
27
27
 
28
28
  let(:hello_message) do
@@ -21,5 +21,3 @@
21
21
  require_relative 'websocket/version'
22
22
  require_relative 'websocket/server'
23
23
  require_relative 'websocket/client'
24
-
25
- require 'async/io'
@@ -0,0 +1,62 @@
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_relative '../connection'
24
+
25
+ module Async
26
+ module WebSocket
27
+ module Adapters
28
+ module Rack
29
+ include ::Protocol::WebSocket::Headers
30
+
31
+ def self.websocket?(env)
32
+ request = env['async.http.request'] and Array(request.protocol).include?(PROTOCOL)
33
+ end
34
+
35
+ def self.open(env, headers: [], protocols: [], handler: Connection, **options, &block)
36
+ if request = env['async.http.request'] and Array(request.protocol).include?(PROTOCOL)
37
+ # Select websocket sub-protocol:
38
+ if requested_protocol = request.headers[SEC_WEBSOCKET_PROTOCOL]
39
+ protocol = (requested_protocol & protocols).first
40
+ end
41
+
42
+ response = Response.for(request, headers, protocol: protocol, **options) do |stream|
43
+ framer = Protocol::WebSocket::Framer.new(stream)
44
+
45
+ yield handler.call(framer, protocol)
46
+ end
47
+
48
+ headers = response.headers
49
+
50
+ if protocol = response.protocol
51
+ headers = Protocol::HTTP::Headers::Merged.new(headers, [
52
+ ['rack.protocol', protocol]
53
+ ])
54
+ end
55
+
56
+ return [response.status, headers, response.body]
57
+ end
58
+ end
59
+ end
60
+ end
61
+ end
62
+ end
@@ -20,99 +20,64 @@
20
20
  # OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
21
21
  # THE SOFTWARE.
22
22
 
23
- require 'protocol/http1/connection'
24
- require 'protocol/websocket/digest'
23
+ require_relative 'request'
25
24
 
26
- require 'securerandom'
25
+ require 'protocol/websocket/headers'
27
26
 
28
- require_relative 'connection'
29
- require_relative 'error'
27
+ require 'protocol/http/middleware'
30
28
 
31
29
  module Async
32
30
  module WebSocket
33
31
  # This is a basic synchronous websocket client:
34
- class Client
35
- def self.open(endpoint, **options, &block)
36
- # endpoint = Async::HTTP::URLEndpoint.parse(url)
37
- client = self.new(endpoint, **options)
38
-
39
- return client unless block_given?
40
-
41
- client.get(endpoint.path, &block)
42
- end
32
+ class Client < ::Protocol::HTTP::Middleware
33
+ include ::Protocol::WebSocket::Headers
43
34
 
44
- # @option protocols [Array] a list of supported sub-protocols to negotiate with the server.
45
- def initialize(endpoint, headers: [], protocols: [], version: 13, key: SecureRandom.base64(16), connect: Connection)
46
- @endpoint = endpoint
47
- @version = version
48
- @headers = headers
35
+ def self.open(endpoint, *args, &block)
36
+ client = self.new(HTTP::Client.new(endpoint, *args), mask: endpoint.secure?)
49
37
 
50
- @protocols = protocols
51
- @key = key
38
+ return client unless block_given?
52
39
 
53
- @connect = connect
54
- end
55
-
56
- def mask
57
- # Mask is only required on insecure connections, because of bad proxy implementations.
58
- unless @endpoint.secure?
59
- SecureRandom.bytes(4)
40
+ begin
41
+ yield client
42
+ ensure
43
+ client.close
60
44
  end
61
45
  end
62
46
 
63
- attr :headers
64
-
65
- def connect
66
- stream = IO::Stream.new(@endpoint.connect)
67
-
68
- return ::Protocol::HTTP1::Connection.new(stream, false)
69
- end
70
-
71
- def request_headers
72
- headers = [
73
- ['sec-websocket-key', @key],
74
- ['sec-websocket-version', @version]
75
- ] + @headers.to_a
76
-
77
- if @protocols.any?
78
- headers << ['sec-websocket-protocol', @protocols.join(',')]
47
+ def self.connect(endpoint, *args, protocols: [], **options, &block)
48
+ self.open(endpoint, *args, **options) do |client|
49
+ connection = client.connect(endpoint.path, protocols: protocols)
50
+
51
+ return connection unless block_given?
52
+
53
+ begin
54
+ yield connection
55
+ ensure
56
+ connection.close
57
+ end
79
58
  end
80
-
81
- return headers
82
- end
83
-
84
- def get(path = '/', &block)
85
- self.call('GET', path, &block)
86
59
  end
87
60
 
88
- HTTP_VERSION = 'HTTP/1.0'.freeze
89
-
90
- def make_connection(stream, headers)
91
- protocol = headers['sec-websocket-protocol']&.first
92
-
93
- framer = Protocol::WebSocket::Framer.new(stream)
61
+ def initialize(delegate, **options)
62
+ super(delegate)
94
63
 
95
- return @connect.call(framer, protocol, mask: self.mask)
64
+ @options = options
96
65
  end
97
66
 
98
- def call(method, path)
99
- client = connect
100
- client.upgrade!("websocket")
67
+ def connect(path, headers: [], handler: Connection, **options)
68
+ request = Request.new(nil, nil, path, headers, **options)
101
69
 
102
- client.write_request(@endpoint.authority, method, @endpoint.path, HTTP_VERSION, self.request_headers)
103
- stream = client.write_upgrade_body
70
+ response = self.call(request)
104
71
 
105
- version, status, reason, headers, body = client.read_response(method)
72
+ unless response.stream?
73
+ raise ProtocolError, "Failed to negotiate connection: #{response.status}"
74
+ end
106
75
 
107
- raise ProtocolError, "Expected status 101, got #{status}!" unless status == 101
76
+ protocol = response.headers[SEC_WEBSOCKET_PROTOCOL]&.first
77
+ framer = Protocol::WebSocket::Framer.new(response.stream)
108
78
 
109
- accept_digest = headers['sec-websocket-accept'].first
110
- if accept_digest.nil? or accept_digest != ::Protocol::WebSocket.accept_digest(@key)
111
- raise ProtocolError, "Invalid accept header, got #{accept_digest.inspect}!"
112
- end
79
+ connection = handler.call(framer, protocol, **@options)
113
80
 
114
- connection = make_connection(stream, headers)
115
-
116
81
  return connection unless block_given?
117
82
 
118
83
  begin
@@ -0,0 +1,89 @@
1
+ # frozen_string_literals: true
2
+ #
3
+ # Copyright, 2015, 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/http/request'
24
+ require 'protocol/http/headers'
25
+ require 'protocol/websocket/headers'
26
+
27
+ module Async
28
+ module WebSocket
29
+ # This is required for HTTP/1.x to upgrade the connection to the WebSocket protocol.
30
+ # See https://tools.ietf.org/html/rfc8441 for more details.
31
+ class ConnectRequest < ::Protocol::HTTP::Request
32
+ include ::Protocol::WebSocket::Headers
33
+
34
+ class Wrapper
35
+ def initialize(body, response)
36
+ @body = body
37
+ @response = response
38
+ @stream = nil
39
+ end
40
+
41
+ def stream?
42
+ @response.success?
43
+ end
44
+
45
+ def status
46
+ @response.status
47
+ end
48
+
49
+ def headers
50
+ @response.headers
51
+ end
52
+
53
+ def body?
54
+ true
55
+ end
56
+
57
+ attr_accessor :body
58
+
59
+ def protocol
60
+ @response.protocol
61
+ end
62
+
63
+ def stream
64
+ @stream ||= Async::HTTP::Body::Stream.new(@response.body, @body)
65
+ end
66
+ end
67
+
68
+ def initialize(request, protocols: [], version: 13)
69
+ body = Async::HTTP::Body::Writable.new
70
+
71
+ headers = []
72
+
73
+ headers << [SEC_WEBSOCKET_VERSION, version]
74
+
75
+ if protocols.any?
76
+ headers << [SEC_WEBSOCKET_PROTOCOL, protocols.join(',')]
77
+ end
78
+
79
+ merged_headers = ::Protocol::HTTP::Headers::Merged.new(request.headers, headers)
80
+
81
+ super(request.scheme, request.authority, ::Protocol::HTTP::Methods::CONNECT, request.path, nil, merged_headers, body, PROTOCOL)
82
+ end
83
+
84
+ def call(connection)
85
+ Wrapper.new(@body, super)
86
+ end
87
+ end
88
+ end
89
+ end
@@ -0,0 +1,44 @@
1
+ # frozen_string_literals: true
2
+ #
3
+ # Copyright, 2015, 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/http/response'
24
+ require 'async/http/body/hijack'
25
+
26
+ module Async
27
+ module WebSocket
28
+ # The response from the server back to the client for negotiating HTTP/2 WebSockets.
29
+ class ConnectResponse < ::Protocol::HTTP::Response
30
+ include ::Protocol::WebSocket::Headers
31
+
32
+ def initialize(request, headers = nil, protocol: nil, &block)
33
+ headers = Protocol::HTTP::Headers::Merged.new(headers)
34
+
35
+ if protocol
36
+ headers << [[SEC_WEBSOCKET_PROTOCOL, protocol]]
37
+ end
38
+
39
+ body = Async::HTTP::Body::Hijack.wrap(request, &block)
40
+ super(request.version, 200, nil, headers, body)
41
+ end
42
+ end
43
+ end
44
+ end