async-websocket 0.10.0 → 0.11.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: 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