diff --git a/src/app.ts b/src/app.ts index 71a85bd..e02953c 100644 --- a/src/app.ts +++ b/src/app.ts @@ -32,6 +32,7 @@ import {endpoint as getSegmentInfo} from './routes/getSegmentInfo'; import {postClearCache} from './routes/postClearCache'; import { addUnlistedVideo } from './routes/addUnlistedVideo'; import {postPurgeAllSegments} from './routes/postPurgeAllSegments'; +import {getUserID} from './routes/getUserID'; export function createServer(callback: () => void) { // Create a service (the app object is just a callback). @@ -146,6 +147,9 @@ function setupRoutes(app: Express) { app.post('/api/purgeAllSegments', postPurgeAllSegments); app.post('/api/unlistedVideo', addUnlistedVideo); + + // get userID from username + app.get('/api/userID', getUserID); if (config.postgres) { app.get('/database', (req, res) => dumpDatabase(req, res, true)); diff --git a/src/routes/getUserID.ts b/src/routes/getUserID.ts new file mode 100644 index 0000000..6ecc1b7 --- /dev/null +++ b/src/routes/getUserID.ts @@ -0,0 +1,55 @@ +import {db} from '../databases/databases'; +import {Request, Response} from 'express'; +import {UserID} from '../types/user.model'; + +function getFuzzyUserID(userName: String): Promise<{userName: String, userID: UserID }[]> { + // escape [_ % \] to avoid ReDOS + userName = userName.replace(/\\/g, '\\\\') + .replace(/_/g, '\\_') + .replace(/%/g, '\\%'); + userName = `%${userName}%`; // add wildcard to username + // LIMIT to reduce overhead | ESCAPE to escape LIKE wildcards + try { + return db.prepare('all', `SELECT "userName", "userID" FROM "userNames" WHERE "userName" + LIKE ? ESCAPE '\\' LIMIT 10`, [userName]) + } catch (err) { + return null; + } +} + +function getExactUserID(userName: String): Promise<{userName: String, userID: UserID }[]> { + try { + return db.prepare('all', `SELECT "userName", "userID" from "userNames" WHERE "userName" = ? LIMIT 10`, [userName]); + } catch (err) { + return null; + } +} + +export async function getUserID(req: Request, res: Response) { + let userName = req.query.username as string; + const exactSearch = req.query.exact + ? req.query.exact == "true" + : false as Boolean; + + // if not exact and length is 1, also skip + if (userName == undefined || userName.length > 64 || + (!exactSearch && userName.length < 3)) { + // invalid request + res.sendStatus(400); + return false; + } + const results = exactSearch + ? await getExactUserID(userName) + : await getFuzzyUserID(userName); + + if (results === undefined || results === null) { + res.sendStatus(500); + return false; + } else if (results.length === 0) { + res.sendStatus(404); + return false; + } else { + res.send(results); + return false; + } +} diff --git a/test/cases/getSegmentInfo.ts b/test/cases/getSegmentInfo.ts index 3f03027..a6c2f0d 100644 --- a/test/cases/getSegmentInfo.ts +++ b/test/cases/getSegmentInfo.ts @@ -3,20 +3,20 @@ import {db} from '../../src/databases/databases'; import {Done, getbaseURL} from '../utils'; import {getHash} from '../../src/utils/getHash'; -const ENOENTID = "0000000000000000000000000000000000000000000000000000000000000000" -const upvotedID = "a000000000000000000000000000000000000000000000000000000000000000" -const downvotedID = "b000000000000000000000000000000000000000000000000000000000000000" -const lockedupID = "c000000000000000000000000000000000000000000000000000000000000000" -const infvotesID = "d000000000000000000000000000000000000000000000000000000000000000" -const shadowhiddenID = "e000000000000000000000000000000000000000000000000000000000000000" -const lockeddownID = "f000000000000000000000000000000000000000000000000000000000000000" -const hiddenID = "1000000000000000000000000000000000000000000000000000000000000000" -const fillerID1 = "1100000000000000000000000000000000000000000000000000000000000000" -const fillerID2 = "1200000000000000000000000000000000000000000000000000000000000000" -const fillerID3 = "1300000000000000000000000000000000000000000000000000000000000000" -const fillerID4 = "1400000000000000000000000000000000000000000000000000000000000000" -const fillerID5 = "1500000000000000000000000000000000000000000000000000000000000000" -const oldID = "a0000000-0000-0000-0000-000000000000" +const ENOENTID = "0".repeat(64); +const upvotedID = "a"+"0".repeat(63); +const downvotedID = "b"+"0".repeat(63); +const lockedupID = "c"+"0".repeat(63); +const infvotesID = "d"+"0".repeat(63); +const shadowhiddenID = "e"+"0".repeat(63); +const lockeddownID = "f"+"0".repeat(63); +const hiddenID = "1"+"0".repeat(63); +const fillerID1 = "11"+"0".repeat(62); +const fillerID2 = "12"+"0".repeat(62); +const fillerID3 = "13"+"0".repeat(62); +const fillerID4 = "14"+"0".repeat(62); +const fillerID5 = "15"+"0".repeat(62); +const oldID = `${'0'.repeat(8)}-${'0000-'.repeat(3)}${'0'.repeat(12)}`; describe('getSegmentInfo', () => { before(async () => { diff --git a/test/cases/getUserID.ts b/test/cases/getUserID.ts new file mode 100644 index 0000000..7fe5d70 --- /dev/null +++ b/test/cases/getUserID.ts @@ -0,0 +1,403 @@ +import fetch from 'node-fetch'; +import {Done, getbaseURL} from '../utils'; +import {db} from '../../src/databases/databases'; +import {getHash} from '../../src/utils/getHash'; + +describe('getUserID', () => { + before(async () => { + const insertUserNameQuery = 'INSERT INTO "userNames" ("userID", "userName") VALUES(?, ?)'; + await db.prepare("run", insertUserNameQuery, [getHash("getuserid_user_01"), 'fuzzy user 01']); + await db.prepare("run", insertUserNameQuery, [getHash("getuserid_user_02"), 'fuzzy user 02']); + await db.prepare("run", insertUserNameQuery, [getHash("getuserid_user_03"), 'specific user 03']); + await db.prepare("run", insertUserNameQuery, [getHash("getuserid_user_04"), 'repeating']); + await db.prepare("run", insertUserNameQuery, [getHash("getuserid_user_05"), 'repeating']); + await db.prepare("run", insertUserNameQuery, [getHash("getuserid_user_06"), getHash("getuserid_user_06")]); + await db.prepare("run", insertUserNameQuery, [getHash("getuserid_user_07"), '0redos0']); + await db.prepare("run", insertUserNameQuery, [getHash("getuserid_user_08"), '%redos%']); + await db.prepare("run", insertUserNameQuery, [getHash("getuserid_user_09"), '_redos_']); + await db.prepare("run", insertUserNameQuery, [getHash("getuserid_user_10"), 'redos\\%']); + await db.prepare("run", insertUserNameQuery, [getHash("getuserid_user_11"), '\\\\\\']); + await db.prepare("run", insertUserNameQuery, [getHash("getuserid_user_12"), 'a']); + }); + + it('Should be able to get a 200', (done: Done) => { + fetch(getbaseURL() + '/api/userID?username=fuzzy+user+01') + .then(async res => { + const text = await res.text() + if (res.status !== 200) done('non 200 (' + res.status + ')'); + else done(); // pass + }) + .catch(err => done('couldn\'t call endpoint')); + }); + + it('Should be able to get a 400 (No username parameter)', (done: Done) => { + fetch(getbaseURL() + '/api/userID') + .then(res => { + if (res.status !== 400) done('non 400 (' + res.status + ')'); + else done(); // pass + }) + .catch(err => done('couldn\'t call endpoint')); + }); + + it('Should be able to get a 200 (username is public id)', (done: Done) => { + fetch(getbaseURL() + '/api/userID?username='+getHash("getuserid_user_06")) + .then(async res => { + const text = await res.text() + if (res.status !== 200) done('non 200 (' + res.status + ')'); + else done(); // pass + }) + .catch(err => done('couldn\'t call endpoint')); + }); + + it('Should be able to get a 400 (username longer than 64 chars)', (done: Done) => { + fetch(getbaseURL() + '/api/userID?username='+getHash("getuserid_user_06")+'0') + .then(res => { + if (res.status !== 400) done('non 400 (' + res.status + ')'); + else done(); // pass + }) + .catch(err => done('couldn\'t call endpoint')); + }); + + it('Should be able to get single username', (done: Done) => { + fetch(getbaseURL() + '/api/userID?username=fuzzy+user+01') + .then(async res => { + if (res.status !== 200) { + done("non 200"); + } else { + const data = await res.json(); + if (data.length !== 1) { + done('Returned incorrect number of users "' + data.length + '"'); + } else if (data[0].userName !== "fuzzy user 01") { + done('Returned incorrect username "' + data.userName + '"'); + } else if (data[0].userID !== getHash("getuserid_user_01")) { + done('Returned incorrect userID "' + data.userID + '"'); + } else { + done(); // pass + } + } + }) + .catch(err => ("couldn't call endpoint")); + }); + + it('Should be able to get multiple fuzzy user info from start', (done: Done) => { + fetch(getbaseURL() + '/api/userID?username=fuzzy+user') + .then(async res => { + if (res.status !== 200) { + done("non 200"); + } else { + const data = await res.json(); + if (data.length !== 2) { + done('Returned incorrect number of users "' + data.length + '"'); + } else if (data[0].userName !== "fuzzy user 01") { + done('Returned incorrect username "' + data.userName + '"'); + } else if (data[0].userID !== getHash("getuserid_user_01")) { + done('Returned incorrect userID "' + data.userID + '"'); + } else if (data[1].userName !== "fuzzy user 02") { + done('Returned incorrect username "' + data.userName + '"'); + } else if (data[1].userID !== getHash("getuserid_user_02")) { + done('Returned incorrect userID "' + data.userID + '"'); + } else { + done(); // pass + } + } + }) + .catch(err => ("couldn't call endpoint")); + }); + + it('Should be able to get multiple fuzzy user info from middle', (done: Done) => { + fetch(getbaseURL() + '/api/userID?username=user') + .then(async res => { + if (res.status !== 200) { + done("non 200"); + } else { + const data = await res.json(); + if (data.length !== 3) { + done('Returned incorrect number of users "' + data.length + '"'); + } else if (data[0].userName !== "fuzzy user 01") { + done('Returned incorrect username "' + data.userName + '"'); + } else if (data[0].userID !== getHash("getuserid_user_01")) { + done('Returned incorrect userID "' + data.userID + '"'); + } else if (data[1].userName !== "fuzzy user 02") { + done('Returned incorrect username "' + data.userName + '"'); + } else if (data[1].userID !== getHash("getuserid_user_02")) { + done('Returned incorrect userID "' + data.userID + '"'); + } else if (data[2].userName !== "specific user 03") { + done('Returned incorrect username "' + data.userName + '"'); + } else if (data[2].userID !== getHash("getuserid_user_03")) { + done('Returned incorrect userID "' + data.userID + '"'); + } else { + done(); // pass + } + } + }) + .catch(err => ("couldn't call endpoint")); + }); + + it('Should be able to get with public ID', (done: Done) => { + const userID = getHash("getuserid_user_06"); + fetch(getbaseURL() + '/api/userID?username='+userID) + .then(async res => { + if (res.status !== 200) { + done("non 200"); + } else { + const data = await res.json(); + if (data.length !== 1) { + done('Returned incorrect number of users "' + data.length + '"'); + } else if (data[0].userName !== userID) { + done('Returned incorrect username "' + data.userName + '"'); + } else if (data[0].userID !== userID) { + done('Returned incorrect userID "' + data.userID + '"'); + } else { + done(); // pass + } + } + }) + .catch(err => ("couldn't call endpoint")); + }); + + it('Should be able to get with fuzzy public ID', (done: Done) => { + const userID = getHash("getuserid_user_06"); + fetch(getbaseURL() + '/api/userID?username='+userID.substr(10,60)) + .then(async res => { + if (res.status !== 200) { + done("non 200"); + } else { + const data = await res.json(); + if (data.length !== 1) { + done('Returned incorrect number of users "' + data.length + '"'); + } else if (data[0].userName !== userID) { + done('Returned incorrect username "' + data.userName + '"'); + } else if (data[0].userID !== userID) { + done('Returned incorrect userID "' + data.userID + '"'); + } else { + done(); // pass + } + } + }) + .catch(err => ("couldn't call endpoint")); + }); + + it('Should be able to get repeating username', (done: Done) => { + fetch(getbaseURL() + '/api/userID?username=repeating') + .then(async res => { + if (res.status !== 200) { + done("non 200"); + } else { + const data = await res.json(); + if (data.length !== 2) { + done('Returned incorrect number of users "' + data.length + '"'); + } else if (data[0].userName !== "repeating") { + done('Returned incorrect username "' + data.userName + '"'); + } else if (data[0].userID !== getHash("getuserid_user_04")) { + done('Returned incorrect userID "' + data.userID + '"'); + } else if (data[1].userName !== "repeating") { + done('Returned incorrect username "' + data.userName + '"'); + } else if (data[1].userID !== getHash("getuserid_user_05")) { + done('Returned incorrect userID "' + data.userID + '"'); + } else { + done(); // pass + } + } + }) + .catch(err => ("couldn't call endpoint")); + }); + + it('Should be able to get repeating fuzzy username', (done: Done) => { + fetch(getbaseURL() + '/api/userID?username=peat') + .then(async res => { + if (res.status !== 200) { + done("non 200"); + } else { + const data = await res.json(); + if (data.length !== 2) { + done('Returned incorrect number of users "' + data.length + '"'); + } else if (data[0].userName !== "repeating") { + done('Returned incorrect username "' + data.userName + '"'); + } else if (data[0].userID !== getHash("getuserid_user_04")) { + done('Returned incorrect userID "' + data.userID + '"'); + } else if (data[1].userName !== "repeating") { + done('Returned incorrect username "' + data.userName + '"'); + } else if (data[1].userID !== getHash("getuserid_user_05")) { + done('Returned incorrect userID "' + data.userID + '"'); + } else { + done(); // pass + } + } + }) + .catch(err => ("couldn't call endpoint")); + }); + + it('should avoid ReDOS with _', (done: Done) => { + fetch(getbaseURL() + '/api/userID?username=_redos_') + .then(async res => { + if (res.status !== 200) { + done("non 200"); + } else { + const data = await res.json(); + if (data.length !== 1) { + done('Returned incorrect number of users "' + data.length + '"'); + } else if (data[0].userName !== "_redos_") { + done('Returned incorrect username "' + data.userName + '"'); + } else if (data[0].userID !== getHash("getuserid_user_09")) { + done('Returned incorrect userID "' + data.userID + '"'); + } else { + done(); // pass + } + } + }) + .catch(err => ("couldn't call endpoint")); + }); + + it('should avoid ReDOS with %', (done: Done) => { + fetch(getbaseURL() + '/api/userID?username=%redos%') + .then(async res => { + if (res.status !== 200) { + done("non 200"); + } else { + const data = await res.json(); + if (data.length !== 1) { + done('Returned incorrect number of users "' + data.length + '"'); + } else if (data[0].userName !== "%redos%") { + done('Returned incorrect username "' + data.userName + '"'); + } else if (data[0].userID !== getHash("getuserid_user_08")) { + done('Returned incorrect userID "' + data.userID + '"'); + } else { + done(); // pass + } + } + }) + .catch(err => ("couldn't call endpoint")); + }); + + it('should return 404 if escaped backslashes present', (done: Done) => { + fetch(getbaseURL() + '/api/userID?username=%redos\\\\_') + .then(res => { + if (res.status !== 404) done('non 404 (' + res.status + ')'); + else done(); // pass + }) + .catch(err => ("couldn't call endpoint")); + }); + + it('should return 404 if backslashes present', (done: Done) => { + fetch(getbaseURL() + '/api/userID?username=\\%redos\\_') + .then(res => { + if (res.status !== 404) done('non 404 (' + res.status + ')'); + else done(); // pass + }) + .catch(err => ("couldn't call endpoint")); + }); + + it('should return user if just backslashes', (done: Done) => { + fetch(getbaseURL() + '/api/userID?username=\\\\\\') + .then(async res => { + if (res.status !== 200) { + done("non 200"); + } else { + const data = await res.json(); + if (data.length !== 1) { + done('Returned incorrect number of users "' + data.length + '"'); + } else if (data[0].userName !== "\\\\\\") { + done('Returned incorrect username "' + data.userName + '"'); + } else if (data[0].userID !== getHash("getuserid_user_11")) { + done('Returned incorrect userID "' + data.userID + '"'); + } else { + done(); // pass + } + } + }) + .catch(err => ("couldn't call endpoint")); + }); + + it('should not allow usernames more than 64 characters', (done: Done) => { + fetch(getbaseURL() + '/api/userID?username='+'0'.repeat(65)) + .then(res => { + if (res.status !== 400) done('non 400 (' + res.status + ')'); + else done(); // pass + }) + .catch(err => ("couldn't call endpoint")); + }); + + it('should not allow usernames less than 3 characters', (done: Done) => { + fetch(getbaseURL() + '/api/userID?username=aa') + .then(res => { + if (res.status !== 400) done('non 400 (' + res.status + ')'); + else done(); // pass + }) + .catch(err => ("couldn't call endpoint")); + }); + + it('should allow exact match', (done: Done) => { + fetch(getbaseURL() + '/api/userID?username=a&exact=true') + .then(async res => { + if (res.status !== 200) { + done("non 200"); + } else { + const data = await res.json(); + if (data.length !== 1) { + done('Returned incorrect number of users "' + data.length + '"'); + } else if (data[0].userName !== "a") { + done('Returned incorrect username "' + data.userName + '"'); + } else if (data[0].userID !== getHash("getuserid_user_12")) { + done('Returned incorrect userID "' + data.userID + '"'); + } else { + done(); // pass + } + } + }) + .catch(err => ("couldn't call endpoint")); + }); + + it('Should be able to get repeating username with exact username', (done: Done) => { + fetch(getbaseURL() + '/api/userID?username=repeating&exact=true') + .then(async res => { + if (res.status !== 200) { + done("non 200"); + } else { + const data = await res.json(); + if (data.length !== 2) { + done('Returned incorrect number of users "' + data.length + '"'); + } else if (data[0].userName !== "repeating") { + done('Returned incorrect username "' + data.userName + '"'); + } else if (data[0].userID !== getHash("getuserid_user_04")) { + done('Returned incorrect userID "' + data.userID + '"'); + } else if (data[1].userName !== "repeating") { + done('Returned incorrect username "' + data.userName + '"'); + } else if (data[1].userID !== getHash("getuserid_user_05")) { + done('Returned incorrect userID "' + data.userID + '"'); + } else { + done(); // pass + } + } + }) + .catch(err => ("couldn't call endpoint")); + }); + + it('Should not get exact unless explicitly set to true', (done: Done) => { + fetch(getbaseURL() + '/api/userID?username=user&exact=1') + .then(async res => { + if (res.status !== 200) { + done("non 200"); + } else { + const data = await res.json(); + if (data.length !== 3) { + done('Returned incorrect number of users "' + data.length + '"'); + } else if (data[0].userName !== "fuzzy user 01") { + done('Returned incorrect username "' + data.userName + '"'); + } else if (data[0].userID !== getHash("getuserid_user_01")) { + done('Returned incorrect userID "' + data.userID + '"'); + } else if (data[1].userName !== "fuzzy user 02") { + done('Returned incorrect username "' + data.userName + '"'); + } else if (data[1].userID !== getHash("getuserid_user_02")) { + done('Returned incorrect userID "' + data.userID + '"'); + } else if (data[2].userName !== "specific user 03") { + done('Returned incorrect username "' + data.userName + '"'); + } else if (data[2].userID !== getHash("getuserid_user_03")) { + done('Returned incorrect userID "' + data.userID + '"'); + } else { + done(); // pass + } + } + }) + .catch(err => ("couldn't call endpoint")); + }); +});