From e64f3a4d0c03234245568dd4ecbf902781126e2f Mon Sep 17 00:00:00 2001 From: Seth Call Date: Mon, 3 Sep 2012 20:22:46 -0500 Subject: [PATCH] * fully supporting json concurrently with protocol buffers. specify ?pb=true to web URI --- bin/websocket_gateway | 12 +++- lib/jam_websockets/message.rb | 38 ++++++++++-- lib/jam_websockets/router.rb | 92 +++++++++++++++++++----------- spec/jam_websockets/router_spec.rb | 17 +++--- 4 files changed, 112 insertions(+), 47 deletions(-) diff --git a/bin/websocket_gateway b/bin/websocket_gateway index 789c35f1d..480ca4f1a 100755 --- a/bin/websocket_gateway +++ b/bin/websocket_gateway @@ -9,9 +9,17 @@ include JamWebsockets jamenv = ENV['JAMENV'] jamenv ||= 'development' -config = YAML::load(File.open('config/application.yml'))[jamenv] -db_config = YAML::load(File.open('config/database.yml'))[jamenv] +bin_dir = File.expand_path(File.dirname(__FILE__)) +app_config_file = File.join(bin_dir, '..', 'config', 'application.yml') +db_config_file = File.join(bin_dir, '..', 'config', 'database.yml') + +# limit execution in base dir of project +#app_config_file = File.join('config', 'application.yml') +#db_config_file = File.join('config', 'database.yml') + +config = YAML::load(File.open(app_config_file))[jamenv] +db_config = YAML::load(File.open(db_config_file))[jamenv] if config["verbose"] Logging.logger.root.level = :debug diff --git a/lib/jam_websockets/message.rb b/lib/jam_websockets/message.rb index edfb31482..91c877a8f 100644 --- a/lib/jam_websockets/message.rb +++ b/lib/jam_websockets/message.rb @@ -5,7 +5,14 @@ require 'protocol_buffers/compiler' class ProtocolBuffers::Message def to_json(*args) - hash = {'json_class' => self.class.name} + + json = to_json_ready_object() + + json.to_json(*args) + end + + def to_json_ready_object() + hash = {} # simpler version, includes all fields in the output, using the default # values if unset. also includes empty repeated fields as empty arrays. @@ -19,15 +26,26 @@ class ProtocolBuffers::Message value = value_for_tag(field.tag) hash[field.name] = value unless value.empty? else - hash[field.name] = value_for_tag(field.tag) if value_for_tag?(field.tag) + if value_for_tag?(field.tag) + value = value_for_tag(field.tag) + if field.instance_of? ProtocolBuffers::Field::EnumField # if value is enum, resolve string value as ruby const + hash[field.name] = field.value_to_name[value] + else + proxy_class = field.instance_variable_get(:@proxy_class) + if proxy_class.nil? + hash[field.name] = value + else + hash[field.name] = value.to_json_ready_object + end + end + end end end - hash.to_json(*args) + + return hash end def self.json_create(hash) - hash.delete('json_class') - # initialize takes a hash of { attribute_name => value } so you can just # pass the hash into the constructor. but we're supposed to be showing off # reflection, here. plus, that raises an exception if there is an unknown @@ -37,7 +55,15 @@ class ProtocolBuffers::Message message = new fields.each do |tag, field| if value = hash[field.name.to_s] - message.set_value_for_tag(field.tag, value) + if value.instance_of? Hash # if the value is a Hash, descend down into PB hierachy + inner_class = field.instance_variable_get(:@proxy_class) + value = inner_class.json_create(value) + message.set_value_for_tag(field.tag, value) + elsif field.instance_of? ProtocolBuffers::Field::EnumField # if value is enum, resolve string value as ruby const + message.set_value_for_tag(field.tag, field.instance_variable_get(:@proxy_enum).const_get(value)) + else + message.set_value_for_tag(field.tag, value) + end end end message diff --git a/lib/jam_websockets/router.rb b/lib/jam_websockets/router.rb index 4f2512231..83e29d16f 100644 --- a/lib/jam_websockets/router.rb +++ b/lib/jam_websockets/router.rb @@ -3,12 +3,24 @@ require 'set' require 'hot_bunnies' require 'thread' require 'json' +require 'eventmachine' import java.util.concurrent.Executors include Jampb +# add new field to client connection +module EventMachine +module WebSocket + class Connection < EventMachine::Connection + attr_accessor :encode_json + end +end +end + module JamWebsockets + + class Router attr_accessor :user_context_lookup, :session_context_lookup @@ -130,6 +142,7 @@ module JamWebsockets @log.debug "received user-directed message for session: #{user_id}" + msg = Jampb::ClientMessage.parse(msg) contexts.each do |context| EM.schedule do @log.debug "sending user message to #{context}" @@ -162,6 +175,7 @@ module JamWebsockets @log.debug "received session-directed message for session: #{session_id}" + msg = Jampb::ClientMessage.parse(msg) contexts.each do |context| EM.schedule do @log.debug "sending session message to #{context}" @@ -178,8 +192,12 @@ module JamWebsockets end def send_to_client(client, msg) - # this is so odd that this is necessary. but searching through the source code... it's all I could find in em-websocket - client.instance_variable_get(:@handler).send_frame(:binary, msg) + if client.encode_json + client.send(msg.to_json.to_s) + else + # this is so odd that this is necessary from an API perspective. but searching through the source code... it's all I could find in em-websocket for allowing a binary message to be sent + client.instance_variable_get(:@handler).send_frame(:binary, msg.to_s) + end end def cleanup() @@ -228,15 +246,21 @@ module JamWebsockets @pending_clients.add(client) end - is_json = false + # default to using json instead of pb + client.encode_json = true client.onopen { #binding.pry @log.debug "client connected #{client}" + + # check for '?pb' or '?pb=true' in url query parameters + query_pb = client.request["query"]["pb"] - p client.request["query"] - is_json = !!client.request["query"]["json"] - } + if !query_pb.nil? && (query_pb == "" || query_pb == "true") + client.encode_json = false + end + + } client.onclose { @log.debug "Connection closed" @@ -263,12 +287,11 @@ module JamWebsockets begin - if is_json -#{"type":100, "target":"server", "Login" : {"username":"hi"}} + if client.encode_json + #example: {"type":"LOGIN", "target":"server", "login" : {"username":"hi"}} parse = JSON.parse(msg) - p parse pb_msg = Jampb::ClientMessage.json_create(parse) - p pb_msg + self.route(pb_msg, client) else pb_msg = Jampb::ClientMessage.parse(msg.to_s) self.route(pb_msg, client) @@ -277,7 +300,7 @@ module JamWebsockets @log.info "ending client session deliberately due to malformed client behavior. reason=#{e}" begin # wrap the message up and send it down - error_msg = @message_factory.server_rejection_error(e.to_s).to_s + error_msg = @message_factory.server_rejection_error(e.to_s) send_to_client(client, error_msg) ensure client.close_websocket @@ -289,7 +312,7 @@ module JamWebsockets begin # wrap the message up and send it down - error_msg = @message_factory.server_generic_error(e.to_s).to_s + error_msg = @message_factory.server_generic_error(e.to_s) send_to_client(client, error_msg) ensure client.close_websocket @@ -385,9 +408,10 @@ module JamWebsockets end def handle_login(login, client) - username = login.username - token = login.token - password = login.password + + username = login.username if login.value_for_tag(1) + password = login.password if login.value_for_tag(2) + token = login.token if login.value_for_tag(3) user = valid_login(username, password, token) @@ -398,7 +422,7 @@ module JamWebsockets # respond with LOGIN_ACK to let client know it was successful #binding.pry remote_port, remote_ip = Socket.unpack_sockaddr_in(client.get_peername) - login_ack = @message_factory.login_ack(remote_ip).to_s + login_ack = @message_factory.login_ack(remote_ip) send_to_client(client, login_ack) # remove from pending_queue @@ -440,13 +464,13 @@ module JamWebsockets rescue => e # send back a failure ack and bail @log.debug "client requested non-existent session. client:#{client.request['origin']} user:#{context.user.email}" - login_jam_session = @message_factory.login_jam_session_ack(true, e.to_s).to_s + login_jam_session = @message_factory.login_jam_session_ack(true, e.to_s) send_to_client(client, login_jam_session) return end # respond with LOGIN_JAM_SESSION_ACK to let client know it was successful - login_jam_session = @message_factory.login_jam_session_ack(false, nil).to_s + login_jam_session = @message_factory.login_jam_session_ack(false, nil) send_to_client(client, login_jam_session) # send 'new client' message to other members in the session @@ -464,18 +488,8 @@ module JamWebsockets def valid_login(username, password, token) - if !username.nil? and !password.nil? - # attempt login with username and password - user = User.find_by_email(username) - - if !user.nil? && user.authenticate(password) - @log.debug "#{username} login via password" - return user - else - @log.debug "#{username} login failure" - return nil - end - elsif !token.nil? + if !token.nil? && token != '' + @log.debug "logging in via token" # attempt login with token user = User.find_by_remember_token(token) @@ -483,10 +497,24 @@ module JamWebsockets @log.debug "no user found with token" return false else - @log.debug "#{username} login via token" + @log.debug "#{user} login via token" + return user + end + + elsif !username.nil? and !password.nil? + + @log.debug "logging in via user/pass '#{username}' '#{password}'" + # attempt login with username and password + user = User.find_by_email(username) + + if !user.nil? && user.authenticate(password) + @log.debug "#{user} login via password" + return user + else + @log.debug "#{username} login failure" return nil end - else + else raise SessionError, 'no login data was found in Login message' end diff --git a/spec/jam_websockets/router_spec.rb b/spec/jam_websockets/router_spec.rb index e0fc699a2..529cdea78 100644 --- a/spec/jam_websockets/router_spec.rb +++ b/spec/jam_websockets/router_spec.rb @@ -2,7 +2,7 @@ require 'spec_helper' require 'thread' LoginClient = Class.new do - attr_accessor :onmsgblock, :onopenblock + attr_accessor :onmsgblock, :onopenblock, :encode_json def initiaize() @@ -43,9 +43,10 @@ def login(router, user, password) login_ack = message_factory.login_ack("127.0.0.1") - router.should_receive(:send_to_client).with(client, login_ack.to_s) + router.should_receive(:send_to_client).with(client, login_ack) client.should_receive(:onclose) client.should_receive(:onerror) + client.should_receive(:request).and_return({ "query" => { "pb" => "true" } }) client.should_receive(:get_peername).and_return("\x00\x02\x93\v\x7F\x00\x00\x01\x00\x00\x00\x00\x00\x00\x00\x00") @router.new_client(client) @@ -65,7 +66,7 @@ def login_jam_session(router, client, jam_session) message_factory = MessageFactory.new login_jam_session = message_factory.login_jam_session(jam_session.id) login_ack = message_factory.login_jam_session_ack(false, nil); - router.should_receive(:send_to_client).with(client, login_ack.to_s) + router.should_receive(:send_to_client).with(client, login_ack) client.onmsgblock.call login_jam_session.to_s end @@ -98,6 +99,7 @@ describe Router do client.should_receive(:onclose) client.should_receive(:onerror) client.should_receive(:onmessage) + client.should_receive(:encode_json=) @router.new_client(client) @@ -128,7 +130,7 @@ describe Router do it "should not allow login of bogus user", :mq => true do TestClient = Class.new do - attr_accessor :onmsgblock, :onopenblock + attr_accessor :onmsgblock, :onopenblock, :encode_json def initiaize() @@ -153,10 +155,11 @@ describe Router do error_msg = message_factory.server_rejection_error("invalid login") - @router.should_receive(:send_to_client).with(client, error_msg.to_s) + @router.should_receive(:send_to_client).with(client, error_msg) client.should_receive(:close_websocket) client.should_receive(:onclose) client.should_receive(:onerror) + client.should_receive(:request).and_return({ "query" => { "pb" => "true" } }) @router.new_client(client) @@ -184,7 +187,7 @@ describe Router do user2 = FactoryGirl.create(:user) # in the jam session user3 = FactoryGirl.create(:user) # not in the jam session - jam_session = FactoryGirl.create(:jam_session, :user => user1) + jam_session = FactoryGirl.create(:jam_session, :creator => user1) jam_session_member1 = FactoryGirl.create(:jam_session_member, :user => user1, :jam_session => jam_session) jam_session_member2 = FactoryGirl.create(:jam_session_member, :user => user2, :jam_session => jam_session) @@ -202,7 +205,7 @@ describe Router do user1 = FactoryGirl.create(:user) # in the jam session user2 = FactoryGirl.create(:user) # in the jam session - jam_session = FactoryGirl.create(:jam_session, :user => user1) + jam_session = FactoryGirl.create(:jam_session, :creator => user1) jam_session_member1 = FactoryGirl.create(:jam_session_member, :user => user1, :jam_session => jam_session) jam_session_member2 = FactoryGirl.create(:jam_session_member, :user => user2, :jam_session => jam_session)