peerconnection.ts 11 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299
  1. /**
  2. * This file is part of Threema Web.
  3. *
  4. * Threema Web is free software: you can redistribute it and/or modify it
  5. * under the terms of the GNU Affero General Public License as published by
  6. * the Free Software Foundation, either version 3 of the License, or (at
  7. * your option) any later version.
  8. *
  9. * This program is distributed in the hope that it will be useful, but
  10. * WITHOUT ANY WARRANTY; without even the implied warranty of
  11. * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU Affero
  12. * General Public License for more details.
  13. *
  14. * You should have received a copy of the GNU Affero General Public License
  15. * along with Threema Web. If not, see <http://www.gnu.org/licenses/>.
  16. */
  17. import TaskConnectionState = threema.TaskConnectionState;
  18. import {Logger} from 'ts-log';
  19. import {ConfidentialIceCandidate} from '../helpers/confidential';
  20. import {UnboundedFlowControlledDataChannel} from '../helpers/data_channel';
  21. import {LogService} from './log';
  22. import {TimeoutService} from './timeout';
  23. /**
  24. * Wrapper around the WebRTC PeerConnection.
  25. */
  26. export class PeerConnectionHelper {
  27. private static readonly CONNECTION_FAILED_TIMEOUT_MS = 15000;
  28. // Angular services
  29. private readonly log: Logger;
  30. private readonly $q: ng.IQService;
  31. private readonly $rootScope: ng.IRootScopeService;
  32. // Custom services
  33. private readonly config: threema.Config;
  34. private readonly logService: LogService;
  35. private readonly timeoutService: TimeoutService;
  36. // WebRTC
  37. public readonly pc: RTCPeerConnection;
  38. private readonly webrtcTask: saltyrtc.tasks.webrtc.WebRTCTask;
  39. private connectionFailedTimer: ng.IPromise<void> | null = null;
  40. // Handed over signalling channel
  41. private sdc: UnboundedFlowControlledDataChannel | null = null;
  42. // Calculated connection state
  43. public connectionState: TaskConnectionState = TaskConnectionState.New;
  44. public onConnectionStateChange: (state: TaskConnectionState) => void = null;
  45. constructor(
  46. $q: ng.IQService,
  47. $rootScope: ng.IRootScopeService,
  48. config: threema.Config,
  49. logService: LogService,
  50. timeoutService: TimeoutService,
  51. webrtcTask: saltyrtc.tasks.webrtc.WebRTCTask,
  52. iceServers: RTCIceServer[],
  53. ) {
  54. this.log = logService.getLogger('PeerConnection', 'color: #fff; background-color: #3333ff');
  55. this.log.info('Initialize WebRTC PeerConnection');
  56. this.log.debug('ICE servers used:', [].concat(...iceServers.map((c) => c.urls)));
  57. this.$q = $q;
  58. this.$rootScope = $rootScope;
  59. this.config = config;
  60. this.logService = logService;
  61. this.timeoutService = timeoutService;
  62. this.webrtcTask = webrtcTask;
  63. // Set up peer connection
  64. this.pc = new RTCPeerConnection({iceServers: iceServers});
  65. this.pc.onnegotiationneeded = () => {
  66. this.log.debug('RTCPeerConnection: negotiation needed');
  67. this.initiatorFlow()
  68. .then(() => this.log.debug('Initiator flow done'));
  69. };
  70. // Handle state changes
  71. this.pc.onconnectionstatechange = () => {
  72. this.log.debug('Connection state change:', this.pc.connectionState);
  73. };
  74. this.pc.onsignalingstatechange = () => {
  75. this.log.debug('Signaling state change:', this.pc.signalingState);
  76. };
  77. // Set up ICE candidate handling
  78. this.setupIceCandidateHandling();
  79. // Log incoming data channels
  80. this.pc.ondatachannel = (e: RTCDataChannelEvent) => {
  81. this.log.debug('New data channel was created:', e.channel.label);
  82. };
  83. }
  84. /**
  85. * Return the wrapped RTCPeerConnection instance.
  86. */
  87. public get peerConnection(): RTCPeerConnection {
  88. return this.pc;
  89. }
  90. /**
  91. * Set up receiving / sending of ICE candidates.
  92. */
  93. private setupIceCandidateHandling() {
  94. this.log.debug('Setting up ICE candidate handling');
  95. this.pc.onicecandidate = (e: RTCPeerConnectionIceEvent) => {
  96. if (e.candidate) {
  97. this.log.debug('Gathered local ICE candidate:', new ConfidentialIceCandidate(e.candidate.candidate));
  98. this.webrtcTask.sendCandidate({
  99. candidate: e.candidate.candidate,
  100. sdpMid: e.candidate.sdpMid,
  101. sdpMLineIndex: e.candidate.sdpMLineIndex,
  102. });
  103. } else {
  104. this.log.debug('No more local ICE candidates');
  105. }
  106. };
  107. this.pc.onicecandidateerror = (e: RTCPeerConnectionIceErrorEvent) => {
  108. this.log.warn(`ICE candidate error: ${e.errorText} ` +
  109. `(url=${e.url}, host-candidate=${e.hostCandidate}, code=${e.errorCode})`);
  110. };
  111. this.pc.oniceconnectionstatechange = () => {
  112. this.log.debug('ICE connection state change:', this.pc.iceConnectionState);
  113. this.$rootScope.$apply(() => {
  114. // Cancel connection failed timer
  115. if (this.connectionFailedTimer !== null) {
  116. this.timeoutService.cancel(this.connectionFailedTimer);
  117. this.connectionFailedTimer = null;
  118. }
  119. // Handle state
  120. switch (this.pc.iceConnectionState) {
  121. case 'new':
  122. this.setConnectionState(TaskConnectionState.New);
  123. break;
  124. case 'checking':
  125. case 'disconnected':
  126. this.setConnectionState(TaskConnectionState.Connecting);
  127. // Setup connection failed timer
  128. // Note: There is no guarantee that we will end up in the 'failed' state, so we need to set up
  129. // our own timer as well.
  130. this.connectionFailedTimer = this.timeoutService.register(() => {
  131. // Closing the peer connection to prevent "SURPRISE, the connection works after all!"
  132. // situations which certainly would lead to ugly race conditions.
  133. this.connectionFailedTimer = null;
  134. this.log.debug('ICE connection considered failed');
  135. this.pc.close();
  136. }, PeerConnectionHelper.CONNECTION_FAILED_TIMEOUT_MS, true, 'connectionFailedTimer');
  137. break;
  138. case 'connected':
  139. case 'completed':
  140. this.setConnectionState(TaskConnectionState.Connected);
  141. break;
  142. case 'failed':
  143. case 'closed':
  144. this.setConnectionState(TaskConnectionState.Disconnected);
  145. break;
  146. default:
  147. this.log.warn('Ignored ICE connection state change to',
  148. this.pc.iceConnectionState);
  149. }
  150. });
  151. };
  152. this.pc.onicegatheringstatechange = () => {
  153. this.log.debug('ICE gathering state change:', this.pc.iceGatheringState);
  154. };
  155. this.webrtcTask.on('candidates', (e: saltyrtc.tasks.webrtc.CandidatesEvent) => {
  156. for (const candidateInit of e.data) {
  157. if (candidateInit) {
  158. this.log.debug('Adding remote ICE candidate:',
  159. new ConfidentialIceCandidate(candidateInit.candidate));
  160. } else {
  161. this.log.debug('No more remote ICE candidates');
  162. }
  163. this.pc.addIceCandidate(candidateInit)
  164. .catch((error) => this.log.warn('Unable to add ice candidate:', error));
  165. }
  166. });
  167. }
  168. private async initiatorFlow(): Promise<void> {
  169. // Send offer
  170. const offer: RTCSessionDescriptionInit = await this.pc.createOffer();
  171. await this.pc.setLocalDescription(offer);
  172. this.log.debug('Created offer, set local description');
  173. this.webrtcTask.sendOffer(offer);
  174. // Receive answer
  175. const receiveAnswer: () => Promise<saltyrtc.tasks.webrtc.Answer> = () => {
  176. return new Promise((resolve) => {
  177. this.webrtcTask.once('answer', (e: saltyrtc.tasks.webrtc.AnswerEvent) => {
  178. resolve(e.data);
  179. });
  180. });
  181. };
  182. const answer: RTCSessionDescriptionInit = await receiveAnswer();
  183. await this.pc.setRemoteDescription(answer);
  184. this.log.debug('Received answer, set remote description');
  185. }
  186. /**
  187. * Initiate the handover process.
  188. */
  189. public handover(): void {
  190. if (this.sdc !== null) {
  191. throw new Error('Handover already inintiated');
  192. }
  193. // Get transport link
  194. const link: saltyrtc.tasks.webrtc.SignalingTransportLink = this.webrtcTask.getTransportLink();
  195. // Create data channel
  196. const dc = this.pc.createDataChannel(link.label, {
  197. id: link.id,
  198. negotiated: true,
  199. ordered: true,
  200. protocol: link.protocol,
  201. });
  202. dc.binaryType = 'arraybuffer';
  203. // Wrap as an unbounded, flow-controlled data channel
  204. this.sdc = new UnboundedFlowControlledDataChannel(dc, this.logService, this.config.TRANSPORT_LOG_LEVEL);
  205. // Create transport handler
  206. const self = this;
  207. const handler = {
  208. get maxMessageSize(): number {
  209. return self.pc.sctp.maxMessageSize;
  210. },
  211. close(): void {
  212. self.log.debug(`Signalling data channel close request`);
  213. dc.close();
  214. },
  215. send(message: Uint8Array): void {
  216. self.log.debug(`Signalling data channel outgoing signaling message of ` +
  217. `length ${message.byteLength}`);
  218. self.sdc.write(message);
  219. },
  220. };
  221. // Bind events
  222. dc.onopen = () => {
  223. this.log.info(`Signalling data channel open`);
  224. // Rebind close event
  225. dc.onclose = () => {
  226. this.log.info(`Signalling data channel closed`);
  227. link.closed();
  228. };
  229. // Initiate handover
  230. this.webrtcTask.handover(handler);
  231. };
  232. dc.onclose = () => {
  233. this.log.error(`Signalling data channel closed`);
  234. };
  235. dc.onerror = (event) => {
  236. this.log.error(`Signalling data channel error:`, event);
  237. };
  238. dc.onmessage = (event) => {
  239. this.log.debug(`Signalling data channel incoming message of length ${event.data.byteLength}`);
  240. link.receive(new Uint8Array(event.data));
  241. };
  242. }
  243. /**
  244. * Set the connection state and update listeners.
  245. */
  246. private setConnectionState(state: TaskConnectionState) {
  247. if (state !== this.connectionState) {
  248. this.connectionState = state;
  249. if (this.onConnectionStateChange !== null) {
  250. this.onConnectionStateChange(state);
  251. }
  252. }
  253. }
  254. /**
  255. * Unbind all event handler and abruptly close the peer connection.
  256. */
  257. public close(): void {
  258. this.webrtcTask.off();
  259. this.pc.onnegotiationneeded = null;
  260. this.pc.onconnectionstatechange = null;
  261. this.pc.onsignalingstatechange = null;
  262. this.pc.onicecandidate = null;
  263. this.pc.onicecandidateerror = null;
  264. this.pc.oniceconnectionstatechange = null;
  265. this.pc.onicegatheringstatechange = null;
  266. this.pc.ondatachannel = null;
  267. this.pc.close();
  268. }
  269. }