diff --git a/keyserver/src/socket/tunnelbroker-socket.js b/keyserver/src/socket/tunnelbroker-socket.js new file mode 100644 --- /dev/null +++ b/keyserver/src/socket/tunnelbroker-socket.js @@ -0,0 +1,152 @@ +// @flow + +import uuid from 'uuid'; +import WebSocket from 'ws'; + +import type { ClientMessageToDevice } from 'lib/tunnelbroker/tunnelbroker-context.js'; +import { + type RefreshKeyRequest, + refreshKeysRequestValidator, +} from 'lib/types/tunnelbroker/keys-types.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 { ConnectionInitializationMessage } from 'lib/types/tunnelbroker/session-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; + promises: Promises; + + constructor(socketURL: string, initMessage: ConnectionInitializationMessage) { + this.connected = false; + this.promises = {}; + + const socket = new WebSocket(socketURL); + + socket.on('open', () => { + socket.send(JSON.stringify(initMessage)); + }); + + socket.on('close', async () => { + this.connected = false; + console.error('Connection to Tunnelbroker closed'); + }); + + 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; + + if ( + message.type === + tunnelbrokerMessageTypes.CONNECTION_INITIALIZATION_RESPONSE + ) { + if (message.status.type === 'Success') { + this.connected = true; + console.info('session with Tunnelbroker created'); + } 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; + await uploadNewOneTimeKeys(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') { + this.promises[status.data]?.resolve(); + delete this.promises[status.data]; + } else if (status.type === 'Error') { + this.promises[status.data.id]?.reject(status.data.error); + delete this.promises[status.data.id]; + } else if (status.type === 'SerializationError') { + console.error('SerializationError for message: ', status.data); + } else if (status.type === 'InvalidRequest') { + console.log('Tunnelbroker recorded InvalidRequest'); + } + } + } + }; + + sendMessage: (message: ClientMessageToDevice) => Promise = ( + message: ClientMessageToDevice, + ) => { + 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)); + }); + }; +} + +export default TunnelbrokerSocket; diff --git a/keyserver/src/socket/tunnelbroker.js b/keyserver/src/socket/tunnelbroker.js --- a/keyserver/src/socket/tunnelbroker.js +++ b/keyserver/src/socket/tunnelbroker.js @@ -1,21 +1,11 @@ // @flow -import WebSocket from 'ws'; - -import { - refreshKeysTBMessageValidator, - type TBKeyserverConnectionInitializationMessage, - type MessageFromTunnelbroker, - tunnelbrokerMessageTypes, -} from 'lib/types/tunnelbroker-messages.js'; +import type { ConnectionInitializationMessage } from 'lib/types/tunnelbroker/session-types.js'; import { getCommConfig } from 'lib/utils/comm-config.js'; -import { ServerError } from 'lib/utils/errors.js'; +import TunnelbrokerSocket from './tunnelbroker-socket.js'; import { type IdentityInfo } from '../user/identity.js'; -import { - uploadNewOneTimeKeys, - getContentSigningKey, -} from '../utils/olm-utils.js'; +import { getContentSigningKey } from '../utils/olm-utils.js'; type TBConnectionInfo = { +url: string, @@ -45,61 +35,15 @@ getTBConnectionInfo(), ]); - openTunnelbrokerConnection( - deviceID, - identityInfo.userId, - identityInfo.accessToken, - tbConnectionInfo.url, - ); -} - -function handleTBMessageEvent(event: ArrayBuffer): Promise { - const rawMessage = JSON.parse(event.toString()); - if (!refreshKeysTBMessageValidator.is(rawMessage)) { - throw new ServerError('unsupported_tunnelbroker_message'); - } - const message: MessageFromTunnelbroker = rawMessage; - - if (message.type === tunnelbrokerMessageTypes.REFRESH_KEYS_REQUEST) { - return uploadNewOneTimeKeys(message.numberOfKeys); - } - throw new ServerError('unsupported_tunnelbroker_message'); -} - -function openTunnelbrokerConnection( - deviceID: string, - userID: string, - accessToken: string, - tbUrl: string, -) { - try { - const tunnelbrokerSocket = new WebSocket(tbUrl); - - tunnelbrokerSocket.on('open', () => { - const message: TBKeyserverConnectionInitializationMessage = { - type: 'sessionRequest', - accessToken, - deviceID, - deviceType: 'keyserver', - userID, - }; - - tunnelbrokerSocket.send(JSON.stringify(message)); - console.info('Connection to Tunnelbroker established'); - }); - - tunnelbrokerSocket.on('close', async () => { - console.warn('Connection to Tunnelbroker closed'); - }); - - tunnelbrokerSocket.on('error', (error: Error) => { - console.error('Tunnelbroker socket error', error.message); - }); + const initMessage: ConnectionInitializationMessage = { + type: 'ConnectionInitializationMessage', + deviceID: deviceID, + accessToken: identityInfo.accessToken, + userID: identityInfo.userId, + deviceType: 'keyserver', + }; - tunnelbrokerSocket.on('message', handleTBMessageEvent); - } catch { - console.log('Failed to open connection with Tunnelbroker'); - } + new TunnelbrokerSocket(tbConnectionInfo.url, initMessage); } export { createAndMaintainTunnelbrokerWebsocket }; diff --git a/lib/types/tunnelbroker-messages.js b/lib/types/tunnelbroker-messages.js deleted file mode 100644 --- a/lib/types/tunnelbroker-messages.js +++ /dev/null @@ -1,55 +0,0 @@ -// @flow - -import t, { type TInterface } from 'tcomb'; - -import { tShape, tString } from '../utils/validation-utils.js'; - -type TBSharedConnectionInitializationMessage = { - +type: 'sessionRequest', - +deviceID: string, - +accessToken: string, - +deviceAppVersion?: string, - +userID: string, -}; - -export type TBKeyserverConnectionInitializationMessage = { - ...TBSharedConnectionInitializationMessage, - +deviceType: 'keyserver', -}; - -export type TBClientConnectionInitializationMessage = { - ...TBSharedConnectionInitializationMessage, - +deviceType: 'web' | 'mobile', -}; - -export type TBNotifyClientConnectionInitializationMessage = { - ...TBClientConnectionInitializationMessage, - +notifyToken: string, - +notifyPlatform: 'apns' | 'fcm' | 'web' | 'wns', -}; - -export type MessageToTunnelbroker = - | TBKeyserverConnectionInitializationMessage - | TBClientConnectionInitializationMessage - | TBNotifyClientConnectionInitializationMessage; - -export const tunnelbrokerMessageTypes = Object.freeze({ - REFRESH_KEYS_REQUEST: 'RefreshKeyRequest', -}); - -export type TBRefreshKeysRequest = { - +type: 'RefreshKeyRequest', - +deviceID: string, - +numberOfKeys: number, -}; - -export const refreshKeysTBMessageValidator: TInterface = - tShape({ - type: tString('RefreshKeyRequest'), - deviceID: t.String, - numberOfKeys: t.Number, - }); - -// Disjoint enumeration of all messages received from Tunnelbroker -// Currently, only a single message -export type MessageFromTunnelbroker = TBRefreshKeysRequest;