bug fixes
[henge/kiak.git] / src / strappFileSystem.js
index 00f1050..bc2b0b2 100644 (file)
 * @copyright August 2017 - Ken Grimes, Jordan Lavatai
 * @summmary  File system implementation for a strapp node
 */
-import LocalForage from "localforage"
+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 = (() => {
-  const localforage = LocalForage.createInstance({
-    driver: [LocalForage.LOCALSTORAGE,
-             LocalForage.INDEXEDDB,
-             LocalForage.WEBSQL],
-    name: 'strapp',
-    version: 0.1,
-    storeName: 'strapp'
-  })
-  const authorize = (pubKey, mode, stat) => {
-    let allowed
-    if (pubKey === stat.owner)
-      allowed = (stat.perms >>> 16) & 0xF
-    else {
-      let gAccess = false
-      let uGroups = StrappFile.get(`acct/${pubKey}/groups`).split(' ')
-      for (let i = 0; i < uGroups.length; i++) {
-        if (uGroups[i] === stat.group) {
-          gAccess = true
-          break
-        }
-      }
-      if (gAccess)
-        allowed = (stat.perms >>> 8) & 0xF
-      else
-        allowed = stat.perms & 0xF
-    }
-    switch(mode){
-    case 'r+':
-    case 'rw':
-    case 'wr':
-      return (allowed & 0x6) === 0x6
-    case 'w':
-      return (allowed & 0x2) === 0x2
-    case 'r':
-      return (allowed & 0x4) === 0x4
-    case 'x':
-      return (allowed & 0x1) === 0x1
-    default:
-      console.log(`Unknown access mode: ${mode}`)
-      return false
-    }
-  }
   class StrappFile extends Object {
     constructor(...props) {
       super()
-      return Object.assign(this, new.target.defaults, ...props)
-    }
-    static PermissionDenied() {
-      return new Promise((resolve, reject) => reject('Permission denied'))
-    }
-    static get(path) {
-      return localforage.getItem(path)
+      return Object.assign(this, new.target.literal(), ...props)
     }
-    static set(path, data) {
-      return localforage.setItem(path, data)
+    static literal(...props) {
+      return Object.assign(Object.create(null), this.defaults, ...props)
     }
-    static delete(path) {
-      return localforage.removeItem(path)
+    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)
+      }
     }
-    static routeMessage(lmkid) {
-      //split lmkid by spaces
-      //regex sanitize.  if '/', MSG.  else if ' ', resolve method
+    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))
     }
-    HEAD(opt) {
-      if (authorize(opt.pubKey, 'r', this.stat))
-        return new Promise((resolve, reject) => resolve(''))
-      else
-        return StrappFile.PermissionDenied()
+    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)
+        })
+      })
     }
-    GET(opt) {
-      if (authorize(opt.pubKey, 'r', this.stat))
-        return StrappFile.get(this.path)
-      else return StrappFile.PermissionDenied()
+    HEAD(pubKey) {
+      return Promise.resolve()
     }
-    PUT(opt) {
-      if (authorize(opt.pubKey, 'w', this.stat))
-        return StrappFile.set(this.path, opt.data)
-      else return StrappFile.PermissionDenied()
+    GET(pubKey) {
+      return localforage.getItem(this.path)
     }
-    POST(opt) {
-      return this.PUT(Object.assign(opt, { data: this.GET(opt) + opt.data }))
+    PUT(pubKey, data) {
+      return localforage.setItem(this.path, data)
     }
-    DELETE(opt) {
-      if (authorize(opt.pubKey, 'w', this.stat))
-        return StrappFile.delete(this.path)
-      else return StrappFile.PermissionDenied()
+    POST(pubKey, data) {
+      return localforage.getItem(this.path).then(
+        (fData) => localforage.setItem(this.path, fData + data))
     }
-    OPTIONS(opt) {
-      return this.stat
+    DELETE(pubKey) {
+      return localforage.removeItem(this.path)
     }
-    CONNECT(opt) { //make channel
-      return this.GET(opt)
+    CONNECT(pubKey) { //TODO
+      return Promise.reject(501)
     }
     TRACE(opt) {
     }
@@ -108,15 +120,13 @@ const StrappFile = (() => {
     }
   }
   StrappFile.defaults = {
-    stat: {
-      type: 'mime/type',
-      perm: 0,
-      owner: 'thisOwnerPubKey',
-      group: 'groupname',
-      changed: 'time',
-      created: 'time',
-      accessed: 'time - not saved'
-    }
+    type: 'strapp/file',
+    perms: 0xF00,
+    owner: 'local',
+    group: '',
+    changed: _bootTime,
+    created: _bootTime,
+    accessed: _bootTime
   }
   return StrappFile
 })()
@@ -133,7 +143,7 @@ const StrappPeerConnection = (() => {
     POST(opts) {
       //send msg
     }
-    MSG(opts) {
+    routeMessage(msg) {
       //send routing message down socket
       //POST(opts.routemessage)
     }
@@ -142,15 +152,235 @@ const 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)
+  }
+  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)
+  }
+  
   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)
     }
   }
+  StrappDirectory.defaults = {
+    type: 'strapp/directory'
+  }
   return StrappDirectory
 })()
 
+const StrappDevice = (() => {
+  class StrappDevice extends StrappFile {
+  }
+  return StrappDevice
+})()
+
+const StrappJSON = (() => {
+  class StrappJSON extends StrappFile {
+    POST(pubKey, data) {
+      return localforage.assignItem(this.path, data)
+    }
+  }
+  return StrappJSON
+})()
+
+
+
+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 StrappFile
-export { StrappPeerConnection, StrappDirectory }
+export default StrappFileSystem
+export {
+  StrappFile,
+  StrappPeerConnection,
+  StrappDirectory,
+  StrappDevice,
+  StrappJSON
+}