Add support to validate self-signed certificates
This commit is contained in:
@@ -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,
|
||||||
|
|||||||
@@ -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,
|
||||||
|
},
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -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,
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
@@ -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": {
|
||||||
|
|||||||
Reference in New Issue
Block a user