mirror of
https://github.com/PretendoNetwork/BOSS.git
synced 2026-04-21 06:27:43 -05:00
bug-fix(spr): Address PR issues
This commit is contained in:
parent
5f83d7a655
commit
c74abb7ca3
|
|
@ -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;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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();
|
||||
|
|
|
|||
|
|
@ -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 {
|
||||
|
|
|
|||
|
|
@ -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
10
src/models/cec-slot.ts
Normal 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);
|
||||
|
|
@ -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);
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
});
|
||||
|
||||
|
|
|
|||
|
|
@ -3,7 +3,7 @@ export enum SendMode {
|
|||
Exchange,
|
||||
RecvOnly,
|
||||
SendOnly,
|
||||
SendRecv,
|
||||
SendRecv
|
||||
}
|
||||
|
||||
export type SPRSlot = {
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
|
|
|
|||
4
src/types/express.d.ts
vendored
4
src/types/express.d.ts
vendored
|
|
@ -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;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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 {}
|
||||
|
|
|
|||
15
src/types/mongoose/cec-slot.ts
Normal file
15
src/types/mongoose/cec-slot.ts
Normal 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>
|
||||
|
|
@ -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);
|
||||
|
||||
|
|
|
|||
Loading…
Reference in New Issue
Block a user