bug-fix(spr): Address PR issues

This commit is contained in:
Daniel López Guimaraes 2024-05-01 19:00:13 +01:00
parent 5f83d7a655
commit c74abb7ca3
No known key found for this signature in database
GPG Key ID: 6AC74DE3DEF050E0
15 changed files with 133 additions and 76 deletions

View File

@ -1,9 +1,11 @@
import mongoose from 'mongoose';
import { CECData } from '@/models/cec-data';
import { CECSlot } from '@/models/cec-slot';
import { Task } from '@/models/task';
import { File } from '@/models/file';
import { config } from '@/config-manager';
import { HydratedCECDataDocument, ICECData } from '@/types/mongoose/cec-data';
import { HydratedCECDataDocument } from '@/types/mongoose/cec-data';
import { HydratedCECSlotDocument, ICECSlot } from '@/types/mongoose/cec-slot';
import { HydratedTaskDocument, ITask } from '@/types/mongoose/task';
import { HydratedFileDocument, IFile } from '@/types/mongoose/file';
@ -163,15 +165,22 @@ export function getDuplicateCECData(pid: number, gameID: number): Promise<Hydrat
export async function getRandomCECData(pids: number[], gameID: number): Promise<HydratedCECDataDocument | null> {
verifyConnected();
const filter: mongoose.FilterQuery<ICECData> = {
// * We search through the CECSlot so that everyone has the same chance of getting their data picked up
const filter: mongoose.FilterQuery<ICECSlot> = {
creator_pid: {
$in: pids,
},
game_id: gameID
};
const count = await CECData.countDocuments(filter);
const count = await CECSlot.countDocuments(filter);
const rand = Math.floor(Math.random() * count);
return CECData.findOne<HydratedCECDataDocument>(filter).skip(rand);
const cecSlot = await CECSlot.findOne<HydratedCECSlotDocument>(filter).skip(rand);
if (cecSlot) {
return CECData.findById(cecSlot.latest_data_id);
}
return null;
}

View File

@ -1,10 +1,10 @@
import express from 'express';
import { getUserDataByPID } from '@/util';
import { getNEXDataByPID } from '@/util';
export default async function authenticationMiddleware(request: express.Request, response: express.Response, next: express.NextFunction): Promise<void> {
if (request.pid) {
// TODO - Handle 3DS NEX accounts
request.pnid = await getUserDataByPID(request.pid);
// TODO - Get users PNIDs
request.nexAccount = await getNEXDataByPID(request.pid);
}
return next();

View File

@ -1,6 +1,6 @@
import express from 'express';
// import RequestException from '@/request-exception';
import type { UserAgentInfo } from '@/types/common/user-agent-info';
import { CTRSystemModel, UserAgentInfo } from '@/types/common/user-agent-info';
const FIRMWARE_PATCH_REGION_WIIU_REGEX = /(\d)([JEU])/;
@ -103,7 +103,7 @@ function parse3DS(userAgent: string): UserAgentInfo | null {
firmwareMinor !== '17' ||
!['0-50J', '0-50U', '0-50E'].includes(firmwarePatchAndRegion) || // TODO - Make this more dynamic?
ctrSdkVersion !== '62452' ||
!consoleModel || // TODO - Actually check this
!Object.values(CTRSystemModel).includes(parseInt(consoleModel)) ||
localFriendCodeSeedHex.length !== 16 ||
friendCodeHex.length !== 16
) {
@ -113,6 +113,7 @@ function parse3DS(userAgent: string): UserAgentInfo | null {
let localFriendCodeSeed: bigint;
let friendCode: bigint;
// TODO - Validate the LFCS, we currently don't store the value on the account server
try {
localFriendCodeSeed = BigInt('0x' + localFriendCodeSeedHex); // * Parse hex string to bigint
} catch {

View File

@ -5,9 +5,9 @@ const CECDataSchema = new mongoose.Schema<ICECData, CECDataModel, ICECDataMethod
creator_pid: Number,
game_id: Number,
data: String,
data_hash: String,
size: Number,
created: BigInt,
updated: BigInt
}, { id: false });
created: BigInt
});
export const CECData: CECDataModel = mongoose.model<ICECData, CECDataModel>('CECData', CECDataSchema);
export const CECData = mongoose.model<ICECData, CECDataModel>('CECData', CECDataSchema);

10
src/models/cec-slot.ts Normal file
View File

@ -0,0 +1,10 @@
import mongoose from 'mongoose';
import { ICECSlot, ICECSlotMethods, CECSlotModel } from '@/types/mongoose/cec-slot';
const CECSlotSchema = new mongoose.Schema<ICECSlot, CECSlotModel, ICECSlotMethods>({
creator_pid: Number,
game_id: Number,
latest_data_id: String
});
export const CECSlot = mongoose.model<ICECSlot, CECSlotModel>('CECSlot', CECSlotSchema);

View File

@ -32,4 +32,4 @@ FileSchema.plugin(AutoIncrementID, {
field: 'data_id'
});
export const File: FileModel = mongoose.model<IFile, FileModel>('File', FileSchema);
export const File = mongoose.model<IFile, FileModel>('File', FileSchema);

View File

@ -20,4 +20,4 @@ const TaskSchema = new mongoose.Schema<ITask, TaskModel, ITaskMethods>({
updated: BigInt
}, { id: false });
export const Task: TaskModel = mongoose.model<ITask, TaskModel>('Task', TaskSchema);
export const Task = mongoose.model<ITask, TaskModel>('Task', TaskSchema);

View File

@ -9,7 +9,7 @@ import { LOG_INFO, LOG_SUCCESS } from '@/logger';
import { config } from '@/config-manager';
import parseUserAgentMiddleware from '@/middleware/parse-user-agent';
// import authenticationMiddleware from '@/middleware/authentication';
import authenticationMiddleware from '@/middleware/authentication';
import nppl from '@/services/nppl';
import npts from '@/services/npts';
@ -28,7 +28,7 @@ app.use(express.urlencoded({
}));
app.use(parseUserAgentMiddleware);
// app.use(authenticationMiddleware);
app.use(authenticationMiddleware);
app.use(nppl);
app.use(npts);

View File

@ -1,9 +1,11 @@
import crypto from 'node:crypto';
import express from 'express';
import subdomain from 'express-subdomain';
import Dicer from 'dicer';
import { getDuplicateCECData, getRandomCECData } from '@/database';
import { getNEXDataByPID, getFriends } from '@/util';
import { getFriends } from '@/util';
import { CECData } from '@/models/cec-data';
import { CECSlot } from '@/models/cec-slot';
import { SendMode, SPRSlot } from '@/types/common/spr-slot';
const spr = express.Router();
@ -41,11 +43,11 @@ function multipartParser(request: express.Request, response: express.Response, n
});
part.on('data', (data: Buffer | string) => {
if (data instanceof String) {
if (typeof data === 'string') {
data = Buffer.from(data);
}
fileBuffer = Buffer.concat([fileBuffer, data as Buffer]);
fileBuffer = Buffer.concat([fileBuffer, data]);
});
part.on('end', () => {
@ -67,34 +69,30 @@ spr.post('/relay/0', multipartParser, async (request, response) => {
return;
}
if (!request.pid) {
if (!request.pid || !request.nexAccount) {
response.sendStatus(400);
return;
}
// * Check that the account is a 3DS and isn't banned
const accountData = await getNEXDataByPID(request.pid);
if (!accountData) {
response.sendStatus(400);
return;
}
if (!accountData.friendCode || accountData.accessLevel < 0) {
if (!request.nexAccount.friendCode || request.nexAccount.accessLevel < 0) {
response.sendStatus(400);
return;
}
const sprMetadataBuffer: Buffer | undefined = request.files['spr-meta'];
if (typeof sprMetadataBuffer === 'undefined') {
if (!sprMetadataBuffer) {
response.sendStatus(400);
return;
}
let sprSlots: SPRSlot[] = [];
const sprSlots: SPRSlot[] = [];
// * Check spr-meta metadata headers
const sprMetadata = sprMetadataBuffer.toString();
const metadataHeaders = sprMetadata.split('\r\n'); // * Split header lines
if (metadataHeaders.length < 1) {
response.sendStatus(400);
return;
@ -102,8 +100,8 @@ spr.post('/relay/0', multipartParser, async (request, response) => {
for (let i = 0; i < metadataHeaders.length; i++) {
const metadataHeader = metadataHeaders[i];
const field = metadataHeader.split(': '); // * Split header and value
if (field.length !== 2) {
const [header, value] = metadataHeader.split(': '); // * Split header and value
if (!header || !value) {
response.sendStatus(400);
return;
}
@ -111,7 +109,7 @@ spr.post('/relay/0', multipartParser, async (request, response) => {
// * Since the headers will always use the same pattern (first the slotsize, then the metadata for each slot),
// * we can guarantee that i must match with the slot we are looking at except for 0, which will be the slotsize
if (i === 0) {
if (field[0] !== 'slotsize') {
if (header !== 'slotsize') {
response.sendStatus(400);
return;
}
@ -119,7 +117,7 @@ spr.post('/relay/0', multipartParser, async (request, response) => {
// * Validate slotsize
let slotsize: number;
try {
slotsize = parseInt(field[1]);
slotsize = parseInt(value);
} catch {
response.sendStatus(400);
return;
@ -134,7 +132,8 @@ spr.post('/relay/0', multipartParser, async (request, response) => {
continue;
}
const metadata = field[1].split(','); // * Split the value to get the metadata
const metadata = value.split(','); // * Split the value to get the metadata
if (metadata.length !== 3) {
response.sendStatus(400);
return;
@ -156,7 +155,8 @@ spr.post('/relay/0', multipartParser, async (request, response) => {
if (size > 0 && sendMode !== SendMode.RecvOnly) {
const slot = i.toString().padStart(2, '0');
const slotData: Buffer | undefined = request.files['spr-slot' + slot];
if (typeof slotData === 'undefined') {
if (!slotData) {
response.sendStatus(400);
return;
}
@ -180,7 +180,7 @@ spr.post('/relay/0', multipartParser, async (request, response) => {
return;
}
if (slotData.readUInt32LE(0) !== 0x6161) {
if (slotData.readUInt32LE() !== 0x6161) {
response.sendStatus(400);
return;
}
@ -198,7 +198,7 @@ spr.post('/relay/0', multipartParser, async (request, response) => {
data = slotData;
}
sprSlots = sprSlots.concat({
sprSlots.push({
sendMode,
gameID,
size,
@ -209,50 +209,62 @@ spr.post('/relay/0', multipartParser, async (request, response) => {
const userFriends = await getFriends(request.pid);
let sprData: Buffer = Buffer.alloc(0);
for (let i = 0; i < sprSlots.length; i++) {
const sprSlot = sprSlots[i];
const slot = String(i + 1).padStart(2, '0');
try {
for (let i = 0; i < sprSlots.length; i++) {
const sprSlot = sprSlots[i];
const slot = String(i + 1).padStart(2, '0');
// * Upload slot data
if (sprSlot.size > 0 && sprSlot.sendMode !== SendMode.RecvOnly) {
const slotData = await getDuplicateCECData(request.pid, sprSlot.gameID);
if (slotData) {
slotData.data = sprSlot.data.toString('base64');
slotData.size = sprSlot.size;
slotData.updated = BigInt(Date.now());
await slotData.save();
} else {
await CECData.create({
creator_pid: request.pid,
game_id: sprSlot.gameID,
data: sprSlot.data.toString('base64'),
size: sprSlot.size,
created: BigInt(Date.now()),
updated: BigInt(Date.now())
});
// * Upload slot data
if (sprSlot.size > 0 && sprSlot.sendMode !== SendMode.RecvOnly) {
const dataHash = crypto.createHash('sha256').update(sprSlot.data).digest('base64');
let slotData = await getDuplicateCECData(request.pid, sprSlot.gameID);
if (!slotData || slotData.data_hash !== dataHash) {
slotData = await CECData.create({
creator_pid: request.pid,
game_id: sprSlot.gameID,
data: sprSlot.data.toString('base64'),
data_hash: dataHash,
size: sprSlot.size,
created: BigInt(Date.now())
});
}
if (slotData.id) {
await CECSlot.findOneAndUpdate({
creator_pid: request.pid,
game_id: slotData.game_id
}, {latest_data_id: slotData.id}, {upsert: true});
}
}
}
if (!userFriends || userFriends.pids.length === 0) {
continue; // * Nothing to receive
}
if (!userFriends || userFriends.pids.length === 0) {
continue; // * Nothing to receive
}
// * Receive slot data
if (sprSlot.sendMode !== SendMode.SendOnly) {
const slotData = await getRandomCECData(userFriends.pids, sprSlot.gameID);
if (slotData) {
sprData = Buffer.concat([sprData, Buffer.from(slotData.data, 'base64')]);
sprSlot.size = slotData.size;
// * Receive slot data
if (sprSlot.sendMode !== SendMode.SendOnly) {
const slotData = await getRandomCECData(userFriends.pids, sprSlot.gameID);
if (slotData) {
sprData = Buffer.concat([sprData, Buffer.from(slotData.data, 'base64')]);
sprSlot.size = slotData.size;
} else {
sprSlot.size = 0;
}
} else {
sprSlot.size = 0;
}
} else {
sprSlot.size = 0;
}
response.setHeader(`X-Spr-Slot${slot}-Result`, `${sprSlot.gameID.toString(16).toUpperCase().padStart(8, '0')},${sprSlot.sendMode},${sprSlot.size}`);
response.setHeader(`X-Spr-Slot${slot}-Result`, `${sprSlot.gameID.toString(16).toUpperCase().padStart(8, '0')},${sprSlot.sendMode},${sprSlot.size}`);
}
} catch (error) {
console.log(error);
response.sendStatus(400);
return;
}
response.send(sprData);
});

View File

@ -3,7 +3,7 @@ export enum SendMode {
Exchange,
RecvOnly,
SendOnly,
SendRecv,
SendRecv
}
export type SPRSlot = {

View File

@ -1,3 +1,12 @@
export enum CTRSystemModel {
CTR, // * Nintendo 3DS
SPR, // * Nintendo 3DS XL
KTR, // * New Nintendo 3DS
FTR, // * Nintendo 2DS
RED, // * New Nintendo 3DS XL
JAN // * New Nintendo 2DS XL
}
export type UserAgentInfo = {
deviceID?: number;
localFriendCodeSeed?: bigint;

View File

@ -1,11 +1,11 @@
import { GetUserDataResponse } from '@pretendonetwork/grpc/account/get_user_data_rpc';
import { GetNEXDataResponse } from '@pretendonetwork/grpc/account/get_nex_data_rpc';
declare global {
namespace Express {
interface Request {
files?: Record<string, any>;
pid: number;
pnid: GetUserDataResponse | null;
nexAccount: GetNEXDataResponse | null;
}
}
}

View File

@ -4,9 +4,9 @@ export interface ICECData {
creator_pid: number;
game_id: number;
data: string;
data_hash: string;
size: number;
created: bigint;
updated: bigint;
}
export interface ICECDataMethods {}

View File

@ -0,0 +1,15 @@
import { Model, HydratedDocument } from 'mongoose';
export interface ICECSlot {
creator_pid: number;
game_id: number;
latest_data_id: string;
}
export interface ICECSlotMethods {}
interface ICECSlotQueryHelpers {}
export interface CECSlotModel extends Model<ICECSlot, ICECSlotQueryHelpers, ICECSlotMethods> {}
export type HydratedCECSlotDocument = HydratedDocument<ICECSlot, ICECSlotMethods>

View File

@ -27,6 +27,7 @@ if (!disabledFeatures.s3) {
const gRPCAccountChannel = createChannel(`${config.grpc.account.address}:${config.grpc.account.port}`);
const gRPCAccountClient: AccountClient = createClient(AccountDefinition, gRPCAccountChannel);
const gRPCFriendsChannel = createChannel(`${config.grpc.friends.address}:${config.grpc.friends.port}`);
const gRPCFriendsClient: FriendsClient = createClient(FriendsDefinition, gRPCFriendsChannel);