troubleshoot.js 10 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308
  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.resultWs = {
  60. state: 'unknown',
  61. showLogs: false,
  62. logs: [],
  63. };
  64. this.resultPc = {
  65. state: 'unknown',
  66. showLogs: false,
  67. };
  68. this.resultDc = {
  69. state: 'unknown',
  70. showLogs: false,
  71. };
  72. this.resultTurn = {
  73. state: 'unknown',
  74. showLogs: false,
  75. logs: [],
  76. };
  77. // Start checks
  78. this.start = (os) => {
  79. this.os = os;
  80. this.state = 'check';
  81. this.doChecks();
  82. };
  83. // Helper: Local storage
  84. function localStorageAvailable() {
  85. var test = 'test';
  86. try {
  87. localStorage.setItem(test, test);
  88. localStorage.removeItem(test);
  89. return true;
  90. } catch(e) {
  91. return false;
  92. }
  93. }
  94. // Helper: Desktop notifications
  95. function desktopNotificationsAvailable() {
  96. return 'Notification' in window;
  97. }
  98. // Helper: Peer connection
  99. function peerConnectionAvailable() {
  100. return window.RTCPeerConnection;
  101. }
  102. // Helper: Data channel
  103. function dataChannelAvailable() {
  104. return window.RTCPeerConnection && (new RTCPeerConnection()).createDataChannel;
  105. }
  106. // Run all the checks and update results
  107. this.doChecks = () => {
  108. // Check for JS
  109. this.resultJs.state = 'yes';
  110. // Check for LocalStorage
  111. if (localStorageAvailable()) {
  112. this.resultLs.state = 'yes';
  113. } else {
  114. this.resultLs.state = 'no';
  115. }
  116. // Check for desktop notifications
  117. if (desktopNotificationsAvailable()) {
  118. this.resultDn.state = 'yes';
  119. } else {
  120. this.resultDn.state = 'no';
  121. }
  122. // Check for RTCPeerConnection
  123. if (peerConnectionAvailable()) {
  124. this.resultPc.state = 'yes';
  125. } else {
  126. this.resultPc.state = 'no';
  127. }
  128. // Check for RTCDataChannel
  129. if (dataChannelAvailable()) {
  130. this.resultDc.state = 'yes';
  131. this.resultTurn.state = 'loading';
  132. } else {
  133. this.resultDc.state = 'no';
  134. this.resultTurn.state = 'no';
  135. }
  136. // Check for WebSocket connectivity
  137. const subprotocol = 'v1.saltyrtc.org';
  138. const path = 'ffffffffffffffff00000000000000000000000000000000ffffffffffffffff';
  139. this.resultWs.showLogs = true;
  140. const ws = new WebSocket('wss://saltyrtc-ff.threema.ch/' + path, subprotocol);
  141. ws.binaryType = 'arraybuffer';
  142. ws.addEventListener('open', (event) => {
  143. $scope.$apply(() => {
  144. this.resultWs.logs.push('Connected');
  145. });
  146. });
  147. ws.addEventListener('message', (event) => {
  148. console.log('Message from server ', event.data);
  149. const success = () => {
  150. $scope.$apply(() => {
  151. this.resultWs.state = 'yes';
  152. this.resultWs.logs.push('Received server-hello message');
  153. });
  154. ws.close(1000);
  155. };
  156. const fail = (msg) => {
  157. $scope.$apply(() => {
  158. this.resultWs.state = 'no';
  159. console.error(msg);
  160. this.resultWs.logs.push(`Invalid server-hello message (${msg})`);
  161. });
  162. ws.close(1000);
  163. };
  164. // This should be the SaltyRTC server-hello message.
  165. const bytes = new Uint8Array(event.data);
  166. console.log('Message bytes:', bytes);
  167. // Validate length
  168. let valid;
  169. if (bytes.length < 81) {
  170. valid = false;
  171. return fail(`Invalid length: ${bytes.length}`);
  172. }
  173. // Split up message
  174. const nonce = bytes.slice(0, 24);
  175. const data = bytes.slice(24);
  176. // Validate nonce
  177. if (nonce[16] !== 0) {
  178. return fail('Invalid nonce (source != 0)');
  179. }
  180. if (nonce[17] !== 0) {
  181. return fail('Invalid nonce (destination != 0)');
  182. }
  183. if (nonce[18] !== 0 || nonce[19] !== 0) {
  184. return fail('Invalid nonce (overflow != 0)');
  185. }
  186. // Data should start with 0x82 (fixmap with 2 entries) followed by a string
  187. // with either the value "type" or "key".
  188. if (data[0] !== 0x82) {
  189. return fail('Invalid data (does not start with 0x82)');
  190. }
  191. if (data[1] === 0xa3 && data[2] === 'k'.charCodeAt(0) && data[3] === 'e'.charCodeAt(0) && data[4] === 'y'.charCodeAt(0)) {
  192. return success();
  193. }
  194. if (data[1] === 0xa4 && data[2] === 't'.charCodeAt(0) && data[3] === 'y'.charCodeAt(0) && data[4] === 'p'.charCodeAt(0) && data[5] === 'e'.charCodeAt(0)) {
  195. return success();
  196. }
  197. return fail('Invalid data (bad map key)');
  198. });
  199. ws.addEventListener('error', (event) => {
  200. console.error('WS error:', event);
  201. $scope.$apply(() => {
  202. this.resultWs.state = 'no';
  203. this.resultWs.logs.push('Error');
  204. });
  205. });
  206. ws.addEventListener('close', (event) => {
  207. $scope.$apply(() => {
  208. this.resultWs.logs.push('Connection closed');
  209. });
  210. });
  211. this.resultWs.logs.push('Connecting');
  212. // Check for TURN connectivity
  213. let timeout = null;
  214. const testTurn = () => {
  215. timeout = $timeout(() => this.turnSuccess = 'no', 10000);
  216. const noop = () => {};
  217. // Detect safari
  218. const uagent = window.navigator.userAgent.toLowerCase();
  219. const isSafari = /safari/.test(uagent) && /applewebkit/.test(uagent) && !/chrome/.test(uagent);
  220. // Determine ICE servers
  221. let iceServers;
  222. if (isSafari) {
  223. iceServers = [
  224. 'turn:turn.threema.ch:443?transport=udp',
  225. 'turn:turn.threema.ch:443?transport=tcp',
  226. 'turns:turn.threema.ch:443',
  227. ];
  228. } else {
  229. iceServers = [
  230. 'turn:ds-turn.threema.ch:443?transport=udp',
  231. 'turn:ds-turn.threema.ch:443?transport=tcp',
  232. 'turns:ds-turn.threema.ch:443',
  233. ];
  234. }
  235. console.debug('Using ICE servers: ' + iceServers);
  236. const pc = new RTCPeerConnection({iceServers: [{
  237. urls: iceServers,
  238. username: 'threema-angular-test',
  239. credential: 'VaoVnhxKGt2wD20F9bTOgiew6yHQmj4P7y7SE4lrahAjTQC0dpnG32FR4fnrlpKa',
  240. }]});
  241. this.resultTurn.showLogs = true;
  242. pc.createDataChannel('test');
  243. this.resultTurn.logs.push('Creating offer...');
  244. pc.createOffer(function(sdp) { pc.setLocalDescription(sdp, noop, noop) }, noop);
  245. pc.onicecandidate = (ice) => {
  246. $scope.$apply(() => {
  247. if (ice.candidate === null) {
  248. this.resultTurn.logs.push('Done collecting candidates.');
  249. if (this.resultTurn.state === 'loading') {
  250. this.resultTurn.state = 'no';
  251. $timeout.cancel(timeout);
  252. }
  253. } else if (ice.candidate.candidate) {
  254. const candidate = SDPUtils.parseCandidate(ice.candidate.candidate);
  255. console.debug(candidate);
  256. let info = `[${candidate.type}] ${candidate.ip}:${candidate.port}`;
  257. if (candidate.relatedAddress) {
  258. info += ` via ${candidate.relatedAddress}`;
  259. }
  260. info += ` (${candidate.protocol})`;
  261. this.resultTurn.logs.push(info);
  262. if (candidate.type === 'relay') {
  263. this.resultTurn.state = 'yes';
  264. $timeout.cancel(timeout);
  265. }
  266. } else {
  267. console.warn('Invalid candidate:', ice.candidate.candidate);
  268. this.resultTurn.logs.push('Invalid candidate (see debug log)');
  269. }
  270. });
  271. }
  272. }
  273. testTurn();
  274. };
  275. });