Last active
August 18, 2020 01:51
-
-
Save davesag/5506864 to your computer and use it in GitHub Desktop.
This is the shell of my Game server built using Sinatra and EventMachine Websockets. I am trying to extract the cookie information that is passed to the Websocket's `onopen` method via the `handshake.headers['Cookie'], but have been unable to work out how to properly decode the cookie. See https://github.com/rack/rack/issues/551 or http://stacko…
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
#!/user/bin/env ruby | |
#coding: utf-8 | |
APP_ROOT = File.dirname(__FILE__) | |
PROJECT_NAME = 'My Fantastic Game' | |
PROJECT_HOST = '0.0.0.0' | |
WEB_PORT = 9292 | |
WS_PORT = 8080 | |
COOKIE_KEY = 'my.session.key' | |
COOKIE_SECRET = 'shh_replace_me_withsomething_moresecret' | |
EventMachine.run do | |
require './models.rb' | |
class MyApp < Sinatra::Base | |
set :root, APP_ROOT | |
set :name, PROJECT_NAME | |
set :root, File.dirname(__FILE__) | |
enable :logging, :show_exceptions | |
use Rack::Session::Cookie, :key => COOKIE_KEY, | |
:path => '/', | |
:expire_after => 2592000, #30 days | |
:secret => COOKIE_SECRET | |
use Rack::Flash | |
@active_user = nil # the active user is reloaded on each request in the before method. | |
helpers do | |
def active_user | |
return @active_user | |
end | |
def logged_in? | |
return @active_user != nil | |
end | |
def refresh_active_user! | |
if session[:user] != nil | |
# there is a currently logged in user so load her up | |
begin | |
@active_user = User.where(_id: session[:user]).first | |
if @active_user == nil | |
session[:user] = nil | |
puts "Sessions have been nuked." | |
end | |
rescue => e | |
puts "Error loading user #{session[:user]} from session." | |
puts e.message | |
session[:user] = nil | |
end | |
end | |
end | |
def log_user_in!(user) | |
puts "logging in user #{user.inspect}" | |
@active_user = user | |
session[:user] = user._id | |
end | |
def log_user_out! | |
@active_user = nil | |
session[:user] = nil | |
end | |
def auth_user(username, password) | |
user = User.where(name: username).first | |
return user if user && user.password == password | |
return nil | |
end | |
end | |
before do | |
refresh_active_user! | |
end | |
# routes | |
get '/' do | |
puts "Home page" | |
if logged_in? | |
haml :game | |
else | |
haml :index | |
end | |
end | |
get '/logout' do | |
log_user_out! | |
redirect to('/') | |
end | |
post '/login' do | |
name = params['username'] | |
pass = params['password'] | |
puts "Login request received for #{name}" | |
entering_user = auth_user(name, pass) | |
if entering_user != nil | |
log_user_in!(entering_user) | |
redirect to('/') | |
else | |
flash[:error] = "There is no user matching those credentials." | |
redirect back | |
end | |
end | |
post '/register' do | |
name = params['reg_username'] | |
pass = params['reg_password'] | |
if name == '' || pass == '' | |
flash[:error] = "You must provide an alias and a password to register." | |
redirect back | |
else | |
if User.where(name: name).exists? | |
flash[:error] = "A user with alias '#{name}' already exists, please choose another." | |
redirect back | |
else | |
begin | |
user = User.create!(name: name, password: pass) | |
puts "User #{user._id} created." | |
rescue => e | |
flash[:error] = "A system error occurred while trying to save your registration." | |
puts "An error occured while saving user #{name}." | |
puts "Error: #{e.message}" | |
ensure | |
redirect back | |
end | |
end | |
end | |
end | |
not_found do | |
status 404 | |
haml :'404' | |
end | |
error do | |
status 500 | |
haml :'500', :locals => {:error => request.env['sinatra_error']} | |
end | |
end | |
class ConnectionManager | |
include Singleton | |
attr_accessor :connections # an array of connections. | |
attr_accessor :users # a map of users and associated connection. | |
def initialize | |
@connections = [] | |
@users = {} | |
end | |
def send_to_all(msg) | |
self.connections.each { |c| c.send(msg.to_json) } | |
end | |
def send_to_one(ws, msg) | |
ws.send(msg.to_json) | |
end | |
end | |
EventMachine::WebSocket.start(:host => PROJECT_HOST, :port => WS_PORT) do |ws| | |
connection_manager = ConnectionManager.instance | |
ws.onopen { |handshake| | |
puts "Connection #{ws.signature}, protocol version #{handshake.protocol_version}, #{ws.state} from #{handshake.origin}#{handshake.path}" | |
begin | |
cookie, bakesale = handshake.headers['Cookie'].split('=') | |
raise RuntimeError, "Missing session cookie in websocket connection." if cookie != COOKIE_KEY | |
rack_cookie = Rack::Session::Cookie.new(MyApp, { | |
:key => COOKIE_KEY, | |
:path => '/', | |
:expire_after => 2592000, #30 days | |
:secret => COOKIE_SECRET, | |
:coder => Rack::Session::Cookie::Base64.new | |
}) | |
puts "first attempt to decode" | |
puts rack_cookie.coder.decode(bakesale) | |
puts "second attempt to decode" | |
puts rack_cookie.coder.decode(bakesale.split('--').first) | |
puts "3rd attempt to decode" | |
# puts Rack::Session::Cookie::Marshal.decode(bakesale.split('--').first) | |
# puts Marshal.load(rack_cookie.coder.decode(bakesale.split('--').first)).inspect | |
# for some reason this isn't working | |
# see http://stackoverflow.com/questions/16312024/how-to-decode-a-cookie-from-the-header-of-a-websocket-connection-handshake-rub | |
# tell all the other connections we've just added a new connection | |
# and add this connection to the list. | |
connection_manager.connections << ws | |
connection_manager.send_to_all({:message => "#{PROJECT_NAME} Connection Opened"}) | |
puts "Connection #{ws.signature} is opened at #{Time.now}" | |
rescue => e | |
puts e.message | |
ws.close(4040) # see http://tools.ietf.org/html/rfc6455#section-7.4 | |
# I would have returned code = 1008 as defined below, however | |
# https://github.com/igrigorik/em-websocket/blob/master/lib/em-websocket/connection.rb | |
# makes it very clear that only codes 1000, (3000..4999) are allowed. | |
# so I am returning 4040 instead. | |
# see https://github.com/igrigorik/em-websocket/issues/98 for my issue on this topic. | |
end | |
} | |
ws.onclose { |evt| | |
puts "Connection #{ws.signature} asked to close #{(evt[:was_clean]) ? 'cleanly' : 'abruptly'}." | |
puts "Connection #{ws.signature} sent close code: #{evt[:code]}" | |
begin | |
connection_manager.connections.delete ws | |
dua = connection_manager.users.rassoc(ws) | |
raise RuntimeError, "No user associated with connection #{ws.inspect}" if dua == nil | |
dropped_user = dua.first | |
connection_manager.users.delete dropped_user | |
rescue => e | |
puts "Error tying to close connection #{ws.signature}." | |
puts e.message | |
puts e.backtrace.join("\n") | |
end | |
} | |
ws.onmessage { |msg| | |
puts "Connection #{ws.signature} received message #{msg}" | |
begin | |
cj = JSON.parse(msg) | |
# do stuff based on the incoming message content | |
rescue => e | |
puts "ERROR #{e.message}" | |
puts e.backtrace.join("\n") | |
ensure | |
puts "onmessage handling concluded for message #{msg}" | |
end | |
} | |
ws.onerror { |err| | |
puts "Error in connection #{ws.signature}." | |
} | |
end | |
puts "Loading Mongo Database in environment #{ENV['RACK_ENV']}" | |
puts Mongoid.load!('./config/mongoid.yml', ENV['RACK_ENV']) | |
MyApp.run!({:server => 'thin', :bind => PROJECT_HOST, :port => WEB_PORT}) | |
end |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment