import { hashAPIPath } from "../../../actions/core/hash"; import { deleteBlobRep } from "../../../actions/azurestorage"; import { respondError, respondSuccess } from "../middleware/apiResponse"; import { getServerSession } from "next-auth/next"; import { authOptions } from "../auth/[...nextauth]"; import nextConnect from "next-connect"; const methodNotAllowed = (req, res) => { res.setHeader("Allow", "POST"); return respondError(res, { status: 405, code: "METHOD_NOT_ALLOWED", message: "Method not allowed" }); }; const ApiProxy = nextConnect({ onNoMatch: methodNotAllowed }); const forbidden = (res) => respondError(res, { status: 403, code: "FORBIDDEN", message: "Request not permitted" }); const isValidPathSegment = (value) => typeof value === "string" && value.length > 0 && value.length <= 255 && value === value.trim() && value !== "." && value !== ".." && !/[\\/\u0000-\u001f\u007f]/.test(value); const hasTrustedOrigin = (req) => { const requestOrigin = req?.headers?.origin; if (typeof requestOrigin !== "string" || requestOrigin === "null") { return false; } try { const requestUrl = new URL(requestOrigin); if (requestOrigin !== requestUrl.origin) { return false; } const normalizedRequestOrigin = requestUrl.origin; const trustedOrigins = [ process.env.NEXTAUTH_URL, process.env.CY_API_ROOT ] .filter((configuredUrl) => typeof configuredUrl === "string" && configuredUrl.length > 0 ) .map((configuredUrl) => new URL(configuredUrl).origin); return trustedOrigins.includes(normalizedRequestOrigin); } catch (_error) { return false; } }; ApiProxy.post(async (req, res) => { if (req.method !== "POST") { return methodNotAllowed(req, res); } if (!hasTrustedOrigin(req)) { return forbidden(res); } const containerName = req.query.container; const casefolderID = req.query.casefolderID; const repfile = req.query.repfile; const checkHash = req.query.hash; if ( typeof containerName === "undefined" || containerName.length === 0 || typeof casefolderID === "undefined" || casefolderID.length === 0 || typeof repfile === "undefined" || repfile.length === 0 || typeof checkHash === "undefined" || checkHash.length === 0 ) { return respondError(res, { status: 400, code: "MISSING_REQUIRED_QUERY", message: "container, casefolderID, repfile and hash are required" }); } const casefolderIDTrimmed = casefolderID.trim(); const repfileTrimmed = repfile.trim(); if ( !isValidPathSegment(casefolderIDTrimmed) || !isValidPathSegment(repfileTrimmed) ) { return respondError(res, { status: 400, code: "INVALID_REQUEST", message: "Invalid request" }); } const session = await getServerSession(req, res, authOptions(req, res)); if ( typeof session?.user?.id !== "string" || session.user.id.length === 0 || session.user.id !== containerName ) { return forbidden(res); } const hashCandidatePaths = [ "/api/file/deleteblobrep?container=" + containerName + "&casefolderID=" + casefolderIDTrimmed + "&repfile=" + repfileTrimmed, "/api/file/deleteblobrep?container=" + containerName + "&casefolderID=" + encodeURIComponent(casefolderIDTrimmed) + "&repfile=" + repfileTrimmed, "/api/file/deleteblobrep?container=" + containerName + "&casefolderID=" + casefolderIDTrimmed + "&repfile=" + encodeURIComponent(repfileTrimmed), "/api/file/deleteblobrep?container=" + containerName + "&casefolderID=" + encodeURIComponent(casefolderIDTrimmed) + "&repfile=" + encodeURIComponent(repfileTrimmed) ]; const isHashValid = hashCandidatePaths.some( (candidatePath) => hashAPIPath(candidatePath) == "&hash=" + checkHash ); if (!isHashValid) { return respondError(res, { status: 400, code: "INVALID_HASH", message: "Invalid hash" }); } try { const normalizedRepPath = casefolderIDTrimmed + "/" + repfileTrimmed; const data = await deleteBlobRep(containerName, normalizedRepPath); return respondSuccess(res, { data: data }); } catch (error) { return respondError(res, { status: 400, code: "DELETE_BLOB_REP_FAILED", message: "Unable to delete blob representation" }); } }); export const config = { api: { bodyParser: false } }; export default ApiProxy;