peerconnection.ts 8.7 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230
  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 * as SDPUtils from 'sdp';
  18. import TaskConnectionState = threema.TaskConnectionState;
  19. /**
  20. * Wrapper around the WebRTC PeerConnection.
  21. */
  22. export class PeerConnectionHelper {
  23. private logTag: string = '[PeerConnectionHelper]';
  24. // Angular services
  25. private $log: ng.ILogService;
  26. private $q: ng.IQService;
  27. private $timeout: ng.ITimeoutService;
  28. private $rootScope: ng.IRootScopeService;
  29. // WebRTC
  30. private pc: RTCPeerConnection;
  31. private webrtcTask: saltyrtc.tasks.webrtc.WebRTCTask;
  32. // Calculated connection state
  33. public connectionState: TaskConnectionState = TaskConnectionState.New;
  34. public onConnectionStateChange: (state: TaskConnectionState) => void = null;
  35. // Debugging
  36. private censorCandidates: boolean;
  37. constructor($log: ng.ILogService, $q: ng.IQService,
  38. $timeout: ng.ITimeoutService, $rootScope: ng.IRootScopeService,
  39. webrtcTask: saltyrtc.tasks.webrtc.WebRTCTask,
  40. iceServers: RTCIceServer[],
  41. censorCandidates: boolean = true) {
  42. this.$log = $log;
  43. this.$log.info(this.logTag, 'Initialize WebRTC PeerConnection');
  44. this.$log.debug(this.logTag, 'ICE servers used:', [].concat(...iceServers.map((c) => c.urls)).join(', '));
  45. this.$q = $q;
  46. this.$timeout = $timeout;
  47. this.$rootScope = $rootScope;
  48. this.webrtcTask = webrtcTask;
  49. this.censorCandidates = censorCandidates;
  50. // Set up peer connection
  51. this.pc = new RTCPeerConnection({iceServers: iceServers});
  52. this.pc.onnegotiationneeded = (e: Event) => {
  53. this.$log.debug(this.logTag, 'RTCPeerConnection: negotiation needed');
  54. this.initiatorFlow().then(
  55. (_) => this.$log.debug(this.logTag, 'Initiator flow done'),
  56. );
  57. };
  58. // Handle state changes
  59. this.pc.onconnectionstatechange = (e: Event) => {
  60. $log.debug(this.logTag, 'Connection state change:', this.pc.connectionState);
  61. };
  62. this.pc.onsignalingstatechange = (e: Event) => {
  63. $log.debug(this.logTag, 'Signaling state change:', this.pc.signalingState);
  64. };
  65. // Set up ICE candidate handling
  66. this.setupIceCandidateHandling();
  67. // Log incoming data channels
  68. this.pc.ondatachannel = (e: RTCDataChannelEvent) => {
  69. $log.debug(this.logTag, 'New data channel was created:', e.channel.label);
  70. };
  71. }
  72. /**
  73. * Return the wrapped RTCPeerConnection instance.
  74. */
  75. public get peerConnection(): RTCPeerConnection {
  76. return this.pc;
  77. }
  78. /**
  79. * Set up receiving / sending of ICE candidates.
  80. */
  81. private setupIceCandidateHandling() {
  82. this.$log.debug(this.logTag, 'Setting up ICE candidate handling');
  83. this.pc.onicecandidate = (e: RTCPeerConnectionIceEvent) => {
  84. if (e.candidate) {
  85. this.$log.debug(this.logTag, 'Gathered local ICE candidate:',
  86. this.censorCandidate(e.candidate.candidate));
  87. this.webrtcTask.sendCandidate({
  88. candidate: e.candidate.candidate,
  89. sdpMid: e.candidate.sdpMid,
  90. sdpMLineIndex: e.candidate.sdpMLineIndex,
  91. });
  92. } else {
  93. this.$log.debug(this.logTag, 'No more local ICE candidates');
  94. }
  95. };
  96. this.pc.onicecandidateerror = (e: RTCPeerConnectionIceErrorEvent) => {
  97. this.$log.error(this.logTag, 'ICE candidate error:', e);
  98. };
  99. this.pc.oniceconnectionstatechange = (e: Event) => {
  100. this.$log.debug(this.logTag, 'ICE connection state change:', this.pc.iceConnectionState);
  101. this.$rootScope.$apply(() => {
  102. switch (this.pc.iceConnectionState) {
  103. case 'new':
  104. this.setConnectionState(TaskConnectionState.New);
  105. break;
  106. case 'checking':
  107. case 'disconnected':
  108. this.setConnectionState(TaskConnectionState.Connecting);
  109. break;
  110. case 'connected':
  111. case 'completed':
  112. this.setConnectionState(TaskConnectionState.Connected);
  113. break;
  114. case 'failed':
  115. case 'closed':
  116. this.setConnectionState(TaskConnectionState.Disconnected);
  117. break;
  118. default:
  119. this.$log.warn(this.logTag, 'Ignored ICE connection state change to',
  120. this.pc.iceConnectionState);
  121. }
  122. });
  123. };
  124. this.pc.onicegatheringstatechange = (e: Event) => {
  125. this.$log.debug(this.logTag, 'ICE gathering state change:', this.pc.iceGatheringState);
  126. };
  127. this.webrtcTask.on('candidates', (e: saltyrtc.tasks.webrtc.CandidatesEvent) => {
  128. for (const candidateInit of e.data) {
  129. if (candidateInit) {
  130. this.$log.debug(this.logTag, 'Adding remote ICE candidate:',
  131. this.censorCandidate(candidateInit.candidate));
  132. } else {
  133. this.$log.debug(this.logTag, 'No more remote ICE candidates');
  134. }
  135. this.pc.addIceCandidate(candidateInit);
  136. }
  137. });
  138. }
  139. private async initiatorFlow(): Promise<void> {
  140. // Send offer
  141. const offer: RTCSessionDescriptionInit = await this.pc.createOffer();
  142. await this.pc.setLocalDescription(offer);
  143. this.$log.debug(this.logTag, 'Created offer, set local description');
  144. this.webrtcTask.sendOffer(offer);
  145. // Receive answer
  146. const receiveAnswer: () => Promise<saltyrtc.tasks.webrtc.Answer> = () => {
  147. return new Promise((resolve) => {
  148. this.webrtcTask.once('answer', (e: saltyrtc.tasks.webrtc.AnswerEvent) => {
  149. resolve(e.data);
  150. });
  151. });
  152. };
  153. const answer: RTCSessionDescriptionInit = await receiveAnswer();
  154. await this.pc.setRemoteDescription(answer);
  155. this.$log.debug(this.logTag, 'Received answer, set remote description');
  156. }
  157. /**
  158. * Create a new secure data channel.
  159. */
  160. public createSecureDataChannel(label: string): saltyrtc.tasks.webrtc.SecureDataChannel {
  161. const dc: RTCDataChannel = this.pc.createDataChannel(label);
  162. dc.binaryType = 'arraybuffer';
  163. return this.webrtcTask.wrapDataChannel(dc);
  164. }
  165. /**
  166. * Set the connection state and update listeners.
  167. */
  168. private setConnectionState(state: TaskConnectionState) {
  169. if (state !== this.connectionState) {
  170. this.connectionState = state;
  171. if (this.onConnectionStateChange !== null) {
  172. this.$timeout(() => this.onConnectionStateChange(state), 0);
  173. }
  174. }
  175. }
  176. /**
  177. * Unbind all event handler and abruptly close the peer connection.
  178. */
  179. public close(): void {
  180. this.webrtcTask.off();
  181. this.pc.onnegotiationneeded = null;
  182. this.pc.onconnectionstatechange = null;
  183. this.pc.onsignalingstatechange = null;
  184. this.pc.onicecandidate = null;
  185. this.pc.onicecandidateerror = null;
  186. this.pc.oniceconnectionstatechange = null;
  187. this.pc.onicegatheringstatechange = null;
  188. this.pc.ondatachannel = null;
  189. this.pc.close();
  190. }
  191. /**
  192. * Censor an ICE candidate's address and port (unless censoring is disabled).
  193. *
  194. * Return the censored ICE candidate.
  195. */
  196. private censorCandidate(candidateInit: string): string {
  197. const candidate = SDPUtils.parseCandidate(candidateInit);
  198. if (this.censorCandidates) {
  199. if (candidate.type !== 'relay') {
  200. candidate.ip = '***';
  201. candidate.port = 1;
  202. }
  203. candidate.relatedAddress = '***';
  204. candidate.relatedPort = 2;
  205. }
  206. return SDPUtils.writeCandidate(candidate);
  207. }
  208. }