import * as archiver from 'archiver'; import axios from 'axios'; import * as cheerio from 'cheerio'; import * as express from 'express'; import * as FormData from 'form-data'; import * as fs from 'fs'; import * as http from 'http'; import { networkInterfaces } from 'os'; import * as Path from 'path'; import * as QRCode from 'qrcode'; import * as request from 'request'; import { v4 as uuidv4 } from 'uuid'; import * as vscode from 'vscode'; export class ServerInstance { private statusBarItem: vscode.StatusBarItem; private outputChannel: vscode.OutputChannel; private server: http.Server | undefined | null; private portNumber: number | undefined | null; private expressApp = express(); constructor() { this.statusBarItem = vscode.window.createStatusBarItem( vscode.StatusBarAlignment.Right, 1 ); this.outputChannel = vscode.window.createOutputChannel( 'PayPaySimulatorServer' ); } start(portNumber: number, workspacePath: string) { return new Promise((resolve, reject) => { if (this.server) { this.outputChannel.appendLine('Simulator already started'); reject(new Error('Simulator already started')); } else if (!portNumber) { this.outputChannel.appendLine('Simulator not started - Invalid Port'); reject(new Error('Simulator not started - Invalid Port')); } else { this.outputChannel.appendLine( `Server is starting to listen to port ${portNumber}` ); this.portNumber = portNumber; const extPath = vscode.extensions.getExtension('PayPay.paypay-devtool') ?.extensionPath; this.expressApp.use('/request/', (req, res) => { req.pipe(request(req.url.substr(1))).pipe(res); }); this.expressApp.use('/simulator', express.static(extPath + '/build')); this.expressApp.get('/', function (_req, res) { res.redirect('/simulator'); }); // WIP: // this.expressApp.get('/notification-center', (req, res) => { // const newUrl = req.query.url + ''; // req // .pipe( // request({ // url: newUrl, // headers: { // Authorization: // 'Uct2BV0z6.....B3pV5LfA969BzcM4Q==', // }, // }) // ) // .pipe(res); // }); /* * Package */ this.expressApp.use('/publish/package', async function (req, res) { let archive = archiver('zip', { zlib: { level: 9 } }); //delete old zip try { if (fs.existsSync(workspacePath + '/app.zip')) { fs.unlinkSync(workspacePath + '/app.zip'); } } catch (err) { vscode.window.showErrorMessage(err.message); res.status(500).send({ error: err.message }); } let output = await fs.createWriteStream(workspacePath + '/app.zip'); archive.on('error', function (err) { vscode.window.showErrorMessage(err.message); res.status(500).send({ error: err.message }); }); archive.pipe(output); archive.append('', { name: 'app/' }); archive.glob( '**', { cwd: workspacePath, ignore: ['app.zip', '.*'], }, { prefix: 'app/' } ); await archive.finalize(); res.send({ project: 'app.zip' }); }); /* * Upload */ this.expressApp.use('/publish/upload', async function (req, res) { try { let zipData = fs.readFileSync(workspacePath + '/app.zip'); if (!zipData) { return res.status(500).send({ error: 'Package not found' }); } const form = new FormData(); form.append('file', zipData, { filename: 'app.zip' }); form.append('version', req.query.version); form.append('activate', req.query.activate); const uploadUrl = 'http://localhost:' + portNumber + '/request/https://stg.paypay-corp.co.jp/opa-mini-app/v1/files/upload?clientId=' + req.query.clientID; const formHeaders = form.getHeaders(); await axios .post(uploadUrl, form, { headers: { ...formHeaders, 'Content-Length': form.getLengthSync(), Authorization: 'Bearer ' + req.query.serviceToken, }, }) .then((response) => { if (response.data.resultInfo.code !== 'SUCCESS') { throw new Error('Unable to upload project'); } }) .catch((err) => { throw err; }); } catch (err) { return res.status(500).send({ error: err }); } res.send(); }); this.expressApp.use('/network', function (req, res, next) { try { let addresses = []; const interfaces = networkInterfaces(); for (var key in interfaces) { let interfaceValues: String[] = []; interfaces[key]?.forEach((element) => { element['address'] && interfaceValues.push(element['address']); }); addresses.push({ nic: key, values: interfaceValues }); } res.send(JSON.stringify({ addresses: addresses })); } catch (err) { return res.status(500).send({ error: err }); } res.send(); }); this.expressApp.use('/generateqrcode', async function (req, res) { try { const qrPath = extPath + '/tmp/MiniAppBarcode/'; if (!fs.existsSync(qrPath)) { fs.mkdirSync(qrPath, { recursive: true }); } const qrFilePath = qrPath + uuidv4() + '.png'; await QRCode.toFile( qrFilePath, req.query.data as string, { margin: 0 }, function (err) { if (err) { throw err; } } ); res.send(JSON.stringify({ filePath: qrFilePath })); } catch (err) { return res.status(500).send({ error: err }); } res.send(); }); ///download files this.expressApp.use('/downloadfile', async function (req, res) { try { const response = await axios({ method: 'GET', url: req.query.url as string, responseType: 'stream', headers: req.query.header, }); if ( Number(response?.headers['content-length']) >= 52428800 //50Mb in bytes ) { return res.status(500).send({ error: 'SIZE_LIMIT_EXCEEDED' }); } let filePath = req.query.filePath ? (req.query.filePath as string) : ''; let filePathwithName = false; const getFileName = function () { let name = ''; let fileExt = response?.headers['content-type'].split('/')[1]; if (!filePath || filePath.charAt(filePath.length - 1) === '/') { name = response?.request?.path?.substring( response?.request?.path?.lastIndexOf('/') + 1 ); name = name?.split('?')[0]; if (name.indexOf('.') === -1) { return name + '.' + fileExt; } } else if (filePath.indexOf('.') !== -1) { filePathwithName = true; return ''; } else { filePathwithName = true; return '.' + fileExt; } return name; }; const dirPath = workspacePath + '/files/' + filePath + getFileName(); let createFoldPath; if (filePathwithName) { let pth = filePath.substring(0, filePath.lastIndexOf('/') + 1); createFoldPath = workspacePath + '/files/' + pth; } else if (filePath.charAt(filePath.length - 1) === '/') { createFoldPath = workspacePath + '/files/' + filePath; } else { createFoldPath = workspacePath + '/files/'; } if (!fs.existsSync(createFoldPath)) { fs.mkdirSync(createFoldPath, { recursive: true, }); } let writer = fs.createWriteStream(dirPath); response.data.pipe(writer); writer.on('error', function (err) { return res.status(500).send({ error: 'INVALID_PATH' }); }); res.send( JSON.stringify( req.query.filePath ? { filePath: dirPath, statusCode: 200, } : { tempFilePath: dirPath, statusCode: 200, } ) ); } catch (err) { return res.status(500).send({ error: 'UNKNOWN' }); } res.send(); }); this.expressApp.use('/getsavedfilelist', async function (req, res) { try { const getAllFiles = function (dirPath: any, arrayOfFiles: any) { let files = fs.readdirSync(dirPath); arrayOfFiles = arrayOfFiles || []; files.forEach(function (file) { let fsState = fs.statSync(dirPath + '/' + file); if (fsState.isDirectory()) { arrayOfFiles = getAllFiles( dirPath + '/' + file, arrayOfFiles ); } else { // eslint-disable-next-line no-useless-escape if (!/(^|\/)\.[^\/\.]/g.test(file)) { arrayOfFiles.push({ filePath: Path.join(dirPath, '/', file), size: fsState.size, createTime: fsState.birthtimeMs, }); } } }); return arrayOfFiles; }; const fileList = getAllFiles(workspacePath + '/files/', []); res.send(fileList); } catch (err) { return res.status(500).send({ error: err }); } res.send(); }); this.expressApp.use('/removesavedfile', async function (req, res) { try { fs.unlinkSync(req.query.filePath as string); res.send(); } catch (err) { return res.status(500).send({ error: err }); } res.send(); }); /* * Workspace */ if (workspacePath.length) { // TODO: improve detection of Vuejs projects if (fs.existsSync(workspacePath + '/src/pages')) { workspacePath += '/dist'; } this.expressApp.use('/workspace', function (req, res, next) { //json try { const jsonFile = workspacePath + req.path; if (jsonFile.endsWith('.json')) { if (jsonFile.includes('mock.json')) { try { const mockJson = JSON.parse( fs.readFileSync(jsonFile).toString() ); res.send(mockJson); } catch (error) { res.send(''); } } let pageJson = JSON.parse(fs.readFileSync(jsonFile).toString()); if (req.originalUrl.toLowerCase() === '/workspace/app.json') { let defaultPageJson = { window: { navigationBarBackgroundColor: '#f7f7f7', navigationBarTextStyle: 'black', navigationBarTitleText: '', navigationStyle: 'default', backgroundColor: '#ffffff', enablePullDownRefresh: false, }, }; Object.assign(defaultPageJson.window, pageJson.window); req.app.set('defaultPageJson', defaultPageJson); Object.assign(pageJson, defaultPageJson); } res.send(pageJson); } } catch (err) { res.send(req.app.get('defaultPageJson')); } //html try { let htmlFile = workspacePath + req.path; if (!htmlFile.endsWith('.html')) { htmlFile += '.html'; } let html = fs.readFileSync(htmlFile); let $ = cheerio.load(html); $('body').prepend(``); $('body').prepend( '' ); if ( req.app.get('defaultPageJson').window?.enablePullDownRefresh ) { $('body').append(` `); } res.send($.html()); } catch { next(); } }); //default fallback this.expressApp.use('/workspace', express.static(workspacePath)); } else { this.outputChannel.appendLine( 'Simulator not started - No project selected' ); return reject( new Error('Simulator not started - No project selected') ); /*app.use( '/workspace', express.static(extPath + '/build/workspace/empty') );*/ } this.outputChannel.show(true); this.server = http .createServer(this.expressApp) .listen(portNumber, () => { this.statusBarItem.command = 'paypay.devtool.simulator.openbrowser'; this.statusBarItem.tooltip = 'Click here to open in browser'; this.statusBarItem.text = `$(server) Simulator ${portNumber}`; this.statusBarItem.show(); this.outputChannel.appendLine('Server started'); resolve(); }) .on('error', (err) => { this.outputChannel.appendLine( `Failed to start server due to ${err.message}` ); reject(err); this.server = null; }) .on('request', (req, _res) => { if (!req.originalUrl.startsWith('/simulator/')) { this.outputChannel.appendLine(`${req.method} ${req.originalUrl}`); } }); } }); } stop() { return new Promise((resolve, reject) => { if (this.server) { this.outputChannel.appendLine('Simulator is stopping'); this.server.close(() => { this.server = null; this.portNumber = null; this.statusBarItem && this.statusBarItem.hide(); if (this.outputChannel) { this.outputChannel.appendLine('Simulator stopped'); this.outputChannel.hide(); } resolve(); }); } else { this.outputChannel.appendLine('Simulator is not running'); reject(new Error('Simulator is not running')); } }); } getUrl() { return this.server ? 'http://localhost:' + this.portNumber : null; } dispose() { this.stop(); this.statusBarItem.dispose(); this.outputChannel.dispose(); } }