Add client certificates

This commit is contained in:
Thomas Nordquist
2019-06-21 01:59:38 +02:00
parent bed5c74150
commit 38b16daf51
9 changed files with 243 additions and 37 deletions

View File

@@ -42,7 +42,8 @@ export const loadConnectionSettings = () => async (dispatch: Dispatch<any>, getS
} }
} }
export const selectCertificate = (connectionId: string) => async ( export type CertificateTypes = 'selfSignedCertificate' | 'clientCertificate' | 'clientKey'
export const selectCertificate = (type: CertificateTypes, connectionId: string) => async (
dispatch: Dispatch<any>, dispatch: Dispatch<any>,
getState: () => AppState getState: () => AppState
) => { ) => {
@@ -50,7 +51,7 @@ export const selectCertificate = (connectionId: string) => async (
const certificate = await openCertificate() const certificate = await openCertificate()
dispatch( dispatch(
updateConnection(connectionId, { updateConnection(connectionId, {
selfSignedCertificate: certificate, [type]: certificate,
}) })
) )
} catch (error) { } catch (error) {
@@ -147,6 +148,10 @@ export const toggleAdvancedSettings = (): Action => ({
type: ActionTypes.CONNECTION_MANAGER_TOGGLE_ADVANCED_SETTINGS, type: ActionTypes.CONNECTION_MANAGER_TOGGLE_ADVANCED_SETTINGS,
}) })
export const toggleCertificateSettings = (): Action => ({
type: ActionTypes.CONNECTION_MANAGER_TOGGLE_CERTIFICATE_SETTINGS,
})
export const deleteConnection = (connectionId: string) => (dispatch: Dispatch<any>, getState: () => AppState) => { export const deleteConnection = (connectionId: string) => (dispatch: Dispatch<any>, getState: () => AppState) => {
const connectionIds = Object.keys(getState().connectionManager.connections) const connectionIds = Object.keys(getState().connectionManager.connections)
const connectionIdLocation = connectionIds.indexOf(connectionId) const connectionIdLocation = connectionIds.indexOf(connectionId)

View File

@@ -44,29 +44,6 @@ 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 => (
@@ -122,16 +99,15 @@ 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> <div>
<Tooltip title="Select certificate to verify authenticity of a self-signed certificate" placement="top"> <Tooltip title="Manage tls connection certificates" placement="top">
<Button <Button
variant="contained" variant="contained"
className={classes.button} className={classes.button}
onClick={() => this.props.managerActions.selectCertificate(this.props.connection.id)} onClick={() => this.props.managerActions.toggleCertificateSettings()}
> >
<Lock /> Certificate <Lock /> Certificates
</Button> </Button>
</Tooltip> </Tooltip>
{this.renderCertificateInfo()}
</div> </div>
</Grid> </Grid>
<Grid item={true} xs={2} className={classes.gridPadding}> <Grid item={true} xs={2} className={classes.gridPadding}>

View File

@@ -0,0 +1,87 @@
import * as React from 'react'
import Add from '@material-ui/icons/Add'
import ClearAdornment from '../helper/ClearAdornment'
import Delete from '@material-ui/icons/Delete'
import Lock from '@material-ui/icons/Lock'
import { bindActionCreators } from 'redux'
import { Button, Theme, Tooltip, Typography } from '@material-ui/core'
import { CertificateParameters, ConnectionOptions } from '../../model/ConnectionOptions'
import { CertificateTypes } from '../../actions/ConnectionManager'
import { connect } from 'react-redux'
import { connectionManagerActions } from '../../actions'
import { withStyles } from '@material-ui/styles'
function CertificateFileSelection(props: {
certificateType: CertificateTypes
title: string
certificate?: CertificateParameters
classes: any
actions: {
connectionManager: typeof connectionManagerActions
}
connection: ConnectionOptions
}) {
const clearCertificate = React.useCallback(() => {
props.actions.connectionManager.updateConnection(props.connection.id, {
[props.certificateType]: undefined,
})
}, [props.connection, props.certificateType])
return (
<span>
<Tooltip title="Select certificate" placement="top">
<Button
variant="contained"
className={props.classes.button}
onClick={() => props.actions.connectionManager.selectCertificate(props.certificateType, props.connection.id)}
>
<Lock /> {props.title}
</Button>
</Tooltip>
<ClearCertificate classes={props.classes} certificate={props.certificate} action={clearCertificate} />
</span>
)
}
function ClearCertificate(props: { classes: any; certificate?: CertificateParameters; action: () => void }) {
if (!props.certificate) {
return null
}
return (
<Tooltip title={props.certificate.name}>
<Typography className={props.classes.certificateName}>
<ClearAdornment action={props.action} value={props.certificate.name} />
{props.certificate.name}
</Typography>
</Tooltip>
)
}
const mapDispatchToProps = (dispatch: any) => {
return {
actions: {
connectionManager: bindActionCreators(connectionManagerActions, dispatch),
},
}
}
const styles = (theme: Theme) => ({
certificateName: {
width: '100%',
height: 'calc(1em + 4px)',
overflow: 'hidden' as 'hidden',
whiteSpace: 'nowrap' as 'nowrap',
textOverflow: 'ellipsis' as 'ellipsis',
color: theme.palette.text.hint,
},
button: {
marginTop: theme.spacing(3),
marginRight: theme.spacing(2),
},
})
export default connect(
undefined,
mapDispatchToProps
)(withStyles(styles)(CertificateFileSelection))

View File

@@ -0,0 +1,107 @@
import * as React from 'react'
import CertificateFileSelection from './CertificateFileSelection'
import Undo from '@material-ui/icons/Undo'
import { bindActionCreators } from 'redux'
import { Button, Grid } from '@material-ui/core'
import { connect } from 'react-redux'
import { connectionManagerActions } from '../../actions'
import { ConnectionOptions } from '../../model/ConnectionOptions'
import { Theme, withStyles } from '@material-ui/core/styles'
interface Props {
connection: ConnectionOptions
classes: any
managerActions: typeof connectionManagerActions
}
interface State {
subscription: string
}
class Certificates extends React.Component<Props, State> {
constructor(props: any) {
super(props)
this.state = { subscription: '' }
}
private handleChange = (name: string) => (event: any) => {
this.props.managerActions.updateConnection(this.props.connection.id, {
[name]: event.target.value,
})
}
private renderCertificateInfo() {
if (!this.props.connection.selfSignedCertificate) {
return null
}
return <span />
}
public render() {
const { classes } = this.props
return (
<div>
<form noValidate={true} autoComplete="off">
<Grid container={true} spacing={3}>
<Grid item={true} xs={12} className={classes.gridPadding}>
<CertificateFileSelection
connection={this.props.connection}
certificate={this.props.connection.selfSignedCertificate}
title="Server Certificate (CA)"
certificateType="selfSignedCertificate"
/>
</Grid>
<Grid item={true} xs={12} className={classes.gridPadding}>
<CertificateFileSelection
connection={this.props.connection}
certificate={this.props.connection.clientCertificate}
title="Client Certificate"
certificateType="clientCertificate"
/>
<CertificateFileSelection
connection={this.props.connection}
certificate={this.props.connection.clientKey}
title="Client Key"
certificateType="clientKey"
/>
</Grid>
<Grid item={true} xs={2} className={classes.gridPadding}>
<Button
variant="contained"
className={classes.button}
onClick={this.props.managerActions.toggleCertificateSettings}
>
<Undo /> Back
</Button>
</Grid>
</Grid>
</form>
</div>
)
}
}
const mapDispatchToProps = (dispatch: any) => {
return {
managerActions: bindActionCreators(connectionManagerActions, dispatch),
}
}
const styles = (theme: Theme) => ({
fullWidth: {
width: '100%',
},
gridPadding: {
padding: '0 12px !important',
},
button: {
marginTop: theme.spacing(3),
marginRight: theme.spacing(2),
},
})
export default connect(
undefined,
mapDispatchToProps
)(withStyles(styles)(Certificates))

View File

@@ -9,6 +9,7 @@ import { ConnectionOptions, toMqttConnection } from '../../model/ConnectionOptio
import { Theme, withStyles } from '@material-ui/core/styles' import { Theme, withStyles } from '@material-ui/core/styles'
import { Modal, Paper, Toolbar, Typography, Collapse } from '@material-ui/core' import { Modal, Paper, Toolbar, Typography, Collapse } from '@material-ui/core'
import AdvancedConnectionSettings from './AdvancedConnectionSettings' import AdvancedConnectionSettings from './AdvancedConnectionSettings'
import Certificates from './Certificates'
interface Props { interface Props {
actions: any actions: any
@@ -16,6 +17,7 @@ interface Props {
connection?: ConnectionOptions connection?: ConnectionOptions
visible: boolean visible: boolean
showAdvancedSettings: boolean showAdvancedSettings: boolean
showCertificateSettings: boolean
} }
class ConnectionSetup extends React.Component<Props, {}> { class ConnectionSetup extends React.Component<Props, {}> {
@@ -24,19 +26,22 @@ class ConnectionSetup extends React.Component<Props, {}> {
} }
private renderSettings() { private renderSettings() {
const { connection, showAdvancedSettings } = this.props const { connection, showAdvancedSettings, showCertificateSettings } = this.props
if (!connection) { if (!connection) {
return null return null
} }
return ( return (
<div> <div>
<Collapse in={!showAdvancedSettings}> <Collapse in={!showAdvancedSettings && !showCertificateSettings}>
<ConnectionSettings connection={connection} /> <ConnectionSettings connection={connection} />
</Collapse> </Collapse>
<Collapse in={showAdvancedSettings}> <Collapse in={showAdvancedSettings && !showCertificateSettings}>
<AdvancedConnectionSettings connection={connection} /> <AdvancedConnectionSettings connection={connection} />
</Collapse> </Collapse>
<Collapse in={showCertificateSettings}>
<Certificates connection={connection} />
</Collapse>
</div> </div>
) )
} }
@@ -115,6 +120,7 @@ const mapStateToProps = (state: AppState) => {
return { return {
visible: !state.connection.connected, visible: !state.connection.connected,
showAdvancedSettings: state.connectionManager.showAdvancedSettings, showAdvancedSettings: state.connectionManager.showAdvancedSettings,
showCertificateSettings: state.connectionManager.showCertificateSettings,
connection: state.connectionManager.selected connection: state.connectionManager.selected
? state.connectionManager.connections[state.connectionManager.selected] ? state.connectionManager.connections[state.connectionManager.selected]
: undefined, : undefined,

View File

@@ -20,6 +20,8 @@ export interface ConnectionOptions {
encryption: boolean encryption: boolean
certValidation: boolean certValidation: boolean
selfSignedCertificate?: CertificateParameters selfSignedCertificate?: CertificateParameters
clientCertificate?: CertificateParameters
clientKey?: CertificateParameters
clientId?: string clientId?: string
subscriptions: Array<string> subscriptions: Array<string>
} }
@@ -38,6 +40,8 @@ export function toMqttConnection(options: ConnectionOptions): MqttOptions | unde
certValidation: options.certValidation, certValidation: options.certValidation,
subscriptions: options.subscriptions, subscriptions: options.subscriptions,
certificateAuthority: options.selfSignedCertificate ? options.selfSignedCertificate.data : undefined, certificateAuthority: options.selfSignedCertificate ? options.selfSignedCertificate.data : undefined,
clientCertificate: options.clientCertificate ? options.clientCertificate.data : undefined,
clientKey: options.clientKey ? options.clientKey.data : undefined,
} }
} }

View File

@@ -6,12 +6,14 @@ export interface ConnectionManagerState {
connections: { [s: string]: ConnectionOptions } connections: { [s: string]: ConnectionOptions }
selected?: string selected?: string
showAdvancedSettings: boolean showAdvancedSettings: boolean
showCertificateSettings: boolean
} }
const initialState: ConnectionManagerState = { const initialState: ConnectionManagerState = {
connections: {}, connections: {},
selected: undefined, selected: undefined,
showAdvancedSettings: false, showAdvancedSettings: false,
showCertificateSettings: false,
} }
export type Action = export type Action =
@@ -21,6 +23,7 @@ export type Action =
| AddConnection | AddConnection
| DeleteConnection | DeleteConnection
| ToggleAdvancedSettings | ToggleAdvancedSettings
| ToggleCertificateSettings
| DeleteSubscription | DeleteSubscription
| AddSubscription | AddSubscription
@@ -31,6 +34,7 @@ export enum ActionTypes {
CONNECTION_MANAGER_ADD_CONNECTION = 'CONNECTION_MANAGER_ADD_CONNECTION', CONNECTION_MANAGER_ADD_CONNECTION = 'CONNECTION_MANAGER_ADD_CONNECTION',
CONNECTION_MANAGER_DELETE_CONNECTION = 'CONNECTION_MANAGER_DELETE_CONNECTION', CONNECTION_MANAGER_DELETE_CONNECTION = 'CONNECTION_MANAGER_DELETE_CONNECTION',
CONNECTION_MANAGER_TOGGLE_ADVANCED_SETTINGS = 'CONNECTION_MANAGER_TOGGLE_ADVANCED_SETTINGS', CONNECTION_MANAGER_TOGGLE_ADVANCED_SETTINGS = 'CONNECTION_MANAGER_TOGGLE_ADVANCED_SETTINGS',
CONNECTION_MANAGER_TOGGLE_CERTIFICATE_SETTINGS = 'CONNECTION_MANAGER_TOGGLE_CERTIFICATE_SETTINGS',
CONNECTION_MANAGER_ADD_SUBSCRIPTION = 'CONNECTION_MANAGER_ADD_SUBSCRIPTION', CONNECTION_MANAGER_ADD_SUBSCRIPTION = 'CONNECTION_MANAGER_ADD_SUBSCRIPTION',
CONNECTION_MANAGER_DELETE_SUBSCRIPTION = 'CONNECTION_MANAGER_DELETE_SUBSCRIPTION', CONNECTION_MANAGER_DELETE_SUBSCRIPTION = 'CONNECTION_MANAGER_DELETE_SUBSCRIPTION',
} }
@@ -77,6 +81,10 @@ export interface ToggleAdvancedSettings {
type: ActionTypes.CONNECTION_MANAGER_TOGGLE_ADVANCED_SETTINGS type: ActionTypes.CONNECTION_MANAGER_TOGGLE_ADVANCED_SETTINGS
} }
export interface ToggleCertificateSettings {
type: ActionTypes.CONNECTION_MANAGER_TOGGLE_CERTIFICATE_SETTINGS
}
export const connectionManagerReducer = createReducer(initialState, { export const connectionManagerReducer = createReducer(initialState, {
CONNECTION_MANAGER_SET_CONNECTIONS: setConnections, CONNECTION_MANAGER_SET_CONNECTIONS: setConnections,
CONNECTION_MANAGER_SELECT_CONNECTION: selectConnection, CONNECTION_MANAGER_SELECT_CONNECTION: selectConnection,
@@ -84,6 +92,7 @@ export const connectionManagerReducer = createReducer(initialState, {
CONNECTION_MANAGER_ADD_CONNECTION: addConnection, CONNECTION_MANAGER_ADD_CONNECTION: addConnection,
CONNECTION_MANAGER_DELETE_CONNECTION: deleteConnection, CONNECTION_MANAGER_DELETE_CONNECTION: deleteConnection,
CONNECTION_MANAGER_TOGGLE_ADVANCED_SETTINGS: toggleAdvancedSettings, CONNECTION_MANAGER_TOGGLE_ADVANCED_SETTINGS: toggleAdvancedSettings,
CONNECTION_MANAGER_TOGGLE_CERTIFICATE_SETTINGS: toggleCertificateSettings,
CONNECTION_MANAGER_DELETE_SUBSCRIPTION: deleteSubscription, CONNECTION_MANAGER_DELETE_SUBSCRIPTION: deleteSubscription,
CONNECTION_MANAGER_ADD_SUBSCRIPTION: addSubscription, CONNECTION_MANAGER_ADD_SUBSCRIPTION: addSubscription,
}) })
@@ -109,6 +118,16 @@ function toggleAdvancedSettings(state: ConnectionManagerState, action: ToggleAdv
} }
} }
function toggleCertificateSettings(
state: ConnectionManagerState,
action: ToggleCertificateSettings
): ConnectionManagerState {
return {
...state,
showCertificateSettings: !state.showCertificateSettings,
}
}
function addConnection(state: ConnectionManagerState, action: AddConnection): ConnectionManagerState { function addConnection(state: ConnectionManagerState, action: AddConnection): ConnectionManagerState {
return { return {
...state, ...state,

View File

@@ -14,6 +14,8 @@ export interface MqttOptions {
clientId?: string clientId?: string
subscriptions: Array<string> subscriptions: Array<string>
certificateAuthority?: string certificateAuthority?: string
clientCertificate?: string
clientKey?: string
} }
export class MqttSource implements DataSource<MqttOptions> { export class MqttSource implements DataSource<MqttOptions> {
@@ -44,8 +46,10 @@ export class MqttSource implements DataSource<MqttOptions> {
username: options.username, username: options.username,
password: options.password, password: options.password,
clientId: options.clientId, clientId: options.clientId,
servername: options.tls ? url.host : undefined, servername: options.tls ? url.hostname : undefined,
ca: options.certificateAuthority ? Buffer.from(options.certificateAuthority, 'base64') : undefined, ca: options.certificateAuthority ? Buffer.from(options.certificateAuthority, 'base64') : undefined,
cert: options.clientCertificate ? Buffer.from(options.clientCertificate, 'base64') : undefined,
key: options.clientKey ? Buffer.from(options.clientKey, 'base64') : undefined,
} as any) } as any)
this.client = client this.client = client

View File

@@ -1,8 +1,6 @@
import 'mocha' import { Edge, TreeNodeFactory } from '../'
import { Edge, TreeNode, TreeNodeFactory } from '../'
import { expect } from 'chai' import { expect } from 'chai'
import 'mocha'
describe('Edge', () => { describe('Edge', () => {
it('should contain a name', () => { it('should contain a name', () => {