diff --git a/keyserver/src/socket/tunnelbroker-socket.js b/keyserver/src/socket/tunnelbroker-socket.js index 751e95d80..bdac75fbd 100644 --- a/keyserver/src/socket/tunnelbroker-socket.js +++ b/keyserver/src/socket/tunnelbroker-socket.js @@ -1,217 +1,217 @@ // @flow import _debounce from 'lodash/debounce.js'; import uuid from 'uuid'; import WebSocket from 'ws'; import { tunnelbrokerHeartbeatTimeout } from 'lib/shared/timeouts.js'; -import type { ClientMessageToDevice } from 'lib/tunnelbroker/tunnelbroker-context.js'; +import type { TunnelbrokerClientMessageToDevice } from 'lib/tunnelbroker/tunnelbroker-context.js'; import type { MessageReceiveConfirmation } from 'lib/types/tunnelbroker/message-receive-confirmation-types.js'; import type { MessageSentStatus } from 'lib/types/tunnelbroker/message-to-device-request-status-types.js'; import type { MessageToDeviceRequest } from 'lib/types/tunnelbroker/message-to-device-request-types.js'; import { type TunnelbrokerMessage, tunnelbrokerMessageTypes, tunnelbrokerMessageValidator, } from 'lib/types/tunnelbroker/messages.js'; import { type RefreshKeyRequest, refreshKeysRequestValidator, } from 'lib/types/tunnelbroker/peer-to-peer-message-types.js'; import type { ConnectionInitializationMessage } from 'lib/types/tunnelbroker/session-types.js'; import type { Heartbeat } from 'lib/types/websocket/heartbeat-types.js'; import { uploadNewOneTimeKeys } from '../utils/olm-utils.js'; type PromiseCallbacks = { +resolve: () => void, +reject: (error: string) => void, }; type Promises = { [clientMessageID: string]: PromiseCallbacks }; class TunnelbrokerSocket { ws: WebSocket; connected: boolean = false; closed: boolean = false; promises: Promises = {}; heartbeatTimeoutID: ?TimeoutID; oneTimeKeysPromise: ?Promise; constructor( socketURL: string, initMessage: ConnectionInitializationMessage, onClose: () => mixed, ) { const socket = new WebSocket(socketURL); socket.on('open', () => { if (!this.closed) { socket.send(JSON.stringify(initMessage)); } }); socket.on('close', async () => { if (this.closed) { return; } this.closed = true; this.connected = false; this.stopHeartbeatTimeout(); console.error('Connection to Tunnelbroker closed'); onClose(); }); socket.on('error', (error: Error) => { console.error('Tunnelbroker socket error:', error.message); }); socket.on('message', this.onMessage); this.ws = socket; } onMessage: (event: ArrayBuffer) => Promise = async ( event: ArrayBuffer, ) => { let rawMessage; try { rawMessage = JSON.parse(event.toString()); } catch (e) { console.error('error while parsing Tunnelbroker message:', e.message); return; } if (!tunnelbrokerMessageValidator.is(rawMessage)) { console.error('invalid TunnelbrokerMessage: ', rawMessage.toString()); return; } const message: TunnelbrokerMessage = rawMessage; this.resetHeartbeatTimeout(); if ( message.type === tunnelbrokerMessageTypes.CONNECTION_INITIALIZATION_RESPONSE ) { if (message.status.type === 'Success' && !this.connected) { this.connected = true; console.info('session with Tunnelbroker created'); } else if (message.status.type === 'Success' && this.connected) { console.info( 'received ConnectionInitializationResponse with status: Success for already connected socket', ); } else { this.connected = false; console.error( 'creating session with Tunnelbroker error:', message.status.data, ); } } else if (message.type === tunnelbrokerMessageTypes.MESSAGE_TO_DEVICE) { const confirmation: MessageReceiveConfirmation = { type: tunnelbrokerMessageTypes.MESSAGE_RECEIVE_CONFIRMATION, messageIDs: [message.messageID], }; this.ws.send(JSON.stringify(confirmation)); const { payload } = message; try { const messageToKeyserver = JSON.parse(payload); if (refreshKeysRequestValidator.is(messageToKeyserver)) { const request: RefreshKeyRequest = messageToKeyserver; this.debouncedRefreshOneTimeKeys(request.numberOfKeys); } } catch (e) { console.error( 'error while processing message to keyserver:', e.message, ); } } else if ( message.type === tunnelbrokerMessageTypes.MESSAGE_TO_DEVICE_REQUEST_STATUS ) { for (const status: MessageSentStatus of message.clientMessageIDs) { if (status.type === 'Success') { if (this.promises[status.data]) { this.promises[status.data].resolve(); delete this.promises[status.data]; } else { console.log( 'received successful response for a non-existent request', ); } } else if (status.type === 'Error') { if (this.promises[status.data.id]) { this.promises[status.data.id].reject(status.data.error); delete this.promises[status.data.id]; } else { console.log('received error response for a non-existent request'); } } else if (status.type === 'SerializationError') { console.error('SerializationError for message: ', status.data); } else if (status.type === 'InvalidRequest') { console.log('Tunnelbroker recorded InvalidRequest'); } } } else if (message.type === tunnelbrokerMessageTypes.HEARTBEAT) { const heartbeat: Heartbeat = { type: tunnelbrokerMessageTypes.HEARTBEAT, }; this.ws.send(JSON.stringify(heartbeat)); } }; refreshOneTimeKeys: (numberOfKeys: number) => void = numberOfKeys => { const oldOneTimeKeysPromise = this.oneTimeKeysPromise; this.oneTimeKeysPromise = (async () => { await oldOneTimeKeysPromise; await uploadNewOneTimeKeys(numberOfKeys); })(); }; debouncedRefreshOneTimeKeys: (numberOfKeys: number) => void = _debounce( this.refreshOneTimeKeys, 100, { leading: true, trailing: true }, ); - sendMessage: (message: ClientMessageToDevice) => Promise = ( - message: ClientMessageToDevice, + sendMessage: (message: TunnelbrokerClientMessageToDevice) => Promise = ( + message: TunnelbrokerClientMessageToDevice, ) => { if (!this.connected) { throw new Error('Tunnelbroker not connected'); } const clientMessageID = uuid.v4(); const messageToDevice: MessageToDeviceRequest = { type: tunnelbrokerMessageTypes.MESSAGE_TO_DEVICE_REQUEST, clientMessageID, deviceID: message.deviceID, payload: message.payload, }; return new Promise((resolve, reject) => { this.promises[clientMessageID] = { resolve, reject, }; this.ws.send(JSON.stringify(messageToDevice)); }); }; stopHeartbeatTimeout() { if (this.heartbeatTimeoutID) { clearTimeout(this.heartbeatTimeoutID); this.heartbeatTimeoutID = null; } } resetHeartbeatTimeout() { this.stopHeartbeatTimeout(); this.heartbeatTimeoutID = setTimeout(() => { this.ws.close(); this.connected = false; }, tunnelbrokerHeartbeatTimeout); } } export default TunnelbrokerSocket; diff --git a/lib/tunnelbroker/tunnelbroker-context.js b/lib/tunnelbroker/tunnelbroker-context.js index 1dfb2480b..7ab9cc53f 100644 --- a/lib/tunnelbroker/tunnelbroker-context.js +++ b/lib/tunnelbroker/tunnelbroker-context.js @@ -1,482 +1,483 @@ // @flow import invariant from 'invariant'; import _isEqual from 'lodash/fp/isEqual.js'; import * as React from 'react'; import uuid from 'uuid'; import type { SecondaryTunnelbrokerConnection } from './secondary-tunnelbroker-connection.js'; import { tunnnelbrokerURL } from '../facts/tunnelbroker.js'; import { peerToPeerMessageHandler } from '../handlers/peer-to-peer-message-handler.js'; import { IdentityClientContext } from '../shared/identity-client-context.js'; import { tunnelbrokerHeartbeatTimeout } from '../shared/timeouts.js'; import { isWebPlatform } from '../types/device-types.js'; import type { MessageReceiveConfirmation } from '../types/tunnelbroker/message-receive-confirmation-types.js'; import type { MessageSentStatus } from '../types/tunnelbroker/message-to-device-request-status-types.js'; import type { MessageToDeviceRequest } from '../types/tunnelbroker/message-to-device-request-types.js'; import { type TunnelbrokerMessage, tunnelbrokerMessageTypes, tunnelbrokerMessageValidator, } from '../types/tunnelbroker/messages.js'; import { type PeerToPeerMessage, peerToPeerMessageValidator, } from '../types/tunnelbroker/peer-to-peer-message-types.js'; import type { AnonymousInitializationMessage, ConnectionInitializationMessage, TunnelbrokerInitializationMessage, TunnelbrokerDeviceTypes, } from '../types/tunnelbroker/session-types.js'; import type { Heartbeat } from '../types/websocket/heartbeat-types.js'; import { getConfig } from '../utils/config.js'; import { getContentSigningKey } from '../utils/crypto-utils.js'; import { useSelector } from '../utils/redux-utils.js'; -export type ClientMessageToDevice = { +export type TunnelbrokerClientMessageToDevice = { +deviceID: string, +payload: string, }; export type TunnelbrokerSocketListener = ( message: TunnelbrokerMessage, ) => mixed; type PromiseCallbacks = { +resolve: () => void, +reject: (error: string) => void, }; type Promises = { [clientMessageID: string]: PromiseCallbacks }; type TunnelbrokerContextType = { - +sendMessage: (message: ClientMessageToDevice) => Promise, + +sendMessage: (message: TunnelbrokerClientMessageToDevice) => Promise, +addListener: (listener: TunnelbrokerSocketListener) => void, +removeListener: (listener: TunnelbrokerSocketListener) => void, +connected: boolean, +isAuthorized: boolean, +setUnauthorizedDeviceID: (unauthorizedDeviceID: ?string) => void, }; const TunnelbrokerContext: React.Context = React.createContext(); type Props = { +children: React.Node, +shouldBeClosed?: boolean, +onClose?: () => mixed, +secondaryTunnelbrokerConnection?: SecondaryTunnelbrokerConnection, }; function getTunnelbrokerDeviceType(): TunnelbrokerDeviceTypes { return isWebPlatform(getConfig().platformDetails.platform) ? 'web' : 'mobile'; } function createAnonymousInitMessage( deviceID: string, ): AnonymousInitializationMessage { return ({ type: 'AnonymousInitializationMessage', deviceID, deviceType: getTunnelbrokerDeviceType(), }: AnonymousInitializationMessage); } function TunnelbrokerProvider(props: Props): React.Node { const { children, shouldBeClosed, onClose, secondaryTunnelbrokerConnection } = props; const accessToken = useSelector(state => state.commServicesAccessToken); const userID = useSelector(state => state.currentUserInfo?.id); const [unauthorizedDeviceID, setUnauthorizedDeviceID] = React.useState(null); const isAuthorized = !unauthorizedDeviceID; const createInitMessage = React.useCallback(async () => { if (shouldBeClosed) { return null; } if (unauthorizedDeviceID) { return createAnonymousInitMessage(unauthorizedDeviceID); } if (!accessToken || !userID) { return null; } const deviceID = await getContentSigningKey(); if (!deviceID) { return null; } return ({ type: 'ConnectionInitializationMessage', deviceID, accessToken, userID, deviceType: getTunnelbrokerDeviceType(), }: ConnectionInitializationMessage); }, [accessToken, shouldBeClosed, unauthorizedDeviceID, userID]); const previousInitMessage = React.useRef(null); const [connected, setConnected] = React.useState(false); const listeners = React.useRef>(new Set()); const socket = React.useRef(null); const currentlyProcessedMessage = React.useRef>(null); const socketSessionCounter = React.useRef(0); const promises = React.useRef({}); const heartbeatTimeoutID = React.useRef(); const identityContext = React.useContext(IdentityClientContext); invariant(identityContext, 'Identity context should be set'); const { identityClient } = identityContext; const stopHeartbeatTimeout = React.useCallback(() => { if (heartbeatTimeoutID.current) { clearTimeout(heartbeatTimeoutID.current); heartbeatTimeoutID.current = null; } }, []); const resetHeartbeatTimeout = React.useCallback(() => { stopHeartbeatTimeout(); heartbeatTimeoutID.current = setTimeout(() => { socket.current?.close(); setConnected(false); }, tunnelbrokerHeartbeatTimeout); }, [stopHeartbeatTimeout]); // determine if the socket is active (not closed or closing) const isSocketActive = socket.current?.readyState === WebSocket.OPEN || socket.current?.readyState === WebSocket.CONNECTING; const connectionChangePromise = React.useRef>(null); // The Tunnelbroker connection can have 4 states: // - DISCONNECTED: isSocketActive = false, connected = false // Should be in this state when initMessage is null // - CONNECTING: isSocketActive = true, connected = false // This lasts until Tunnelbroker sends ConnectionInitializationResponse // - CONNECTED: isSocketActive = true, connected = true // - DISCONNECTING: isSocketActive = false, connected = true // This lasts between socket.close() and socket.onclose() React.useEffect(() => { connectionChangePromise.current = (async () => { await connectionChangePromise.current; try { const initMessage = await createInitMessage(); const initMessageChanged = !_isEqual( previousInitMessage.current, initMessage, ); previousInitMessage.current = initMessage; // when initMessage changes, we need to close the socket // and open a new one if ( (!initMessage || initMessageChanged) && isSocketActive && socket.current ) { socket.current?.close(); return; } // when we're already connected (or pending disconnection), // or there's no init message to start with, we don't need // to do anything if (connected || !initMessage || socket.current) { return; } const tunnelbrokerSocket = new WebSocket(tunnnelbrokerURL); tunnelbrokerSocket.onopen = () => { tunnelbrokerSocket.send(JSON.stringify(initMessage)); }; tunnelbrokerSocket.onclose = () => { // this triggers the effect hook again and reconnect setConnected(false); onClose?.(); socket.current = null; console.log('Connection to Tunnelbroker closed'); }; tunnelbrokerSocket.onerror = e => { console.log('Tunnelbroker socket error:', e.message); }; tunnelbrokerSocket.onmessage = (event: MessageEvent) => { if (typeof event.data !== 'string') { console.log('socket received a non-string message'); return; } let rawMessage; try { rawMessage = JSON.parse(event.data); } catch (e) { console.log('error while parsing Tunnelbroker message:', e.message); return; } if (!tunnelbrokerMessageValidator.is(rawMessage)) { console.log('invalid TunnelbrokerMessage'); return; } const message: TunnelbrokerMessage = rawMessage; resetHeartbeatTimeout(); for (const listener of listeners.current) { listener(message); } if ( message.type === tunnelbrokerMessageTypes.CONNECTION_INITIALIZATION_RESPONSE ) { if (message.status.type === 'Success' && !connected) { setConnected(true); console.log( 'session with Tunnelbroker created. isAuthorized:', isAuthorized, ); } else if (message.status.type === 'Success' && connected) { console.log( 'received ConnectionInitializationResponse with status: Success for already connected socket', ); } else { setConnected(false); console.log( 'creating session with Tunnelbroker error:', message.status.data, ); } } else if ( message.type === tunnelbrokerMessageTypes.MESSAGE_TO_DEVICE ) { const confirmation: MessageReceiveConfirmation = { type: tunnelbrokerMessageTypes.MESSAGE_RECEIVE_CONFIRMATION, messageIDs: [message.messageID], }; let rawPeerToPeerMessage; try { rawPeerToPeerMessage = JSON.parse(message.payload); } catch (e) { console.log( 'error while parsing Tunnelbroker peer-to-peer message:', e.message, ); // Client received incorrect message, confirm to remove from // Tunnelbroker queue. socket.current?.send(JSON.stringify(confirmation)); return; } if (!peerToPeerMessageValidator.is(rawPeerToPeerMessage)) { console.log('invalid Tunnelbroker PeerToPeerMessage'); // The client received an invalid Tunnelbroker message, // and cannot process this type of request. socket.current?.send(JSON.stringify(confirmation)); return; } const peerToPeerMessage: PeerToPeerMessage = rawPeerToPeerMessage; currentlyProcessedMessage.current = (async () => { const localSocketSessionCounter = socketSessionCounter.current; await currentlyProcessedMessage.current; // Since scheduling processing this message socket is closed // or was closed and reopened, we have to stop processing // because Tunnelbroker flushes the message again when opening // the socket, and we want to process this only once // to maintain order. if ( localSocketSessionCounter !== socketSessionCounter.current || !socket.current ) { return; } try { await peerToPeerMessageHandler( peerToPeerMessage, identityClient, message.messageID, ); } catch (e) { console.log(e.message); } finally { if ( localSocketSessionCounter === socketSessionCounter.current && socket.current ) { // We confirm regardless of success or error while processing. socket.current.send(JSON.stringify(confirmation)); } } })(); } else if ( message.type === tunnelbrokerMessageTypes.MESSAGE_TO_DEVICE_REQUEST_STATUS ) { for (const status: MessageSentStatus of message.clientMessageIDs) { if (status.type === 'Success') { promises.current[status.data]?.resolve(); delete promises.current[status.data]; } else if (status.type === 'Error') { promises.current[status.data.id]?.reject(status.data.error); delete promises.current[status.data.id]; } else if (status.type === 'SerializationError') { console.log('SerializationError for message: ', status.data); } else if (status.type === 'InvalidRequest') { console.log('Tunnelbroker recorded InvalidRequest'); } } } else if (message.type === tunnelbrokerMessageTypes.HEARTBEAT) { const heartbeat: Heartbeat = { type: tunnelbrokerMessageTypes.HEARTBEAT, }; socket.current?.send(JSON.stringify(heartbeat)); } }; socket.current = tunnelbrokerSocket; socketSessionCounter.current = socketSessionCounter.current + 1; } catch (err) { console.log('Tunnelbroker connection error:', err); } })(); }, [ connected, isSocketActive, isAuthorized, resetHeartbeatTimeout, stopHeartbeatTimeout, identityClient, onClose, createInitMessage, ]); const sendMessageToDeviceRequest: ( request: MessageToDeviceRequest, ) => Promise = React.useCallback( request => { return new Promise((resolve, reject) => { const socketActive = connected && socket.current; if (!shouldBeClosed && !socketActive) { throw new Error('Tunnelbroker not connected'); } promises.current[request.clientMessageID] = { resolve, reject, }; if (socketActive) { socket.current?.send(JSON.stringify(request)); } else { secondaryTunnelbrokerConnection?.sendMessage(request); } }); }, [connected, secondaryTunnelbrokerConnection, shouldBeClosed], ); - const sendMessage: (message: ClientMessageToDevice) => Promise = - React.useCallback( - (message: ClientMessageToDevice) => { - const clientMessageID = uuid.v4(); - const messageToDevice: MessageToDeviceRequest = { - type: tunnelbrokerMessageTypes.MESSAGE_TO_DEVICE_REQUEST, - clientMessageID, - deviceID: message.deviceID, - payload: message.payload, - }; - - return sendMessageToDeviceRequest(messageToDevice); - }, - [sendMessageToDeviceRequest], - ); + const sendMessage: ( + message: TunnelbrokerClientMessageToDevice, + ) => Promise = React.useCallback( + (message: TunnelbrokerClientMessageToDevice) => { + const clientMessageID = uuid.v4(); + const messageToDevice: MessageToDeviceRequest = { + type: tunnelbrokerMessageTypes.MESSAGE_TO_DEVICE_REQUEST, + clientMessageID, + deviceID: message.deviceID, + payload: message.payload, + }; + + return sendMessageToDeviceRequest(messageToDevice); + }, + [sendMessageToDeviceRequest], + ); React.useEffect( () => secondaryTunnelbrokerConnection?.onSendMessage(message => { if (shouldBeClosed) { // We aren't supposed to be handling it return; } void (async () => { try { await sendMessageToDeviceRequest(message); secondaryTunnelbrokerConnection.setMessageStatus( message.clientMessageID, ); } catch (error) { secondaryTunnelbrokerConnection.setMessageStatus( message.clientMessageID, error, ); } })(); }), [ secondaryTunnelbrokerConnection, sendMessageToDeviceRequest, shouldBeClosed, ], ); React.useEffect( () => secondaryTunnelbrokerConnection?.onMessageStatus((messageID, error) => { if (error) { promises.current[messageID].reject(error); } else { promises.current[messageID].resolve(); } delete promises.current[messageID]; }), [secondaryTunnelbrokerConnection], ); const addListener = React.useCallback( (listener: TunnelbrokerSocketListener) => { listeners.current.add(listener); }, [], ); const removeListener = React.useCallback( (listener: TunnelbrokerSocketListener) => { listeners.current.delete(listener); }, [], ); const value: TunnelbrokerContextType = React.useMemo( () => ({ sendMessage, connected, isAuthorized, addListener, removeListener, setUnauthorizedDeviceID, }), [addListener, connected, removeListener, sendMessage, isAuthorized], ); return ( {children} ); } function useTunnelbroker(): TunnelbrokerContextType { const context = React.useContext(TunnelbrokerContext); invariant(context, 'TunnelbrokerContext not found'); return context; } export { TunnelbrokerProvider, useTunnelbroker }; diff --git a/lib/utils/crypto-utils.js b/lib/utils/crypto-utils.js index f53973248..832919d6c 100644 --- a/lib/utils/crypto-utils.js +++ b/lib/utils/crypto-utils.js @@ -1,118 +1,118 @@ // @flow import t from 'tcomb'; import { type TInterface } from 'tcomb'; import { getConfig } from './config.js'; import { primaryIdentityPublicKeyRegex } from './siwe-utils.js'; import { tRegex, tShape } from './validation-utils.js'; import type { AuthMetadata } from '../shared/identity-client-context.js'; -import type { ClientMessageToDevice } from '../tunnelbroker/tunnelbroker-context.js'; +import type { TunnelbrokerClientMessageToDevice } from '../tunnelbroker/tunnelbroker-context.js'; import type { IdentityKeysBlob, OLMIdentityKeys, SignedIdentityKeysBlob, } from '../types/crypto-types'; import type { IdentityServiceClient } from '../types/identity-service-types'; import { type OutboundSessionCreation, peerToPeerMessageTypes, } from '../types/tunnelbroker/peer-to-peer-message-types.js'; const signedIdentityKeysBlobValidator: TInterface = tShape({ payload: t.String, signature: t.String, }); const olmIdentityKeysValidator: TInterface = tShape({ ed25519: tRegex(primaryIdentityPublicKeyRegex), curve25519: tRegex(primaryIdentityPublicKeyRegex), }); const identityKeysBlobValidator: TInterface = tShape({ primaryIdentityPublicKeys: olmIdentityKeysValidator, notificationIdentityPublicKeys: olmIdentityKeysValidator, }); async function getContentSigningKey(): Promise { const { olmAPI } = getConfig(); await olmAPI.initializeCryptoAccount(); const { primaryIdentityPublicKeys: { ed25519 }, } = await olmAPI.getUserPublicKey(); return ed25519; } async function createOlmSessionsWithOwnDevices( authMetadata: AuthMetadata, identityClient: IdentityServiceClient, - sendMessage: (message: ClientMessageToDevice) => Promise, + sendMessage: (message: TunnelbrokerClientMessageToDevice) => Promise, ): Promise { const { olmAPI } = getConfig(); const { userID, deviceID, accessToken } = authMetadata; await olmAPI.initializeCryptoAccount(); if (!userID || !deviceID || !accessToken) { throw new Error('CommServicesAuthMetadata is missing'); } const keysResponse = await identityClient.getOutboundKeysForUser(userID); for (const deviceKeys of keysResponse) { const { keys } = deviceKeys; if (!keys) { console.log(`Keys missing for device ${deviceKeys.deviceID}`); continue; } const { primaryIdentityPublicKeys } = keys.identityKeysBlob; if (primaryIdentityPublicKeys.ed25519 === deviceID) { continue; } const recipientDeviceID = primaryIdentityPublicKeys.ed25519; if (!keys.contentInitializationInfo.oneTimeKey) { console.log(`One-time key is missing for device ${recipientDeviceID}`); continue; } try { const { sessionVersion, encryptedData } = await olmAPI.contentOutboundSessionCreator( primaryIdentityPublicKeys, keys.contentInitializationInfo, ); const sessionCreationMessage: OutboundSessionCreation = { type: peerToPeerMessageTypes.OUTBOUND_SESSION_CREATION, senderInfo: { userID, deviceID, }, encryptedData, sessionVersion, }; await sendMessage({ deviceID: recipientDeviceID, payload: JSON.stringify(sessionCreationMessage), }); console.log( `Request to create a session with device ${recipientDeviceID} sent.`, ); } catch (e) { console.log( 'Error creating outbound session with ' + `device ${recipientDeviceID}: ${e.message}`, ); } } } export { signedIdentityKeysBlobValidator, identityKeysBlobValidator, getContentSigningKey, createOlmSessionsWithOwnDevices, }; diff --git a/web/settings/tunnelbroker-test.react.js b/web/settings/tunnelbroker-test.react.js index ec612b067..b1e5962f8 100644 --- a/web/settings/tunnelbroker-test.react.js +++ b/web/settings/tunnelbroker-test.react.js @@ -1,152 +1,152 @@ // @flow import invariant from 'invariant'; import * as React from 'react'; -import { type ClientMessageToDevice } from 'lib/tunnelbroker/tunnelbroker-context.js'; +import { type TunnelbrokerClientMessageToDevice } from 'lib/tunnelbroker/tunnelbroker-context.js'; import { type EncryptedMessage, peerToPeerMessageTypes, } from 'lib/types/tunnelbroker/peer-to-peer-message-types.js'; import { getContentSigningKey } from 'lib/utils/crypto-utils.js'; import css from './tunnelbroker-test.css'; import Button from '../components/button.react.js'; import { olmAPI } from '../crypto/olm-api.js'; import Input from '../modals/input.react.js'; import Modal from '../modals/modal.react.js'; import { useSelector } from '../redux/redux-utils.js'; type Props = { - +sendMessage: (message: ClientMessageToDevice) => Promise, + +sendMessage: (message: TunnelbrokerClientMessageToDevice) => Promise, +onClose: () => void, }; function TunnelbrokerTestScreen(props: Props): React.Node { const { sendMessage, onClose } = props; const [recipient, setRecipient] = React.useState(''); const [message, setMessage] = React.useState(''); const [loading, setLoading] = React.useState(false); const [errorMessage, setErrorMessage] = React.useState(''); const recipientInput = React.useRef(null); const messageInput = React.useRef(null); const currentUserID = useSelector( state => state.currentUserInfo && state.currentUserInfo.id, ); const onSubmit = React.useCallback( async (event: SyntheticEvent) => { event.preventDefault(); setLoading(true); try { await sendMessage({ deviceID: recipient, payload: message }); } catch (e) { setErrorMessage(e.message); } setLoading(false); }, [message, recipient, sendMessage], ); const onSubmitEncrypted = React.useCallback( async (event: SyntheticEvent) => { event.preventDefault(); if (!currentUserID) { return; } setLoading(true); try { await olmAPI.initializeCryptoAccount(); const encryptedData = await olmAPI.encrypt( `Encrypted message to ${recipient}`, recipient, ); const deviceID = await getContentSigningKey(); const encryptedMessage: EncryptedMessage = { type: peerToPeerMessageTypes.ENCRYPTED_MESSAGE, senderInfo: { deviceID, userID: currentUserID, }, encryptedData, }; await sendMessage({ deviceID: recipient, payload: JSON.stringify(encryptedMessage), }); } catch (e) { setErrorMessage(e.message); } setLoading(false); }, [currentUserID, recipient, sendMessage], ); let errorMsg; if (errorMessage) { errorMsg =
{errorMessage}
; } return (
) => { const target = event.target; invariant(target instanceof HTMLInputElement, 'target not input'); setRecipient(target.value); }} disabled={loading} ref={recipientInput} label="Recipient" /> ) => { const target = event.target; invariant(target instanceof HTMLInputElement, 'target not input'); setMessage(target.value); }} disabled={loading} ref={messageInput} label="Message" />
{errorMsg}
{errorMsg}
); } export default TunnelbrokerTestScreen;