Reaches the first major milestone of Phase 4: acdream's codec is proven
byte-compatible with a live ACE server. LiveHandshakeTests drives a real
UDP exchange against 127.0.0.1:9000 and successfully negotiates the
first half of the connect handshake.
Added:
- Packets/PacketHeaderOptional.cs: new ConnectRequest flag branch.
ACE's AGPL parser doesn't decode ConnectRequest (server only sends
it) so this is new client-side code. Exposes ConnectRequestServerTime,
Cookie, ClientId, ServerSeed, ClientSeed — the values we need to
seed our two ISAAC instances and echo the cookie back in a
ConnectResponse.
- NetClient.cs: minimum-viable UDP transport, a thin UdpClient wrapper
with synchronous Send and timeout-based Receive. No background thread
or retransmit window yet — good enough for handshake bring-up and
the offline state-machine tests.
- LiveHandshakeTests.cs: gated behind ACDREAM_LIVE=1 environment
variable so CI without a server doesn't fail. Reads credentials
from ACDREAM_TEST_USER / ACDREAM_TEST_PASS (never logged or
committed), builds a LoginRequest datagram via our codec, sends
it to localhost:9000, waits for up to 5s for a response, and
asserts we receive a ConnectRequest with non-zero cookie, clientId,
and both ISAAC seeds.
Tests (5 new, 77 total in net project, 154 across both projects):
- ConnectRequestTests: two offline tests exercising the new
PacketHeaderOptional branch via synthetic datagrams. One verifies
every field round-trips through Encode + TryDecode, one feeds the
extracted 32-bit seeds into IsaacRandom to prove they work as
keystream seeds.
- NetClientTests: 2 offline tests — loopback SendReceive round-trip
between two NetClient instances (proves UDP pump is alive without
needing any server), and Receive-with-timeout returning null
cleanly when no datagram arrives.
- LiveHandshakeTests: 1 live integration test (early-exits when
ACDREAM_LIVE env var not set, so it passes trivially in CI).
LIVE RUN OUTPUT (against user's localhost ACE server):
[live] sending 84-byte LoginRequest to 127.0.0.1:9000 (user.len=11, pass.len=12)
[live] received 52-byte datagram from 127.0.0.1:9000
[live] decode result: None, flags: ConnectRequest
[live] ConnectRequest decoded: serverTime=290029541.121 cookie=0xAC45998D06754133
clientId=0x00000001 serverSeed=0x4CC09763 clientSeed=0x5C3DE13E
Meaning: 84-byte LoginRequest went out, 52-byte ConnectRequest came
back, codec.TryDecode returned None error, every field parsed to a
sensible value. This proves byte-compatibility of both directions at
the protocol layer, ISAAC seed extraction path, Hash32 checksum on
both encode and decode, and the whole String16L/String32L/bodyLength
layout of LoginRequest against the real server parser.
Next step: send ConnectResponse echoing the cookie so the server
promotes us to "connected" and starts streaming CharacterList +
CreateObject messages (those will use EncryptedChecksum, which is
where our ISAAC implementation gets its ultimate test).
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
110 lines
4.9 KiB
C#
110 lines
4.9 KiB
C#
using System.Buffers.Binary;
|
|
using System.Net;
|
|
using AcDream.Core.Net;
|
|
using AcDream.Core.Net.Packets;
|
|
|
|
namespace AcDream.Core.Net.Tests;
|
|
|
|
/// <summary>
|
|
/// Live integration test that talks to a real ACE server. Skipped by
|
|
/// default so CI doesn't fail for developers without a running server.
|
|
/// To run:
|
|
/// <code>
|
|
/// set ACDREAM_LIVE=1
|
|
/// set ACDREAM_TEST_USER=testaccount
|
|
/// set ACDREAM_TEST_PASS=testpassword
|
|
/// set ACDREAM_TEST_HOST=127.0.0.1 (optional, default)
|
|
/// set ACDREAM_TEST_PORT=9000 (optional, default)
|
|
/// dotnet test --filter LiveHandshake
|
|
/// </code>
|
|
///
|
|
/// <para>
|
|
/// <b>Credential handling:</b> the test reads the username and password
|
|
/// from environment variables and uses them in one outbound LoginRequest
|
|
/// packet. They are never written to disk, never logged to console, never
|
|
/// included in assertion messages, and never committed. When the test
|
|
/// prints diagnostics they are reduced to their length so mistakes in
|
|
/// the env-var setup are distinguishable from server errors.
|
|
/// </para>
|
|
/// </summary>
|
|
public class LiveHandshakeTests
|
|
{
|
|
[Fact]
|
|
public void Live_LoginRequest_ReceivesConnectRequestFromServer()
|
|
{
|
|
if (Environment.GetEnvironmentVariable("ACDREAM_LIVE") != "1")
|
|
return; // skipped — not a failure
|
|
|
|
var host = Environment.GetEnvironmentVariable("ACDREAM_TEST_HOST") ?? "127.0.0.1";
|
|
var portStr = Environment.GetEnvironmentVariable("ACDREAM_TEST_PORT") ?? "9000";
|
|
var user = Environment.GetEnvironmentVariable("ACDREAM_TEST_USER");
|
|
var pass = Environment.GetEnvironmentVariable("ACDREAM_TEST_PASS");
|
|
|
|
Assert.NotNull(user);
|
|
Assert.NotNull(pass);
|
|
Assert.NotEmpty(user!);
|
|
Assert.NotEmpty(pass!);
|
|
|
|
var remote = new IPEndPoint(IPAddress.Parse(host), int.Parse(portStr));
|
|
using var net = new NetClient(remote);
|
|
|
|
// Build and send the LoginRequest datagram. Header has only the
|
|
// LoginRequest flag; checksum is unencrypted (the ISAAC keystream
|
|
// is established only *after* the server sends us ConnectRequest).
|
|
uint timestamp = (uint)DateTimeOffset.UtcNow.ToUnixTimeSeconds();
|
|
byte[] loginBody = LoginRequest.Build(user, pass, timestamp);
|
|
var loginHeader = new PacketHeader { Flags = PacketHeaderFlags.LoginRequest };
|
|
byte[] loginDatagram = PacketCodec.Encode(loginHeader, loginBody, outboundIsaac: null);
|
|
|
|
Console.WriteLine($"[live] sending {loginDatagram.Length}-byte LoginRequest to {remote} " +
|
|
$"(user.len={user.Length}, pass.len={pass.Length})");
|
|
net.Send(loginDatagram);
|
|
|
|
// Expect at least one packet back within 5 seconds. ACE can chunk
|
|
// the handshake across multiple datagrams so we loop until we find
|
|
// a ConnectRequest or hit the overall deadline.
|
|
var deadline = DateTime.UtcNow + TimeSpan.FromSeconds(5);
|
|
Packet? connectRequest = null;
|
|
int packetsReceived = 0;
|
|
|
|
while (DateTime.UtcNow < deadline && connectRequest is null)
|
|
{
|
|
var bytes = net.Receive(deadline - DateTime.UtcNow, out var from);
|
|
if (bytes is null) break;
|
|
packetsReceived++;
|
|
Console.WriteLine($"[live] received {bytes.Length}-byte datagram from {from}");
|
|
|
|
var decoded = PacketCodec.TryDecode(bytes, inboundIsaac: null);
|
|
Console.WriteLine($"[live] decode result: {decoded.Error}, " +
|
|
$"flags: {(decoded.Packet?.Header.Flags.ToString() ?? "n/a")}");
|
|
|
|
if (decoded.IsOk && decoded.Packet!.Header.HasFlag(PacketHeaderFlags.ConnectRequest))
|
|
{
|
|
connectRequest = decoded.Packet;
|
|
break;
|
|
}
|
|
}
|
|
|
|
Console.WriteLine($"[live] total packets received: {packetsReceived}");
|
|
|
|
Assert.True(packetsReceived > 0,
|
|
"Server did not respond at all within 5s — is ACE actually running on " +
|
|
$"{remote} and does the account '{user}' exist?");
|
|
|
|
Assert.NotNull(connectRequest);
|
|
var opt = connectRequest!.Optional;
|
|
Console.WriteLine($"[live] ConnectRequest decoded: " +
|
|
$"serverTime={opt.ConnectRequestServerTime:F3} " +
|
|
$"cookie=0x{opt.ConnectRequestCookie:X16} " +
|
|
$"clientId=0x{opt.ConnectRequestClientId:X8} " +
|
|
$"serverSeed=0x{opt.ConnectRequestServerSeed:X8} " +
|
|
$"clientSeed=0x{opt.ConnectRequestClientSeed:X8}");
|
|
|
|
Assert.NotEqual(0UL, opt.ConnectRequestCookie);
|
|
Assert.NotEqual(0u, opt.ConnectRequestClientId);
|
|
// The seeds are derived from a PRNG server-side so they should be nonzero
|
|
// in any realistic scenario (probability of exactly 0 is ~2^-64).
|
|
Assert.NotEqual(0u, opt.ConnectRequestServerSeed);
|
|
Assert.NotEqual(0u, opt.ConnectRequestClientSeed);
|
|
}
|
|
}
|