kozukata-toa/src/servers/aimedb/index.ts

179 lines
4.5 KiB
TypeScript

import { AIMEDB_HANDLERS, EXPECTED_PACKET_LENGTH } from "./handlers";
import { PacketHeaderStruct } from "./types/header";
import { decryptPacket, encryptPacket } from "./utils/crypto";
import { CommandId, ResultCodes } from "./utils/misc";
import CreateLogCtx from "lib/logger/logger";
import { Config } from "lib/setup/config";
import { createHash } from "crypto";
import type { Socket } from "net";
const logger = CreateLogCtx(__filename);
/**
* For legal reasons, we probably should not include the actual AimeDB key here.
* However, we can still check the key for correctness, so that users get feedback.
*/
const AIMEDB_KEY_SHA256 = "4968f79897cd9517f2b6050a951456106ca98b23535b3e985d62a800b66f3240";
const AimeDBServerFactory = () => {
if (!Config.AIMEDB_CONFIG.KEY) {
throw new Error("AimeDB key not set.");
}
const hash = createHash("sha256");
const digest = hash.update(Config.AIMEDB_CONFIG.KEY).digest("hex");
if (digest !== AIMEDB_KEY_SHA256) {
logger.warn(
"AimeDB key seems incorrect. Allowing it anyways, though games will probably break.",
{
key: Config.AIMEDB_CONFIG.KEY,
expectedSha256: AIMEDB_KEY_SHA256,
}
);
}
const AIMEDB_KEY = Buffer.from(Config.AIMEDB_CONFIG.KEY, "utf-8");
const logResponse = (header: InstanceType<typeof PacketHeaderStruct>, body: unknown) => {
if (header.result !== 1) {
logger.info(`Returned result code ${header.result}.`, {
header,
body,
});
} else {
logger.verbose(`Returned result code ${header.result}.`, {
header,
body,
});
}
};
const writeResponse = (
socket: Socket,
header: InstanceType<typeof PacketHeaderStruct>,
body: unknown
) => {
const headerBuf = PacketHeaderStruct.raw(header);
const chunks = [headerBuf];
// https://github.com/sarakusha/typed-struct/blob/main/src/struct.ts#L799
if (body !== null && typeof body === "object" && "$raw" in body) {
const bodyBuf = (body as { $raw: Buffer }).$raw;
chunks.push(bodyBuf);
}
const raw = Buffer.concat(chunks);
let encrypted: Buffer;
try {
encrypted = encryptPacket(raw, AIMEDB_KEY);
} catch (err) {
logger.error("Could not encrypt AimeDB response.", { err });
return;
}
socket.write(encrypted);
};
return (socket: Socket) => {
socket.on("connect", () => {
logger.debug(`${socket.remoteAddress} connected.`);
});
socket.on("data", async (data) => {
logger.verbose(`Received packet: ${data.toString("hex")}.`);
let packet: Buffer;
try {
packet = decryptPacket(data, AIMEDB_KEY);
} catch (err) {
logger.error("Could not decrypt AimeDB packet.", {
err,
});
return;
}
let header;
try {
header = new PacketHeaderStruct(packet.slice(0, 32));
} catch (err) {
logger.error("Decoding AimeDB header failed.", {
err,
data: packet.toString("hex"),
});
return;
}
logger.debug("Received AimeDB request.", {
header,
data: packet.slice(32).toString("hex"),
});
if (header.magic !== 0xa13e) {
logger.error("Request's magic bytes did not match expected value 0xA13E.", {
header,
});
return;
}
if (header.keychipId === "ABCD1234567" || header.storeId === 0xfff0) {
logger.warning("Received request from uninitialized AMLib.", { header });
}
if (header.commandId === CommandId.CLIENT_END_REQUEST) {
logger.debug("Client ended the session.", { header });
socket.destroy();
return;
}
const expectedLength = EXPECTED_PACKET_LENGTH[header.commandId];
if (!expectedLength) {
logger.warn(
`Packet 0x${header.commandId.toString(
16
)} does not declare an expected size. Allowing all packets.`,
{ header }
);
} else if (expectedLength !== header.length || packet.length !== header.length) {
logger.error("Packet does not have expected size.", {
expectedLength,
actualLength: packet.length,
declaredLength: header.length,
});
header.result = ResultCodes.UNKNOWN_ERROR;
header.length = 32;
logResponse(header, null);
writeResponse(socket, header, null);
return;
}
const handler = AIMEDB_HANDLERS[header.commandId];
if (!handler) {
logger.error("No handlers available for the requested command ID.", { header });
return;
}
const body = await handler(header, packet.slice(32));
logResponse(header, body);
writeResponse(socket, header, body);
});
socket.on("end", () => {
logger.debug(`${socket.remoteAddress} disconnected.`);
});
};
};
export default AimeDBServerFactory;