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({
17 saveUninitialized: false,
18 secret: crypto.randomBytes(16).toString('base64'),
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';
37 const security = new Security();
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';
53 var clientPublicKeys = {};
55 // The pincode is disabled just for demo.
56 var DEMO_MODE = false;
58 function addD2Ddata(pkgId, appId, appName, iconPath) {
61 let metaDataArray = tizen.application.getAppMetaData(appId);
62 metaDataArray = metaDataArray.filter(function(metaData) {
63 if (metaData.key !== 'd2dservice')
66 if (metaData.value !== 'enable')
67 metaAppID = metaData.value;
70 metaDataArray.forEach(function() {
73 appId: metaAppID === '' ? appId : metaAppID,
77 app.path = path.join(platform_app_path, pkgId, TIZEN_WEB_APP_SHARED_RESOURCES);
78 console.log(`${TAG} app : ${JSON.stringify(app)}`);
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);
93 for (let i = 0; i < apps.length; i++) {
94 addD2Ddata(apps[i].packageId, apps[i].id, apps[i].name, apps[i].iconPath);
98 function getAppList(resolve) {
100 tizen.application.getAppsInfo(function(applications) {
103 getWebclipsManifest();
111 function promiseGetAppList() {
112 return new Promise(resolve => {
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');
127 data = fileHandle.readString();
128 data = data.replace(/\n/g, '');
129 data = JSON.parse(data);
133 path: path.join(app.path, WEBCLIP_DIRECTORY, directory),
136 console.log(`${TAG} fileHandle.readString directory (error): ${err}`);
141 if (typeof callback === 'function') {
146 console.log(`${TAG} ${err}`);
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}`);
157 let fileHandle = undefined;
158 let skipMainDir = false;
162 fileHandle = tizen.filesystem.openFile(filePath, 'r');
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);
171 if (fileHandle && !skipMainDir) {
173 data = fileHandle.readString();
174 data = data.replace(/\n/g, '');
175 data = JSON.parse(data);
179 path: path.join(app.path, WEBCLIP_DIRECTORY)
182 console.log(`${TAG} fileHandle.readString (error): ${err}`);
186 if (typeof callback === 'function') {
194 function getWebclipsManifest() {
196 getWebclipManifestsByApp
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) {
207 await getWebclipManifestsByApp(app, function () {
208 console.log(`${TAG} Webclip has been updated`);
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);
215 evtEmit.emit('updateapplist', 'message', dataApps);
218 onupdated: function(packageInfo) {
219 console.log(`${TAG} The package ${packageInfo.name} is updated`);
221 onuninstalled: function(packageId) {
222 console.log(`${TAG} The package ${packageId} is uninstalled`);
223 removeD2Ddata(packageId);
224 evtEmit.emit('updateapplist', 'message', dataApps);
227 tizen.package.setPackageInfoEventListener(packageEventCallback);
230 function unsetPackageInfoEventListener() {
231 tizen.package.unsetPackageInfoEventListener();
234 function getWebClipsList() {
238 dataApps.forEach(function(app) {
240 app.webclips.forEach(function (webclip) {
242 url: path.join('client', 'webclip', webclip.manifest.name),
247 appId: app.d2dApp.appId,
248 pkgId: app.d2dApp.pkgId,
251 webClipsList: webclips
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);
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;
275 console.log(`${TAG} pincode passed`);
278 if (tryCount === 5 && !pincode_passed) {
283 res.send(pincode_passed);
287 async function displayPincode(req) {
289 const byteData = crypto.randomBytes(256);
290 req.session.pincode = parseInt(byteData.toString('hex').substr(0, 8), 16).toString().substr(0, 4);
292 await security.awaitKeyPair(req);
293 // Show pincode popup
294 webapis.postPlainNotification('Input Pincode: ', req.session.pincode, 10);
297 function getIp(rawIp) {
298 return rawIp.slice(rawIp.lastIndexOf(':') + 1);
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());
312 app.use(express.urlencoded({
315 app.use(express.json());
316 // For session management
317 app.use(sessionMiddleware);
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)) {
341 var hasFilename = function(url) {
343 var m = url.toString().match(/.*\/(.+?)\./);
344 if (m && m.length > 1)
351 app.use(sessionChecker);
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}`);
359 platform_app_path = '/opt/usr/apps';
360 if (__dirname.indexOf('/wsa/') > -1) {
361 platform_client_res_path = '/res/wsa/client';
363 console.log(`${TAG} TV Profile`);
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}`);
372 clientRouter.get('/webclip/*', function(req, res) {
373 let file = req.originalUrl.replace('/client/webclip/', '').replace(/\?.+$/, '');
374 let webclipName = '';
378 const match = file.match(/^[^\/]+/);
380 webclipName = match[0];
382 console.log(`${TAG} webclip name: ${webclipName}`);
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;
390 webclip = webclipTemp;
392 return !!webclipTemp;
395 appId = app.d2dApp.pkgId;
398 console.log(`${TAG} root : ${platform_app_path}/${appId}/${TIZEN_WEB_APP_SHARED_RESOURCES}/${WEBCLIP_DIRECTORY}`);
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) {
409 console.log(`${TAG} err: ${err}`);
410 res.status(404).send(`${err}`);
412 console.log(`${TAG} res.sendFile() done: ${file}`);
417 clientRouter.get('/updateWebclip', function(req, res) {
418 console.log(`${TAG} get(/updateWebclip)`);
419 const apps = getWebClipsList();
426 console.log(`${TAG} webclip : ${JSON.stringify(result)}`);
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);
438 app.get('/', async function(req, res) {
439 console.log(`${TAG} URL parameter : ${req.originalUrl}`);
440 urlParam = req.originalUrl;
443 res.redirect('/pincode/pincode.html');
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);
456 res.render('client/client.html');
461 app.get('/d2dIcon/*', (req, res) => {
462 let fullPath = req.originalUrl.replace('d2dIcon', platform_app_path);
463 res.sendFile(fullPath);
466 app.get('/appList', (req, res) => {
470 app.get('/updateAppList', (req, res) => {
472 'Content-Type': 'text/event-stream',
473 'Cache-Control': 'no-cache',
474 'Connection': 'keep-alive'
476 evtEmit.on('updateapplist', (event, data) => {
477 res.write('event: ' + String(event) + '\n' + 'data: ' + JSON.stringify(data) + '\n\n');
481 app.get('/pincode/publicKey', async (req, res) => {
483 await displayPincode(req);
484 res.send(req.session.serverPublicKey);
487 app.get('/pincode/getServerPublicKey', (req, res) => {
488 res.send(req.session.serverPublicKey);
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);
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]}`);
507 app.post('/d2d', (req, res) => {
508 if (req.session.ip !== undefined) {
509 console.log(`${TAG} client.html`);
510 res.render('client/client.html');
512 console.log(`${TAG} no client.html`);
513 res.redirect(401, PUBLIC_DOMAIN);
517 // receive data or cmd to app on device
518 app.post('/app', (req, res) => {
524 app.get('/service', (req, res) => {
527 app.post('/url', (req, res) => {
528 if (typeof webapis.mde !== 'undefined')
529 webapis.mde.launchBrowserFromUrl(req.body.url);
532 httpserver = http.createServer(app);
533 httpserver.listen(g.port, function() {
534 console.log(`Device home is running on port ${g.port}`);
536 relayServer(httpserver, dataApps, sessionMiddleware, clientPublicKeys);
539 function hashing(req, res) {
540 const hash = crypto.randomBytes(16).toString('base64');
541 console.log(`${TAG} hash : ${hash}`);
543 res.cookie('exchange', hash, {
546 req.session.exchange = hash;
549 module.exports.getUrlParam = function () {
550 console.log(`${TAG} getUrlParam is called`);
554 module.exports.onStart = async function() {
555 await promiseGetAppList();
557 setPackageInfoEventListener();
558 console.log(`${TAG} onStart is called in DNS Resolver`);
561 module.exports.onStop = function() {
564 console.log(`${TAG} Server Terminated`);
566 unsetPackageInfoEventListener();
567 evtEmit.off('updateapplist');
568 console.log(`${TAG} onStop is called in DNS Resolver`);
571 module.exports.onRequest = function() {