Skip to content

Instantly share code, notes, and snippets.

Show Gist options
  • Star 3 You must be signed in to star a gist
  • Fork 1 You must be signed in to fork a gist
  • Save davesag/5506864 to your computer and use it in GitHub Desktop.
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…
#!/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