diff --git a/index.jsx b/index.jsx index cd2920f..6366822 100644 --- a/index.jsx +++ b/index.jsx @@ -36,7 +36,7 @@ import React from 'react'; import SidePanel from './lib/components/SidePanel'; -import MainView from './lib/containers/MainView'; +import MainView from './lib/components/MainView'; import NavMenu from './lib/containers/NavMenu'; import './resources/css/index.scss'; @@ -129,8 +129,25 @@ export default { dispatch(loadCommands()); dispatch(loadSettings()); }, - decorateMainView: () => () => , - decorateNavMenu: () => () => , + mapMainViewState: ({ core }, props) => ({ + ...props, + viewId: core.navMenu.selectedItemId < 0 ? 1 : core.navMenu.selectedItemId, + }), + decorateMainView: () => props => , + decorateNavMenu: CoreNavMenu => ({ selectedItemId, ...rest }) => ( + <> + + + + ), mapDeviceSelectorState: (state, props) => ({ autoDeviceFilter: state.app.ui.autoDeviceFilter, portIndicatorStatus: (state.app.modemPort.deviceName !== null) ? 'on' : 'off', diff --git a/lib/actions/modemActions.js b/lib/actions/modemActions.js index d82e326..bc2b4ca 100644 --- a/lib/actions/modemActions.js +++ b/lib/actions/modemActions.js @@ -375,6 +375,15 @@ export function writeTLSCredential(secTag, type, content, password) { }; } +export function deleteTLSCredential(secTag, type) { + return async () => { + if (!port) { + throw new Error('Device is not open.'); + } + return port.deleteTLSCredential(secTag, type); + }; +} + export function close() { return async () => { if (port && port.isOpen) { diff --git a/lib/actions/uiActions.js b/lib/actions/uiActions.js index eeeeade..1c004ff 100644 --- a/lib/actions/uiActions.js +++ b/lib/actions/uiActions.js @@ -35,7 +35,7 @@ */ import { - SET_MAIN_VIEW, TERMINAL_AUTO_SCROLL, + TERMINAL_AUTO_SCROLL, API_TOKEN_UPDATE, AUTO_REQUESTS, SIGNAL_QUALITY_INTERVAL, AUTO_DEVICE_FILTER_TOGGLED, @@ -43,17 +43,6 @@ import { import { changeSignalQualityInterval } from './modemActions'; import persistentStore from './persistentStore'; -export const MAIN_VIEW_CHART = 'MAIN_VIEW_CHART'; -export const MAIN_VIEW_TERMINAL = 'MAIN_VIEW_TERMINAL'; -export const MAIN_VIEW_CERTMGR = 'MAIN_VIEW_CERTMGR'; - -function setMainViewAction(mainView) { - return { - type: SET_MAIN_VIEW, - mainView, - }; -} - export function autoScrollToggledAction(autoScroll) { persistentStore.set('autoScroll', !!autoScroll); return { @@ -62,24 +51,6 @@ export function autoScrollToggledAction(autoScroll) { }; } -export function setChartView() { - return dispatch => { - dispatch(setMainViewAction(MAIN_VIEW_CHART)); - }; -} - -export function setTerminalView() { - return dispatch => { - dispatch(setMainViewAction(MAIN_VIEW_TERMINAL)); - }; -} - -export function setCertManagerView() { - return dispatch => { - dispatch(setMainViewAction(MAIN_VIEW_CERTMGR)); - }; -} - export function apiTokenUpdateAction(apiToken) { persistentStore.set('apiToken', apiToken); return { diff --git a/lib/components/CertificateManagerView.jsx b/lib/components/CertificateManagerView.jsx index 6bfb8fb..5526491 100644 --- a/lib/components/CertificateManagerView.jsx +++ b/lib/components/CertificateManagerView.jsx @@ -35,24 +35,75 @@ */ import React, { useState } from 'react'; -import { bool, func } from 'prop-types'; +import { + bool, func, string, shape, +} from 'prop-types'; import Form from 'react-bootstrap/Form'; import Button from 'react-bootstrap/Button'; import ButtonGroup from 'react-bootstrap/ButtonGroup'; import Alert from 'react-bootstrap/Alert'; import Row from 'react-bootstrap/Row'; import Col from 'react-bootstrap/Col'; +import Modal from 'react-bootstrap/Modal'; import { remote } from 'electron'; import { homedir } from 'os'; import { readFileSync } from 'fs'; import { logger } from 'nrfconnect/core'; -const CertificateManagerView = ({ hidden, writeTLSCredential }) => { +const NRF_CLOUD_TAG = 16842753; + +const FormGroupWithCheckbox = ({ + controlId, controlProps, label, value, set, clearLabel, clear, setClear, +}) => ( + + + {label} + set(target.value)} + disabled={clear} + /> + + + {clearLabel}  + setClear(target.checked)} + /> + + +); +FormGroupWithCheckbox.propTypes = { + controlId: string.isRequired, + controlProps: shape({}).isRequired, + label: string.isRequired, + value: string.isRequired, + set: func.isRequired, + clearLabel: string, + clear: bool.isRequired, + setClear: func.isRequired, +}; +FormGroupWithCheckbox.defaultProps = { + clearLabel: null, +}; + +const CertificateManagerView = ({ hidden, writeTLSCredential, deleteTLSCredential }) => { const [caCert, setCACert] = useState(''); const [clientCert, setClientCert] = useState(''); const [privateKey, setPrivateKey] = useState(''); - const [secTag, setSecTag] = useState(16842753); + const [preSharedKey, setPreSharedKey] = useState(''); + const [pskIdentity, setPskIdentity] = useState(''); + const [clearCaCert, setClearCACert] = useState(false); + const [clearClientCert, setClearClientCert] = useState(false); + const [clearPrivateKey, setClearPrivateKey] = useState(false); + const [clearPreSharedKey, setClearPreSharedKey] = useState(false); + const [clearPskIdentity, setClearPskIdentity] = useState(false); + const [secTag, setSecTag] = useState(NRF_CLOUD_TAG); + const [showWarning, setShowWarning] = useState(false); function loadJsonFile(filename) { if (!filename) { @@ -60,9 +111,9 @@ const CertificateManagerView = ({ hidden, writeTLSCredential }) => { } try { const json = JSON.parse(readFileSync(filename, 'utf8')); - setCACert(json.caCert); - setClientCert(json.clientCert); - setPrivateKey(json.privateKey); + setCACert(json.caCert || ''); + setClientCert(json.clientCert || ''); + setPrivateKey(json.privateKey || ''); } catch (err) { logger.error(err.message); } @@ -88,18 +139,41 @@ const CertificateManagerView = ({ hidden, writeTLSCredential }) => { event.preventDefault(); } - async function updateCertificate() { - try { - logger.info('Updating CA certificate...'); - await writeTLSCredential(secTag, 0, caCert); - logger.info('Updating client certificate...'); - await writeTLSCredential(secTag, 1, clientCert); - logger.info('Updating private key...'); - await writeTLSCredential(secTag, 2, privateKey); - logger.info('Certificate update completed successfully'); - } catch (err) { - logger.error(err.message); + async function performCertificateUpdate() { + setShowWarning(false); + + async function oneUpdate(info, type, content, clear) { + if (clear) { + logger.info(`Clearing ${info}...`); + try { + await deleteTLSCredential(secTag, type); + } catch (err) { + logger.error(err.message); + } + } else if (content) { + logger.info(`Updating ${info}...`); + try { + await writeTLSCredential(secTag, type, content); + } catch (err) { + logger.error(err.message); + } + } } + await oneUpdate('CA certificate', 0, caCert, clearCaCert); + await oneUpdate('client certificate', 1, clientCert, clearClientCert); + await oneUpdate('private key', 2, privateKey, clearPrivateKey); + await oneUpdate('pre-shared key', 3, preSharedKey, clearPreSharedKey); + await oneUpdate('PSK identity', 4, pskIdentity, clearPskIdentity); + + logger.info('Certificate update completed'); + } + + function updateCertificate() { + if (clearCaCert || clearClientCert || clearPrivateKey + || clearPreSharedKey || clearPskIdentity) { + return setShowWarning(true); + } + return performCertificateUpdate(); } const className = 'cert-mgr-view d-flex flex-column p-5 h-100 pretty-scrollbar'; @@ -108,6 +182,7 @@ const CertificateManagerView = ({ hidden, writeTLSCredential }) => { className: 'text-monospace', rows: 4, }; + const textProps = { type: 'text' }; return (
{ onDrop={onDrop} > - The modem must be in offline state - (AT+CFUN=4) for updating certificates. - You can drag-and-drop a JSON file over this window. + +
+ The modem must be in offline state + (AT+CFUN=4) for updating certificates.
+ You can drag-and-drop a JSON file over this window.
+ You can use AT%CMNG=1 command in the + Terminal screen to list all stored certificates.
+ Make sure your device runs a firmware with increased buffer + to support long AT-commands.
+ Use security tag {NRF_CLOUD_TAG} to manage nRF Connect + for Cloud certificate, otherwise pick a different tag. +
- - CA certificate - setCACert(target.value)} - /> - - - Client certificate - setClientCert(target.value)} - /> - - - Private key - setPrivateKey(target.value)} - /> - - - Security tag - - setSecTag(Number(target.value))} - /> + + + {FormGroupWithCheckbox({ + controlId: 'certMgr.caCert', + controlProps: textAreaProps, + label: 'CA certificate', + value: caCert, + set: setCACert, + clearLabel: 'Delete', + clear: clearCaCert, + setClear: setClearCACert, + })} + {FormGroupWithCheckbox({ + controlId: 'certMgr.clientCert', + controlProps: textAreaProps, + label: 'Client certificate', + value: clientCert, + set: setClientCert, + clear: clearClientCert, + setClear: setClearClientCert, + })} + {FormGroupWithCheckbox({ + controlId: 'certMgr.privKey', + controlProps: textAreaProps, + label: 'Private key', + value: privateKey, + set: setPrivateKey, + clear: clearPrivateKey, + setClear: setClearPrivateKey, + })} - + + {FormGroupWithCheckbox({ + controlId: 'certMgr.preSharedKey', + controlProps: textProps, + label: 'Pre-shared key', + value: preSharedKey, + set: setPreSharedKey, + clearLabel: 'Delete', + clear: clearPreSharedKey, + setClear: setClearPreSharedKey, + })} + {FormGroupWithCheckbox({ + controlId: 'certMgr.pskIdentity', + controlProps: textProps, + label: 'PSK identity', + value: pskIdentity, + set: setPskIdentity, + clear: clearPskIdentity, + setClear: setClearPskIdentity, + })} + + Security tag + + setSecTag(Number(target.value))} + /> + + + +
- + + setShowWarning(false)}> + + Warning + + + You are about to delete credentials, are you sure to proceed? + + + + + +
); }; @@ -179,6 +308,7 @@ const CertificateManagerView = ({ hidden, writeTLSCredential }) => { CertificateManagerView.propTypes = { hidden: bool.isRequired, writeTLSCredential: func.isRequired, + deleteTLSCredential: func.isRequired, }; export default CertificateManagerView; diff --git a/lib/components/MainView.jsx b/lib/components/MainView.jsx index 881edb4..47b6a12 100644 --- a/lib/components/MainView.jsx +++ b/lib/components/MainView.jsx @@ -40,18 +40,16 @@ import TerminalView from '../containers/TerminalView'; import Chart from '../containers/Chart'; import CertificateManagerView from '../containers/CertificateManagerView'; -const MainView = ({ chartVisible, terminalVisible, certManagerVisible }) => ( +const MainView = ({ viewId }) => (
-
); MainView.propTypes = { - chartVisible: PropTypes.bool.isRequired, - terminalVisible: PropTypes.bool.isRequired, - certManagerVisible: PropTypes.bool.isRequired, + viewId: PropTypes.number.isRequired, }; export default MainView; diff --git a/lib/components/NavMenu.jsx b/lib/components/NavMenu.jsx index d028f5e..d936d3f 100644 --- a/lib/components/NavMenu.jsx +++ b/lib/components/NavMenu.jsx @@ -41,12 +41,6 @@ import Button from 'react-bootstrap/Button'; const NavMenu = ({ enableOpen, openLogfile, - setChartView, - setChartViewActive, - setTerminalView, - setTerminalViewActive, - setCertManagerView, - setCertManagerViewActive, }) => (
- - -
); NavMenu.propTypes = { enableOpen: PropTypes.bool.isRequired, openLogfile: PropTypes.func.isRequired, - setChartView: PropTypes.func.isRequired, - setChartViewActive: PropTypes.bool.isRequired, - setTerminalView: PropTypes.func.isRequired, - setTerminalViewActive: PropTypes.bool.isRequired, - setCertManagerView: PropTypes.func.isRequired, - setCertManagerViewActive: PropTypes.bool.isRequired, }; export default NavMenu; diff --git a/lib/containers/CertificateManagerView.js b/lib/containers/CertificateManagerView.js index 9d28835..481c991 100644 --- a/lib/containers/CertificateManagerView.js +++ b/lib/containers/CertificateManagerView.js @@ -35,12 +35,13 @@ */ import { connect } from 'react-redux'; -import { writeTLSCredential } from '../actions/modemActions'; +import { writeTLSCredential, deleteTLSCredential } from '../actions/modemActions'; import CertificateManagerView from '../components/CertificateManagerView'; export default connect( () => ({}), dispatch => ({ writeTLSCredential: (...args) => dispatch(writeTLSCredential(...args)), + deleteTLSCredential: (...args) => dispatch(deleteTLSCredential(...args)), }), )(CertificateManagerView); diff --git a/lib/containers/MainView.js b/lib/containers/MainView.js deleted file mode 100644 index f28bb06..0000000 --- a/lib/containers/MainView.js +++ /dev/null @@ -1,48 +0,0 @@ -/* Copyright (c) 2015 - 2018, Nordic Semiconductor ASA - * - * All rights reserved. - * - * Use in source and binary forms, redistribution in binary form only, with - * or without modification, are permitted provided that the following conditions - * are met: - * - * 1. Redistributions in binary form, except as embedded into a Nordic - * Semiconductor ASA integrated circuit in a product or a software update for - * such product, must reproduce the above copyright notice, this list of - * conditions and the following disclaimer in the documentation and/or other - * materials provided with the distribution. - * - * 2. Neither the name of Nordic Semiconductor ASA nor the names of its - * contributors may be used to endorse or promote products derived from this - * software without specific prior written permission. - * - * 3. This software, with or without modification, must only be used with a Nordic - * Semiconductor ASA integrated circuit. - * - * 4. Any software provided in binary form under this license must not be reverse - * engineered, decompiled, modified and/or disassembled. - * - * THIS SOFTWARE IS PROVIDED BY NORDIC SEMICONDUCTOR ASA "AS IS" AND ANY EXPRESS OR - * IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF - * MERCHANTABILITY, NONINFRINGEMENT, AND FITNESS FOR A PARTICULAR PURPOSE ARE - * DISCLAIMED. IN NO EVENT SHALL NORDIC SEMICONDUCTOR ASA OR CONTRIBUTORS BE LIABLE - * FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL - * DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR - * SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER - * CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR - * TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF - * THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. - */ - -import { connect } from 'react-redux'; -import MainView from '../components/MainView'; - -import { MAIN_VIEW_CHART, MAIN_VIEW_TERMINAL, MAIN_VIEW_CERTMGR } from '../actions/uiActions'; - -export default connect( - state => ({ - chartVisible: state.app.ui.mainView === MAIN_VIEW_CHART, - terminalVisible: state.app.ui.mainView === MAIN_VIEW_TERMINAL, - certManagerVisible: state.app.ui.mainView === MAIN_VIEW_CERTMGR, - }), -)(MainView); diff --git a/lib/containers/NavMenu.js b/lib/containers/NavMenu.js index c2b41a4..24f1e6c 100644 --- a/lib/containers/NavMenu.js +++ b/lib/containers/NavMenu.js @@ -36,23 +36,13 @@ import { connect } from 'react-redux'; import openLogfile from '../actions/logfileActions'; -import { - setChartView, setTerminalView, setCertManagerView, - MAIN_VIEW_CHART, MAIN_VIEW_TERMINAL, MAIN_VIEW_CERTMGR, -} from '../actions/uiActions'; import NavMenu from '../components/NavMenu'; export default connect( - state => ({ - enableOpen: (state.app.modemPort.deviceName === null), - setChartViewActive: (state.app.ui.mainView === MAIN_VIEW_CHART), - setTerminalViewActive: (state.app.ui.mainView === MAIN_VIEW_TERMINAL), - setCertManagerViewActive: (state.app.ui.mainView === MAIN_VIEW_CERTMGR), + ({ app }) => ({ + enableOpen: (app.modemPort.deviceName === null), }), dispatch => ({ openLogfile: () => dispatch(openLogfile()), - setChartView: () => dispatch(setChartView()), - setTerminalView: () => dispatch(setTerminalView()), - setCertManagerView: () => dispatch(setCertManagerView()), }), )(NavMenu); diff --git a/lib/reducers/uiReducer.js b/lib/reducers/uiReducer.js index 000d335..0e8fd53 100644 --- a/lib/reducers/uiReducer.js +++ b/lib/reducers/uiReducer.js @@ -35,10 +35,8 @@ */ import * as actions from '../actions/actionIds'; -import { MAIN_VIEW_TERMINAL } from '../actions/uiActions'; const initialState = { - mainView: MAIN_VIEW_TERMINAL, terminalUpdate: 0, commands: [], autoScroll: true, @@ -50,13 +48,6 @@ const initialState = { export default function reducer(state = initialState, action) { switch (action.type) { - case actions.SET_MAIN_VIEW: { - const { mainView } = action; - return { - ...state, - mainView, - }; - } case actions.UPDATE_TERMINAL: { const { update } = action; return { diff --git a/modemtalk/api/TLSCredentials.js b/modemtalk/api/TLSCredentials.js index ea7a256..1504e25 100644 --- a/modemtalk/api/TLSCredentials.js +++ b/modemtalk/api/TLSCredentials.js @@ -84,7 +84,7 @@ module.exports = target => { Object.assign(target.prototype, { CredentialType, writeTLSCredential(secTag, type, content, password) { - let cmd = `%CMNG=0,${secTag},${type},"\n${content}"`; + let cmd = `%CMNG=0,${secTag},${type},"${content.trim()}"`; if (password !== undefined) { cmd = `${cmd},${password}`; } @@ -114,7 +114,9 @@ module.exports = target => { }); }, deleteTLSCredential(secTag, type) { - return this.writeAT(`%CMNG=3,${secTag},${type}`); + return this.writeAT(`%CMNG=3,${secTag},${type}`, { + timeout: 2000, + }); }, }); }; diff --git a/modemtalk/index.js b/modemtalk/index.js index 5dca6ae..ed72969 100644 --- a/modemtalk/index.js +++ b/modemtalk/index.js @@ -157,13 +157,13 @@ class ModemPort extends SerialPort { } write(...args) { - if (this.defaults.writeCallback) { - this.defaults.writeCallback(...args); - } - const writeLine = (lines, cb) => { const [line, ...rest] = lines; - super.write(line, err => { + const data = `${line.trim()}${this.eol}`; + if (this.defaults.writeCallback) { + this.defaults.writeCallback(data); + } + super.write(data, err => { if (err) { cb(err); } else if (rest.length) {