class Rsrb::Net::Connection

Attributes

ip[R]

The connection address.

session[R]

The connection credentials, once they have been validated.

state[R]

The current state of the connection.

Public Instance Methods

post_init() click to toggle source

Called when the connection has been opened.

# File lib/rsrb/net/connection.rb, line 63
def post_init
  init_loggers
  @state = :opcode
  @buffer = Packet.new(-1, :RAW, '')
  @popcode = -1
  @psize = -1
  port, @ip = Socket.unpack_sockaddr_in(get_peername)

  response, throttled = should_throttle

  if throttled
    log! "Throttling connection for #{@ip}"
    send_data ([0] * 8 + [response]).pack('C' * 9)
    close_connection_after_writing
  else
    log "New connection from #{@ip}"
  end
end
process_buffer(lim = 0) click to toggle source

Handles each stage of the connection process. If any invalid data is received, the connection is closed and an error response may be sent.

# File lib/rsrb/net/connection.rb, line 129
def process_buffer(lim = 0)
  return if lim >= 32
  return if @buffer.empty?

  case @state
  when :opcode
    if @buffer.length >= 1
      opcode = @buffer.read_byte

      if opcode == OPCODE_PLAYERCOUNT
        log '[TYPE] : Online'
        send_data [WORLD.players.size].pack('n')
        close_connection true
      elsif opcode == OPCODE_UPDATE
        log '[TYPE] Update'
        send_data Array.new(8, 0).pack('C' * 8)
        @state = :update
      elsif opcode == OPCODE_GAME
        log '[TYPE] Login'
        @state = :login
      else
        log!"Invalid connection type [#{opcode}] received from #{@ip}"
      end
    end
  when :update
    if @buffer.length >= 4
      cache_id = @buffer.read_byte.ubyte
      file_id = @buffer.read_short.ushort
      priority = @buffer.read_byte.ubyte

      #          Logging.logger['cache'].debug "update server request (cache: #{cache_id}, file: #{file_id}, prio: #{priority})"

      data = $cache.get(cache_id + 1, file_id)
      total_size = data.size
      rounded_size = total_size
      rounded_size += 1 while rounded_size % 500 != 0
      blocks = rounded_size / 500
      sent_bytes = 0

      blocks.times do |i|
        pb = PacketBuilder.new(-1, :RAW)
        block_size = total_size - sent_bytes

        pb.add_byte cache_id
        pb.add_short file_id
        pb.add_short total_size
        pb.add_byte i

        block_size = 500 if block_size > 500

        pb.buffer << data.slice(sent_bytes, block_size)

        sent_bytes += block_size
        send_data pb.to_packet
      end
    end
  when :login
    if @buffer.length >= 1
      # Name hash
      @buffer.read_byte

      # Generate server key
      @server_key = rand(1 << 32)
      @state = :precrypted

      # Server update check
      if SERVER.settings[:update_mode]
        send_data (Array.new(8, 0) + [14]).pack('C' * 8 + 'C')
        return
      end

      # World full check
      if WORLD.full?
        send_data (Array.new(8, 0) + [7]).pack('C' * 8 + 'C')
        return
      end

      # Send response
      response = Array.new(8, 0)
      response << 0
      response << @server_key
      send_data response.pack('C' * 8 + 'C' + 'q')
    end
  when :precrypted
    if @buffer.length >= 2
      # Parse login opcode
      login_opcode = @buffer.read_byte.ubyte

      unless [16, 18].include?(login_opcode)
        log! "Invalid login opcode: #{login_opcode}"
        return
      end

      log "Got #{login_opcode}"
      # Parse login packet size
      login_size = @buffer.read_byte.ubyte
      log "Got Size #{login_size}"
      enc_size = login_size - (36 + 1 + 1 + 2)

      unless enc_size >= 1
        log! "Encrypted packet size zero or negative: #{enc_size}"
        return
      end

      @state = :crypted
      @login_size = login_size
      @enc_size = enc_size
    end
  when :crypted
    if @buffer.length >= @login_size
      # Magic ID
      magic = @buffer.read_byte.ubyte
      log "Got Magic #{magic}"
      return unless magic == 255

      # Version
      version = @buffer.read_short.ushort
      log "Got Protocol #{version}"
      #send_data [6].pack('C') unless version == SERVER.protocol
      #return unless version == SERVER.protocol


      # Low memory
      @buffer.read_byte

      9.times do
        crc = @buffer.read_int
        log "Read CRC#{crc}"
      end

      @enc_size -= 1


      ##
      # client we're using does not seem to use this
      ###
      reported_size = @buffer.read_byte.ubyte

      unless reported_size == @enc_size
        log! "Packet size mismatch (expected: #{@enc_size}, reported: #{reported_size})"
        return
      end

      secure_id = @buffer.read_byte.ubyte
      log "Got SID#{secure_id}"
      return unless secure_id == 10




      # Check to see that the keys match
      client_key = [@buffer.read_int, @buffer.read_int]
      server_key = [@buffer.read_int, @buffer.read_int]
      reported_server_key = server_key.pack('NN').unpack1('q')
      log "Got key #{reported_server_key} against #{@server_key}"

      unless reported_server_key == @server_key
        log! 'Session key mismatch!', "[EXPECTED: #{@server_key}]\n[REPORTED: #{reported_server_key}]"
        send_data [10].pack('C') # "Bad session id"
      end

      # Read credentials
      uid = @buffer.read_int
      username = Rsrb::Misc::NameUtils.format_name_protocol(@buffer.read_str)
      password = @buffer.read_str

      log "Got credentials for: [#{username.capitalize}] FROM #{@ip}"
      unless Rsrb::Misc::NameUtils.valid_name?(username)
        log! "Invalid username: [#{username.capitalize}] FROM #{@ip}"
        send_data [11].pack('C')
      end

      # Set up cipher
      session_key = client_key + server_key
      session_key.collect(&:int)
      in_cipher = Rsrb::Net::ISAAC.new(session_key)
      out_cipher = Rsrb::Net::ISAAC.new(session_key.collect { |i| i + 50 })

      @session = Session.new(self, username, password, uid, in_cipher, out_cipher)
      @state = :authenticated
      log "Authenticated session for #{@session.username.capitalize}"
      WORLD.receive(@session)
    end
  when :authenticated
    if @popcode == -1
      if @buffer.length >= 1
        opcode = @buffer.read_byte
        random = @session.in_cipher.next_value.ubyte
        @popcode = (opcode - random).ubyte
        @psize = PACKET_SIZES[@popcode]
      else
        return
      end
    end

    if @psize == -1
      if @buffer.length >= 1
        @psize = @buffer.read_byte
      else
        return
      end
    end

    if @buffer.length >= @psize
      payload = @buffer.slice!(0...@psize)
      process_packet Packet.new(@popcode, :fixed, payload)
      @popcode = -1
      @psize = -1
    end
  end

  process_buffer(lim + 1) if !@buffer.empty?
end
process_packet(packet) click to toggle source
# File lib/rsrb/net/connection.rb, line 343
def process_packet(packet)
  return if packet.opcode.zero?

  Rsrb::World::Task.new do
    Rsrb::Net.handle_packet(@session.player, packet)
  end

rescue StandardError => e
  err 'Error processing packet', e
end
receive_data(data) click to toggle source

Reads data into the buffer, and runs process_buffer on the received data.

# File lib/rsrb/net/connection.rb, line 123
def receive_data(data)
  @buffer << data unless data.empty?
  process_buffer
end
send_data(data) click to toggle source

Sends data back

Calls superclass method
# File lib/rsrb/net/connection.rb, line 102
def send_data(data)
  if data.instance_of? Packet
    if data.type == :RAW
      super data.buffer
    else
      out_buffer = [data.opcode+@session.out_cipher.next_value.ubyte]
      out_types = 'C'

      if data.type != :FIXED
        out_buffer << data.buffer.size
        out_types << (data.type == :VARSH ? 'n' : 'C')
      end

      super out_buffer.pack(out_types) + data.buffer
    end
  else
    super
  end
end
should_throttle() click to toggle source
# File lib/rsrb/net/connection.rb, line 82
def should_throttle
  # Increase connection count
  CONNECTION_COUNTS[@ip] = CONNECTION_COUNTS.include?(@ip) ? CONNECTION_COUNTS[@ip] + 1 : 1

  # Limit connection time
  time_exceeded = CONNECTION_TIMES.include?(@ip) && (Time.now - CONNECTION_TIMES[@ip] < CONNECTION_INTERVAL)

  CONNECTION_TIMES[@ip] = Time.now

  return 16, true if time_exceeded

  # Limit connection count
  return 9, true if CONNECTION_COUNTS[@ip] > CONNECTION_MAX

  log "#{CONNECTION_COUNTS[@ip]} connections now from #{@ip}"

  return 0, false
end
unbind() click to toggle source

Called when the connection has been closed.

# File lib/rsrb/net/connection.rb, line 356
def unbind
  log! "Closed connection for #{@ip}"

  if CONNECTION_COUNTS.include?(@ip)
    count = CONNECTION_COUNTS[@ip]
    count -= 1

    if count <= 0
      CONNECTION_COUNTS.delete @ip
    else
      CONNECTION_COUNTS[@ip] = count
    end
  end
  Rsrb::World::Task.new do
    WORLD.unregister(@session.player) if !@session.nil? && !@session.player.nil?
  end
rescue StandardError => e
  err('An error occurred while unbinding peer from socket!', e)
end