From f56634206b2af30ec1a840c6d185f1b9d9b5507a Mon Sep 17 00:00:00 2001 From: larryrider Date: Fri, 27 Feb 2026 17:02:34 +0100 Subject: [PATCH 01/12] feat: move WebDavFolderService and test --- .../services => services/webdav}/webdav-folder.service.ts | 0 src/webdav/handlers/MKCOL.handler.ts | 2 +- src/webdav/handlers/MOVE.handler.ts | 2 +- src/webdav/handlers/PUT.handler.ts | 2 +- .../services => services/webdav}/webdav-folder.service.test.ts | 2 +- test/webdav/handlers/MKCOL.handler.test.ts | 2 +- 6 files changed, 5 insertions(+), 5 deletions(-) rename src/{webdav/services => services/webdav}/webdav-folder.service.ts (100%) rename test/{webdav/services => services/webdav}/webdav-folder.service.test.ts (98%) diff --git a/src/webdav/services/webdav-folder.service.ts b/src/services/webdav/webdav-folder.service.ts similarity index 100% rename from src/webdav/services/webdav-folder.service.ts rename to src/services/webdav/webdav-folder.service.ts diff --git a/src/webdav/handlers/MKCOL.handler.ts b/src/webdav/handlers/MKCOL.handler.ts index f015cc90..6679b431 100644 --- a/src/webdav/handlers/MKCOL.handler.ts +++ b/src/webdav/handlers/MKCOL.handler.ts @@ -3,7 +3,7 @@ import { Request, Response } from 'express'; import { WebDavUtils } from '../../utils/webdav.utils'; import { webdavLogger } from '../../utils/logger.utils'; import { XMLUtils } from '../../utils/xml.utils'; -import { WebDavFolderService } from '../services/webdav-folder.service'; +import { WebDavFolderService } from '../../services/webdav/webdav-folder.service'; import { MethodNotAllowed } from '../../utils/errors.utils'; export class MKCOLRequestHandler implements WebDavMethodHandler { diff --git a/src/webdav/handlers/MOVE.handler.ts b/src/webdav/handlers/MOVE.handler.ts index c3df9e54..4fddefb2 100644 --- a/src/webdav/handlers/MOVE.handler.ts +++ b/src/webdav/handlers/MOVE.handler.ts @@ -5,7 +5,7 @@ import { WebDavMethodHandler } from '../../types/webdav.types'; import { NotFoundError } from '../../utils/errors.utils'; import { webdavLogger } from '../../utils/logger.utils'; import { WebDavUtils } from '../../utils/webdav.utils'; -import { WebDavFolderService } from '../services/webdav-folder.service'; +import { WebDavFolderService } from '../../services/webdav/webdav-folder.service'; export class MOVERequestHandler implements WebDavMethodHandler { handle = async (req: Request, res: Response) => { diff --git a/src/webdav/handlers/PUT.handler.ts b/src/webdav/handlers/PUT.handler.ts index 6f8dda34..20539f30 100644 --- a/src/webdav/handlers/PUT.handler.ts +++ b/src/webdav/handlers/PUT.handler.ts @@ -10,7 +10,7 @@ import { EncryptionVersion } from '@internxt/sdk/dist/drive/storage/types'; import { CLIUtils } from '../../utils/cli.utils'; import { BufferStream } from '../../utils/stream.utils'; import { Readable } from 'node:stream'; -import { WebDavFolderService } from '../services/webdav-folder.service'; +import { WebDavFolderService } from '../../services/webdav/webdav-folder.service'; import { AsyncUtils } from '../../utils/async.utils'; import { ThumbnailUtils } from '../../utils/thumbnail.utils'; import { ThumbnailService } from '../../services/thumbnail.service'; diff --git a/test/webdav/services/webdav-folder.service.test.ts b/test/services/webdav/webdav-folder.service.test.ts similarity index 98% rename from test/webdav/services/webdav-folder.service.test.ts rename to test/services/webdav/webdav-folder.service.test.ts index 09a99dac..73dbb081 100644 --- a/test/webdav/services/webdav-folder.service.test.ts +++ b/test/services/webdav/webdav-folder.service.test.ts @@ -1,6 +1,6 @@ import { beforeEach, describe, expect, it, vi } from 'vitest'; import { fail } from 'node:assert'; -import { WebDavFolderService } from '../../../src/webdav/services/webdav-folder.service'; +import { WebDavFolderService } from '../../../src/services/webdav/webdav-folder.service'; import { DriveFolderService } from '../../../src/services/drive/drive-folder.service'; import { ConfigService } from '../../../src/services/config.service'; import { AuthService } from '../../../src/services/auth.service'; diff --git a/test/webdav/handlers/MKCOL.handler.test.ts b/test/webdav/handlers/MKCOL.handler.test.ts index 35e9d541..8535d30c 100644 --- a/test/webdav/handlers/MKCOL.handler.test.ts +++ b/test/webdav/handlers/MKCOL.handler.test.ts @@ -11,7 +11,7 @@ import { WebDavRequestedResource } from '../../../src/types/webdav.types'; import { WebDavUtils } from '../../../src/utils/webdav.utils'; import { AuthService } from '../../../src/services/auth.service'; import { UserCredentialsFixture } from '../../fixtures/login.fixture'; -import { WebDavFolderService } from '../../../src/webdav/services/webdav-folder.service'; +import { WebDavFolderService } from '../../../src/services/webdav/webdav-folder.service'; describe('MKCOL request handler', () => { let sut: MKCOLRequestHandler; From 765ccd3906e9d68d81b5ca0c95ef2a6aebac6788 Mon Sep 17 00:00:00 2001 From: larryrider Date: Fri, 27 Feb 2026 17:47:58 +0100 Subject: [PATCH 02/12] feat: add WebDAV custom authentication support --- src/constants/configs.ts | 3 + src/services/config.service.ts | 9 +++ src/types/command.types.ts | 3 + .../middewares/webdav-auth.middleware.ts | 56 +++++++++++++++++++ src/webdav/webdav-server.ts | 7 ++- test/services/config.service.test.ts | 15 +++++ 6 files changed, 91 insertions(+), 2 deletions(-) create mode 100644 src/webdav/middewares/webdav-auth.middleware.ts diff --git a/src/constants/configs.ts b/src/constants/configs.ts index 726acc4c..53cdfec3 100644 --- a/src/constants/configs.ts +++ b/src/constants/configs.ts @@ -12,3 +12,6 @@ export const WEBDAV_DEFAULT_PORT = '3005'; export const WEBDAV_DEFAULT_PROTOCOL = 'https'; export const WEBDAV_DEFAULT_TIMEOUT = 0; export const WEBDAV_DEFAULT_CREATE_FULL_PATH = true; +export const WEBDAV_DEFAULT_USE_AUTH = false; +export const WEBDAV_DEFAULT_AUTH_USERNAME = ''; +export const WEBDAV_DEFAULT_AUTH_PASSWORD = ''; diff --git a/src/services/config.service.ts b/src/services/config.service.ts index 1d625267..11566258 100644 --- a/src/services/config.service.ts +++ b/src/services/config.service.ts @@ -8,11 +8,14 @@ import { INTERNXT_CLI_DATA_DIR, INTERNXT_CLI_LOGS_DIR, WEBDAV_CONFIGS_FILE, + WEBDAV_DEFAULT_AUTH_PASSWORD, + WEBDAV_DEFAULT_AUTH_USERNAME, WEBDAV_DEFAULT_CREATE_FULL_PATH, WEBDAV_DEFAULT_HOST, WEBDAV_DEFAULT_PORT, WEBDAV_DEFAULT_PROTOCOL, WEBDAV_DEFAULT_TIMEOUT, + WEBDAV_DEFAULT_USE_AUTH, WEBDAV_SSL_CERTS_DIR, } from '../constants/configs'; @@ -92,6 +95,9 @@ export class ConfigService { protocol: configs?.protocol ?? WEBDAV_DEFAULT_PROTOCOL, timeoutMinutes: configs?.timeoutMinutes ?? WEBDAV_DEFAULT_TIMEOUT, createFullPath: configs?.createFullPath ?? WEBDAV_DEFAULT_CREATE_FULL_PATH, + useAuth: configs?.useAuth ?? WEBDAV_DEFAULT_USE_AUTH, + username: configs?.username ?? WEBDAV_DEFAULT_AUTH_USERNAME, + password: configs?.password ?? WEBDAV_DEFAULT_AUTH_PASSWORD, }; } catch { return { @@ -100,6 +106,9 @@ export class ConfigService { protocol: WEBDAV_DEFAULT_PROTOCOL, timeoutMinutes: WEBDAV_DEFAULT_TIMEOUT, createFullPath: WEBDAV_DEFAULT_CREATE_FULL_PATH, + useAuth: WEBDAV_DEFAULT_USE_AUTH, + username: WEBDAV_DEFAULT_AUTH_USERNAME, + password: WEBDAV_DEFAULT_AUTH_PASSWORD, }; } }; diff --git a/src/types/command.types.ts b/src/types/command.types.ts index ef53f0ee..997c0c13 100644 --- a/src/types/command.types.ts +++ b/src/types/command.types.ts @@ -52,6 +52,9 @@ export interface WebdavConfig { protocol: 'http' | 'https'; timeoutMinutes: number; createFullPath: boolean; + useAuth: boolean; + username: string; + password: string; } export class NotValidEmailError extends Error { diff --git a/src/webdav/middewares/webdav-auth.middleware.ts b/src/webdav/middewares/webdav-auth.middleware.ts new file mode 100644 index 00000000..32e4645b --- /dev/null +++ b/src/webdav/middewares/webdav-auth.middleware.ts @@ -0,0 +1,56 @@ +import { RequestHandler } from 'express'; +import { webdavLogger } from '../../utils/logger.utils'; +import { XMLUtils } from '../../utils/xml.utils'; +import { WebdavConfig } from '../../types/command.types'; + +export const WebDAVAuthMiddleware = (configs: WebdavConfig): RequestHandler => { + return (req, res, next) => { + (async () => { + if (configs.useAuth) { + const authHeader = req.headers.authorization; + + if (!authHeader) { + webdavLogger.info('No authentication provided, proceeding with anonymous access'); + next(); + return; + } + + // Parse Basic Authentication + if (!authHeader.startsWith('Basic ')) { + throw new Error('Unsupported authentication method. Only Basic authentication is supported.'); + } + + const base64Credentials = authHeader.substring(6); // Remove 'Basic ' prefix + const credentials = Buffer.from(base64Credentials, 'base64').toString('utf-8'); + const [username, password] = credentials.split(':'); + + if (!username || !password) { + throw new Error('Invalid authentication credentials format.'); + } + + if (username !== configs.username || password !== configs.password) { + const message = 'Authentication failed. Please check your WebDAV custom credentials.'; + + const errorBodyXML = XMLUtils.toWebDavXML( + { + [XMLUtils.addDefaultNamespace('responsedescription')]: message, + }, + {}, + 'error', + ); + + // Send 401 with WWW-Authenticate header to prompt client for credentials + res.setHeader('WWW-Authenticate', 'Basic realm="WebDAV Server"'); + res.status(401).send(errorBodyXML); + return; + } else { + webdavLogger.info(`User authenticated successfully: ${username}`); + next(); + } + } else { + next(); + return; + } + })(); + }; +}; diff --git a/src/webdav/webdav-server.ts b/src/webdav/webdav-server.ts index 4c13b994..f0405016 100644 --- a/src/webdav/webdav-server.ts +++ b/src/webdav/webdav-server.ts @@ -21,11 +21,14 @@ import { PROPPATCHRequestHandler } from './handlers/PROPPATCH.handler'; import { MOVERequestHandler } from './handlers/MOVE.handler'; import { COPYRequestHandler } from './handlers/COPY.handler'; import { MkcolMiddleware } from './middewares/mkcol.middleware'; +import { WebdavConfig } from '../types/command.types'; +import { WebDAVAuthMiddleware } from './middewares/webdav-auth.middleware'; export class WebDavServer { constructor(private readonly app: Express) {} - private readonly registerStartMiddlewares = () => { + private readonly registerStartMiddlewares = (configs: WebdavConfig) => { + this.app.use(WebDAVAuthMiddleware(configs)); this.app.use(AuthMiddleware()); this.app.use( RequestLoggerMiddleware({ @@ -59,7 +62,7 @@ export class WebDavServer { start = async () => { const configs = await ConfigService.instance.readWebdavConfig(); this.app.disable('x-powered-by'); - this.registerStartMiddlewares(); + this.registerStartMiddlewares(configs); await this.registerHandlers(); this.registerEndMiddleWares(); diff --git a/test/services/config.service.test.ts b/test/services/config.service.test.ts index 65851bb9..2d387db6 100644 --- a/test/services/config.service.test.ts +++ b/test/services/config.service.test.ts @@ -9,11 +9,14 @@ import { fail } from 'node:assert'; import { CREDENTIALS_FILE, WEBDAV_CONFIGS_FILE, + WEBDAV_DEFAULT_AUTH_PASSWORD, + WEBDAV_DEFAULT_AUTH_USERNAME, WEBDAV_DEFAULT_CREATE_FULL_PATH, WEBDAV_DEFAULT_HOST, WEBDAV_DEFAULT_PORT, WEBDAV_DEFAULT_PROTOCOL, WEBDAV_DEFAULT_TIMEOUT, + WEBDAV_DEFAULT_USE_AUTH, WEBDAV_SSL_CERTS_DIR, } from '../../src/constants/configs'; @@ -143,6 +146,9 @@ describe('Config service', () => { protocol: 'https', timeoutMinutes: crypto.randomInt(100), createFullPath: false, + useAuth: WEBDAV_DEFAULT_USE_AUTH, + username: WEBDAV_DEFAULT_AUTH_USERNAME, + password: WEBDAV_DEFAULT_AUTH_PASSWORD, }; const stringConfig = JSON.stringify(webdavConfig); @@ -159,6 +165,9 @@ describe('Config service', () => { protocol: 'http', timeoutMinutes: crypto.randomInt(100), createFullPath: false, + useAuth: WEBDAV_DEFAULT_USE_AUTH, + username: WEBDAV_DEFAULT_AUTH_USERNAME, + password: WEBDAV_DEFAULT_AUTH_PASSWORD, }; const stringConfig = JSON.stringify(webdavConfig); @@ -176,6 +185,9 @@ describe('Config service', () => { protocol: WEBDAV_DEFAULT_PROTOCOL, timeoutMinutes: WEBDAV_DEFAULT_TIMEOUT, createFullPath: WEBDAV_DEFAULT_CREATE_FULL_PATH, + useAuth: WEBDAV_DEFAULT_USE_AUTH, + username: WEBDAV_DEFAULT_AUTH_USERNAME, + password: WEBDAV_DEFAULT_AUTH_PASSWORD, }; const fsStub = vi.spyOn(fs, 'readFile').mockResolvedValue(''); @@ -192,6 +204,9 @@ describe('Config service', () => { protocol: WEBDAV_DEFAULT_PROTOCOL, timeoutMinutes: WEBDAV_DEFAULT_TIMEOUT, createFullPath: WEBDAV_DEFAULT_CREATE_FULL_PATH, + useAuth: WEBDAV_DEFAULT_USE_AUTH, + username: WEBDAV_DEFAULT_AUTH_USERNAME, + password: WEBDAV_DEFAULT_AUTH_PASSWORD, }; const fsStub = vi.spyOn(fs, 'readFile').mockRejectedValue(new Error()); From af727445c9498d550cc285537f30a4674a68cf42 Mon Sep 17 00:00:00 2001 From: larryrider Date: Mon, 2 Mar 2026 13:12:14 +0100 Subject: [PATCH 03/12] feat: implement custom authentication for WebDAV configuration --- README.md | 6 ++- docker/README.md | 21 +++++++++ docker/entrypoint.sh | 10 +++++ src/commands/webdav-config.ts | 44 ++++++++++++++++++- src/constants/configs.ts | 2 +- src/services/config.service.ts | 6 +-- src/types/command.types.ts | 10 ++++- .../middewares/webdav-auth.middleware.ts | 2 +- src/webdav/webdav-server.ts | 3 +- test/services/config.service.test.ts | 10 ++--- 10 files changed, 99 insertions(+), 15 deletions(-) diff --git a/README.md b/README.md index 1c9b2157..0b14e47d 100644 --- a/README.md +++ b/README.md @@ -1396,15 +1396,19 @@ Edit the configuration of the Internxt CLI WebDav server as the port or the prot ``` USAGE - $ internxt webdav-config [--json] [-l ] [-p ] [-s | -h] [-t ] [-c] + $ internxt webdav-config [--json] [-l ] [-p ] [-s | -h] [-t ] [-c] [-u -a] [-w + ] FLAGS + -a, --[no-]customAuth Configures the WebDAV server to use custom authentication. -c, --[no-]createFullPath Auto-create missing parent directories during file uploads. -h, --http Configures the WebDAV server to use insecure plain HTTP. -l, --host= The listening host for the WebDAV server. -p, --port= The new port for the WebDAV server. -s, --https Configures the WebDAV server to use HTTPS with self-signed certificates. -t, --timeout= Configures the WebDAV server to use this timeout in minutes. + -u, --username= Configures the WebDAV server to use this username for custom authentication. + -w, --password= Configures the WebDAV server to use this password for custom authentication. GLOBAL FLAGS --json Format output as json. diff --git a/docker/README.md b/docker/README.md index 70bc112c..7e365918 100644 --- a/docker/README.md +++ b/docker/README.md @@ -22,6 +22,9 @@ services: INXT_OTPTOKEN: "" # (Optional) OTP secret for auto-generating 2FA codes WEBDAV_PORT: "" # (Optional) WebDAV port. Defaults to 3005 if empty WEBDAV_PROTOCOL: "" # (Optional) WebDAV protocol. Accepts 'http' or 'https'. Defaults to 'https' if empty + WEBDAV_CUSTOM_AUTH: "" # (Optional) Enable custom authentication. Set to 'true' to enable + WEBDAV_USERNAME: "" # (Optional) Custom username for WebDAV authentication + WEBDAV_PASSWORD: "" # (Optional) Custom password for WebDAV authentication ports: - "127.0.0.1:3005:3005" # Map container port to host. Change if WEBDAV_PORT is customized ``` @@ -44,6 +47,9 @@ docker run -d \ -e INXT_OTPTOKEN="" \ -e WEBDAV_PORT="" \ -e WEBDAV_PROTOCOL="" \ + -e WEBDAV_CUSTOM_AUTH="false" \ + -e WEBDAV_USERNAME="" \ + -e WEBDAV_PASSWORD="" \ -p 127.0.0.1:3005:3005 \ internxt/webdav:latest ``` @@ -79,10 +85,25 @@ You can also run the `internxt/webdav` image directly on popular NAS devices lik | `INXT_OTPTOKEN` | ❌ No | OTP secret key (base32). Used to auto-generate fresh codes at runtime. | | `WEBDAV_PORT` | ❌ No | Port for the WebDAV server. Defaults to `3005` if left empty. | | `WEBDAV_PROTOCOL` | ❌ No | Protocol for the WebDAV server. Accepts `http` or `https`. Defaults to `https` if left empty. | +| `WEBDAV_CUSTOM_AUTH` | ❌ No | Enable custom Basic Authentication for WebDAV. Set to `true` to enable. | +| `WEBDAV_USERNAME` | ❌ No | Username for custom WebDAV authentication. Required if `WEBDAV_CUSTOM_AUTH` is enabled. | +| `WEBDAV_PASSWORD` | ❌ No | Password for custom WebDAV authentication. Required if `WEBDAV_CUSTOM_AUTH` is enabled. | --- +### Custom WebDAV Authentication + +By default, the WebDAV server starts with anonymous authentication enabled, meaning anyone with access to the server URL can connect without credentials. Under the hood, the server uses your Internxt credentials to access your files, but clients don't need to authenticate. If you want to restrict access to your WebDAV server or simply enhance its security, you can enable custom authentication with `WEBDAV_CUSTOM_AUTH`. + +**Security recommendations:** +- 🚨 **We strongly recommend NOT exposing your WebDAV server to the internet.** Keep it on your secure local network whenever possible. +- ⚠️ **Do NOT use your Internxt username and password** for `WEBDAV_USERNAME` and `WEBDAV_PASSWORD` +- Create unique, strong credentials specifically for WebDAV access +- Try to always use HTTPS (`WEBDAV_PROTOCOL=https`) when enabling custom authentication + +**Important:** When connecting to your WebDAV server with custom authentication enabled, you must use the credentials defined in `WEBDAV_USERNAME` and `WEBDAV_PASSWORD`, not your Internxt account credentials. + ### 🔄 2FA Options Explained If your Internxt account has **two-factor authentication enabled**, you can choose one of the following: diff --git a/docker/entrypoint.sh b/docker/entrypoint.sh index 9eb7d6ad..50c13af9 100644 --- a/docker/entrypoint.sh +++ b/docker/entrypoint.sh @@ -36,6 +36,16 @@ elif [ "$proto" = "https" ]; then WEBDAV_ARGS="$WEBDAV_ARGS -s" fi +customAuth=$(echo "$WEBDAV_CUSTOM_AUTH" | tr '[:upper:]' '[:lower:]') +if [ "$customAuth" = "true" ] || [ "$customAuth" = "1" ]; then + if [ -z "$WEBDAV_USERNAME" ] || [ -z "$WEBDAV_PASSWORD" ]; then + echo "Error: WEBDAV_USERNAME and WEBDAV_PASSWORD must be set when WEBDAV_CUSTOM_AUTH is enabled." + exit 1 + fi + echo "Enabling custom WebDAV authentication for user: $WEBDAV_USERNAME" + WEBDAV_ARGS="$WEBDAV_ARGS --customAuth -u=$WEBDAV_USERNAME -w=$WEBDAV_PASSWORD" +fi + internxt webdav-config $WEBDAV_ARGS internxt webdav enable diff --git a/src/commands/webdav-config.ts b/src/commands/webdav-config.ts index a75b9d60..f692cfc4 100644 --- a/src/commands/webdav-config.ts +++ b/src/commands/webdav-config.ts @@ -1,7 +1,7 @@ import { Command, Flags } from '@oclif/core'; import { ConfigService } from '../services/config.service'; import { CLIUtils } from '../utils/cli.utils'; -import { NotValidPortError } from '../types/command.types'; +import { MissingCredentialsWhenUsingAuthError, NotValidPortError } from '../types/command.types'; import { ValidationService } from '../services/validation.service'; export default class WebDAVConfig extends Command { @@ -44,12 +44,31 @@ export default class WebDAVConfig extends Command { required: false, allowNo: true, }), + customAuth: Flags.boolean({ + char: 'a', + description: 'Configures the WebDAV server to use custom authentication.', + required: false, + default: undefined, + allowNo: true, + }), + username: Flags.string({ + char: 'u', + description: 'Configures the WebDAV server to use this username for custom authentication.', + required: false, + dependsOn: ['customAuth'], + }), + password: Flags.string({ + char: 'w', + description: 'Configures the WebDAV server to use this password for custom authentication.', + required: false, + dependsOn: ['customAuth'], + }), }; static readonly enableJsonFlag = true; public run = async () => { const { - flags: { host, port, http, https, timeout, createFullPath }, + flags: { host, port, http, https, timeout, createFullPath, customAuth, username, password }, } = await this.parse(WebDAVConfig); const webdavConfig = await ConfigService.instance.readWebdavConfig(); @@ -81,7 +100,28 @@ export default class WebDAVConfig extends Command { webdavConfig['createFullPath'] = createFullPath; } + if (customAuth !== undefined) { + if (customAuth === true) { + webdavConfig['customAuth'] = true; + if (!username || !password) { + throw new MissingCredentialsWhenUsingAuthError(); + } else { + webdavConfig['username'] = username; + webdavConfig['password'] = password; + } + } else { + webdavConfig['customAuth'] = false; + webdavConfig['username'] = ''; + webdavConfig['password'] = ''; + } + } + await ConfigService.instance.saveWebdavConfig(webdavConfig); + + if (webdavConfig['password']) { + webdavConfig['password'] = '********'; + } + const message = `On the next start, the WebDAV server will use the next config: ${JSON.stringify(webdavConfig)}`; CLIUtils.success(this.log.bind(this), message); return { success: true, message, config: webdavConfig }; diff --git a/src/constants/configs.ts b/src/constants/configs.ts index 53cdfec3..5c2994e0 100644 --- a/src/constants/configs.ts +++ b/src/constants/configs.ts @@ -12,6 +12,6 @@ export const WEBDAV_DEFAULT_PORT = '3005'; export const WEBDAV_DEFAULT_PROTOCOL = 'https'; export const WEBDAV_DEFAULT_TIMEOUT = 0; export const WEBDAV_DEFAULT_CREATE_FULL_PATH = true; -export const WEBDAV_DEFAULT_USE_AUTH = false; +export const WEBDAV_DEFAULT_CUSTOM_AUTH = false; export const WEBDAV_DEFAULT_AUTH_USERNAME = ''; export const WEBDAV_DEFAULT_AUTH_PASSWORD = ''; diff --git a/src/services/config.service.ts b/src/services/config.service.ts index 11566258..73753cc1 100644 --- a/src/services/config.service.ts +++ b/src/services/config.service.ts @@ -15,7 +15,7 @@ import { WEBDAV_DEFAULT_PORT, WEBDAV_DEFAULT_PROTOCOL, WEBDAV_DEFAULT_TIMEOUT, - WEBDAV_DEFAULT_USE_AUTH, + WEBDAV_DEFAULT_CUSTOM_AUTH, WEBDAV_SSL_CERTS_DIR, } from '../constants/configs'; @@ -95,7 +95,7 @@ export class ConfigService { protocol: configs?.protocol ?? WEBDAV_DEFAULT_PROTOCOL, timeoutMinutes: configs?.timeoutMinutes ?? WEBDAV_DEFAULT_TIMEOUT, createFullPath: configs?.createFullPath ?? WEBDAV_DEFAULT_CREATE_FULL_PATH, - useAuth: configs?.useAuth ?? WEBDAV_DEFAULT_USE_AUTH, + customAuth: configs?.customAuth ?? WEBDAV_DEFAULT_CUSTOM_AUTH, username: configs?.username ?? WEBDAV_DEFAULT_AUTH_USERNAME, password: configs?.password ?? WEBDAV_DEFAULT_AUTH_PASSWORD, }; @@ -106,7 +106,7 @@ export class ConfigService { protocol: WEBDAV_DEFAULT_PROTOCOL, timeoutMinutes: WEBDAV_DEFAULT_TIMEOUT, createFullPath: WEBDAV_DEFAULT_CREATE_FULL_PATH, - useAuth: WEBDAV_DEFAULT_USE_AUTH, + customAuth: WEBDAV_DEFAULT_CUSTOM_AUTH, username: WEBDAV_DEFAULT_AUTH_USERNAME, password: WEBDAV_DEFAULT_AUTH_PASSWORD, }; diff --git a/src/types/command.types.ts b/src/types/command.types.ts index 997c0c13..9115cb4d 100644 --- a/src/types/command.types.ts +++ b/src/types/command.types.ts @@ -52,7 +52,7 @@ export interface WebdavConfig { protocol: 'http' | 'https'; timeoutMinutes: number; createFullPath: boolean; - useAuth: boolean; + customAuth: boolean; username: string; password: string; } @@ -185,6 +185,14 @@ export class NotValidWorkspaceUuidError extends Error { } } +export class MissingCredentialsWhenUsingAuthError extends Error { + constructor() { + super('When using custom WebDAV authentication, both username and password must be provided'); + + Object.setPrototypeOf(this, MissingCredentialsWhenUsingAuthError.prototype); + } +} + export interface PaginatedItem { name: string; type: string; diff --git a/src/webdav/middewares/webdav-auth.middleware.ts b/src/webdav/middewares/webdav-auth.middleware.ts index 32e4645b..5e79eeaa 100644 --- a/src/webdav/middewares/webdav-auth.middleware.ts +++ b/src/webdav/middewares/webdav-auth.middleware.ts @@ -6,7 +6,7 @@ import { WebdavConfig } from '../../types/command.types'; export const WebDAVAuthMiddleware = (configs: WebdavConfig): RequestHandler => { return (req, res, next) => { (async () => { - if (configs.useAuth) { + if (configs.customAuth) { const authHeader = req.headers.authorization; if (!authHeader) { diff --git a/src/webdav/webdav-server.ts b/src/webdav/webdav-server.ts index f0405016..e47c03ac 100644 --- a/src/webdav/webdav-server.ts +++ b/src/webdav/webdav-server.ts @@ -81,7 +81,8 @@ export class WebDavServer { server.listen(Number(configs.port), configs.host, undefined, () => { webdavLogger.info( `Internxt ${SdkManager.getAppDetails().clientVersion} WebDav server ` + - `listening at ${configs.protocol}://${configs.host}:${configs.port}`, + `listening at ${configs.protocol}://${configs.host}:${configs.port}` + + `${configs.customAuth ? ' (with custom authentication)' : ''}`, ); }); }; diff --git a/test/services/config.service.test.ts b/test/services/config.service.test.ts index 2d387db6..daf25c5d 100644 --- a/test/services/config.service.test.ts +++ b/test/services/config.service.test.ts @@ -16,7 +16,7 @@ import { WEBDAV_DEFAULT_PORT, WEBDAV_DEFAULT_PROTOCOL, WEBDAV_DEFAULT_TIMEOUT, - WEBDAV_DEFAULT_USE_AUTH, + WEBDAV_DEFAULT_CUSTOM_AUTH, WEBDAV_SSL_CERTS_DIR, } from '../../src/constants/configs'; @@ -146,7 +146,7 @@ describe('Config service', () => { protocol: 'https', timeoutMinutes: crypto.randomInt(100), createFullPath: false, - useAuth: WEBDAV_DEFAULT_USE_AUTH, + customAuth: WEBDAV_DEFAULT_CUSTOM_AUTH, username: WEBDAV_DEFAULT_AUTH_USERNAME, password: WEBDAV_DEFAULT_AUTH_PASSWORD, }; @@ -165,7 +165,7 @@ describe('Config service', () => { protocol: 'http', timeoutMinutes: crypto.randomInt(100), createFullPath: false, - useAuth: WEBDAV_DEFAULT_USE_AUTH, + customAuth: WEBDAV_DEFAULT_CUSTOM_AUTH, username: WEBDAV_DEFAULT_AUTH_USERNAME, password: WEBDAV_DEFAULT_AUTH_PASSWORD, }; @@ -185,7 +185,7 @@ describe('Config service', () => { protocol: WEBDAV_DEFAULT_PROTOCOL, timeoutMinutes: WEBDAV_DEFAULT_TIMEOUT, createFullPath: WEBDAV_DEFAULT_CREATE_FULL_PATH, - useAuth: WEBDAV_DEFAULT_USE_AUTH, + customAuth: WEBDAV_DEFAULT_CUSTOM_AUTH, username: WEBDAV_DEFAULT_AUTH_USERNAME, password: WEBDAV_DEFAULT_AUTH_PASSWORD, }; @@ -204,7 +204,7 @@ describe('Config service', () => { protocol: WEBDAV_DEFAULT_PROTOCOL, timeoutMinutes: WEBDAV_DEFAULT_TIMEOUT, createFullPath: WEBDAV_DEFAULT_CREATE_FULL_PATH, - useAuth: WEBDAV_DEFAULT_USE_AUTH, + customAuth: WEBDAV_DEFAULT_CUSTOM_AUTH, username: WEBDAV_DEFAULT_AUTH_USERNAME, password: WEBDAV_DEFAULT_AUTH_PASSWORD, }; From 19462a96ecd843a299abbf8bcbc79074efce27b7 Mon Sep 17 00:00:00 2001 From: larryrider Date: Mon, 2 Mar 2026 13:36:18 +0100 Subject: [PATCH 04/12] feat: enhance WebDAV configuration with custom authentication and non-interactive mode --- README.md | 10 ++++- src/commands/webdav-config.ts | 80 +++++++++++++++++++++++++++++------ src/types/command.types.ts | 16 +++++++ 3 files changed, 91 insertions(+), 15 deletions(-) diff --git a/README.md b/README.md index 0b14e47d..72dd6862 100644 --- a/README.md +++ b/README.md @@ -1396,8 +1396,8 @@ Edit the configuration of the Internxt CLI WebDav server as the port or the prot ``` USAGE - $ internxt webdav-config [--json] [-l ] [-p ] [-s | -h] [-t ] [-c] [-u -a] [-w - ] + $ internxt webdav-config [--json] [-x] [--debug] [-l ] [-p ] [-s | -h] [-t ] [-c] [-u + -a] [-w ] FLAGS -a, --[no-]customAuth Configures the WebDAV server to use custom authentication. @@ -1410,6 +1410,12 @@ FLAGS -u, --username= Configures the WebDAV server to use this username for custom authentication. -w, --password= Configures the WebDAV server to use this password for custom authentication. +HELPER FLAGS + -x, --non-interactive [env: INXT_NONINTERACTIVE] Prevents the CLI from being interactive. When enabled, the CLI will + not request input through the console and will throw errors directly. + --debug [env: INXT_DEBUG] Enables debug mode. When enabled, the CLI will print debug messages to the + console. + GLOBAL FLAGS --json Format output as json. diff --git a/src/commands/webdav-config.ts b/src/commands/webdav-config.ts index f692cfc4..bfaf8164 100644 --- a/src/commands/webdav-config.ts +++ b/src/commands/webdav-config.ts @@ -1,7 +1,12 @@ import { Command, Flags } from '@oclif/core'; import { ConfigService } from '../services/config.service'; import { CLIUtils } from '../utils/cli.utils'; -import { MissingCredentialsWhenUsingAuthError, NotValidPortError } from '../types/command.types'; +import { + EmptyCustomAuthUsernameError, + MissingCredentialsWhenUsingAuthError, + NotValidPortError, + EmptyCustomAuthPasswordError, +} from '../types/command.types'; import { ValidationService } from '../services/validation.service'; export default class WebDAVConfig extends Command { @@ -10,6 +15,7 @@ export default class WebDAVConfig extends Command { static readonly aliases = []; static readonly examples = ['<%= config.bin %> <%= command.id %>']; static readonly flags = { + ...CLIUtils.CommonFlags, host: Flags.string({ char: 'l', description: 'The listening host for the WebDAV server.', @@ -67,9 +73,10 @@ export default class WebDAVConfig extends Command { static readonly enableJsonFlag = true; public run = async () => { - const { - flags: { host, port, http, https, timeout, createFullPath, customAuth, username, password }, - } = await this.parse(WebDAVConfig); + const { flags } = await this.parse(WebDAVConfig); + const { host, port, https, http, timeout, createFullPath, customAuth, username, password } = flags; + const nonInteractive = flags['non-interactive']; + const webdavConfig = await ConfigService.instance.readWebdavConfig(); if (host) { @@ -103,12 +110,13 @@ export default class WebDAVConfig extends Command { if (customAuth !== undefined) { if (customAuth === true) { webdavConfig['customAuth'] = true; - if (!username || !password) { + + if (!username && !password && nonInteractive) { throw new MissingCredentialsWhenUsingAuthError(); - } else { - webdavConfig['username'] = username; - webdavConfig['password'] = password; } + + webdavConfig['username'] = await this.getUsername(username, nonInteractive); + webdavConfig['password'] = await this.getPassword(password, nonInteractive); } else { webdavConfig['customAuth'] = false; webdavConfig['username'] = ''; @@ -118,13 +126,15 @@ export default class WebDAVConfig extends Command { await ConfigService.instance.saveWebdavConfig(webdavConfig); - if (webdavConfig['password']) { - webdavConfig['password'] = '********'; - } + const printWebdavConfig = { + ...webdavConfig, + password: undefined, + }; - const message = `On the next start, the WebDAV server will use the next config: ${JSON.stringify(webdavConfig)}`; + const message = + 'On the next start, the WebDAV server will use the next config: ' + JSON.stringify(printWebdavConfig); CLIUtils.success(this.log.bind(this), message); - return { success: true, message, config: webdavConfig }; + return { success: true, message, config: printWebdavConfig }; }; public catch = async (error: Error) => { @@ -137,4 +147,48 @@ export default class WebDAVConfig extends Command { }); this.exit(1); }; + + private getUsername = async (usernameFlag: string | undefined, nonInteractive: boolean): Promise => { + const username = await CLIUtils.getValueFromFlag( + { + value: usernameFlag, + name: WebDAVConfig.flags['username'].name, + }, + { + nonInteractive, + prompt: { + message: 'What is the custom auth username?', + options: { type: 'input' }, + }, + }, + { + validate: ValidationService.instance.validateStringIsNotEmpty, + error: new EmptyCustomAuthUsernameError(), + }, + this.log.bind(this), + ); + return username; + }; + + private getPassword = async (passwordFlag: string | undefined, nonInteractive: boolean): Promise => { + const password = await CLIUtils.getValueFromFlag( + { + value: passwordFlag, + name: WebDAVConfig.flags['password'].name, + }, + { + nonInteractive, + prompt: { + message: 'What is the custom auth password?', + options: { type: 'password' }, + }, + }, + { + validate: ValidationService.instance.validateStringIsNotEmpty, + error: new EmptyCustomAuthPasswordError(), + }, + this.log.bind(this), + ); + return password; + }; } diff --git a/src/types/command.types.ts b/src/types/command.types.ts index 9115cb4d..b5649501 100644 --- a/src/types/command.types.ts +++ b/src/types/command.types.ts @@ -153,6 +153,22 @@ export class EmptyFolderNameError extends Error { } } +export class EmptyCustomAuthUsernameError extends Error { + constructor() { + super('Custom auth username can not be empty'); + + Object.setPrototypeOf(this, EmptyCustomAuthUsernameError.prototype); + } +} + +export class EmptyCustomAuthPasswordError extends Error { + constructor() { + super('Custom auth password can not be empty'); + + Object.setPrototypeOf(this, EmptyCustomAuthPasswordError.prototype); + } +} + export class NotValidPortError extends Error { constructor() { super('Port should be a number between 1 and 65535'); From 5f4223ccc68007637f737dfeebb558bf488f5c47 Mon Sep 17 00:00:00 2001 From: larryrider Date: Mon, 2 Mar 2026 14:03:26 +0100 Subject: [PATCH 05/12] feat: improve WebDAV authentication error handling --- .../middewares/webdav-auth.middleware.ts | 42 ++++++++++--------- src/webdav/webdav-server.ts | 4 +- 2 files changed, 24 insertions(+), 22 deletions(-) diff --git a/src/webdav/middewares/webdav-auth.middleware.ts b/src/webdav/middewares/webdav-auth.middleware.ts index 5e79eeaa..96532bad 100644 --- a/src/webdav/middewares/webdav-auth.middleware.ts +++ b/src/webdav/middewares/webdav-auth.middleware.ts @@ -1,4 +1,4 @@ -import { RequestHandler } from 'express'; +import { RequestHandler, Response } from 'express'; import { webdavLogger } from '../../utils/logger.utils'; import { XMLUtils } from '../../utils/xml.utils'; import { WebdavConfig } from '../../types/command.types'; @@ -10,14 +10,15 @@ export const WebDAVAuthMiddleware = (configs: WebdavConfig): RequestHandler => { const authHeader = req.headers.authorization; if (!authHeader) { - webdavLogger.info('No authentication provided, proceeding with anonymous access'); - next(); - return; + return sendUnauthorizedError(res, 'Missing Authorization header.'); } // Parse Basic Authentication if (!authHeader.startsWith('Basic ')) { - throw new Error('Unsupported authentication method. Only Basic authentication is supported.'); + return sendUnauthorizedError( + res, + 'Unsupported authentication method. Only Basic authentication is supported.', + ); } const base64Credentials = authHeader.substring(6); // Remove 'Basic ' prefix @@ -25,27 +26,15 @@ export const WebDAVAuthMiddleware = (configs: WebdavConfig): RequestHandler => { const [username, password] = credentials.split(':'); if (!username || !password) { - throw new Error('Invalid authentication credentials format.'); + return sendUnauthorizedError(res, 'Invalid authentication credentials format.'); } if (username !== configs.username || password !== configs.password) { - const message = 'Authentication failed. Please check your WebDAV custom credentials.'; - - const errorBodyXML = XMLUtils.toWebDavXML( - { - [XMLUtils.addDefaultNamespace('responsedescription')]: message, - }, - {}, - 'error', - ); - - // Send 401 with WWW-Authenticate header to prompt client for credentials - res.setHeader('WWW-Authenticate', 'Basic realm="WebDAV Server"'); - res.status(401).send(errorBodyXML); - return; + return sendUnauthorizedError(res, 'Authentication failed. Please check your WebDAV custom credentials.'); } else { webdavLogger.info(`User authenticated successfully: ${username}`); next(); + return; } } else { next(); @@ -54,3 +43,16 @@ export const WebDAVAuthMiddleware = (configs: WebdavConfig): RequestHandler => { })(); }; }; + +const sendUnauthorizedError = (res: Response, message: string) => { + const errorBodyXML = XMLUtils.toWebDavXML( + { + [XMLUtils.addDefaultNamespace('responsedescription')]: message, + }, + {}, + 'error', + ); + + res.status(401).send(errorBodyXML); + return; +}; diff --git a/src/webdav/webdav-server.ts b/src/webdav/webdav-server.ts index e47c03ac..2ad468dd 100644 --- a/src/webdav/webdav-server.ts +++ b/src/webdav/webdav-server.ts @@ -43,7 +43,7 @@ export class WebDavServer { this.app.use(ErrorHandlingMiddleware); }; - private readonly registerHandlers = async () => { + private readonly registerHandlers = () => { const serverListenPath = /(.*)/; this.app.head(serverListenPath, asyncHandler(new HEADRequestHandler().handle)); this.app.get(serverListenPath, asyncHandler(new GETRequestHandler().handle)); @@ -63,7 +63,7 @@ export class WebDavServer { const configs = await ConfigService.instance.readWebdavConfig(); this.app.disable('x-powered-by'); this.registerStartMiddlewares(configs); - await this.registerHandlers(); + this.registerHandlers(); this.registerEndMiddleWares(); const plainHttp = configs.protocol === 'http'; From 74b15ac22052e3d30167cf8c65203a2b7719c188 Mon Sep 17 00:00:00 2001 From: larryrider Date: Mon, 2 Mar 2026 14:17:38 +0100 Subject: [PATCH 06/12] feat: remove logging of successful WebDAV authentication --- src/webdav/middewares/webdav-auth.middleware.ts | 2 -- 1 file changed, 2 deletions(-) diff --git a/src/webdav/middewares/webdav-auth.middleware.ts b/src/webdav/middewares/webdav-auth.middleware.ts index 96532bad..9fc6c48a 100644 --- a/src/webdav/middewares/webdav-auth.middleware.ts +++ b/src/webdav/middewares/webdav-auth.middleware.ts @@ -1,5 +1,4 @@ import { RequestHandler, Response } from 'express'; -import { webdavLogger } from '../../utils/logger.utils'; import { XMLUtils } from '../../utils/xml.utils'; import { WebdavConfig } from '../../types/command.types'; @@ -32,7 +31,6 @@ export const WebDAVAuthMiddleware = (configs: WebdavConfig): RequestHandler => { if (username !== configs.username || password !== configs.password) { return sendUnauthorizedError(res, 'Authentication failed. Please check your WebDAV custom credentials.'); } else { - webdavLogger.info(`User authenticated successfully: ${username}`); next(); return; } From d320b945dbf3ccc607b03a9df4a428624b4a6938 Mon Sep 17 00:00:00 2001 From: larryrider Date: Mon, 2 Mar 2026 16:49:29 +0100 Subject: [PATCH 07/12] feat: simplified and improved webdav-config customAuth --- README.md | 4 ++-- src/commands/webdav-config.ts | 26 ++++++++++---------------- 2 files changed, 12 insertions(+), 18 deletions(-) diff --git a/README.md b/README.md index 72dd6862..6705aab7 100644 --- a/README.md +++ b/README.md @@ -1396,8 +1396,8 @@ Edit the configuration of the Internxt CLI WebDav server as the port or the prot ``` USAGE - $ internxt webdav-config [--json] [-x] [--debug] [-l ] [-p ] [-s | -h] [-t ] [-c] [-u - -a] [-w ] + $ internxt webdav-config [--json] [-x] [--debug] [-l ] [-p ] [-s | -h] [-t ] [-c] [-a] [-u + ] [-w ] FLAGS -a, --[no-]customAuth Configures the WebDAV server to use custom authentication. diff --git a/src/commands/webdav-config.ts b/src/commands/webdav-config.ts index bfaf8164..8685f228 100644 --- a/src/commands/webdav-config.ts +++ b/src/commands/webdav-config.ts @@ -61,13 +61,11 @@ export default class WebDAVConfig extends Command { char: 'u', description: 'Configures the WebDAV server to use this username for custom authentication.', required: false, - dependsOn: ['customAuth'], }), password: Flags.string({ char: 'w', description: 'Configures the WebDAV server to use this password for custom authentication.', required: false, - dependsOn: ['customAuth'], }), }; static readonly enableJsonFlag = true; @@ -108,20 +106,16 @@ export default class WebDAVConfig extends Command { } if (customAuth !== undefined) { - if (customAuth === true) { - webdavConfig['customAuth'] = true; - - if (!username && !password && nonInteractive) { - throw new MissingCredentialsWhenUsingAuthError(); - } - - webdavConfig['username'] = await this.getUsername(username, nonInteractive); - webdavConfig['password'] = await this.getPassword(password, nonInteractive); - } else { - webdavConfig['customAuth'] = false; - webdavConfig['username'] = ''; - webdavConfig['password'] = ''; - } + webdavConfig['customAuth'] = customAuth; + } + if (username) { + webdavConfig['username'] = await this.getUsername(username, nonInteractive); + } + if (password) { + webdavConfig['password'] = await this.getPassword(password, nonInteractive); + } + if (webdavConfig['customAuth'] && (!webdavConfig['username'] || !webdavConfig['password'])) { + throw new MissingCredentialsWhenUsingAuthError(); } await ConfigService.instance.saveWebdavConfig(webdavConfig); From de820c8ce9732e9ac0d67ad1693542a64f9c1726 Mon Sep 17 00:00:00 2001 From: larryrider Date: Mon, 2 Mar 2026 16:54:17 +0100 Subject: [PATCH 08/12] feat: enhance custom authentication check for WebDAV configuration --- docker/entrypoint.sh | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docker/entrypoint.sh b/docker/entrypoint.sh index 50c13af9..5e17087c 100644 --- a/docker/entrypoint.sh +++ b/docker/entrypoint.sh @@ -37,7 +37,7 @@ elif [ "$proto" = "https" ]; then fi customAuth=$(echo "$WEBDAV_CUSTOM_AUTH" | tr '[:upper:]' '[:lower:]') -if [ "$customAuth" = "true" ] || [ "$customAuth" = "1" ]; then +if [ "$customAuth" = "true" ] || [ "$customAuth" = "1" ] || [ "$customAuth" = "yes" ] || [ "$customAuth" = "y" ]; then if [ -z "$WEBDAV_USERNAME" ] || [ -z "$WEBDAV_PASSWORD" ]; then echo "Error: WEBDAV_USERNAME and WEBDAV_PASSWORD must be set when WEBDAV_CUSTOM_AUTH is enabled." exit 1 From e4ea67e67eb528002e51c70709249d37fb5b0296 Mon Sep 17 00:00:00 2001 From: larryrider Date: Mon, 2 Mar 2026 17:27:49 +0100 Subject: [PATCH 09/12] fix: remove log --- src/services/crypto.service.ts | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/src/services/crypto.service.ts b/src/services/crypto.service.ts index fc1d5ea3..0095d6dc 100644 --- a/src/services/crypto.service.ts +++ b/src/services/crypto.service.ts @@ -6,7 +6,6 @@ import { ConfigService } from '../services/config.service'; import { StreamUtils } from '../utils/stream.utils'; import { LoginCredentials } from '../types/command.types'; import { WorkspaceData } from '@internxt/sdk/dist/workspaces'; -import { logger } from '../utils/logger.utils'; export class CryptoService { public static readonly instance: CryptoService = new CryptoService(); @@ -206,8 +205,7 @@ export class CryptoService { privateKeyInBase64, privateKyberKeyInBase64, }); - } catch (error) { - logger.warn('Decryption failed, using fallback mnemonic', { error }); + } catch { return user.mnemonic; } }; From 76e14e10da489915e45fa74fba63b575f6f49a6e Mon Sep 17 00:00:00 2001 From: larryrider Date: Mon, 2 Mar 2026 17:59:07 +0100 Subject: [PATCH 10/12] feat: remove default WebDAV authentication credentials from configs and tests --- src/constants/configs.ts | 2 -- src/services/config.service.ts | 10 ++++------ test/services/config.service.test.ts | 18 ++++++++---------- test/utils/network.utils.test.ts | 10 ++++++++++ test/webdav/webdav-server.test.ts | 7 +++++++ 5 files changed, 29 insertions(+), 18 deletions(-) diff --git a/src/constants/configs.ts b/src/constants/configs.ts index 5c2994e0..1f7bd8cd 100644 --- a/src/constants/configs.ts +++ b/src/constants/configs.ts @@ -13,5 +13,3 @@ export const WEBDAV_DEFAULT_PROTOCOL = 'https'; export const WEBDAV_DEFAULT_TIMEOUT = 0; export const WEBDAV_DEFAULT_CREATE_FULL_PATH = true; export const WEBDAV_DEFAULT_CUSTOM_AUTH = false; -export const WEBDAV_DEFAULT_AUTH_USERNAME = ''; -export const WEBDAV_DEFAULT_AUTH_PASSWORD = ''; diff --git a/src/services/config.service.ts b/src/services/config.service.ts index 73753cc1..8b04692a 100644 --- a/src/services/config.service.ts +++ b/src/services/config.service.ts @@ -8,8 +8,6 @@ import { INTERNXT_CLI_DATA_DIR, INTERNXT_CLI_LOGS_DIR, WEBDAV_CONFIGS_FILE, - WEBDAV_DEFAULT_AUTH_PASSWORD, - WEBDAV_DEFAULT_AUTH_USERNAME, WEBDAV_DEFAULT_CREATE_FULL_PATH, WEBDAV_DEFAULT_HOST, WEBDAV_DEFAULT_PORT, @@ -96,8 +94,8 @@ export class ConfigService { timeoutMinutes: configs?.timeoutMinutes ?? WEBDAV_DEFAULT_TIMEOUT, createFullPath: configs?.createFullPath ?? WEBDAV_DEFAULT_CREATE_FULL_PATH, customAuth: configs?.customAuth ?? WEBDAV_DEFAULT_CUSTOM_AUTH, - username: configs?.username ?? WEBDAV_DEFAULT_AUTH_USERNAME, - password: configs?.password ?? WEBDAV_DEFAULT_AUTH_PASSWORD, + username: configs?.username ?? '', + password: configs?.password ?? '', }; } catch { return { @@ -107,8 +105,8 @@ export class ConfigService { timeoutMinutes: WEBDAV_DEFAULT_TIMEOUT, createFullPath: WEBDAV_DEFAULT_CREATE_FULL_PATH, customAuth: WEBDAV_DEFAULT_CUSTOM_AUTH, - username: WEBDAV_DEFAULT_AUTH_USERNAME, - password: WEBDAV_DEFAULT_AUTH_PASSWORD, + username: '', + password: '', }; } }; diff --git a/test/services/config.service.test.ts b/test/services/config.service.test.ts index daf25c5d..2e023813 100644 --- a/test/services/config.service.test.ts +++ b/test/services/config.service.test.ts @@ -9,8 +9,6 @@ import { fail } from 'node:assert'; import { CREDENTIALS_FILE, WEBDAV_CONFIGS_FILE, - WEBDAV_DEFAULT_AUTH_PASSWORD, - WEBDAV_DEFAULT_AUTH_USERNAME, WEBDAV_DEFAULT_CREATE_FULL_PATH, WEBDAV_DEFAULT_HOST, WEBDAV_DEFAULT_PORT, @@ -147,8 +145,8 @@ describe('Config service', () => { timeoutMinutes: crypto.randomInt(100), createFullPath: false, customAuth: WEBDAV_DEFAULT_CUSTOM_AUTH, - username: WEBDAV_DEFAULT_AUTH_USERNAME, - password: WEBDAV_DEFAULT_AUTH_PASSWORD, + username: '', + password: '', }; const stringConfig = JSON.stringify(webdavConfig); @@ -166,8 +164,8 @@ describe('Config service', () => { timeoutMinutes: crypto.randomInt(100), createFullPath: false, customAuth: WEBDAV_DEFAULT_CUSTOM_AUTH, - username: WEBDAV_DEFAULT_AUTH_USERNAME, - password: WEBDAV_DEFAULT_AUTH_PASSWORD, + username: '', + password: '', }; const stringConfig = JSON.stringify(webdavConfig); @@ -186,8 +184,8 @@ describe('Config service', () => { timeoutMinutes: WEBDAV_DEFAULT_TIMEOUT, createFullPath: WEBDAV_DEFAULT_CREATE_FULL_PATH, customAuth: WEBDAV_DEFAULT_CUSTOM_AUTH, - username: WEBDAV_DEFAULT_AUTH_USERNAME, - password: WEBDAV_DEFAULT_AUTH_PASSWORD, + username: '', + password: '', }; const fsStub = vi.spyOn(fs, 'readFile').mockResolvedValue(''); @@ -205,8 +203,8 @@ describe('Config service', () => { timeoutMinutes: WEBDAV_DEFAULT_TIMEOUT, createFullPath: WEBDAV_DEFAULT_CREATE_FULL_PATH, customAuth: WEBDAV_DEFAULT_CUSTOM_AUTH, - username: WEBDAV_DEFAULT_AUTH_USERNAME, - password: WEBDAV_DEFAULT_AUTH_PASSWORD, + username: '', + password: '', }; const fsStub = vi.spyOn(fs, 'readFile').mockRejectedValue(new Error()); diff --git a/test/utils/network.utils.test.ts b/test/utils/network.utils.test.ts index e5c86d02..389d6e25 100644 --- a/test/utils/network.utils.test.ts +++ b/test/utils/network.utils.test.ts @@ -6,6 +6,7 @@ import { NetworkUtils } from '../../src/utils/network.utils'; import { Stats } from 'node:fs'; import { fail } from 'node:assert'; import { WebdavConfig } from '../../src/types/command.types'; +import { WEBDAV_DEFAULT_CUSTOM_AUTH } from '../../src/constants/configs'; vi.mock('node:fs/promises', async () => { const actual = await vi.importActual('node:fs/promises'); @@ -46,6 +47,9 @@ describe('Network utils', () => { protocol: 'https', timeoutMinutes: randomInt(900), createFullPath: true, + customAuth: WEBDAV_DEFAULT_CUSTOM_AUTH, + username: '', + password: '', }; const sslSelfSigned: GenerateResult = { private: randomBytes(8).toString('hex'), @@ -79,6 +83,9 @@ describe('Network utils', () => { protocol: 'https', timeoutMinutes: randomInt(900), createFullPath: true, + customAuth: WEBDAV_DEFAULT_CUSTOM_AUTH, + username: '', + password: '', }; const sslMock = { private: randomBytes(8).toString('hex'), @@ -121,6 +128,9 @@ describe('Network utils', () => { protocol: 'https', timeoutMinutes: randomInt(900), createFullPath: true, + customAuth: WEBDAV_DEFAULT_CUSTOM_AUTH, + username: '', + password: '', }; const sslSelfSigned: GenerateResult = { private: randomBytes(8).toString('hex'), diff --git a/test/webdav/webdav-server.test.ts b/test/webdav/webdav-server.test.ts index 6c749bbb..57221854 100644 --- a/test/webdav/webdav-server.test.ts +++ b/test/webdav/webdav-server.test.ts @@ -8,6 +8,7 @@ import { WebDavServer } from '../../src/webdav/webdav-server'; import { NetworkUtils } from '../../src/utils/network.utils'; import { WebdavConfig } from '../../src/types/command.types'; import { UserCredentialsFixture } from '../fixtures/login.fixture'; +import { WEBDAV_DEFAULT_CUSTOM_AUTH } from '../../src/constants/configs'; describe('WebDav server', () => { it('When the WebDav server is started with https, it should generate self-signed certificates', async () => { @@ -17,6 +18,9 @@ describe('WebDav server', () => { protocol: 'https', timeoutMinutes: randomInt(900), createFullPath: true, + customAuth: WEBDAV_DEFAULT_CUSTOM_AUTH, + username: '', + password: '', }; const sslSelfSigned = { private: randomBytes(8).toString('hex'), @@ -56,6 +60,9 @@ describe('WebDav server', () => { protocol: 'http', timeoutMinutes: randomInt(900), createFullPath: true, + customAuth: WEBDAV_DEFAULT_CUSTOM_AUTH, + username: '', + password: '', }; vi.spyOn(ConfigService.instance, 'readWebdavConfig').mockResolvedValue(webdavConfig); From 50f2906d986add995095c4b2b5f64382a315c63d Mon Sep 17 00:00:00 2001 From: larryrider Date: Mon, 2 Mar 2026 18:00:24 +0100 Subject: [PATCH 11/12] feat: refactor workspace selection logic and improve error handling --- src/commands/workspaces-use.ts | 92 +++++++++++++++++++++------------- 1 file changed, 56 insertions(+), 36 deletions(-) diff --git a/src/commands/workspaces-use.ts b/src/commands/workspaces-use.ts index b019a9d2..b180486e 100644 --- a/src/commands/workspaces-use.ts +++ b/src/commands/workspaces-use.ts @@ -1,7 +1,12 @@ import { Command, Flags } from '@oclif/core'; import { ConfigService } from '../services/config.service'; -import { CLIUtils } from '../utils/cli.utils'; -import { MissingCredentialsError, NotValidWorkspaceUuidError } from '../types/command.types'; +import { CLIUtils, LogReporter } from '../utils/cli.utils'; +import { + LoginCredentials, + MissingCredentialsError, + NotValidWorkspaceUuidError, + Workspace, +} from '../types/command.types'; import { WorkspaceService } from '../services/drive/workspace.service'; import { FormatUtils } from '../utils/format.utils'; import { ValidationService } from '../services/validation.service'; @@ -43,46 +48,22 @@ export default class WorkspacesUse extends Command { const userCredentials = await ConfigService.instance.readUser(); if (!userCredentials) throw new MissingCredentialsError(); + const reporter = this.log.bind(this); + if (flags['personal']) { - return WorkspacesUnset.unsetWorkspace(userCredentials, this.log.bind(this)); + return WorkspacesUnset.unsetWorkspace(userCredentials, reporter); } - const workspaces = await WorkspaceService.instance.getAvailableWorkspaces(userCredentials.user); - const availableWorkspaces: string[] = workspaces.map((workspaceData) => { - const name = workspaceData.workspace.name; - const id = workspaceData.workspace.id; - const totalUsedSpace = - Number(workspaceData.workspaceUser?.driveUsage ?? 0) + Number(workspaceData.workspaceUser?.backupsUsage ?? 0); - const spaceLimit = Number(workspaceData.workspaceUser?.spaceLimit ?? 0); - const usedSpace = FormatUtils.humanFileSize(totalUsedSpace); - const availableSpace = FormatUtils.formatLimit(spaceLimit); + const workspace = await this.getWorkspace(userCredentials, flags['id'], nonInteractive, reporter); - return `[${id}] Name: ${name} | Used Space: ${usedSpace} | Available Space: ${availableSpace}`; - }); - const workspaceUuid = await this.getWorkspaceUuid(flags['id'], availableWorkspaces, nonInteractive); - - const workspaceCredentials = await WorkspaceService.instance.getWorkspaceCredentials(workspaceUuid); - const selectedWorkspace = workspaces.find((workspace) => workspace.workspace.id === workspaceUuid); - if (!selectedWorkspace) throw new NotValidWorkspaceUuidError(); - - SdkManager.init({ token: userCredentials.token, workspaceToken: workspaceCredentials.token }); - - await ConfigService.instance.saveUser({ - ...userCredentials, - workspace: { - workspaceCredentials, - workspaceData: selectedWorkspace, - }, - }); - - void DatabaseService.instance.clear(); + await this.setWorkspace(userCredentials, workspace); const message = - `Workspace ${workspaceUuid} selected successfully. Now WebDAV and all of the CLI commands ` + - 'will operate within this workspace until it is changed or unset.'; - CLIUtils.success(this.log.bind(this), message); + `Workspace ${workspace.workspaceCredentials.id} selected successfully. Now WebDAV and ` + + 'all of the CLI commands will operate within this workspace until it is changed or unset.'; + CLIUtils.success(reporter, message); - return { success: true, list: { workspaces } }; + return { success: true, workspace }; }; public catch = async (error: Error) => { @@ -100,6 +81,7 @@ export default class WorkspacesUse extends Command { workspaceUuidFlag: string | undefined, availableWorkspaces: string[], nonInteractive: boolean, + reporter: LogReporter, ): Promise => { const workspaceUuid = await CLIUtils.getValueFromFlag( { @@ -121,11 +103,49 @@ export default class WorkspacesUse extends Command { ValidationService.instance.validateUUIDv4(this.extractUuidFromWorkspaceString(value)), error: new NotValidWorkspaceUuidError(), }, - this.log.bind(this), + reporter, ); return this.extractUuidFromWorkspaceString(workspaceUuid); }; private extractUuidFromWorkspaceString = (workspaceString: string) => workspaceString.match(/\[(.*?)\]/)?.[1] ?? workspaceString; + + private getWorkspace = async ( + userCredentials: LoginCredentials, + workspaceUuidFlag: string | undefined, + nonInteractive: boolean, + reporter: LogReporter, + ): Promise => { + const workspaces = await WorkspaceService.instance.getAvailableWorkspaces(userCredentials.user); + const availableWorkspaces: string[] = workspaces.map((workspaceData) => { + const name = workspaceData.workspace.name; + const id = workspaceData.workspace.id; + const totalUsedSpace = + Number(workspaceData.workspaceUser?.driveUsage ?? 0) + Number(workspaceData.workspaceUser?.backupsUsage ?? 0); + const spaceLimit = Number(workspaceData.workspaceUser?.spaceLimit ?? 0); + const usedSpace = FormatUtils.humanFileSize(totalUsedSpace); + const availableSpace = FormatUtils.formatLimit(spaceLimit); + + return `[${id}] Name: ${name} | Used Space: ${usedSpace} | Available Space: ${availableSpace}`; + }); + const workspaceUuid = await this.getWorkspaceUuid(workspaceUuidFlag, availableWorkspaces, nonInteractive, reporter); + + const workspaceCredentials = await WorkspaceService.instance.getWorkspaceCredentials(workspaceUuid); + const selectedWorkspace = workspaces.find((workspace) => workspace.workspace.id === workspaceUuid); + if (!selectedWorkspace) throw new NotValidWorkspaceUuidError(); + + return { workspaceCredentials, workspaceData: selectedWorkspace }; + }; + + private setWorkspace = async (userCredentials: LoginCredentials, workspace: Workspace) => { + SdkManager.init({ token: userCredentials.token, workspaceToken: workspace.workspaceCredentials.token }); + + await ConfigService.instance.saveUser({ + ...userCredentials, + workspace, + }); + + void DatabaseService.instance.clear(); + }; } From 97461690b54de0ec2b98109ef421343225d01b36 Mon Sep 17 00:00:00 2001 From: larryrider Date: Mon, 2 Mar 2026 18:10:57 +0100 Subject: [PATCH 12/12] feat: add support for workspace ID in WebDAV configuration and entrypoint script --- docker/README.md | 6 ++++++ docker/entrypoint.sh | 5 +++++ 2 files changed, 11 insertions(+) diff --git a/docker/README.md b/docker/README.md index 7e365918..e7cb76b7 100644 --- a/docker/README.md +++ b/docker/README.md @@ -20,6 +20,7 @@ services: INXT_PASSWORD: "" # Your Internxt account password INXT_TWOFACTORCODE: "" # (Optional) Current 2FA one-time code INXT_OTPTOKEN: "" # (Optional) OTP secret for auto-generating 2FA codes + INXT_WORKSPACE_ID: "" # (Optional) Workspace ID to use for WebDAV server WEBDAV_PORT: "" # (Optional) WebDAV port. Defaults to 3005 if empty WEBDAV_PROTOCOL: "" # (Optional) WebDAV protocol. Accepts 'http' or 'https'. Defaults to 'https' if empty WEBDAV_CUSTOM_AUTH: "" # (Optional) Enable custom authentication. Set to 'true' to enable @@ -45,6 +46,7 @@ docker run -d \ -e INXT_PASSWORD="your_password" \ -e INXT_TWOFACTORCODE="" \ -e INXT_OTPTOKEN="" \ + -e INXT_WORKSPACE_ID="" \ -e WEBDAV_PORT="" \ -e WEBDAV_PROTOCOL="" \ -e WEBDAV_CUSTOM_AUTH="false" \ @@ -83,6 +85,7 @@ You can also run the `internxt/webdav` image directly on popular NAS devices lik | `INXT_PASSWORD` | ✅ Yes | Your Internxt account password. | | `INXT_TWOFACTORCODE` | ❌ No | Temporary one-time code from your 2FA app. Must be refreshed every startup. | | `INXT_OTPTOKEN` | ❌ No | OTP secret key (base32). Used to auto-generate fresh codes at runtime. | +| `INXT_WORKSPACE_ID` | ❌ No | Workspace ID to use. If set, the WebDAV server will operate within this workspace. | | `WEBDAV_PORT` | ❌ No | Port for the WebDAV server. Defaults to `3005` if left empty. | | `WEBDAV_PROTOCOL` | ❌ No | Protocol for the WebDAV server. Accepts `http` or `https`. Defaults to `https` if left empty. | | `WEBDAV_CUSTOM_AUTH` | ❌ No | Enable custom Basic Authentication for WebDAV. Set to `true` to enable. | @@ -116,6 +119,9 @@ If your Internxt account has **two-factor authentication enabled**, you can choo 💡 **Recommended:** Use `INXT_OTPTOKEN` if you want your container to run unattended without re-entering codes on each restart. +### Using Workspaces +If you have access to Internxt Workspaces and want to use the WebDAV server with a specific workspace instead of your personal drive, you can set the INXT_WORKSPACE_ID environment variable. + ## 🌐 Accessing WebDAV Once running, your Internxt WebDAV server will be available at: diff --git a/docker/entrypoint.sh b/docker/entrypoint.sh index 5e17087c..55089ea6 100644 --- a/docker/entrypoint.sh +++ b/docker/entrypoint.sh @@ -22,6 +22,11 @@ fi internxt login-legacy $LOGIN_ARGS +if [ -n "$INXT_WORKSPACE_ID" ]; then + echo "Switching to workspace: $INXT_WORKSPACE_ID" + internxt workspaces use -i="$INXT_WORKSPACE_ID" +fi + WEBDAV_ARGS="-l=0.0.0.0"