[DeviceHome][VD] Fix setting the 'wsa' path
[platform/framework/web/wrtjs.git] / device_home / service / service.js
1 'use strict';
2
3 const express = require('express');
4 const fs = require('fs');
5 const http = require('http');
6 const path = require('path');
7 const relayServer = require('./relay-server.js');
8 const session = require('express-session');
9 const cookieParser = require('cookie-parser');
10 const EventEmitter = require('events');
11 const XMLHttpRequest = require('xmlhttprequest').XMLHttpRequest;
12 const crypto = require('crypto');
13 const { Security } = require('./security.js');
14 const JSEncryptLib = require('./jsencrypt');
15 const sessionMiddleware = session({
16   resave: true,
17   saveUninitialized: false,
18   secret: crypto.randomBytes(16).toString('base64'),
19   cookie: {
20     httpOnly: true,
21     secure: false,
22 }});
23
24 const PUBLIC_DOMAIN = 'https://devicehome.net';
25 const TAG = '[DeviceHome][service.js]'
26 const TIZEN_WEB_APP_SHARED_RESOURCES = 'shared/res/';
27 const WEBCLIP_DIRECTORY = 'webclip';
28 const WEBCLIP_MANIFEST = 'manifest.json';
29 const is_tv = webapis.cachedProperty !== undefined;
30 const local_ip = '127.0.0.1';
31 const non_ip_list = [
32   '1',
33   '192.168.250.250',
34   local_ip
35 ]
36
37 const security = new Security();
38
39 var apps = [];
40 var dataApps = [];
41 var clientRouter = express.Router();
42 var httpserver, evtEmit;
43 var platform_app_path = '/opt/usr/globalapps';
44 var platform_client_res_path = '/res/wgt/client';
45 var serverAppId = '';
46 var urlParam = '';
47 var g = {
48   port: 9000,
49   baseDir: __dirname,
50 };
51 var tryCount = 0;
52 var sip;
53 var clientPublicKeys = {};
54
55 // The pincode is disabled just for demo.
56 var DEMO_MODE = false;
57
58 function addD2Ddata(pkgId, appId, appName, iconPath) {
59   let app = {};
60   let metaAppID = '';
61   let metaDataArray = tizen.application.getAppMetaData(appId);
62   metaDataArray = metaDataArray.filter(function(metaData) {
63     if (metaData.key !== 'd2dservice')
64       return false;
65
66     if (metaData.value !== 'enable')
67       metaAppID = metaData.value;
68     return true;
69   });
70   metaDataArray.forEach(function() {
71     app.d2dApp = {
72       pkgId: pkgId,
73       appId: metaAppID === '' ? appId : metaAppID,
74       appName: appName,
75       iconPath: iconPath
76     },
77     app.path = path.join(platform_app_path, pkgId, TIZEN_WEB_APP_SHARED_RESOURCES);
78     console.log(`${TAG} app : ${JSON.stringify(app)}`);
79     dataApps.push(app);
80   });
81   return app;
82 }
83
84 function removeD2Ddata(packageId) {
85   for (var j = 0; j < dataApps.length; j++) {
86     if (packageId && !packageId.indexOf(dataApps[j].d2dApp.pkgId)) {
87       dataApps.splice(j, 1);
88     }
89   }
90 }
91
92 function setData() {
93   for (let i = 0; i < apps.length; i++) {
94     addD2Ddata(apps[i].packageId, apps[i].id, apps[i].name, apps[i].iconPath);
95   }
96 }
97
98 function getAppList(resolve) {
99   try {
100     tizen.application.getAppsInfo(function(applications) {
101       apps = applications;
102       setData();
103       getWebclipsManifest();
104       resolve(true);
105     });
106   } catch (err) {
107     resolve(false);
108   }
109 }
110
111 function promiseGetAppList() {
112   return new Promise(resolve => {
113     getAppList(resolve);
114   })
115 }
116
117 function checkWebclipSubDirectories(app, data, callback) {
118   return new Promise(resolve => {
119     tizen.filesystem.listDirectory(path.join(app.path, WEBCLIP_DIRECTORY), function(dirs) {
120       dirs.forEach(function (directory) {
121         const filePath = path.join(app.path, WEBCLIP_DIRECTORY, directory, WEBCLIP_MANIFEST);
122         console.log(`${TAG} webclip path : ${filePath}`);
123         const fileHandle = tizen.filesystem.openFile(filePath, 'r');
124
125         if (fileHandle) {
126           try {
127             data = fileHandle.readString();
128             data = data.replace(/\n/g, '');
129             data = JSON.parse(data);
130
131             app.webclips.push({
132               manifest: data,
133               path: path.join(app.path, WEBCLIP_DIRECTORY, directory),
134             });
135           } catch (err) {
136             console.log(`${TAG} fileHandle.readString directory (error): ${err}`);
137           }
138           fileHandle.close();
139         }
140       });
141       if (typeof callback === 'function') {
142         callback();
143       }
144       resolve(true);
145     }, function(err) {
146       console.log(`${TAG} ${err}`);
147       resolve(false);
148     });
149   });
150 }
151
152 function getWebclipManifestsByApp(app, callback) {
153   return new Promise(async resolve => {
154     const filePath = path.join(app.path, WEBCLIP_DIRECTORY, WEBCLIP_MANIFEST);
155     console.log(`${TAG} webclip path : ${filePath}`);
156     let data = '';
157     let fileHandle = undefined;
158     let skipMainDir = false;
159     app.webclips = [];
160
161     try {
162       fileHandle = tizen.filesystem.openFile(filePath, 'r');
163     } catch (err) {
164       skipMainDir = true;
165       console.log(`${TAG} No manifest in main directory : ${err}`);
166       // check subdirectories
167       console.log(`${TAG} Check subdirectories`);
168       await checkWebclipSubDirectories(app, data, callback);
169     }
170
171     if (fileHandle && !skipMainDir) {
172       try {
173         data = fileHandle.readString();
174         data = data.replace(/\n/g, '');
175         data = JSON.parse(data);
176
177         app.webclips.push({
178           manifest: data,
179           path: path.join(app.path, WEBCLIP_DIRECTORY)
180         });
181       } catch (err) {
182         console.log(`${TAG} fileHandle.readString (error): ${err}`);
183       }
184       fileHandle.close();
185
186       if (typeof callback === 'function') {
187         callback();
188       }
189     }
190     resolve(true);
191   });
192 }
193
194 function getWebclipsManifest() {
195   dataApps.forEach(
196     getWebclipManifestsByApp
197   );
198 }
199
200 function setPackageInfoEventListener() {
201   const packageEventCallback = {
202     oninstalled: async function(packageInfo) {
203       console.log(`${TAG} The package ${packageInfo.name} is installed`);
204       const app = addD2Ddata(packageInfo.id, packageInfo.appIds[0], packageInfo.name, packageInfo.iconPath);
205       if (app.path !== undefined) {
206         // only for webclip
207         await getWebclipManifestsByApp(app, function () {
208           console.log(`${TAG} Webclip has been updated`);
209         });
210         console.log(`${TAG} Emit app list`);
211         // for both companion and webclip
212         evtEmit.emit('updateapplist', 'message', dataApps);
213         relayServer(httpserver, dataApps, sessionMiddleware, clientPublicKeys, packageInfo.id);
214       } else {
215         evtEmit.emit('updateapplist', 'message', dataApps);
216       }
217     },
218     onupdated: function(packageInfo) {
219       console.log(`${TAG} The package ${packageInfo.name} is updated`);
220     },
221     onuninstalled: function(packageId) {
222       console.log(`${TAG} The package ${packageId} is uninstalled`);
223       removeD2Ddata(packageId);
224       evtEmit.emit('updateapplist', 'message', dataApps);
225     }
226   };
227   tizen.package.setPackageInfoEventListener(packageEventCallback);
228 }
229
230 function unsetPackageInfoEventListener() {
231   tizen.package.unsetPackageInfoEventListener();
232 }
233
234 function getWebClipsList() {
235   let result = [];
236   let webclips = [];
237
238   dataApps.forEach(function(app) {
239     webclips = [];
240     app.webclips.forEach(function (webclip) {
241       webclips.push({
242         url: path.join('client', 'webclip', webclip.manifest.name),
243         isSelected: true
244       });
245     });
246     result.push({
247       appId: app.d2dApp.appId,
248       pkgId: app.d2dApp.pkgId,
249       isInstalled: true,
250       isActive: false,
251       webClipsList: webclips
252     });
253   });
254
255   return result;
256 }
257
258 function comparePincode(req, res, encrypted) {
259   console.log(`${TAG} comparePincode`);
260   console.log(`${TAG} encrypted : ${encrypted}`);
261   // Decrypt pincode using private key
262   const decrypt = new JSEncryptLib();
263   decrypt.setPrivateKey(req.session.serverPrivateKey);
264   const decrypted = decrypt.decrypt(encrypted);
265
266   const pincode_passed = decrypted === req.session.pincode ? true : false;
267   console.log(`${TAG} pincode result : ${pincode_passed}`);
268   if (pincode_passed) {
269     // The pincode is disposable.
270     req.session.pincode = undefined;
271     if (!req.session.ip || req.session.ip === undefined) {
272       req.session.ip = sip;
273     }
274     hashing(req, res);
275     console.log(`${TAG} pincode passed`);
276   }
277   tryCount++;
278   if (tryCount === 5 && !pincode_passed) {
279     tryCount = 0;
280     displayPincode(req);
281     res.send('retry');
282   } else {
283     res.send(pincode_passed);
284   }
285 }
286
287 async function displayPincode(req) {
288   // Generate pincode
289   const byteData = crypto.randomBytes(256);
290   req.session.pincode = parseInt(byteData.toString('hex').substr(0, 8), 16).toString().substr(0, 4);
291   // Generate RSA keys
292   await security.awaitKeyPair(req);
293   // Show pincode popup
294   webapis.postPlainNotification('Input Pincode: ', req.session.pincode, 10);
295 }
296
297 function getIp(rawIp) {
298   return rawIp.slice(rawIp.lastIndexOf(':') + 1);
299 }
300
301 var HTTPserverStart = function() {
302   evtEmit = new EventEmitter();
303   const app = express();
304   app.engine('html', require('ejs').renderFile);
305   app.set('view engine', 'ejs');
306   app.set('views', `${g.baseDir}/../`);
307   app.disable('x-powered-by');
308   app.use('/pincode', express.static(`${g.baseDir}/../pincode`));
309   app.use(cookieParser());
310
311   // For post method
312   app.use(express.urlencoded({
313     extended: true
314   }));
315   app.use(express.json());
316   // For session management
317   app.use(sessionMiddleware);
318
319   var sessionChecker = function(req, res, next) {
320     console.log(`${TAG} url : ${req.url}`);
321     console.log(`${TAG} session id : ${req.session.id}`);
322     sip = getIp(req.socket.remoteAddress);
323     console.log(`${TAG} ip : ${sip}`);
324     // The pincode page and local connections are allowed without session.
325     if (req.session.ip !== sip &&
326         req.session.ip === undefined &&
327         !req.url.includes('id=') &&
328         !req.url.includes('/pincode/') &&
329         !non_ip_list.includes(sip) &&
330         req.session.exchange !== req.cookies.exchange) {
331       console.log(`${TAG} Not valid access`);
332       res.redirect(401, PUBLIC_DOMAIN);
333     } else if (hasFilename(req.url)) {
334       next();
335     } else {
336       hashing(req, res);
337       next();
338     }
339   };
340
341   var hasFilename = function(url) {
342     if (url) {
343       var m = url.toString().match(/.*\/(.+?)\./);
344       if (m && m.length > 1)
345         return true;
346     }
347     return false;
348   }
349
350   if (!DEMO_MODE)
351     app.use(sessionChecker);
352
353   const appProxy = require('./app_proxy');
354   app.use('/app', appProxy(app, g.port));
355   app.use('/client', clientRouter);
356   console.log(`${TAG} __dirname: ${__dirname}`);
357
358   if (is_tv) {
359     platform_app_path = '/opt/usr/apps';
360     if (__dirname.indexOf('/wsa/') > -1) {
361       platform_client_res_path = '/res/wsa/client';
362     }
363     console.log(`${TAG} TV Profile`);
364   }
365
366   var tizenApp = tizen.application.getCurrentApplication();
367   console.log(`${TAG} ID, packageId: ${tizenApp.appInfo.id} ${tizenApp.appInfo.packageId}`);
368   serverAppId = tizenApp.appInfo.id.split('.')[0];
369   g.baseDir = __dirname.split(serverAppId)[0];
370   console.log(`${TAG} g.baseDir: ${g.baseDir}`);
371
372   clientRouter.get('/webclip/*', function(req, res) {
373     let file = req.originalUrl.replace('/client/webclip/', '').replace(/\?.+$/, '');
374     let webclipName = '';
375     let appId = '';
376     let webclip;
377
378     const match = file.match(/^[^\/]+/);
379     if (match) {
380       webclipName = match[0];
381     }
382     console.log(`${TAG} webclip name: ${webclipName}`);
383
384     // find appId by webclip name
385     const app = dataApps.filter(function (app) {
386       let webclipTemp = app.webclips.filter(function (webclip) {
387         return webclip.manifest.name === webclipName;
388       })[0];
389       if (webclipTemp) {
390         webclip = webclipTemp;
391       }
392       return !!webclipTemp;
393     })[0];
394     if (app) {
395       appId = app.d2dApp.pkgId;
396     }
397
398     console.log(`${TAG} root : ${platform_app_path}/${appId}/${TIZEN_WEB_APP_SHARED_RESOURCES}/${WEBCLIP_DIRECTORY}`);
399     const options = {
400       root: webclip.path
401     };
402
403     // remove webclip name from path
404     file = file.replace(webclipName + '/', '');
405     // decode chars like %20 - space
406     file = window.decodeURI(file);
407     res.sendFile(file, options, function(err) {
408       if (err) {
409         console.log(`${TAG} err: ${err}`);
410         res.status(404).send(`${err}`);
411       } else {
412         console.log(`${TAG} res.sendFile() done: ${file}`);
413       }
414     });
415   });
416
417   clientRouter.get('/updateWebclip', function(req, res) {
418     console.log(`${TAG} get(/updateWebclip)`);
419     const apps = getWebClipsList();
420     const result = {
421       type: 'full',
422       data: {
423         apps: apps
424       }
425     }
426     console.log(`${TAG} webclip : ${JSON.stringify(result)}`);
427     res.send(result);
428   });
429
430   clientRouter.get('/*', function(req, res) {
431     const file = req.originalUrl.replace('/client/', '').replace(/\?.+$/, '');
432     const pkgId = webapis.getPackageId();
433     const fullPath = require('path').join(g.baseDir, pkgId, platform_client_res_path, file);
434     console.log(`${TAG} pkgId: ${pkgId}, fullPath: ${fullPath}`);
435     res.sendFile(fullPath);
436   });
437
438   app.get('/', async function(req, res) {
439     console.log(`${TAG} URL parameter : ${req.originalUrl}`);
440     urlParam = req.originalUrl;
441
442     if (!DEMO_MODE) {
443       res.redirect('/pincode/pincode.html');
444     } else {
445       console.log(`${TAG} Set session ip and RSA keys in WT mode`);
446       req.session.ip = getIp(req.socket.remoteAddress);
447       await security.awaitKeyPair(req);
448       if (req.query.roomId !== undefined) {
449         // FIXME: Remove app logic here
450         res.render('client/invited.html');
451       } else if (req.query.pageUrl !== undefined) {
452         if (typeof webapis.mde !== 'undefined')
453           webapis.mde.launchBrowserFromUrl(req.query.pageUrl);
454       } else {
455         // Device home
456         res.render('client/client.html');
457       }
458     }
459   });
460
461   app.get('/d2dIcon/*', (req, res) => {
462     let fullPath = req.originalUrl.replace('d2dIcon', platform_app_path);
463     res.sendFile(fullPath);
464   });
465
466   app.get('/appList', (req, res) => {
467     res.send(dataApps);
468   });
469
470   app.get('/updateAppList', (req, res) => {
471     res.writeHead(200, {
472       'Content-Type': 'text/event-stream',
473       'Cache-Control': 'no-cache',
474       'Connection': 'keep-alive'
475     });
476     evtEmit.on('updateapplist', (event, data) => {
477       res.write('event: ' + String(event) + '\n' + 'data: ' + JSON.stringify(data) + '\n\n');
478     });
479   });
480
481   app.get('/pincode/publicKey', async (req, res) => {
482     tryCount = 0;
483     await displayPincode(req);
484     res.send(req.session.serverPublicKey);
485   });
486
487   app.get('/pincode/getServerPublicKey', (req, res) => {
488     res.send(req.session.serverPublicKey);
489   });
490
491   app.post('/pincode/pinCodeToServer', express.json(), (req, res) => {
492     // Verify encrypted pincode
493     const resultData = req.body['pincode'];
494     console.log(`${TAG} pinCodeToServer resultData : ${resultData}`);
495     comparePincode(req, res, resultData);
496   });
497
498   app.post('/pincode/publicKeyToServer', express.json(), (req, res) => {
499     const pkgId = req.body['pkgId']
500     if (clientPublicKeys[pkgId] === undefined)
501       clientPublicKeys[pkgId] = {};
502     const ip  = req.session.ip.toString();
503     clientPublicKeys[pkgId][ip] = req.body['publicKey'];
504     console.log(`${TAG} client publicKey : ${clientPublicKeys[pkgId][ip]}`);
505   });
506
507   app.post('/d2d', (req, res) => {
508     if (req.session.ip !== undefined) {
509       console.log(`${TAG} client.html`);
510       res.render('client/client.html');
511     } else {
512       console.log(`${TAG} no client.html`);
513       res.redirect(401, PUBLIC_DOMAIN);
514     }
515   });
516
517   // receive data or cmd to app on device
518   app.post('/app', (req, res) => {
519     res.send({
520       result: 'ok'
521     });
522   });
523
524   app.get('/service', (req, res) => {
525   });
526
527   app.post('/url', (req, res) => {
528     if (typeof webapis.mde !== 'undefined')
529       webapis.mde.launchBrowserFromUrl(req.body.url);
530   });
531
532   httpserver = http.createServer(app);
533   httpserver.listen(g.port, function() {
534     console.log(`Device home is running on port ${g.port}`);
535   });
536   relayServer(httpserver, dataApps, sessionMiddleware, clientPublicKeys);
537 };
538
539 function hashing(req, res) {
540   const hash = crypto.randomBytes(16).toString('base64');
541   console.log(`${TAG} hash : ${hash}`);
542
543   res.cookie('exchange', hash, {
544     httpOnly: true
545   });
546   req.session.exchange = hash;
547 }
548
549 module.exports.getUrlParam = function () {
550   console.log(`${TAG} getUrlParam is called`);
551   return urlParam;
552 };
553
554 module.exports.onStart = async function() {
555   await promiseGetAppList();
556   HTTPserverStart();
557   setPackageInfoEventListener();
558   console.log(`${TAG} onStart is called in DNS Resolver`);
559 };
560
561 module.exports.onStop = function() {
562   if (httpserver) {
563     httpserver.close();
564     console.log(`${TAG} Server Terminated`);
565   }
566   unsetPackageInfoEventListener();
567   evtEmit.off('updateapplist');
568   console.log(`${TAG} onStop is called in DNS Resolver`);
569 };
570
571 module.exports.onRequest = function() {
572 };