diff --git a/src/api/v1/challenge.ts b/src/api/v1/challenge.ts new file mode 100644 index 0000000..6f4b857 --- /dev/null +++ b/src/api/v1/challenge.ts @@ -0,0 +1,39 @@ +import { NextFunction, Request, Response, Router } from "express"; +import Challenge from "../../services/Challenge"; +import { notImplemented } from "../../util"; + +export default (): Router => { + const router = Router(); + + router + .route("/") + .get(getChallenges) + .post(createChallenge) + .all(notImplemented); + router.route("/:challengeID").get(getChallenge).all(notImplemented); + + 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)); +} + +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)); +} + +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/api/v1/ctf.ts b/src/api/v1/ctf.ts index 083ed53..e98ff87 100644 --- a/src/api/v1/ctf.ts +++ b/src/api/v1/ctf.ts @@ -3,11 +3,15 @@ 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"; +import challenge from "./challenge"; export default (): Router => { const router = Router({ mergeParams: true }); - router.use(attachUser()); + + 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/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); diff --git a/src/services/Challenge.ts b/src/services/Challenge.ts new file mode 100644 index 0000000..6f61bf0 --- /dev/null +++ b/src/services/Challenge.ts @@ -0,0 +1,184 @@ +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 { + BadRequestError, + ForbiddenError, + InternalServerError, + NotFoundError, +} from "../types/httperrors"; + +export default class Challenge { + private _hedgeDocAPI = axios.create({ + baseURL: config.get("hedgeDoc.baseURL"), + 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, + 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; + } + + /** + * 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, + 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; + } + + /** + * 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 + * + * @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 Challenge + */ + 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/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/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 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(); + }; +};