Add support to validate self-signed certificates

This commit is contained in:
Thomas Nordquist
2019-03-26 14:42:28 +01:00
parent 89d363fbaa
commit c1bc96da01
5 changed files with 115 additions and 6 deletions

View File

@@ -1,9 +1,13 @@
import { AppState } from '../reducers'
import { clearLegacyConnectionOptions, loadLegacyConnectionOptions } from '../model/LegacyConnectionSettings'
import { ConnectionOptions, createEmptyConnection, makeDefaultConnections } from '../model/ConnectionOptions'
import { ConnectionOptions, createEmptyConnection, makeDefaultConnections, CertificateParameters } from '../model/ConnectionOptions'
import { default as persistantStorage, StorageIdentifier } from '../PersistantStorage'
import { Dispatch } from 'redux'
import { showError } from './Global'
import { remote } from 'electron'
import * as fs from 'fs'
import * as path from 'path'
import {
ActionTypes,
Action,
@@ -33,6 +37,53 @@ export const loadConnectionSettings = () => async (dispatch: Dispatch<any>, getS
}
}
export const selectCertificate = (connectionId: string) => async (dispatch: Dispatch<any>, getState: () => AppState) => {
try {
const certificate = await openCertificate()
console.log(certificate)
dispatch(updateConnection(connectionId, {
selfSignedCertificate: certificate,
}))
} catch (error) {
console.log(error)
dispatch(showError(error))
}
}
async function openCertificate(): Promise<CertificateParameters> {
const rejectReasons = {
noCertificateSelected: 'No certificate selected',
certificateSizeDoesNotMatch: 'Certificate size larger/smaller then expected.',
}
return new Promise((resolve, reject) => {
remote.dialog.showOpenDialog({ properties: ['openFile'], securityScopedBookmarks: true }, (filePaths?: string[]) => {
const selectedFile = filePaths && filePaths[0]
if (!selectedFile) {
reject(rejectReasons.noCertificateSelected)
return
}
fs.readFile(selectedFile, (error, data) => {
if (error) {
reject(error)
return
}
if (data.length > 16_384 || data.length < 128) {
reject(rejectReasons.certificateSizeDoesNotMatch)
return
}
resolve({
data: data.toString('base64'),
name: path.basename(selectedFile),
})
})
})
})
}
export const saveConnectionSettings = () => async (dispatch: Dispatch<any>, getState: () => AppState) => {
try {
console.log('store settings')
@@ -42,7 +93,7 @@ export const saveConnectionSettings = () => async (dispatch: Dispatch<any>, getS
}
}
export const updateConnection = (connectionId: string, changeSet: any): Action => ({
export const updateConnection = (connectionId: string, changeSet: Partial<ConnectionOptions>): Action => ({
connectionId,
changeSet,
type: ActionTypes.CONNECTION_MANAGER_UPDATE_CONNECTION,

View File

@@ -2,6 +2,7 @@ import * as React from 'react'
import Add from '@material-ui/icons/Add'
import Delete from '@material-ui/icons/Delete'
import Undo from '@material-ui/icons/Undo'
import Lock from '@material-ui/icons/Lock'
import { bindActionCreators } from 'redux'
import { connect } from 'react-redux'
import { connectionManagerActions } from '../../actions'
@@ -16,7 +17,10 @@ import {
List,
ListItem,
ListItemText,
Tooltip,
Typography,
} from '@material-ui/core'
import ClearAdornment from '../helper/ClearAdornment';
interface Props {
connection: ConnectionOptions
@@ -74,7 +78,7 @@ class ConnectionSettings extends React.Component<Props, State> {
</div>
</List>
</Grid>
<Grid item={true} xs={9} className={classes.gridPadding}>
<Grid item={true} xs={7} className={classes.gridPadding}>
<TextField
className={classes.fullWidth}
label="MQTT Client ID"
@@ -84,6 +88,20 @@ class ConnectionSettings extends React.Component<Props, State> {
/>
</Grid>
<Grid item={true} xs={3} className={classes.gridPadding}>
<div>
<Tooltip title="Select certificate to verify authenticity of a self-signed certificate" placement="top">
<Button
variant="contained"
className={classes.button}
onClick={() => this.props.managerActions.selectCertificate(this.props.connection.id)}
>
<Lock /> Certificate
</Button>
</Tooltip>
{this.renderCertificateInfo()}
</div>
</Grid>
<Grid item={true} xs={2} className={classes.gridPadding}>
<Button
variant="contained"
className={classes.button}
@@ -98,6 +116,29 @@ class ConnectionSettings extends React.Component<Props, State> {
)
}
private renderCertificateInfo() {
if (!this.props.connection.selfSignedCertificate) {
return null
}
return (
<span>
<Tooltip title={this.props.connection.selfSignedCertificate.name}>
<Typography className={this.props.classes.certificateName}>
<ClearAdornment action={this.clearCertificate} value={this.props.connection.selfSignedCertificate.name} />
{this.props.connection.selfSignedCertificate.name}
</Typography>
</Tooltip>
</span>
)
}
private clearCertificate = () => {
this.props.managerActions.updateConnection(this.props.connection.id, {
selfSignedCertificate: undefined,
})
}
private renderSubscriptions() {
const connection = this.props.connection
return connection.subscriptions.map(subscription => (
@@ -149,6 +190,14 @@ const styles: StyleRulesCallback<string> = (theme: Theme) => {
marginTop: theme.spacing(3),
float: 'right',
},
certificateName: {
width: '100%',
height: 'calc(1em + 4px)',
overflow: 'hidden' as 'hidden',
whiteSpace: 'nowrap' as 'nowrap',
textOverflow: 'ellipsis' as 'ellipsis',
color: theme.palette.text.hint,
},
}
}

View File

@@ -2,6 +2,11 @@ import { MqttOptions } from '../../../backend/src/DataSource'
import { v4 } from 'uuid'
const sha1 = require('sha1')
export interface CertificateParameters {
name: string
/** @property data base64 encoded data */
data: string
}
export interface ConnectionOptions {
type: 'mqtt'
id: string
@@ -14,6 +19,7 @@ export interface ConnectionOptions {
password?: string
encryption: boolean
certValidation: boolean
selfSignedCertificate?: CertificateParameters
clientId?: string
subscriptions: string[]
}
@@ -30,10 +36,11 @@ export function toMqttConnection(options: ConnectionOptions): MqttOptions | unde
tls: options.encryption,
certValidation: options.certValidation,
subscriptions: options.subscriptions,
certificateAuthority: options.selfSignedCertificate ? options.selfSignedCertificate.data : undefined,
}
}
export function generateClienId() {
function generateClientId() {
const clientIdSha = sha1(`${Math.random()}`).slice(0, 8)
return `mqtt-explorer-${clientIdSha}`
}
@@ -41,7 +48,7 @@ export function generateClienId() {
export function createEmptyConnection(): ConnectionOptions {
return {
certValidation: true,
clientId: generateClienId(),
clientId: generateClientId(),
id: v4() as string,
name: 'new connection',
encryption: false,