kozukata-toa/src/servers/allnet/router/sys/servlet/PowerOn/router.ts

227 lines
6.8 KiB
TypeScript

import { eq } from "drizzle-orm";
import { Router } from "express";
import { db } from "external/db/db";
import { arcade, machine } from "external/db/schemas";
import CreateLogCtx from "lib/logger/logger";
import { Config } from "lib/setup/config";
import { DateTime } from "luxon";
import {
PowerOnRequestChinaSchema,
PowerOnRequestV1Schema,
PowerOnRequestV2Schema,
PowerOnRequestV3Schema,
PowerOnRequestV5Schema,
PowerOnRequestV6Schema,
PowerOnStat,
} from "servers/allnet/types/power-on";
import { fromZodError } from "zod-validation-error";
import type {
PowerOnResponseChina,
PowerOnResponseV1,
PowerOnResponseV2,
PowerOnResponseV3,
PowerOnResponseV5,
} from "servers/allnet/types/power-on";
const logger = CreateLogCtx(__filename);
const router: Router = Router({ mergeParams: true });
router.post("/", async (req, res) => {
if (!req.ip) {
logger.error("Request does not have an IP associated with it?");
return res.status(500).send();
}
const formatVer = req.safeBody.format_ver ?? "1";
let parseResult;
// TODO: ALL.Net China
if (formatVer === "1") {
parseResult = PowerOnRequestV1Schema.safeParse(req.safeBody);
} else if (formatVer === "2") {
parseResult = PowerOnRequestV2Schema.safeParse(req.safeBody);
} else if (formatVer === "3") {
parseResult =
"game_ver" in req.safeBody
? PowerOnRequestChinaSchema.safeParse(req.safeBody)
: PowerOnRequestV3Schema.safeParse(req.safeBody);
} else if (formatVer === "5") {
parseResult = PowerOnRequestV5Schema.safeParse(req.safeBody);
} else if (formatVer === "6") {
parseResult = PowerOnRequestV6Schema.safeParse(req.safeBody);
} else {
logger.error("Received PowerOn request of unknown version.", { formatVer });
return res.status(400).send("");
}
if (!parseResult.success) {
logger.error(`Received invalid PowerOn request: ${fromZodError(parseResult.error)}`, {
formatVer,
});
return res.status(400).send("");
}
const request = parseResult.data;
const gameId = "game_id" in request ? request.game_id : request.title_id;
const serial = "serial" in request ? request.serial : request.machine;
const titleVer =
// eslint-disable-next-line no-nested-ternary
"ver" in request
? request.ver
: "game_ver" in request
? request.game_ver
: request.title_ver;
// We reject non-DFI requests late, so we can find the offending
// "machine".
if (req.headers.pragma?.toUpperCase() !== "DFI") {
logger.error("Received non-DFI-encoded request.", {
gameId,
serial,
ip: req.ip,
});
return res.status(400).send("");
}
if (titleVer.length === 0) {
logger.error("Received PowerOn request with empty title version.", {
formatVer,
titleVer,
});
return res.status(400).send("");
}
// TODO: Implement store authentication + fetch arcade information
// Reference implementation: https://sega.bsnk.me/allnet/auth/power-on
// For now, we just check if there's a cab registered in the database.
const cabAndStore = await db
.select()
.from(machine)
.leftJoin(arcade, eq(arcade.id, machine.arcade_id))
.where(eq(machine.serial, serial))
.then((r) => r[0]);
if (!cabAndStore && !Config.ALLNET_CONFIG.ALLOW_UNREGISTERED_SERIALS) {
logger.error("Unregistered serial attempted ALL.Net authentication.", {
gameId,
serial,
ip: req.ip,
});
return res.status(400).send({ stat: PowerOnStat.BOARD_ERROR });
}
// TODO: Verify that title exists and is enabled.
const serverTime = DateTime.now().setZone("Asia/Tokyo");
const baseResponse = {
// Same thing
stat: 1,
place_id: (0x123).toString(16),
// uri: `http://localhost:8080/${gameId}/${titleVer.replace(/\./u, "")}/`,
uri: `http://localhost:8080/${titleVer.replace(/\./u, "")}/`,
host: "localhost:8080",
name: cabAndStore?.allnet_arcade?.name ?? Config.NAME,
nickname: cabAndStore?.allnet_arcade?.nickname ?? "kozukata-toa",
setting: 1,
region0: cabAndStore?.allnet_arcade?.regionId ?? 1,
region_name0: cabAndStore?.allnet_arcade?.regionName0 ?? "W",
region_name1: cabAndStore?.allnet_arcade?.regionName1 ?? "",
region_name2: cabAndStore?.allnet_arcade?.regionName2 ?? "",
region_name3: cabAndStore?.allnet_arcade?.regionName3 ?? "",
};
let response;
if (request.format_ver === "1") {
response = {
...baseResponse,
year: serverTime.year,
month: serverTime.month,
day: serverTime.day,
hour: serverTime.hour,
minute: serverTime.minute,
second: serverTime.second,
} satisfies PowerOnResponseV1;
} else if (request.format_ver === "2") {
response = {
...baseResponse,
year: serverTime.year,
month: serverTime.month,
day: serverTime.day,
hour: serverTime.hour,
minute: serverTime.minute,
second: serverTime.second,
country: "JPN",
timezone: "+09:00",
res_class: "PowerOnResponseVer2",
} satisfies PowerOnResponseV2;
} else if (request.format_ver === "3" && "ver" in request) {
response = {
...baseResponse,
res_ver: 3,
allnet_id: 456,
utc_time: serverTime.toFormat("yyyy-MM-dd'T'HH:mm:ss'Z'"),
country: "JPN",
client_timezone: "+0900",
token: request.token ?? "null",
} satisfies PowerOnResponseV3;
} else if (request.format_ver === "3" && "game_ver" in request) {
response = {
result: baseResponse.stat,
place_id: baseResponse.place_id,
uri1: baseResponse.uri,
uri2: baseResponse.uri,
name: baseResponse.name,
nickname: baseResponse.nickname,
setting: baseResponse.setting,
region0: baseResponse.region0,
region_name0: baseResponse.region_name0,
region_name1: baseResponse.region_name1,
region_name2: baseResponse.region_name2,
region_name3: baseResponse.region_name3,
country: "CHN",
client_timezone: "+800",
token: request.token ?? "null",
res_ver: 3,
allnet_id: 456,
utc_time: serverTime.toFormat("yyyy-MM-dd'T'HH:mm:ss'Z'"),
} satisfies PowerOnResponseChina;
} else if (request.format_ver === "5") {
response = {
result: baseResponse.stat,
place_id: baseResponse.place_id,
title_uri: baseResponse.uri,
title_host: baseResponse.host,
name: baseResponse.name,
nickname: baseResponse.nickname,
setting: baseResponse.setting,
region0: baseResponse.region0,
region_name0: baseResponse.region_name0,
region_name1: baseResponse.region_name1,
region_name2: baseResponse.region_name2,
region_name3: baseResponse.region_name3,
res_ver: 5,
allnet_id: 456,
utc_time: serverTime.toFormat("yyyy-MM-dd'T'HH:mm:ss'Z'"),
country: "JPN",
client_timezone: "+0900",
token: request.token ?? "null",
} satisfies PowerOnResponseV5;
} else {
logger.error("Received auth20 PowerOn request, which is currently unsupported.", {
gameId,
serial,
ip: req.ip,
});
return res.status(403).send({ stat: 0 });
}
logger.info("Authenticated machine with ALL.Net.", { serial, gameId, ip: req.ip });
return res.status(200).send(response);
});
export default router;