Created
June 10, 2016 13:48
-
-
Save anonymous/b750379be78556a2f49ab7ec90d9fd49 to your computer and use it in GitHub Desktop.
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
#!/usr/bin/env ruby | |
# Author: Romi Strub | |
# Last Edited: 03-06-2016 (DD-MM-YYYY) | |
# WebSocket chat script. | |
# | |
puts "RUBY_VERSION #{RUBY_VERSION}" | |
puts "Process.pid #{Process.pid}" | |
Thread.abort_on_exception = true | |
require 'digest/sha1' | |
require 'socket' | |
require 'awesome_print' | |
class Framework | |
def self.int_to_byte_string(int, string_length) | |
# check to see if integer exceeds maximum size given by string_length | |
max_int_size = (2**(8*string_length))-1 | |
if int > max_int_size | |
raise ArgumentError, "integer #{int} exceeds maximum size allowed by string_length #{max_int_size}" | |
end | |
chars = [] | |
mask = 255 # 11111111 in binary | |
while int > 0 do | |
chars << (mask & int).chr | |
int = int >> 8 | |
end | |
(string_length - chars.size).times{|c| | |
chars << (0).chr | |
} | |
chars = chars.reverse | |
chars.join | |
end | |
def self.byte_string_to_int(byte_string) | |
bytes = byte_string.bytes | |
bytes = bytes.reverse | |
ap bytes | |
sum = 0 | |
bytes.each_index{|i| | |
sum = sum + (bytes[i] << (8*i)) | |
} | |
sum | |
end | |
module WebSocket | |
def self.decode_frame(connection) | |
header = connection.recv(2) | |
bytes = header.bytes | |
byte_one = bytes.shift | |
# whittle down the first byte to get the opcode | |
# first bit is set to one if message is final | |
final = byte_one[7] == 1 ? true : false | |
byte_one = final ? byte_one-128 : byte_one | |
reserve_1 = byte_one[6] | |
byte_one = reserve_1 == 1 ? byte_one-64 : byte_one | |
reserve_2 = byte_one[5] | |
byte_one = reserve_2 == 1 ? byte_one-32 : byte_one | |
reserve_3 = byte_one[4] | |
byte_one = reserve_3 == 1 ? byte_one-16 : byte_one | |
opcode = byte_one | |
byte_two = bytes.shift | |
# first bit (of second byte) is set to one if message is masked | |
masked = (byte_two[7] == 1) ? true : false | |
# negate mask flag bit | |
if masked | |
byte_two = byte_two-128 | |
end | |
# get payload length | |
if byte_two == 126 | |
payload_length = self.byte_string_to_int(connection.recv(2)) | |
elsif byte_two == 127 | |
payload_length = self.byte_string_to_int(connection.recv(8)) | |
else | |
payload_length = byte_two | |
end | |
if masked | |
mask = connection.recv(4) # receieve mask string (4 bytes) | |
end | |
# receieve payload | |
payload = connection.recv(payload_length) | |
if masked | |
payload = MyWebSocket.unmask_payload(mask, payload) | |
end | |
frame = { | |
"fin"=>final, | |
"rsv1"=>reserve_1, | |
"rsv2"=>reserve_2, | |
"rsv3"=>reserve_3, | |
"opcode"=>opcode, | |
"masked"=>masked, | |
"payload_length"=>payload_length, | |
"mask"=>mask, | |
"payload"=>payload | |
} | |
puts "\r\n#########################################\r\n" | |
puts "\r\nCONNECTION\r\n\r\n ##{connections.find_index(connection)+1} #{connection}" | |
puts "\r\nMESSAGE NUMBER\r\n\r\n#{j}" | |
puts "\r\nDECODED FRAME\r\n\r\n" | |
ap decoded_frame | |
frame | |
end | |
def self.encode_frame(fin, opcode, payload) | |
body = "" | |
byte_one = (fin) ? 128 : 0 | |
byte_one += opcode | |
if payload.length > 65535 # largest expressible integer using two bytes | |
byte_two = 127 | |
body << MyWebSocket.int_to_byte_string(payload.length, 8) | |
elsif payload.length > 125 | |
byte_two = 126 | |
body << MyWebSocket.int_to_byte_string(payload.length, 2) | |
else | |
byte_two = payload.length # assumes message is not masked | |
end | |
body << payload | |
byte_one.chr << byte_two.chr << body | |
end | |
def self.unmask_payload(mask, payload) | |
#mask expects string of 4 chars | |
#payload expects string of n chars | |
mask = mask.bytes | |
payload = payload.bytes | |
payload = payload.map.with_index{|byte, i| | |
byte^mask[i%4] ## if either bit is 1, result is 1 | |
} | |
payload.map{|c|c.chr}.join | |
end | |
class Server | |
def initialize (hostname, port) | |
@server = TCPServer.new(hostname, port) | |
@hostname = hostname | |
@port = port | |
@connection_count = 0 | |
@connections = [] | |
@connection_history = [] | |
puts "server started on port #{@port}" ## | |
puts "\r\nSTART TESTS" | |
#puts MyWebSocket.bytes_to_int([0,0,1]) | |
puts "END TESTS\r\n" | |
end | |
def accept(handlers) | |
connection = MyWebSocket.new(@server.accept, self, handlers) | |
@connection_count += 1 | |
@connections << connection | |
puts "connection #{@connection_count} on port #{@port}" ## | |
puts "local address: " + connection.local_address.inspect | |
puts "remote address: " + connection.remote_address.inspect | |
puts "connections:" ## | |
ap connections ## | |
connection | |
end | |
def remove_connection(connection) | |
@connection_count -= 1 | |
@connection_history << connection | |
@connections.delete(connection) | |
end | |
end | |
class Connection | |
def initialize(connection, server, handlers) | |
@connection = connection | |
@server = server | |
@message_count = 0 | |
@messages = [] | |
@on_open = handlers[:on_open] | |
@on_message = handlers[:on_message] | |
@on_close = handlers[:on_close] | |
handshake_response | |
@on_open.call(self) | |
# receive messages | |
Thread.new { | |
loop { | |
decoded_frame = MyWebSocket.decode_frame(this_connection) | |
opcode = decoded_frame["opcode"] | |
payload = decoded_frame["payload"] | |
fin = decoded_frame["fin"] | |
case opcode | |
when 10 # pong | |
# register pong successful | |
when 9 # ping | |
# send pong | |
write self.encode_frame(true,10,"") | |
when 8 # close | |
@on_close.call() | |
close | |
when 2 # binary | |
message = add_message(payload) | |
when 1 # text | |
message = add_message(payload) | |
when 0 # continuation | |
message << payload | |
end | |
if fin | |
messages << message | |
@on_message.call(message) | |
end | |
} | |
} | |
end | |
def close(code, reason = "") | |
# code expects integer | |
write encode_frame(true, 8, self.int_to_byte_string(code, 2) << reason) # encode close message | |
server.remove_connection | |
super # call close method of IO | |
end | |
def add_message(msg) | |
MyWebsocketMessage.new(self, msg) | |
end | |
def handshake_response | |
string = recv(1024) | |
in_headers = MyWebSocket.parse_http_headers(string) | |
ap in_headers ## | |
if defined? in_headers["Sec-WebSocket-Key"] | |
# compute response for WebSocket | |
websocket_key = in_headers["Sec-WebSocket-Key"] | |
key_suffix = "258EAFA5-E914-47DA-95CA-C5AB0DC85B11" | |
computed_key = Digest::SHA1.base64digest(websocket_key + key_suffix) | |
out_headers = "HTTP/1.1 101 Switching Protocols\r\n" | |
out_headers << "Connection: Upgrade\r\n" | |
out_headers << "Date: #{Time.now}\r\n" | |
out_headers << "Sec-Websocket-Accept: #{computed_key}\r\n" | |
out_headers << "Server: Ruby Websocket\r\n" | |
out_headers << "Upgrade: websocket\r\n" | |
out_headers << "\r\n" | |
write out_headers | |
puts "RESPONSE SENT:" ## | |
puts out_headers ## | |
end | |
end | |
def self.parse_http_headers(header_string) | |
return_hash = {} | |
lines = header_string.split("\r\n") | |
first_line = lines.shift | |
first_line_words = first_line.split(" ") | |
return_hash["Method"] = first_line_words[0] | |
return_hash["URI"] = first_line_words[1] | |
return_hash["Protocol"] = first_line_words[2] | |
lines.each {|line| | |
key_val = line.split(": ") | |
return_hash[key_val[0]] = key_val[1] | |
} | |
return_hash | |
end | |
end | |
class Message | |
def initialize(connection, msg = "") | |
@message = msg | |
@connection = connection | |
end | |
end | |
end | |
end | |
port = 9292 | |
server = Framework::WebSocket::Server.new('', port) | |
handlers = { | |
:on_open => lambda { | |
}, | |
:on_message => lambda(connection, message) { | |
return_message = MyWebSocket.encode_frame(true, 1, message) | |
puts "\r\nCONNECTIONS\r\n\r\n" | |
ap connections | |
connections.each {|c| | |
c.write return_message | |
} | |
}, | |
:on_close => lambda { | |
} | |
} | |
loop { | |
# receive connection | |
connection = server.accept handlers | |
} |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment