troubleshoot.js 6.8 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219
  1. var app = angular.module('troubleshoot', ['ngSanitize']);
  2. app.filter('osName', function() {
  3. return function(id) {
  4. switch (id) {
  5. case 'android':
  6. return 'Android';
  7. case 'ios':
  8. return 'iOS';
  9. default:
  10. return '?';
  11. }
  12. }
  13. });
  14. app.component('check', {
  15. bindings: {
  16. result: '<',
  17. textNo: '@',
  18. },
  19. template: `
  20. <div class="status status-no" ng-if="$ctrl.result.state === 'no'">
  21. <i class="material-icons md-36" aria-label="No">error</i> <span class="text">No</span>
  22. <p class="small" ng-if="$ctrl.textNo" ng-bind-html="$ctrl.textNo"></p>
  23. </div>
  24. <div class="status status-yes" ng-if="$ctrl.result.state === 'yes'">
  25. <i class="material-icons md-36" aria-label="Yes">check_circle</i> <span class="text">Yes</span>
  26. </div>
  27. <div class="status status-unknown" ng-if="$ctrl.result.state === 'unknown'">
  28. <i class="material-icons md-36" aria-label="Unknown">help</i> <span class="text">Unknown</span>
  29. </div>
  30. <div class="status status-test" ng-if="$ctrl.result.state === 'loading'">
  31. <img src="loading.gif" alt="Loading..." aria-label="Loading">
  32. </div>
  33. <div class="logs" ng-if="$ctrl.result.showLogs">
  34. <p>Results:</p>
  35. <div class="log-data">
  36. <p ng-repeat="log in $ctrl.result.logs">{{ log }}</p>
  37. </div>
  38. </div>
  39. `,
  40. });
  41. app.controller('ChecksController', function($scope, $timeout) {
  42. // Initialize state
  43. this.state = 'init'; // Either 'init' or 'check'
  44. this.os = null; // Either 'android' or 'ios'
  45. // Initialize results
  46. // Valid states: yes, no, unknown, loading
  47. this.resultJs = {
  48. state: 'unknown',
  49. showLogs: false,
  50. };
  51. this.resultLs = {
  52. state: 'unknown',
  53. showLogs: false,
  54. };
  55. this.resultDn = {
  56. state: 'unknown',
  57. showLogs: false,
  58. };
  59. this.resultPc = {
  60. state: 'unknown',
  61. showLogs: false,
  62. };
  63. this.resultDc = {
  64. state: 'unknown',
  65. showLogs: false,
  66. };
  67. this.resultTurn = {
  68. state: 'unknown',
  69. showLogs: false,
  70. logs: [],
  71. };
  72. // Start checks
  73. this.start = (os) => {
  74. this.os = os;
  75. this.state = 'check';
  76. this.doChecks();
  77. };
  78. // Helper: Local storage
  79. function localStorageAvailable() {
  80. var test = 'test';
  81. try {
  82. localStorage.setItem(test, test);
  83. localStorage.removeItem(test);
  84. return true;
  85. } catch(e) {
  86. return false;
  87. }
  88. }
  89. // Helper: Desktop notifications
  90. function desktopNotificationsAvailable() {
  91. return 'Notification' in window;
  92. }
  93. // Helper: Peer connection
  94. function peerConnectionAvailable() {
  95. return window.RTCPeerConnection;
  96. }
  97. // Helper: Data channel
  98. function dataChannelAvailable() {
  99. return window.RTCPeerConnection && (new RTCPeerConnection()).createDataChannel;
  100. }
  101. // Run all the checks and update results
  102. this.doChecks = () => {
  103. // Check for JS
  104. this.resultJs.state = 'yes';
  105. // Check for LocalStorage
  106. if (localStorageAvailable()) {
  107. this.resultLs.state = 'yes';
  108. } else {
  109. this.resultLs.state = 'no';
  110. }
  111. // Check for desktop notifications
  112. if (desktopNotificationsAvailable()) {
  113. this.resultDn.state = 'yes';
  114. } else {
  115. this.resultDn.state = 'no';
  116. }
  117. // Check for RTCPeerConnection
  118. if (peerConnectionAvailable()) {
  119. this.resultPc.state = 'yes';
  120. } else {
  121. this.resultPc.state = 'no';
  122. }
  123. // Check for RTCDataChannel
  124. if (dataChannelAvailable()) {
  125. this.resultDc.state = 'yes';
  126. this.resultTurn.state = 'loading';
  127. } else {
  128. this.resultDc.state = 'no';
  129. this.resultTurn.state = 'no';
  130. }
  131. // Check for TURN connectivity
  132. let timeout = null;
  133. const testTurn = () => {
  134. timeout = $timeout(() => this.turnSuccess = 'no', 10000);
  135. const noop = () => {};
  136. // Detect safari
  137. const uagent = window.navigator.userAgent.toLowerCase();
  138. const isSafari = /safari/.test(uagent) && /applewebkit/.test(uagent) && !/chrome/.test(uagent);
  139. // Determine ICE servers
  140. let iceServers;
  141. if (isSafari) {
  142. iceServers = [
  143. 'turn:turn.threema.ch:443?transport=udp',
  144. 'turn:turn.threema.ch:443?transport=tcp',
  145. 'turns:turn.threema.ch:443',
  146. ];
  147. } else {
  148. iceServers = [
  149. 'turn:ds-turn.threema.ch:443?transport=udp',
  150. 'turn:ds-turn.threema.ch:443?transport=tcp',
  151. 'turns:ds-turn.threema.ch:443',
  152. ];
  153. }
  154. console.debug('Using ICE servers: ' + iceServers);
  155. const pc = new RTCPeerConnection({iceServers: [{
  156. urls: iceServers,
  157. username: 'threema-angular-test',
  158. credential: 'VaoVnhxKGt2wD20F9bTOgiew6yHQmj4P7y7SE4lrahAjTQC0dpnG32FR4fnrlpKa',
  159. }]});
  160. this.resultTurn.showLogs = true;
  161. pc.createDataChannel('test');
  162. this.resultTurn.logs.push('Creating offer...');
  163. pc.createOffer(function(sdp) { pc.setLocalDescription(sdp, noop, noop) }, noop);
  164. pc.onicecandidate = (ice) => {
  165. $scope.$apply(() => {
  166. if (ice.candidate === null) {
  167. this.resultTurn.logs.push('Done collecting candidates.');
  168. if (this.resultTurn.state === 'loading') {
  169. this.resultTurn.state = 'no';
  170. $timeout.cancel(timeout);
  171. }
  172. } else if (ice.candidate.candidate) {
  173. const candidate = SDPUtils.parseCandidate(ice.candidate.candidate);
  174. console.debug(candidate);
  175. let info = `[${candidate.type}] ${candidate.ip}:${candidate.port}`;
  176. if (candidate.relatedAddress) {
  177. info += ` via ${candidate.relatedAddress}`;
  178. }
  179. info += ` (${candidate.protocol})`;
  180. this.resultTurn.logs.push(info);
  181. if (candidate.type === 'relay') {
  182. this.resultTurn.state = 'yes';
  183. $timeout.cancel(timeout);
  184. }
  185. } else {
  186. console.warn('Invalid candidate:', ice.candidate.candidate);
  187. this.resultTurn.logs.push('Invalid candidate (see debug log)');
  188. }
  189. });
  190. }
  191. }
  192. testTurn();
  193. };
  194. });