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