From 15b17d5338ca744c48aa7c9a32e7bffdbad4b49a Mon Sep 17 00:00:00 2001 From: pjht Date: Mon, 11 Sep 2017 07:37:59 -0500 Subject: [PATCH] Initial commit --- .gitignore | 1 + README.md | 25 ++++++++ lib/wsserver.rb | 3 + lib/wsserver/websocket_connection.rb | 86 ++++++++++++++++++++++++++++ lib/wsserver/websocket_server.rb | 54 +++++++++++++++++ wsserver.gemspec | 17 ++++++ 6 files changed, 186 insertions(+) create mode 100644 .gitignore create mode 100644 README.md create mode 100644 lib/wsserver.rb create mode 100644 lib/wsserver/websocket_connection.rb create mode 100644 lib/wsserver/websocket_server.rb create mode 100644 wsserver.gemspec diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..c111b33 --- /dev/null +++ b/.gitignore @@ -0,0 +1 @@ +*.gem diff --git a/README.md b/README.md new file mode 100644 index 0000000..c1d9482 --- /dev/null +++ b/README.md @@ -0,0 +1,25 @@ +# wsserver +This gem is a WebSocket server for Ruby. + +## Installation +Download the latest release and run `gem install wsserver.gem` in the directory where you put it. + +## Usage +Put this code in a file: + +```ruby +server = WebsocketServer.new + +while true + Thread.new(server.accept) do |connection| + # Your code here + end +end +``` +To receive a message from the client do `connection.recv`. +Note: connection.recv will return false if the connection was closed. +To send a message to the client do `connection.send`. +To close the connection do `connection.close` +The WebSocketServer class accepts two named arguments as well: +host: The host this server will run on. Defaults to localhost. +port: The port the server will run on. Defaults to 4567. diff --git a/lib/wsserver.rb b/lib/wsserver.rb new file mode 100644 index 0000000..84a23f1 --- /dev/null +++ b/lib/wsserver.rb @@ -0,0 +1,3 @@ +require_relative "wsserver/version" +require_relative "wsserver/websocket_connection" +require_relative "wsserver/websocket_server" diff --git a/lib/wsserver/websocket_connection.rb b/lib/wsserver/websocket_connection.rb new file mode 100644 index 0000000..028a4ab --- /dev/null +++ b/lib/wsserver/websocket_connection.rb @@ -0,0 +1,86 @@ +class WebSocketConnection + + def initialize(socket) + @socket = socket + end + + # Recives a frame from the client. + def recv + puts @socket.inspect + fin_and_opcode = @socket.read(1).bytes[0] + fin = fin_and_opcode & 0b10000000 + opcode = fin_and_opcode & 0b00001111 + + case opcode + when 1,0 + mask_and_length_indicator = @socket.read(1).bytes[0] + length_indicator = mask_and_length_indicator & 0x7f + + if length_indicator <= 125 + length = length_indicator + elsif length_indicator == 126 + length = @socket.read(2).unpack("n")[0] + else + length = @socket.read(8).unpack("Q>")[0] + end + + mask = @socket.read(4).bytes + + masked = @socket.read(length).bytes + + i=0 + $data=[] + masked.each do |byte| + $data[i]=byte ^ mask[i % 4] + i+=1 + end + data=$data + string=data.pack("c*") + + if not fin + string += parse_frame(@socket) + end + + puts "Got frame: #{string}" + + return string + when 8 + close + return false + else + send("This server ony supports text data and the close frame") + close + return false + end + end + + def close() + bytes = [0b10001000,0] + data = bytes.pack("C*") + puts data.inspect + socket << data + @socket.close + end + + # Sends a frame to the client. + def send(string) + return if string == false + + puts "Sending frame: #{string}" + + bytes = [0b10000001] + size = string.bytesize + + if size <= 125 + bytes += [size] + elsif size < 2**16 + bytes += [126] + [size].pack("n").bytes + else + bytes += [127] + [size].pack("Q>").bytes + end + + bytes += string.bytes + data = bytes.pack("C*") + @socket << data + end +end diff --git a/lib/wsserver/websocket_server.rb b/lib/wsserver/websocket_server.rb new file mode 100644 index 0000000..e87ac5d --- /dev/null +++ b/lib/wsserver/websocket_server.rb @@ -0,0 +1,54 @@ +require 'socket' +require 'digest/sha1' +require 'base64' +require_relative "websocket_connection.rb" + +class WebSocketServer + # Initalize a new WebSocketServer. + def initialize(path: '/', port: 4567, host: 'localhost') + @path=path + @tcp_server = TCPServer.new(host, port) + end + + # Accept a WebSocket connection. Returns a new WebSocketConnection bound to the connection. + def accept + socket = @tcp_server.accept + success=send_handshake(socket) + return WebSocketConnection.new(socket) if success + end + + private + + def send_handshake(socket) + http_request = {} + while (line = socket.gets) && (line != "\r\n") + key, value = line.split(": ") + value=value.chomp if value != nil + http_request[key] = value + end + + if http_request.has_key? "Sec-WebSocket-Key" + websocket_key = http_request["Sec-WebSocket-Key"] + puts "Websocket handshake detected with key: #{ websocket_key.inspect }" + else + puts "Aborting non-websocket connection" + socket << "HTTP/1.1 400 Bad Request\r\n" + + "Content-Type: text/plain\r\n" + + "Connection: close\r\n" + + "\r\n" + socket.close + return false + end + + response_key = Digest::SHA1.base64digest(websocket_key+"258EAFA5-E914-47DA-95CA-C5AB0DC85B11") + puts "Responding to handshake with key: #{ response_key }" + + socket << "HTTP/1.1 101 Switching Protocols\r\n" + + "Upgrade: websocket\r\n" + + "Connection: Upgrade\r\n"+ + "Sec-WebSocket-Accept: #{ response_key }\r\n" + + "\r\n" + return true + end + +end diff --git a/wsserver.gemspec b/wsserver.gemspec new file mode 100644 index 0000000..230aaed --- /dev/null +++ b/wsserver.gemspec @@ -0,0 +1,17 @@ +VERSION="1.0" +Gem::Specification.new do |spec| + spec.name = "wsserver" + spec.version=VERSION + spec.authors = ["pjht"] + spec.summary = "This gem is a WebSocket server for Ruby" + + # Prevent pushing this gem to RubyGems.org. To allow pushes either set the 'allowed_push_host' + # to allow pushing to a single host or delete this section to allow pushing to any host. + if spec.respond_to?(:metadata) + spec.metadata["allowed_push_host"] = "" + else + raise "RubyGems 2.0 or newer is required to protect against public gem pushes." + end + + spec.files = ["lib/wsserver.rb","lib/wsserver/websocket_connection.rb","lib/wsserver/websocket_server.rb"] +end