Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Implement range support for files/beatmap/audio, added new values for api v2, Fix grades for lazer and widlcard support for ALLOWED_IPS #283

Merged
merged 7 commits into from
Jan 23, 2025
4 changes: 1 addition & 3 deletions packages/common/utils/downloader.ts
Original file line number Diff line number Diff line change
Expand Up @@ -18,9 +18,7 @@ export const updateProgressBar = (
const progressBar = '█'.repeat(filledWidth) + '░'.repeat(emptyWidth);

process.stdout.write(
`${coloredText} ${title}: [${progressBar}] ${(progress * 100).toFixed(
2
)}%${message}\r`
`${coloredText} ${title}: [${progressBar}] ${(progress * 100).toFixed(2)}%${message}\r`
);

if (progress === 1) {
Expand Down
1 change: 0 additions & 1 deletion packages/server/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -121,7 +121,6 @@ export class Server {
}

wLogger.warn('[request]', 'Unallowed request', req.url, {
address: req.socket.remoteAddress,
origin: req.headers.origin,
referer: req.headers.referer
});
Expand Down
18 changes: 18 additions & 0 deletions packages/server/router/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -436,6 +436,24 @@ export default function buildBaseApi(server: Server) {
);
});

server.app.route('/favicon.ico', 'GET', (req, res) => {
fs.readFile(path.join(pkgAssetsPath, 'favicon.ico'), (err, content) => {
if (err) {
wLogger.debug(`/${'favicon.ico'}`, err);
res.writeHead(404, { 'Content-Type': 'text/html' });

res.end('<html>page not found</html>');
return;
}

res.writeHead(200, {
'Content-Type': 'image/vnd.microsoft.icon; charset=utf-8'
});

res.end(content);
});
});

server.app.route(/.*/, 'GET', (req, res) => {
const url = req.pathname || '/';
const staticPath = getStaticPath();
Expand Down
46 changes: 35 additions & 11 deletions packages/server/scripts/beatmapFile.ts
Original file line number Diff line number Diff line change
@@ -1,17 +1,15 @@
import { ServerResponse } from 'http';
import fs from 'fs';
import type { ServerResponse } from 'http';
import path from 'path';

import { directoryWalker } from '../utils/directories';
import { ExtendedIncomingMessage } from '../utils/http';
import { sendJson } from '../utils/index';
import type { ExtendedIncomingMessage } from '../utils/http';
import { getContentType, sendJson } from '../utils/index';

export function beatmapFileShortcut(
req: ExtendedIncomingMessage,
res: ServerResponse,
beatmapFileType: 'audio' | 'background' | 'file'
) {
const url = req.pathname || '/';

const osuInstance: any = req.instanceManager.getInstance(
req.instanceManager.focusedClient
);
Expand Down Expand Up @@ -40,10 +38,36 @@ export function beatmapFileShortcut(
});
}

directoryWalker({
res,
baseUrl: url,
pathname: fileName || '',
folderPath: folder
const filePath = path.join(folder, fileName);
const fileStat = fs.statSync(filePath);

if (req.headers.range) {
const range = req.headers.range.replace('bytes=', '').split('-');
const start = parseInt(range[0]);
const end = range[1] ? parseInt(range[1]) : fileStat.size - 1;

if (start >= fileStat.size || end >= fileStat.size) {
res.writeHead(416, {
'Content-Range': `bytes */${fileStat.size}`
});
return res.end();
}

res.writeHead(206, {
'Accept-Ranges': 'bytes',
'Content-Type': getContentType(fileName),
'Content-Range': `bytes ${start}-${end}/${fileStat.size}`,
'Content-Length': end - start + 1
});

fs.createReadStream(filePath, { start, end }).pipe(res);
return;
}

res.writeHead(200, {
'Content-Type': getContentType(fileName),
'Content-Length': fileStat.size
});

fs.createReadStream(filePath).pipe(res);
}
2 changes: 1 addition & 1 deletion packages/server/utils/counters.ts
Original file line number Diff line number Diff line change
Expand Up @@ -712,7 +712,7 @@ export function buildSettings(res: http.ServerResponse) {
.replace('{NAME}', 'ALLOWED_IPS')
.replace(
'{DESCRIPTION}',
`Specify IP's which allowed to change and access tosu API's`
`IP's or domain names, which allowed to change and access tosu API's.<br /><br />- Supports wildcard syntax: 192.*.60.*<br />- Compares against values printed out in «Unallowed request» message in console`
)
.replace(
'{INPUT}',
Expand Down
4 changes: 1 addition & 3 deletions packages/server/utils/directories.ts
Original file line number Diff line number Diff line change
Expand Up @@ -120,9 +120,7 @@ export function readDirectory(
const html = folders.map((r) => {
const slashAtTheEnd = getContentType(r) === '' ? '/' : '';

return `<li><a href="${
url === '/' ? '' : url
}${encodeURIComponent(r)}${slashAtTheEnd}">${r}</a></li>`;
return `<li><a href="${url === '/' ? '' : url}${encodeURIComponent(r)}${slashAtTheEnd}">${r}</a></li>`;
});

return callback(
Expand Down
29 changes: 23 additions & 6 deletions packages/server/utils/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -116,12 +116,29 @@ function isAllowedIP(url: string | undefined) {

const allowedIPs = config.allowedIPs.split(',');
try {
const parseURL = new URL(url);
return allowedIPs.some(
(r) =>
r.toLowerCase().trim() ===
parseURL.hostname.toLowerCase().trim()
);
const hostname = new URL(url).hostname.toLowerCase().trim();
return allowedIPs.some((pattern) => {
// compare IP's length and match wildcard like comparision
if (pattern.includes('*') && pattern.includes('.')) {
const patternLength = pattern.match(/\./g)?.length || 0;
const hostnameLength = hostname.match(/\./g)?.length || 0;

if (patternLength !== 3 || hostnameLength !== 3) return false;

const patternParts = pattern.split('.');
const hostnameParts = hostname.split('.');

const matches = hostnameParts.filter((r, index) => {
if (patternParts[index] === '*') return true;

return patternParts[index] === r;
});

return matches.length === 4;
}

return pattern.toLowerCase().trim() === hostname;
});
} catch (error) {
return allowedIPs.some(
(r) => r.toLowerCase().trim() === url.toLowerCase().trim()
Expand Down
2 changes: 2 additions & 0 deletions packages/tosu/src/api/types/v2.ts
Original file line number Diff line number Diff line change
Expand Up @@ -117,6 +117,8 @@ export interface Profile {
}

export interface Beatmap {
isKiai: boolean;
isBreak: boolean;
isConvert: boolean;
time: BeatmapTime;
status: NumberName;
Expand Down
1 change: 1 addition & 0 deletions packages/tosu/src/api/utils/buildResult.ts
Original file line number Diff line number Diff line change
Expand Up @@ -230,6 +230,7 @@ export const buildResult = (instanceManager: InstanceManager): ApiAnswer => {
name: resultScreen.playerName,
score: resultScreen.score,
accuracy: calculateAccuracy({
isRound: true,
hits: resultScreenHits,
mode: gameplay.mode
}),
Expand Down
6 changes: 5 additions & 1 deletion packages/tosu/src/api/utils/buildResultSC.ts
Original file line number Diff line number Diff line change
Expand Up @@ -123,7 +123,11 @@ export const buildResult = (instanceManager: InstanceManager): ApiAnswer => {
acc[v] = value <= 0 ? 0 : value;
return acc;
}, {}),
mapBreaks: beatmapPP.breaks,
mapBreaks: beatmapPP.breaks.map((r) => ({
startTime: r.start,
endTime: r.end,
hasEffect: r.hasEffect
})),
mapKiaiPoints: [], // TODO: add
mapPosition: formatMilliseconds(global.playTime), // convert to osu format
mapTimingPoints: beatmapPP.timingPoints.map((r) => ({
Expand Down
14 changes: 11 additions & 3 deletions packages/tosu/src/api/utils/buildResultV2.ts
Original file line number Diff line number Diff line change
Expand Up @@ -32,7 +32,8 @@ import { CalculateMods } from '@/utils/osuMods.types';

const convertMemoryPlayerToResult = (
memoryPlayer: MemoryLeaderboardPlayer,
gameMode: any
gameMode: any,
client: ClientType
): Leaderboard => {
const hits = {
300: memoryPlayer.h300,
Expand All @@ -53,7 +54,7 @@ const convertMemoryPlayerToResult = (
name: memoryPlayer.name,

score: memoryPlayer.score,
accuracy: calculateAccuracy({ hits, mode: gameMode }),
accuracy: calculateAccuracy({ isRound: true, hits, mode: gameMode }),

hits,

Expand All @@ -69,6 +70,7 @@ const convertMemoryPlayerToResult = (
rate: memoryPlayer.mods.rate
},
rank: calculateGrade({
isLazer: client === ClientType.lazer,
mods: memoryPlayer.mods.number,
mode: gameMode,
hits
Expand Down Expand Up @@ -228,6 +230,8 @@ export const buildResult = (instanceManager: InstanceManager): ApiAnswer => {
backgroundColour: user.backgroundColour?.toString(16)
},
beatmap: {
isKiai: beatmapPP.isKiai,
isBreak: beatmapPP.isBreak,
isConvert:
beatmapPP.mode === 0 ? beatmapPP.mode !== currentMode : false,
time: {
Expand Down Expand Up @@ -263,7 +267,11 @@ export const buildResult = (instanceManager: InstanceManager): ApiAnswer => {
},
play: buildPlay(gameplay, beatmapPP, currentMods),
leaderboard: gameplay.leaderboardScores.map((slot) =>
convertMemoryPlayerToResult(slot, Rulesets[gameplay.mode])
convertMemoryPlayerToResult(
slot,
Rulesets[gameplay.mode],
osuInstance.client
)
),
performance: {
accuracy: beatmapPP.ppAcc,
Expand Down
2 changes: 1 addition & 1 deletion packages/tosu/src/instances/lazerInstance.ts
Original file line number Diff line number Diff line change
Expand Up @@ -108,7 +108,7 @@ export class LazerInstance extends AbstractInstance {
this.previousMP3Length = menu.mp3Length;
}

beatmapPP.updateRealTimeBPM(global.playTime, currentMods.rate);
beatmapPP.updateEventsStatus(global.playTime, currentMods.rate);

switch (global.status) {
case GameState.menu:
Expand Down
2 changes: 1 addition & 1 deletion packages/tosu/src/instances/osuInstance.ts
Original file line number Diff line number Diff line change
Expand Up @@ -123,7 +123,7 @@ export class OsuInstance extends AbstractInstance {
this.previousMP3Length = menu.mp3Length;
}

beatmapPP.updateRealTimeBPM(global.playTime, currentMods.rate);
beatmapPP.updateEventsStatus(global.playTime, currentMods.rate);

switch (global.status) {
case GameState.menu:
Expand Down
56 changes: 46 additions & 10 deletions packages/tosu/src/states/beatmap.ts
Original file line number Diff line number Diff line change
@@ -1,11 +1,7 @@
import rosu from '@kotrikd/rosu-pp';
import { ClientType, config, wLogger } from '@tosu/common';
import fs from 'fs';
import {
BeatmapBreakEvent,
Beatmap as ParsedBeatmap,
TimingPoint
} from 'osu-classes';
import { Beatmap as ParsedBeatmap, TimingPoint } from 'osu-classes';
import { BeatmapDecoder } from 'osu-parsers';
import path from 'path';

Expand Down Expand Up @@ -77,7 +73,21 @@ interface BeatmapPPTimings {
full: number;
}

interface BreakPoint {
hasEffect: boolean;
start: number;
end: number;
}

interface KiaiPoint {
start: number;
end: number;
}

export class BeatmapPP extends AbstractState {
isKiai: boolean;
isBreak: boolean;

beatmap?: rosu.Beatmap;
lazerBeatmap?: ParsedBeatmap;
performanceAttributes?: rosu.PerformanceAttributes;
Expand Down Expand Up @@ -123,7 +133,8 @@ export class BeatmapPP extends AbstractState {
};

timingPoints: TimingPoint[] = [];
breaks: BeatmapBreakEvent[] = [];
breaks: BreakPoint[] = [];
kiais: KiaiPoint[] = [];

constructor(game: AbstractInstance) {
super(game);
Expand All @@ -132,6 +143,9 @@ export class BeatmapPP extends AbstractState {
}

init() {
this.isKiai = false;
this.isBreak = false;

this.strains = [];
this.strainsAll = {
series: [],
Expand Down Expand Up @@ -208,6 +222,7 @@ export class BeatmapPP extends AbstractState {
};
this.timingPoints = [];
this.breaks = [];
this.kiais = [];
}

updatePPAttributes(
Expand Down Expand Up @@ -466,7 +481,11 @@ export class BeatmapPP extends AbstractState {
this.minBPM = Math.round(bpmMin * this.clockRate);
this.maxBPM = Math.round(bpmMax * this.clockRate);

this.breaks = this.lazerBeatmap.events.breaks;
this.breaks = this.lazerBeatmap.events.breaks.map((r) => ({
hasEffect: r.hasEffect,
start: r.startTime,
end: r.endTime
}));

const firstObj = Math.round(
this.lazerBeatmap.hitObjects.at(0)?.startTime ?? 0
Expand All @@ -481,6 +500,20 @@ export class BeatmapPP extends AbstractState {
this.timingPoints =
this.lazerBeatmap.controlPoints.timingPoints;

const kiais: KiaiPoint[] = [];
const points = this.lazerBeatmap.controlPoints.effectPoints;
for (let i = 0; i < points.length; i++) {
const point = points[i];
if (point.kiai === false) {
cyperdark marked this conversation as resolved.
Show resolved Hide resolved
kiais[kiais.length - 1].end = point.startTime;
continue;
}

kiais.push({ start: point.startTime, end: -1 });
}

this.kiais = kiais;

this.resetReportCount('beatmapPP updateMapMetadataTimings');
} catch (exc) {
this.reportError(
Expand Down Expand Up @@ -799,17 +832,20 @@ export class BeatmapPP extends AbstractState {
}
}

updateRealTimeBPM(timeMS: number, multiply: number) {
updateEventsStatus(ms: number, multiply: number) {
if (!this.lazerBeatmap) return;

const bpm =
this.lazerBeatmap.controlPoints.timingPoints
// @ts-ignore
// @ts-expect-error
cyperdark marked this conversation as resolved.
Show resolved Hide resolved
.toReversed()
.find((r) => r.startTime <= timeMS && r.bpm !== 0)?.bpm ||
.find((r) => r.startTime <= ms && r.bpm !== 0)?.bpm ||
this.lazerBeatmap.controlPoints.timingPoints[0]?.bpm ||
0.0;

this.realtimeBPM = Math.round(bpm * multiply);

this.isKiai = this.kiais.some((r) => ms >= r.start && ms <= r.end);
this.isBreak = this.breaks.some((r) => ms >= r.start && ms <= r.end);
}
}
Loading
Loading