/**
- * @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
+}