196 lines
4.9 KiB
TypeScript
196 lines
4.9 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 "../../lib/constants/aimedb";
|
|
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 = (
|
|
socket: Socket,
|
|
header: InstanceType<typeof PacketHeaderStruct>,
|
|
body: unknown
|
|
) => {
|
|
if (header.result !== 1) {
|
|
logger.info(`Returned result code ${header.result}.`, {
|
|
fromIp: socket.remoteAddress,
|
|
header,
|
|
body,
|
|
});
|
|
} else {
|
|
logger.verbose(`Returned result code ${header.result}.`, {
|
|
fromIp: socket.remoteAddress,
|
|
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.debug(`Received packet: ${data.toString("hex")}.`, {
|
|
ip: socket.remoteAddress,
|
|
});
|
|
|
|
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,
|
|
header: packet.slice(0, 32).toString("hex"),
|
|
});
|
|
return;
|
|
}
|
|
|
|
if (header.magic !== 0xa13e) {
|
|
logger.error("Request's magic bytes did not match expected value 0xA13E.", {
|
|
ip: socket.remoteAddress,
|
|
header,
|
|
});
|
|
return;
|
|
}
|
|
|
|
if (header.keychipId === "ABCD1234567" || header.storeId === 0xfff0) {
|
|
logger.warn("Received request from uninitialized AMLib.", {
|
|
ip: socket.remoteAddress,
|
|
header,
|
|
body: packet.slice(0, 32).toString("hex"),
|
|
});
|
|
} else {
|
|
logger.debug("Received AimeDB request.", {
|
|
ip: socket.remoteAddress,
|
|
header,
|
|
body: packet.slice(0, 32).toString("hex"),
|
|
});
|
|
}
|
|
|
|
if (header.commandId === CommandId.CLIENT_END_REQUEST) {
|
|
logger.debug("Client ended the session.", { ip: socket.remoteAddress });
|
|
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.", {
|
|
ip: socket.remoteAddress,
|
|
header,
|
|
expectedLength,
|
|
actualLength: packet.length,
|
|
declaredLength: header.length,
|
|
});
|
|
|
|
header.result = ResultCodes.UNKNOWN_ERROR;
|
|
header.length = 32;
|
|
|
|
logResponse(socket, 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.", {
|
|
ip: socket.remoteAddress,
|
|
header,
|
|
});
|
|
return;
|
|
}
|
|
|
|
const body = await handler(header, packet.slice(32));
|
|
|
|
logResponse(socket, header, body);
|
|
writeResponse(socket, header, body);
|
|
});
|
|
|
|
socket.on("end", () => {
|
|
logger.debug(`${socket.remoteAddress} disconnected.`);
|
|
});
|
|
};
|
|
};
|
|
|
|
export default AimeDBServerFactory;
|