223 lines
6.5 KiB
TypeScript
223 lines
6.5 KiB
TypeScript
import { Router } from "express";
|
|
import { Machine } from "external/db/entity/allnet";
|
|
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 as string | undefined) ?? "1";
|
|
|
|
let parseResult;
|
|
|
|
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.`, {
|
|
err: fromZodError(parseResult.error).message,
|
|
formatVer,
|
|
body: req.safeBody,
|
|
});
|
|
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 result = await Machine.findOne({
|
|
where: {
|
|
serial,
|
|
},
|
|
});
|
|
|
|
if (!result && !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 = {
|
|
stat: 1,
|
|
place_id: (result?.arcade.id ?? 0x123).toString(16).toUpperCase(),
|
|
// uri: `http://localhost:8080/${gameId}/${titleVer.replace(/\./u, "")}/`,
|
|
uri: `http://localhost:8080/${titleVer.replace(/\./u, "")}/`,
|
|
host: "localhost:8080",
|
|
name: result?.arcade.name ?? Config.NAME,
|
|
nickname: result?.arcade.nickname ?? "kozukatatoa",
|
|
setting: 1,
|
|
region0: result?.arcade.regionId ?? 1,
|
|
region_name0: result?.arcade.regionName0 ?? "W",
|
|
region_name1: result?.arcade.regionName1 ?? "",
|
|
region_name2: result?.arcade.regionName2 ?? "",
|
|
region_name3: result?.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;
|