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 +4 -4
- data/README.md +2 -2
- data/async-websocket.gemspec +1 -1
- data/examples/chat/client.rb +2 -2
- data/examples/chat/multi-client.rb +2 -2
- data/examples/mud/client.rb +2 -2
- data/examples/utopia/Gemfile +1 -3
- data/examples/utopia/pages/server/controller.rb +10 -3
- data/examples/utopia/spec/website_spec.rb +2 -2
- data/lib/async/websocket.rb +0 -2
- data/lib/async/websocket/adapters/rack.rb +62 -0
- data/lib/async/websocket/client.rb +35 -70
- data/lib/async/websocket/connect_request.rb +89 -0
- data/lib/async/websocket/connect_response.rb +44 -0
- data/lib/async/websocket/connection.rb +12 -3
- data/lib/async/websocket/proxy.rb +0 -0
- data/lib/async/websocket/request.rb +71 -0
- data/lib/async/websocket/response.rb +41 -0
- data/lib/async/websocket/server.rb +36 -1
- data/lib/async/websocket/upgrade_request.rb +106 -0
- data/lib/async/websocket/upgrade_response.rb +52 -0
- data/lib/async/websocket/version.rb +1 -1
- data/spec/async/websocket/{server → adapters}/rack/client.rb +2 -2
- data/spec/async/websocket/{server → adapters}/rack/config.ru +2 -2
- data/spec/async/websocket/{server → adapters}/rack_spec.rb +11 -10
- data/spec/async/websocket/client_spec.rb +15 -3
- data/spec/async/websocket/server_examples.rb +77 -0
- data/spec/async/websocket/server_spec.rb +31 -0
- metadata +24 -13
- data/lib/async/websocket/server/rack.rb +0 -104
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA256:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: 0d90af5dc72de1e262ea96046aac6d5aea83dd2dba9d7a6f98684da3107d152d
|
4
|
+
data.tar.gz: 9544351dc684062ea0a78b50d15bf9441d18f401fd23fc30eae768a8ef45cd86
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
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/
|
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::
|
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
|
data/async-websocket.gemspec
CHANGED
@@ -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 "
|
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"
|
data/examples/chat/client.rb
CHANGED
@@ -2,12 +2,12 @@
|
|
2
2
|
|
3
3
|
require 'async'
|
4
4
|
require 'async/io/stream'
|
5
|
-
require 'async/http/
|
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::
|
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/
|
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::
|
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
|
data/examples/mud/client.rb
CHANGED
@@ -2,7 +2,7 @@
|
|
2
2
|
|
3
3
|
require 'async'
|
4
4
|
require 'async/io/stream'
|
5
|
-
require 'async/http/
|
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::
|
16
|
+
endpoint = Async::HTTP::Endpoint.parse(URL)
|
17
17
|
|
18
18
|
Async::WebSocket::Client.open(endpoint) do |connection|
|
19
19
|
task.async do
|
data/examples/utopia/Gemfile
CHANGED
@@ -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
|
-
|
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/
|
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::
|
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
|
data/lib/async/websocket.rb
CHANGED
@@ -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
|
-
|
24
|
-
require 'protocol/websocket/digest'
|
23
|
+
require_relative 'request'
|
25
24
|
|
26
|
-
require '
|
25
|
+
require 'protocol/websocket/headers'
|
27
26
|
|
28
|
-
|
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
|
-
|
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
|
-
|
45
|
-
|
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
|
-
|
51
|
-
@key = key
|
38
|
+
return client unless block_given?
|
52
39
|
|
53
|
-
|
54
|
-
|
55
|
-
|
56
|
-
|
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
|
-
|
64
|
-
|
65
|
-
|
66
|
-
|
67
|
-
|
68
|
-
|
69
|
-
|
70
|
-
|
71
|
-
|
72
|
-
|
73
|
-
|
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
|
-
|
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
|
-
|
64
|
+
@options = options
|
96
65
|
end
|
97
66
|
|
98
|
-
def
|
99
|
-
|
100
|
-
client.upgrade!("websocket")
|
67
|
+
def connect(path, headers: [], handler: Connection, **options)
|
68
|
+
request = Request.new(nil, nil, path, headers, **options)
|
101
69
|
|
102
|
-
|
103
|
-
stream = client.write_upgrade_body
|
70
|
+
response = self.call(request)
|
104
71
|
|
105
|
-
|
72
|
+
unless response.stream?
|
73
|
+
raise ProtocolError, "Failed to negotiate connection: #{response.status}"
|
74
|
+
end
|
106
75
|
|
107
|
-
|
76
|
+
protocol = response.headers[SEC_WEBSOCKET_PROTOCOL]&.first
|
77
|
+
framer = Protocol::WebSocket::Framer.new(response.stream)
|
108
78
|
|
109
|
-
|
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
|