diff --git a/.env.template b/.env.template index 4baea95c..2cef71c1 100644 --- a/.env.template +++ b/.env.template @@ -2,11 +2,8 @@ DRIVE_URL=https://drive.internxt.com DRIVE_API_URL=https://drive.internxt.com/api DRIVE_NEW_API_URL=https://api.internxt.com/drive PAYMENTS_API_URL=https://api.internxt.com/payments -PHOTOS_API_URL=https://photos.internxt.com/api NETWORK_URL=https://api.internxt.com APP_CRYPTO_SECRET=6KYQBP847D4ATSFA APP_CRYPTO_SECRET2=8Q8VMUE3BJZV87GT APP_MAGIC_IV=d139cb9a2cd17092e79e1861cf9d7023 -APP_MAGIC_SALT=38dce0391b49efba88dbc8c39ebf868f0267eb110bb0012ab27dc52a528d61b1d1ed9d76f400ff58e3240028442b1eab9bb84e111d9dadd997982dbde9dbd25e -RUDDERSTACK_WRITE_KEY= -RUDDERSTACK_DATAPLANE_URL= \ No newline at end of file +APP_MAGIC_SALT=38dce0391b49efba88dbc8c39ebf868f0267eb110bb0012ab27dc52a528d61b1d1ed9d76f400ff58e3240028442b1eab9bb84e111d9dadd997982dbde9dbd25e \ No newline at end of file diff --git a/.github/workflows/publish.yml b/.github/workflows/publish.yml index 6ff180fe..3128bf61 100644 --- a/.github/workflows/publish.yml +++ b/.github/workflows/publish.yml @@ -27,14 +27,11 @@ jobs: echo "DRIVE_API_URL=https://drive.internxt.com/api" >> .env echo "DRIVE_NEW_API_URL=https://api.internxt.com/drive" >> .env echo "PAYMENTS_API_URL=https://api.internxt.com/payments" >> .env - echo "PHOTOS_API_URL=https://photos.internxt.com/api" >> .env echo "NETWORK_URL=https://api.internxt.com" >> .env echo "APP_CRYPTO_SECRET=6KYQBP847D4ATSFA" >> .env echo "APP_CRYPTO_SECRET2=8Q8VMUE3BJZV87GT" >> .env echo "APP_MAGIC_IV=d139cb9a2cd17092e79e1861cf9d7023" >> .env echo "APP_MAGIC_SALT=38dce0391b49efba88dbc8c39ebf868f0267eb110bb0012ab27dc52a528d61b1d1ed9d76f400ff58e3240028442b1eab9bb84e111d9dadd997982dbde9dbd25e" >> .env - echo "RUDDERSTACK_WRITE_KEY=" >> .env - echo "RUDDERSTACK_DATAPLANE_URL=" >> .env echo "NODE_ENV=production" >> .env - run: yarn run build diff --git a/src/types/config.types.ts b/src/types/config.types.ts index 9cc43e16..3ae05aee 100644 --- a/src/types/config.types.ts +++ b/src/types/config.types.ts @@ -3,12 +3,9 @@ export interface ConfigKeys { readonly DRIVE_API_URL: string; readonly DRIVE_NEW_API_URL: string; readonly PAYMENTS_API_URL: string; - readonly PHOTOS_API_URL: string; readonly APP_CRYPTO_SECRET: string; readonly APP_CRYPTO_SECRET2: string; readonly APP_MAGIC_IV: string; readonly APP_MAGIC_SALT: string; readonly NETWORK_URL: string; - readonly RUDDERSTACK_WRITE_KEY: string; - readonly RUDDERSTACK_DATAPLANE_URL: string; } diff --git a/src/utils/webdav.utils.ts b/src/utils/webdav.utils.ts index ca989fc4..85323c97 100644 --- a/src/utils/webdav.utils.ts +++ b/src/utils/webdav.utils.ts @@ -7,8 +7,9 @@ import { DriveDatabaseManager } from '../services/database/drive-database-manage import { DriveFolderService } from '../services/drive/drive-folder.service'; import { DriveFileService } from '../services/drive/drive-file.service'; import { DriveFileItem, DriveFolderItem } from '../types/drive.types'; -import { NotFoundError } from './errors.utils'; +import { ConflictError, NotFoundError } from './errors.utils'; import { webdavLogger } from './logger.utils'; +import AppError from '@internxt/sdk/dist/shared/types/errors'; export class WebDavUtils { static joinURL(...pathComponents: string[]): string { @@ -34,6 +35,7 @@ export class WebDavUtils { } else { requestUrl = urlObject.url; } + const decodedUrl = decodeURIComponent(requestUrl).replaceAll('/./', '/'); const parsedPath = path.parse(decodedUrl); let parentPath = path.dirname(decodedUrl); @@ -95,8 +97,21 @@ export class WebDavUtils { driveFileService?: DriveFileService, ): Promise { let item: DriveFileItem | DriveFolderItem | undefined = undefined; + if (resource.type === 'folder') { - item = await driveFolderService?.getFolderMetadataByPath(resource.url); + // if resource has a parentPath it means it's a subfolder then try to get it; if it throws an error it means it doesn't + // exist and we should throw a 409 error in compliance with the WebDAV RFC + // catch the error during getting parent folder and throw a 409 error in compliance with the WebDAV RFC + try { + item = await driveFolderService?.getFolderMetadataByPath(resource.url); + } catch (error) { + // if the error is a 404 error, it means the resource doesn't exist + // in this case, throw a 409 error in compliance with the WebDAV RFC + if ((error as AppError).status === 404) { + throw new ConflictError(`Resource not found on Internxt Drive at ${resource.url}`); + } + throw error; + } } if (resource.type === 'file') { item = await driveFileService?.getFileMetadataByPath(resource.url); diff --git a/src/webdav/handlers/MKCOL.handler.ts b/src/webdav/handlers/MKCOL.handler.ts index 44578f94..04c04830 100644 --- a/src/webdav/handlers/MKCOL.handler.ts +++ b/src/webdav/handlers/MKCOL.handler.ts @@ -7,6 +7,8 @@ import { webdavLogger } from '../../utils/logger.utils'; import { XMLUtils } from '../../utils/xml.utils'; import { AsyncUtils } from '../../utils/async.utils'; import { DriveFolderItem } from '../../types/drive.types'; +import { MethodNotAllowed } from '../../utils/errors.utils'; +import AppError from '@internxt/sdk/dist/shared/types/errors'; export class MKCOLRequestHandler implements WebDavMethodHandler { constructor( @@ -30,6 +32,22 @@ export class MKCOLRequestHandler implements WebDavMethodHandler { driveFolderService, })) as DriveFolderItem; + let folderAlreadyExists = true; + // try to get the folder from the drive before creating it + // The method getFolderMetadataByPath will throw an error if the folder does not exist, so we need to catch it + try { + await driveFolderService.getFolderMetadataByPath(resource.url); + } catch (error) { + if ((error as AppError).status === 404) { + folderAlreadyExists = false; + } + } + + if (folderAlreadyExists) { + webdavLogger.info(`[MKCOL] ❌ Folder '${resource.url}' already exists`); + throw new MethodNotAllowed('Folder already exists'); + } + const [createFolder] = driveFolderService.createFolder({ plainName: resource.path.base, parentFolderUuid: parentFolderItem.uuid, diff --git a/src/webdav/handlers/PUT.handler.ts b/src/webdav/handlers/PUT.handler.ts index 214b9c55..ca85bede 100644 --- a/src/webdav/handlers/PUT.handler.ts +++ b/src/webdav/handlers/PUT.handler.ts @@ -154,6 +154,6 @@ export class PUTRequestHandler implements WebDavMethodHandler { await driveDatabaseManager.createFile(file, resource.path.dir + '/'); - res.status(200).send(); + res.status(201).send(); }; } diff --git a/src/webdav/middewares/mkcol.middleware.ts b/src/webdav/middewares/mkcol.middleware.ts new file mode 100644 index 00000000..5d613e30 --- /dev/null +++ b/src/webdav/middewares/mkcol.middleware.ts @@ -0,0 +1,25 @@ +import { RequestHandler } from 'express'; +import { UnsupportedMediaTypeError } from '../../utils/errors.utils'; + +export const MkcolMiddleware: RequestHandler = (req, _, next) => { + // if request content types are not 'application/xml', 'text/xml' or not defined, return 415 + if (req.method === 'MKCOL') { + let contentType = req.headers['Content-Type'] ?? req.get('content-type'); + if (contentType && contentType.length > 0) { + if (Array.isArray(contentType)) { + contentType = contentType[0]; + } + contentType = contentType.toLowerCase().trim(); + + if (contentType !== 'application/xml' && contentType !== 'text/xml') { + throw new UnsupportedMediaTypeError('Unsupported Media Type'); + } + } + // body must be empty + if (req.body && Object.keys(req.body).length > 0) { + throw new UnsupportedMediaTypeError('Unsupported Media Type'); + } + } + + next(); +}; diff --git a/src/webdav/webdav-server.ts b/src/webdav/webdav-server.ts index 96531915..c743fa71 100644 --- a/src/webdav/webdav-server.ts +++ b/src/webdav/webdav-server.ts @@ -29,6 +29,7 @@ import { MOVERequestHandler } from './handlers/MOVE.handler'; import { COPYRequestHandler } from './handlers/COPY.handler'; import { TrashService } from '../services/drive/trash.service'; import { Environment } from '@internxt/inxt-js'; +import { MkcolMiddleware } from './middewares/mkcol.middleware'; export class WebDavServer { constructor( @@ -68,7 +69,6 @@ export class WebDavServer { }; private readonly registerMiddlewares = async () => { - this.app.use(bodyParser.text({ type: ['application/xml', 'text/xml'] })); this.app.use(ErrorHandlingMiddleware); this.app.use(AuthMiddleware(AuthService.instance)); this.app.use( @@ -76,6 +76,8 @@ export class WebDavServer { enable: true, }), ); + this.app.use(bodyParser.text({ type: ['application/xml', 'text/xml'] })); + this.app.use(MkcolMiddleware); }; private readonly registerHandlers = async () => { diff --git a/test/utils/webdav.utils.test.ts b/test/utils/webdav.utils.test.ts index f2cff85b..55713c81 100644 --- a/test/utils/webdav.utils.test.ts +++ b/test/utils/webdav.utils.test.ts @@ -11,7 +11,8 @@ import { newFileItem, newFolderItem } from '../fixtures/drive.fixture'; import { DriveFolderService } from '../../src/services/drive/drive-folder.service'; import { DriveFileService } from '../../src/services/drive/drive-file.service'; import { fail } from 'node:assert'; -import { NotFoundError } from '../../src/utils/errors.utils'; +import { ConflictError, NotFoundError } from '../../src/utils/errors.utils'; +import AppError from '@internxt/sdk/dist/shared/types/errors'; describe('Webdav utils', () => { beforeEach(() => { @@ -260,6 +261,23 @@ describe('Webdav utils', () => { expect(findFileStub).not.toHaveBeenCalled(); }); + it('When folder is not found, then it throws ConflictError', async () => { + const findFolderStub = vi + .spyOn(DriveFolderService.instance, 'getFolderMetadataByPath') + .mockRejectedValue(new AppError('Folder not found', 404)); + const findFileStub = vi.spyOn(DriveFileService.instance, 'getFileMetadataByPath').mockRejectedValue(new Error()); + + try { + await WebDavUtils.getDriveItemFromResource(requestFolderFixture, DriveFolderService.instance, undefined); + fail('Expected function to throw an error, but it did not.'); + } catch (error) { + expect(error).to.be.instanceOf(ConflictError); + } + + expect(findFolderStub).toHaveBeenCalledOnce(); + expect(findFileStub).not.toHaveBeenCalled(); + }); + it('When file resource is looked by its path, then it is returned', async () => { const expectedFile = newFileItem(); const findFileStub = vi.spyOn(DriveFileService.instance, 'getFileMetadataByPath').mockResolvedValue(expectedFile); diff --git a/test/webdav/handlers/MKCOL.handler.test.ts b/test/webdav/handlers/MKCOL.handler.test.ts index 4c028b7f..a9c4fa5a 100644 --- a/test/webdav/handlers/MKCOL.handler.test.ts +++ b/test/webdav/handlers/MKCOL.handler.test.ts @@ -13,6 +13,7 @@ import { newCreateFolderResponse, newFolderItem } from '../../fixtures/drive.fix import { WebDavRequestedResource } from '../../../src/types/webdav.types'; import { WebDavUtils } from '../../../src/utils/webdav.utils'; import { DriveFolder } from '../../../src/services/database/drive-folder/drive-folder.domain'; +import AppError from '@internxt/sdk/dist/shared/types/errors'; describe('MKCOL request handler', () => { beforeEach(() => { @@ -61,6 +62,9 @@ describe('MKCOL request handler', () => { const createFolderStub = vi .spyOn(driveFolderService, 'createFolder') .mockReturnValue([Promise.resolve(newFolderResponse), { cancel: () => {} }]); + const getFolderMetadataStub = vi + .spyOn(driveFolderService, 'getFolderMetadataByPath') + .mockRejectedValue(new AppError('Folder not found', 404)); const createDatabaseFolderStub = vi .spyOn(driveDatabaseManager, 'createFolder') .mockResolvedValue({} as DriveFolder); @@ -74,5 +78,6 @@ describe('MKCOL request handler', () => { parentFolderUuid: parentFolder.uuid, }); expect(createDatabaseFolderStub).toHaveBeenCalledOnce(); + expect(getFolderMetadataStub).toHaveBeenCalledOnce(); }); }); diff --git a/test/webdav/handlers/PUT.handler.test.ts b/test/webdav/handlers/PUT.handler.test.ts index 41b81276..bacb7e9d 100644 --- a/test/webdav/handlers/PUT.handler.test.ts +++ b/test/webdav/handlers/PUT.handler.test.ts @@ -138,7 +138,7 @@ describe('PUT request handler', () => { const createDBFileStub = vi.spyOn(driveDatabaseManager, 'createFile').mockResolvedValue(fileFixture); await sut.handle(request, response); - expect(response.status).toHaveBeenCalledWith(200); + expect(response.status).toHaveBeenCalledWith(201); expect(getRequestedResourceStub).toHaveBeenCalledTimes(2); expect(getAndSearchItemFromResourceStub).toHaveBeenCalledTimes(2); expect(getAuthDetailsStub).toHaveBeenCalledOnce(); @@ -206,7 +206,7 @@ describe('PUT request handler', () => { const createDBFileStub = vi.spyOn(driveDatabaseManager, 'createFile').mockResolvedValue(fileFixture); await sut.handle(request, response); - expect(response.status).toHaveBeenCalledWith(200); + expect(response.status).toHaveBeenCalledWith(201); expect(getRequestedResourceStub).toHaveBeenCalledTimes(2); expect(getAndSearchItemFromResourceStub).toHaveBeenCalledTimes(2); expect(getAuthDetailsStub).toHaveBeenCalledOnce(); diff --git a/test/webdav/middlewares/mkcol.middleware.test.ts b/test/webdav/middlewares/mkcol.middleware.test.ts new file mode 100644 index 00000000..282ad3ab --- /dev/null +++ b/test/webdav/middlewares/mkcol.middleware.test.ts @@ -0,0 +1,74 @@ +import { beforeEach, describe, expect, it, vi } from 'vitest'; +import { createWebDavRequestFixture, createWebDavResponseFixture } from '../../fixtures/webdav.fixture'; +import { MkcolMiddleware } from '../../../src/webdav/middewares/mkcol.middleware'; +import { fail } from 'node:assert'; +import { UnsupportedMediaTypeError } from '../../../src/utils/errors.utils'; + +describe('MKCOL middleware', () => { + beforeEach(() => { + vi.restoreAllMocks(); + }); + + it('When MKCOL content is application/xml, then it should call next', () => { + const req = createWebDavRequestFixture({ + method: 'MKCOL', + url: '/anypath', + headers: { 'Content-Type': 'application/xml' }, + }); + const res = createWebDavResponseFixture({}); + const next = vi.fn(); + + MkcolMiddleware(req, res, next); + + expect(next).toHaveBeenCalledExactlyOnceWith(); + }); + + it('When MKCOL content is text/xml, then it should call next', () => { + const req = createWebDavRequestFixture({ + method: 'MKCOL', + url: '/anypath', + headers: [{ 'Content-Type': 'text/xml' }], + }); + const res = createWebDavResponseFixture({}); + const next = vi.fn(); + + MkcolMiddleware(req, res, next); + + expect(next).toHaveBeenCalledExactlyOnceWith(); + }); + + it('When MKCOL content is not XML, then it should call next with error', async () => { + const req = createWebDavRequestFixture({ + method: 'MKCOL', + url: '/anypath', + headers: { 'Content-Type': 'application/json' }, + }); + const res = createWebDavResponseFixture({}); + const next = vi.fn(); + + try { + await MkcolMiddleware(req, res, next); + fail('Expected function to throw an error, but it did not.'); + } catch (error) { + expect(error).to.be.instanceOf(UnsupportedMediaTypeError); + } + }); + + it('When MKCOL has body content, then it should call next with error', async () => { + const req = createWebDavRequestFixture({ + method: 'MKCOL', + url: '/anypath', + headers: { 'Content-Type': 'text/xml' }, + body: '', + }); + const res = createWebDavResponseFixture({}); + const next = vi.fn(); + + try { + await MkcolMiddleware(req, res, next); + fail('Expected function to throw an error, but it did not.'); + } catch (error) { + expect(error).to.be.instanceOf(UnsupportedMediaTypeError); + } + }); +});