web_socket_rb 0.1.1

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.
Files changed (40) hide show
  1. checksums.yaml +7 -0
  2. data/.gitignore +10 -0
  3. data/.rspec +2 -0
  4. data/.ruby-gemset +1 -0
  5. data/.ruby-version +1 -0
  6. data/.travis.yml +4 -0
  7. data/CODE_OF_CONDUCT.md +49 -0
  8. data/Gemfile +4 -0
  9. data/LICENSE.txt +21 -0
  10. data/README.md +41 -0
  11. data/Rakefile +6 -0
  12. data/bin/console +14 -0
  13. data/bin/setup +8 -0
  14. data/examples/client/index.html +37 -0
  15. data/examples/server/server.rb +26 -0
  16. data/js/websocketrb_client.js +43 -0
  17. data/lib/web_socket_rb.rb +27 -0
  18. data/lib/web_socket_rb/context/sandbox.rb +29 -0
  19. data/lib/web_socket_rb/error/handshake_error.rb +17 -0
  20. data/lib/web_socket_rb/error/wrong_frame_error.rb +9 -0
  21. data/lib/web_socket_rb/protocol/frames_handler.rb +78 -0
  22. data/lib/web_socket_rb/protocol/handshake.rb +59 -0
  23. data/lib/web_socket_rb/routes.rb +27 -0
  24. data/lib/web_socket_rb/server.rb +47 -0
  25. data/lib/web_socket_rb/service/build_close_frame_service.rb +16 -0
  26. data/lib/web_socket_rb/service/build_ping_frame_service.rb +11 -0
  27. data/lib/web_socket_rb/service/build_pong_frame_service.rb +19 -0
  28. data/lib/web_socket_rb/service/build_text_frame_service.rb +17 -0
  29. data/lib/web_socket_rb/service/frames_sender.rb +47 -0
  30. data/lib/web_socket_rb/service/read_frame_service.rb +89 -0
  31. data/lib/web_socket_rb/version.rb +3 -0
  32. data/lib/web_socket_rb/wrapper/frame_base.rb +110 -0
  33. data/lib/web_socket_rb/wrapper/frame_close.rb +12 -0
  34. data/lib/web_socket_rb/wrapper/frame_outgoing.rb +20 -0
  35. data/lib/web_socket_rb/wrapper/frame_ping.rb +13 -0
  36. data/lib/web_socket_rb/wrapper/frame_pong.rb +12 -0
  37. data/lib/web_socket_rb/wrapper/frame_text.rb +13 -0
  38. data/lib/web_socket_rb/wrapper/handshake_request.rb +61 -0
  39. data/web_socket_rb.gemspec +33 -0
  40. metadata +139 -0
checksums.yaml ADDED
@@ -0,0 +1,7 @@
1
+ ---
2
+ SHA1:
3
+ metadata.gz: eb1f89b03d703f0ccb4d4993b3b291ea198c9ab1
4
+ data.tar.gz: 55820c722b44c2a238fdea8c859df4804cf9859d
5
+ SHA512:
6
+ metadata.gz: 8703dd1ab0f52f1c2b58b5360f3a6a32675e761106fd3d99e5a04793a9da18482a751ecc34cf17e1b6ca433ae6886f4c115594b8de28531362208761b11e1a27
7
+ data.tar.gz: 5ac78d00af0af664721ba5a11c5111aaa34176b386e1b4e41f3f0e8b38436f531ac7eb88eebcc534816af435ed6c22852692bcab37875ee44c032e004cfc9158
data/.gitignore ADDED
@@ -0,0 +1,10 @@
1
+ /.bundle/
2
+ /.yardoc
3
+ /Gemfile.lock
4
+ /_yardoc/
5
+ /coverage/
6
+ /doc/
7
+ /pkg/
8
+ /spec/reports/
9
+ /tmp/
10
+ /.idea/
data/.rspec ADDED
@@ -0,0 +1,2 @@
1
+ --format documentation
2
+ --color
data/.ruby-gemset ADDED
@@ -0,0 +1 @@
1
+ web_socket_rb
data/.ruby-version ADDED
@@ -0,0 +1 @@
1
+ ruby-2.2.3
data/.travis.yml ADDED
@@ -0,0 +1,4 @@
1
+ language: ruby
2
+ rvm:
3
+ - 2.2.3
4
+ before_install: gem install bundler -v 1.11.2
@@ -0,0 +1,49 @@
1
+ # Contributor Code of Conduct
2
+
3
+ As contributors and maintainers of this project, and in the interest of
4
+ fostering an open and welcoming community, we pledge to respect all people who
5
+ contribute through reporting issues, posting feature requests, updating
6
+ documentation, submitting pull requests or patches, and other activities.
7
+
8
+ We are committed to making participation in this project a harassment-free
9
+ experience for everyone, regardless of level of experience, gender, gender
10
+ identity and expression, sexual orientation, disability, personal appearance,
11
+ body size, race, ethnicity, age, religion, or nationality.
12
+
13
+ Examples of unacceptable behavior by participants include:
14
+
15
+ * The use of sexualized language or imagery
16
+ * Personal attacks
17
+ * Trolling or insulting/derogatory comments
18
+ * Public or private harassment
19
+ * Publishing other's private information, such as physical or electronic
20
+ addresses, without explicit permission
21
+ * Other unethical or unprofessional conduct
22
+
23
+ Project maintainers have the right and responsibility to remove, edit, or
24
+ reject comments, commits, code, wiki edits, issues, and other contributions
25
+ that are not aligned to this Code of Conduct, or to ban temporarily or
26
+ permanently any contributor for other behaviors that they deem inappropriate,
27
+ threatening, offensive, or harmful.
28
+
29
+ By adopting this Code of Conduct, project maintainers commit themselves to
30
+ fairly and consistently applying these principles to every aspect of managing
31
+ this project. Project maintainers who do not follow or enforce the Code of
32
+ Conduct may be permanently removed from the project team.
33
+
34
+ This code of conduct applies both within project spaces and in public spaces
35
+ when an individual is representing the project or its community.
36
+
37
+ Instances of abusive, harassing, or otherwise unacceptable behavior may be
38
+ reported by contacting a project maintainer at karol.bajko@rst-it.com. All
39
+ complaints will be reviewed and investigated and will result in a response that
40
+ is deemed necessary and appropriate to the circumstances. Maintainers are
41
+ obligated to maintain confidentiality with regard to the reporter of an
42
+ incident.
43
+
44
+ This Code of Conduct is adapted from the [Contributor Covenant][homepage],
45
+ version 1.3.0, available at
46
+ [http://contributor-covenant.org/version/1/3/0/][version]
47
+
48
+ [homepage]: http://contributor-covenant.org
49
+ [version]: http://contributor-covenant.org/version/1/3/0/
data/Gemfile ADDED
@@ -0,0 +1,4 @@
1
+ source 'https://rubygems.org'
2
+
3
+ # Specify your gem's dependencies in web_socket_rb.gemspec
4
+ gemspec
data/LICENSE.txt ADDED
@@ -0,0 +1,21 @@
1
+ The MIT License (MIT)
2
+
3
+ Copyright (c) 2016 Karol Bajko
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.
data/README.md ADDED
@@ -0,0 +1,41 @@
1
+ # WebSocketRb
2
+
3
+ Welcome to your new gem! In this directory, you'll find the files you need to be able to package up your Ruby library into a gem. Put your Ruby code in the file `lib/web_socket_rb`. To experiment with that code, run `bin/console` for an interactive prompt.
4
+
5
+ TODO: Delete this and the text above, and describe your gem
6
+
7
+ ## Installation
8
+
9
+ Add this line to your application's Gemfile:
10
+
11
+ ```ruby
12
+ gem 'web_socket_rb'
13
+ ```
14
+
15
+ And then execute:
16
+
17
+ $ bundle
18
+
19
+ Or install it yourself as:
20
+
21
+ $ gem install web_socket_rb
22
+
23
+ ## Usage
24
+
25
+ TODO: Write usage instructions here
26
+
27
+ ## Development
28
+
29
+ After checking out the repo, run `bin/setup` to install dependencies. Then, run `rake spec` to run the tests. You can also run `bin/console` for an interactive prompt that will allow you to experiment.
30
+
31
+ 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 tags, and push the `.gem` file to [rubygems.org](https://rubygems.org).
32
+
33
+ ## Contributing
34
+
35
+ Bug reports and pull requests are welcome on GitHub at https://github.com/[USERNAME]/web_socket_rb. This project is intended to be a safe, welcoming space for collaboration, and contributors are expected to adhere to the [Contributor Covenant](http://contributor-covenant.org) code of conduct.
36
+
37
+
38
+ ## License
39
+
40
+ The gem is available as open source under the terms of the [MIT License](http://opensource.org/licenses/MIT).
41
+
data/Rakefile ADDED
@@ -0,0 +1,6 @@
1
+ require 'bundler/gem_tasks'
2
+ require 'rspec/core/rake_task'
3
+
4
+ RSpec::Core::RakeTask.new(:spec)
5
+
6
+ task default: :spec
data/bin/console ADDED
@@ -0,0 +1,14 @@
1
+ #!/usr/bin/env ruby
2
+
3
+ require 'bundler/setup'
4
+ require 'web_socket_rb'
5
+
6
+ # You can add fixtures and/or initialization code here to make experimenting
7
+ # with your gem easier. You can also use a different console, if you like.
8
+
9
+ # (If you use this, don't forget to add pry to your Gemfile!)
10
+ # require "pry"
11
+ # Pry.start
12
+
13
+ require 'irb'
14
+ IRB.start
data/bin/setup ADDED
@@ -0,0 +1,8 @@
1
+ #!/usr/bin/env bash
2
+ set -euo pipefail
3
+ IFS=$'\n\t'
4
+ set -vx
5
+
6
+ bundle install
7
+
8
+ # Do any other automated setup that you need to do here
@@ -0,0 +1,37 @@
1
+ <html>
2
+ <head>
3
+ <title>Prosty chat</title>
4
+ <meta charset="UTF-8">
5
+ <script src="../../js/websocketrb_client.js"></script>
6
+ <script language="javascript">
7
+ // Initiate websocket server connection
8
+ var websocketServer = new WebSocketRbClient("ws://localhost:9292");
9
+
10
+ // New chat message
11
+ websocketServer.onmessage('chat', function(message){
12
+ var messagesListDiv = document.getElementById("messages-list");
13
+ var newHtml = "<p>" + message + "</p>";
14
+ messagesListDiv.insertAdjacentHTML('beforeend', newHtml);
15
+ });
16
+
17
+ // Outgoing messages
18
+ function sendMessage() {
19
+ var messageField = document.getElementById("message");
20
+ websocketServer.send('chat', messageField.value);
21
+ messageField.value = '';
22
+ }
23
+
24
+ websocketServer.connect();
25
+
26
+ </script>
27
+ </head>
28
+ <body>
29
+ <h1>Chat anonimowy</h1>
30
+ <p>To jest bardzo prosty system do komunikacji</p>
31
+ <h2>Wiadomości</h2>
32
+ <input type="text" id="message">
33
+ <button onclick="sendMessage()">Wyślij wiadomość</button>
34
+ <div id="messages-list"></div>
35
+ </body>
36
+ </html>
37
+
@@ -0,0 +1,26 @@
1
+ require 'web_socket_rb'
2
+
3
+ WebSocketApp = WebSocketRb::App.new
4
+
5
+ # Define endpoints
6
+ WebSocketApp.routes do
7
+ config.port = 9292
8
+
9
+ # New connection
10
+ init_connection do
11
+ broadcast_message('chat', 'Hello!')
12
+ end
13
+
14
+ # Close some connection
15
+ close_connection do
16
+ broadcast_message('chat', 'Good bye')
17
+ end
18
+
19
+ # Listen for every new incoming message
20
+ subscribe 'chat' do |msg|
21
+ broadcast_message('chat', msg)
22
+ end
23
+ end
24
+
25
+
26
+ WebSocketApp.run
@@ -0,0 +1,43 @@
1
+ /**
2
+ * Created by Karol Bajko
3
+ * Client library for WebSocketRb
4
+ */
5
+
6
+ var WebSocketRbClient = (function (url, protocols) {
7
+
8
+ var url = url;
9
+ var protocols = protocols;
10
+ var websocket = null;
11
+ var callbacks = {};
12
+
13
+ this.send = function (destination, message) {
14
+ websocket.send(JSON.stringify({destination: destination, message: message}));
15
+ return this;
16
+ };
17
+
18
+ this.close = function (code, reason) {
19
+ websocket.close(code, reason);
20
+ return this;
21
+ };
22
+
23
+ this.onmessage = function (destination, callback) {
24
+ callbacks[destination] = callbacks[destination] || [];
25
+ callbacks[destination].push(callback)
26
+ };
27
+
28
+ this.connect = function () {
29
+ websocket = new WebSocket(url, protocols);
30
+
31
+ websocket.onmessage = function (event) {
32
+ var incomingData = JSON.parse(event.data);
33
+ var destination = incomingData.destination;
34
+ var message = incomingData.message;
35
+
36
+ var chain = callbacks[destination];
37
+ if (typeof chain == 'undefined') return;
38
+ for (var i = 0; i < chain.length; i++) {
39
+ chain[i](message)
40
+ }
41
+ }
42
+ }
43
+ });
@@ -0,0 +1,27 @@
1
+ require 'web_socket_rb/version'
2
+ require 'web_socket_rb/server'
3
+ require 'web_socket_rb/routes'
4
+
5
+ module WebSocketRb
6
+ class App
7
+ @logger = Logger.new(STDOUT)
8
+
9
+ def initialize
10
+ @routes = WebSocketRb::Routes.new
11
+ @server = WebSocketRb::Server.new(@routes)
12
+ end
13
+
14
+ def routes(&block)
15
+ @routes.instance_eval(&block)
16
+ end
17
+
18
+ # Run WebSocketRb server
19
+ def run
20
+ @server.run
21
+ end
22
+
23
+ class << self
24
+ attr_reader :logger
25
+ end
26
+ end
27
+ end
@@ -0,0 +1,29 @@
1
+ require 'web_socket_rb/service/build_text_frame_service'
2
+
3
+ # This class provides methods available inside of blocks executed in routes
4
+ module WebSocketRb
5
+ module Context
6
+ class Sandbox
7
+ def initialize(frames_sender)
8
+ @frames_sender = frames_sender
9
+ end
10
+
11
+ # Method to get count of current connections
12
+ def connections_size
13
+ @frames_sender.connections.size
14
+ end
15
+
16
+ # Method to send message to client
17
+ def send_message(destination, message)
18
+ frame = Service::BuildTextFrameService.new(destination, message).run
19
+ @frames_sender.frame_to_send(frame)
20
+ end
21
+
22
+ # Method to broadcast message to all clients
23
+ def broadcast_message(destination, message)
24
+ frame = Service::BuildTextFrameService.new(destination, message).run
25
+ @frames_sender.frame_to_broadcast(frame)
26
+ end
27
+ end
28
+ end
29
+ end
@@ -0,0 +1,17 @@
1
+ module WebSocketRb
2
+ module Error
3
+ class HandshakeError < StandardError
4
+ def initialize(msg)
5
+ App.logger.error('Handshake') { msg }
6
+ end
7
+
8
+ def messages(conn)
9
+ conn.puts('HTTP/1.1 400 Bad Request')
10
+ conn.puts('Upgrade: websocket')
11
+ conn.puts('Connection: Upgrade')
12
+ conn.puts('Sec-WebSocket-Version: 13')
13
+ conn.puts('Sec-WebSocket-Protocol: json')
14
+ end
15
+ end
16
+ end
17
+ end
@@ -0,0 +1,9 @@
1
+ module WebSocketRb
2
+ module Error
3
+ class WrongFrameError < StandardError
4
+ def initialize(msg)
5
+ App.logger.error('Frame') { msg }
6
+ end
7
+ end
8
+ end
9
+ end
@@ -0,0 +1,78 @@
1
+ require 'web_socket_rb/context/sandbox'
2
+ require 'web_socket_rb/service/frames_sender'
3
+ require 'web_socket_rb/service/read_frame_service'
4
+ require 'web_socket_rb/service/build_ping_frame_service'
5
+ require 'web_socket_rb/service/build_pong_frame_service'
6
+ require 'web_socket_rb/service/build_close_frame_service'
7
+ require 'web_socket_rb/error/wrong_frame_error'
8
+
9
+ module WebSocketRb
10
+ module Protocol
11
+ class FramesHandler
12
+ def initialize(connections, connection, routes, frames_sender, sandbox)
13
+ @connections = connections
14
+ @conn = connection
15
+ @routes = routes
16
+ @frames_sender = frames_sender
17
+ @sandbox = sandbox
18
+ @threads = []
19
+ end
20
+
21
+ # Run reading and sending frames
22
+ def run
23
+ @threads << Thread.new { verify_status }
24
+ @threads << Thread.new { process_incoming_frames }
25
+ @threads.each(&:join)
26
+ end
27
+
28
+ private
29
+
30
+ # Send PING frame every 10 seconds to verify connection
31
+ def verify_status
32
+ loop do
33
+ frame = Service::BuildPingFrameService.new.run
34
+ @frames_sender.frame_to_send(frame)
35
+ sleep(10)
36
+ end
37
+ end
38
+
39
+ # Read incoming frames
40
+ def process_incoming_frames
41
+ loop do
42
+ frame = Service::ReadFrameService.new(@conn).run
43
+ process_incoming_frame(frame) unless frame.nil?
44
+ end
45
+ end
46
+
47
+ # Process incoming frame
48
+ def process_incoming_frame(frame)
49
+ if frame.ping?
50
+
51
+ # Send PONG frame if requested PING frame
52
+ App.logger.info('Frames handler') { 'Replying for PING request' }
53
+ pong_frame = Service::BuildPongFrameService.new(frame).run
54
+ @frames_sender.frame_to_send(pong_frame)
55
+
56
+ elsif frame.close?
57
+
58
+ # Send CLOSE frame if requested CLOSE frame
59
+ App.logger.info('Frames handler') { 'Replying for CLOSE request' }
60
+ close_frame = Service::BuildCloseFrameService.new(frame).run
61
+ @frames_sender.frame_to_send(close_frame)
62
+ @threads.each(&:exit)
63
+
64
+ elsif frame.pong?
65
+ # Do nothing
66
+ elsif frame.text? || frame.binary?
67
+
68
+ # Call subscribe block
69
+ App.logger.info('Frames handler') { 'Processing frame' }
70
+ subscribe_block = @routes.subscribes[frame.destination]
71
+ App.logger.info('Frames handler') { frame.message }
72
+ Thread.new { @sandbox.instance_exec(frame.message, &subscribe_block) }
73
+
74
+ end
75
+ end
76
+ end
77
+ end
78
+ end
@@ -0,0 +1,59 @@
1
+ require 'web_socket_rb/error/handshake_error'
2
+ require 'web_socket_rb/wrapper/handshake_request'
3
+ require 'digest'
4
+
5
+ module WebSocketRb
6
+ module Protocol
7
+ class Handshake
8
+ MAGIC_KEY = '258EAFA5-E914-47DA-95CA-C5AB0DC85B11'.freeze
9
+
10
+ def initialize(conn)
11
+ @conn = conn
12
+ end
13
+
14
+ # Generate a proper handshake response based on request
15
+ def run
16
+ validate_request
17
+ generate_proper_response
18
+ @conn
19
+ end
20
+
21
+ private
22
+
23
+ # Read incoming data from socket and convert it to handshake request
24
+ def request
25
+ @request ||= Wrapper::HandshakeRequest.new(@conn)
26
+ end
27
+
28
+ # Validate incoming request - is it a WebSocket upgrade request?
29
+ def validate_request
30
+ App.logger.info('Handshake') { 'Verification started' }
31
+ raise Error::HandshakeError, 'It is not an upgrade request' unless request.upgrade?
32
+ App.logger.info('Handshake') { 'Valid upgrade request' }
33
+ raise Error::HandshakeError, 'Not valid protocol version.' unless request.valid_version?
34
+ App.logger.info('Handshake') { 'Valid protocol version' }
35
+ raise Error::HandshakeError, 'Not valid sub-protocol. Only JSON available.' unless request.valid_protocol?
36
+ App.logger.info('Handshake') { 'Valid sub-protocol request' }
37
+ App.logger.info('Handshake') { 'Verification passed' }
38
+ end
39
+
40
+ # Generate a proper response based on request
41
+ def generate_proper_response
42
+ App.logger.info('Handshake') { 'Send accept response' }
43
+ @conn.puts('HTTP/1.1 101 Switching Protocols')
44
+ @conn.puts('Upgrade: websocket')
45
+ @conn.puts('Connection: Upgrade')
46
+ @conn.puts("Sec-WebSocket-Accept: #{calculate_key(request.key)}")
47
+ # @conn.puts("Sec-WebSocket-Protocol: #{Wrapper::HandshakeRequest::PROTOCOL}")
48
+ @conn.puts('')
49
+ App.logger.info('Handshake') { 'Finished sending accept response' }
50
+ end
51
+
52
+ # Calculate handshake key
53
+ def calculate_key(key)
54
+ raise ArgumentError, 'Key must be a String' unless key.is_a?(String)
55
+ ::Digest::SHA1.base64digest(key.strip + MAGIC_KEY)
56
+ end
57
+ end
58
+ end
59
+ end
@@ -0,0 +1,27 @@
1
+ # Class defines methods available in routes section to configure WebSocket server
2
+ module WebSocketRb
3
+ class Routes
4
+ attr_reader :config, :subscribes, :init_connection_code, :close_connection_code
5
+
6
+ def initialize
7
+ @subscribes = Hash.new([])
8
+ @config = OpenStruct.new(port: '9292')
9
+ end
10
+
11
+ # Define method to subscribe incoming messages
12
+ def subscribe(name, &block)
13
+ raise ArgumentError, 'Invalid name' unless name.is_a?(String)
14
+ @subscribes[name] = block
15
+ end
16
+
17
+ # Execute block of code while initiation new connection
18
+ def init_connection(&block)
19
+ @init_connection_code = block
20
+ end
21
+
22
+ # Execute this block of code when connection is closed
23
+ def close_connection(&block)
24
+ @close_connection_code = block
25
+ end
26
+ end
27
+ end
@@ -0,0 +1,47 @@
1
+ require 'web_socket_rb/protocol/handshake'
2
+ require 'web_socket_rb/protocol/frames_handler'
3
+ require 'web_socket_rb/error/handshake_error'
4
+ require 'socket'
5
+ require 'logger'
6
+
7
+ module WebSocketRb
8
+ class Server
9
+ def initialize(routes)
10
+ @routes = routes
11
+ @connections = []
12
+ @mutex = Mutex.new
13
+ end
14
+
15
+ # Run each request in separate thread
16
+ def run
17
+ hostname = 'localhost'
18
+ port = @routes.config.port || 9292
19
+ @socket = TCPServer.new(hostname, port)
20
+ App.logger.info('Server') { "Started '#{hostname}' on port #{port}" }
21
+ loop do
22
+ Thread.start(@socket.accept) do |conn|
23
+ @mutex.synchronize { @connections << conn }
24
+ App.logger.info('Server') { 'New incoming connection' }
25
+ begin
26
+ Protocol::Handshake.new(conn).run
27
+ @frames_sender = Service::FramesSender.new(@connections, conn)
28
+ @sandbox = Context::Sandbox.new(@frames_sender)
29
+ init_connection_code = @routes.init_connection_code
30
+ @sandbox.instance_eval(&init_connection_code) if init_connection_code.is_a?(Proc)
31
+ Protocol::FramesHandler.new(@connections, conn, @routes, @frames_sender, @sandbox).run
32
+ rescue Error::HandshakeError => e
33
+ e.messages(conn)
34
+ rescue => e
35
+ App.logger.error('Server') { e.message }
36
+ ensure
37
+ conn.close
38
+ @mutex.synchronize { @connections.delete(conn) }
39
+ close_block = @routes.close_connection_code
40
+ @sandbox.instance_eval(&close_block) if close_block.is_a?(Proc)
41
+ App.logger.info('Server') { 'Connection closed' }
42
+ end
43
+ end
44
+ end
45
+ end
46
+ end
47
+ end
@@ -0,0 +1,16 @@
1
+ require 'web_socket_rb/wrapper/frame_close'
2
+
3
+ module WebSocketRb
4
+ module Service
5
+ class BuildCloseFrameService
6
+ def initialize(close_frame)
7
+ @close_frame = close_frame
8
+ end
9
+
10
+ # Every PONG frame should have
11
+ def run
12
+ WebSocketRb::Wrapper::FrameClose.new
13
+ end
14
+ end
15
+ end
16
+ end
@@ -0,0 +1,11 @@
1
+ require 'web_socket_rb/wrapper/frame_ping'
2
+
3
+ module WebSocketRb
4
+ module Service
5
+ class BuildPingFrameService
6
+ def run
7
+ WebSocketRb::Wrapper::FramePing.new
8
+ end
9
+ end
10
+ end
11
+ end
@@ -0,0 +1,19 @@
1
+ require 'web_socket_rb/wrapper/frame_pong'
2
+
3
+ # Create PONG frame based of PING request.
4
+ module WebSocketRb
5
+ module Service
6
+ class BuildPongFrameService
7
+ def initialize(ping_frame)
8
+ @ping_frame = ping_frame
9
+ end
10
+
11
+ # Every PONG frame should have
12
+ def run
13
+ pong_frame = WebSocketRb::Wrapper::FramePong.new
14
+ pong_frame.payload_data = @ping_frame.payload_data
15
+ pong_frame
16
+ end
17
+ end
18
+ end
19
+ end
@@ -0,0 +1,17 @@
1
+ require 'web_socket_rb/wrapper/frame_text'
2
+
3
+ module WebSocketRb
4
+ module Service
5
+ class BuildTextFrameService
6
+ def initialize(destination, message)
7
+ @destination = destination
8
+ @message = message
9
+ end
10
+
11
+ def run
12
+ payload_data = JSON.generate(destination: @destination, message: @message)
13
+ WebSocketRb::Wrapper::FrameText.new(payload_data)
14
+ end
15
+ end
16
+ end
17
+ end
@@ -0,0 +1,47 @@
1
+ module WebSocketRb
2
+ module Service
3
+ class FramesSender
4
+ attr_reader :connections
5
+
6
+ def initialize(connections, connection)
7
+ @connections = connections
8
+ @connection = connection
9
+ @frames_to_send = []
10
+ @frames_to_broadcast = []
11
+ Thread.new do
12
+ loop do
13
+ frame = @frames_to_send.shift
14
+ send_frame(frame) unless frame.nil?
15
+ frame = @frames_to_broadcast.shift
16
+ broadcast_frame(frame) unless frame.nil?
17
+ end
18
+ end
19
+ end
20
+
21
+ # Method to send frame in current connection
22
+ def frame_to_send(frame)
23
+ @frames_to_send << frame
24
+ end
25
+
26
+ # Method to send frame in all connections
27
+ def frame_to_broadcast(frame)
28
+ @frames_to_broadcast << frame
29
+ end
30
+
31
+ private
32
+
33
+ # Broadcast message through current connection
34
+ def send_frame(frame)
35
+ @connection.write(frame.to_bytes)
36
+ Thread.close if frame.close?
37
+ end
38
+
39
+ # Broadcast message through all connections
40
+ def broadcast_frame(frame)
41
+ @connections.each do |conn|
42
+ conn.write(frame.to_bytes)
43
+ end
44
+ end
45
+ end
46
+ end
47
+ end
@@ -0,0 +1,89 @@
1
+ require 'web_socket_rb/wrapper/frame_base'
2
+
3
+ # Service reads socket and returns readed frame.
4
+ module WebSocketRb
5
+ module Service
6
+ class ReadFrameService
7
+ def initialize(conn)
8
+ @conn = conn
9
+ @frame = WebSocketRb::Wrapper::FrameBase.new
10
+ end
11
+
12
+ # Read socket and return new frame
13
+ def run
14
+ # 1st byte
15
+ read_fin_rsv_opcode
16
+ # 2nd byte
17
+ read_mask_payload_len
18
+ # Optionally: 3rd - 10th byte
19
+ read_ext_payload_len
20
+ # Optionally: Masking-key
21
+ read_masking_key
22
+ # Payload data
23
+ read_payload_data
24
+ # Return frame
25
+ @frame
26
+ rescue Error
27
+ nil
28
+ end
29
+
30
+ private
31
+
32
+ def read_fin_rsv_opcode
33
+ byte = @conn.read(1)
34
+ raise Error if byte.nil?
35
+ byte = byte.unpack('C*').first
36
+ @frame.fin = byte & '10000000'.to_i(2) > 0
37
+ @frame.rsv1 = byte & '01000000'.to_i(2) > 0
38
+ @frame.rsv2 = byte & '00100000'.to_i(2) > 0
39
+ @frame.rsv3 = byte & '00010000'.to_i(2) > 0
40
+ @frame.opcode = byte & '00001111'.to_i(2)
41
+ end
42
+
43
+ def read_mask_payload_len
44
+ byte = @conn.read(1)
45
+ raise Error if byte.nil?
46
+ byte = byte.unpack('C*').first
47
+ @frame.mask = byte & '10000000'.to_i(2) > 0
48
+ @frame.payload_len = (byte & '01111111'.to_i(2)).to_i
49
+ end
50
+
51
+ def read_ext_payload_len
52
+ if @frame.payload_len == 126
53
+ bytes = @conn.read(2)
54
+ raise Error if bytes.nil?
55
+ @frame.payload_len = bytes.unpack('S')
56
+ elsif @frame.payload_len == 127
57
+ bytes = @conn.read(8)
58
+ raise Error if bytes.nil?
59
+ @frame.payload_len = bytes.unpack('Q')
60
+ end
61
+ end
62
+
63
+ def read_masking_key
64
+ if @frame.mask
65
+ bytes = @conn.read(4)
66
+ raise Error if bytes.nil?
67
+ @frame.masking_key = bytes.unpack('C*')
68
+ end
69
+ end
70
+
71
+ def read_payload_data
72
+ @frame.payload_data = @conn.read(@frame.payload_len)
73
+ raise Error if @frame.payload_data.nil? && @frame.payload_len > 0
74
+ @frame.payload_data = @frame.payload_data.unpack('C*').each_slice(4).map do |slice|
75
+ slice.zip(@frame.masking_key).map { |a, b| a ^ b }
76
+ end.flatten.pack('C*') if @frame.mask
77
+ end
78
+
79
+ def log_readed_data
80
+ [:fin, :rsv1, :rsv2, :rsv3, :opcode, :mask, :payload_len, :masking_key, :payload_data].each do |var|
81
+ App.logger.info('Frame structure') { "#{var}: #{send(var)}" }
82
+ end
83
+ end
84
+
85
+ class Error < StandardError
86
+ end
87
+ end
88
+ end
89
+ end
@@ -0,0 +1,3 @@
1
+ module WebSocketRb
2
+ VERSION = '0.1.1'.freeze
3
+ end
@@ -0,0 +1,110 @@
1
+ module WebSocketRb
2
+ module Wrapper
3
+ # Class defines base frame structure.
4
+ class FrameBase
5
+ # Constants represents type of frame
6
+ CONTINUATION = 0x0.to_i
7
+ TEXT = 0x1.to_i
8
+ BINARY = 0x2.to_i
9
+ CLOSE = 0x8.to_i
10
+ PING = 0x9.to_i
11
+ PONG = 0xA.to_i
12
+
13
+ attr_accessor :fin, :rsv1, :rsv2, :rsv3, :opcode, :mask, :payload_len,
14
+ :masking_key, :payload_data
15
+
16
+ # Method converts frame to bytes representation.
17
+ def to_bytes
18
+ bytes = ''
19
+ bytes << fin_rsv_opcode_to_byte
20
+ bytes << mask_payload_length_to_byte
21
+ bytes << ext_payload_len_to_byte
22
+ bytes << masking_key_to_byte
23
+ bytes << payload_data_to_byte
24
+ bytes
25
+ end
26
+
27
+ # Define methods to verify types of frame
28
+ [:continuation, :text, :binary, :close, :ping, :pong].each do |constant|
29
+ define_method "#{constant}?" do
30
+ opcode.eql?(self.class.const_get(constant.upcase))
31
+ end
32
+ end
33
+
34
+ # Read destination from payload data
35
+ def destination
36
+ json_payload_data = JSON.parse(payload_data)
37
+ json_payload_data.is_a?(Hash) ? json_payload_data['destination'] : nil
38
+ end
39
+
40
+ # Read message from payload data
41
+ def message
42
+ json_payload_data = JSON.parse(payload_data)
43
+ json_payload_data.is_a?(Hash) ? json_payload_data['message'] : nil
44
+ end
45
+
46
+ protected
47
+
48
+ def payload_data_to_byte
49
+ if mask
50
+ payload_data.bytes.each_slice(4).map do |slice|
51
+ slice.zip(masking_key.bytes).map { |a, b| a ^ b }
52
+ end.flatten.pack('C*')
53
+ else
54
+ payload_data.bytes.pack('C*')
55
+ end
56
+ end
57
+
58
+ def masking_key_to_byte
59
+ mask && masking_key ? masking_key.bytes.pack('C*') : ''
60
+ end
61
+
62
+ def ext_payload_len_to_byte
63
+ case payload_len
64
+ when 0..125
65
+ ''
66
+ when 126..65_535
67
+ [payload_len].pack('S>')
68
+ when 65_536..18_446_744_073_709_551_615
69
+ [payload_len].pack('Q>')
70
+ end
71
+ end
72
+
73
+ def mask_payload_length_to_byte
74
+ byte = 0
75
+ byte |= 1 if mask
76
+ byte <<= 7
77
+
78
+ case payload_len
79
+ when 0..125
80
+ byte |= payload_len
81
+ when 126..65_535
82
+ byte |= 126
83
+ when 65_536..18_446_744_073_709_551_615
84
+ byte |= 127
85
+ end
86
+
87
+ byte.chr
88
+ end
89
+
90
+ def fin_rsv_opcode_to_byte
91
+ byte = 0
92
+ byte |= 1 if fin
93
+
94
+ byte <<= 1
95
+ byte |= 1 if rsv1
96
+
97
+ byte <<= 1
98
+ byte |= 1 if rsv2
99
+
100
+ byte <<= 1
101
+ byte |= 1 if rsv3
102
+
103
+ byte <<= 4
104
+ byte |= opcode
105
+
106
+ byte.chr
107
+ end
108
+ end
109
+ end
110
+ end
@@ -0,0 +1,12 @@
1
+ require 'web_socket_rb/wrapper/frame_outgoing'
2
+
3
+ # Class describes Closing frame
4
+ module WebSocketRb
5
+ module Wrapper
6
+ class FrameClose < FrameOutgoing
7
+ def initialize
8
+ super(opcode: CLOSE)
9
+ end
10
+ end
11
+ end
12
+ end
@@ -0,0 +1,20 @@
1
+ require 'web_socket_rb/wrapper/frame_base'
2
+
3
+ # Class describes any sends frame. It accepts message
4
+ module WebSocketRb
5
+ module Wrapper
6
+ class FrameOutgoing < FrameBase
7
+ def initialize(options = {})
8
+ @payload_data = options.fetch(:payload_data, '')
9
+ @payload_len = @payload_data.bytes.size
10
+ @fin = options.fetch(:fin, true)
11
+ @rsv1 = options.fetch(:rsv1, false) # No externals (false by default)
12
+ @rsv2 = options.fetch(:rsv2, false) # No externals (false by default)
13
+ @rsv3 = options.fetch(:rsv3, false) # No externals (false by default)
14
+ @opcode = options.fetch(:opcode, CONTINUATION) # Type of frame (continuation by default)
15
+ @mask = options.fetch(:mask, false) # Mask (false by default)
16
+ @masking_key = Random.new.bytes(4) if @mask # Generate random masking key if mask
17
+ end
18
+ end
19
+ end
20
+ end
@@ -0,0 +1,13 @@
1
+ require 'web_socket_rb/wrapper/frame_outgoing'
2
+
3
+ # Class describes Ping frame - to verify connection status.
4
+ module WebSocketRb
5
+ module Wrapper
6
+ class FramePing < FrameOutgoing
7
+ def initialize
8
+ rand_payload_data = Random.new.bytes(10)
9
+ super(opcode: PING, payload_data: rand_payload_data)
10
+ end
11
+ end
12
+ end
13
+ end
@@ -0,0 +1,12 @@
1
+ require 'web_socket_rb/wrapper/frame_outgoing'
2
+
3
+ # Class describes Pong frame - reply to Ping frame
4
+ module WebSocketRb
5
+ module Wrapper
6
+ class FramePong < FrameOutgoing
7
+ def initialize
8
+ super(opcode: PONG)
9
+ end
10
+ end
11
+ end
12
+ end
@@ -0,0 +1,13 @@
1
+ require 'web_socket_rb/wrapper/frame_outgoing'
2
+ require 'json'
3
+
4
+ # Class describes Closing frame
5
+ module WebSocketRb
6
+ module Wrapper
7
+ class FrameText < FrameOutgoing
8
+ def initialize(payload_data)
9
+ super(opcode: TEXT, payload_data: payload_data)
10
+ end
11
+ end
12
+ end
13
+ end
@@ -0,0 +1,61 @@
1
+ # Wrapper for request to get.
2
+ # It extracts a proper information from request header, like:
3
+ # - requested path
4
+ # - http protocol version
5
+ # - list of header key-val pairs
6
+ module WebSocketRb
7
+ module Wrapper
8
+ class HandshakeRequest
9
+ HTTP_HEADER = /^GET (\/[[:graph:]]*[\/[[:graph:]]]*) HTTP\/([[:digit:]]+\.[[:digit:]]+)$/ # GET /chat HTTP/1.1
10
+ VERSION = 13
11
+ PROTOCOL = 'json'.freeze
12
+
13
+ attr_reader :path, :version, :headers
14
+
15
+ def initialize(request)
16
+ @request = request
17
+ @headers = {}
18
+ @version = '1.1'
19
+ @path = '/'
20
+
21
+ convert_request_to_hash
22
+ end
23
+
24
+ # Verify if request contains upgrade to websocket demand
25
+ def upgrade?
26
+ headers['Connection'] == 'Upgrade' && headers['Upgrade'] == 'websocket'
27
+ end
28
+
29
+ # Verify if request contains a valid version of websocket protocol
30
+ def valid_version?
31
+ headers['Sec-WebSocket-Version'].to_i == VERSION
32
+ end
33
+
34
+ # Verify if request contains a valid protocol request
35
+ def valid_protocol?
36
+ protocols = headers['Sec-WebSocket-Protocol'].to_s
37
+ protocols = protocols.split(',').map(&:strip)
38
+ protocols.empty? || protocols.include?(PROTOCOL)
39
+ end
40
+
41
+ def key
42
+ headers['Sec-WebSocket-Key'].to_s
43
+ end
44
+
45
+ private
46
+
47
+ # Read each line of request and store it into Hash
48
+ # as key-val pair of all parsed headers
49
+ def convert_request_to_hash
50
+ until (line = @request.gets.strip).empty?
51
+ if !!(line =~ HTTP_HEADER)
52
+ _, @path, @version = line.match(HTTP_HEADER).to_a
53
+ else
54
+ key, val = line.split(':', 2).map(&:strip)
55
+ @headers.store(key, val)
56
+ end
57
+ end
58
+ end
59
+ end
60
+ end
61
+ end
@@ -0,0 +1,33 @@
1
+ # coding: utf-8
2
+ lib = File.expand_path('../lib', __FILE__)
3
+ $LOAD_PATH.unshift(lib) unless $LOAD_PATH.include?(lib)
4
+ require 'web_socket_rb/version'
5
+
6
+ Gem::Specification.new do |spec|
7
+ spec.name = 'web_socket_rb'
8
+ spec.version = WebSocketRb::VERSION
9
+ spec.authors = ['Karol Bajko']
10
+ spec.email = ['karol.bajko@gmail.com']
11
+
12
+ spec.summary = 'WebSocket implementation for Ruby'
13
+ spec.homepage = 'https://bitbucket.org/qarol/websocketrb'
14
+ spec.license = 'MIT'
15
+
16
+ # Prevent pushing this gem to RubyGems.org by setting 'allowed_push_host', or
17
+ # delete this section to allow pushing this gem to any host.
18
+ if spec.respond_to?(:metadata)
19
+ spec.metadata['allowed_push_host'] = 'https://rubygems.org'
20
+ else
21
+ raise 'RubyGems 2.0 or newer is required to protect against public gem pushes.'
22
+ end
23
+
24
+ spec.files = `git ls-files -z`.split("\x0").reject { |f| f.match(%r{^(test|spec|features)/}) }
25
+ spec.bindir = 'exe'
26
+ spec.executables = spec.files.grep(%r{^exe/}) { |f| File.basename(f) }
27
+ spec.require_paths = ['lib']
28
+
29
+ spec.add_development_dependency 'bundler', '~> 1.11'
30
+ spec.add_development_dependency 'rake', '~> 10.0'
31
+ spec.add_development_dependency 'rspec', '~> 3.0'
32
+ spec.add_development_dependency 'simplecov', '~> 0.12.0'
33
+ end
metadata ADDED
@@ -0,0 +1,139 @@
1
+ --- !ruby/object:Gem::Specification
2
+ name: web_socket_rb
3
+ version: !ruby/object:Gem::Version
4
+ version: 0.1.1
5
+ platform: ruby
6
+ authors:
7
+ - Karol Bajko
8
+ autorequire:
9
+ bindir: exe
10
+ cert_chain: []
11
+ date: 2016-09-27 00:00:00.000000000 Z
12
+ dependencies:
13
+ - !ruby/object:Gem::Dependency
14
+ name: bundler
15
+ requirement: !ruby/object:Gem::Requirement
16
+ requirements:
17
+ - - "~>"
18
+ - !ruby/object:Gem::Version
19
+ version: '1.11'
20
+ type: :development
21
+ prerelease: false
22
+ version_requirements: !ruby/object:Gem::Requirement
23
+ requirements:
24
+ - - "~>"
25
+ - !ruby/object:Gem::Version
26
+ version: '1.11'
27
+ - !ruby/object:Gem::Dependency
28
+ name: rake
29
+ requirement: !ruby/object:Gem::Requirement
30
+ requirements:
31
+ - - "~>"
32
+ - !ruby/object:Gem::Version
33
+ version: '10.0'
34
+ type: :development
35
+ prerelease: false
36
+ version_requirements: !ruby/object:Gem::Requirement
37
+ requirements:
38
+ - - "~>"
39
+ - !ruby/object:Gem::Version
40
+ version: '10.0'
41
+ - !ruby/object:Gem::Dependency
42
+ name: rspec
43
+ requirement: !ruby/object:Gem::Requirement
44
+ requirements:
45
+ - - "~>"
46
+ - !ruby/object:Gem::Version
47
+ version: '3.0'
48
+ type: :development
49
+ prerelease: false
50
+ version_requirements: !ruby/object:Gem::Requirement
51
+ requirements:
52
+ - - "~>"
53
+ - !ruby/object:Gem::Version
54
+ version: '3.0'
55
+ - !ruby/object:Gem::Dependency
56
+ name: simplecov
57
+ requirement: !ruby/object:Gem::Requirement
58
+ requirements:
59
+ - - "~>"
60
+ - !ruby/object:Gem::Version
61
+ version: 0.12.0
62
+ type: :development
63
+ prerelease: false
64
+ version_requirements: !ruby/object:Gem::Requirement
65
+ requirements:
66
+ - - "~>"
67
+ - !ruby/object:Gem::Version
68
+ version: 0.12.0
69
+ description:
70
+ email:
71
+ - karol.bajko@gmail.com
72
+ executables: []
73
+ extensions: []
74
+ extra_rdoc_files: []
75
+ files:
76
+ - ".gitignore"
77
+ - ".rspec"
78
+ - ".ruby-gemset"
79
+ - ".ruby-version"
80
+ - ".travis.yml"
81
+ - CODE_OF_CONDUCT.md
82
+ - Gemfile
83
+ - LICENSE.txt
84
+ - README.md
85
+ - Rakefile
86
+ - bin/console
87
+ - bin/setup
88
+ - examples/client/index.html
89
+ - examples/server/server.rb
90
+ - js/websocketrb_client.js
91
+ - lib/web_socket_rb.rb
92
+ - lib/web_socket_rb/context/sandbox.rb
93
+ - lib/web_socket_rb/error/handshake_error.rb
94
+ - lib/web_socket_rb/error/wrong_frame_error.rb
95
+ - lib/web_socket_rb/protocol/frames_handler.rb
96
+ - lib/web_socket_rb/protocol/handshake.rb
97
+ - lib/web_socket_rb/routes.rb
98
+ - lib/web_socket_rb/server.rb
99
+ - lib/web_socket_rb/service/build_close_frame_service.rb
100
+ - lib/web_socket_rb/service/build_ping_frame_service.rb
101
+ - lib/web_socket_rb/service/build_pong_frame_service.rb
102
+ - lib/web_socket_rb/service/build_text_frame_service.rb
103
+ - lib/web_socket_rb/service/frames_sender.rb
104
+ - lib/web_socket_rb/service/read_frame_service.rb
105
+ - lib/web_socket_rb/version.rb
106
+ - lib/web_socket_rb/wrapper/frame_base.rb
107
+ - lib/web_socket_rb/wrapper/frame_close.rb
108
+ - lib/web_socket_rb/wrapper/frame_outgoing.rb
109
+ - lib/web_socket_rb/wrapper/frame_ping.rb
110
+ - lib/web_socket_rb/wrapper/frame_pong.rb
111
+ - lib/web_socket_rb/wrapper/frame_text.rb
112
+ - lib/web_socket_rb/wrapper/handshake_request.rb
113
+ - web_socket_rb.gemspec
114
+ homepage: https://bitbucket.org/qarol/websocketrb
115
+ licenses:
116
+ - MIT
117
+ metadata:
118
+ allowed_push_host: https://rubygems.org
119
+ post_install_message:
120
+ rdoc_options: []
121
+ require_paths:
122
+ - lib
123
+ required_ruby_version: !ruby/object:Gem::Requirement
124
+ requirements:
125
+ - - ">="
126
+ - !ruby/object:Gem::Version
127
+ version: '0'
128
+ required_rubygems_version: !ruby/object:Gem::Requirement
129
+ requirements:
130
+ - - ">="
131
+ - !ruby/object:Gem::Version
132
+ version: '0'
133
+ requirements: []
134
+ rubyforge_project:
135
+ rubygems_version: 2.4.8
136
+ signing_key:
137
+ specification_version: 4
138
+ summary: WebSocket implementation for Ruby
139
+ test_files: []