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))}
+ />
+
+
+
+
-
{
Update certificates
+
+
setShowWarning(false)}>
+
+ Warning
+
+
+ You are about to delete credentials, are you sure to proceed?
+
+
+ setShowWarning(false)}>
+ Cancel
+
+
+ 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,
}) => (
+
Open logfile
-
- Chart
-
-
- Terminal
-
-
- Certificate manager
-
);
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) {