Last active
March 17, 2017 14:02
-
-
Save llekn/336af5bd3fce417285cf1abb7489e9b7 to your computer and use it in GitHub Desktop.
Websocket server from scratch using TCPServer
This file contains hidden or 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
# http://blog.honeybadger.io/building-a-simple-websockets-server-from-scratch-in-ruby/ | |
require 'socket' # Provides TCPServer and TCPSocket classes | |
require 'digest/sha1' | |
server = TCPServer.new('localhost', 2345) | |
loop do | |
# Wait for a connection | |
socket = server.accept | |
STDERR.puts "Incoming Request" | |
# Read the HTTP request. We know it's finished when we see a line with nothing but \r\n | |
http_request = "" | |
while (line = socket.gets) && (line != "\r\n") | |
http_request += line | |
end | |
# Grab the security key from the headers. If one isn't present, close the connection. | |
if matches = http_request.match(/^Sec-WebSocket-Key: (\S+)/) | |
websocket_key = matches[1] | |
STDERR.puts "Websocket handshake detected with key: #{ websocket_key }" | |
else | |
STDERR.puts "Aborting non-websocket connection" | |
socket.close | |
next | |
end | |
response_key = Digest::SHA1.base64digest([websocket_key, "258EAFA5-E914-47DA-95CA-C5AB0DC85B11"].join) | |
STDERR.puts "Responding to handshake with key: #{ response_key }" | |
socket.write <<-eos | |
HTTP/1.1 101 Switching Protocols | |
Upgrade: websocket | |
Connection: Upgrade | |
Sec-WebSocket-Accept: #{ response_key } | |
eos | |
STDERR.puts "Handshake completed. Starting to parse the websocket frame." | |
first_byte = socket.getbyte | |
fin = first_byte & 0b10000000 | |
opcode = first_byte & 0b00001111 | |
raise "We don't support continuations" unless fin | |
raise "We only support opcode 1" unless opcode == 1 | |
second_byte = socket.getbyte | |
is_masked = second_byte & 0b10000000 | |
payload_size = second_byte & 0b01111111 | |
raise "All incoming frames should be masked according to the websocket spec" unless is_masked | |
raise "We only support payloads < 126 bytes in length" unless payload_size < 126 | |
STDERR.puts "Payload size: #{ payload_size } bytes" | |
mask = 4.times.map { socket.getbyte } | |
STDERR.puts "Got mask: #{ mask.inspect }" | |
data = payload_size.times.map { socket.getbyte } | |
STDERR.puts "Got masked data: #{ data.inspect }" | |
unmasked_data = data.each_with_index.map { |byte, i| byte ^ mask[i % 4] } | |
STDERR.puts "Unmasked the data: #{ unmasked_data.inspect }" | |
STDERR.puts "Converted to a string: #{ unmasked_data.pack('C*').force_encoding('utf-8').inspect }" | |
response = "Loud and clear!" | |
STDERR.puts "Sending response: #{ response.inspect }" | |
output = [0b10000001, response.size, response] | |
socket.write output.pack("CCA#{ response.size }") | |
socket.close | |
end |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment