From 7594fca07112be28ae7eb778e5bcad231ef98d76 Mon Sep 17 00:00:00 2001 From: larry-internxt Date: Tue, 12 Mar 2024 16:08:16 +0100 Subject: [PATCH 01/11] added selfsigned certificates management --- package.json | 3 ++- yarn.lock | 20 ++++++++++++++++++++ 2 files changed, 22 insertions(+), 1 deletion(-) diff --git a/package.json b/package.json index 2ca4c42..93ae58b 100644 --- a/package.json +++ b/package.json @@ -10,7 +10,7 @@ "prepack": "yarn build && oclif manifest && oclif readme", "prepare": "husky", "test:unit": "nyc --reporter=lcov --reporter=text mocha \"test/**/*.test.ts\" --exit", - "dev:webdav": "nodemon -e ts --exec 'ts-node src/webdav/index.ts'", + "dev:webdav": "nodemon -e ts --exec ts-node src/webdav/index.ts", "version": "oclif readme && git add README.md" }, "homepage": "https://github.com/internxt/cli", @@ -44,6 +44,7 @@ "fast-xml-parser": "^4.3.5", "openpgp": "^5.11.1", "realm": "^12.6.2", + "selfsigned": "^2.4.1", "superagent": "^8.1.2", "winston": "^3.12.0" }, diff --git a/yarn.lock b/yarn.lock index 29c0d7a..0d8b158 100644 --- a/yarn.lock +++ b/yarn.lock @@ -2832,6 +2832,13 @@ resolved "https://registry.yarnpkg.com/@types/mocha/-/mocha-10.0.6.tgz#818551d39113081048bdddbef96701b4e8bb9d1b" integrity sha512-dJvrYWxP/UcXm36Qn36fxhUKu8A/xMRXVT2cliFF1Z7UA9liG5Psj3ezNSZw+5puH2czDXRLcXQxf8JbJt0ejg== +"@types/node-forge@^1.3.0": + version "1.3.11" + resolved "https://registry.yarnpkg.com/@types/node-forge/-/node-forge-1.3.11.tgz#0972ea538ddb0f4d9c2fa0ec5db5724773a604da" + integrity sha512-FQx220y22OKNTqaByeBGqHWYz4cl94tpcxeFdvBo3wjG6XPBuZ0BNgNZRV5J5TFmmcsJ4IzsLkmGRiQbnYsBEQ== + dependencies: + "@types/node" "*" + "@types/node@*", "@types/node@^18": version "18.19.17" resolved "https://registry.yarnpkg.com/@types/node/-/node-18.19.17.tgz#a581a9fb4b2cfdbc61f008804f4436b2d5c40354" @@ -7116,6 +7123,11 @@ node-fetch@^2.6.7: dependencies: whatwg-url "^5.0.0" +node-forge@^1: + version "1.3.1" + resolved "https://registry.yarnpkg.com/node-forge/-/node-forge-1.3.1.tgz#be8da2af243b2417d5f646a770663a92b7e9ded3" + integrity sha512-dPEtOeMvF9VMcYV/1Wb8CPoVAXtp6MKMlcbAt4ddqmGqUJ6fQZFXkNZNkNlfevtNkGtaSoXf/vNNNSvgrdXwtA== + node-gyp@^8.2.0: version "8.4.1" resolved "https://registry.yarnpkg.com/node-gyp/-/node-gyp-8.4.1.tgz#3d49308fc31f768180957d6b5746845fbd429937" @@ -8297,6 +8309,14 @@ scoped-regex@^2.0.0: resolved "https://registry.yarnpkg.com/scoped-regex/-/scoped-regex-2.1.0.tgz#7b9be845d81fd9d21d1ec97c61a0b7cf86d2015f" integrity sha512-g3WxHrqSWCZHGHlSrF51VXFdjImhwvH8ZO/pryFH56Qi0cDsZfylQa/t0jCzVQFNbNvM00HfHjkDPEuarKDSWQ== +selfsigned@^2.4.1: + version "2.4.1" + resolved "https://registry.yarnpkg.com/selfsigned/-/selfsigned-2.4.1.tgz#560d90565442a3ed35b674034cec4e95dceb4ae0" + integrity sha512-th5B4L2U+eGLq1TVh7zNRGBapioSORUeymIydxgFpwww9d2qyKvtuPU2jJuHvYAwwqi2Y596QBL3eEqcPEYL8Q== + dependencies: + "@types/node-forge" "^1.3.0" + node-forge "^1" + "semver@2 || 3 || 4 || 5": version "5.7.2" resolved "https://registry.yarnpkg.com/semver/-/semver-5.7.2.tgz#48d55db737c3287cd4835e17fa13feace1c41ef8" From 1e5952547b2d399cccd808c62bc6ec0cf04fa892 Mon Sep 17 00:00:00 2001 From: larry-internxt Date: Tue, 12 Mar 2024 16:09:15 +0100 Subject: [PATCH 02/11] fixed crypto utils test breaking sandbox of other tests --- test/utils/crypto.utils.test.ts | 10 ++++++++-- 1 file changed, 8 insertions(+), 2 deletions(-) diff --git a/test/utils/crypto.utils.test.ts b/test/utils/crypto.utils.test.ts index 445a0c4..2b0c671 100644 --- a/test/utils/crypto.utils.test.ts +++ b/test/utils/crypto.utils.test.ts @@ -1,15 +1,21 @@ import { expect } from 'chai'; -import { CryptoUtils } from '../../src/utils/crypto.utils'; import sinon from 'sinon'; +import crypto from 'node:crypto'; +import { CryptoUtils } from '../../src/utils/crypto.utils'; import { ConfigService } from '../../src/services/config.service'; import { AesInit } from '../../src/types/keys.types'; -import crypto from 'node:crypto'; + describe('Crypto utils', () => { const sandbox = sinon.createSandbox(); const aesInit: AesInit = { iv: crypto.randomBytes(16).toString('hex'), salt: crypto.randomBytes(64).toString('hex'), }; + + afterEach(() => { + sandbox.restore(); + }); + it('When Magic IV or Magic Salt are missing should throw an error', async () => { try { CryptoUtils.getAesInit(); From a4789ab559429c2621bfa6bf77ee0e9ba0709229 Mon Sep 17 00:00:00 2001 From: larry-internxt Date: Tue, 12 Mar 2024 16:09:44 +0100 Subject: [PATCH 03/11] added ssl webdav support --- src/webdav/webdav-server.ts | 21 +++++++++++--- test/webdav/webdav-server.test.ts | 47 +++++++++++++++++++++++++++---- 2 files changed, 59 insertions(+), 9 deletions(-) diff --git a/src/webdav/webdav-server.ts b/src/webdav/webdav-server.ts index 728f4e2..61ad0de 100644 --- a/src/webdav/webdav-server.ts +++ b/src/webdav/webdav-server.ts @@ -1,4 +1,6 @@ import { Express } from 'express'; +import https from 'https'; +import selfsigned from 'selfsigned'; import { ConfigService } from '../services/config.service'; import { OPTIONSRequestHandler } from './handlers/OPTIONS.handler'; import { PROPFINDRequestHandler } from './handlers/PROPFIND.handler'; @@ -41,15 +43,26 @@ export class WebDavServer { ); }; - async start() { + start() { const port = this.configService.get('WEBDAV_SERVER_PORT'); this.app.disable('x-powered-by'); this.registerMiddlewares(); this.registerHandlers(); - this.app.listen(port, () => { - webdavLogger.info(`Internxt WebDav server listening at http://localhost:${port}`); - }); + const attrs = [{ name: 'internxt-cli', value: 'Internxt CLI', type: 'commonName' }]; + const pems = selfsigned.generate(attrs, { days: 365, algorithm: 'sha256', keySize: 2048 }); + + https + .createServer( + { + cert: pems.cert, + key: pems.private, + }, + this.app, + ) + .listen(port, () => { + webdavLogger.info(`Internxt WebDav server listening at https://localhost:${port}`); + }); } } diff --git a/test/webdav/webdav-server.test.ts b/test/webdav/webdav-server.test.ts index 1a5d89b..09c8971 100644 --- a/test/webdav/webdav-server.test.ts +++ b/test/webdav/webdav-server.test.ts @@ -1,10 +1,15 @@ import { expect } from 'chai'; import express from 'express'; import sinon from 'sinon'; +import { randomBytes, randomInt } from 'crypto'; +import https, { Server } from 'https'; +import selfsigned from 'selfsigned'; import { ConfigService } from '../../src/services/config.service'; import { DriveFolderService } from '../../src/services/drive/drive-folder.service'; import { WebDavServer } from '../../src/webdav/webdav-server'; import { getDriveRealmManager } from '../fixtures/drive-realm.fixture'; +import { ConfigKeys } from '../../src/types/config.types'; + describe('WebDav server', () => { const sandbox = sinon.createSandbox(); @@ -12,17 +17,49 @@ describe('WebDav server', () => { sandbox.restore(); }); - it('When the WebDav server is started, should listen on the specified port', () => { + it('When the WebDav server is started, should listen on the specified port using https', () => { + const envEndpoint: { key: keyof ConfigKeys; value: string } = { + key: 'WEBDAV_SERVER_PORT', + value: randomInt(65535).toString(), + }; + + sandbox.stub(ConfigService.instance, 'get').withArgs(envEndpoint.key).returns(envEndpoint.value); + // @ts-expect-error - We stub the method partially + const createServerStub = sandbox.stub(https, 'createServer').returns({ + listen: sandbox.stub().resolves(), + }); + const app = express(); const server = new WebDavServer(app, ConfigService.instance, DriveFolderService.instance, getDriveRealmManager()); + server.start(); - // @ts-expect-error - We are faking partially the listen method - const listenStub = sandbox.stub(app, 'listen').callsFake((callback) => { - callback(); + expect(createServerStub.calledOnce).to.be.true; + }); + + it('When the WebDav server is started, it should generate self-signed certificates', () => { + const envEndpoint: { key: keyof ConfigKeys; value: string } = { + key: 'WEBDAV_SERVER_PORT', + value: randomInt(65535).toString(), + }; + const sslSelfSigned = { + private: randomBytes(8).toString('hex'), + public: randomBytes(8).toString('hex'), + cert: randomBytes(8).toString('hex'), + fingerprint: randomBytes(8).toString('hex'), + }; + + sandbox.stub(ConfigService.instance, 'get').withArgs(envEndpoint.key).returns(envEndpoint.value); + // @ts-expect-error - We stub the method partially + const createServerStub = sandbox.stub(https, 'createServer').returns({ + listen: sandbox.stub().resolves(), }); + // @ts-expect-error - We only mock the properties we need + sandbox.stub(selfsigned, 'generate').returns(sslSelfSigned); + const app = express(); + const server = new WebDavServer(app, ConfigService.instance, DriveFolderService.instance, getDriveRealmManager()); server.start(); - expect(listenStub.calledOnce).to.be.true; + expect(createServerStub).to.be.calledOnceWith({ cert: sslSelfSigned.cert, key: sslSelfSigned.private }); }); }); From 3535cbc0db38b610b323a977008df9be00e7e54f Mon Sep 17 00:00:00 2001 From: larry-internxt Date: Tue, 12 Mar 2024 16:15:57 +0100 Subject: [PATCH 04/11] restored async start function --- src/webdav/webdav-server.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/webdav/webdav-server.ts b/src/webdav/webdav-server.ts index 61ad0de..56cce64 100644 --- a/src/webdav/webdav-server.ts +++ b/src/webdav/webdav-server.ts @@ -43,7 +43,7 @@ export class WebDavServer { ); }; - start() { + async start() { const port = this.configService.get('WEBDAV_SERVER_PORT'); this.app.disable('x-powered-by'); From e594cb1c0511793c86fdcba56714df925bfd6cfe Mon Sep 17 00:00:00 2001 From: larry-internxt Date: Thu, 14 Mar 2024 11:40:23 +0100 Subject: [PATCH 05/11] fixed path join for windows --- src/services/realms/drive-realm-manager.service.ts | 10 +++++----- src/utils/webdav.utils.ts | 4 +++- src/webdav/handlers/PROPFIND.handler.ts | 4 ++-- .../realms/drive-realm-manager.service.test.ts | 4 ++-- test/utils/webdav.utils.test.ts | 4 ++-- 5 files changed, 14 insertions(+), 12 deletions(-) diff --git a/src/services/realms/drive-realm-manager.service.ts b/src/services/realms/drive-realm-manager.service.ts index 0ff2d5f..5e0ac88 100644 --- a/src/services/realms/drive-realm-manager.service.ts +++ b/src/services/realms/drive-realm-manager.service.ts @@ -1,7 +1,7 @@ -import path from 'path'; import { DriveFileItem, DriveFolderItem } from '../../types/drive.types'; import { DriveFileRealmSchema, DriveFilesRealm } from './drive-files.realm'; import { DriveFolderRealmSchema, DriveFoldersRealm } from './drive-folders.realm'; +import { WebDavUtils } from '../../utils/webdav.utils'; export class DriveRealmManager { constructor( @@ -40,23 +40,23 @@ export class DriveRealmManager { const parentFolder = await this.driveFoldersRealm.findByParentId(parentId); if (!parentFolder) { - return path.join('/', fileName); + return WebDavUtils.joinPath('/', fileName); } const parentPath = await this.buildRelativePathForFile(parentFolder.name, parentFolder.parent_id ?? null); - return path.join(parentPath, fileName); + return WebDavUtils.joinPath(parentPath, fileName); } async buildRelativePathForFolder(folderName: string, parentId: number | null): Promise { const parentFolder = await this.driveFoldersRealm.findByParentId(parentId); if (!parentFolder) { - return path.join('/', folderName, '/'); + return WebDavUtils.joinPath('/', folderName, '/'); } const parentPath = await this.buildRelativePathForFolder(parentFolder.name, parentFolder.parent_id ?? null); - return path.join(parentPath, folderName, '/'); + return WebDavUtils.joinPath(parentPath, folderName, '/'); } } diff --git a/src/utils/webdav.utils.ts b/src/utils/webdav.utils.ts index b11e4d6..1bf7fc7 100644 --- a/src/utils/webdav.utils.ts +++ b/src/utils/webdav.utils.ts @@ -1,10 +1,12 @@ import { Request } from 'express'; import path from 'path'; import { WebDavRequestedResource } from '../types/webdav.types'; + export class WebDavUtils { - static getHref(...pathComponents: string[]): string { + static joinPath(...pathComponents: string[]): string { return path.posix.join(...pathComponents); } + static getRequestedResource(req: Request): WebDavRequestedResource { const decodedUrl = decodeURI(req.url); const parsedPath = path.parse(decodedUrl); diff --git a/src/webdav/handlers/PROPFIND.handler.ts b/src/webdav/handlers/PROPFIND.handler.ts index fbb187a..5a00032 100644 --- a/src/webdav/handlers/PROPFIND.handler.ts +++ b/src/webdav/handlers/PROPFIND.handler.ts @@ -86,7 +86,7 @@ export class PROPFINDRequestHandler implements WebDavMethodHandler { const folderContent = await driveFolderService.getFolderContent(folderUuid); const foldersXML = folderContent.folders.map((folder) => { - const folderRelativePath = WebDavUtils.getHref(relativePath, folder.plainName, '/'); + const folderRelativePath = WebDavUtils.joinPath(relativePath, folder.plainName, '/'); return this.driveFolderItemToXMLNode( { @@ -114,7 +114,7 @@ export class PROPFINDRequestHandler implements WebDavMethodHandler { ); const filesXML = folderContent.files.map((file) => { - const fileRelativePath = WebDavUtils.getHref( + const fileRelativePath = WebDavUtils.joinPath( relativePath, file.type ? `${file.plainName}.${file.type}` : file.plainName, ); diff --git a/test/services/realms/drive-realm-manager.service.test.ts b/test/services/realms/drive-realm-manager.service.test.ts index 12ab2c0..69869c8 100644 --- a/test/services/realms/drive-realm-manager.service.test.ts +++ b/test/services/realms/drive-realm-manager.service.test.ts @@ -90,7 +90,7 @@ describe('DriveRealmManager service', () => { const relativePath = await sut.buildRelativePathForFolder('folderD', 1); - expect(relativePath).to.be.equal(path.join('/', 'folderA', 'folderB', 'folderC', 'folderD', '/')); + expect(relativePath).to.be.equal(path.posix.join('/', 'folderA', 'folderB', 'folderC', 'folderD', '/')); }); it('When a file is created, should build the correct relative path', async () => { @@ -124,6 +124,6 @@ describe('DriveRealmManager service', () => { const relativePath = await sut.buildRelativePathForFile('file.png', 1); - expect(relativePath).to.be.equal(path.join('/', 'folderA', 'folderB', 'folderC', 'file.png')); + expect(relativePath).to.be.equal(path.posix.join('/', 'folderA', 'folderB', 'folderC', 'file.png')); }); }); diff --git a/test/utils/webdav.utils.test.ts b/test/utils/webdav.utils.test.ts index 8fe4cd8..ebaee25 100644 --- a/test/utils/webdav.utils.test.ts +++ b/test/utils/webdav.utils.test.ts @@ -4,12 +4,12 @@ import { createWebDavRequestFixture } from '../fixtures/webdav.fixture'; describe('Webdav utils', () => { it('When a list of path components are given, should generate a correct href', () => { - const href = WebDavUtils.getHref('/path', 'to', 'file'); + const href = WebDavUtils.joinPath('/path', 'to', 'file'); expect(href).to.equal('/path/to/file'); }); it('When a list of path components are given, should generate a correct href and remove incorrect characters', () => { - const href = WebDavUtils.getHref('/path', 'to', 'folder/'); + const href = WebDavUtils.joinPath('/path', 'to', 'folder/'); expect(href).to.equal('/path/to/folder/'); }); From 7e02067c386a7f7e641d7645fb6e034a26f72945 Mon Sep 17 00:00:00 2001 From: larry-internxt Date: Thu, 14 Mar 2024 11:50:00 +0100 Subject: [PATCH 06/11] renamed joinPath to joinURL to avoid confusions --- src/services/realms/drive-realm-manager.service.ts | 8 ++++---- src/utils/webdav.utils.ts | 2 +- src/webdav/handlers/PROPFIND.handler.ts | 4 ++-- test/utils/webdav.utils.test.ts | 4 ++-- 4 files changed, 9 insertions(+), 9 deletions(-) diff --git a/src/services/realms/drive-realm-manager.service.ts b/src/services/realms/drive-realm-manager.service.ts index 5e0ac88..fc8302a 100644 --- a/src/services/realms/drive-realm-manager.service.ts +++ b/src/services/realms/drive-realm-manager.service.ts @@ -40,23 +40,23 @@ export class DriveRealmManager { const parentFolder = await this.driveFoldersRealm.findByParentId(parentId); if (!parentFolder) { - return WebDavUtils.joinPath('/', fileName); + return WebDavUtils.joinURL('/', fileName); } const parentPath = await this.buildRelativePathForFile(parentFolder.name, parentFolder.parent_id ?? null); - return WebDavUtils.joinPath(parentPath, fileName); + return WebDavUtils.joinURL(parentPath, fileName); } async buildRelativePathForFolder(folderName: string, parentId: number | null): Promise { const parentFolder = await this.driveFoldersRealm.findByParentId(parentId); if (!parentFolder) { - return WebDavUtils.joinPath('/', folderName, '/'); + return WebDavUtils.joinURL('/', folderName, '/'); } const parentPath = await this.buildRelativePathForFolder(parentFolder.name, parentFolder.parent_id ?? null); - return WebDavUtils.joinPath(parentPath, folderName, '/'); + return WebDavUtils.joinURL(parentPath, folderName, '/'); } } diff --git a/src/utils/webdav.utils.ts b/src/utils/webdav.utils.ts index 1bf7fc7..9451149 100644 --- a/src/utils/webdav.utils.ts +++ b/src/utils/webdav.utils.ts @@ -3,7 +3,7 @@ import path from 'path'; import { WebDavRequestedResource } from '../types/webdav.types'; export class WebDavUtils { - static joinPath(...pathComponents: string[]): string { + static joinURL(...pathComponents: string[]): string { return path.posix.join(...pathComponents); } diff --git a/src/webdav/handlers/PROPFIND.handler.ts b/src/webdav/handlers/PROPFIND.handler.ts index 5a00032..337337c 100644 --- a/src/webdav/handlers/PROPFIND.handler.ts +++ b/src/webdav/handlers/PROPFIND.handler.ts @@ -86,7 +86,7 @@ export class PROPFINDRequestHandler implements WebDavMethodHandler { const folderContent = await driveFolderService.getFolderContent(folderUuid); const foldersXML = folderContent.folders.map((folder) => { - const folderRelativePath = WebDavUtils.joinPath(relativePath, folder.plainName, '/'); + const folderRelativePath = WebDavUtils.joinURL(relativePath, folder.plainName, '/'); return this.driveFolderItemToXMLNode( { @@ -114,7 +114,7 @@ export class PROPFINDRequestHandler implements WebDavMethodHandler { ); const filesXML = folderContent.files.map((file) => { - const fileRelativePath = WebDavUtils.joinPath( + const fileRelativePath = WebDavUtils.joinURL( relativePath, file.type ? `${file.plainName}.${file.type}` : file.plainName, ); diff --git a/test/utils/webdav.utils.test.ts b/test/utils/webdav.utils.test.ts index ebaee25..53041e3 100644 --- a/test/utils/webdav.utils.test.ts +++ b/test/utils/webdav.utils.test.ts @@ -4,12 +4,12 @@ import { createWebDavRequestFixture } from '../fixtures/webdav.fixture'; describe('Webdav utils', () => { it('When a list of path components are given, should generate a correct href', () => { - const href = WebDavUtils.joinPath('/path', 'to', 'file'); + const href = WebDavUtils.joinURL('/path', 'to', 'file'); expect(href).to.equal('/path/to/file'); }); it('When a list of path components are given, should generate a correct href and remove incorrect characters', () => { - const href = WebDavUtils.joinPath('/path', 'to', 'folder/'); + const href = WebDavUtils.joinURL('/path', 'to', 'folder/'); expect(href).to.equal('/path/to/folder/'); }); From 0713a0570e2a1450f64475b4c5f1022b41c23726 Mon Sep 17 00:00:00 2001 From: larry-internxt Date: Thu, 14 Mar 2024 14:09:49 +0100 Subject: [PATCH 07/11] added ssl certs file management --- src/services/config.service.ts | 9 ++++++++ src/utils/network.utils.ts | 37 +++++++++++++++++++++++++++++++ src/webdav/index.ts | 1 + src/webdav/webdav-server.ts | 19 ++++------------ test/webdav/webdav-server.test.ts | 4 ++-- 5 files changed, 53 insertions(+), 17 deletions(-) diff --git a/src/services/config.service.ts b/src/services/config.service.ts index 2d5428e..294a264 100644 --- a/src/services/config.service.ts +++ b/src/services/config.service.ts @@ -10,6 +10,7 @@ export class ConfigService { static readonly INTERNXT_TMP_DIR = os.tmpdir(); static readonly CREDENTIALS_FILE = path.join(this.INTERNXT_CLI_DATA_DIR, '.inxtcli'); static readonly DRIVE_REALM_FILE = path.join(this.INTERNXT_CLI_DATA_DIR, 'internxt-cli-drive.realm'); + static readonly WEBDAV_SSL_CERTS_DIR = path.join(this.INTERNXT_CLI_DATA_DIR, 'certs'); public static readonly instance: ConfigService = new ConfigService(); /** @@ -75,4 +76,12 @@ export class ConfigService { await fs.mkdir(ConfigService.INTERNXT_CLI_DATA_DIR); } }; + + ensureWebdavCertsDirExists = async () => { + try { + await fs.access(ConfigService.WEBDAV_SSL_CERTS_DIR); + } catch { + await fs.mkdir(ConfigService.WEBDAV_SSL_CERTS_DIR); + } + }; } diff --git a/src/utils/network.utils.ts b/src/utils/network.utils.ts index 590a0e2..1811ed0 100644 --- a/src/utils/network.utils.ts +++ b/src/utils/network.utils.ts @@ -1,5 +1,10 @@ import { NetworkCredentials } from '../types/network.types'; import { createHash } from 'node:crypto'; +import { existsSync, readFileSync, writeFileSync } from 'node:fs'; +import path from 'node:path'; +import selfsigned from 'selfsigned'; +import { ConfigService } from '../services/config.service'; + export class NetworkUtils { static getAuthFromCredentials(creds: NetworkCredentials): { username: string; password: string } { return { @@ -7,4 +12,36 @@ export class NetworkUtils { password: createHash('SHA256').update(Buffer.from(creds.pass)).digest().toString('hex'), }; } + + static readonly WEBDAV_SSL_CERTS_PATH = { + cert: path.join(ConfigService.WEBDAV_SSL_CERTS_DIR, 'cert.crt'), + privateKey: path.join(ConfigService.WEBDAV_SSL_CERTS_DIR, 'priv.key'), + }; + + static getWebdavSSLCerts() { + if (!existsSync(this.WEBDAV_SSL_CERTS_PATH.cert) || !existsSync(this.WEBDAV_SSL_CERTS_PATH.privateKey)) { + const newCerts = this.generateSelfSignedSSLCerts(); + this.saveWebdavSSLCerts(newCerts); + return { + cert: newCerts.cert, + key: newCerts.private, + }; + } else { + return { + cert: readFileSync(this.WEBDAV_SSL_CERTS_PATH.cert), + key: readFileSync(this.WEBDAV_SSL_CERTS_PATH.privateKey), + }; + } + } + + static saveWebdavSSLCerts(pems: selfsigned.GenerateResult): void { + writeFileSync(this.WEBDAV_SSL_CERTS_PATH.cert, pems.cert, 'utf8'); + writeFileSync(this.WEBDAV_SSL_CERTS_PATH.privateKey, pems.private, 'utf8'); + } + + static generateSelfSignedSSLCerts(): selfsigned.GenerateResult { + const attrs = [{ name: 'internxt-cli', value: 'Internxt CLI', type: 'commonName' }]; + const pems = selfsigned.generate(attrs, { days: 365, algorithm: 'sha256', keySize: 2048 }); + return pems; + } } diff --git a/src/webdav/index.ts b/src/webdav/index.ts index d484065..64344ce 100644 --- a/src/webdav/index.ts +++ b/src/webdav/index.ts @@ -17,6 +17,7 @@ dotenv.config(); const init = async () => { await ConfigService.instance.ensureInternxtCliDataDirExists(); + await ConfigService.instance.ensureWebdavCertsDirExists(); const realm = await Realm.open({ path: ConfigService.DRIVE_REALM_FILE, schema: [DriveFileRealmSchema, DriveFolderRealmSchema], diff --git a/src/webdav/webdav-server.ts b/src/webdav/webdav-server.ts index 3877ece..69df8d6 100644 --- a/src/webdav/webdav-server.ts +++ b/src/webdav/webdav-server.ts @@ -1,6 +1,5 @@ import { Express } from 'express'; import https from 'https'; -import selfsigned from 'selfsigned'; import { ConfigService } from '../services/config.service'; import { OPTIONSRequestHandler } from './handlers/OPTIONS.handler'; import { PROPFINDRequestHandler } from './handlers/PROPFIND.handler'; @@ -21,6 +20,7 @@ import { ErrorHandlingMiddleware } from './middewares/errors.middleware'; import asyncHandler from 'express-async-handler'; import { SdkManager } from '../services/sdk-manager.service'; import { NetworkFacade } from '../services/network/network-facade.service'; +import { NetworkUtils } from '../utils/network.utils'; export class WebDavServer { constructor( @@ -94,19 +94,8 @@ export class WebDavServer { this.registerMiddlewares(); this.registerHandlers(); - const attrs = [{ name: 'internxt-cli', value: 'Internxt CLI', type: 'commonName' }]; - const pems = selfsigned.generate(attrs, { days: 365, algorithm: 'sha256', keySize: 2048 }); - - https - .createServer( - { - cert: pems.cert, - key: pems.private, - }, - this.app, - ) - .listen(port, () => { - webdavLogger.info(`Internxt WebDav server listening at https://localhost:${port}`); - }); + https.createServer(NetworkUtils.getWebdavSSLCerts(), this.app).listen(port, () => { + webdavLogger.info(`Internxt WebDav server listening at https://localhost:${port}`); + }); } } diff --git a/test/webdav/webdav-server.test.ts b/test/webdav/webdav-server.test.ts index 82fa852..5d388e6 100644 --- a/test/webdav/webdav-server.test.ts +++ b/test/webdav/webdav-server.test.ts @@ -14,6 +14,7 @@ import { DownloadService } from '../../src/services/network/download.service'; import { AuthService } from '../../src/services/auth.service'; import { CryptoService } from '../../src/services/crypto.service'; import { ConfigKeys } from '../../src/types/config.types'; +import { NetworkUtils } from '../../src/utils/network.utils'; describe('WebDav server', () => { const sandbox = sinon.createSandbox(); @@ -68,8 +69,7 @@ describe('WebDav server', () => { const createServerStub = sandbox.stub(https, 'createServer').returns({ listen: sandbox.stub().resolves(), }); - // @ts-expect-error - We only mock the properties we need - sandbox.stub(selfsigned, 'generate').returns(sslSelfSigned); + sandbox.stub(NetworkUtils, 'getWebdavSSLCerts').returns({ cert: sslSelfSigned.cert, key: sslSelfSigned.private }); const app = express(); const server = new WebDavServer( From b7df74e4fb8001d08fb5b235c99a3428eb6c6708 Mon Sep 17 00:00:00 2001 From: larry-internxt Date: Thu, 14 Mar 2024 14:26:35 +0100 Subject: [PATCH 08/11] removed duplicated test --- test/webdav/webdav-server.test.ts | 29 ----------------------------- 1 file changed, 29 deletions(-) diff --git a/test/webdav/webdav-server.test.ts b/test/webdav/webdav-server.test.ts index 5d388e6..f6c3a8d 100644 --- a/test/webdav/webdav-server.test.ts +++ b/test/webdav/webdav-server.test.ts @@ -23,35 +23,6 @@ describe('WebDav server', () => { sandbox.restore(); }); - it('When the WebDav server is started, should listen on the specified port using https', () => { - const envEndpoint: { key: keyof ConfigKeys; value: string } = { - key: 'WEBDAV_SERVER_PORT', - value: randomInt(65535).toString(), - }; - - sandbox.stub(ConfigService.instance, 'get').withArgs(envEndpoint.key).returns(envEndpoint.value); - // @ts-expect-error - We stub the method partially - const createServerStub = sandbox.stub(https, 'createServer').returns({ - listen: sandbox.stub().resolves(), - }); - - const app = express(); - const server = new WebDavServer( - app, - ConfigService.instance, - DriveFileService.instance, - DriveFolderService.instance, - getDriveRealmManager(), - UploadService.instance, - DownloadService.instance, - AuthService.instance, - CryptoService.instance, - ); - server.start(); - - expect(createServerStub.calledOnce).to.be.true; - }); - it('When the WebDav server is started, it should generate self-signed certificates', () => { const envEndpoint: { key: keyof ConfigKeys; value: string } = { key: 'WEBDAV_SERVER_PORT', From a6a2feee6cd90e2df0e3fe4254515dc324d0a6f4 Mon Sep 17 00:00:00 2001 From: larry-internxt Date: Thu, 14 Mar 2024 15:53:08 +0100 Subject: [PATCH 09/11] added ssl tests --- test/services/config.service.test.ts | 17 +++++++++++- test/utils/network.utils.test.ts | 39 ++++++++++++++++++++++++++++ test/webdav/webdav-server.test.ts | 3 +-- 3 files changed, 56 insertions(+), 3 deletions(-) diff --git a/test/services/config.service.test.ts b/test/services/config.service.test.ts index 8c15cda..a874d0a 100644 --- a/test/services/config.service.test.ts +++ b/test/services/config.service.test.ts @@ -129,7 +129,7 @@ describe('Config service', () => { expect(credentialsFileContent).to.be.empty; }); - it('When user credentials are cleared and the file is empty, an error is thrown', async () => { + it('When user credentials are cleared and the file is empty, then an error is thrown', async () => { configServiceSandbox .stub(fs, 'stat') .withArgs(ConfigService.CREDENTIALS_FILE) @@ -143,4 +143,19 @@ describe('Config service', () => { expect((error as Error).message).to.equal('Credentials file is already empty'); } }); + + it('When webdav certs directory is required to exist, then it is created', async () => { + configServiceSandbox.stub(fs, 'access').withArgs(ConfigService.WEBDAV_SSL_CERTS_DIR).rejects(); + + const stubMkdir = configServiceSandbox.stub(fs, 'mkdir').withArgs(ConfigService.WEBDAV_SSL_CERTS_DIR).resolves(); + + try { + await ConfigService.instance.ensureWebdavCertsDirExists(); + expect(false).to.be.true; + } catch { + /*noop*/ + } + + expect(stubMkdir).to.be.calledOnceWith(ConfigService.WEBDAV_SSL_CERTS_DIR); + }); }); diff --git a/test/utils/network.utils.test.ts b/test/utils/network.utils.test.ts index c750411..7a4e856 100644 --- a/test/utils/network.utils.test.ts +++ b/test/utils/network.utils.test.ts @@ -1,7 +1,21 @@ import { expect } from 'chai'; +import Sinon, { SinonSandbox } from 'sinon'; +import fs from 'fs'; +import { randomBytes } from 'crypto'; +import selfsigned from 'selfsigned'; import { NetworkUtils } from '../../src/utils/network.utils'; describe('Network utils', () => { + let networkUtilsSandbox: SinonSandbox; + + beforeEach(() => { + networkUtilsSandbox = Sinon.createSandbox(); + }); + + afterEach(() => { + networkUtilsSandbox.restore(); + }); + it('When obtaining auth credentials, should return the password as a SHA256 hash', async () => { const result = NetworkUtils.getAuthFromCredentials({ user: 'test', @@ -10,4 +24,29 @@ describe('Network utils', () => { expect(result.password).to.be.equal('5751a44782594819e4cb8aa27c2c9d87a420af82bc6a5a05bc7f19c3bb00452b'); }); + + it('When webdav ssl certs are required but they dont exist, then they are generated and self signed on the fly, and they are also saved to files', async () => { + const sslSelfSigned: selfsigned.GenerateResult = { + private: randomBytes(8).toString('hex'), + public: randomBytes(8).toString('hex'), + cert: randomBytes(8).toString('hex'), + fingerprint: randomBytes(8).toString('hex'), + }; + networkUtilsSandbox.stub(fs, 'existsSync').returns(false); + + const stubGerateSelfsignedCerts = networkUtilsSandbox + .stub(selfsigned, 'generate') + // @ts-expect-error - We stub the stat method partially + .returns(sslSelfSigned); + + const stubSaveCerts = networkUtilsSandbox.stub(fs, 'writeFileSync').returns(); + + networkUtilsSandbox.stub(fs, 'readFileSync').rejects(); + + const result = NetworkUtils.getWebdavSSLCerts(); + + expect(result).to.deep.equal({ cert: sslSelfSigned.cert, key: sslSelfSigned.private }); + expect(stubGerateSelfsignedCerts.calledOnce).to.be.true; + expect(stubSaveCerts.calledTwice).to.be.true; + }); }); diff --git a/test/webdav/webdav-server.test.ts b/test/webdav/webdav-server.test.ts index f6c3a8d..e1b9b48 100644 --- a/test/webdav/webdav-server.test.ts +++ b/test/webdav/webdav-server.test.ts @@ -2,8 +2,7 @@ import { expect } from 'chai'; import express from 'express'; import sinon from 'sinon'; import { randomBytes, randomInt } from 'crypto'; -import https, { Server } from 'https'; -import selfsigned from 'selfsigned'; +import https from 'https'; import { ConfigService } from '../../src/services/config.service'; import { DriveFolderService } from '../../src/services/drive/drive-folder.service'; import { WebDavServer } from '../../src/webdav/webdav-server'; From 5e02e83f77283517227cb0b986fe2a5858395998 Mon Sep 17 00:00:00 2001 From: larry-internxt Date: Thu, 14 Mar 2024 16:00:05 +0100 Subject: [PATCH 10/11] added webdav ssl test --- test/utils/network.utils.test.ts | 20 ++++++++++++++++++++ 1 file changed, 20 insertions(+) diff --git a/test/utils/network.utils.test.ts b/test/utils/network.utils.test.ts index 7a4e856..def5a1b 100644 --- a/test/utils/network.utils.test.ts +++ b/test/utils/network.utils.test.ts @@ -49,4 +49,24 @@ describe('Network utils', () => { expect(stubGerateSelfsignedCerts.calledOnce).to.be.true; expect(stubSaveCerts.calledTwice).to.be.true; }); + + it('When webdav ssl certs are required but they exist, then they are read from the files', async () => { + const sslMock = { + private: randomBytes(8).toString('hex'), + cert: randomBytes(8).toString('hex'), + }; + networkUtilsSandbox.stub(fs, 'existsSync').returns(true); + networkUtilsSandbox.stub(fs, 'writeFileSync').rejects(); + + networkUtilsSandbox + .stub(fs, 'readFileSync') + .withArgs(NetworkUtils.WEBDAV_SSL_CERTS_PATH.cert) + .returns(sslMock.cert) + .withArgs(NetworkUtils.WEBDAV_SSL_CERTS_PATH.privateKey) + .returns(sslMock.private); + + const result = NetworkUtils.getWebdavSSLCerts(); + + expect(result).to.deep.equal({ cert: sslMock.cert, key: sslMock.private }); + }); }); From 025fa0309db7fb5bbd73cb121c4f0381de5a63ac Mon Sep 17 00:00:00 2001 From: larry-internxt Date: Thu, 14 Mar 2024 16:07:23 +0100 Subject: [PATCH 11/11] removed unused try catch --- test/services/config.service.test.ts | 7 +------ 1 file changed, 1 insertion(+), 6 deletions(-) diff --git a/test/services/config.service.test.ts b/test/services/config.service.test.ts index a874d0a..16fede1 100644 --- a/test/services/config.service.test.ts +++ b/test/services/config.service.test.ts @@ -149,12 +149,7 @@ describe('Config service', () => { const stubMkdir = configServiceSandbox.stub(fs, 'mkdir').withArgs(ConfigService.WEBDAV_SSL_CERTS_DIR).resolves(); - try { - await ConfigService.instance.ensureWebdavCertsDirExists(); - expect(false).to.be.true; - } catch { - /*noop*/ - } + await ConfigService.instance.ensureWebdavCertsDirExists(); expect(stubMkdir).to.be.calledOnceWith(ConfigService.WEBDAV_SSL_CERTS_DIR); });