added more injection, all done other than uploading

This commit is contained in:
subnub
2024-07-05 04:46:39 -04:00
parent 7bd81a9aa5
commit c0e171b88d
8 changed files with 197 additions and 189 deletions
+90 -4
View File
@@ -10,6 +10,7 @@ import {
} from "../cookies/createCookies";
import ChunkService from "../services/ChunkService";
import streamToBuffer from "../utils/streamToBuffer";
import { read } from "fs";
const fileService = new FileService();
@@ -151,16 +152,50 @@ class FileController {
};
getPublicDownload = async (req: RequestType, res: Response) => {
let responseSent = false;
try {
const ID = req.params.id;
const tempToken = req.params.tempToken;
await this.chunkService.getPublicDownload(ID, tempToken, res);
const { readStream, decipher, file } =
await this.chunkService.getPublicDownload(ID, tempToken, res);
readStream.on("error", (e: Error) => {
console.log("read stream error", e);
if (!responseSent) {
responseSent = true;
res.status(500).send("Server error downloading publicfile");
}
});
decipher.on("error", (e: Error) => {
console.log("decipher stream error", e);
if (!responseSent) {
responseSent = true;
res.status(500).send("Server error downloading public file");
}
});
res.set("Content-Type", "binary/octet-stream");
res.set(
"Content-Disposition",
'attachment; filename="' + file.filename + '"'
);
res.set("Content-Length", file.metadata.size.toString());
readStream.pipe(decipher).pipe(res);
// if (file.metadata.linkType === "one") {
// await dbUtilsFile.removeOneTimePublicLink(fileID);
// }
} catch (e: unknown) {
if (e instanceof Error) {
console.log("\nGet Public Download Error File Route:", e.message);
}
res.status(500).send("Server error downloading");
if (!responseSent) {
responseSent = true;
res.status(500).send("Server error downloading public file");
}
}
};
@@ -408,19 +443,70 @@ class FileController {
if (!req.user) {
return;
}
let responseSent = false;
try {
const user = req.user;
const fileID = req.params.id;
const headers = req.headers;
await this.chunkService.streamVideo(user, fileID, headers, res, req);
const { decipher, readStream, head } =
await this.chunkService.streamVideo(user, fileID, headers);
const cleanUp = () => {
if (readStream) readStream.destroy();
if (decipher) decipher.end();
};
const handleError = (e: Error) => {
console.log("stream video read stream error", e);
if (!responseSent) {
responseSent = true;
res.status(500).send("Server error streaming video");
}
cleanUp();
};
readStream.on("error", handleError);
decipher.on("error", handleError);
readStream.on("end", () => {
if (!responseSent) {
responseSent = true;
res.end();
}
cleanUp();
});
readStream.on("close", () => {
cleanUp();
});
decipher.on("end", () => {
if (!responseSent) {
responseSent = true;
res.end();
}
cleanUp();
});
decipher.on("close", () => {
cleanUp();
});
res.writeHead(206, head);
readStream.pipe(decipher).pipe(res);
} catch (e: unknown) {
if (e instanceof Error) {
console.log("\nStream Video Error File Route:", e.message);
}
res.status(500).send("Server error streaming video");
if (!responseSent) {
responseSent = true;
res.status(500).send("Server error streaming video");
}
}
};
@@ -1,18 +1,26 @@
import fs from "fs";
import { UserInterface } from "../../models/user";
import { AuthParams, IStorageActions } from "./StoreTypes";
import { GenericParams, IStorageActions } from "./StoreTypes";
class FilesystemActions implements IStorageActions {
async getAuth() {
return {};
}
createReadStream(params: AuthParams): NodeJS.ReadableStream {
createReadStream(params: GenericParams): NodeJS.ReadableStream {
if (!params.filePath) throw new Error("File path not configured");
const fsReadableStream = fs.createReadStream(params.filePath);
return fsReadableStream;
}
async removeChunks(params: AuthParams) {
createReadStreamWithRange(params: GenericParams, start: number, end: number) {
if (!params.filePath) throw new Error("File path not configured");
const fsReadableStream = fs.createReadStream(params.filePath, {
start,
end,
});
return fsReadableStream;
}
async removeChunks(params: GenericParams) {
return new Promise<void>((resolve, reject) => {
if (!params.filePath) {
reject("File path not configured");
@@ -28,6 +36,19 @@ class FilesystemActions implements IStorageActions {
});
});
}
async getPrevIV(params: GenericParams, start: number) {
return new Promise<Buffer | string>((resolve, reject) => {
if (!params.filePath) throw new Error("File path not configured");
const stream = fs.createReadStream(params.filePath, {
start,
end: start + 15,
});
stream.on("data", (data) => {
resolve(data);
});
});
}
}
export { FilesystemActions };
+32 -3
View File
@@ -1,7 +1,7 @@
import { UserInterface } from "../../models/user";
import s3 from "../../db/s3";
import env from "../../enviroment/env";
import { AuthParams, IStorageActions } from "./StoreTypes";
import { GenericParams, IStorageActions } from "./StoreTypes";
import internal from "stream";
class S3Actions implements IStorageActions {
@@ -9,7 +9,7 @@ class S3Actions implements IStorageActions {
return { s3Storage: s3, bucket: env.s3Bucket! };
}
createReadStream(params: AuthParams): internal.Readable {
createReadStream(params: GenericParams): internal.Readable {
if (!params.Key) throw new Error("S3 not configured");
const { s3Storage, bucket } = this.getAuth();
const s3ReadableStream = s3Storage
@@ -18,7 +18,21 @@ class S3Actions implements IStorageActions {
return s3ReadableStream;
}
removeChunks(params: AuthParams) {
createReadStreamWithRange(
params: GenericParams,
start: number,
end: number
): internal.Readable {
if (!params.Key) throw new Error("S3 not configured");
const range = `bytes=${start}-${end}`;
const { s3Storage, bucket } = this.getAuth();
const s3ReadableStream = s3Storage
.getObject({ Key: params.Key, Bucket: bucket, Range: range })
.createReadStream();
return s3ReadableStream;
}
removeChunks(params: GenericParams) {
return new Promise<void>((resolve, reject) => {
if (!params.Key) {
reject("S3 not configured");
@@ -34,6 +48,21 @@ class S3Actions implements IStorageActions {
});
});
}
async getPrevIV(params: GenericParams, start: number) {
return new Promise<Buffer | string>((resolve, reject) => {
if (!params.Key) throw new Error("S3 not configured");
const { s3Storage, bucket } = this.getAuth();
const range = `bytes=${start}-${start + 15}`;
const stream = s3Storage
.getObject({ Key: params.Key, Bucket: bucket, Range: range })
.createReadStream();
stream.on("data", (data) => {
resolve(data);
});
});
}
}
export { S3Actions };
+3 -3
View File
@@ -1,6 +1,6 @@
import internal from "stream";
export interface AuthParams {
export interface GenericParams {
Key?: string;
Bucket?: string;
filePath?: string;
@@ -10,7 +10,7 @@ export interface AuthParams {
export interface IStorageActions {
getAuth(): Object;
createReadStream(
params: AuthParams
params: GenericParams
): NodeJS.ReadableStream | internal.Readable;
removeChunks(params: AuthParams): Promise<void>;
removeChunks(params: GenericParams): Promise<void>;
}
+39 -87
View File
@@ -46,7 +46,6 @@ class StorageService {
constructor() {}
uploadFile = async (user: UserInterface, busboy: any, req: Request) => {
console.log("upeload file2");
const password = user.getEncryptionKey();
if (!password) throw new ForbiddenError("Invalid Encryption Key");
@@ -60,14 +59,12 @@ class StorageService {
const { file, filename: fileInfo, formData } = await getBusboyData(busboy);
const filename = fileInfo.filename;
console.log("1");
const parent = formData.get("parent") || "/";
const size = formData.get("size") || "";
const personalFile = formData.get("personal-file") ? true : false;
let hasThumbnail = false;
let thumbnailID = "";
const isVideo = videoChecker(filename);
console.log("2", parent, typeof parent);
const parentList = [];
@@ -81,8 +78,6 @@ class StorageService {
parentList.push("/");
}
console.log("parent list", parentList);
const systemFileName = uuid.v4();
const metadata = {
@@ -127,7 +122,6 @@ class StorageService {
const videoCheck = videoChecker(currentFile.filename);
if (videoCheck) {
console.log("is vidoe");
const updatedFile = await createVideoThumbnailFS(
currentFile,
filename,
@@ -255,23 +249,8 @@ class StorageService {
};
};
streamVideo = async (
user: UserInterface,
fileID: string,
headers: any,
res: Response,
req: Request
) => {
// To get this all working correctly with encryption and across
// All browsers took many days, tears, and some of my sanity.
// Shoutout to Tyzoid for helping me with the decryption
// And and helping me understand how the IVs work.
// P.S I hate safari >:(
// Why do yall have to be weird with video streaming
// 90% of the issues with this are only in Safari
// Is safari going to be the next internet explorer?
// INJECTED
streamVideo = async (user: UserInterface, fileID: string, headers: any) => {
const userID = user._id;
const currentFile = await dbUtilsFile.getFileInfo(
fileID,
@@ -306,52 +285,33 @@ class StorageService {
let fixedEnd = currentFile.length;
if (start === 0 && end === 1) {
// This is for Safari/iOS, Safari will request the first
// Byte before actually playing the video. Needs to be
// 16 bytes.
fixedStart = 0;
fixedEnd = 15;
} else {
// If you're a normal browser, or this isn't Safari's first request
// We need to make it so start is divisible by 16, since AES256
// Has a block size of 16 bytes.
fixedStart = start % 16 === 0 ? start : fixStartChunkLength(start);
}
if (+start === 0) {
// This math will not work if the start is 0
// So if it is we just change fixed start back
// To 0.
fixedStart = 0;
}
// We also need to calculate the difference between the start and the
// Fixed start position. Since there will be an offset if the original
// Request is not divisible by 16, it will not return the right part
// Of the file, you will see how we do this in the awaitStreamVideo
// code.
const differenceStart = start - fixedStart;
const readStreamParams = createGenericParams({
filePath: currentFile.metadata.filePath,
Key: currentFile.metadata.s3ID,
});
if (fixedStart !== 0 && start !== 0) {
// If this isn't the first request, the way AES256 works is when you try to
// Decrypt a certain part of the file that isn't the start, the IV will
// Actually be the 16 bytes ahead of where you are trying to
// Start the decryption.
currentIV = (await getPrevIVFS(
fixedStart - 16,
currentFile.metadata.filePath!
currentIV = (await storageActions.getPrevIV(
readStreamParams,
fixedStart - 16
)) as Buffer;
}
const readStream = fs.createReadStream(currentFile.metadata.filePath!, {
start: fixedStart,
end: fixedEnd,
});
const readStream = storageActions.createReadStreamWithRange(
readStreamParams,
fixedStart,
fixedEnd
);
const CIPHER_KEY = crypto.createHash("sha256").update(password).digest();
@@ -359,26 +319,15 @@ class StorageService {
decipher.setAutoPadding(false);
res.writeHead(206, head);
const allStreamsToErrorCatch = [readStream, decipher];
readStream.pipe(decipher);
await awaitStreamVideo(
start,
end,
differenceStart,
return {
readStream,
decipher,
res,
req,
allStreamsToErrorCatch,
readStream
);
readStream.destroy();
file: currentFile,
head,
};
};
// INJECTED
getPublicDownload = async (fileID: string, tempToken: any, res: Response) => {
const file: FileInterface = await dbUtilsFile.getPublicFile(fileID);
@@ -386,34 +335,37 @@ class StorageService {
throw new NotAuthorizedError("File Not Public");
}
await dbUtilsFile.removeOneTimePublicLink(fileID);
const user = (await User.findById(file.metadata.owner)) as UserInterface;
const password = user.getEncryptionKey();
if (!password) throw new ForbiddenError("Invalid Encryption Key");
const IV = file.metadata.IV.buffer as Buffer;
// TODO: I believe this has to do with using conn.db instead of the mongoose driver
// We should switch to the mongoose driver
let IV = file.metadata.IV as any;
if (!(IV instanceof Buffer)) {
IV = IV.buffer;
}
const readStream = fs.createReadStream(file.metadata.filePath!);
const readStreamParams = createGenericParams({
filePath: file.metadata.filePath,
Key: file.metadata.s3ID,
});
const readStream = storageActions.createReadStream(readStreamParams);
const CIPHER_KEY = crypto.createHash("sha256").update(password).digest();
const decipher = crypto.createDecipheriv("aes256", CIPHER_KEY, IV);
res.set("Content-Type", "binary/octet-stream");
res.set(
"Content-Disposition",
'attachment; filename="' + file.filename + '"'
);
res.set("Content-Length", file.metadata.size.toString());
const allStreamsToErrorCatch = [readStream, decipher];
await awaitStream(readStream.pipe(decipher), res, allStreamsToErrorCatch);
if (file.metadata.linkType === "one") {
await dbUtilsFile.removeOneTimePublicLink(fileID);
}
return {
readStream,
decipher,
file: file,
};
};
// INJECTED
@@ -1,11 +1,7 @@
import { ManagedUpload } from "aws-sdk/clients/s3";
import s3 from "../../../db/s3";
const awaitUploadStreamS3 = (
params: any,
personalFile: boolean,
s3Data: { id: string; key: string; bucket: string }
) => {
const uploadStreamS3 = (params: any) => {
return new Promise<void>((resolve, reject) => {
s3.upload(params, (err: any, data: ManagedUpload.SendData) => {
if (err) {
@@ -18,4 +14,4 @@ const awaitUploadStreamS3 = (
});
};
export default awaitUploadStreamS3;
export default uploadStreamS3;
@@ -45,43 +45,6 @@ const createVideoThumbnailFS = (
const decryptedReadStream = readStream.pipe(decipher);
// const encryptedWriteStream = thumbnailCipher
// .pipe(writeStream)
// .on("finish", async () => {
// const thumbnailModel = new Thumbnail({
// name: filename,
// owner: user._id,
// IV: thumbnailIV,
// path: env.fsDirectory + thumbnailFilename,
// });
// await thumbnailModel.save();
// if (!file._id) {
// return reject();
// }
// const updateFileResponse = await File.updateOne(
// { _id: new ObjectId(file._id), "metadata.owner": user._id },
// {
// $set: {
// "metadata.hasThumbnail": true,
// "metadata.thumbnailID": thumbnailModel._id,
// },
// }
// );
// if (updateFileResponse.modifiedCount === 0) {
// return reject();
// }
// const updatedFile = await File.findById({
// _id: new ObjectId(file._id),
// "metadata.owner": user._id,
// });
// if (!updatedFile) return reject();
// resolve(updatedFile?.toObject());
// });
ffmpeg(decryptedReadStream, {
timeout: 60,
})
@@ -146,44 +109,6 @@ const createVideoThumbnailFS = (
})
.pipe(thumbnailCipher)
.pipe(writeStream, { end: true });
// ffmpeg()
// .input(readStream.pipe(decipher))
// .seekInput("00:00:01.00")
// .outputFormat("image2")
// .pipe(writeStream, { end: true })
// .on("end", async () => {
// const thumbnailModel = new Thumbnail({
// name: filename,
// owner: user._id,
// IV: thumbnailIV,
// path: env.fsDirectory + thumbnailFilename,
// });
// await thumbnailModel.save();
// resolve(file);
// })
// .on("error", (e) => {
// console.log("error", e);
// reject(e);
// });
// ffmpeg()
// .input(readStream.pipe(decipher))
// .size("300x?")
// .outputOptions("-frames:v 1")
// .on("end", async () => {
// const thumbnailModel = new Thumbnail({
// name: filename,
// owner: user._id,
// IV: thumbnailIV,
// path: env.fsDirectory + thumbnailFilename,
// });
// await thumbnailModel.save();
// resolve(file);
// })
// .output(writeStream);
});
};
+7 -8
View File
@@ -104,10 +104,9 @@ const LeftSection = (props) => {
)}
</div>
<div className="pl-2 mr-[20px] py-2 hover:bg-[#f6f5fd] rounded-md">
<ul className="m-0 list-none p-0 cursor-pointer">
<ul onClick={goHome} className="m-0 list-none p-0 cursor-pointer">
<li>
<a
onClick={goHome}
className={classNames(
"flex items-center text-[#3c85ee] font-medium no-underline animate",
isHome ? "text-[#3c85ee]" : "text-[#637381]"
@@ -123,10 +122,9 @@ const LeftSection = (props) => {
</div>
</div>
<div className="pl-2 mr-[20px] py-2 hover:bg-[#f6f5fd] mt-1 mb-1 rounded-md">
<ul className="m-0 list-none p-0 cursor-pointer ">
<ul onClick={goMedia} className="m-0 list-none p-0 cursor-pointer ">
<li>
<a
onClick={goMedia}
className={classNames(
"flex items-center text-[#3c85ee] font-medium no-underline animate",
isMedia ? "text-[#3c85ee]" : "text-[#637381]"
@@ -141,10 +139,12 @@ const LeftSection = (props) => {
</ul>
</div>
<div className="pl-2 mr-[20px] py-2 hover:bg-[#f6f5fd] rounded-md block desktopMode:hidden mb-1">
<ul className="m-0 list-none p-0 cursor-pointer ">
<ul
onClick={goSettings}
className="m-0 list-none p-0 cursor-pointer "
>
<li>
<a
onClick={goSettings}
className={classNames(
"flex items-center text-[#3c85ee] font-medium no-underline animate",
isSettings ? "text-[#3c85ee]" : "text-[#637381]"
@@ -159,10 +159,9 @@ const LeftSection = (props) => {
</ul>
</div>
<div className="pl-2 mr-[20px] py-2 hover:bg-[#f6f5fd] rounded-md">
<ul className="m-0 list-none p-0 cursor-pointer ">
<ul onClick={goTrash} className="m-0 list-none p-0 cursor-pointer ">
<li>
<a
onClick={goTrash}
className={classNames(
"flex items-center text-[#3c85ee] font-medium no-underline animate",
isTrash ? "text-red-500" : "text-[#637381]"