kozukata-toa/src/servers/aimedb/handlers/felica-conversion.ts

272 lines
7.4 KiB
TypeScript

// On real ALL.Net, there's just a massive database of every supported FeliCa's
// IDm -> access code mappings. Since we don't have access to such information,
// every FeliCa card will be treated like FeliCa mobile: an access code is
// generated, and then stored with its IDm for future lookups.
//
// This also means you'll need to get some more keys, but if you're lazy
// you can probably just set
// - RESERVED_CARD_PREFIX to something not starting with 0 or 3
// - RESERVED_CARD_KEY to a random 16-digit hex string, where each digit displays
// exactly once.
import {
FelicaExtendedLookupRequestStruct,
FelicaExtendedLookupResponseStruct,
FelicaLookupRequestStruct,
FelicaLookupResponseStruct,
} from "../types/felica-conversion";
import { PacketHeaderStruct } from "../types/header";
import { CalculateAccessCode } from "../utils/access-code";
import { IsSupportedFelicaMobile, IsSupportedFelica } from "../utils/felica";
import { CommandId, CompanyCodes, PortalRegistration, ResultCodes } from "../utils/misc";
import { desc, eq } from "drizzle-orm";
import { db } from "external/db/db";
import { card, felicaCardLookup, felicaMobileLookup } from "external/db/schemas/index";
import CreateLogCtx from "lib/logger/logger";
import { Config } from "lib/setup/config";
import type { AimeDBHandlerFn } from "../types/handlers";
const logger = CreateLogCtx(__filename);
export const FelicaLookupHandler: AimeDBHandlerFn<"FelicaLookupResponse"> = async (
header,
data
) => {
header.commandId = CommandId.FELICA_LOOKUP_RESPONSE;
header.length = PacketHeaderStruct.baseSize + FelicaLookupResponseStruct.baseSize;
header.result = ResultCodes.SUCCESS;
const req = new FelicaLookupRequestStruct(data);
const resp = new FelicaLookupResponseStruct();
if (!IsSupportedFelica(req.osVer)) {
header.result = ResultCodes.INVALID_AIME_ID;
resp.felicaIndex = -1;
return resp;
}
const table = IsSupportedFelicaMobile(req.osVer) ? felicaMobileLookup : felicaCardLookup;
const result = await db
.select()
.from(table)
.where(eq(table.idm, req.idm.toString(16)))
.then((r) => r[0]);
if (!result) {
resp.felicaIndex = -1;
} else {
resp.felicaIndex = result.id;
resp.accessCode.set(Buffer.from(result.accessCode, "hex"));
}
return resp;
};
export const FelicaExtendedLookupHandler: AimeDBHandlerFn<"FelicaExtendedLookupResponse"> = async (
header,
data
) => {
header.commandId = CommandId.EXTENDED_FELICA_ACCOUNT_RESPONSE;
header.length = PacketHeaderStruct.baseSize + FelicaExtendedLookupResponseStruct.baseSize;
header.result = ResultCodes.SUCCESS;
const req = new FelicaExtendedLookupRequestStruct(data);
const resp = new FelicaExtendedLookupResponseStruct();
resp.accountId = -1;
logger.debug("Parsed response body.", { req });
if (req.companyCode < 0 || req.companyCode > 4) {
header.result = ResultCodes.INVALID_AIME_ID;
return resp;
}
if (!IsSupportedFelica(req.osVer)) {
header.result = ResultCodes.INVALID_AIME_ID;
return resp;
}
let result;
if (IsSupportedFelicaMobile(req.osVer)) {
result = await db
.select()
.from(felicaMobileLookup)
.where(eq(felicaMobileLookup.idm, req.idm.toString(16)))
.leftJoin(card, eq(card.accessCode, felicaMobileLookup.accessCode))
.then((r) => r[0]);
} else {
result = await db
.select()
.from(felicaCardLookup)
.where(eq(felicaCardLookup.idm, req.idm.toString(16)))
.leftJoin(card, eq(card.accessCode, felicaCardLookup.accessCode))
.then((r) => r[0]);
}
if (result) {
const lookupResult =
"aimedb_felica_mobile_lookup" in result
? result.aimedb_felica_mobile_lookup
: result.aimedb_felica_card_lookup;
const cardResult = result.aimedb_card;
resp.accessCode.set(Buffer.from(lookupResult.accessCode, "hex"));
if (cardResult) {
resp.accountId = cardResult.id;
resp.portalRegistered = PortalRegistration.UNREGISTERED;
// HACK: Since we cannot possibly know who made it (even AICC cards have
// the same manufacturer code `01:2e`!), we're just going to treat everything
// as a SEGA card.
resp.companyCode = CompanyCodes.SEGA;
}
return resp;
}
// Assuming that FeliCa Mobile is handled by their own registration endpoint...
if (IsSupportedFelicaMobile(req.osVer)) {
return resp;
}
// Card is not in the lookup tables, register a new card...
if (!Config.AIMEDB_CONFIG.RESERVED_CARD_KEY || !Config.AIMEDB_CONFIG.RESERVED_CARD_PREFIX) {
logger.error(
"AIMEDB_CONFIG.RESERVED_CARD_KEY or AIMEDB_CONFIG.RESERVED_CARD_PREFIX is not set in config file. Cannot generate a new access code.",
{ req }
);
header.result = ResultCodes.INVALID_AIME_ID;
return resp;
}
const mostRecentRow = await db
.select()
.from(felicaCardLookup)
.orderBy(desc(felicaCardLookup.id))
.limit(1)
.then((r) => r[0]);
const nextId = mostRecentRow ? mostRecentRow.id + 1 : 1;
const accessCode = CalculateAccessCode(
nextId,
Config.AIMEDB_CONFIG.RESERVED_CARD_PREFIX,
Config.AIMEDB_CONFIG.RESERVED_CARD_KEY
);
logger.verbose(`Created FeliCa Card access code for serial ${nextId}.`, {
nextId,
accessCode,
});
const value = { idm: req.idm.toString(16), accessCode };
const row = await db
.insert(felicaCardLookup)
.values(value)
.returning()
.then((r) => r[0]);
if (!row) {
logger.crit("Failed to insert new lookup entry into the database.", value);
header.result = ResultCodes.UNKNOWN_ERROR;
return resp;
}
header.result = ResultCodes.SUCCESS;
resp.accessCode.set(Buffer.from(row.accessCode, "hex"));
return resp;
};
export const FelicaRegisterHandler: AimeDBHandlerFn<"FelicaLookupResponse"> = async (
header,
data
) => {
header.commandId = CommandId.FELICA_LOOKUP_RESPONSE;
header.length = PacketHeaderStruct.baseSize + FelicaLookupResponseStruct.baseSize;
const req = new FelicaLookupRequestStruct(data);
const resp = new FelicaLookupResponseStruct();
if (!IsSupportedFelica(req.osVer)) {
logger.verbose("Rejecting card of unknown OS version.", {
req,
});
header.result = ResultCodes.INVALID_AIME_ID;
resp.felicaIndex = -1;
return resp;
}
const result = await db
.select()
.from(felicaMobileLookup)
.where(eq(felicaMobileLookup.idm, req.idm.toString(16)))
.limit(1)
.then((r) => r[0]);
if (result) {
header.result = ResultCodes.ID_ALREADY_REGISTERED;
resp.felicaIndex = result.id;
resp.accessCode.set(Buffer.from(result.accessCode, "hex"));
return resp;
}
if (!Config.AIMEDB_CONFIG.AIME_MOBILE_CARD_KEY) {
logger.error(
"AIMEDB_CONFIG.AIME_MOBILE_CARD_KEY is not set in config file. Cannot register FeliCa Mobile ID.",
{ req }
);
header.result = ResultCodes.INVALID_AIME_ID;
resp.felicaIndex = -1;
return resp;
}
const mostRecentRow = await db
.select()
.from(felicaMobileLookup)
.orderBy(desc(felicaMobileLookup.id))
.limit(1)
.then((r) => r[0]);
const nextId = mostRecentRow ? mostRecentRow.id + 1 : 1;
const accessCode = CalculateAccessCode(
nextId,
"01035",
Config.AIMEDB_CONFIG.AIME_MOBILE_CARD_KEY
);
logger.verbose(`Created FeliCa Mobile access code for serial ${nextId}.`, {
nextId,
accessCode,
});
const value = { idm: req.idm.toString(16), accessCode };
const row = await db
.insert(felicaMobileLookup)
.values(value)
.returning()
.then((r) => r[0]);
if (!row) {
logger.crit("Failed to insert new lookup entry into the database.", value);
header.result = ResultCodes.UNKNOWN_ERROR;
resp.felicaIndex = -1;
return resp;
}
header.result = ResultCodes.SUCCESS;
resp.felicaIndex = row.id;
resp.accessCode.set(Buffer.from(row.accessCode, "hex"));
return resp;
};