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.
@@ -19,6 +19,7 @@
19
19
  # THE SOFTWARE.
20
20
 
21
21
  require 'protocol/websocket/connection'
22
+ require 'protocol/websocket/headers'
22
23
 
23
24
  require 'json'
24
25
 
@@ -28,8 +29,10 @@ module Async
28
29
 
29
30
  # This is a basic synchronous websocket client:
30
31
  class Connection < ::Protocol::WebSocket::Connection
31
- def self.call(framer, protocol = nil, **options)
32
- self.new(framer, protocol, **options)
32
+ include ::Protocol::WebSocket::Headers
33
+
34
+ def self.call(framer, protocol = [], **options)
35
+ return self.new(framer, Array(protocol).first, **options)
33
36
  end
34
37
 
35
38
  def initialize(framer, protocol = nil, **options)
@@ -40,7 +43,9 @@ module Async
40
43
  attr :protocol
41
44
 
42
45
  def read
43
- parse(super)
46
+ if buffer = super
47
+ parse(buffer)
48
+ end
44
49
  end
45
50
 
46
51
  def write(object)
@@ -54,6 +59,10 @@ module Async
54
59
  def dump(object)
55
60
  JSON.dump(object)
56
61
  end
62
+
63
+ def call
64
+ self.close
65
+ end
57
66
  end
58
67
  end
59
68
  end
File without changes
@@ -0,0 +1,71 @@
1
+ # Copyright, 2017, 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_relative 'connect_request'
22
+ require_relative 'upgrade_request'
23
+
24
+ module Async
25
+ module WebSocket
26
+ class Request
27
+ include ::Protocol::WebSocket::Headers
28
+
29
+ def self.websocket?(request)
30
+ Array(request.protocol).include?(PROTOCOL)
31
+ end
32
+
33
+ def initialize(scheme = nil, authority = nil, path = nil, headers = [], **options)
34
+ @scheme = scheme
35
+ @authority = authority
36
+ @path = path
37
+ @headers = headers
38
+
39
+ @options = options
40
+
41
+ @body = nil
42
+ end
43
+
44
+ attr_accessor :scheme
45
+ attr_accessor :authority
46
+ attr_accessor :path
47
+ attr_accessor :headers
48
+
49
+ attr_accessor :body
50
+
51
+ # Send the request to the given connection.
52
+ def call(connection)
53
+ if connection.http1?
54
+ return UpgradeRequest.new(self, **@options).call(connection)
55
+ elsif connection.http2?
56
+ return ConnectRequest.new(self, **@options).call(connection)
57
+ end
58
+
59
+ raise HTTP::Error, "Unsupported HTTP version: #{connection.version}!"
60
+ end
61
+
62
+ def idempotent?
63
+ true
64
+ end
65
+
66
+ def to_s
67
+ "\#<#{self.class} #{@scheme}://#{@authority}: #{@path}>"
68
+ end
69
+ end
70
+ end
71
+ end
@@ -0,0 +1,41 @@
1
+ # Copyright, 2017, 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_relative 'upgrade_response'
22
+ require_relative 'connect_response'
23
+
24
+ require_relative 'error'
25
+
26
+ module Async
27
+ module WebSocket
28
+ module Response
29
+ # Send the request to the given connection.
30
+ def self.for(request, headers = [], **options, &body)
31
+ if request.version =~ /http\/1/i
32
+ return UpgradeResponse.new(request, headers, **options, &body)
33
+ elsif request.version =~ /h2/i
34
+ return ConnectResponse.new(request, headers, **options, &body)
35
+ end
36
+
37
+ raise ProtocolError, "Unsupported HTTP version: #{request.version}!"
38
+ end
39
+ end
40
+ end
41
+ end
@@ -19,10 +19,45 @@
19
19
  # THE SOFTWARE.
20
20
 
21
21
  require_relative 'connection'
22
+ require_relative 'response'
23
+
24
+ require 'protocol/http/middleware'
22
25
 
23
26
  module Async
24
27
  module WebSocket
25
- module Server
28
+ class Server < ::Protocol::HTTP::Middleware
29
+ include ::Protocol::WebSocket::Headers
30
+
31
+ def initialize(delegate, protocols: [], handler: Connection)
32
+ super(delegate)
33
+
34
+ @protocols = protocols
35
+ @handler = handler
36
+ end
37
+
38
+ def select_protocol(request)
39
+ if requested_protocol = request.headers[SEC_WEBSOCKET_PROTOCOL]
40
+ return (requested_protocol & @protocols).first
41
+ end
42
+ end
43
+
44
+ def response(request)
45
+ end
46
+
47
+ def call(request)
48
+ if request.protocol == PROTOCOL
49
+ # Select websocket sub-protocol:
50
+ protocol = select_protocol(request)
51
+
52
+ Response.for(request, headers, protocol: protocol, **options) do |stream|
53
+ framer = Protocol::WebSocket::Framer.new(stream)
54
+
55
+ yield handler.call(framer, protocol)
56
+ end
57
+ else
58
+ super
59
+ end
60
+ end
26
61
  end
27
62
  end
28
63
  end
@@ -0,0 +1,106 @@
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/middleware'
24
+ require 'protocol/http/request'
25
+
26
+ require 'protocol/http/headers'
27
+ require 'protocol/websocket/headers'
28
+
29
+ require 'securerandom'
30
+
31
+ require_relative 'error'
32
+
33
+ module Async
34
+ module WebSocket
35
+ # This is required for HTTP/1.x to upgrade the connection to the WebSocket protocol.
36
+ class UpgradeRequest < ::Protocol::HTTP::Request
37
+ include ::Protocol::WebSocket::Headers
38
+
39
+ class Wrapper
40
+ def initialize(response)
41
+ @response = response
42
+ @stream = nil
43
+ end
44
+
45
+ def stream?
46
+ @response.status == 101
47
+ end
48
+
49
+ def status
50
+ @response.status
51
+ end
52
+
53
+ def headers
54
+ @response.headers
55
+ end
56
+
57
+ def body?
58
+ false
59
+ end
60
+
61
+ def body
62
+ nil
63
+ end
64
+
65
+ def protocol
66
+ @response.protocol
67
+ end
68
+
69
+ def stream
70
+ @stream ||= @response.hijack!
71
+ end
72
+ end
73
+
74
+ def initialize(request, protocols: [], version: 13)
75
+ @key = Nounce.generate_key
76
+
77
+ headers = [
78
+ [SEC_WEBSOCKET_KEY, @key],
79
+ [SEC_WEBSOCKET_VERSION, version],
80
+ ]
81
+
82
+ if protocols.any?
83
+ headers << [SEC_WEBSOCKET_PROTOCOL, protocols.join(',')]
84
+ end
85
+
86
+ merged_headers = ::Protocol::HTTP::Headers::Merged.new(request.headers, headers)
87
+
88
+ super(request.scheme, request.authority, ::Protocol::HTTP::Methods::GET, request.path, nil, merged_headers, nil, PROTOCOL)
89
+ end
90
+
91
+ def call(connection)
92
+ response = super
93
+
94
+ if accept_digest = response.headers[SEC_WEBSOCKET_ACCEPT]&.first
95
+ expected_accept_digest = Nounce.accept_digest(@key)
96
+
97
+ unless accept_digest and accept_digest == expected_accept_digest
98
+ raise ProtocolError, "Invalid accept digest, expected #{expected_accept_digest.inspect}, got #{accept_digest.inspect}!"
99
+ end
100
+ end
101
+
102
+ return Wrapper.new(response)
103
+ end
104
+ end
105
+ end
106
+ end
@@ -0,0 +1,52 @@
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 'async/http/body/hijack'
24
+ require 'protocol/http/response'
25
+ require 'protocol/websocket/headers'
26
+
27
+ module Async
28
+ module WebSocket
29
+ # The response from the server back to the client for negotiating HTTP/1.x WebSockets.
30
+ class UpgradeResponse < ::Protocol::HTTP::Response
31
+ include ::Protocol::WebSocket::Headers
32
+
33
+ def initialize(request, headers = nil, protocol: nil, &block)
34
+ headers = Protocol::HTTP::Headers::Merged.new(headers)
35
+
36
+ if accept_nounce = request.headers[SEC_WEBSOCKET_KEY]&.first
37
+ headers << [[SEC_WEBSOCKET_ACCEPT, Nounce.accept_digest(accept_nounce)]]
38
+ status = 101
39
+ else
40
+ status = 400
41
+ end
42
+
43
+ if protocol
44
+ headers << [[SEC_WEBSOCKET_PROTOCOL, protocol]]
45
+ end
46
+
47
+ body = Async::HTTP::Body::Hijack.wrap(request, &block)
48
+ super(request.version, status, nil, headers, body, PROTOCOL)
49
+ end
50
+ end
51
+ end
52
+ end
@@ -20,6 +20,6 @@
20
20
 
21
21
  module Async
22
22
  module WebSocket
23
- VERSION = "0.10.0"
23
+ VERSION = "0.11.0"
24
24
  end
25
25
  end
@@ -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
  headers = {'token' => 'wubalubadubdub'}
18
18
 
19
19
  Async::WebSocket::Client.open(endpoint, headers: headers) do |connection|
@@ -1,12 +1,12 @@
1
1
  #!/usr/bin/env -S falcon serve --bind http://localhost:7070 --count 1 -c
2
2
 
3
- require 'async/websocket/server/rack'
3
+ require 'async/websocket/adapters/rack'
4
4
  require 'set'
5
5
 
6
6
  $connections = Set.new
7
7
 
8
8
  run lambda {|env|
9
- Async::WebSocket::Server::Rack.open(env, supported_protocols: ['ws']) do |connection|
9
+ Async::WebSocket::Adapters::Rack.open(env, protocols: ['ws']) do |connection|
10
10
  $connections << connection
11
11
 
12
12
  begin
@@ -20,20 +20,20 @@
20
20
 
21
21
  require 'async/websocket'
22
22
  require 'async/websocket/client'
23
- require 'async/websocket/server/rack'
23
+ require 'async/websocket/adapters/rack'
24
24
 
25
25
  require 'rack/test'
26
26
  require 'falcon/server'
27
27
  require 'falcon/adapters/rack'
28
- require 'async/http/url_endpoint'
28
+ require 'async/http/endpoint'
29
29
 
30
- RSpec.describe Async::WebSocket::Server::Rack do
30
+ RSpec.describe Async::WebSocket::Adapters::Rack do
31
31
  include_context Async::RSpec::Reactor
32
32
 
33
- let(:server_address) {Async::HTTP::URLEndpoint.parse("http://localhost:7050")}
33
+ let(:endpoint) {Async::HTTP::Endpoint.parse("http://localhost:7050")}
34
34
  let(:app) {Rack::Builder.parse_file(File.expand_path('rack/config.ru', __dir__)).first}
35
- let(:server) {Falcon::Server.new(Falcon::Server.middleware(app), server_address)}
36
- let(:client) {Async::HTTP::Client.new(server_address)}
35
+ let(:server) {Falcon::Server.new(Falcon::Server.middleware(app), endpoint)}
36
+ let(:client) {Async::HTTP::Client.new(endpoint)}
37
37
 
38
38
  let!(:server_task) do
39
39
  reactor.async do
@@ -47,6 +47,7 @@ RSpec.describe Async::WebSocket::Server::Rack do
47
47
 
48
48
  it "can make non-websocket connection to server" do
49
49
  response = client.get("/")
50
+
50
51
  expect(response).to be_success
51
52
  expect(response.read).to be == "Hello World"
52
53
 
@@ -58,7 +59,7 @@ RSpec.describe Async::WebSocket::Server::Rack do
58
59
  end
59
60
 
60
61
  it "can make websocket connection to server" do
61
- Async::WebSocket::Client.open(server_address) do |connection|
62
+ Async::WebSocket::Client.connect(endpoint) do |connection|
62
63
  connection.write(message)
63
64
 
64
65
  expect(connection.read).to be == message
@@ -68,15 +69,15 @@ RSpec.describe Async::WebSocket::Server::Rack do
68
69
  end
69
70
 
70
71
  it "should use mask over insecure connection" do
71
- expect(server_address).to_not be_secure
72
+ expect(endpoint).to_not be_secure
72
73
 
73
- Async::WebSocket::Client.open(server_address) do |connection|
74
+ Async::WebSocket::Client.connect(endpoint) do |connection|
74
75
  expect(connection.mask).to_not be_nil
75
76
  end
76
77
  end
77
78
 
78
79
  it "should negotiate protocol" do
79
- Async::WebSocket::Client.open(server_address, protocols: ['ws']) do |connection|
80
+ Async::WebSocket::Client.connect(endpoint, protocols: ['ws']) do |connection|
80
81
  expect(connection.protocol).to be == 'ws'
81
82
  end
82
83
  end