Skip to content

Commit

Permalink
feat(websocket): add authentication to websocket connection
Browse files Browse the repository at this point in the history
Merge pull request #345 from qri-io/ramfox/feat_websocket_auth
  • Loading branch information
ramfox committed Sep 24, 2021
2 parents ef65584 + 481f0a7 commit 6726bc5
Show file tree
Hide file tree
Showing 9 changed files with 81 additions and 22 deletions.
8 changes: 5 additions & 3 deletions src/features/app/App.tsx
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import React, { useEffect } from 'react'
import { ConnectedRouter } from 'connected-react-router'
import { useDispatch } from 'react-redux'
import { useDispatch, useSelector } from 'react-redux'

import { history } from '../../store/store'
import Routes from '../../routes'
Expand All @@ -10,13 +10,15 @@ import SnackBar from '../snackBar/SnackBar'
import ToastRenderer from '../toast/ToastRenderer'

import './App.css'
import { selectSessionTokens } from '../session/state/sessionState'

const App: React.FC<any> = () => {
const dispatch = useDispatch()

const tokens = useSelector(selectSessionTokens)
useEffect(() => {
dispatch(wsConnect())
})
dispatch(wsConnect(tokens.token))
}, [dispatch, tokens.token])

return (
<div id='app' className='flex flex-col h-screen w-screen' style={{ minWidth: '1100px' }}>
Expand Down
2 changes: 1 addition & 1 deletion src/features/app/modal/Modal.tsx
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import React, { useCallback, useEffect, useRef, useState } from 'react'
import React, { useCallback, useEffect, useRef } from 'react'
import { useDispatch, useSelector } from 'react-redux'

import { ModalType, selectModal } from '../state/appState'
Expand Down
2 changes: 1 addition & 1 deletion src/features/commits/CommitSummaryHeader.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,7 @@ const CommitSummaryHeader: React.FC<CommitSummaryHeaderProps> = ({
dataset,
children
}) => {
const { commit, path } = dataset
const { commit } = dataset
if (commit) {
return (
<div className='min-height-200 py-4 px-8 rounded-lg bg-white flex'>
Expand Down
2 changes: 1 addition & 1 deletion src/features/dataset/DatasetRoutes.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -31,7 +31,7 @@ const DatasetRoutes: React.FC<{}> = () => {
const dispatch = useDispatch()

useEffect(() => {
dispatch(loadHeader(qriRef))
dispatch(loadHeader({username: qriRef.username, name: qriRef.name, path: qriRef.path}))
},[dispatch, qriRef.username, qriRef.name, qriRef.path]);

return (
Expand Down
1 change: 0 additions & 1 deletion src/features/deploy/state/deployActions.ts
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,6 @@ import {
DEPLOY_SAVEDATASET_START,
DEPLOY_SAVEDATASET_END,
} from './deployState'
import { prepareTriggersForDeploy } from '../../trigger/util'
import { prepareWorkflowForDeploy } from '../../workflow/utils/prepareWorkflowForDeploy'

export interface DeployEvent {
Expand Down
2 changes: 1 addition & 1 deletion src/features/dsPreview/state/dsPreviewActions.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import { QriRef, refStringFromQriRef } from "../../../qri/ref";
import { QriRef } from "../../../qri/ref";
import { ApiAction, ApiActionThunk, CALL_API } from "../../../store/api";

export function loadDsPreview(ref: QriRef): ApiActionThunk {
Expand Down
71 changes: 59 additions & 12 deletions src/features/websocket/middleware/websocket.ts
Original file line number Diff line number Diff line change
Expand Up @@ -33,6 +33,8 @@ import {
ETAutomationDeploySaveDatasetEnd,
ETAutomationDeploySaveWorkflowStart,
ETAutomationDeploySaveWorkflowEnd,
WSSubscribeRequest,
WSUnsubscribeRequest,
} from '../../../qri/events'
import { wsConnectionChange } from '../state/websocketActions'
import { WS_CONNECT, WS_DISCONNECT } from '../state/websocketState'
Expand All @@ -56,8 +58,8 @@ export type RemoteEvents = Record<string, RemoteEvent>

const WEBSOCKETS_URL = process.env.REACT_APP_WS_BASE_URL || 'ws://localhost:2503'
const WEBSOCKETS_PROTOCOL = 'qri-websocket'
const numReconnectAttempts = 2
const msToAddBeforeReconnectAttempt = 3000
const numReconnectAttempts: number = 2
const msToAddBeforeReconnectAttempt: number = 3000

function newReconnectDeadline(): Date {
let d = new Date()
Expand All @@ -71,17 +73,18 @@ const middleware = () => {
status: WSConnectionStatus.disconnected,
}

const onOpen = (dispatch: Dispatch ) => (event: Event) => {
const onOpen = (dispatch: Dispatch, token: string) => (event: Event) => {
state = {
status: WSConnectionStatus.connected,
reconnectAttemptsRemaining: 0,
reconnectTime: undefined,
}
const stateCopy = NewWebsocketState(state.status)
dispatch(wsConnectionChange(stateCopy))
subscribe(token)
}

const onClose = (dispatch: Dispatch) => (event: Event) => {
const onClose = (dispatch: Dispatch, token: string) => (event: Event) => {
// code 1006 is an "Abnormal Closure" where no close frame is sent. Happens
// when there isn't a websocket host on the other end (aka: server down)
// https://developer.mozilla.org/en-US/docs/Web/API/CloseEvent
Expand All @@ -96,7 +99,7 @@ const middleware = () => {
state.reconnectAttemptsRemaining = numReconnectAttempts
}

reconnect(dispatch)
reconnect(dispatch, token)
const stateCopy = NewWebsocketState(state.status, state.reconnectAttemptsRemaining, state.reconnectTime)
dispatch(wsConnectionChange(stateCopy))
}
Expand Down Expand Up @@ -173,7 +176,6 @@ const middleware = () => {
case ETAutomationDeploySaveDatasetEnd:
dispatch(deploySaveDatasetEnded(event.data, event.sessionID))
break

default:
// console.log(`received websocket event: ${event.type}`)
}
Expand All @@ -183,26 +185,66 @@ const middleware = () => {
}
}

const connect = (dispatch: Dispatch) => {
const connect = (dispatch: Dispatch, token: string) => {
if (socket !== undefined) {
socket.close()
}
// connect to the remote host
socket = new WebSocket(WEBSOCKETS_URL, WEBSOCKETS_PROTOCOL)
socket.onmessage = onMessage(dispatch)
socket.onclose = onClose(dispatch)
socket.onopen = onOpen(dispatch)
socket.onclose = onClose(dispatch, token)
socket.onopen = onOpen(dispatch, token)
}

const subscribe = (token: string) => {
if (token === "") {
return
}
if (socket === undefined) {
return
}
let reconnectAttemptsRemaining = numReconnectAttempts
const msg = JSON.stringify({ type: WSSubscribeRequest, payload: { token }})
const attempt = setInterval(() => {
if (reconnectAttemptsRemaining === 0) {
clearInterval(attempt)
}
if (socket?.readyState === 1) {
socket.send(msg)
clearInterval(attempt)
return
}
reconnectAttemptsRemaining--
}, msToAddBeforeReconnectAttempt)
}

const reconnect = (dispatch: Dispatch) => {
const unsubscribe = () => {
if (socket === undefined) {
return
}
var reconnectAttemptsRemaining = numReconnectAttempts
const attempt = setInterval(() => {
if (reconnectAttemptsRemaining === 0) {
clearInterval(attempt)
}
if (socket?.readyState === 1) {
socket.send(JSON.stringify({ type: WSUnsubscribeRequest }))
clearInterval(attempt)
return
}
reconnectAttemptsRemaining--
}, msToAddBeforeReconnectAttempt)
}

const reconnect = (dispatch: Dispatch, token: string) => {
if (state.reconnectAttemptsRemaining && state.reconnectAttemptsRemaining > 0) {
state = {
status: WSConnectionStatus.interrupted,
reconnectAttemptsRemaining: state.reconnectAttemptsRemaining - 1,
reconnectTime: newReconnectDeadline()
}
setTimeout(() => {
connect(dispatch)
connect(dispatch, token)
}, msToAddBeforeReconnectAttempt)
return
}
Expand All @@ -217,14 +259,19 @@ const middleware = () => {
return (store: Store<RootState>) => (next: Dispatch<AnyAction>) => (action: AnyAction) => {
switch (action.type) {
case WS_CONNECT:
connect(next)
connect(next, action.token)
break
case WS_DISCONNECT:
if (socket !== undefined) {
socket.close()
}
socket = undefined
break
case 'LOGIN_SUCCESS':
subscribe(action.token)
break
case 'LOGOUT_SUCCESS':
unsubscribe()
}

return next(action)
Expand Down
8 changes: 6 additions & 2 deletions src/features/websocket/state/websocketActions.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,9 +6,13 @@ import {
WebsocketState,
} from './websocketState'

interface ConnectAction extends Action {
token: string
}

// tell websocket to connect
export function wsConnect(): Action {
return { type: WS_CONNECT }
export function wsConnect(token?: string): ConnectAction {
return { type: WS_CONNECT, token: token || '' }
}

// tell websocket to disconnect
Expand Down
7 changes: 7 additions & 0 deletions src/qri/events.ts
Original file line number Diff line number Diff line change
Expand Up @@ -51,3 +51,10 @@ export const ETAutomationDeploySaveWorkflowEnd = "automation:DeploySaveWorkflowE

export const ETAutomationDeploySaveDatasetStart = "automation:DeploySaveDatasetStart"
export const ETAutomationDeploySaveDatasetEnd = "automation:DeploySaveDatasetEnd"

// Websocket message types describe the different types of messages that can be
// sent over the websocket connection to establish the authentication handshake
export const WSSubscribeRequest = "subscribe:request"
export const WSSubscribeSuccess = "subscribe:success"
export const WSSubscribeFailure = "subscribe:failure"
export const WSUnsubscribeRequest = "unsubscribe:request"

0 comments on commit 6726bc5

Please sign in to comment.