diff --git a/lib/webInterface.js b/lib/webInterface.js index 9620663..4285728 100644 --- a/lib/webInterface.js +++ b/lib/webInterface.js @@ -1,7 +1,5 @@ 'use strict'; -const DEFAULT_WEB_PORT = 8002; // port for web interface - const express = require('express'), url = require('url'), bodyParser = require('body-parser'), @@ -16,9 +14,13 @@ const express = require('express'), ip = require('ip'), compress = require('compression'); +const DEFAULT_WEB_PORT = 8002; // port for web interface + const packageJson = require('../package.json'); const MAX_CONTENT_SIZE = 1024 * 2000; // 2000kb + +const certFileTypes = ['crt', 'cer', 'pem', 'der']; /** * * @@ -204,8 +206,9 @@ class webInterface extends events.EventEmitter { res.setHeader('Access-Control-Allow-Origin', '*'); const _crtFilePath = certMgr.getRootCAFilePath(); if (_crtFilePath) { + const fileType = certFileTypes.indexOf(req.query.type) !== -1 ? req.query.type : 'crt'; res.setHeader('Content-Type', 'application/x-x509-ca-cert'); - res.setHeader('Content-Disposition', 'attachment; filename="rootCA.crt"'); + res.setHeader('Content-Disposition', `attachment; filename="rootCA.${fileType}"`); res.end(fs.readFileSync(_crtFilePath, { encoding: null })); } else { res.setHeader('Content-Type', 'text/html'); @@ -213,25 +216,12 @@ class webInterface extends events.EventEmitter { } }); - //make qr code - app.get('/qr', (req, res) => { - res.setHeader('Access-Control-Allow-Origin', '*'); - res.setHeader('Content-Type', 'text/html'); - - const qr = qrCode.qrcode(4, 'M'); - const targetUrl = req.protocol + '://' + req.get('host'); - qr.addData(targetUrl); - qr.make(); - const qrImageTag = qr.createImgTag(4); - const resDom = '<a href="__url"> __img <br> click or scan qr code to start client </a>'.replace(/__url/, targetUrl).replace(/__img/, qrImageTag); - res.end(resDom); - }); - app.get('/api/getQrCode', (req, res) => { res.setHeader('Access-Control-Allow-Origin', '*'); + const fileType = certFileTypes.indexOf(req.query.type) !== -1 ? req.query.type : 'crt'; const qr = qrCode.qrcode(4, 'M'); - const targetUrl = req.protocol + '://' + req.get('host') + '/fetchCrtFile'; + const targetUrl = req.protocol + '://' + req.get('host') + '/fetchCrtFile?type=' + fileType; const isRootCAFileExists = certMgr.isRootCAFileExists(); qr.addData(targetUrl); diff --git a/test/spec_lib/webInterface.js b/test/spec_lib/webInterface.js new file mode 100644 index 0000000..3eac176 --- /dev/null +++ b/test/spec_lib/webInterface.js @@ -0,0 +1,44 @@ +const WebInterface = require('../../lib/webInterface.js'); +const Recorder = require('../../lib/recorder'); +const { directGet } = require('../util/HttpUtil.js'); + +describe('WebInterface server', () => { + let webServer = null; + let webHost = 'http://127.0.0.1:8002'; + + beforeAll(() => { + const recorder = new Recorder(); + webServer = new WebInterface({ + webPort: 8002, + }, recorder); + }); + + afterAll(() => { + webServer.close(); + }); + + it('should support change CA extensions in /getQrCode', done => { + const certFileTypes = ['crt', 'cer', 'pem', 'der']; + const tasks = certFileTypes.map((type) => { + return directGet(`${webHost}/api/getQrCode`, { type }) + .then(res => { + const body = JSON.parse(res.body); + expect(body.qrImgDom).toMatch('<img src="data:image/'); + expect(body.url).toBe(`${webHost}/fetchCrtFile?type=${type}`); + }); + }); + + Promise.all(tasks) + .then(done) + .catch(done); + }); + + it('should fallback to .crt file in /getQrCode', done => { + directGet(`${webHost}/api/getQrCode`, { type: 'unkonw' }) + .then(res => { + expect(JSON.parse(res.body).url).toBe(`${webHost}/fetchCrtFile?type=crt`); + done(); + }) + .catch(done); + }); +}); \ No newline at end of file diff --git a/web/src/component/download-root-ca.jsx b/web/src/component/download-root-ca.jsx index 55d1322..ba63f57 100644 --- a/web/src/component/download-root-ca.jsx +++ b/web/src/component/download-root-ca.jsx @@ -5,26 +5,29 @@ import React, { PropTypes } from 'react'; import ReactDOM from 'react-dom'; -import ClassBind from 'classnames/bind'; import { connect } from 'react-redux'; -import { message, Button, Spin } from 'antd'; +import { message, Button, Spin, Select } from 'antd'; import ResizablePanel from 'component/resizable-panel'; import { hideRootCA, updateIsRootCAExists } from 'action/globalStatusAction'; import { MenuKeyMap } from 'common/Constant'; -import { getJSON, ajaxGet, postJSON } from 'common/ApiUtil'; +import { getJSON, postJSON } from 'common/ApiUtil'; import Style from './download-root-ca.less'; import CommonStyle from '../style/common.less'; +const certFileTypes = ['crt', 'cer', 'pem', 'der']; + class DownloadRootCA extends React.Component { constructor () { super(); this.state = { loadingCAQr: false, - generatingCA: false + generatingCA: false, + fileType: certFileTypes[0] }; this.onClose = this.onClose.bind(this); + this.onFileTypeChange = this.onFileTypeChange.bind(this); this.getQrCodeContent = this.getQrCodeContent.bind(this); } @@ -38,7 +41,7 @@ class DownloadRootCA extends React.Component { loadingCAQr: true }); - getJSON('/api/getQrCode') + getJSON('/api/getQrCode', { type: this.state.fileType }) .then((response) => { this.setState({ loadingCAQr: false, @@ -57,12 +60,33 @@ class DownloadRootCA extends React.Component { this.props.dispatch(hideRootCA()); } + onFileTypeChange (value) { + this.setState({ + fileType: value + }, () => { + this.fetchData(); + }); + } + getQrCodeContent () { const imgDomContent = { __html: this.state.CAQrCodeImageDom }; const content = ( <div className={Style.qrCodeWrapper} > <div dangerouslySetInnerHTML={imgDomContent} /> - <span>Scan to download rootCA.crt to your Phone</span> + <div>Scan to download rootCA.{this.state.fileType} to your Phone</div> + <div>You can change the CA's file extension: + <Select + defaultValue={this.state.fileType} + className={Style.fileSelect} + onChange={this.onFileTypeChange} + > + { + certFileTypes.map(key => ( + <Option key={key} value={key}>{key}</Option> + )) + } + </Select> + </div> </div> ); @@ -71,7 +95,6 @@ class DownloadRootCA extends React.Component { } getGenerateRootCADiv () { - const doToggleRemoteIntercept = () => { postJSON('/api/generateRootCA') .then((result) => { @@ -127,8 +150,8 @@ class DownloadRootCA extends React.Component { </div> <div className={Style.buttons} > - <a href="/fetchCrtFile" target="_blank"> - <Button type="primary" size="large" > Download </Button> + <a href={`/fetchCrtFile?type=${this.state.fileType}`} target="_blank"> + <Button type="primary" size="large" >Download</Button> </a> <span className={Style.tipSpan} >Or click the button to download.</span> </div> @@ -146,7 +169,6 @@ class DownloadRootCA extends React.Component { return ( <ResizablePanel onClose={this.onClose} visible={panelVisible} > {this.props.globalStatus.isRootCAFileExists ? this.getDownloadDiv() : this.getGenerateRootCADiv()} - </ResizablePanel> ); } diff --git a/web/src/component/download-root-ca.less b/web/src/component/download-root-ca.less index 8c9d1de..550227d 100644 --- a/web/src/component/download-root-ca.less +++ b/web/src/component/download-root-ca.less @@ -55,4 +55,9 @@ margin-top: 18px; display: block; } +} + +.fileSelect { + width: 60px; + margin-left: 8px; } \ No newline at end of file