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