defmodule Odinsea.Login.Client do @moduledoc """ Client connection handler for the login server. Manages the login session state and packet encryption/decryption. """ use GenServer, restart: :temporary require Logger alias Odinsea.Net.Packet.In alias Odinsea.Net.Opcodes alias Odinsea.Net.PacketLogger alias Odinsea.Login.Packets alias Odinsea.Net.Cipher.ClientCrypto alias Odinsea.Util.BitTools defstruct [ :socket, :ip, :state, :account_id, :account_name, :character_id, :world, :channel, :logged_in, :login_attempts, :second_password, :gender, :is_gm, :hardware_info, :crypto, :handshake_complete, # === NEW FIELDS - Critical Priority === :created_at, # Session creation time (for session timeout) :last_alive_ack, # Last pong received timestamp :server_transition, # Boolean - migrating between servers :macs, # [String.t()] - MAC addresses for ban checking :character_slots, # integer() - Max chars per world (default 3) # === NEW FIELDS - Medium Priority === :birthday, # integer() - YYMMDD format for PIN/SPW verification :monitored, # boolean() - GM monitoring flag :tempban, # DateTime.t() | nil - Temporary ban info :chat_mute, # boolean() - Chat restriction buffer: <<>>, character_ids: [] ] def start_link(socket) do GenServer.start_link(__MODULE__, socket) end @impl true def init(socket) do {:ok, {ip, _port}} = :inet.peername(socket) ip_string = format_ip(ip) Logger.info("Login client connected from #{ip_string}") # Generate IVs for encryption (4 bytes each) send_iv = :crypto.strong_rand_bytes(4) recv_iv = :crypto.strong_rand_bytes(4) # Create crypto context crypto = ClientCrypto.new_from_ivs(112, send_iv, recv_iv) # Get current timestamp for session tracking current_time = System.system_time(:millisecond) state = %__MODULE__{ socket: socket, ip: ip_string, state: :connected, account_id: nil, account_name: nil, character_id: nil, world: nil, channel: nil, logged_in: false, login_attempts: 0, second_password: nil, gender: 0, is_gm: false, hardware_info: nil, crypto: crypto, handshake_complete: false, buffer: <<>>, character_ids: [], # === NEW FIELDS INITIALIZATION === created_at: current_time, last_alive_ack: current_time, server_transition: false, macs: [], character_slots: 3, birthday: nil, monitored: false, tempban: nil, chat_mute: false } # Send hello packet (handshake) - unencrypted send_hello_packet(state, send_iv, recv_iv) # Start receiving packets send(self(), :receive) {:ok, state} end @impl true def handle_info(:receive, %{socket: socket} = state) do case :gen_tcp.recv(socket, 0, 30_000) do {:ok, data} -> # Append to buffer and process all complete packets new_state = %{state | buffer: state.buffer <> data} new_state = process_buffer(new_state) send(self(), :receive) {:noreply, new_state} {:error, :closed} -> Logger.info("Login client disconnected: #{state.ip}") {:stop, :normal, state} {:error, reason} -> Logger.warning("Login client error: #{inspect(reason)}") {:stop, :normal, state} end end @impl true def handle_info({:disconnect, reason}, state) do Logger.info("Disconnecting client #{state.ip}: #{inspect(reason)}") {:stop, :normal, state} end @impl true def terminate(_reason, state) do if state.socket do :gen_tcp.close(state.socket) end :ok end # Process all complete packets from the TCP buffer defp process_buffer(state) do case extract_packet(state.buffer, state.crypto) do {:ok, payload, remaining} -> # Decrypt the payload (AES then Shanda) and morph recv IV {updated_crypto, decrypted} = ClientCrypto.decrypt(state.crypto, payload) state = %{state | buffer: remaining, crypto: updated_crypto, handshake_complete: true } state = process_decrypted_packet(decrypted, state) # Try to process more packets from the buffer process_buffer(state) {:need_more, _} -> state {:error, reason} -> Logger.error("Packet error from #{state.ip}: #{inspect(reason)}") send(self(), {:disconnect, reason}) state end end # Extract a complete encrypted packet from the buffer using the 4-byte header defp extract_packet(buffer, _crypto) when byte_size(buffer) < 4 do {:need_more, buffer} end defp extract_packet(buffer, crypto) do <> = buffer # Validate header against current recv IV if not ClientCrypto.decode_header_valid?(crypto, raw_seq) do Logger.warning( "Invalid packet header: raw_seq=#{raw_seq} (0x#{Integer.to_string(raw_seq, 16)}), " <> "expected version check failed" ) {:error, :invalid_header} else # Decode actual packet length from header packet_len = ClientCrypto.decode_header_len(crypto, raw_seq, raw_len) cond do packet_len < 2 -> {:error, :invalid_length_small} packet_len > 65535 -> {:error, :invalid_length_large} byte_size(rest) < packet_len -> # Incomplete packet - wait for more data (don't consume header) {:need_more, buffer} true -> # Extract the encrypted payload and keep remainder payload = binary_part(rest, 0, packet_len) remaining = binary_part(rest, packet_len, byte_size(rest) - packet_len) {:ok, payload, remaining} end end end defp process_decrypted_packet(decrypted_data, state) do packet = In.new(decrypted_data) # Read opcode (first 2 bytes) case In.decode_short(packet) do {opcode, packet} -> # Extract remaining data (after opcode) for logging remaining_data = binary_part(packet.data, packet.index, packet.length - packet.index) # Log the decrypted packet context = %{ ip: state.ip, server_type: :login } PacketLogger.log_client_packet(opcode, remaining_data, context) dispatch_packet(opcode, packet, state) :error -> Logger.warning("Failed to read packet opcode from #{state.ip}") state end end defp dispatch_packet(opcode, packet, state) do alias Odinsea.Net.Processor case Processor.handle(opcode, packet, state, :login) do {:ok, new_state} -> new_state {:error, reason, new_state} -> Logger.error("Packet processing error: #{inspect(reason)}") new_state {:disconnect, reason} -> Logger.warning("Client disconnected: #{inspect(reason)}") send(self(), {:disconnect, reason}) state end end defp format_ip({a, b, c, d}) do "#{a}.#{b}.#{c}.#{d}" end defp format_ip({a, b, c, d, e, f, g, h}) do "#{a}:#{b}:#{c}:#{d}:#{e}:#{f}:#{g}:#{h}" end defp send_hello_packet(state, send_iv, recv_iv) do # Get maple version from config maple_version = 112 # Build hello packet hello_packet = Packets.get_hello(maple_version, send_iv, recv_iv) # Log the hello packet context = %{ip: state.ip, server_type: :login} PacketLogger.log_raw_packet("loopback", "HELLO", hello_packet, context) # Send the hello packet (it already includes the length header) case :gen_tcp.send(state.socket, hello_packet) do :ok -> :ok {:error, reason} -> Logger.error("Failed to send hello packet: #{inspect(reason)}") {:error, reason} end end @doc """ Sends a packet to the client with proper encryption. """ def send_packet(client_pid, packet_data) when is_pid(client_pid) do GenServer.call(client_pid, {:send_packet, packet_data}) end @impl true def handle_call({:send_packet, packet_data}, _from, state) do case encrypt_and_send(packet_data, state) do {:ok, new_state} -> {:reply, :ok, new_state} {:error, reason} -> {:reply, {:error, reason}, state} end end defp encrypt_and_send(data, state) do # Encrypt the data (Shanda then AES) and morph send IV {updated_crypto, encrypted, header} = ClientCrypto.encrypt(state.crypto, data) # Combine header and encrypted payload full_packet = header <> encrypted # Log the outgoing packet context = %{ip: state.ip, server_type: :login} PacketLogger.log_server_packet("SERVER", data, context) # Send the packet case :gen_tcp.send(state.socket, full_packet) do :ok -> {:ok, %{state | crypto: updated_crypto}} {:error, reason} -> Logger.error("Failed to send packet: #{inspect(reason)}") {:error, reason} end end end