UNPKG

datona-lib

Version:

Core library for access to the Datona IO Platform. Datona Protocol v0.0.2. Adds HTTP and WebSocket support.

505 lines (420 loc) 18.3 kB
"use strict"; /* * Datona Vault Library * * datona-lib vault features. * * Copyright (C) 2020 Datona Labs * * This program is free software; you can redistribute it and/or * modify it under the terms of the GNU Lesser General Public * License as published by the Free Software Foundation; either * version 3 of the License, or (at your option) any later version. * * This program is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU * Lesser General Public License for more details. * * You should have received a copy of the GNU Lesser General Public License * along with this program; if not, write to the Free Software Foundation, * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. * */ const errors = require('./errors'); const assert = require('./assertions'); const crypto = require('./datona-crypto'); const comms = require('./datona-comms'); const blockchain = require('./datona-blockchain'); /* * Classes */ /* * Form for the name of a file within a vault. File naming convention is as follows: * * [directory/]<file> * * If the directory part is present it must be a single blockchain address and the file part can be any POSIX file name except . and .. * If not present then the file part must be a single blockchain address. * Nested directories are not permitted. * * E.g.: * - Valid File: 0x0000000000000000000000000000000000000001 * - Valid File: 0x0000000000000000000000000000000000000002/my_file.txt * - Invalid File: my_file.txt * - Invalid File; 0x0000000000000000000000000000000000000002/0x0000000000000000000000000000000000000001/my_file.txt */ class VaultFilename { constructor(filenameStr) { this.fullFilename = filenameStr; if (filenameStr.length <= 42) { this.isValid = assert.isAddress(filenameStr); this.file = filenameStr; this.hasDirectory = false; } else { this.directory = filenameStr.substring(0, 42); this.file = filenameStr.substring(43); this.hasDirectory = true; this.isValid = assert.isAddress(this.directory) && filenameStr[42] === '/' && // valid separator /^[^\0\/]*$/.test(this.file) && // POSIX files can be any string but must not contain null or '/' this.file.length > 0 && this.file !== "." && // must not be a special file this.file !== ".."; } }; } /* * An instance of this class represents a single vault within a vault server * controlled by a single S-DAC. */ class RemoteVault extends comms.DatonaConnector { constructor(url, contractAddress, localPrivateKey, remoteAddress) { try { super(url, localPrivateKey, remoteAddress); assert.isAddress(contractAddress, "contractAddress"); } catch (error) { throw new errors.VaultError("Failed to construct vault: " + error.message, error.details); } this.contract = contractAddress; } /* * Promises to create the vault on the remote server. This method creates the * data request, signs it, initiates the request and validates the response. */ create(options) { const request = { txnType: "VaultRequest", requestType: "create", contract: this.contract, }; if (options !== undefined) request.options = options; return this.send(request) .then( function(response){ comms.validateResponse(response.txn, "VaultResponse"); if (response.txn.responseType === "error") throw errors.fromObject(response.txn.error); return response; }); } /* * Promises to write or rewrite the given file of this vault. */ write(data, file = blockchain.ZERO_ADDRESS, options) { const vaultFile = new VaultFilename(file); if (!vaultFile.isValid) throw new errors.TypeError("file: invalid filename"); assert.isPresent(data, "data"); const request = { txnType: "VaultRequest", requestType: "write", contract: this.contract, file: file, data: data }; if (options !== undefined) request.options = options; return this.send(request) .then( function(response){ comms.validateResponse(response.txn, "VaultResponse"); if (response.txn.responseType === "error") throw errors.fromObject(response.txn.error); return response; }); } /* * Promises to append to the given file of this vault. */ append(data, file = blockchain.ZERO_ADDRESS, options) { const vaultFile = new VaultFilename(file); if (!vaultFile.isValid) throw new errors.TypeError("file: invalid filename"); assert.isPresent(data, "data"); const request = { txnType: "VaultRequest", requestType: "append", contract: this.contract, file: file, data: data }; if (options !== undefined) request.options = options; return this.send(request) .then( function(response){ comms.validateResponse(response.txn, "VaultResponse"); if (response.txn.responseType === "error") throw errors.fromObject(response.txn.error); return response; }); } /* * Promises to retrieve the data held in this vault, if permitted by the contract. */ read(file = blockchain.ZERO_ADDRESS, options) { const vaultFile = new VaultFilename(file); if (!vaultFile.isValid) throw new errors.TypeError("file: invalid filename"); const request = { txnType: "VaultRequest", requestType: "read", contract: this.contract, file: file }; if (options !== undefined) request.options = options; return this.send(request) .then( function(response){ comms.validateResponse(response.txn, "VaultResponse"); if (response.txn.responseType === "error") throw errors.fromObject(response.txn.error); return response.txn.data; }); } /* * Promises to delete the vault and its data, if permitted by the contract. */ delete(options) { const request = { txnType: "VaultRequest", requestType: "delete", contract: this.contract }; if (options !== undefined) request.options = options; return this.send(request) .then( function(response){ comms.validateResponse(response.txn, "VaultResponse"); if (response.txn.responseType === "error") throw errors.fromObject(response.txn.error); return response; }); } } /* * Guardian of a Vault Data Server. All create, update, access and delete requests * go through the Vault Keeper, where they are approved or rejected against the * Datona Smart Data Access Protocol. If approved and permission granted by * the vault's Smart Data Access Contract, the raw request is passed to the * given VaultDataServer object. */ class VaultKeeper { constructor(vaultDataServer, key) { assert.isInstanceOf(vaultDataServer, "VaultKeeper vaultDataServer", VaultDataServer); assert.isInstanceOf(key, "VaultKeeper key", crypto.Key); this.key = key; this.vaultDataServer = vaultDataServer; } /* * Decodes and processes a signed VaultRequest, checking the validity of the * signature, validating the request and executing it via the * VaultDataServer */ handleSignedRequest(signedRequestStr) { assert.isString(signedRequestStr, "VaultKeeper handleSignedRequest signedRequestStr"); const key = this.key; try { const {txn, signatory} = comms.decodeTransaction(signedRequestStr); if (txn.txnType !== "VaultRequest") throw new errors.RequestError("Invalid transaction type ('"+txn.txnType+"')"); var func; switch (txn.requestType) { case "create": func = this.createVault.bind(this); break; case "write": func = this.writeVault.bind(this); break; case "append": func = this.appendVault.bind(this); break; case "read": func = this.readVault.bind(this); break; case "delete": func = this.deleteVault.bind(this); break; default: throw new errors.InvalidTransactionError("Invalid request type ('" + txn.requestType + "')"); } return func(txn, signatory) .then( function encode(response){ return comms.encodeTransaction(response, key); }); } catch (error) { const exception = error instanceof errors.DatonaError ? error : new errors.TransactionError("VaultKeeper handleSignedRequest: "+error.message); return new Promise(function(resolve, reject) { resolve(comms.encodeTransaction(createErrorResponse(exception), key)); }); } } /* * Handles a valid create request and promises to create a new vault via the * VaultDataServer. */ createVault(request, signatory) { function checkPermissions(contract, signatory, file) { return contract.assertOwner(signatory).then(contract.assertNotExpired.bind(contract)); } function callDataServer(contractAddress, file, data, options) { // bound to this instance return this.vaultDataServer.create(contractAddress, options); } return _handleVaultKeeperRequest("create", request, signatory, checkPermissions, callDataServer.bind(this)); } /* * Handles a valid write request and promises to write the vault via the * VaultDataServer. */ writeVault(request, signatory) { function checkPermissions(contract, signatory, vaultFile) { if (!assert.isNotNull(request.data)) throw new errors.MalformedTransactionError("Missing request data field"); return contract.assertCanWrite(signatory, (vaultFile.hasDirectory ? vaultFile.directory : vaultFile.file)) .then( (permissions) => { if (permissions.isDirectory() && !vaultFile.hasDirectory) throw new errors.PermissionError("Cannot write to a directory") }) .then(contract.assertNotExpired.bind(contract)); } function callDataServer(contractAddress, file, data, options) { // bound to this instance return this.vaultDataServer.write(contractAddress, file, data, options); } return _handleVaultKeeperRequest("write", request, signatory, checkPermissions, callDataServer.bind(this)); } /* * Handles a valid append request and promises to append to the vault via the * VaultDataServer. */ appendVault(request, signatory) { var directoryIsWritable = false; var fileWithinDirectory = false; function checkPermissions(contract, signatory, vaultFile) { if (!assert.isNotNull(request.data)) throw new errors.MalformedTransactionError("Missing request data field"); fileWithinDirectory = vaultFile.hasDirectory; function checkFileOrDirectoryPermissions(permissions) { if (vaultFile.hasDirectory) { // file within a directory. Permit append if directory has either append or full write permissions. if (permissions.canWrite() === false && permissions.canAppend() === false) throw new errors.PermissionError("permission denied"); directoryIsWritable = permissions.canWrite(); } else { // file or directory in the root if (permissions.isDirectory() && !vaultFile.hasDirectory) throw new errors.PermissionError("Cannot append data to a directory") if (permissions.canAppend() === false) throw new errors.PermissionError("permission denied"); } } return contract.getPermissions(signatory, (vaultFile.hasDirectory ? vaultFile.directory : vaultFile.file)) .then(checkFileOrDirectoryPermissions) .then(contract.assertNotExpired.bind(contract)); } function callDataServer(contractAddress, file, data, options) { // bound to this instance // If appending to a directory that is not fully writable then this is a createFile command. // Otherwise it is an unconditional append which will create the file if it doesn't exist or append the data if it does. if (fileWithinDirectory && !directoryIsWritable) { return this.vaultDataServer.createFile(contractAddress, file, data, options); } else { return this.vaultDataServer.append(contractAddress, file, data, options); } } return _handleVaultKeeperRequest("append", request, signatory, checkPermissions, callDataServer.bind(this)); } /* * Handles a valid read request and promises to return the data from the * vault via VaultDataServer. */ readVault(request, signatory) { var filePermissions; function checkPermissions(contract, signatory, vaultFile) { return contract.assertCanRead(signatory, (vaultFile.hasDirectory ? vaultFile.directory : vaultFile.file)) .then( function(permissions){ filePermissions = permissions; }) .then(contract.assertNotExpired.bind(contract)); } function callDataServer(contractAddress, file, data, options) { // bound to this instance const vaultFile = new VaultFilename(file); if (filePermissions.isDirectory() && !vaultFile.hasDirectory) { return this.vaultDataServer.readDir(contractAddress, file, options); } else{ return this.vaultDataServer.read(contractAddress, file, options); } } return _handleVaultKeeperRequest("read", request, signatory, checkPermissions, callDataServer.bind(this)); } /* * Handles a valid delete request and promises to delete the vault via the * VaultDataServer. */ deleteVault(request, signatory) { function checkPermissions(contract, signatory, file) { return contract.assertOwner(signatory).then(contract.assertHasExpired.bind(contract)); } function callDataServer(contractAddress, file, data, options) { // bound to this instance return this.vaultDataServer.delete(contractAddress, options); } return _handleVaultKeeperRequest("delete", request, signatory, checkPermissions, callDataServer.bind(this)); } } /* * Interface. A class that implements this interface must be given to the * VaultKeeper on construction to provide the data vault server capability. */ class VaultDataServer { constructor() {}; /* * Unconditionally creates a new vault controlled by the given contract. */ create(contract, options) { throw new errors.DeveloperError("Vault server's create function has not been implemented"); }; /* * Unconditionally creates or overwrites data in a file within the vault controlled by the given contract. */ write(contract, file, data, options) { throw new errors.DeveloperError("Vault server's write has not been implemented"); }; /* * Creates a new file within the vault controlled by the given contract and writes the given data to it. * Must throw a VaultError if the file already exists */ createFile(contract, file, data, options) { throw new errors.DeveloperError("Vault server's createFile has not been implemented"); }; /* * Unconditionally appends data to a file within the vault controlled by the given contract. */ append(contract, file, data, options) { throw new errors.DeveloperError("Vault server's append has not been implemented"); }; /* * Unconditionally reads the data from a file within the vault controlled by the given contract. */ read(contract, file, options) { throw new errors.DeveloperError("Vault server's read function has not been implemented"); }; /* * Unconditionally reads a list of the files from a directory within the vault controlled by the given contract. * Returns the list as a '\n' separated string of the form * [<file1>][\n<file2>]... * If no files exist then the empty string is returned. */ readDir(contract, dir, options) { throw new errors.DeveloperError("Vault server's readDir function has not been implemented"); }; /* * Unconditionally deletes a file controlled by the given contract. If the file is null then the * entire vault is deleted. */ delete(contract, file, options) { throw new errors.DeveloperError("Vault server's delete function has not been implemented"); }; } /* * External Functions */ /* * Constructs a success VaultResponse to return from a Vault Server in response * to a VaultRequest */ function createSuccessResponse(data) { var response = comms.createSuccessResponse("VaultResponse"); response.data = data; return response; } /* * Constructs a failure VaultResponse to return from a Vault Server in response * to a VaultRequest */ function createErrorResponse(error) { return comms.createErrorResponse(error, "VaultResponse"); } /* * Exports */ module.exports = { RemoteVault: RemoteVault, VaultKeeper: VaultKeeper, VaultDataServer: VaultDataServer }; /* * Internal Functions */ function _handleVaultKeeperRequest(requestType, request, signatory, permissionFunction, dataServerFunction) { assert.isObject(request, "VaultKeeper "+requestType+"Vault request"); assert.isAddress(signatory, "VaultKeeper "+requestType+"Vault signatory"); try { // validate the request and obtain the contract if (request.requestType !== requestType) throw new errors.InvalidTransactionError("attempted to "+requestType+" a vault with an invalid request type '"+request.requestType+"'"); if (!assert.isAddress(request.contract)) throw new errors.MalformedTransactionError("Invalid request contract field", request.contract); const contract = new blockchain.GenericSmartDataAccessContract(request.contract); const file = (request.file !== undefined) ? request.file : blockchain.ZERO_ADDRESS; const vaultFile = new VaultFilename(file); if (!vaultFile.isValid) throw new errors.MalformedTransactionError("invalid file"); return permissionFunction(contract, signatory, vaultFile) .then( function(){ return dataServerFunction(request.contract, file, request.data, request.options) }) .then(createSuccessResponse) .catch(createErrorResponse); } catch (error) { const exception = error instanceof errors.DatonaError ? error : new errors.TransactionError("VaultKeeper "+requestType+"Vault: "+error.message); return new Promise(function(resolve, reject) { resolve(createErrorResponse(exception)) }); } }