549 lines
14 KiB
C++
549 lines
14 KiB
C++
#include "classic_stream.h"
|
|
|
|
#include <arpa/inet.h>
|
|
#include <cerrno>
|
|
#include <chrono>
|
|
#include <cmath>
|
|
#include <cstdio>
|
|
#include <cstring>
|
|
#include <fcntl.h>
|
|
#include <netdb.h>
|
|
#include <netinet/in.h>
|
|
#include <netinet/tcp.h>
|
|
#include <sys/socket.h>
|
|
#include <unistd.h>
|
|
|
|
namespace mtnet::classic {
|
|
|
|
namespace {
|
|
constexpr size_t RECV_CHUNK = 32 * 1024;
|
|
|
|
uint32_t now_ms() {
|
|
using namespace std::chrono;
|
|
return static_cast<uint32_t>(
|
|
duration_cast<milliseconds>(steady_clock::now().time_since_epoch()).count());
|
|
}
|
|
|
|
void set_nonblocking(int fd) {
|
|
int fl = fcntl(fd, F_GETFL, 0);
|
|
fcntl(fd, F_SETFL, fl | O_NONBLOCK);
|
|
}
|
|
|
|
bool plausible_character_add(const uint8_t *p, size_t available) {
|
|
if (available < sizeof(GCCharacterAdd) || p[0] != HDR_GC_CHARACTER_ADD) {
|
|
return false;
|
|
}
|
|
GCCharacterAdd packet{};
|
|
std::memcpy(&packet, p, sizeof(packet));
|
|
return packet.vid != 0 && packet.race != 0 && packet.type <= 8 &&
|
|
std::isfinite(packet.angle) && std::fabs(packet.angle) <= 360.0f &&
|
|
packet.moving_speed <= 255 && packet.attack_speed <= 255;
|
|
}
|
|
} // namespace
|
|
|
|
ClassicStream::~ClassicStream() {
|
|
disconnect();
|
|
}
|
|
|
|
void ClassicStream::set_state(State s) {
|
|
if (m_state == s) {
|
|
return;
|
|
}
|
|
m_state = s;
|
|
if (on_state_change) {
|
|
on_state_change(s);
|
|
}
|
|
}
|
|
|
|
// --------------------------------------------------------------------- socket
|
|
bool ClassicStream::connect(const std::string &host, uint16_t port) {
|
|
disconnect();
|
|
|
|
addrinfo hints{};
|
|
hints.ai_family = AF_INET;
|
|
hints.ai_socktype = SOCK_STREAM;
|
|
addrinfo *res = nullptr;
|
|
char portbuf[8];
|
|
std::snprintf(portbuf, sizeof(portbuf), "%u", port);
|
|
if (getaddrinfo(host.c_str(), portbuf, &hints, &res) != 0 || !res) {
|
|
m_last_error = "getaddrinfo(" + host + ") failed";
|
|
return false;
|
|
}
|
|
|
|
m_sock = ::socket(res->ai_family, res->ai_socktype, res->ai_protocol);
|
|
if (m_sock < 0) {
|
|
m_last_error = std::string("socket: ") + std::strerror(errno);
|
|
freeaddrinfo(res);
|
|
return false;
|
|
}
|
|
set_nonblocking(m_sock);
|
|
int one = 1;
|
|
setsockopt(m_sock, IPPROTO_TCP, TCP_NODELAY, &one, sizeof(one));
|
|
|
|
int rc = ::connect(m_sock, res->ai_addr, res->ai_addrlen);
|
|
freeaddrinfo(res);
|
|
if (rc == 0) {
|
|
set_state(State::Online);
|
|
} else if (errno == EINPROGRESS || errno == EWOULDBLOCK) {
|
|
set_state(State::Connecting);
|
|
} else {
|
|
m_last_error = std::string("connect: ") + std::strerror(errno);
|
|
::close(m_sock);
|
|
m_sock = -1;
|
|
return false;
|
|
}
|
|
return true;
|
|
}
|
|
|
|
void ClassicStream::disconnect() {
|
|
if (m_sock >= 0) {
|
|
::close(m_sock);
|
|
m_sock = -1;
|
|
}
|
|
m_recv.clear();
|
|
m_send.clear();
|
|
m_seq_idx = 0;
|
|
m_seq_on = false;
|
|
m_handshake_seen = false;
|
|
m_phase = PHASE_HANDSHAKE;
|
|
m_cipher.clean_up();
|
|
set_state(State::Offline);
|
|
}
|
|
|
|
void ClassicStream::discard_recv(size_t n) {
|
|
m_recv.discard(n);
|
|
}
|
|
|
|
void ClassicStream::decrypt_appended(size_t n) {
|
|
if (n == 0 || !m_cipher.activated()) {
|
|
return;
|
|
}
|
|
// the n bytes just written sit at the tail of the unread region
|
|
m_cipher.decrypt(m_recv.mutable_unread() + (m_recv.readable() - n), n);
|
|
}
|
|
|
|
bool ClassicStream::recv_into_buffer() {
|
|
uint8_t *dst = m_recv.reserve_write(RECV_CHUNK);
|
|
ssize_t n = ::recv(m_sock, dst, RECV_CHUNK, 0);
|
|
if (n > 0) {
|
|
m_recv.commit_write(static_cast<size_t>(n), RECV_CHUNK);
|
|
decrypt_appended(static_cast<size_t>(n));
|
|
return true;
|
|
}
|
|
m_recv.commit_write(0, RECV_CHUNK);
|
|
if (n == 0) {
|
|
m_last_error = "peer closed";
|
|
return false;
|
|
}
|
|
if (errno == EAGAIN || errno == EWOULDBLOCK) {
|
|
return true;
|
|
}
|
|
m_last_error = std::string("recv: ") + std::strerror(errno);
|
|
return false;
|
|
}
|
|
|
|
bool ClassicStream::flush_send() {
|
|
while (m_send.readable() > 0) {
|
|
ssize_t n = ::send(m_sock, m_send.read_ptr(), m_send.readable(), 0);
|
|
if (n > 0) {
|
|
m_send.discard(static_cast<size_t>(n));
|
|
continue;
|
|
}
|
|
if (n < 0 && (errno == EAGAIN || errno == EWOULDBLOCK)) {
|
|
break;
|
|
}
|
|
m_last_error = std::string("send: ") + std::strerror(errno);
|
|
return false;
|
|
}
|
|
return true;
|
|
}
|
|
|
|
void ClassicStream::process() {
|
|
if (m_sock < 0) {
|
|
return;
|
|
}
|
|
if (m_state == State::Connecting) {
|
|
int err = 0;
|
|
socklen_t len = sizeof(err);
|
|
if (getsockopt(m_sock, SOL_SOCKET, SO_ERROR, &err, &len) < 0 || err != 0) {
|
|
m_last_error = "connect failed";
|
|
disconnect();
|
|
return;
|
|
}
|
|
set_state(State::Online);
|
|
}
|
|
if (!recv_into_buffer()) {
|
|
disconnect();
|
|
return;
|
|
}
|
|
dispatch();
|
|
if (m_sock >= 0 && !flush_send()) {
|
|
disconnect();
|
|
}
|
|
}
|
|
|
|
// ----------------------------------------------------------------- framing core
|
|
void ClassicStream::feed(const void *data, size_t n) {
|
|
m_recv.write(data, n);
|
|
decrypt_appended(n);
|
|
dispatch();
|
|
}
|
|
|
|
void ClassicStream::emit_bytes(const void *p, size_t n) {
|
|
if (!m_cipher.activated()) {
|
|
m_send.write(p, n);
|
|
return;
|
|
}
|
|
// CTR stream: encrypt in call order; the encoder keeps its counter across calls.
|
|
std::string tmp(static_cast<const char *>(p), n);
|
|
m_cipher.encrypt(tmp.data(), n);
|
|
m_send.write(tmp.data(), n);
|
|
}
|
|
|
|
size_t ClassicStream::take_outgoing(void *dst, size_t cap) {
|
|
size_t n = m_send.readable() < cap ? m_send.readable() : cap;
|
|
if (n) {
|
|
std::memcpy(dst, m_send.read_ptr(), n);
|
|
m_send.discard(n);
|
|
}
|
|
return n;
|
|
}
|
|
|
|
void ClassicStream::append_sequence_if_needed(uint8_t header) {
|
|
if (!m_seq_on || !is_sequence_cg(header)) {
|
|
return;
|
|
}
|
|
uint8_t seq = SEQUENCE_TABLE[m_seq_idx];
|
|
m_seq_idx = (m_seq_idx + 1) % SEQUENCE_TABLE_SIZE;
|
|
emit_bytes(&seq, 1);
|
|
}
|
|
|
|
bool ClassicStream::send_fixed(const void *struct_bytes, size_t n) {
|
|
if (n == 0) {
|
|
return false;
|
|
}
|
|
uint8_t header = *static_cast<const uint8_t *>(struct_bytes);
|
|
emit_bytes(struct_bytes, n);
|
|
append_sequence_if_needed(header);
|
|
if (m_trace) {
|
|
std::fprintf(stderr, "[classic] send hdr=%u n=%zu seq=%d\n", header, n,
|
|
(m_seq_on && is_sequence_cg(header)));
|
|
}
|
|
return true;
|
|
}
|
|
|
|
bool ClassicStream::send_dynamic(const void *bytes, size_t n) {
|
|
if (n < sizeof(DynHead)) {
|
|
return false;
|
|
}
|
|
uint8_t header = *static_cast<const uint8_t *>(bytes);
|
|
emit_bytes(bytes, n);
|
|
append_sequence_if_needed(header);
|
|
return true;
|
|
}
|
|
|
|
// ----------------------------------------------------------------- dispatch
|
|
bool ClassicStream::handle_control(uint8_t header, bool &consumed) {
|
|
consumed = false;
|
|
switch (header) {
|
|
case HDR_HANDSHAKE: {
|
|
Handshake hs{};
|
|
if (!m_recv.peek(&hs, sizeof(hs))) {
|
|
return true; // need more
|
|
}
|
|
discard_recv(sizeof(hs));
|
|
consumed = true;
|
|
// mirror EterLib/PythonNetworkStreamPhaseHandshake.cpp + desc.cpp:
|
|
m_server_time_base = hs.time + static_cast<uint32_t>(hs.delta);
|
|
m_client_time_base = now_ms();
|
|
hs.time = hs.time + 2u * static_cast<uint32_t>(hs.delta);
|
|
hs.delta = 0;
|
|
const bool initial = !m_handshake_seen;
|
|
m_handshake_seen = true;
|
|
if (!initial && m_time_sync_mode) {
|
|
// PythonNetworkStreamPhaseHandShake.cpp uses 0xFF only for the
|
|
// first handshake. Select/Game phase resyncs are sent as the
|
|
// same struct with header 0xFC and the next sequence byte.
|
|
hs.header = HDR_CG_TIME_SYNC;
|
|
send_fixed(&hs, sizeof(hs));
|
|
} else {
|
|
// Initial handshake (and the mark side's repeated login
|
|
// handshake) stays 0xFF and has no sequence byte.
|
|
emit_bytes(&hs, sizeof(hs));
|
|
}
|
|
if (on_server_handshake) {
|
|
on_server_handshake();
|
|
}
|
|
if (m_trace) {
|
|
std::fprintf(stderr, "[classic] handshake echo time=%u\n", hs.time);
|
|
}
|
|
return true;
|
|
}
|
|
case HDR_GC_TIME_SYNC: { // 0xFC — 1-byte "handshake ok" blank (re-sync path)
|
|
Blank b{};
|
|
if (!m_recv.peek(&b, sizeof(b))) {
|
|
return true;
|
|
}
|
|
discard_recv(sizeof(b));
|
|
consumed = true;
|
|
if (on_handshake_ok) {
|
|
on_handshake_ok();
|
|
}
|
|
return true;
|
|
}
|
|
case HDR_GC_PHASE: {
|
|
Phase_ p{};
|
|
if (!m_recv.peek(&p, sizeof(p))) {
|
|
return true;
|
|
}
|
|
discard_recv(sizeof(p));
|
|
consumed = true;
|
|
m_phase = p.phase;
|
|
if (m_trace) {
|
|
std::fprintf(stderr, "[classic] phase -> %u\n", p.phase);
|
|
}
|
|
if (on_phase) {
|
|
on_phase(p.phase);
|
|
}
|
|
return true;
|
|
}
|
|
case HDR_GC_PING: {
|
|
Blank b{};
|
|
if (!m_recv.peek(&b, sizeof(b))) {
|
|
return true;
|
|
}
|
|
discard_recv(sizeof(b));
|
|
consumed = true;
|
|
uint8_t pong = HDR_CG_PONG; // 0xFE
|
|
send_fixed(&pong, 1); // CG_PONG is bSeq=true (sizeof(BYTE)+seq)
|
|
return true;
|
|
}
|
|
case HDR_GC_BINDUDP: {
|
|
BindUDP u{};
|
|
if (!m_recv.peek(&u, sizeof(u))) {
|
|
return true;
|
|
}
|
|
discard_recv(sizeof(u));
|
|
consumed = true;
|
|
return true; // UDP not used
|
|
}
|
|
case HDR_KEY_AGREEMENT: { // 0xFB — GC_KEY_AGREEMENT (server's DH2 blob)
|
|
if (m_recv.readable() < sizeof(KeyAgreement)) {
|
|
return true; // need the whole 261-byte packet (consumed stays false)
|
|
}
|
|
consumed = true;
|
|
return handle_key_agreement();
|
|
}
|
|
case HDR_GC_KEY_AGREEMENT_COMPLETED: { // 0xFA
|
|
KeyAgreementCompleted kac{};
|
|
if (!m_recv.peek(&kac, sizeof(kac))) {
|
|
return true;
|
|
}
|
|
discard_recv(sizeof(kac));
|
|
consumed = true;
|
|
m_cipher.set_activated(true);
|
|
// the server may have pipelined encrypted bytes right behind this;
|
|
// decrypt whatever's already buffered before we dispatch it.
|
|
if (m_recv.readable() > 0) {
|
|
m_cipher.decrypt(m_recv.mutable_unread(), m_recv.readable());
|
|
}
|
|
if (m_trace) {
|
|
std::fprintf(stderr, "[classic] cipher activated\n");
|
|
}
|
|
if (on_cipher_active) {
|
|
on_cipher_active();
|
|
}
|
|
return true;
|
|
}
|
|
default:
|
|
return true; // not a control header
|
|
}
|
|
}
|
|
|
|
bool ClassicStream::handle_key_agreement() {
|
|
KeyAgreement pkt{};
|
|
if (!m_recv.peek(&pkt, sizeof(pkt))) {
|
|
return true; // need the whole 261-byte packet
|
|
}
|
|
discard_recv(sizeof(pkt));
|
|
|
|
// generate our own DH2 public blob
|
|
KeyAgreement out{};
|
|
out.header = HDR_KEY_AGREEMENT; // CG_KEY_AGREEMENT (same 0xFB)
|
|
size_t data_len = sizeof(out.data);
|
|
size_t agreed = m_cipher.prepare(out.data, &data_len);
|
|
if (agreed == 0) {
|
|
m_last_error = "classic cipher: DH2 Prepare failed";
|
|
if (on_error) {
|
|
on_error(m_last_error);
|
|
}
|
|
return false;
|
|
}
|
|
out.agreed_length = static_cast<uint16_t>(agreed);
|
|
out.data_length = static_cast<uint16_t>(data_len);
|
|
|
|
// derive keys from the peer's blob (does NOT flip activated_ yet)
|
|
if (!m_cipher.activate(m_polarity, pkt.agreed_length, pkt.data, pkt.data_length)) {
|
|
m_last_error = "classic cipher: DH2 Agree/SetUp failed";
|
|
if (on_error) {
|
|
on_error(m_last_error);
|
|
}
|
|
return false;
|
|
}
|
|
|
|
// reply with our blob — still plaintext (peer activates on its own COMPLETED)
|
|
emit_bytes(&out, sizeof(out));
|
|
if (m_trace) {
|
|
std::fprintf(stderr, "[classic] key agreement: replied, keys ready\n");
|
|
}
|
|
return true;
|
|
}
|
|
|
|
void ClassicStream::dispatch() {
|
|
for (;;) {
|
|
if (m_recv.readable() == 0) {
|
|
return;
|
|
}
|
|
uint8_t header = *m_recv.read_ptr();
|
|
|
|
// zero padding between packets (cipher block alignment on the m2dev side;
|
|
// harmless to skip here too).
|
|
if (header == 0) {
|
|
discard_recv(1);
|
|
continue;
|
|
}
|
|
|
|
bool consumed = false;
|
|
if (!handle_control(header, consumed)) {
|
|
if (on_error && m_last_error.empty()) {
|
|
on_error("dispatch aborted");
|
|
}
|
|
disconnect();
|
|
return;
|
|
}
|
|
if (consumed) {
|
|
continue;
|
|
}
|
|
// control handler said "need more bytes" for a control header it owns?
|
|
if (header == HDR_HANDSHAKE || header == HDR_GC_TIME_SYNC || header == HDR_GC_PHASE ||
|
|
header == HDR_GC_PING || header == HDR_GC_BINDUDP || header == HDR_KEY_AGREEMENT ||
|
|
header == HDR_GC_KEY_AGREEMENT_COMPLETED) {
|
|
return; // wait for the rest of this control packet
|
|
}
|
|
|
|
// Some side connections share the classic cipher/control handshake but
|
|
// have their own packet framing. Give that connection a chance before
|
|
// the normal GC static/dynamic table is consulted.
|
|
if (on_raw_packet) {
|
|
RawPacketResult raw = on_raw_packet(header, m_recv.read_ptr(), m_recv.readable());
|
|
switch (raw.status) {
|
|
case RawPacketStatus::NeedMore:
|
|
return;
|
|
case RawPacketStatus::Consumed:
|
|
if (raw.bytes == 0 || raw.bytes > m_recv.readable()) {
|
|
m_last_error = "raw packet consumed invalid byte count";
|
|
if (on_error) {
|
|
on_error(m_last_error);
|
|
}
|
|
disconnect();
|
|
return;
|
|
}
|
|
discard_recv(raw.bytes);
|
|
continue;
|
|
case RawPacketStatus::Error:
|
|
m_last_error = "raw packet framing error";
|
|
if (on_error) {
|
|
on_error(m_last_error);
|
|
}
|
|
disconnect();
|
|
return;
|
|
case RawPacketStatus::NotHandled:
|
|
break;
|
|
}
|
|
}
|
|
|
|
// --- dynamic-size GC packet ---
|
|
if (is_dynamic_gc(header)) {
|
|
DynHead dh{};
|
|
if (!m_recv.peek(&dh, sizeof(dh))) {
|
|
return;
|
|
}
|
|
if (dh.size < sizeof(DynHead)) {
|
|
m_last_error = "dynamic packet size underflow";
|
|
if (on_error) {
|
|
on_error(m_last_error);
|
|
}
|
|
disconnect();
|
|
return;
|
|
}
|
|
if (m_recv.readable() < dh.size) {
|
|
return; // whole packet not here yet
|
|
}
|
|
m_recv.discard(sizeof(DynHead));
|
|
uint32_t body_len = dh.size - sizeof(DynHead);
|
|
const uint8_t *body = m_recv.read_ptr();
|
|
if (m_trace) {
|
|
std::fprintf(stderr, "[classic] recv hdr=%u n=%u dynamic=1\n", header, dh.size);
|
|
}
|
|
bool ok = !on_packet || on_packet(header, body, body_len);
|
|
discard_recv(body_len);
|
|
if (!ok) {
|
|
disconnect();
|
|
return;
|
|
}
|
|
continue;
|
|
}
|
|
|
|
// --- static-size GC packet --- (packet_size_gc = FULL size incl. header)
|
|
int total = packet_size_gc(header);
|
|
if (total <= 0) {
|
|
m_last_error = "unknown/unsupported GC header " + std::to_string(header);
|
|
// Some 40250 deployments append one legacy entity packet variant that
|
|
// is not present in the checked-in header map. It has no length field,
|
|
// but the following normal GC_CHARACTER_ADD stream is self-describing.
|
|
// During PHASE_LOADING, recover only at a strongly validated character
|
|
// boundary; this keeps a deployment-specific spawn packet from aborting
|
|
// the whole login while leaving malformed encrypted data fatal elsewhere.
|
|
if (m_phase == PHASE_LOADING) {
|
|
const uint8_t *bytes = m_recv.read_ptr();
|
|
bool recovered = false;
|
|
for (size_t i = 1; i + sizeof(GCCharacterAdd) <= m_recv.readable(); ++i) {
|
|
if (!plausible_character_add(bytes + i, m_recv.readable() - i)) {
|
|
continue;
|
|
}
|
|
if (m_trace) {
|
|
std::fprintf(stderr, "[classic] recovering loading stream at +%zu\n", i);
|
|
}
|
|
discard_recv(i);
|
|
m_last_error.clear();
|
|
recovered = true;
|
|
break;
|
|
}
|
|
if (recovered) {
|
|
continue;
|
|
}
|
|
}
|
|
if (on_error) {
|
|
on_error(m_last_error);
|
|
}
|
|
disconnect();
|
|
return;
|
|
}
|
|
if (m_recv.readable() < static_cast<size_t>(total)) {
|
|
return;
|
|
}
|
|
const uint8_t *p = m_recv.read_ptr();
|
|
if (m_trace) {
|
|
std::fprintf(stderr, "[classic] recv hdr=%u n=%d dynamic=0\n", header, total);
|
|
}
|
|
bool ok = !on_packet || on_packet(header, p + 1, static_cast<uint32_t>(total - 1));
|
|
discard_recv(static_cast<size_t>(total));
|
|
if (!ok) {
|
|
disconnect();
|
|
return;
|
|
}
|
|
}
|
|
}
|
|
|
|
} // namespace mtnet::classic
|