wands 0.1.0

Sign up to get free protection for your applications and to get access to all the features.
checksums.yaml ADDED
@@ -0,0 +1,7 @@
1
+ ---
2
+ SHA256:
3
+ metadata.gz: ed955880d15ca35b40902f4be003d4615c049b113f7408873a64a0553a1a9e78
4
+ data.tar.gz: da44bcd5f0c393bf14ff2c55d16acd1eef21ac2073ad60f3e58b5d267fbc4db7
5
+ SHA512:
6
+ metadata.gz: e75f52070180a29dee8a4d2a0dbfa0d7bf077cf09d917948610f6db0c06b4be21c5ad3a91adb62f5d2992746488e08580d0e9780e09d7d528dc4b111055ba35d
7
+ data.tar.gz: c2dcf0cc618b07e62f79f5113c4596b848a77122cb97e60457bebf108404122898261a7610de4fc0c119c3aa629c9705a630f4fc17d2d6308be98d625de4041c
data/.idea/.gitignore ADDED
@@ -0,0 +1,3 @@
1
+ # Default ignored files
2
+ /shelf/
3
+ /workspace.xml
data/.idea/misc.xml ADDED
@@ -0,0 +1,4 @@
1
+ <?xml version="1.0" encoding="UTF-8"?>
2
+ <project version="4">
3
+ <component name="ProjectRootManager" version="2" project-jdk-name="rbenv: 3.3.5" project-jdk-type="RUBY_SDK" />
4
+ </project>
data/.idea/modules.xml ADDED
@@ -0,0 +1,8 @@
1
+ <?xml version="1.0" encoding="UTF-8"?>
2
+ <project version="4">
3
+ <component name="ProjectModuleManager">
4
+ <modules>
5
+ <module fileurl="file://$PROJECT_DIR$/.idea/wands.iml" filepath="$PROJECT_DIR$/.idea/wands.iml" />
6
+ </modules>
7
+ </component>
8
+ </project>
data/.idea/vcs.xml ADDED
@@ -0,0 +1,6 @@
1
+ <?xml version="1.0" encoding="UTF-8"?>
2
+ <project version="4">
3
+ <component name="VcsDirectoryMappings">
4
+ <mapping directory="" vcs="Git" />
5
+ </component>
6
+ </project>
data/.idea/wands.iml ADDED
@@ -0,0 +1,25 @@
1
+ <?xml version="1.0" encoding="UTF-8"?>
2
+ <module type="RUBY_MODULE" version="4">
3
+ <component name="ModuleRunConfigurationManager">
4
+ <shared />
5
+ </component>
6
+ <component name="NewModuleRootManager">
7
+ <content url="file://$MODULE_DIR$">
8
+ <sourceFolder url="file://$MODULE_DIR$/features" isTestSource="true" />
9
+ <sourceFolder url="file://$MODULE_DIR$/spec" isTestSource="true" />
10
+ <sourceFolder url="file://$MODULE_DIR$/test" isTestSource="true" />
11
+ </content>
12
+ <orderEntry type="inheritedJdk" />
13
+ <orderEntry type="sourceFolder" forTests="false" />
14
+ <orderEntry type="library" scope="PROVIDED" name="ast (v2.4.2, rbenv: 3.3.5) [gem]" level="application" />
15
+ <orderEntry type="library" scope="PROVIDED" name="fiber-annotation (v0.2.0, rbenv: 3.3.5) [gem]" level="application" />
16
+ <orderEntry type="library" scope="PROVIDED" name="fiber-local (v1.1.0, rbenv: 3.3.5) [gem]" level="application" />
17
+ <orderEntry type="library" scope="PROVIDED" name="fiber-storage (v1.0.0, rbenv: 3.3.5) [gem]" level="application" />
18
+ <orderEntry type="library" scope="PROVIDED" name="language_server-protocol (v3.17.0.3, rbenv: 3.3.5) [gem]" level="application" />
19
+ <orderEntry type="library" scope="PROVIDED" name="protocol-websocket (v0.20.1, rbenv: 3.3.5) [gem]" level="application" />
20
+ <orderEntry type="library" scope="PROVIDED" name="racc (v1.8.1, rbenv: 3.3.5) [gem]" level="application" />
21
+ <orderEntry type="library" scope="PROVIDED" name="rainbow (v3.1.1, rbenv: 3.3.5) [gem]" level="application" />
22
+ <orderEntry type="library" scope="PROVIDED" name="rake (v13.2.1, rbenv: 3.3.5) [gem]" level="application" />
23
+ <orderEntry type="library" scope="PROVIDED" name="stringio (v3.1.2, rbenv: 3.3.5) [gem]" level="application" />
24
+ </component>
25
+ </module>
data/.rubocop.yml ADDED
@@ -0,0 +1,20 @@
1
+ AllCops:
2
+ TargetRubyVersion: 3.1
3
+ NewCops: enable
4
+
5
+ Metrics/MethodLength:
6
+ Max: 15
7
+
8
+ Style/StringLiterals:
9
+ EnforcedStyle: double_quotes
10
+
11
+ Style/StringLiteralsInInterpolation:
12
+ EnforcedStyle: double_quotes
13
+
14
+ Layout/LineLength:
15
+ Exclude:
16
+ - wands.gemspec
17
+
18
+ require:
19
+ - rubocop-minitest
20
+ - rubocop-rake
data/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2024 shigeru.nakajima
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 all
13
+ 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 THE
21
+ SOFTWARE.
data/README.md ADDED
@@ -0,0 +1,50 @@
1
+ # Wands
2
+
3
+ A low-level WebSocket library compatible with Ruby's TCPSocket.
4
+
5
+ ## Installation
6
+
7
+ Install the gem and add to the application's Gemfile by executing:
8
+
9
+ ```bash
10
+ bundle add wands
11
+ ```
12
+
13
+ If bundler is not being used to manage dependencies, install the gem by executing:
14
+
15
+ ```bash
16
+ gem install wands
17
+ ```
18
+
19
+ ## Usage
20
+
21
+ ### Server
22
+
23
+ ```ruby
24
+ server = Wands::WebSocketServer.open('localhost', 2345)
25
+
26
+ loop do
27
+ client = server.accept
28
+ client.write("Hello World!")
29
+ client.close
30
+ end
31
+ ```
32
+
33
+ ### Client
34
+
35
+ ```ruby
36
+ web_socket = Wands::WebSocket.open('localhost', 2345)
37
+ web_socket.write("Hello World!")
38
+ puts web_socket.gets
39
+ web_socket.close
40
+ ```
41
+
42
+ ## Development
43
+
44
+ After checking out the repo, run `bin/setup` to install dependencies. Then, run `rake test` to run the tests. You can also run `bin/console` for an interactive prompt that will allow you to experiment.
45
+
46
+ To install this gem onto your local machine, run `bundle exec rake install`. To release a new version, update the version number in `version.rb`, and then run `bundle exec rake release`, which will create a git tag for the version, push git commits and the created tag, and push the `.gem` file to [rubygems.org](https://rubygems.org).
47
+
48
+ ## Contributing
49
+
50
+ Bug reports and pull requests are welcome on GitHub at https://github.com/ledsun/wands.
data/Rakefile ADDED
@@ -0,0 +1,12 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "bundler/gem_tasks"
4
+ require "minitest/test_task"
5
+
6
+ Minitest::TestTask.create
7
+
8
+ require "rubocop/rake_task"
9
+
10
+ RuboCop::RakeTask.new
11
+
12
+ task default: %i[test rubocop]
@@ -0,0 +1,71 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Wands
4
+ # This is a class that parses the response from the server and stores the headers in a hash.
5
+ # The parse and header methods in this class are modeled on WEBrick::HTTPRequest.
6
+ #
7
+ # The expected HTTP response string is:
8
+ #
9
+ # HTTP/1.1 101 Switching Protocols
10
+ # Upgrade: websocket
11
+ # Connection: Upgrade
12
+ # Sec-WebSocket-Accept: s3pPLMBiTxaQ9kYGzzhZRbK+xOo=
13
+ # Sec-WebSocket-Protocol: chat
14
+ # Sec-WebSocket-Version: 13
15
+ #
16
+
17
+ # Example usage:
18
+ #
19
+ # response = HTTPResponse.new
20
+ # response.parse(socket)
21
+ # response.header["upgrade"] # => ["websocket"]
22
+ # response.header["connection"] # => ["Upgrade"]
23
+ # response.header["sec-websocket-accept"] # => ["s3pPLMBiTxaQ9kYGzzhZRbK+xOo="]
24
+ #
25
+ class HTTPResponse
26
+ attr_reader :status, :header
27
+
28
+ def parse(stream)
29
+ @response = read_from stream
30
+ @status, @header = headers_of @response
31
+ end
32
+
33
+ def to_s
34
+ @response
35
+ end
36
+
37
+ private
38
+
39
+ def read_from(stream)
40
+ response_string = ""
41
+ while (line = stream.gets) != "\r\n"
42
+ response_string += line
43
+ end
44
+
45
+ response_string
46
+ end
47
+
48
+ # Parse the headers from the HTTP response string.
49
+ def headers_of(response_string)
50
+ # Split the response string into headers and body.
51
+ headers, _body = response_string.split("\r\n\r\n", 2)
52
+
53
+ # Split the headers into lines.
54
+ headers_lines = headers.split("\r\n")
55
+
56
+ # The first line is the status line.
57
+ # We don't need it, so we remove it from the headers.
58
+ status_line = headers_lines.shift
59
+ status_code = status_line.split[1]
60
+
61
+ # Parse the headers into a hash.
62
+ headers = headers_lines.to_h do |line|
63
+ # Split the line into header name and value.
64
+ header_name, value = line.split(": ", 2)
65
+ [header_name.downcase, [value.strip]]
66
+ end
67
+
68
+ [status_code, headers]
69
+ end
70
+ end
71
+ end
@@ -0,0 +1,17 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Wands
4
+ # This exception is raised when the response from the server is not as expected.
5
+ class ResponseException < StandardError
6
+ attr_reader :response
7
+
8
+ def initialize(message, response)
9
+ super(message)
10
+ @response = response
11
+ end
12
+
13
+ def to_s
14
+ "#{super} from '#{response}'"
15
+ end
16
+ end
17
+ end
@@ -0,0 +1,40 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "erb"
4
+ require "protocol/websocket/headers"
5
+ require_relative "response_exception"
6
+
7
+ module Wands
8
+ # The request is used to upgrade the HTTP connection to a WebSocket connection.
9
+ class UpgradeRequest
10
+ include ::Protocol::WebSocket::Headers
11
+
12
+ # The Host header is required in HTTP 1.1.
13
+ TEMPLATE = <<~REQUEST
14
+ GET / HTTP/1.1
15
+ Host: <%= @host %>:<%= @port %>
16
+ Connection: Upgrade
17
+ Upgrade: websocket
18
+ Sec-WebSocket-Version: 13
19
+ Sec-WebSocket-Key: <%= @key %>
20
+
21
+ REQUEST
22
+
23
+ ERB = ERB.new(TEMPLATE).freeze
24
+
25
+ def initialize(host, port)
26
+ @host = host
27
+ @port = port
28
+ @key = Nounce.generate_key
29
+ end
30
+
31
+ def to_s
32
+ ERB.result(binding).gsub(/\r?\n/, "\r\n")
33
+ end
34
+
35
+ def verify(response)
36
+ accept_digest = response.header[SEC_WEBSOCKET_ACCEPT].first
37
+ accept_digest == Nounce.accept_digest(@key) || raise(ResponseException.new("Invalid accept digest", response))
38
+ end
39
+ end
40
+ end
@@ -0,0 +1,5 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Wands
4
+ VERSION = "0.1.0"
5
+ end
@@ -0,0 +1,65 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "socket"
4
+ require "forwardable"
5
+ require "protocol/websocket/headers"
6
+ require "protocol/websocket/framer"
7
+ require "protocol/websocket/text_frame"
8
+ require_relative "upgrade_request"
9
+ require_relative "http_response"
10
+ require_relative "response_exception"
11
+
12
+ module Wands
13
+ # This is a class that represents WebSocket, which has the same interface as TCPSocket.
14
+ #
15
+ # The WebSocket class is responsible for reading and writing messages to the server.
16
+ #
17
+ # Example usage:
18
+ #
19
+ # web_socket = WebSocket.open('localhost', 2345)
20
+ # web_socket.write("Hello World!")
21
+ #
22
+ # puts web_socket.gets
23
+ #
24
+ # web_socket.close
25
+ #
26
+ class WebSocket
27
+ include Protocol::WebSocket::Headers
28
+ extend Forwardable
29
+
30
+ def_delegators :@socket, :addr, :remote_address, :close, :to_io
31
+
32
+ def self.open(host, port)
33
+ socket = TCPSocket.new(host, port)
34
+ request = UpgradeRequest.new(host, port)
35
+ socket.write(request.to_s)
36
+ socket.flush
37
+
38
+ response = HTTPResponse.new
39
+ response.parse(socket)
40
+ raise ResponseException.new("Bad Status", response) unless response.status == "101"
41
+
42
+ request.verify response
43
+
44
+ new(socket)
45
+ end
46
+
47
+ def initialize(socket)
48
+ @socket = socket
49
+ end
50
+
51
+ # @return [String]
52
+ def gets
53
+ framer = Protocol::WebSocket::Framer.new(@socket)
54
+ frame = framer.read_frame
55
+ raise "frame is not a text" unless frame.is_a? Protocol::WebSocket::TextFrame
56
+
57
+ frame.unpack
58
+ end
59
+
60
+ def write(message)
61
+ frame = Protocol::WebSocket::TextFrame.new(true, message)
62
+ frame.write(@socket)
63
+ end
64
+ end
65
+ end
@@ -0,0 +1,93 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "socket"
4
+ require "protocol/websocket/headers"
5
+ require "webrick/httprequest"
6
+ require "webrick/httpresponse"
7
+ require "webrick/config"
8
+ require_relative "web_socket"
9
+
10
+ module Wands
11
+ # The WebSocketServer class is responsible for accepting WebSocket connections.
12
+ # This class has the same interface as TCPServer.
13
+ #
14
+ # Example usage:
15
+ #
16
+ # server = WebSocketServer.new('localhost', 2345)
17
+ # loop do
18
+ # begin
19
+ # socket = server.accept
20
+ # next unless socket
21
+ # puts "Accepted connection from #{socket.remote_address.ip_address} #{socket.remote_address.ip_port}"
22
+ #
23
+ # received_message = socket.gets
24
+ # puts "Received: #{received_message}"
25
+ #
26
+ # socket.write received_message
27
+ # socket.close
28
+ # rescue WEBrick::HTTPStatus::EOFError => e
29
+ # STDERR.puts e.message
30
+ # rescue Errno::ECONNRESET => e
31
+ # STDERR.puts "#{e.message} #{socket.remote_address.ip_address} #{socket.remote_address.ip_port}"
32
+ # rescue EOFError => e
33
+ # STDERR.puts "#{e.message} #{socket.remote_address.ip_address} #{socket.remote_address.ip_port}"
34
+ # end
35
+ # end
36
+ #
37
+ class WebSocketServer
38
+ include Protocol::WebSocket::Headers
39
+
40
+ def self.open(hostname, port)
41
+ new(hostname, port)
42
+ end
43
+
44
+ def initialize(hostname, port)
45
+ @tcp_server = TCPServer.new hostname, port
46
+ end
47
+
48
+ def accept
49
+ socket = @tcp_server.accept
50
+
51
+ request = WEBrick::HTTPRequest.new(WEBrick::Config::HTTP)
52
+ headers = read_headers_from request, socket
53
+ unless headers["upgrade"].include? PROTOCOL
54
+ socket.close
55
+ raise "Not a websocket request"
56
+ end
57
+
58
+ response = response_to headers
59
+ response.send_response socket
60
+
61
+ WebSocket.new socket
62
+ rescue WEBrick::HTTPStatus::BadRequest => e
63
+ warn "WEBRick error message: #{e.full_message}"
64
+ warn "HTTP request string: #{request}" if request
65
+ if socket
66
+ socket.write "HTTP/1.1 400 Bad Request\r\n\r\n"
67
+ socket.close
68
+ end
69
+ end
70
+
71
+ private
72
+
73
+ def read_headers_from(request, socket)
74
+ request.parse(socket)
75
+ request.header
76
+ end
77
+
78
+ def response_to(headers)
79
+ response_key = calculate_accept_nonce_from headers
80
+ response = WEBrick::HTTPResponse.new(WEBrick::Config::HTTP)
81
+ response.status = 101
82
+ response.upgrade! PROTOCOL
83
+ response[SEC_WEBSOCKET_ACCEPT] = response_key
84
+
85
+ response
86
+ end
87
+
88
+ def calculate_accept_nonce_from(headers)
89
+ key = headers[SEC_WEBSOCKET_KEY].first
90
+ Nounce.accept_digest(key)
91
+ end
92
+ end
93
+ end
data/lib/wands.rb ADDED
@@ -0,0 +1,5 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative "wands/version"
4
+ require_relative "wands/web_socket"
5
+ require_relative "wands/web_socket_server"
data/sig/wands.rbs ADDED
@@ -0,0 +1,30 @@
1
+ module Wands
2
+ VERSION: String
3
+
4
+ class WebSocket
5
+ @socket: TCPSocket
6
+
7
+ def initialize: (TCPSocket) -> void
8
+ def gets: () -> String
9
+ def write: (String) -> void
10
+ def self.open: (String, Integer) -> WebSocket
11
+ def close: () -> void
12
+ def addr: () -> Addrinfo
13
+ def remote_address: () -> Addrinfo
14
+ end
15
+
16
+ class WebSocketServer
17
+ @tcp_server: TCPServer
18
+
19
+ def initialize: (String, Integer) -> void
20
+ def self.open: (String, Integer) -> WebSocketServer
21
+ def accept: () -> WebSocket
22
+ def close: () -> void
23
+
24
+ private
25
+
26
+ def calculate_accept_nonce_from: (Hash[String, Array[String]]?) -> String
27
+ def read_headers_from: (WEBrick::HTTPRequest, TCPSocket) -> Hash[String, Array[String]]?
28
+ def response_to: (Hash[String, Array[String]]?) -> WEBrick::HTTPResponse
29
+ end
30
+ end
metadata ADDED
@@ -0,0 +1,90 @@
1
+ --- !ruby/object:Gem::Specification
2
+ name: wands
3
+ version: !ruby/object:Gem::Version
4
+ version: 0.1.0
5
+ platform: ruby
6
+ authors:
7
+ - shigeru.nakajima
8
+ bindir: exe
9
+ cert_chain: []
10
+ date: 2024-12-31 00:00:00.000000000 Z
11
+ dependencies:
12
+ - !ruby/object:Gem::Dependency
13
+ name: protocol-websocket
14
+ requirement: !ruby/object:Gem::Requirement
15
+ requirements:
16
+ - - "~>"
17
+ - !ruby/object:Gem::Version
18
+ version: '0.20'
19
+ type: :runtime
20
+ prerelease: false
21
+ version_requirements: !ruby/object:Gem::Requirement
22
+ requirements:
23
+ - - "~>"
24
+ - !ruby/object:Gem::Version
25
+ version: '0.20'
26
+ - !ruby/object:Gem::Dependency
27
+ name: webrick
28
+ requirement: !ruby/object:Gem::Requirement
29
+ requirements:
30
+ - - "~>"
31
+ - !ruby/object:Gem::Version
32
+ version: '1.9'
33
+ type: :runtime
34
+ prerelease: false
35
+ version_requirements: !ruby/object:Gem::Requirement
36
+ requirements:
37
+ - - "~>"
38
+ - !ruby/object:Gem::Version
39
+ version: '1.9'
40
+ description: A WebSocket library with the same layer as Ruby's TCPSocket, which does
41
+ not have the function of an HTTP server, in order to use WebSocket as a new protocol
42
+ for dRuby.
43
+ email:
44
+ - shigeru.nakajima@gmail.com
45
+ executables: []
46
+ extensions: []
47
+ extra_rdoc_files: []
48
+ files:
49
+ - ".idea/.gitignore"
50
+ - ".idea/misc.xml"
51
+ - ".idea/modules.xml"
52
+ - ".idea/vcs.xml"
53
+ - ".idea/wands.iml"
54
+ - ".rubocop.yml"
55
+ - LICENSE
56
+ - README.md
57
+ - Rakefile
58
+ - lib/wands.rb
59
+ - lib/wands/http_response.rb
60
+ - lib/wands/response_exception.rb
61
+ - lib/wands/upgrade_request.rb
62
+ - lib/wands/version.rb
63
+ - lib/wands/web_socket.rb
64
+ - lib/wands/web_socket_server.rb
65
+ - sig/wands.rbs
66
+ homepage: https://github.com/ledsun/wands
67
+ licenses:
68
+ - MIT
69
+ metadata:
70
+ allowed_push_host: https://rubygems.org
71
+ homepage_uri: https://github.com/ledsun/wands
72
+ rubygems_mfa_required: 'true'
73
+ rdoc_options: []
74
+ require_paths:
75
+ - lib
76
+ required_ruby_version: !ruby/object:Gem::Requirement
77
+ requirements:
78
+ - - ">="
79
+ - !ruby/object:Gem::Version
80
+ version: 3.1.0
81
+ required_rubygems_version: !ruby/object:Gem::Requirement
82
+ requirements:
83
+ - - ">="
84
+ - !ruby/object:Gem::Version
85
+ version: '0'
86
+ requirements: []
87
+ rubygems_version: 3.6.2
88
+ specification_version: 4
89
+ summary: A low-level WebSocket library compatible with Ruby's TCPSocket.
90
+ test_files: []