From c27c0dcf22eb8d012193364e4dedaeb3969ea343 Mon Sep 17 00:00:00 2001 From: 1yam <40899431+1yam@users.noreply.github.com> Date: Thu, 12 Oct 2023 19:18:01 +0200 Subject: [PATCH] Feature: Secure upload mechanism (#153) * fix: axios header issue * Feature: secure upload * Refactor: more simple code * Fix & refactor --------- Co-authored-by: Bonjour Internet --- src/messages/store/publish.ts | 128 ++++++++++++++++++++++++++++++---- src/messages/store/utils.ts | 20 ++++++ 2 files changed, 134 insertions(+), 14 deletions(-) create mode 100644 src/messages/store/utils.ts diff --git a/src/messages/store/publish.ts b/src/messages/store/publish.ts index bcf151db..2e2bb676 100644 --- a/src/messages/store/publish.ts +++ b/src/messages/store/publish.ts @@ -1,10 +1,15 @@ import * as base from "../../accounts/account"; -import { ItemType, MessageType, StoreContent, StoreMessage } from "../types"; +import { Account } from "../../accounts/account"; +import { BaseMessage, ItemType, MessageType, StoreContent, StoreMessage } from "../types"; import { PushFileToStorageEngine, PutContentToStorageEngine } from "../create/publish"; import { SignAndBroadcast } from "../create/signature"; import { RequireOnlyOne } from "../../utils/requiredOnlyOne"; import { DEFAULT_API_V2 } from "../../global"; import { MessageBuilder } from "../../utils/messageBuilder"; +import { stripTrailingSlash } from "../../utils/url"; +import FormData from "form-data"; +import axios from "axios"; +import { blobToBuffer, calculateSHA256Hash } from "./utils"; /** * channel: The channel in which the message will be published. @@ -29,6 +34,7 @@ type StorePublishConfiguration = { storageEngine?: ItemType.ipfs | ItemType.storage; inlineRequested?: boolean; APIServer?: string; + sync?: boolean; }; /** @@ -45,26 +51,68 @@ export async function Publish({ channel, fileHash, fileObject, + sync = false, }: RequireOnlyOne): Promise { if (!fileObject && !fileHash) throw new Error("You need to specify a File to upload or a Hash to pin."); if (fileObject && fileHash) throw new Error("You can't pin a file and upload it at the same time."); - if (fileHash && storageEngine !== ItemType.ipfs) throw new Error("You must choose ipfs to pin file."); + if (fileHash && storageEngine !== ItemType.ipfs) throw new Error("You must choose ipfs to pin the file."); - const hash = - fileHash || - (await PushFileToStorageEngine({ + const myHash = await getHash(fileObject, storageEngine, fileHash, APIServer); + + const message = await createAndSendStoreMessage( + account, + channel, + myHash, + storageEngine, + APIServer, + inlineRequested, + sync, + fileObject, + ); + + return message; +} + +async function getHash( + fileObject: Buffer | Blob | null | undefined, + storageEngine: ItemType, + fileHash: string | undefined, + APIServer: string, +) { + if (fileObject && storageEngine !== ItemType.ipfs) { + const hash = await processFileObject(fileObject); + if (hash === null || hash === undefined) { + throw new Error("Cannot process file"); + } + return hash; + } else if (fileObject && storageEngine === ItemType.ipfs) { + return await PushFileToStorageEngine({ APIServer, storageEngine, - // eslint-disable-next-line @typescript-eslint/ban-ts-comment - // @ts-ignore file: fileObject, - })); + }); + } else if (fileHash) { + return fileHash; + } else { + throw new Error("Error with File Hash"); + } +} +async function createAndSendStoreMessage( + account: base.Account, + channel: string, + myHash: string, + storageEngine: ItemType, + APIServer: string, + inlineRequested: boolean, + sync: boolean, + fileObject: Buffer | Blob | undefined, +) { const timestamp = Date.now() / 1000; const storeContent: StoreContent = { address: account.address, item_type: storageEngine, - item_hash: hash, + item_hash: myHash, time: timestamp, }; @@ -84,11 +132,63 @@ export async function Publish({ APIServer, }); - await SignAndBroadcast({ - message: message, - account, - APIServer, - }); + if (ItemType.ipfs === message.item_type) { + await SignAndBroadcast({ + message: message, + account, + APIServer, + }); + } else if (!fileObject) { + throw new Error("You need to specify a File to upload or a Hash to pin."); + } else { + return await sendMessage( + { + message: message, + account, + APIServer, + sync, + }, + fileObject, + ); + } return message; } + +async function processFileObject(fileObject: Blob | Buffer | null): Promise { + if (!fileObject) throw new Error("fileObject is null"); + + if (fileObject instanceof Blob) { + fileObject = await blobToBuffer(fileObject); + } + return calculateSHA256Hash(fileObject); +} + +type SignAndBroadcastConfiguration = { + message: BaseMessage; + account: Account; + APIServer: string; + sync: boolean; +}; + +async function sendMessage(configuration: SignAndBroadcastConfiguration, fileObject: Blob | Buffer) { + const form = new FormData(); + const metadata = { + message: { + ...configuration.message, + signature: await configuration.account.Sign(configuration.message), + }, + sync: configuration.sync, + }; + + form.append("file", fileObject); + form.append("metadata", JSON.stringify(metadata)); + + const response = await axios.post(`${stripTrailingSlash(configuration.APIServer)}/api/v0/storage/add_file`, form, { + headers: { + Accept: "application/json", + "Content-Type": "multipart/form-data", + }, + }); + return response.data; +} diff --git a/src/messages/store/utils.ts b/src/messages/store/utils.ts new file mode 100644 index 00000000..5f0b649e --- /dev/null +++ b/src/messages/store/utils.ts @@ -0,0 +1,20 @@ +import shajs from "sha.js"; + +export async function blobToBuffer(blob: Blob): Promise { + return new Promise((resolve, reject) => { + const reader = new FileReader(); + reader.onload = () => { + if (reader.result instanceof ArrayBuffer) { + resolve(Buffer.from(reader.result)); + } else { + reject("Failed to convert Blob to Buffer."); + } + }; + reader.readAsArrayBuffer(blob); + }); +} + +export function calculateSHA256Hash(data: ArrayBuffer | Buffer): string { + const buffer = Buffer.from(data); + return new shajs.sha256().update(buffer).digest("hex"); +}