Skip to content

Commit

Permalink
Merge pull request #51 from internxt/feat/pb-1339-upload-webdav-content
Browse files Browse the repository at this point in the history
[PB-1339]: Feat/Upload webdav content
  • Loading branch information
PixoDev authored Apr 3, 2024
2 parents 902477f + b410f4d commit de8d251
Show file tree
Hide file tree
Showing 22 changed files with 435 additions and 159 deletions.
2 changes: 2 additions & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -44,6 +44,7 @@
"express-basic-auth": "^1.2.1",
"fast-xml-parser": "^4.3.5",
"mime-types": "^2.1.35",
"node-fetch": "2",
"openpgp": "^5.11.1",
"pm2": "^5.3.1",
"realm": "^12.6.2",
Expand All @@ -60,6 +61,7 @@
"@types/mime-types": "^2.1.4",
"@types/mocha": "^10",
"@types/node": "^18",
"@types/node-fetch": "^2.6.11",
"@types/sinon-chai": "^3.2.12",
"@types/sinon-express-mock": "^1.3.12",
"@types/superagent": "^8.1.3",
Expand Down
3 changes: 1 addition & 2 deletions src/commands/upload.ts
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,6 @@ import { DriveFileService } from '../services/drive/drive-file.service';
import { UploadService } from '../services/network/upload.service';
import { CryptoService } from '../services/crypto.service';
import { DownloadService } from '../services/network/download.service';
import { StreamUtils } from '../utils/stream.utils';
import { ErrorUtils } from '../utils/errors.utils';
import { DriveFolderService } from '../services/drive/drive-folder.service';

Expand Down Expand Up @@ -76,7 +75,7 @@ export default class Upload extends Command {
user.bucket,
mnemonic,
stat.size,
StreamUtils.readStreamToReadableStream(fileStream),
fileStream,
{
progressCallback: (progress) => {
progressBar.update(progress);
Expand Down
9 changes: 8 additions & 1 deletion src/services/auth.service.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ import { CryptoService } from './crypto.service';
import { ConfigService } from './config.service';
import { LoginCredentials } from '../types/command.types';
import { ValidationService } from './validation.service';
import { UserSettings } from '@internxt/sdk/dist/shared/types/userSettings';

export class AuthService {
public static readonly instance: AuthService = new AuthService();
Expand Down Expand Up @@ -88,7 +89,12 @@ export class AuthService {
*
* @returns The user plain mnemonic and the auth tokens
*/
public getAuthDetails = async (): Promise<{ token: string; newToken: string; mnemonic: string }> => {
public getAuthDetails = async (): Promise<{
token: string;
newToken: string;
mnemonic: string;
user: UserSettings;
}> => {
const loginCredentials = await ConfigService.instance.readUser();
if (!loginCredentials) {
throw new Error('Credentials not found, please login first');
Expand Down Expand Up @@ -116,6 +122,7 @@ export class AuthService {
token,
newToken,
mnemonic,
user: loginCredentials.user,
};
};
}
55 changes: 4 additions & 51 deletions src/services/crypto.service.ts
Original file line number Diff line number Diff line change
@@ -1,9 +1,10 @@
import { CryptoProvider } from '@internxt/sdk';
import { Keys, Password } from '@internxt/sdk/dist/auth';
import crypto, { Cipher, createCipheriv, createDecipheriv, createHash } from 'crypto';
import crypto, { createCipheriv, createDecipheriv } from 'node:crypto';
import { KeysService } from './keys.service';
import { ConfigService } from '../services/config.service';
import { StreamUtils } from '../utils/stream.utils';
import { Transform } from 'stream';

export class CryptoService {
public static readonly instance: CryptoService = new CryptoService();
Expand Down Expand Up @@ -109,29 +110,6 @@ export class CryptoService {
return Buffer.concat([decipher.update(contentsToDecrypt), decipher.final()]).toString('utf8');
};

private encryptReadable(readable: ReadableStream<Uint8Array>, cipher: Cipher): ReadableStream<Uint8Array> {
const reader = readable.getReader();

const encryptedFileReadable = new ReadableStream({
async start(controller) {
let done = false;

while (!done) {
const status = await reader.read();

if (!status.done) {
controller.enqueue(cipher.update(status.value));
}

done = status.done;
}
controller.close();
},
});

return encryptedFileReadable;
}

public async decryptStream(inputSlices: ReadableStream<Uint8Array>[], key: Buffer, iv: Buffer) {
const decipher = createDecipheriv('aes-256-ctr', key, iv);
const encryptedStream = StreamUtils.joinReadableBinaryStreams(inputSlices);
Expand Down Expand Up @@ -161,36 +139,11 @@ export class CryptoService {
return decryptedStream;
}

public async encryptStream(
input: ReadableStream<Uint8Array>,
key: Buffer,
iv: Buffer,
): Promise<{ blob: Blob; hash: Buffer }> {
public async getEncryptionTransform(key: Buffer, iv: Buffer): Promise<Transform> {
const cipher = createCipheriv('aes-256-ctr', key, iv);

const readable = this.encryptReadable(input, cipher).getReader();
const hasher = createHash('sha256');
const blobParts: ArrayBuffer[] = [];

let done = false;

while (!done) {
const status = await readable.read();

if (!status.done) {
hasher.update(status.value);
blobParts.push(status.value);
}

done = status.done;
}

return {
blob: new Blob(blobParts, { type: 'application/octet-stream' }),
hash: createHash('ripemd160').update(Buffer.from(hasher.digest())).digest(),
};
return cipher;
}

/**
* Generates the key and the iv by transforming a secret and a salt.
* It will generate the same key and iv if the same secret and salt is used.
Expand Down
2 changes: 1 addition & 1 deletion src/services/drive/drive-file.service.ts
Original file line number Diff line number Diff line change
Expand Up @@ -36,7 +36,7 @@ export class DriveFileService {
});

return {
size: driveFile.size,
size: Number(driveFile.size),
uuid: driveFile.uuid,
encryptedName,
name: payload.name,
Expand Down
28 changes: 16 additions & 12 deletions src/services/network/network-facade.service.ts
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,9 @@ import { CryptoService } from '../crypto.service';
import { UploadService } from './upload.service';
import { DownloadService } from './download.service';
import { ValidationService } from '../validation.service';
import { Readable } from 'node:stream';
import { Transform } from 'stream';
import { HashStream } from '../../utils/hash.utils';

export class NetworkFacade {
private readonly cryptoLib: Network.Crypto;
Expand Down Expand Up @@ -122,12 +125,13 @@ export class NetworkFacade {
bucketId: string,
mnemonic: string,
size: number,
from: ReadableStream<Uint8Array>,
from: Readable,
options?: UploadOptions,
): Promise<[Promise<{ fileId: string; hash: Buffer }>, AbortController]> {
const hashStream = new HashStream();
const abortable = options?.abortController ?? new AbortController();
let fileHash: Buffer;
let encryptedBlob: Blob;
let encryptionTransform: Transform;

const onProgress: UploadProgressCallback = (progress: number) => {
if (!options?.progressCallback) return;
Expand All @@ -139,23 +143,23 @@ export class NetworkFacade {
};

const encryptFile: EncryptFileFunction = async (_, key, iv) => {
const { blob, hash } = await this.cryptoService.encryptStream(
from,
Buffer.from(key as ArrayBuffer),
Buffer.from(iv as ArrayBuffer),
);

fileHash = hash;
encryptedBlob = blob;
encryptionTransform = from
.pipe(
await this.cryptoService.getEncryptionTransform(
Buffer.from(key as ArrayBuffer),
Buffer.from(iv as ArrayBuffer),
),
)
.pipe(hashStream);
};

const uploadFile: UploadFileFunction = async (url) => {
await this.uploadService.uploadFile(url, encryptedBlob, {
await this.uploadService.uploadFile(url, encryptionTransform, {
abortController: abortable,
progressCallback: onUploadProgress,
});

return fileHash.toString('hex');
return hashStream.getHash().toString('hex');
};
const uploadOperation = async () => {
const uploadResult = await NetworkUpload.uploadFile(
Expand Down
25 changes: 5 additions & 20 deletions src/services/network/upload.service.ts
Original file line number Diff line number Diff line change
@@ -1,29 +1,14 @@
import { Readable } from 'node:stream';
import { UploadOptions } from '../../types/network.types';
import superagent from 'superagent';

import fetch from 'node-fetch';
export class UploadService {
public static readonly instance: UploadService = new UploadService();

async uploadFile(url: string, data: Blob, options: UploadOptions): Promise<{ etag: string }> {
const request = superagent
.put(url)
.set('Content-Length', data.size.toString())
.set('Content-Type', data.type)
.send(Buffer.from(await data.arrayBuffer()))
.on('progress', (progressEvent) => {
if (options.progressCallback && progressEvent.total) {
const reportedProgress = progressEvent.loaded / parseInt(progressEvent.total);
options.progressCallback(reportedProgress);
}
});
async uploadFile(url: string, from: Readable, options: UploadOptions): Promise<{ etag: string }> {
const response = await fetch(url, { method: 'PUT', body: from, signal: options.abortController?.signal });

options.abortController?.signal.addEventListener('abort', () => {
request.abort();
});

const response = await request;

const etag = response.headers.etag;
const etag = response.headers.get('etag');
options.progressCallback(1);
if (!etag) {
throw new Error('Missing Etag in response when uploading file');
Expand Down
2 changes: 1 addition & 1 deletion src/types/webdav.types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@ export type WebDavMethodHandlerOptions = {
};

export type WebDavRequestedResource = {
type: 'file' | 'folder' | 'root';
type: 'file' | 'folder';
url: string;
name: string;
path: ParsedPath;
Expand Down
10 changes: 10 additions & 0 deletions src/utils/errors.utils.ts
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,16 @@ export class BadRequestError extends Error {
}
}

export class UnsupportedMediaTypeError extends Error {
public statusCode = 415;

constructor(message: string) {
super(message);
this.name = 'UnsupportedMediaTypeError';
Object.setPrototypeOf(this, UnsupportedMediaTypeError.prototype);
}
}

export class NotImplementedError extends Error {
public statusCode = 501;

Expand Down
42 changes: 42 additions & 0 deletions src/utils/hash.utils.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,42 @@
import { createHash, Hash } from 'crypto';
import { Transform, TransformCallback, TransformOptions } from 'stream';

export class HashStream extends Transform {
hasher: Hash;
finalHash: Buffer;

constructor(opts?: TransformOptions) {
super(opts);
this.hasher = createHash('sha256');
this.finalHash = Buffer.alloc(0);
}

_transform(chunk: Buffer, enc: BufferEncoding, cb: TransformCallback) {
this.hasher.update(chunk);
cb(null, chunk);
}

_flush(cb: (err: Error | null) => void) {
return this.hasher.end(cb);
}

reset() {
this.hasher = createHash('sha256');
}

readHash() {
if (!this.finalHash.length) {
this.finalHash = this.hasher.read();
}

return this.finalHash;
}

getHash() {
if (!this.finalHash.length) {
this.readHash();
}

return createHash('ripemd160').update(this.finalHash).digest();
}
}
6 changes: 3 additions & 3 deletions src/webdav/handlers/PROPFIND.handler.ts
Original file line number Diff line number Diff line change
Expand Up @@ -22,7 +22,7 @@ export class PROPFINDRequestHandler implements WebDavMethodHandler {

switch (resource.type) {
case 'file': {
res.status(200).send(await this.getFileMetaXML(resource));
res.status(207).send(await this.getFileMetaXML(resource));
break;
}

Expand All @@ -39,7 +39,7 @@ export class PROPFINDRequestHandler implements WebDavMethodHandler {
createdAt: new Date(rootFolder.createdAt),
updatedAt: new Date(rootFolder.updatedAt),
});
res.status(200).send(await this.getFolderContentXML('/', rootFolder.uuid, depth, true));
res.status(207).send(await this.getFolderContentXML('/', rootFolder.uuid, depth, true));
break;
}

Expand All @@ -50,7 +50,7 @@ export class PROPFINDRequestHandler implements WebDavMethodHandler {
return;
}

res.status(200).send(await this.getFolderContentXML(resource.url, driveParentFolder.uuid, depth));
res.status(207).send(await this.getFolderContentXML(resource.url, driveParentFolder.uuid, depth));
break;
}
}
Expand Down
Loading

0 comments on commit de8d251

Please sign in to comment.