Skip to content
This repository was archived by the owner on May 1, 2021. It is now read-only.

Challenge service and API #57

Draft
wants to merge 7 commits into
base: dev
Choose a base branch
from
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
39 changes: 39 additions & 0 deletions src/api/v1/challenge.ts
Original file line number Diff line number Diff line change
@@ -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));
}
6 changes: 5 additions & 1 deletion src/api/v1/ctf.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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);
Expand Down
6 changes: 3 additions & 3 deletions src/models/Challenge.ts
Original file line number Diff line number Diff line change
@@ -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<IUser>;
// participants: Array<IUser>;
}

const ChallengeSchema = new Schema<IChallenge>({
notepad: String,
points: Number,
solved: Boolean,
participants: [{ type: Schema.Types.ObjectId, ref: "User" }],
// participants: [{ type: Schema.Types.ObjectId, ref: "User" }],
});

const ChallengeModel = model<IChallenge>("Challenge", ChallengeSchema);
Expand Down
184 changes: 184 additions & 0 deletions src/services/Challenge.ts
Original file line number Diff line number Diff line change
@@ -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<IChallenge>} returns the challenge that was created
* @memberof Challenge
*/
public async createChallenge(
user: IUser,
team: ITeam,
ctf: ICTF,
challengeOptions: ChallengeOptions
): Promise<IChallenge> {
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<IChallenge>} the challenge itself
* @memberof Challenge
*/
public async getChallenge(
user: IUser,
team: ITeam,
ctf: ICTF,
challengeID: string
): Promise<IChallenge> {
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<Array<IChallenge>>} An array of the challenges
* @memberof Challenge
*/
public async getChallenges(
user: IUser,
team: ITeam,
ctf: ICTF
): Promise<Array<IChallenge>> {
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<string>} returns the URL of the new note (including any leading slashes)
* @memberof Challenge
*/
private async createNote(
noteName: string,
pointsValue: string
): Promise<string> {
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();
});
}
}
2 changes: 2 additions & 0 deletions src/types/express.d.ts
Original file line number Diff line number Diff line change
@@ -1,9 +1,11 @@
import { ICTF } from "../models/CTF";
import { ITeam } from "../models/Team";
import { IUser } from "../models/User";

declare module "express" {
export interface Request {
user?: IUser;
team?: ITeam;
ctf?: ICTF;
}
}
8 changes: 8 additions & 0 deletions src/types/index.d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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<void>
25 changes: 25 additions & 0 deletions src/util/middleware/ctf.ts
Original file line number Diff line number Diff line change
@@ -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<void> {
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();
};
};