fix login issue*
This commit is contained in:
@@ -1,7 +1,7 @@
|
||||
defmodule Odinsea.Login.Client do
|
||||
@moduledoc """
|
||||
Client connection handler for the login server.
|
||||
Manages the login session state.
|
||||
Manages the login session state and packet encryption/decryption.
|
||||
"""
|
||||
|
||||
use GenServer, restart: :temporary
|
||||
@@ -10,6 +10,10 @@ defmodule Odinsea.Login.Client do
|
||||
|
||||
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,
|
||||
@@ -25,7 +29,25 @@ defmodule Odinsea.Login.Client do
|
||||
:second_password,
|
||||
:gender,
|
||||
:is_gm,
|
||||
:hardware_info
|
||||
: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
|
||||
@@ -39,6 +61,16 @@ defmodule Odinsea.Login.Client do
|
||||
|
||||
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,
|
||||
@@ -53,9 +85,27 @@ defmodule Odinsea.Login.Client do
|
||||
second_password: nil,
|
||||
gender: 0,
|
||||
is_gm: false,
|
||||
hardware_info: nil
|
||||
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)
|
||||
|
||||
@@ -66,8 +116,9 @@ defmodule Odinsea.Login.Client do
|
||||
def handle_info(:receive, %{socket: socket} = state) do
|
||||
case :gen_tcp.recv(socket, 0, 30_000) do
|
||||
{:ok, data} ->
|
||||
# Handle packet
|
||||
new_state = handle_packet(data, state)
|
||||
# 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}
|
||||
|
||||
@@ -96,23 +147,100 @@ defmodule Odinsea.Login.Client do
|
||||
:ok
|
||||
end
|
||||
|
||||
defp handle_packet(data, state) do
|
||||
packet = In.new(data)
|
||||
# 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
|
||||
<<raw_seq::little-16, raw_len::little-16, rest::binary>> = 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} ->
|
||||
Logger.debug("Login packet received: opcode=0x#{Integer.to_string(opcode, 16)}")
|
||||
# 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")
|
||||
Logger.warning("Failed to read packet opcode from #{state.ip}")
|
||||
state
|
||||
end
|
||||
end
|
||||
|
||||
defp dispatch_packet(opcode, packet, state) do
|
||||
# Use PacketProcessor to route packets
|
||||
alias Odinsea.Net.Processor
|
||||
|
||||
case Processor.handle(opcode, packet, state, :login) do
|
||||
@@ -137,4 +265,66 @@ defmodule Odinsea.Login.Client do
|
||||
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
|
||||
|
||||
Reference in New Issue
Block a user