X-Git-Url: https://git.kengrimes.com/?p=henge%2Fkiak.git;a=blobdiff_plain;f=src%2FstrappFileSystem.js;h=bc2b0b23ecf99667a208261f940dd9ce84818d02;hp=6f0b9d48864ffebaac49279043e5732f923a2c01;hb=4e6773ca79a062ba35c0659e32951e11a3db9cf6;hpb=552b28b4fc1ed42e3362c1826acf94c349425b1c diff --git a/src/strappFileSystem.js b/src/strappFileSystem.js index 6f0b9d4..bc2b0b2 100644 --- a/src/strappFileSystem.js +++ b/src/strappFileSystem.js @@ -1,224 +1,386 @@ /** - * @file File System Interface - * @desc Provides basic file commands for interacting with Strapp - * file system as well as storage (and backups) of file system - * @author Jordan Lavatai and Ken Grimes - * @version 0.0.1 - * @license AGPL-3.0 - * @copyright Strapp.io - */ - -import localforage from "localforage" -import StrappPeerConnection from "strappPeerConnection" - -/* File constructor */ -class File extends Object { - constructor(...props) { - super() - return Object.assign(this, new.target.defaults, ...props) +* @file Strapp File System +* @author Jordan Lavatai, Ken Grimes +* @version 0.0.1 +* @license AGPL-3.0 +* @copyright August 2017 - Ken Grimes, Jordan Lavatai +* @summmary File system implementation for a strapp node +*/ +import strapp from './strapp.js' +import localforage from 'localforage' +localforage.config({ + name: 'strapp' +}) +/** extend localforage with an assign operation */ +localforage.assignItem = (path, data) => localforage.getItem(path).then((fdata) => localforage.setItem(path, fdata ? Object.assign(fdata, data) : data)) +localforage.getOrSetItem = (path, defaults) => localforage.getItem(path).then((data) => data === null ? localforage.setItem(path, defaults) : Promise.resolve(data)) + +const _bootTime = new Date() + +const StrappFile = (() => { + class StrappFile extends Object { + constructor(...props) { + super() + return Object.assign(this, new.target.literal(), ...props) + } + static literal(...props) { + return Object.assign(Object.create(null), this.defaults, ...props) + } + static parse(fileLiteral, ...props) { + switch(fileLiteral.type) { + case 'application/JSON': + return new StrappJSON(fileLiteral, ...props) + case 'strapp/directory': + return new StrappDirectory(fileLiteral, ...props) + case 'strapp/file': + default: + return new StrappFile(fileLiteral, ...props) + } + } + resolveRequest(method, pubKey, data, locStack) { + if (!locStack) + return Promise.reject(400) + if (locStack.length > 0) + return Promise.reject(404) + let reqPerms = 0 + switch(method) { + case 'OPTIONS': + return this.resolveClientPerms(pubKey).then( + (perms) => { + let allow = ['OPTIONS'] + if (perms & 0x9) + allow.push('CONNECT') + if (perms & 0x2) + allow.push('DELETE').push('PUT').push('POST') + if (perms & 0x4) + allow.push('GET').push('HEAD') + return Promise.resolve(allow.join(', ')) + }, + (err) => Promise.reject(500) + ) + default: + return Promise.reject(405) + case 'PUT': + case 'POST': + case 'DELETE': + reqPerms = 0x2 + break + case 'GET': + case 'HEAD': + reqPerms = 0x4 + break + case 'CONNECT': + reqPerms = 0x9 + break + } + return this.resolveClientPerms(pubKey) + .then((perms) => { console.log(`got ${perms} for ${reqPerms} in ${this.path}, owned by ${this.owner} with ${this.perms.toString(16)}`) + return Promise.resolve(perms)}) + .then( + (perms) => ((reqPerms & perms) === reqPerms) ? + this[method](pubKey, data) : Promise.reject(401), + (err) => Promise.reject(500)) + } + resolveClientPerms(pubKey) { + if (!pubKey || pubKey === '') + return Promise.resolve((this.perms >>> 12) & 0xF) + return localforage.getItem(`acct/${this.owner}`).then((fData) => { + if (fData && 'pubKey' in fData && fData.pubKey === pubKey) + return Promise.resolve((this.perms >>> 8) & 0xF) + return localforage.getItem(`acct/${pubKey}`).then((account) => { + if (account && account.groups && + account.groups.some((group) => group === this.group)) + return Promise.resolve((this.perms >>> 4) & 0xF) + return Promise.resolve(this.perms & 0xF) + }) + }) + } + HEAD(pubKey) { + return Promise.resolve() + } + GET(pubKey) { + return localforage.getItem(this.path) + } + PUT(pubKey, data) { + return localforage.setItem(this.path, data) + } + POST(pubKey, data) { + return localforage.getItem(this.path).then( + (fData) => localforage.setItem(this.path, fData + data)) + } + DELETE(pubKey) { + return localforage.removeItem(this.path) + } + CONNECT(pubKey) { //TODO + return Promise.reject(501) + } + TRACE(opt) { + } + PATCH(opt) { + } } - get() { - return this.data + StrappFile.defaults = { + type: 'strapp/file', + perms: 0xF00, + owner: 'local', + group: '', + changed: _bootTime, + created: _bootTime, + accessed: _bootTime } - post(postedData) { - this.data += postedData - this.lastModified = new Date() + return StrappFile +})() + +const StrappPeerConnection = (() => { + class StrappPeerConnection extends StrappFile { + GET(opts) { + //get metadata (held in filesystem), with owner, usage info, etc + //if unauthed, send message down socket + } + PUT(opts) { + //create w/ sdp, register callback (or pipe), set owner + } + POST(opts) { + //send msg + } + routeMessage(msg) { + //send routing message down socket + //POST(opts.routemessage) + } } - put(putData) { - this.data = putData - this.lastModified = new Date() + return StrappPeerConnection +})() + +const StrappDirectory = (() => { + const _traverse_loaded = function(method, pubKey, data, locStack) { + if (locStack[0] in this.files) + return this.files[locStack[0]].resolveRequest(method, pubKey, data, locStack.slice(1)) + console.log(`didnt find ${locStack[0]} in ${this.path}`) + localforage.getItem(this.path).then(console.log) + console.log(this.files) + return Promise.resolve(0) } - delete() { - this.data = '' - this.lastModified = new Date() + const _traverse_loading = function(method, pubKey, data, locStack) { + if (this.loadWaiters) + return new Promise((resolve, reject) => this.loadWaiters.push( + [method, pubKey, data, locStack, resolve, reject] + )) + return _traverse_loaded(method, pubKey, data, locStack) } - connect() { - + + class StrappDirectory extends StrappFile { + static literal(...props) { + return StrappFile.literal(this.defaults, ...props) + } + traverse(method, pubKey, data, locStack) { + if (this.files) { + this.traverse = _traverse_loaded + return this.traverse(method, pubKey, data, locStack) + } + this.files = {} + this.traverse = _traverse_loading + this.loadWaiters = [] + return localforage.getItem(this.path).then((fileData) => { + if (fileData) + this.loadFiles(fileData) + this.traverse = _traverse_loaded + this.loadWaiters + .map((w) => this.traverse(w[0], w[1], w[2], w[3]).then(w[4], w[5])) + delete this.loadWaiters + return this.traverse(method, pubKey, data, locStack) + }) + } + loadFile (fileName, fileLiteral) { + this.files[fileName] = StrappFile.parse(fileLiteral, { path: this.path + '/' + fileName }) + } + loadFiles (fileData) { + Object.keys(fileData).map((key) => this.loadFile(key, fileData[key])) + } + resolveRequest(method, pubKey, data, locStack) { + return super.resolveRequest(method, pubKey, data, locStack) + .catch((err) => + err === 404 ? + this.traverse(method, pubKey, data, locStack) : + Promise.reject(err)) + } + CONNECT(opts) { + //send routing message to the directory (handle the next part here) + } } - options(publicKey) { - return this.availPermissions(publicKey) + StrappDirectory.defaults = { + type: 'strapp/directory' } -} -/* TODO: Continue to flesh this out */ -File.defaults = { - name: '', - data: '', - mode: {}, - size: 0 - //lastModified: new Date()? - //lastAccessed: new Date()? - -} + return StrappDirectory +})() +const StrappDevice = (() => { + class StrappDevice extends StrappFile { + } + return StrappDevice +})() -/* Filesystem maintains the current session memory for the strapp instance. Files can be - created and killed from the filesystem without leveraging localForage. Files that are - in the filesystem can be stored to localForage while files that are in localForage can be loaded - to the filesystem. When a Filesystem is first intialized, it attempts to get its strappID it populates itself from localForage and - overwrites any files in its current memory. Files that have restore() as a property (which will be some method needed to - make the file functions e.g. strappPeerConnections will restore() themselves lazily, i.e. when they are needed.*/ - - /* TODO: Should it be possible to create/preserve/destroy a file to both localForage and fileSystem? (same time) */ - /* TODO: Should initFileSystem not overwrite files? */ - -/* These are the default files on all file systems */ -let defaultFiles = [ "..", ".", "accounts", "ice", "log", "run" ] - - -/* TODO: Protect data via closures? */ -const fs = { - /* TODO: What if files are added to file system before init is is called? */ - initFileSystem(){ - this.db = localforage.createInstance({ name: "database" }) - /* Iterate through all files on localforage, adding them to FileSystem - and calling their restore methods */ - this.db.iterate( (value, key, n) => { - /* just btw, return !undefined to exit early */ - this.loadFile(key, true) - - }).catch( (err) => { - console.log(`error: ${err} when iterating through localForage during initFileSystem`) - }) - /* Add the hardcoded default files if they dont already exist */ - /* Restore these files --> need the private/public key*/ - initialFiles.map( (val, idx, array) => { - if (this.fileExists(val)) { - let file = this.getFile(val) - let restoreProp = file['restore'] - if (restoreProp === undefined && typeof restoreFx === 'function') { - //restore file - } - /* Else don't do anything, file exists in FS and doesnt need to be restored */ - } - else { - /* TODO: Remove checking for every file --> although its only for the default files which - will probably be a low number. Still, unnecessary. Could make initialFiles a - Map object with fileType in it and switch(val.fileType) */ - if (val === '..') { - let file = new StrappPeerConnection() - /* Connect with host */ - } - else { - /* Each default file is going to have specific permissions, */ - let filedata = new File() - filedata.name = val - filedata.mode = - this.createFile(val,) - } - } - - }) - - }, - - fileExists(filename) { - return this.files[filename] === undefined ? false : true - }, - - /* Create a file in the file system, if specified overwrites any file that is already there - else does nothing */ - createFile(filename, filedata, overwrite = false){ - filedata.name = filename - if (this.files[filename] === undefined) { - this.files[filename] = filedata - } - else { - if (overwrite) { - console.log(`Overwriting ${filename}`) - this.files[filename] = filedata - } - else { - console.log(`Didn't overwrite file so nothing happened`) - } +const StrappJSON = (() => { + class StrappJSON extends StrappFile { + POST(pubKey, data) { + return localforage.assignItem(this.path, data) } - }, - - /* Get a file from browser session memory */ - /* TODO: Option to get from localForage? */ - getFile(filename) { - return this.files[filename] - }, - - /* Save a file to file system*/ - saveFile(filename, filedata) { - /* TODO: Determine if file to be saved is of saveable nature e.g. SPC's cant really be saved */ - this.db.setItem(filename, filedata) - }, - - /* Delete file from localForage */ - removeFile(filename) { - this.db.removeItem(filename) - }, - - /* Delete a file from file system */ - killFile(filename) { - delete this.files[filename] - - }, - - /* Store file in file system to localForage */ - storeFile(filename) { - this.db.setItem(filename, this.files[filename].filedata) - }, - - /* Load file from localForage to file system */ - loadFile(filename, overwrite = false) { - let filedata = this.db.getItem(filename) - filedata = filedata.restore === undefined ? filedata : filedata.restore() - this.createFile(filename, filedata, overwrite) - }, - - saveFileSystem() { - /* TODO: save all files in filesystem to localforage */ - }, - db: {}, - files: {} - -} - -/* File System API */ - - -/* Load file system from localStorage/indexedDB if strapp.js is running on a host */ -function loadFileSystem(){ - -} - -/* Store file system before shutting down if strapp.js is running on a host- */ -function storeFileSystem(){} - - - -/* addFile - adds a created file to a file */ -//@arg fileType - what to set the type property -//@arg fileData - what to set the data property -//@arg filePos - where to create the file - -/* Determine if a publicKey has permissions to execute methods - -/* rm - Delete file */ - -/* ls - display file contents */ - //open file -//return all of file files + } + return StrappJSON +})() -/* cat - display file data */ -//return file data -/* open - Open file */ - //traverse to file path -/* perm - display file permissions (if you have permissions) */ -/* find - find a file */ -//@arg fileToFind -/* stat - info about a file */ -//@arg path - path to the file -/* exists - determine if a file contains a file */ -//@arg {String} searchedFile - file to be searched -//@arg {String} fileName -//@arg {Number} Depth to look -//@return {Boolean} true if exists, false if doesn't +const StrappFileSystem = (() => { + /* Internal State */ + let rootKey, rootPubKey + const rootDir = new StrappDirectory(StrappFile.literal({ + type: 'strapp/directory', + perms: 0xFF4, + path: '.', + files: {}, + loadFile(fileName, fileLiteral) { + this.files[fileName] = StrappFile.parse(fileLiteral, { path: fileName }) + } + })) + const _strappRuntime = { + '.': rootDir, + '/': rootDir, + run: new StrappDirectory({ + perms: 0xFFF4, + files: { + keyboard: new StrappDevice(), + mouse: new StrappDevice(), + touch: new StrappDevice(), + client: new StrappDirectory({ + perms: 0xFF0, + files: { + create: new StrappFile({ + GET(){}, + POST(){} //TODO: create client action + }) + } + }), + relay: new StrappDirectory({ + perms: 0x000, + files: { + create: new StrappFile({ + GET(){}, + POST(){} //TODO: create relay + }) + } + }), + spc: new StrappDirectory({ + perms: 0xF00, + files: { + create: new StrappFile({ + GET(){}, + POST(){} //TODO: spc creation (probably only internal) + }) + } + }) + } + }) + } + /* Internal Functions */ + const _request = (location, method, data) => + rootDir.resolveRequest(method, rootPubKey, data, location.split('/')) + + /* API Definition */ + const StrappFileSystem = { + request: (location, method, data) => + new Promise((resolve, reject) => StrappFileSystem.loadWaiters.push([ + location, method, data, resolve, reject + ])), + get: (location) => StrappFileSystem.request(location, 'GET'), + set: (location, data) => StrappFileSystem.request(location, 'PUT', data) + } + StrappFileSystem.loadWaiters = [] + /* Init sequence */ + const _defaultFS = { + '.': { + acct: StrappDirectory.literal() + }, + 'acct': { + local: StrappFile.literal() + } + } + const _algo = { + name: 'RSA-OAEP', + modulusLength: 2048, + publicExponent: new Uint8Array([0x01, 0x00, 0x01]), + hash: { name: 'SHA-1' } + } + const _loadRootKeys = () => + Promise.all([ + localforage.getItem('/keys/id_rsa'), + localforage.getItem('/keys/id_rsa.pub') + ]) + .then((files) => + files[0] === null || files[1] === null ? + window.crypto.subtle.generateKey(_algo, true, ['encrypt', 'decrypt']) + .then((keyPair) => Promise.all([ + window.crypto.subtle.exportKey('jwk', keyPair.privateKey), + window.crypto.subtle.exportKey('jwk', keyPair.publicKey) + ])) + .then((jwks) => Promise.all([ + localforage.setItem('/keys/id_rsa', jwks[0]), + localforage.setItem('/keys/id_rsa.pub', jwks[1]) + ])) : + Promise.resolve(files)) + .then((jwks) => { + let hashAlg = jwks[0].alg.replace(_algo.name, '') + if (hashAlg.length === 0) + hashAlg = 'SHA-1' + else + hashAlg = 'SHA' + hashAlg + if (jwks[0].alg.indexOf(_algo.name) !== 0 || hashAlg != _algo.hash.name) { + console.log('Secure hash algorithm updated, old keys deleted') + return Promise.all([localforage.removeItem('/keys/id_rsa'), + localforage.removeItem('/keys/id_rsa.pub') + ]).then(_loadRootKeys) + } + rootPubKey = jwks[1].n + return localforage.assignItem('acct/local', { pubKey: jwks[1].n }).then( + () => window.crypto.subtle.importKey('jwk', jwks[0], _algo, true, ['decrypt'])) + .then((key) => Promise.resolve(rootKey = key)) + }) + + const _init = () => localforage.getItem('.') + .then((data) => + data === null ? + Promise.all( + Object.keys(_defaultFS) + .map((key) => localforage.setItem(key, _defaultFS[key]))) + .then(() => localforage.getItem('.')) : + Promise.resolve(data)) + .then((rootFiles) => { + rootDir.loadFiles(rootFiles) + Object.assign(rootDir.files, _strappRuntime) + _loadRootKeys().then((data) => { + StrappFileSystem.request = _request + StrappFileSystem.loadWaiters + .map((w) => _request(w[0], w[1], w[2]).then(w[3], w[4])) + delete StrappFileSystem.loadWaiters + }) + }) + + _init() + + //localforage.clear().then(_init) + + return StrappFileSystem +})() + + +export default StrappFileSystem +export { + StrappFile, + StrappPeerConnection, + StrappDirectory, + StrappDevice, + StrappJSON +}