From f963113bddfb14175b8eac434af0aa2c1ff739a6 Mon Sep 17 00:00:00 2001 From: dawit2123 Date: Fri, 14 Mar 2025 19:39:55 +0300 Subject: [PATCH 1/2] intial version is done --- eslint.config.js | 139 ------------------------ package-lock.json | 42 +++++++- package.json | 3 +- src/backend/webdav/webdav-server.js | 160 ++++++++++++++++++++++++++++ tools/run-selfhosted.js | 12 +++ 5 files changed, 215 insertions(+), 141 deletions(-) delete mode 100644 eslint.config.js create mode 100644 src/backend/webdav/webdav-server.js diff --git a/eslint.config.js b/eslint.config.js deleted file mode 100644 index 64ffeb03c9..0000000000 --- a/eslint.config.js +++ /dev/null @@ -1,139 +0,0 @@ -import js from "@eslint/js"; -import globals from "globals"; - -export default [ - js.configs.recommended, - - { - // Global ignores - ignores: [ - "**/*.min.js", - "**/src/lib/**", - "**/dist/", - "src/backend/src/public/assets/**", - "incubator/**" - ], - }, - { - // Top-level and tools use Node - files: [ - "tools/**/*.js", - ], - languageOptions: { - globals: { - ...globals.node, - } - } - }, - { - // Back end - files: [ - "src/backend/**/*.js", - "mods/**/*.js", - "dev-server.js", - "utils.js", - ], - languageOptions: { - globals: { - ...globals.node, - "kv": true, - "def": true, - "use": true, - "ll":true, - } - } - }, - { - // Front end - files: [ - "src/**/*.js", - ], - ignores: [ - "src/backend/**/*.js", - ], - languageOptions: { - globals: { - ...globals.browser, - ...globals.commonjs, - // Weird false positives - "Buffer": true, - // Puter Common - "puter": true, - "i18n": true, - "html_encode": true, - "html_decode": true, - "isMobile": true, - // Class Registry - "logger": true, - "def": true, - "use": true, - // Libraries - "saveAs": true, // FileSaver - "iro": true, // iro.js color picker - "$": true, // jQuery - "jQuery": true, // jQuery - "fflate": true, // fflate - "_": true, // lodash - "QRCode": true, // qrcode - "io": true, // socket.io - "timeago": true, // timeago - "SelectionArea": true, // viselect - // Puter GUI Globals - "set_menu_item_prop": true, - "determine_active_container_parent": true, - "privacy_aware_path": true, - "api_origin": true, - "auth_token": true, - "logout": true, - "is_email": true, - "select_ctxmenu_item": true, - } - } - }, - { - // Mods - // NOTE: Mods have backend and frontend parts, so this just includes the globals for both. - files: [ - "mods/**/*.js", - ], - languageOptions: { - globals: { - ...globals.node, - "use": true, - "window": true, - "puter": true, - } - } - }, - { - // Tests - files: [ - "**/test/**/*.js", - ], - languageOptions: { - globals: { - ...globals.mocha, - } - } - }, - { - // Phoenix - files: [ - "src/phoenix/**/*.js", - ], - languageOptions: { - globals: { - ...globals.node, - } - } - }, - { - // Global rule settings - rules: { - "no-prototype-builtins": "off", // Complains about any use of hasOwnProperty() - "no-unused-vars": "off", // Temporary, we just have a lot of these - "no-debugger": "warn", - "no-async-promise-executor": "off", // We do this quite often and it's fine - } - }, -]; diff --git a/package-lock.json b/package-lock.json index 0544b0765a..3a2cfa7d89 100644 --- a/package-lock.json +++ b/package-lock.json @@ -24,7 +24,8 @@ "sharp-ico": "^0.1.5", "simple-git": "^3.25.0", "string-template": "^1.0.0", - "uuid": "^9.0.1" + "uuid": "^9.0.1", + "webdav-server": "^2.6.2" }, "devDependencies": { "@eslint/js": "^9.1.1", @@ -16944,6 +16945,18 @@ "node": ">= 8" } }, + "node_modules/webdav-server": { + "version": "2.6.2", + "resolved": "https://registry.npmjs.org/webdav-server/-/webdav-server-2.6.2.tgz", + "integrity": "sha512-0iHdrOzlKGFD96bTvPF8IIEfxw9Q7jB5LqWqhjyBYsofD6T6mOYqWtAvR88VY9Mq0xeg8bCRHC2Vifc9iuTYuw==", + "dependencies": { + "mime-types": "^2.1.18", + "xml-js-builder": "^1.0.3" + }, + "engines": { + "node": ">= 4" + } + }, "node_modules/webidl-conversions": { "version": "3.0.1", "resolved": "https://registry.npmjs.org/webidl-conversions/-/webidl-conversions-3.0.1.tgz", @@ -17327,6 +17340,33 @@ "xtend": "^4.0.0" } }, + "node_modules/xml-js": { + "version": "1.6.11", + "resolved": "https://registry.npmjs.org/xml-js/-/xml-js-1.6.11.tgz", + "integrity": "sha512-7rVi2KMfwfWFl+GpPg6m80IVMWXLRjO+PxTq7V2CDhoGak0wzYzFgUY2m4XJ47OGdXd8eLE8EmwfAmdjw7lC1g==", + "dependencies": { + "sax": "^1.2.4" + }, + "bin": { + "xml-js": "bin/cli.js" + } + }, + "node_modules/xml-js-builder": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/xml-js-builder/-/xml-js-builder-1.0.3.tgz", + "integrity": "sha512-BoLgG/glT45M0jK5PGh9h+iGrQxa8jJk9ofR63GroRifl2tbGB3/yYiVY3wQWHrZgWWfl9+7fhEB/VoD9mWnSg==", + "dependencies": { + "xml-js": "^1.6.2" + }, + "engines": { + "node": ">= 4" + } + }, + "node_modules/xml-js/node_modules/sax": { + "version": "1.4.1", + "resolved": "https://registry.npmjs.org/sax/-/sax-1.4.1.tgz", + "integrity": "sha512-+aWOz7yVScEGoKNd4PA10LZ8sk0A/z5+nXQG5giUO5rprX9jgYsTdov9qCchZiPIZezbZH+jRut8nPodFAX4Jg==" + }, "node_modules/xml-parse-from-string": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/xml-parse-from-string/-/xml-parse-from-string-1.0.1.tgz", diff --git a/package.json b/package.json index 817be96573..9443b3917d 100644 --- a/package.json +++ b/package.json @@ -57,6 +57,7 @@ "sharp-ico": "^0.1.5", "simple-git": "^3.25.0", "string-template": "^1.0.0", - "uuid": "^9.0.1" + "uuid": "^9.0.1", + "webdav-server": "^2.6.2" } } diff --git a/src/backend/webdav/webdav-server.js b/src/backend/webdav/webdav-server.js new file mode 100644 index 0000000000..5918926a0a --- /dev/null +++ b/src/backend/webdav/webdav-server.js @@ -0,0 +1,160 @@ +const webdav = require('webdav-server').v2; +const bcrypt = require('bcrypt'); +const express = require('express'); +const { FSNodeContext } = require('../src/filesystem/FSNodeContext.js'); +const { PuterFSProvider } = require('../src/modules/puterfs/lib/PuterFSProvider.js'); +const { get_user } = require('../src/helpers'); +const path = require('path'); + +class PuterFileSystem extends webdav.FileSystem { + constructor(fsProvider, user, Context) { + super("puter", { uid: 1, gid: 1 }); + this.fsProvider = fsProvider; + this.user = user; + this.Context = Context; + } + + _getPath(filePath) { + return path.normalize(filePath); + } + + async _getFSNode(filePath) { + const normalizedPath = this._getPath(filePath); + return new FSNodeContext({ + services: this.Context.get('services'), + selector: { path: normalizedPath }, + provider: this.fsProvider, + fs: { node: this._getFSNode } + }); + } + + // Implement required WebDAV methods + async _openReadStream(ctx, filePath, callback) { + try { + const node = await this._getFSNode(filePath); + const stream = await node.read(); + callback(null, stream); + } catch (e) { + callback(e); + } + } + + async _openWriteStream(ctx, filePath, callback) { + try { + const node = await this._getFSNode(filePath); + const stream = await node.write(); + callback(null, stream); + } catch (e) { + callback(e); + } + } + + async _create(ctx, filePath, type, callback) { + try { + const node = await this._getFSNode(filePath); + if (type === webdav.ResourceType.Directory) { + await node.mkdir(); + } else { + await node.create(); + } + callback(); + } catch (e) { + callback(e); + } + } + + async _delete(ctx, filePath, callback) { + try { + const node = await this._getFSNode(filePath); + await node.delete(); + callback(); + } catch (e) { + callback(e); + } + } + + // Implement other required methods (size, lastModifiedDate, etc.) + async _size(ctx, filePath, callback) { + try { + const node = await this._getFSNode(filePath); + const size = await node.size(); + callback(null, size); + } catch (e) { + callback(e); + } + } +} + +async function validateUser(username, password, Context) { + try { + const services = Context.get('services'); + + // Fetch user from Puter's authentication service + const user = await get_user({ username, cached: false }); + if (!user) { + console.log(`Authentication failed: User '${username}' not found.`); + return null; + } + + // Validate password with bcrypt + const isMatch = await bcrypt.compare(password, user.password); + + if (!isMatch) { + console.log(`Authentication failed: Incorrect password.`); + return null; + } + + console.log(`Authentication successful for user: ${username}`); + return user; + } catch (error) { + console.error('Error during authentication:', error); + return null; + } +} + +async function startWebDAVServer(port, Context) { + const app = express(); + // Initialize Puter filesystem components + const services = Context.get('services'); + const fsProvider = new PuterFSProvider(services); + const puterFS = new PuterFileSystem(fsProvider, null, Context); + + const server = new webdav.WebDAVServer({ + port: port, + autoSave: false, + rootFileSystem: puterFS // Use Puter filesystem as root + }); + + // Authentication middleware + app.use(async (req, res, next) => { + const authHeader = req.headers.authorization; + + + const credentials = Buffer.from(authHeader.split(' ')[1], 'base64').toString(); + const [username, password] = credentials.split(':'); + + try { + const user = await validateUser(username, password, Context); + if (!user) return res.status(401).send('Invalid credentials'); + req.user = user; + next(); + } catch (error) { + console.error('Authentication error:', error); + res.status(500).send('Internal server error'); + } + }); + // Mount WebDAV server + app.use(webdav.extensions.express('/webdav', server)); + + // Start server + app.listen(port, () => { + console.log(`Puter WebDAV server running on port ${port}`); + console.log(`Access via: http://puter.localhost:${port}/webdav`); + }); + + return server; +} + +module.exports = { + startWebDAVServer +}; \ No newline at end of file diff --git a/tools/run-selfhosted.js b/tools/run-selfhosted.js index 88a5b6e3a5..9fafa81bd6 100644 --- a/tools/run-selfhosted.js +++ b/tools/run-selfhosted.js @@ -25,6 +25,7 @@ // it here. import console from 'node:console'; import process from 'node:process'; +import {startWebDAVServer} from '../src/backend/webdav/webdav-server.js'; const surrounding_box = (col, lines) => { const lengths = lines.map(line => line.length); @@ -114,6 +115,17 @@ const main = async () => { k.add_module(new DevelopmentModule()); } k.boot(); + const webdavContext = { + get: (serviceName) => { + // Special case to get the services container itself + if (serviceName === 'services') return k.services; + + // Normal service resolution + return k.services.get(serviceName, { optional: true }); + } + }; + + startWebDAVServer(1900, webdavContext); }; const early_init_errors = [ From a8299133d3c94580230a3a25c686ad5c04ef9e45 Mon Sep 17 00:00:00 2001 From: dawit2123 Date: Sun, 16 Mar 2025 20:33:56 +0300 Subject: [PATCH 2/2] authentication issue solved --- src/backend/webdav/webdav-server.js | 240 +++++++++++++++++++++++----- 1 file changed, 196 insertions(+), 44 deletions(-) diff --git a/src/backend/webdav/webdav-server.js b/src/backend/webdav/webdav-server.js index 5918926a0a..0ff4769ab6 100644 --- a/src/backend/webdav/webdav-server.js +++ b/src/backend/webdav/webdav-server.js @@ -1,88 +1,228 @@ const webdav = require('webdav-server').v2; const bcrypt = require('bcrypt'); const express = require('express'); -const { FSNodeContext } = require('../src/filesystem/FSNodeContext.js'); +const FSNodeContext = require('../src/filesystem/FSNodeContext.js'); const { PuterFSProvider } = require('../src/modules/puterfs/lib/PuterFSProvider.js'); const { get_user } = require('../src/helpers'); const path = require('path'); +const APIError = require('../src/api/APIError.js'); +const { NodePathSelector } = require('../src/filesystem/node/selectors'); // Import NodePathSelector class PuterFileSystem extends webdav.FileSystem { - constructor(fsProvider, user, Context) { + constructor(fsProvider, Context) { +/** + * Initializes a new instance of the PuterFileSystem class. + * + * @param {PuterFSProvider} fsProvider - The file system provider instance. + * @param {Context} Context - The context containing configuration and services. + */ + super("puter", { uid: 1, gid: 1 }); this.fsProvider = fsProvider; - this.user = user; this.Context = Context; + this.services = Context.get('services'); } _getPath(filePath) { - return path.normalize(filePath); + try { + if (typeof filePath !== 'string') { + filePath = filePath.toString(); + } + return path.resolve('/', filePath).replace(/\.\./g, ''); + } catch (e) { + console.error("error in _getPath", e); + throw e; + } } - async _getFSNode(filePath) { + async getFSNode(filePath) { const normalizedPath = this._getPath(filePath); return new FSNodeContext({ - services: this.Context.get('services'), - selector: { path: normalizedPath }, + services: this.services, + selector: new NodePathSelector(normalizedPath), // Use NodePathSelector instance provider: this.fsProvider, - fs: { node: this._getFSNode } + fs: this.services.get('filesystem') }); } - // Implement required WebDAV methods + async _type(ctx, filePath, callback) { + try { + const node = await this.getFSNode(filePath); + const exists = await node.exists(); + if (!exists) { + return callback(webdav.Errors.ResourceNotFound); + } + const isDir = await node.get('is_dir'); + callback(null, isDir ? webdav.ResourceType.Directory : webdav.ResourceType.File); + } catch (e) { + this._mapError(e, callback, '_type'); + } + } + + async _exist(ctx, filePath, callback) { + try { + const node = await this.getFSNode(filePath); + const exists = await node.exists(); + callback(null, exists); + } catch (e) { + this._mapError(e, callback, '_exist'); + } + } + async _openReadStream(ctx, filePath, callback) { try { - const node = await this._getFSNode(filePath); - const stream = await node.read(); - callback(null, stream); + const node = await this.getFSNode(filePath); + if (await node.get('is_dir')) { + return callback(webdav.Errors.IsADirectory); + } + const content = await this.services.get('filesystem').read(node); + callback(null, content); } catch (e) { - callback(e); + this._mapError(e, callback, '_openReadStream'); } } async _openWriteStream(ctx, filePath, callback) { try { - const node = await this._getFSNode(filePath); - const stream = await node.write(); - callback(null, stream); + const node = await this.getFSNode(filePath); + const parentPath = path.dirname(filePath); + const parentNode = await this.getFSNode(parentPath); + + return callback(null, { + write: async (content) => { + await this.services.get('filesystem').write(node, content, { + parent: parentNode, + name: path.basename(filePath) + }); + }, + end: callback + }); } catch (e) { - callback(e); + this._mapError(e, callback, '_openWriteStream'); } } async _create(ctx, filePath, type, callback) { try { - const node = await this._getFSNode(filePath); + console.log('Create operation is called for:', filePath); + const parentPath = path.dirname(filePath); + const name = path.basename(filePath); + const parentNode = await this.getFSNode(parentPath); if (type === webdav.ResourceType.Directory) { - await node.mkdir(); + console.log('making directory: ', name); + await this.services.get('filesystem').mkdir(parentNode, name); } else { - await node.create(); + await this.services.get('filesystem').write( + { path: filePath }, + Buffer.alloc(0), + { parent: parentNode, name } + ); } callback(); } catch (e) { - callback(e); + this._mapError(e, callback, '_create'); } } async _delete(ctx, filePath, callback) { try { - const node = await this._getFSNode(filePath); - await node.delete(); + const node = await this.getFSNode(filePath); + if (await node.get('is_dir')) { + await this.services.get('filesystem').rmdir(node); + } else { + await this.services.get('filesystem').unlink(node); + } callback(); } catch (e) { - callback(e); + this._mapError(e, callback, '_delete'); } } - // Implement other required methods (size, lastModifiedDate, etc.) async _size(ctx, filePath, callback) { try { - const node = await this._getFSNode(filePath); - const size = await node.size(); - callback(null, size); + const node = await this.getFSNode(filePath); + const size = await node.get('size'); + callback(null, size || 0); } catch (e) { - callback(e); + this._mapError(e, callback, '_size'); } } + + async _lastModifiedDate(ctx, filePath, callback) { + try { + const node = await this.getFSNode(filePath); + const modified = await node.get('modified'); + callback(null, modified ? new Date(modified * 1000) : new Date()); + } catch (e) { + this._mapError(e, callback, '_lastModifiedDate'); + } + } + + async _move(ctx, srcPath, destPath, callback) { + try { + const srcNode = await this.getFSNode(srcPath); + const destParent = await this.getFSNode(path.dirname(destPath)); + await this.services.get('filesystem').move( + srcNode, + destParent, + path.basename(destPath) + ); + callback(); + } catch (e) { + this._mapError(e, callback, '_move'); + } + } + + async _copy(ctx, srcPath, destPath, callback) { + try { + const srcNode = await this.getFSNode(srcPath); + const destParent = await this.getFSNode(path.dirname(destPath)); + await this.services.get('filesystem').copy( + srcNode, + destParent, + path.basename(destPath) + ); + callback(); + } catch (e) { + this._mapError(e, callback, '_copy'); + } + } + + async _propertyManager(ctx, filePath, callback) { + callback(null, { + getProperties: async (name, callback) => { + try { + const node = await this.getFSNode(filePath); + const entry = await node.fetchEntry(); + callback(null, { + displayname: entry.name, + getlastmodified: new Date(entry.modified * 1000).toUTCString(), + getcontentlength: entry.size || '0', + resourcetype: entry.is_dir ? ['collection'] : [], + getcontenttype: entry.mime_type || 'application/octet-stream' + }); + } catch (e) { + this._mapError(e, callback, '_propertyManager'); + } + } + }); + } + + _mapError(e, callback, methodName) { + console.error('WebDAV operation error:', e); + if (e instanceof APIError) { + switch (e.code) { + case 'not_found': return callback(webdav.Errors.ResourceNotFound); + case 'item_with_same_name_exists': return callback(webdav.Errors.InvalidOperation); + case 'not_empty': return callback(webdav.Errors.Forbidden); + default: return callback(webdav.Errors.InternalError); + } + } + if (e instanceof TypeError && e.message.includes('Cannot read properties of undefined (reading \'isDirectory\')')) { + return callback(webdav.Errors.InternalServerError); + } + return callback(e); + } } async function validateUser(username, password, Context) { @@ -98,7 +238,7 @@ async function validateUser(username, password, Context) { // Validate password with bcrypt const isMatch = await bcrypt.compare(password, user.password); - + if (!isMatch) { console.log(`Authentication failed: Incorrect password.`); return null; @@ -114,21 +254,36 @@ async function validateUser(username, password, Context) { async function startWebDAVServer(port, Context) { const app = express(); - // Initialize Puter filesystem components - const services = Context.get('services'); - const fsProvider = new PuterFSProvider(services); - const puterFS = new PuterFileSystem(fsProvider, null, Context); + const fsProvider = new PuterFSProvider(Context.get('services')); + const puterFS = new PuterFileSystem(fsProvider, Context); const server = new webdav.WebDAVServer({ - port: port, + rootFileSystem: puterFS, autoSave: false, - rootFileSystem: puterFS // Use Puter filesystem as root + strictMode: false + }); + + // Add the missing functions to the PuterFileSystem prototype + PuterFileSystem.prototype.type = PuterFileSystem.prototype._type; + PuterFileSystem.prototype.exist = PuterFileSystem.prototype._exist; + PuterFileSystem.prototype.create = PuterFileSystem.prototype._create; + PuterFileSystem.prototype.delete = PuterFileSystem.prototype._delete; + PuterFileSystem.prototype.openReadStream = PuterFileSystem.prototype._openReadStream; + PuterFileSystem.prototype.openWriteStream = PuterFileSystem.prototype._openWriteStream; + PuterFileSystem.prototype.size = PuterFileSystem.prototype._size; + PuterFileSystem.prototype.lastModifiedDate = PuterFileSystem.prototype._lastModifiedDate; + PuterFileSystem.prototype.move = PuterFileSystem.prototype._move; + PuterFileSystem.prototype.copy = PuterFileSystem.prototype._copy; + PuterFileSystem.prototype.propertyManager = PuterFileSystem.prototype._propertyManager; + + server.beforeRequest((ctx, next) => { + ctx.response.setHeader('MS-Author-Via', 'DAV'); + next(); }); // Authentication middleware app.use(async (req, res, next) => { const authHeader = req.headers.authorization; - const credentials = Buffer.from(authHeader.split(' ')[1], 'base64').toString(); const [username, password] = credentials.split(':'); @@ -137,24 +292,21 @@ async function startWebDAVServer(port, Context) { const user = await validateUser(username, password, Context); if (!user) return res.status(401).send('Invalid credentials'); req.user = user; + delete req.headers.authorization; next(); } catch (error) { console.error('Authentication error:', error); res.status(500).send('Internal server error'); } }); - // Mount WebDAV server - app.use(webdav.extensions.express('/webdav', server)); - // Start server + app.use('/webdav', webdav.extensions.express('/', server)); + app.listen(port, () => { console.log(`Puter WebDAV server running on port ${port}`); - console.log(`Access via: http://puter.localhost:${port}/webdav`); }); return server; } -module.exports = { - startWebDAVServer -}; \ No newline at end of file +module.exports = { startWebDAVServer }; \ No newline at end of file