From 936ac3b253050994c219cdbb5cbb7bba0fe0642e Mon Sep 17 00:00:00 2001 From: S1LV3R Date: Thu, 18 Feb 2021 16:08:15 +0100 Subject: [PATCH 1/7] feat: add middleware to attach CTF --- src/api/v1/ctf.ts | 4 +++- src/types/express.d.ts | 2 ++ src/util/middleware/ctf.ts | 25 +++++++++++++++++++++++++ 3 files changed, 30 insertions(+), 1 deletion(-) create mode 100644 src/util/middleware/ctf.ts diff --git a/src/api/v1/ctf.ts b/src/api/v1/ctf.ts index 083ed53..c796d1e 100644 --- a/src/api/v1/ctf.ts +++ b/src/api/v1/ctf.ts @@ -3,11 +3,13 @@ import Logger from "../../loaders/logger"; import CTFService from "../../services/CTF"; import { notImplemented } from "../../util"; +import attachCTF from "../../util/middleware/ctf"; import attachUser from "../../util/middleware/user"; export default (): Router => { const router = Router({ mergeParams: true }); - router.use(attachUser()); + + router.use(attachUser(), attachCTF()); router.route("/").get(listCTFs).post(createCTF).all(notImplemented); router.route("/:ctfID").get(getCTF).all(notImplemented); diff --git a/src/types/express.d.ts b/src/types/express.d.ts index e652bb4..d7c0e41 100644 --- a/src/types/express.d.ts +++ b/src/types/express.d.ts @@ -1,3 +1,4 @@ +import { ICTF } from "../models/CTF"; import { ITeam } from "../models/Team"; import { IUser } from "../models/User"; @@ -5,5 +6,6 @@ declare module "express" { export interface Request { user?: IUser; team?: ITeam; + ctf?: ICTF; } } diff --git a/src/util/middleware/ctf.ts b/src/util/middleware/ctf.ts new file mode 100644 index 0000000..34dd1dd --- /dev/null +++ b/src/util/middleware/ctf.ts @@ -0,0 +1,25 @@ +import { NextFunction, Request, Response } from "express"; +import { CTFModel } from "../../models/CTF"; + +import { Middleware } from "../../types"; +import { InternalServerError, NotFoundError } from "../../types/httperrors"; + + +export default (): Middleware => { + return async function ( + req: Request, + res: Response, + next: NextFunction + ): Promise { + const ctf = await CTFModel.findById(req.params.ctfID) + .then() + .catch(() => next(new InternalServerError())); + + if (!ctf) { + return next(new NotFoundError({ errorCode: "error_ctf_not_found" })); + } + + req.ctf = ctf; + return next(); + }; +}; From 311b41d81dad632a706973a8596f1e3b46d327c0 Mon Sep 17 00:00:00 2001 From: S1LV3R Date: Thu, 18 Feb 2021 16:10:25 +0100 Subject: [PATCH 2/7] refactor: disable participants from challenge Will be re-added later --- src/models/Challenge.ts | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/src/models/Challenge.ts b/src/models/Challenge.ts index 31d5cfc..b70fe6b 100755 --- a/src/models/Challenge.ts +++ b/src/models/Challenge.ts @@ -1,19 +1,19 @@ import { model, Document, Schema } from "mongoose"; -import { IUser } from "./User"; +// import { IUser } from "./User"; interface IChallenge extends Document { notepad: string; points: number; solved: boolean; - participants: Array; + // participants: Array; } const ChallengeSchema = new Schema({ notepad: String, points: Number, solved: Boolean, - participants: [{ type: Schema.Types.ObjectId, ref: "User" }], + // participants: [{ type: Schema.Types.ObjectId, ref: "User" }], }); const ChallengeModel = model("Challenge", ChallengeSchema); From d9abae32c7ff4ee5ca569bed296d91c9dd822a03 Mon Sep 17 00:00:00 2001 From: S1LV3R Date: Thu, 18 Feb 2021 16:12:02 +0100 Subject: [PATCH 3/7] feat: create initial service, api Create the initial challenge service and challenge api, along with everything that comes with it. Types for example --- src/api/v1/challenge.ts | 19 +++++++++ src/api/v1/ctf.ts | 2 + src/services/Challenge.ts | 89 +++++++++++++++++++++++++++++++++++++++ src/types/index.d.ts | 8 ++++ 4 files changed, 118 insertions(+) create mode 100644 src/api/v1/challenge.ts create mode 100644 src/services/Challenge.ts diff --git a/src/api/v1/challenge.ts b/src/api/v1/challenge.ts new file mode 100644 index 0000000..848f3c1 --- /dev/null +++ b/src/api/v1/challenge.ts @@ -0,0 +1,19 @@ +import { NextFunction, Request, Response, Router } from "express"; +import Challenge from "../../services/Challenge"; + +export default (): Router => { + const router = Router(); + + router.post("/", createChallenge); + + return router; +}; + +const challengeService = new Challenge(); + +function createChallenge(req: Request, res: Response, next: NextFunction) { + challengeService + .createChallenge(req.user, req.team, req.ctf, req.body) + .then((challenge) => res.status(201).send(challenge)) + .catch((err) => next(err)); +} diff --git a/src/api/v1/ctf.ts b/src/api/v1/ctf.ts index c796d1e..e98ff87 100644 --- a/src/api/v1/ctf.ts +++ b/src/api/v1/ctf.ts @@ -5,11 +5,13 @@ import CTFService from "../../services/CTF"; import { notImplemented } from "../../util"; import attachCTF from "../../util/middleware/ctf"; import attachUser from "../../util/middleware/user"; +import challenge from "./challenge"; export default (): Router => { const router = Router({ mergeParams: true }); router.use(attachUser(), attachCTF()); + router.use("/:ctfID/challenges", challenge()); router.route("/").get(listCTFs).post(createCTF).all(notImplemented); router.route("/:ctfID").get(getCTF).all(notImplemented); diff --git a/src/services/Challenge.ts b/src/services/Challenge.ts new file mode 100644 index 0000000..c3d303d --- /dev/null +++ b/src/services/Challenge.ts @@ -0,0 +1,89 @@ +import axios from "axios"; + +import config from "../config"; +import Logger from "../loaders/logger"; +import { IChallenge, ChallengeModel } from "../models/Challenge"; +import { ICTF } from "../models/CTF"; +import { ITeam } from "../models/Team"; +import { IUser } from "../models/User"; +import { ChallengeOptions } from "../types"; +import { ForbiddenError, InternalServerError } from "../types/httperrors"; + +export default class Challenge { + private _hedgeDocAPI = axios.create({ + baseURL: config.get("hedgeDoc.baseURL"), + timeout: config.get("hedgeDoc.timeout"), + }); + + public async createChallenge( + user: IUser, + team: ITeam, + ctf: ICTF, + challengeOptions: ChallengeOptions + ): Promise { + Logger.verbose( + `ChallengeService >>> Creating new challenge in CTF "${team._id}"` + ); + + if (!team.inTeam(user) && !user.isAdmin) { + throw new ForbiddenError({ + errorCode: "error_invalid_permissions", + errorMessage: "You either are not in this team or not an admin.", + details: + "Only people who are in a team (or is an admin) can create challenges", + }); + } + + const notepad = await this.createNote( + challengeOptions.name, + challengeOptions.points + ); + + const challenge = new ChallengeModel({ + notepad: notepad.slice(1), + points: challengeOptions.points, + solved: false, + }); + + await challenge.save(); + + ctf.challenges.push(challenge); + await ctf.save(); + + return challenge; + } + + /** + * create a new HedgeDoc note + * + * @private + * @param {string} noteName the title of the note + * @param {string} pointsValue how many points the challenge is worth + * @return {Promise} returns the URL of the new note (including any leading slashes) + * @memberof CTFService + */ + private async createNote( + noteName: string, + pointsValue: string + ): Promise { + return await this._hedgeDocAPI + .post( + "/new", + `${noteName}\n${"=".repeat( + noteName.length + )}\n|Points|\n----\n${pointsValue}`, + { + headers: { "Content-Type": "text/markdown" }, + maxRedirects: 0, + validateStatus: (status) => status >= 200 && status < 400, // Accept responses in the 200-399 range + } + ) + .then((response) => { + return response.headers.location; + }) + .catch((_) => { + Logger.warn("Request response code outside acceptable range."); + throw new InternalServerError(); + }); + } +} diff --git a/src/types/index.d.ts b/src/types/index.d.ts index b263c3d..a10b30d 100644 --- a/src/types/index.d.ts +++ b/src/types/index.d.ts @@ -76,4 +76,12 @@ export interface CTFOptions { name: string; } +export interface ChallengeOptions { + // Name of the challenge + name: string; + + // How many points the challenge is worth + points: string; +} + export type Middleware = (req: Request, res: Response, next: NextFunction) => void | Promise From e5d07fcfebd8ac31d620bc065a164c5d3a2ad194 Mon Sep 17 00:00:00 2001 From: S1LV3R Date: Sun, 21 Feb 2021 15:20:51 +0100 Subject: [PATCH 4/7] feat: getting single CTF --- src/api/v1/challenge.ts | 8 ++++++++ src/services/Challenge.ts | 38 +++++++++++++++++++++++++++++++++++++- 2 files changed, 45 insertions(+), 1 deletion(-) diff --git a/src/api/v1/challenge.ts b/src/api/v1/challenge.ts index 848f3c1..1da4d77 100644 --- a/src/api/v1/challenge.ts +++ b/src/api/v1/challenge.ts @@ -5,6 +5,7 @@ export default (): Router => { const router = Router(); router.post("/", createChallenge); + router.get("/:challengeID", getChallenge); return router; }; @@ -17,3 +18,10 @@ function createChallenge(req: Request, res: Response, next: NextFunction) { .then((challenge) => res.status(201).send(challenge)) .catch((err) => next(err)); } + +function getChallenge(req: Request, res: Response, next: NextFunction) { + challengeService + .getChallenge(req.user, req.team, req.ctf, req.params.challengeID) + .then((challenge) => res.status(200).send(challenge)) + .catch((err) => next(err)); +} diff --git a/src/services/Challenge.ts b/src/services/Challenge.ts index c3d303d..8dc61a1 100644 --- a/src/services/Challenge.ts +++ b/src/services/Challenge.ts @@ -7,7 +7,11 @@ import { ICTF } from "../models/CTF"; import { ITeam } from "../models/Team"; import { IUser } from "../models/User"; import { ChallengeOptions } from "../types"; -import { ForbiddenError, InternalServerError } from "../types/httperrors"; +import { + ForbiddenError, + InternalServerError, + NotFoundError, +} from "../types/httperrors"; export default class Challenge { private _hedgeDocAPI = axios.create({ @@ -53,6 +57,38 @@ export default class Challenge { return challenge; } + public async getChallenge( + user: IUser, + team: ITeam, + ctf: ICTF, + challengeID: string + ): Promise { + Logger.verbose( + `ChallengeService >>> Getting challenge with ID ${challengeID} from CTF ${ctf._id}` + ); + + const challenge = await ChallengeModel.findById(challengeID).then(); + + if (!team.inTeam(user) || user.isAdmin) { + throw new ForbiddenError({ + errorCode: "error_invalid_permissions", + errorMessage: + "Cannot get challenge from team where you are not a member.", + details: + "Only people who are in a team (or is an admin) can get challenges from CTFs of that team", + }); + } + + if (!ctf.challenges.includes(challenge._id)) { + throw new NotFoundError({ + errorCode: "error_challenge_not_found", + errorMessage: "Challenge not found on specified CTF", + }); + } + + return challenge; + } + /** * create a new HedgeDoc note * From f6b1abf9bd0dc656f1a36544b7dab1130c112ef1 Mon Sep 17 00:00:00 2001 From: S1LV3R Date: Sun, 21 Feb 2021 15:22:20 +0100 Subject: [PATCH 5/7] refactor: update to use router.route.method instead of router.method --- src/api/v1/challenge.ts | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/src/api/v1/challenge.ts b/src/api/v1/challenge.ts index 1da4d77..e88689e 100644 --- a/src/api/v1/challenge.ts +++ b/src/api/v1/challenge.ts @@ -1,11 +1,12 @@ import { NextFunction, Request, Response, Router } from "express"; import Challenge from "../../services/Challenge"; +import { notImplemented } from "../../util"; export default (): Router => { const router = Router(); - router.post("/", createChallenge); - router.get("/:challengeID", getChallenge); + router.route("/").post(createChallenge).all(notImplemented); + router.route("/:challengeID").get(getChallenge).all(notImplemented); return router; }; From d9a279cdce89b7525c68d0483ec23eb364ee9d1d Mon Sep 17 00:00:00 2001 From: S1LV3R Date: Sun, 21 Feb 2021 15:32:25 +0100 Subject: [PATCH 6/7] docs: add JSDoc comments --- src/services/Challenge.ts | 22 +++++++++++++++++++++- 1 file changed, 21 insertions(+), 1 deletion(-) diff --git a/src/services/Challenge.ts b/src/services/Challenge.ts index 8dc61a1..de1d6cb 100644 --- a/src/services/Challenge.ts +++ b/src/services/Challenge.ts @@ -19,6 +19,16 @@ export default class Challenge { timeout: config.get("hedgeDoc.timeout"), }); + /** + * Creates a new challenge in a CTF + * + * @param {IUser} user the user performing the action + * @param {ITeam} team the team the user is using + * @param {ICTF} ctf the CTF the challenge should be created on + * @param {ChallengeOptions} challengeOptions options for the challenge, like points value and name + * @return {Promise} returns the challenge that was created + * @memberof Challenge + */ public async createChallenge( user: IUser, team: ITeam, @@ -57,6 +67,16 @@ export default class Challenge { return challenge; } + /** + * Gets a spesific challenge + * + * @param {IUser} user the user performing the action + * @param {ITeam} team the team the user is using + * @param {ICTF} ctf the CTF the challenge belongs on + * @param {string} challengeID the challenge to return + * @return {Promise} the challenge itself + * @memberof Challenge + */ public async getChallenge( user: IUser, team: ITeam, @@ -96,7 +116,7 @@ export default class Challenge { * @param {string} noteName the title of the note * @param {string} pointsValue how many points the challenge is worth * @return {Promise} returns the URL of the new note (including any leading slashes) - * @memberof CTFService + * @memberof Challenge */ private async createNote( noteName: string, From b0a773fbb4e8953b3f7a16675902d92e3ab47eec Mon Sep 17 00:00:00 2001 From: S1LV3R Date: Sun, 21 Feb 2021 16:30:09 +0100 Subject: [PATCH 7/7] feat: getting all challenges from ctf Filtering not yet added --- src/api/v1/challenge.ts | 13 ++++++++++++- src/services/Challenge.ts | 39 +++++++++++++++++++++++++++++++++++++++ 2 files changed, 51 insertions(+), 1 deletion(-) diff --git a/src/api/v1/challenge.ts b/src/api/v1/challenge.ts index e88689e..6f4b857 100644 --- a/src/api/v1/challenge.ts +++ b/src/api/v1/challenge.ts @@ -5,7 +5,11 @@ import { notImplemented } from "../../util"; export default (): Router => { const router = Router(); - router.route("/").post(createChallenge).all(notImplemented); + router + .route("/") + .get(getChallenges) + .post(createChallenge) + .all(notImplemented); router.route("/:challengeID").get(getChallenge).all(notImplemented); return router; @@ -26,3 +30,10 @@ function getChallenge(req: Request, res: Response, next: NextFunction) { .then((challenge) => res.status(200).send(challenge)) .catch((err) => next(err)); } + +function getChallenges(req: Request, res: Response, next: NextFunction) { + challengeService + .getChallenges(req.user, req.team, req.ctf) + .then((challenges) => res.status(200).send(challenges)) + .catch((err) => next(err)); +} diff --git a/src/services/Challenge.ts b/src/services/Challenge.ts index de1d6cb..6f61bf0 100644 --- a/src/services/Challenge.ts +++ b/src/services/Challenge.ts @@ -8,6 +8,7 @@ import { ITeam } from "../models/Team"; import { IUser } from "../models/User"; import { ChallengeOptions } from "../types"; import { + BadRequestError, ForbiddenError, InternalServerError, NotFoundError, @@ -109,6 +110,44 @@ export default class Challenge { return challenge; } + /** + * Returns all challenges in a provided CTF + * + * @param {IUser} user the user performing the action + * @param {ITeam} team the team the ctf belongs to + * @param {ICTF} ctf the ctf to get all the challenges from + * @return {Promise>} An array of the challenges + * @memberof Challenge + */ + public async getChallenges( + user: IUser, + team: ITeam, + ctf: ICTF + ): Promise> { + Logger.verbose( + `ChallengeService >>> Getting challenges from CTF with ID ${ctf._id}` + ); + + if (!team.inTeam(user) || !user.isAdmin) { + throw new ForbiddenError({ + errorCode: "error_invalid_permissions", + errorMessage: + "Cannot get challenges from team where you are not a member.", + details: + "Only people who are in a team (or is an admin) can get challenges from CTFs of that team", + }); + } + + if (!team.CTFs.includes(ctf._id)) { + throw new BadRequestError({ + errorCode: "error_ctf_not_in_team", + errorMessage: "This CTF is not in the provided team", + }); + } + + return ctf.challenges; + } + /** * create a new HedgeDoc note *