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