merge: branch main into dev
This commit is contained in:
commit
253742f681
@ -1,5 +1,6 @@
|
|||||||
import { describe, it, expect } from 'vitest';
|
import { describe, it, expect } from 'vitest';
|
||||||
import formatDuration from '$lib/formatDuration';
|
import formatDuration from '$lib/formatDuration';
|
||||||
|
import { safePath } from '$lib/server/safePath';
|
||||||
|
|
||||||
describe('formatDuration test', () => {
|
describe('formatDuration test', () => {
|
||||||
it('converts 120 seconds to "2:00"', () => {
|
it('converts 120 seconds to "2:00"', () => {
|
||||||
@ -22,3 +23,25 @@ describe('formatDuration test', () => {
|
|||||||
expect(formatDuration(3601)).toBe('1:00:01');
|
expect(formatDuration(3601)).toBe('1:00:01');
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
describe('safePath test', () => {
|
||||||
|
const base = "data/subdir";
|
||||||
|
it('rejects empty string', () => {
|
||||||
|
expect(safePath('', { base })).toBe(null);
|
||||||
|
});
|
||||||
|
it('accepts a regular path', () => {
|
||||||
|
expect(safePath('subsubdir/file.txt', { base })).toBe('data/subdir/subsubdir/file.txt');
|
||||||
|
});
|
||||||
|
it('rejects path with ..', () => {
|
||||||
|
expect(safePath('../file.txt', { base })).toBe(null);
|
||||||
|
});
|
||||||
|
it('accepts path with .', () => {
|
||||||
|
expect(safePath('./file.txt', { base })).toBe('data/subdir/file.txt');
|
||||||
|
});
|
||||||
|
it('accepts path traversal within base', () => {
|
||||||
|
expect(safePath('subsubdir/../file.txt', { base })).toBe('data/subdir/file.txt');
|
||||||
|
});
|
||||||
|
it('rejects path with subdir if noSubDir is true', () => {
|
||||||
|
expect(safePath('subsubdir/file.txt', { base, noSubDir: true })).toBe(null);
|
||||||
|
});
|
||||||
|
});
|
@ -1,6 +1,7 @@
|
|||||||
import fs from 'fs';
|
import fs from 'fs';
|
||||||
import { globalMemoryStorage, songData, songNameCache } from '$lib/server/cache.js';
|
import { globalMemoryStorage, songData, songNameCache } from '$lib/server/cache.js';
|
||||||
import { getDirectoryHash } from '../dirHash';
|
import { getDirectoryHash } from '../dirHash';
|
||||||
|
import { safePath } from '../safePath';
|
||||||
|
|
||||||
const dataPath = './data/song/';
|
const dataPath = './data/song/';
|
||||||
|
|
||||||
@ -25,7 +26,12 @@ export async function loadData() {
|
|||||||
songNameCache.flushAll();
|
songNameCache.flushAll();
|
||||||
for (const songID of songList) {
|
for (const songID of songList) {
|
||||||
try {
|
try {
|
||||||
const fileContentString = fs.readFileSync(dataPath + songID + '.json').toString();
|
const normPath = safePath(songID + '.json', { base: dataPath });
|
||||||
|
if (!normPath) {
|
||||||
|
console.error(`[load-song-data] Invalid path for song ID ${songID}`);
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
const fileContentString = fs.readFileSync(normPath).toString();
|
||||||
const data = JSON.parse(fileContentString);
|
const data = JSON.parse(fileContentString);
|
||||||
songData.set(songID, data);
|
songData.set(songID, data);
|
||||||
const metadata: MusicMetadata = data;
|
const metadata: MusicMetadata = data;
|
||||||
|
@ -12,7 +12,7 @@ export function getDirectoryHash(dir: string): string {
|
|||||||
|
|
||||||
files.forEach(file => {
|
files.forEach(file => {
|
||||||
const filePath = path.join(currentDir, file);
|
const filePath = path.join(currentDir, file);
|
||||||
const stats = fs.statSync(filePath);
|
const stats = fs.lstatSync(filePath);
|
||||||
|
|
||||||
if (stats.isDirectory()) {
|
if (stats.isDirectory()) {
|
||||||
traverseDirectory(filePath);
|
traverseDirectory(filePath);
|
||||||
@ -30,7 +30,7 @@ export function getDirectoryHash(dir: string): string {
|
|||||||
|
|
||||||
// Create hash from file details
|
// Create hash from file details
|
||||||
const hash = crypto.createHash('sha256');
|
const hash = crypto.createHash('sha256');
|
||||||
hash.update(fileDetails.join('|'));
|
hash.update(fileDetails.join('\x00'));
|
||||||
|
|
||||||
return hash.digest('hex');
|
return hash.digest('hex');
|
||||||
}
|
}
|
22
src/lib/server/safePath.ts
Normal file
22
src/lib/server/safePath.ts
Normal file
@ -0,0 +1,22 @@
|
|||||||
|
import path from 'path';
|
||||||
|
|
||||||
|
export function safePath(pathIn: string, options: { base: string, noSubDir?: boolean }): string | null {
|
||||||
|
const base = options.base.endsWith('/') ? options.base : options.base + '/';
|
||||||
|
if (!pathIn.startsWith("./")) {
|
||||||
|
pathIn = "./" + pathIn;
|
||||||
|
}
|
||||||
|
pathIn = path.join(base, pathIn);
|
||||||
|
const normBase = path.normalize(base);
|
||||||
|
const normPath = path.normalize(pathIn);
|
||||||
|
|
||||||
|
if (normPath !== normBase && normPath.startsWith(normBase)) {
|
||||||
|
if (options.noSubDir) {
|
||||||
|
let rel = path.relative(normBase, normPath);
|
||||||
|
if (rel.indexOf(path.sep) !== -1) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return normPath;
|
||||||
|
}
|
||||||
|
return null;
|
||||||
|
}
|
@ -1,31 +1,43 @@
|
|||||||
|
import { safePath } from '$lib/server/safePath';
|
||||||
import { getCurrentFormattedDateTime } from '$lib/songUpdateTime';
|
import { getCurrentFormattedDateTime } from '$lib/songUpdateTime';
|
||||||
import { json, error } from '@sveltejs/kit';
|
import { json, error } from '@sveltejs/kit';
|
||||||
import fs from 'fs';
|
import fs from 'fs';
|
||||||
import type { RequestHandler } from './$types';
|
import type { RequestHandler } from './$types';
|
||||||
|
|
||||||
export const GET: RequestHandler = async ({ params }) => {
|
export const GET: RequestHandler = async ({ params }) => {
|
||||||
const filePath = `./data/song/${params.id}.json`;
|
const filePath = safePath(`${params.id}.json`, { base: './data/song' });
|
||||||
if (!fs.existsSync(filePath)) {
|
if (!filePath) {
|
||||||
|
return error(404, {
|
||||||
|
message: "No correspoding song."
|
||||||
|
});
|
||||||
|
}
|
||||||
|
let data;
|
||||||
|
try { data = fs.readFileSync(filePath); } catch (e) {
|
||||||
return error(404, {
|
return error(404, {
|
||||||
message: "No corresponding song."
|
message: "No corresponding song."
|
||||||
})
|
});
|
||||||
}
|
}
|
||||||
const data = fs.readFileSync(filePath);
|
return json(JSON.parse(data.toString()));
|
||||||
return json(JSON.parse(data.toString()));
|
|
||||||
}
|
}
|
||||||
|
|
||||||
export const POST: RequestHandler = async ({ request, params }) => {
|
export const POST: RequestHandler = async ({ request, params }) => {
|
||||||
const timeStamp = new Date().getTime();
|
const timeStamp = new Date().getTime();
|
||||||
if (!fs.existsSync("./data/pending/")) {
|
try {
|
||||||
fs.mkdirSync("./data/pending");
|
if (!fs.existsSync("./data/pending/")) {
|
||||||
|
fs.mkdirSync("./data/pending", { mode: 0o755 });
|
||||||
|
}
|
||||||
|
const filePath = `./data/pending/${params.id}-${timeStamp}.json`;
|
||||||
|
const data: MusicMetadata = await request.json();
|
||||||
|
data.updateTime = getCurrentFormattedDateTime();
|
||||||
|
fs.writeFileSync(filePath, JSON.stringify(data, null, 4), { mode: 0o644 });
|
||||||
|
return json({
|
||||||
|
"message": "successfully created"
|
||||||
|
}, {
|
||||||
|
status: 201
|
||||||
|
});
|
||||||
|
} catch (e) {
|
||||||
|
return error(500, {
|
||||||
|
message: "Internal server error."
|
||||||
|
});
|
||||||
}
|
}
|
||||||
const filePath = `./data/pending/${params.id}-${timeStamp}.json`;
|
|
||||||
const data: MusicMetadata = await request.json();
|
|
||||||
data.updateTime = getCurrentFormattedDateTime();
|
|
||||||
fs.writeFileSync(filePath, JSON.stringify(data, null, 4));
|
|
||||||
return json({
|
|
||||||
"message": "successfully created"
|
|
||||||
}, {
|
|
||||||
status: 201
|
|
||||||
});
|
|
||||||
}
|
}
|
@ -1,22 +1,21 @@
|
|||||||
import type { PageServerLoad } from './$types';
|
|
||||||
import fs from 'fs';
|
import fs from 'fs';
|
||||||
|
import type { PageServerLoad } from './$types';
|
||||||
|
import { safePath } from '$lib/server/safePath';
|
||||||
|
|
||||||
export const load: PageServerLoad = ({ params }) => {
|
export const load: PageServerLoad = ({ params }) => {
|
||||||
const filePath = `./data/song/${params.id}.json`;
|
const filePath = safePath(`${params.id}.json`, { base: './data/song' });
|
||||||
if (!fs.existsSync(filePath)) {
|
if (!filePath) {
|
||||||
return {
|
return {
|
||||||
songData: null
|
songData: null
|
||||||
}
|
};
|
||||||
}
|
}
|
||||||
const dataBuffer = fs.readFileSync(filePath);
|
|
||||||
try {
|
try {
|
||||||
|
const dataBuffer = fs.readFileSync(filePath);
|
||||||
const data = JSON.parse(dataBuffer.toString());
|
const data = JSON.parse(dataBuffer.toString());
|
||||||
return {
|
return {
|
||||||
songData: data
|
songData: data
|
||||||
};
|
};
|
||||||
}
|
} catch {
|
||||||
catch {
|
|
||||||
return {
|
return {
|
||||||
songData: null
|
songData: null
|
||||||
}
|
}
|
||||||
|
Loading…
Reference in New Issue
Block a user