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 { AppState } from '../reducers'
import { clearLegacyConnectionOptions, loadLegacyConnectionOptions } from '../model/LegacyConnectionSettings' 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 { default as persistantStorage, StorageIdentifier } from '../PersistantStorage'
import { Dispatch } from 'redux' import { Dispatch } from 'redux'
import { showError } from './Global' import { showError } from './Global'
import { remote } from 'electron'
import * as fs from 'fs'
import * as path from 'path'
import { import {
ActionTypes, ActionTypes,
Action, 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) => { export const saveConnectionSettings = () => async (dispatch: Dispatch<any>, getState: () => AppState) => {
try { try {
console.log('store settings') 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, connectionId,
changeSet, changeSet,
type: ActionTypes.CONNECTION_MANAGER_UPDATE_CONNECTION, 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 Add from '@material-ui/icons/Add'
import Delete from '@material-ui/icons/Delete' import Delete from '@material-ui/icons/Delete'
import Undo from '@material-ui/icons/Undo' import Undo from '@material-ui/icons/Undo'
import Lock from '@material-ui/icons/Lock'
import { bindActionCreators } from 'redux' import { bindActionCreators } from 'redux'
import { connect } from 'react-redux' import { connect } from 'react-redux'
import { connectionManagerActions } from '../../actions' import { connectionManagerActions } from '../../actions'
@@ -16,7 +17,10 @@ import {
List, List,
ListItem, ListItem,
ListItemText, ListItemText,
Tooltip,
Typography,
} from '@material-ui/core' } from '@material-ui/core'
import ClearAdornment from '../helper/ClearAdornment';
interface Props { interface Props {
connection: ConnectionOptions connection: ConnectionOptions
@@ -74,7 +78,7 @@ class ConnectionSettings extends React.Component<Props, State> {
</div> </div>
</List> </List>
</Grid> </Grid>
<Grid item={true} xs={9} className={classes.gridPadding}> <Grid item={true} xs={7} className={classes.gridPadding}>
<TextField <TextField
className={classes.fullWidth} className={classes.fullWidth}
label="MQTT Client ID" label="MQTT Client ID"
@@ -84,6 +88,20 @@ class ConnectionSettings extends React.Component<Props, State> {
/> />
</Grid> </Grid>
<Grid item={true} xs={3} className={classes.gridPadding}> <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 <Button
variant="contained" variant="contained"
className={classes.button} 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() { private renderSubscriptions() {
const connection = this.props.connection const connection = this.props.connection
return connection.subscriptions.map(subscription => ( return connection.subscriptions.map(subscription => (
@@ -149,6 +190,14 @@ const styles: StyleRulesCallback<string> = (theme: Theme) => {
marginTop: theme.spacing(3), marginTop: theme.spacing(3),
float: 'right', 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' import { v4 } from 'uuid'
const sha1 = require('sha1') const sha1 = require('sha1')
export interface CertificateParameters {
name: string
/** @property data base64 encoded data */
data: string
}
export interface ConnectionOptions { export interface ConnectionOptions {
type: 'mqtt' type: 'mqtt'
id: string id: string
@@ -14,6 +19,7 @@ export interface ConnectionOptions {
password?: string password?: string
encryption: boolean encryption: boolean
certValidation: boolean certValidation: boolean
selfSignedCertificate?: CertificateParameters
clientId?: string clientId?: string
subscriptions: string[] subscriptions: string[]
} }
@@ -30,10 +36,11 @@ export function toMqttConnection(options: ConnectionOptions): MqttOptions | unde
tls: options.encryption, tls: options.encryption,
certValidation: options.certValidation, certValidation: options.certValidation,
subscriptions: options.subscriptions, subscriptions: options.subscriptions,
certificateAuthority: options.selfSignedCertificate ? options.selfSignedCertificate.data : undefined,
} }
} }
export function generateClienId() { function generateClientId() {
const clientIdSha = sha1(`${Math.random()}`).slice(0, 8) const clientIdSha = sha1(`${Math.random()}`).slice(0, 8)
return `mqtt-explorer-${clientIdSha}` return `mqtt-explorer-${clientIdSha}`
} }
@@ -41,7 +48,7 @@ export function generateClienId() {
export function createEmptyConnection(): ConnectionOptions { export function createEmptyConnection(): ConnectionOptions {
return { return {
certValidation: true, certValidation: true,
clientId: generateClienId(), clientId: generateClientId(),
id: v4() as string, id: v4() as string,
name: 'new connection', name: 'new connection',
encryption: false, encryption: false,

View File

@@ -12,6 +12,7 @@ export interface MqttOptions {
certValidation: boolean certValidation: boolean
clientId?: string clientId?: string
subscriptions: string[] subscriptions: string[]
certificateAuthority?: string
} }
export class MqttSource implements DataSource<MqttOptions> { export class MqttSource implements DataSource<MqttOptions> {
@@ -42,6 +43,7 @@ export class MqttSource implements DataSource<MqttOptions> {
username: options.username, username: options.username,
password: options.password, password: options.password,
clientId: options.clientId, clientId: options.clientId,
ca: options.certificateAuthority ? Buffer.from(options.certificateAuthority, 'base64') : undefined,
}) })
this.client = client this.client = client

View File

@@ -1,6 +1,6 @@
{ {
"name": "MQTT-Explorer", "name": "MQTT-Explorer",
"version": "0.2.3", "version": "0.2.4-alpha1",
"description": "Explore your message queues", "description": "Explore your message queues",
"main": "dist/src/electron.js", "main": "dist/src/electron.js",
"scripts": { "scripts": {