bc2b0b23ecf99667a208261f940dd9ce84818d02
[henge/kiak.git] / src / strappFileSystem.js
1 /**
2 * @file Strapp File System
3 * @author Jordan Lavatai, Ken Grimes
4 * @version 0.0.1
5 * @license AGPL-3.0
6 * @copyright August 2017 - Ken Grimes, Jordan Lavatai
7 * @summmary File system implementation for a strapp node
8 */
9 import strapp from './strapp.js'
10 import localforage from 'localforage'
11 localforage.config({
12 name: 'strapp'
13 })
14 /** extend localforage with an assign operation */
15 localforage.assignItem = (path, data) => localforage.getItem(path).then((fdata) => localforage.setItem(path, fdata ? Object.assign(fdata, data) : data))
16 localforage.getOrSetItem = (path, defaults) => localforage.getItem(path).then((data) => data === null ? localforage.setItem(path, defaults) : Promise.resolve(data))
17
18 const _bootTime = new Date()
19
20 const StrappFile = (() => {
21 class StrappFile extends Object {
22 constructor(...props) {
23 super()
24 return Object.assign(this, new.target.literal(), ...props)
25 }
26 static literal(...props) {
27 return Object.assign(Object.create(null), this.defaults, ...props)
28 }
29 static parse(fileLiteral, ...props) {
30 switch(fileLiteral.type) {
31 case 'application/JSON':
32 return new StrappJSON(fileLiteral, ...props)
33 case 'strapp/directory':
34 return new StrappDirectory(fileLiteral, ...props)
35 case 'strapp/file':
36 default:
37 return new StrappFile(fileLiteral, ...props)
38 }
39 }
40 resolveRequest(method, pubKey, data, locStack) {
41 if (!locStack)
42 return Promise.reject(400)
43 if (locStack.length > 0)
44 return Promise.reject(404)
45 let reqPerms = 0
46 switch(method) {
47 case 'OPTIONS':
48 return this.resolveClientPerms(pubKey).then(
49 (perms) => {
50 let allow = ['OPTIONS']
51 if (perms & 0x9)
52 allow.push('CONNECT')
53 if (perms & 0x2)
54 allow.push('DELETE').push('PUT').push('POST')
55 if (perms & 0x4)
56 allow.push('GET').push('HEAD')
57 return Promise.resolve(allow.join(', '))
58 },
59 (err) => Promise.reject(500)
60 )
61 default:
62 return Promise.reject(405)
63 case 'PUT':
64 case 'POST':
65 case 'DELETE':
66 reqPerms = 0x2
67 break
68 case 'GET':
69 case 'HEAD':
70 reqPerms = 0x4
71 break
72 case 'CONNECT':
73 reqPerms = 0x9
74 break
75 }
76 return this.resolveClientPerms(pubKey)
77 .then((perms) => { console.log(`got ${perms} for ${reqPerms} in ${this.path}, owned by ${this.owner} with ${this.perms.toString(16)}`)
78 return Promise.resolve(perms)})
79 .then(
80 (perms) => ((reqPerms & perms) === reqPerms) ?
81 this[method](pubKey, data) : Promise.reject(401),
82 (err) => Promise.reject(500))
83 }
84 resolveClientPerms(pubKey) {
85 if (!pubKey || pubKey === '')
86 return Promise.resolve((this.perms >>> 12) & 0xF)
87 return localforage.getItem(`acct/${this.owner}`).then((fData) => {
88 if (fData && 'pubKey' in fData && fData.pubKey === pubKey)
89 return Promise.resolve((this.perms >>> 8) & 0xF)
90 return localforage.getItem(`acct/${pubKey}`).then((account) => {
91 if (account && account.groups &&
92 account.groups.some((group) => group === this.group))
93 return Promise.resolve((this.perms >>> 4) & 0xF)
94 return Promise.resolve(this.perms & 0xF)
95 })
96 })
97 }
98 HEAD(pubKey) {
99 return Promise.resolve()
100 }
101 GET(pubKey) {
102 return localforage.getItem(this.path)
103 }
104 PUT(pubKey, data) {
105 return localforage.setItem(this.path, data)
106 }
107 POST(pubKey, data) {
108 return localforage.getItem(this.path).then(
109 (fData) => localforage.setItem(this.path, fData + data))
110 }
111 DELETE(pubKey) {
112 return localforage.removeItem(this.path)
113 }
114 CONNECT(pubKey) { //TODO
115 return Promise.reject(501)
116 }
117 TRACE(opt) {
118 }
119 PATCH(opt) {
120 }
121 }
122 StrappFile.defaults = {
123 type: 'strapp/file',
124 perms: 0xF00,
125 owner: 'local',
126 group: '',
127 changed: _bootTime,
128 created: _bootTime,
129 accessed: _bootTime
130 }
131 return StrappFile
132 })()
133
134 const StrappPeerConnection = (() => {
135 class StrappPeerConnection extends StrappFile {
136 GET(opts) {
137 //get metadata (held in filesystem), with owner, usage info, etc
138 //if unauthed, send message down socket
139 }
140 PUT(opts) {
141 //create w/ sdp, register callback (or pipe), set owner
142 }
143 POST(opts) {
144 //send msg
145 }
146 routeMessage(msg) {
147 //send routing message down socket
148 //POST(opts.routemessage)
149 }
150 }
151 return StrappPeerConnection
152 })()
153
154 const StrappDirectory = (() => {
155 const _traverse_loaded = function(method, pubKey, data, locStack) {
156 if (locStack[0] in this.files)
157 return this.files[locStack[0]].resolveRequest(method, pubKey, data, locStack.slice(1))
158 console.log(`didnt find ${locStack[0]} in ${this.path}`)
159 localforage.getItem(this.path).then(console.log)
160 console.log(this.files)
161 return Promise.resolve(0)
162 }
163 const _traverse_loading = function(method, pubKey, data, locStack) {
164 if (this.loadWaiters)
165 return new Promise((resolve, reject) => this.loadWaiters.push(
166 [method, pubKey, data, locStack, resolve, reject]
167 ))
168 return _traverse_loaded(method, pubKey, data, locStack)
169 }
170
171 class StrappDirectory extends StrappFile {
172 static literal(...props) {
173 return StrappFile.literal(this.defaults, ...props)
174 }
175 traverse(method, pubKey, data, locStack) {
176 if (this.files) {
177 this.traverse = _traverse_loaded
178 return this.traverse(method, pubKey, data, locStack)
179 }
180 this.files = {}
181 this.traverse = _traverse_loading
182 this.loadWaiters = []
183 return localforage.getItem(this.path).then((fileData) => {
184 if (fileData)
185 this.loadFiles(fileData)
186 this.traverse = _traverse_loaded
187 this.loadWaiters
188 .map((w) => this.traverse(w[0], w[1], w[2], w[3]).then(w[4], w[5]))
189 delete this.loadWaiters
190 return this.traverse(method, pubKey, data, locStack)
191 })
192 }
193 loadFile (fileName, fileLiteral) {
194 this.files[fileName] = StrappFile.parse(fileLiteral, { path: this.path + '/' + fileName })
195 }
196 loadFiles (fileData) {
197 Object.keys(fileData).map((key) => this.loadFile(key, fileData[key]))
198 }
199 resolveRequest(method, pubKey, data, locStack) {
200 return super.resolveRequest(method, pubKey, data, locStack)
201 .catch((err) =>
202 err === 404 ?
203 this.traverse(method, pubKey, data, locStack) :
204 Promise.reject(err))
205 }
206 CONNECT(opts) {
207 //send routing message to the directory (handle the next part here)
208 }
209 }
210 StrappDirectory.defaults = {
211 type: 'strapp/directory'
212 }
213 return StrappDirectory
214 })()
215
216 const StrappDevice = (() => {
217 class StrappDevice extends StrappFile {
218 }
219 return StrappDevice
220 })()
221
222 const StrappJSON = (() => {
223 class StrappJSON extends StrappFile {
224 POST(pubKey, data) {
225 return localforage.assignItem(this.path, data)
226 }
227 }
228 return StrappJSON
229 })()
230
231
232
233 const StrappFileSystem = (() => {
234 /* Internal State */
235 let rootKey, rootPubKey
236 const rootDir = new StrappDirectory(StrappFile.literal({
237 type: 'strapp/directory',
238 perms: 0xFF4,
239 path: '.',
240 files: {},
241 loadFile(fileName, fileLiteral) {
242 this.files[fileName] = StrappFile.parse(fileLiteral, { path: fileName })
243 }
244 }))
245 const _strappRuntime = {
246 '.': rootDir,
247 '/': rootDir,
248 run: new StrappDirectory({
249 perms: 0xFFF4,
250 files: {
251 keyboard: new StrappDevice(),
252 mouse: new StrappDevice(),
253 touch: new StrappDevice(),
254 client: new StrappDirectory({
255 perms: 0xFF0,
256 files: {
257 create: new StrappFile({
258 GET(){},
259 POST(){} //TODO: create client action
260 })
261 }
262 }),
263 relay: new StrappDirectory({
264 perms: 0x000,
265 files: {
266 create: new StrappFile({
267 GET(){},
268 POST(){} //TODO: create relay
269 })
270 }
271 }),
272 spc: new StrappDirectory({
273 perms: 0xF00,
274 files: {
275 create: new StrappFile({
276 GET(){},
277 POST(){} //TODO: spc creation (probably only internal)
278 })
279 }
280 })
281 }
282 })
283 }
284
285 /* Internal Functions */
286 const _request = (location, method, data) =>
287 rootDir.resolveRequest(method, rootPubKey, data, location.split('/'))
288
289 /* API Definition */
290 const StrappFileSystem = {
291 request: (location, method, data) =>
292 new Promise((resolve, reject) => StrappFileSystem.loadWaiters.push([
293 location, method, data, resolve, reject
294 ])),
295 get: (location) => StrappFileSystem.request(location, 'GET'),
296 set: (location, data) => StrappFileSystem.request(location, 'PUT', data)
297 }
298 StrappFileSystem.loadWaiters = []
299
300
301 /* Init sequence */
302 const _defaultFS = {
303 '.': {
304 acct: StrappDirectory.literal()
305 },
306 'acct': {
307 local: StrappFile.literal()
308 }
309 }
310 const _algo = {
311 name: 'RSA-OAEP',
312 modulusLength: 2048,
313 publicExponent: new Uint8Array([0x01, 0x00, 0x01]),
314 hash: { name: 'SHA-1' }
315 }
316
317 const _loadRootKeys = () =>
318 Promise.all([
319 localforage.getItem('/keys/id_rsa'),
320 localforage.getItem('/keys/id_rsa.pub')
321 ])
322 .then((files) =>
323 files[0] === null || files[1] === null ?
324 window.crypto.subtle.generateKey(_algo, true, ['encrypt', 'decrypt'])
325 .then((keyPair) => Promise.all([
326 window.crypto.subtle.exportKey('jwk', keyPair.privateKey),
327 window.crypto.subtle.exportKey('jwk', keyPair.publicKey)
328 ]))
329 .then((jwks) => Promise.all([
330 localforage.setItem('/keys/id_rsa', jwks[0]),
331 localforage.setItem('/keys/id_rsa.pub', jwks[1])
332 ])) :
333 Promise.resolve(files))
334 .then((jwks) => {
335 let hashAlg = jwks[0].alg.replace(_algo.name, '')
336 if (hashAlg.length === 0)
337 hashAlg = 'SHA-1'
338 else
339 hashAlg = 'SHA' + hashAlg
340 if (jwks[0].alg.indexOf(_algo.name) !== 0 || hashAlg != _algo.hash.name) {
341 console.log('Secure hash algorithm updated, old keys deleted')
342 return Promise.all([localforage.removeItem('/keys/id_rsa'),
343 localforage.removeItem('/keys/id_rsa.pub')
344 ]).then(_loadRootKeys)
345 }
346 rootPubKey = jwks[1].n
347 return localforage.assignItem('acct/local', { pubKey: jwks[1].n }).then(
348 () => window.crypto.subtle.importKey('jwk', jwks[0], _algo, true, ['decrypt']))
349 .then((key) => Promise.resolve(rootKey = key))
350 })
351
352 const _init = () => localforage.getItem('.')
353 .then((data) =>
354 data === null ?
355 Promise.all(
356 Object.keys(_defaultFS)
357 .map((key) => localforage.setItem(key, _defaultFS[key])))
358 .then(() => localforage.getItem('.')) :
359 Promise.resolve(data))
360 .then((rootFiles) => {
361 rootDir.loadFiles(rootFiles)
362 Object.assign(rootDir.files, _strappRuntime)
363 _loadRootKeys().then((data) => {
364 StrappFileSystem.request = _request
365 StrappFileSystem.loadWaiters
366 .map((w) => _request(w[0], w[1], w[2]).then(w[3], w[4]))
367 delete StrappFileSystem.loadWaiters
368 })
369 })
370
371 _init()
372
373 //localforage.clear().then(_init)
374
375 return StrappFileSystem
376 })()
377
378
379 export default StrappFileSystem
380 export {
381 StrappFile,
382 StrappPeerConnection,
383 StrappDirectory,
384 StrappDevice,
385 StrappJSON
386 }