|
@@ -24,12 +24,23 @@ import {Logger} from 'ts-log';
|
|
|
|
|
|
import * as msgpack from 'msgpack-lite';
|
|
|
import {
|
|
|
- arraysAreEqual, base64ToU8a, bufferToUrl, copyDeepOrReference, hasFeature, hasValue, hexToU8a,
|
|
|
- msgpackVisualizer, randomString, stringToUtf8a, u8aToHex,
|
|
|
+ arraysAreEqual,
|
|
|
+ base64ToU8a,
|
|
|
+ bufferToUrl,
|
|
|
+ copyDeepOrReference,
|
|
|
+ hasFeature,
|
|
|
+ hasValue,
|
|
|
+ hexToU8a,
|
|
|
+ msgpackVisualizer,
|
|
|
+ randomString,
|
|
|
+ stringToUtf8a,
|
|
|
+ u8aToHex,
|
|
|
} from '../helpers';
|
|
|
import {
|
|
|
- isContactReceiver, isDistributionListReceiver,
|
|
|
- isGroupReceiver, isValidReceiverType,
|
|
|
+ isContactReceiver,
|
|
|
+ isDistributionListReceiver,
|
|
|
+ isGroupReceiver,
|
|
|
+ isValidReceiverType,
|
|
|
} from '../typeguards';
|
|
|
import {BatteryStatusService} from './battery';
|
|
|
import {BrowserService} from './browser';
|
|
@@ -49,6 +60,7 @@ import {VersionService} from './version';
|
|
|
|
|
|
import {TimeoutError} from '../exceptions';
|
|
|
import {ConfidentialWireMessage} from '../helpers/confidential';
|
|
|
+import {UnboundedFlowControlledDataChannel} from '../helpers/data_channel';
|
|
|
import {DeviceUnreachableController} from '../partials/messenger';
|
|
|
import {ChunkCache} from '../protocol/cache';
|
|
|
import {SequenceNumber} from '../protocol/sequence_number';
|
|
@@ -82,13 +94,14 @@ const fakeConnectionId = Uint8Array.from([
|
|
|
*/
|
|
|
export class WebClientService {
|
|
|
public static readonly MAX_CONNECT_ATTEMPTS = 3;
|
|
|
- private static CHUNK_SIZE = 64 * 1024;
|
|
|
+ private static DATA_CHANNEL_MAX_CHUNK_SIZE = 256 * 1024;
|
|
|
+ private static RELAYED_DATA_CHUNK_SIZE = 64 * 1024;
|
|
|
private static SEQUENCE_NUMBER_MIN = 0;
|
|
|
private static SEQUENCE_NUMBER_MAX = (2 ** 32) - 1;
|
|
|
private static CHUNK_CACHE_SIZE_MAX = 2 * 1024 * 1024;
|
|
|
private static AVATAR_LOW_MAX_SIZE = 48;
|
|
|
private static MAX_TEXT_LENGTH = 3500;
|
|
|
- private static MAX_FILE_SIZE_WEBRTC = 15 * 1024 * 1024;
|
|
|
+ private static MAX_FILE_SIZE_WEBRTC_TASK_V0 = 15 * 1024 * 1024;
|
|
|
private static CONNECTION_ID_NONCE = stringToUtf8a('connectionidconnectionid');
|
|
|
|
|
|
private static TYPE_REQUEST = 'request';
|
|
@@ -203,9 +216,11 @@ export class WebClientService {
|
|
|
private saltyRtcHost: string = null;
|
|
|
public salty: saltyrtc.SaltyRTC = null;
|
|
|
private connectionInfoFuture: Future<ConnectionInfo> = null;
|
|
|
- private webrtcTask: saltyrtc.tasks.webrtc.WebRTCTask = null;
|
|
|
private relayedDataTask: saltyrtc.tasks.relayed_data.RelayedDataTask = null;
|
|
|
- private secureDataChannel: saltyrtc.tasks.webrtc.SecureDataChannel = null;
|
|
|
+ private secureDataChannel: UnboundedFlowControlledDataChannel = null;
|
|
|
+ private secureDataChannelCrypto: saltyrtc.tasks.webrtc.DataChannelCryptoContext = null;
|
|
|
+ private secureDataChannelChunkLength: number = null;
|
|
|
+ private secureDataChannelMessageId: number = 0;
|
|
|
public chosenTask: threema.ChosenTask = threema.ChosenTask.None;
|
|
|
private outgoingMessageSequenceNumber: SequenceNumber;
|
|
|
private previousConnectionId: Uint8Array = null;
|
|
@@ -219,7 +234,7 @@ export class WebClientService {
|
|
|
private pendingAckRequest: number | null = null;
|
|
|
|
|
|
// Message chunking
|
|
|
- private unchunker: chunkedDc.Unchunker = null;
|
|
|
+ private unchunker: chunkedDc.UnreliableUnorderedUnchunker = null;
|
|
|
|
|
|
// Messenger data
|
|
|
public messages: threema.Container.Messages;
|
|
@@ -481,12 +496,28 @@ export class WebClientService {
|
|
|
// Create new handshake future
|
|
|
this.connectionInfoFuture = new Future();
|
|
|
|
|
|
- // Create WebRTC task instance
|
|
|
- const maxPacketSize = this.browserService.getBrowser().isFirefox(false) ? 16384 : 65536;
|
|
|
- this.webrtcTask = new saltyrtcTaskWebrtc.WebRTCTask(true, maxPacketSize, this.config.SALTYRTC_LOG_LEVEL);
|
|
|
+ // Create tasks
|
|
|
+ const tasks: saltyrtc.Task[] = [];
|
|
|
+
|
|
|
+ // Create WebRTC task instance (if supported)
|
|
|
+ if (this.browserService.supportsWebrtcTask()) {
|
|
|
+ // TODO: Remove legacy v0 after a transitional period
|
|
|
+ tasks.push(new saltyrtcTaskWebrtc.WebRTCTaskBuilder()
|
|
|
+ .withLoggingLevel(this.config.SALTYRTC_LOG_LEVEL)
|
|
|
+ .withVersion('v1')
|
|
|
+ .withHandover(true)
|
|
|
+ .build());
|
|
|
+ tasks.push(new saltyrtcTaskWebrtc.WebRTCTaskBuilder()
|
|
|
+ .withLoggingLevel(this.config.SALTYRTC_LOG_LEVEL)
|
|
|
+ .withVersion('v0')
|
|
|
+ .withHandover(true)
|
|
|
+ .withMaxChunkLength(this.browserService.getBrowser().isFirefox(false) ? 16384 : 65536)
|
|
|
+ .build());
|
|
|
+ }
|
|
|
|
|
|
// Create Relayed Data task instance
|
|
|
this.relayedDataTask = new saltyrtcTaskRelayedData.RelayedDataTask(this.config.SALTYRTC_LOG_LEVEL === 'debug');
|
|
|
+ tasks.push(this.relayedDataTask);
|
|
|
|
|
|
// Create new keystore if necessary
|
|
|
if (!keyStore) {
|
|
@@ -504,14 +535,6 @@ export class WebClientService {
|
|
|
+ this.config.SALTYRTC_HOST_SUFFIX;
|
|
|
}
|
|
|
|
|
|
- // Determine SaltyRTC tasks
|
|
|
- let tasks;
|
|
|
- if (this.browserService.supportsWebrtcTask()) {
|
|
|
- tasks = [this.webrtcTask, this.relayedDataTask];
|
|
|
- } else {
|
|
|
- tasks = [this.relayedDataTask];
|
|
|
- }
|
|
|
-
|
|
|
// Create SaltyRTC client
|
|
|
let builder = new saltyrtcClient.SaltyRTCBuilder()
|
|
|
.connectTo(this.saltyRtcHost, this.config.SALTYRTC_PORT)
|
|
@@ -570,11 +593,8 @@ export class WebClientService {
|
|
|
|
|
|
// Wait for handover to be finished
|
|
|
this.salty.on('handover', () => {
|
|
|
- // Ignore handovers requested by non-WebRTC tasks
|
|
|
- if (this.chosenTask === threema.ChosenTask.WebRTC) {
|
|
|
- this.arpLog.debug('Handover done');
|
|
|
- this.onHandover(resumeSession);
|
|
|
- }
|
|
|
+ this.arpLog.debug('Handover done');
|
|
|
+ this.onHandover(resumeSession);
|
|
|
});
|
|
|
|
|
|
// Handle SaltyRTC errors
|
|
@@ -768,7 +788,7 @@ export class WebClientService {
|
|
|
this.outgoingMessageSequenceNumber = new SequenceNumber(
|
|
|
0, WebClientService.SEQUENCE_NUMBER_MIN, WebClientService.SEQUENCE_NUMBER_MAX);
|
|
|
}
|
|
|
- this.unchunker = new chunkedDc.Unchunker();
|
|
|
+ this.unchunker = new chunkedDc.UnreliableUnorderedUnchunker();
|
|
|
this.unchunker.onMessage = this.handleIncomingMessageBytes.bind(this);
|
|
|
|
|
|
// Discard previous connection instances
|
|
@@ -871,18 +891,19 @@ export class WebClientService {
|
|
|
this.skipIceDs();
|
|
|
}
|
|
|
|
|
|
+ // Create peer connection
|
|
|
this.pcHelper = new PeerConnectionHelper(
|
|
|
this.$q, this.$rootScope,
|
|
|
- this.logService, this.timeoutService,
|
|
|
- this.webrtcTask, this.config.ICE_SERVERS);
|
|
|
+ this.config, this.logService, this.timeoutService,
|
|
|
+ task as saltyrtc.tasks.webrtc.WebRTCTask, this.config.ICE_SERVERS);
|
|
|
|
|
|
// On state changes in the PeerConnectionHelper class, let state service know about it
|
|
|
this.pcHelper.onConnectionStateChange = (state: threema.TaskConnectionState) => {
|
|
|
this.stateService.updateTaskConnectionState(state);
|
|
|
};
|
|
|
|
|
|
- // Initiate handover
|
|
|
- this.webrtcTask.handover(this.pcHelper.peerConnection);
|
|
|
+ // Initiate handover process
|
|
|
+ this.pcHelper.handover();
|
|
|
|
|
|
// Otherwise, no handover is necessary.
|
|
|
} else {
|
|
@@ -1031,35 +1052,66 @@ export class WebClientService {
|
|
|
|
|
|
// Derive connection ID
|
|
|
// Note: We need to make sure this is done before any ARP messages can be received
|
|
|
- const box = this.salty.encryptForPeer(new Uint8Array(0), WebClientService.CONNECTION_ID_NONCE);
|
|
|
+ const connectionIdBox = this.salty.encryptForPeer(new Uint8Array(0), WebClientService.CONNECTION_ID_NONCE);
|
|
|
// Note: We explicitly copy the data here to be able to use the underlying buffer directly
|
|
|
- this.currentConnectionId = new Uint8Array(box.data);
|
|
|
+ this.currentConnectionId = new Uint8Array(connectionIdBox.data);
|
|
|
|
|
|
// If the WebRTC task was chosen, initialize the data channel
|
|
|
if (this.chosenTask === threema.ChosenTask.WebRTC) {
|
|
|
- // Create secure data channel
|
|
|
- this.arpLog.debug('Create SecureDataChannel "' + WebClientService.DC_LABEL + '"...');
|
|
|
- this.secureDataChannel = this.pcHelper.createSecureDataChannel(WebClientService.DC_LABEL);
|
|
|
- this.secureDataChannel.onopen = () => {
|
|
|
- this.arpLog.debug('SecureDataChannel open');
|
|
|
+ const task = this.salty.getTask() as saltyrtc.tasks.webrtc.WebRTCTask;
|
|
|
+
|
|
|
+ // Create data channel
|
|
|
+ this.arpLog.debug(`Creating data channel ${WebClientService.DC_LABEL}`);
|
|
|
+ const dc = this.pcHelper.pc.createDataChannel(WebClientService.DC_LABEL);
|
|
|
+ dc.binaryType = 'arraybuffer';
|
|
|
+
|
|
|
+ // Wrap as unbounded, flow-controlled data channel
|
|
|
+ this.secureDataChannel = new UnboundedFlowControlledDataChannel(
|
|
|
+ dc, this.logService, this.config.TRANSPORT_LOG_LEVEL);
|
|
|
+
|
|
|
+ // Create crypto context
|
|
|
+ // Note: We need to apply encrypt-then-chunk for backwards
|
|
|
+ // compatibility reasons.
|
|
|
+ this.secureDataChannelCrypto = task.createCryptoContext(dc.id);
|
|
|
+
|
|
|
+ // Create unchunker
|
|
|
+ // Note: We need to use an unreliable unordered unchunker for backwards
|
|
|
+ // compatibility reasons.
|
|
|
+ const unchunker = new chunkedDc.UnreliableUnorderedUnchunker();
|
|
|
+
|
|
|
+ // Bind events
|
|
|
+ dc.onopen = () => {
|
|
|
+ this.arpLog.info(`Data channel ${dc.label} open`);
|
|
|
+
|
|
|
+ // Determine chunk length
|
|
|
+ this.secureDataChannelChunkLength = Math.min(
|
|
|
+ WebClientService.DATA_CHANNEL_MAX_CHUNK_SIZE, this.pcHelper.pc.sctp.maxMessageSize);
|
|
|
+ this.arpLog.debug(`Using chunk length: ${this.secureDataChannelChunkLength} for data channel` +
|
|
|
+ dc.label);
|
|
|
+
|
|
|
+ // Connection established
|
|
|
this.onConnectionEstablished(resumeSession).catch((error) => {
|
|
|
this.arpLog.error('Error during handshake:', error);
|
|
|
});
|
|
|
};
|
|
|
-
|
|
|
- // Handle incoming messages
|
|
|
- this.secureDataChannel.onmessage = (ev: MessageEvent) => {
|
|
|
- const bytes = new Uint8Array(ev.data);
|
|
|
- this.handleIncomingMessageBytes(bytes);
|
|
|
+ dc.onclose = () => {
|
|
|
+ this.arpLog.error(`Data channel ${dc.label} closed prematurely`);
|
|
|
+ this.failSession();
|
|
|
};
|
|
|
- this.secureDataChannel.onbufferedamountlow = (ev: Event) => {
|
|
|
- this.arpLog.debug('Secure data channel: Buffered amount low');
|
|
|
+ dc.onerror = (event) => {
|
|
|
+ this.arpLog.error(`Data channel ${dc.label} error:`, event);
|
|
|
+ this.failSession();
|
|
|
};
|
|
|
- this.secureDataChannel.onerror = (e: ErrorEvent) => {
|
|
|
- this.arpLog.warn('Secure data channel: Error:', e.message);
|
|
|
+ dc.onmessage = (event) => {
|
|
|
+ this.arpLogV.debug(`Data channel ${dc.label} incoming chunk of length ${event.data.byteLength}`);
|
|
|
+ unchunker.add(new Uint8Array(event.data));
|
|
|
};
|
|
|
- this.secureDataChannel.onclose = (ev: Event) => {
|
|
|
- this.arpLog.warn('Secure data channel: Closed');
|
|
|
+ // noinspection JSUndefinedPropertyAssignment
|
|
|
+ unchunker.onMessage = (array) => {
|
|
|
+ const box = saltyrtcClient.Box.fromUint8Array(
|
|
|
+ array, saltyrtcTaskWebrtc.DataChannelCryptoContext.NONCE_LENGTH);
|
|
|
+ const message = this.secureDataChannelCrypto.decrypt(box);
|
|
|
+ this.handleIncomingMessageBytes(message);
|
|
|
};
|
|
|
|
|
|
// Mark as handed over
|
|
@@ -1324,13 +1376,13 @@ export class WebClientService {
|
|
|
|
|
|
// Close data channel
|
|
|
if (this.secureDataChannel !== null) {
|
|
|
- this.arpLog.debug('Closing secure datachannel');
|
|
|
- this.secureDataChannel.onopen = null;
|
|
|
- this.secureDataChannel.onmessage = null;
|
|
|
- this.secureDataChannel.onbufferedamountlow = null;
|
|
|
- this.secureDataChannel.onerror = null;
|
|
|
- this.secureDataChannel.onclose = null;
|
|
|
- this.secureDataChannel.close();
|
|
|
+ this.arpLog.debug('Closing data channel');
|
|
|
+ this.secureDataChannel.dc.onopen = null;
|
|
|
+ this.secureDataChannel.dc.onmessage = null;
|
|
|
+ this.secureDataChannel.dc.onbufferedamountlow = null;
|
|
|
+ this.secureDataChannel.dc.onerror = null;
|
|
|
+ this.secureDataChannel.dc.onclose = null;
|
|
|
+ this.secureDataChannel.dc.close();
|
|
|
}
|
|
|
|
|
|
// Close SaltyRTC connection
|
|
@@ -1743,7 +1795,8 @@ export class WebClientService {
|
|
|
|
|
|
// Validate max file size
|
|
|
if (this.chosenTask === threema.ChosenTask.WebRTC) {
|
|
|
- if (fileData.size > WebClientService.MAX_FILE_SIZE_WEBRTC) {
|
|
|
+ const task = this.salty.getTask() as saltyrtc.tasks.webrtc.WebRTCTask;
|
|
|
+ if (task.version === 'v0' && fileData.size > WebClientService.MAX_FILE_SIZE_WEBRTC_TASK_V0) {
|
|
|
throw this.$translate.instant('error.FILE_TOO_LARGE_WEB');
|
|
|
}
|
|
|
} else {
|
|
@@ -3954,7 +4007,14 @@ export class WebClientService {
|
|
|
if (this.config.MSGPACK_LOG_TRACE) {
|
|
|
this.msgpackLog.debug('Outgoing message payload: ' + msgpackVisualizer(bytes));
|
|
|
}
|
|
|
- this.secureDataChannel.send(bytes);
|
|
|
+ const box = this.secureDataChannelCrypto.encrypt(bytes);
|
|
|
+ const chunker = new chunkedDc.UnreliableUnorderedChunker(
|
|
|
+ this.secureDataChannelMessageId++, box.toUint8Array(), this.secureDataChannelChunkLength);
|
|
|
+ for (const chunk of chunker) {
|
|
|
+ this.arpLogV.debug(`Data channel ${this.secureDataChannel.dc.label} outgoing ` +
|
|
|
+ `chunk of length ${chunk.byteLength}`);
|
|
|
+ this.secureDataChannel.write(chunk);
|
|
|
+ }
|
|
|
}
|
|
|
break;
|
|
|
case threema.ChosenTask.RelayedData:
|
|
@@ -3971,7 +4031,8 @@ export class WebClientService {
|
|
|
|
|
|
// Increment the outgoing message sequence number
|
|
|
const messageSequenceNumber = this.outgoingMessageSequenceNumber.increment();
|
|
|
- const chunker = new chunkedDc.Chunker(messageSequenceNumber, bytes, WebClientService.CHUNK_SIZE);
|
|
|
+ const chunker = new chunkedDc.UnreliableUnorderedChunker(
|
|
|
+ messageSequenceNumber, bytes, WebClientService.RELAYED_DATA_CHUNK_SIZE);
|
|
|
for (const chunk of chunker) {
|
|
|
// Send (and cache)
|
|
|
this.sendChunk(chunk, retransmit, canQueue, true);
|
|
@@ -4075,7 +4136,7 @@ export class WebClientService {
|
|
|
// Warning: Nothing should be called after the unchunker has processed
|
|
|
// the chunk since the message event is synchronous and can
|
|
|
// result in a call to .stop!
|
|
|
- this.unchunker.add(chunk.buffer);
|
|
|
+ this.unchunker.add(chunk);
|
|
|
}
|
|
|
|
|
|
/**
|
|
@@ -4089,7 +4150,6 @@ export class WebClientService {
|
|
|
|
|
|
// Decode bytes
|
|
|
const message: threema.WireMessage = this.msgpackDecode(bytes);
|
|
|
-
|
|
|
return this.handleIncomingMessage(message);
|
|
|
}
|
|
|
|