Jelajahi Sumber

Merge pull request #648 from threema-ch/troubleshoot-ios

Rewrite and improve troubleshooting
Danilo Bargen 6 tahun lalu
induk
melakukan
4d85853c4b
2 mengubah file dengan 353 tambahan dan 213 penghapusan
  1. 67 89
      troubleshoot/index.html
  2. 286 124
      troubleshoot/troubleshoot.js

+ 67 - 89
troubleshoot/index.html

@@ -1,7 +1,7 @@
 <!DOCTYPE html>
 <!--
 
-    Copyright © 2016-2018 Threema GmbH (https://threema.ch/).
+    Copyright © 2017-2018 Threema GmbH (https://threema.ch/).
 
     This file is part of Threema Web.
 
@@ -72,8 +72,8 @@
             margin: 0px auto 16px;
         }
 
-        h1 { margin-top: 0; font-weight: 500; }
-        h2 { font-weight: 300; }
+        h1 { margin-top: 0; font-size: 30px; font-weight: 500; }
+        h2 { font-weight: 300; font-size: 22px; }
         p { font-weight: 300; }
 
         .status span {
@@ -90,6 +90,17 @@
 
         .small { font-size: 0.8em; font-weight: 300; }
 
+        .log-data {
+            background-color: #eee;
+            border: 1px solid #ccc;
+            padding: 8px;
+        }
+
+        .log-data p {
+            margin: 4px 0;
+            font-family: monospace;
+        }
+
         footer {
             color: white;
             font-weight: 300;
@@ -99,11 +110,13 @@
     </style>
 
     <!-- JS -->
+    <script src="../node_modules/angular/angular.js?v=[[VERSION]]"></script>
+    <script src="../node_modules/angular-sanitize/angular-sanitize.min.js?v=[[VERSION]]"></script>
     <script src="../node_modules/webrtc-adapter/out/adapter.js?v=[[VERSION]]"></script>
     <script src="../node_modules/sdp/sdp.js?v=[[VERSION]]"></script>
     <script src="troubleshoot.js?v=[[VERSION]]"></script>
 </head>
-<body>
+<body ng-app="troubleshoot">
     <img src="../img/bg.jpg?v=[[VERSION]]" id="background-image" draggable="false" alt="">
 
     <header>
@@ -114,109 +127,74 @@
         </div>
     </header>
 
-    <div id="wrapper">
+    <div id="wrapper" ng-controller="ChecksController as $ctrl">
+
+        <h1 ng-if="$ctrl.state === 'init'">Threema Web Diagnostics</h1>
+        <h1 ng-if="$ctrl.state === 'check'">Threema Web Diagnostics ({{ $ctrl.os | osName }})</h1>
 
-        <h1>Threema Web Diagnostics</h1>
+        <p ng-if="$ctrl.state === 'init'">This test will check your browser for
+        compatibility or configuration problems.</p>
 
-        <p id="help-text">This test will check your browser for compatibility problems. It
-        will also check whether WebRTC connection buildup using STUN/TURN works.</p>
+        <p ng-if="$ctrl.state === 'init'">What type of mobile device do you use?</p>
 
-        <button id="start">Start Test</button>
+        <div ng-if="$ctrl.state === 'init'">
+            <button role="button" aria-label="Android" ng-click="$ctrl.start('android')">Android</button>
+            <button role="button" aria-label="iOS" ng-click="$ctrl.start('ios')">iOS</button>
+        </div>
+
+        <div id="checks" ng-if="$ctrl.state === 'check'">
+
+            <div id="check-js">
+                <h2>Is JavaScript enabled?</h2>
+                <check
+                    result="$ctrl.resultJs">
+            </div>
 
-        <div id="checks" class="hidden">
-            <h2>Is JavaScript enabled?</h2>
-            <div id="status-js">
-                <div class="status status-no">
-                    <i class="material-icons md-36">error</i> <span class="text">No</span>
-                </div>
-                <div class="status status-yes hidden">
-                    <i class="material-icons md-36">check_circle</i> <span class="text">Yes</span>
-                </div>
+            <div id="check-ls">
+                <h2>Is LocalStorage available?</h2>
+                <check
+                    result="$ctrl.resultLs"
+                    text-no="Without LocalStorage, persistent sessions and settings<br>cannot be stored in the browser.<br>See the <a href='https://threema.ch/faq/web_browser_settings'>FAQ</a> for information on how to fix this.">
             </div>
 
-            <h2>Is WebRTC available?</h2>
-            <div id="status-pc">
-                <div class="status status-unknown">
-                    <i class="material-icons md-36">help</i> <span class="text">Unknown</span>
-                </div>
-                <div class="status status-no hidden">
-                    <i class="material-icons md-36">error</i> <span class="text">No</span>
-                    <p class="small">RTCPeerConnection is a part of WebRTC.<br>Threema Web cannot work without it.</p>
-                </div>
-                <div class="status status-yes hidden">
-                    <i class="material-icons md-36">check_circle</i> <span class="text">Yes</span>
-                </div>
+            <div id="check-dn">
+                <h2>Are desktop notifications available?</h2>
+                <check
+                    result="$ctrl.resultDn"
+                    text-no="Without desktop notifications, we cannot notify you when a new message arrives.">
             </div>
 
-            <h2>Are WebRTC DataChannels available?</h2>
-            <div id="status-dc">
-                <div class="status status-unknown">
-                    <i class="material-icons md-36">help</i> <span class="text">Unknown</span>
-                </div>
-                <div class="status status-no hidden">
-                    <i class="material-icons md-36">error</i> <span class="text">No</span>
-                    <p class="small">RTCDataChannel is a part of WebRTC.<br>Threema Web cannot work without it.</p>
-                </div>
-                <div class="status status-yes hidden">
-                    <i class="material-icons md-36">check_circle</i> <span class="text">Yes</span>
-                </div>
+            <div id="check-ws">
+                <h2>Are WebSocket connections possible?</h2>
+                <check
+                    result="$ctrl.resultWs"
+                    text-no="Threema Web must be able to open a working WebSocket connection to the SaltyRTC signaling server.">
             </div>
 
-            <h2>Is LocalStorage available?</h2>
-            <div id="status-ls">
-                <div class="status status-unknown">
-                    <i class="material-icons md-36">help</i> <span class="text">Unknown</span>
-                </div>
-                <div class="status status-no hidden">
-                    <i class="material-icons md-36">error</i> <span class="text">No</span>
-                    <p class="small">Without LocalStorage, persistent sessions and settings<br>cannot be stored in the browser.<br>
-                    See the <a href="https://threema.ch/faq/web_browser_settings">FAQ</a> for information on how to fix this.</p>
-                </div>
-                <div class="status status-yes hidden">
-                    <i class="material-icons md-36">check_circle</i> <span class="text">Yes</span>
-                </div>
+            <div id="check-pc" ng-if="$ctrl.os === 'android'">
+                <h2>Is WebRTC available?</h2>
+                <check
+                    result="$ctrl.resultPc"
+                    text-no="RTCPeerConnection is a part of WebRTC.<br>Threema Web cannot work without it.">
             </div>
 
-            <h2>Are desktop notifications available?</h2>
-            <div id="status-dn">
-                <div class="status status-unknown">
-                    <i class="material-icons md-36">help</i> <span class="text">Unknown</span>
-                </div>
-                <div class="status status-no hidden">
-                    <i class="material-icons md-36">error</i> <span class="text">No</span>
-                    <p class="small">Without desktop notifications, we cannot notify you when a new message arrives.</p>
-                </div>
-                <div class="status status-yes hidden">
-                    <i class="material-icons md-36">check_circle</i> <span class="text">Yes</span>
-                </div>
+            <div id="check-dc" ng-if="$ctrl.os === 'android'">
+                <h2>Are WebRTC DataChannels available?</h2>
+                <check
+                    result="$ctrl.resultDc"
+                    text-no="RTCDataChannel is a part of WebRTC.<br>Threema Web cannot work without it.">
             </div>
 
-            <h2>Does TURN work?</h2>
-            <div id="status-turn">
-                <div class="status status-unknown">
-                    <i class="material-icons md-36">help</i> <span class="text">Unknown</span>
-                </div>
-                <div class="status status-no hidden">
-                    <i class="material-icons md-36">error</i> <span class="text">No</span>
-                    <p class="small hidden">It looks like TURN traffic is being blocked by your firewall.<br>
-                    Without TURN, connections can only be established if your computer<br>
-                    and your phone are in the same network.</p>
-                </div>
-                <div class="status status-yes hidden">
-                    <i class="material-icons md-36">check_circle</i> <span class="text">Yes</span>
-                </div>
-                <div class="status status-test hidden">
-                    <img src="loading.gif" alt="Loading...">
-                </div>
-                <div class="results hidden">
-                    <p>Results:</p>
-                    <p class="result-data"></p>
-                </div>
+            <div id="check-turn" ng-if="$ctrl.os === 'android'">
+                <h2>Does TURN work?</h2>
+                <check
+                    result="$ctrl.resultTurn"
+                    text-no="It looks like TURN traffic is being blocked by your firewall.<br>Without TURN, connections can only be established if your computer<br>and your phone are in the same network.">
             </div>
         </div>
     </div>
     <footer>
-        &copy; 2017 Threema GmbH
+        &copy; 2017&ndash;2018 Threema GmbH
     </footer>
 </body>
 </html>

+ 286 - 124
troubleshoot/troubleshoot.js

@@ -1,51 +1,93 @@
-function switchTo(type, newStatus) {
-    var unknown = document.querySelector('#status-' + type + ' .status-unknown');
-    if (unknown) {
-        unknown.classList.add('hidden');
-    }
-    var test = document.querySelector('#status-' + type + ' .status-test')
-    if (test) {
-        test.classList.add('hidden');
-    }
-    document.querySelector('#status-' + type + ' .status-no').classList.add('hidden');
-    document.querySelector('#status-' + type + ' .status-yes').classList.add('hidden');
-    document.querySelector('#status-' + type + ' .status-' + newStatus).classList.remove('hidden');
-}
-
-function setupChecks() {
-    var start = document.querySelector('#start');
-    var helpText = document.querySelector('#help-text');
-    var checks = document.querySelector('#checks');
-    start.addEventListener('click', function(e) {
-        start.classList.add('hidden');
-        helpText.classList.add('hidden');
-        checks.classList.remove('hidden');
-        doChecks();
-    });
-}
-
-function doChecks() {
-    // Check for JS
-    switchTo('js', 'yes');
-
-    // Check for RTCPeerConnection
-    if (window.RTCPeerConnection) {
-        switchTo('pc', 'yes');
-    } else {
-        switchTo('pc', 'no');
-    }
+var app = angular.module('troubleshoot', ['ngSanitize']);
 
-    // Check for RTCDataChannel
-    if (window.RTCPeerConnection && (new RTCPeerConnection()).createDataChannel) {
-        switchTo('dc', 'yes');
-        switchTo('turn', 'test');
-    } else {
-        switchTo('dc', 'no');
-        switchTo('turn', 'no');
+app.filter('osName', function() {
+    return function(id) {
+        switch (id) {
+            case 'android':
+                return 'Android';
+            case 'ios':
+                return 'iOS';
+            default:
+                return '?';
+        }
     }
+});
+
+app.component('check', {
+    bindings: {
+        result: '<',
+        textNo: '@',
+    },
+    template: `
+        <div class="status status-no" ng-if="$ctrl.result.state === 'no'">
+            <i class="material-icons md-36" aria-label="No">error</i> <span class="text">No</span>
+            <p class="small" ng-if="$ctrl.textNo" ng-bind-html="$ctrl.textNo"></p>
+        </div>
+        <div class="status status-yes" ng-if="$ctrl.result.state === 'yes'">
+            <i class="material-icons md-36" aria-label="Yes">check_circle</i> <span class="text">Yes</span>
+        </div>
+        <div class="status status-unknown" ng-if="$ctrl.result.state === 'unknown'">
+            <i class="material-icons md-36" aria-label="Unknown">help</i> <span class="text">Unknown</span>
+        </div>
+        <div class="status status-test" ng-if="$ctrl.result.state === 'loading'">
+            <img src="loading.gif" alt="Loading..." aria-label="Loading">
+        </div>
+        <div class="logs" ng-if="$ctrl.result.showLogs">
+            <p>Results:</p>
+            <div class="log-data">
+                <p ng-repeat="log in $ctrl.result.logs">{{ log }}</p>
+            </div>
+        </div>
+    `,
+});
+
+app.controller('ChecksController', function($scope, $timeout) {
+    // Initialize state
+    this.state = 'init';  // Either 'init' or 'check'
+    this.os = null;  // Either 'android' or 'ios'
+
+    // Initialize results
+    // Valid states: yes, no, unknown, loading
+    this.resultJs = {
+        state: 'unknown',
+        showLogs: false,
+    };
+    this.resultLs = {
+        state: 'unknown',
+        showLogs: false,
+    };
+    this.resultDn = {
+        state: 'unknown',
+        showLogs: false,
+    };
+    this.resultWs = {
+        state: 'unknown',
+        showLogs: false,
+        logs: [],
+    };
+    this.resultPc = {
+        state: 'unknown',
+        showLogs: false,
+    };
+    this.resultDc = {
+        state: 'unknown',
+        showLogs: false,
+    };
+    this.resultTurn = {
+        state: 'unknown',
+        showLogs: false,
+        logs: [],
+    };
 
-    // Check for LocalStorage
-    function localStorageAvailable(){
+    // Start checks
+    this.start = (os) => {
+        this.os = os;
+        this.state = 'check';
+        this.doChecks();
+    };
+
+    // Helper: Local storage
+    function localStorageAvailable() {
         var test = 'test';
         try {
             localStorage.setItem(test, test);
@@ -55,92 +97,212 @@ function doChecks() {
             return false;
         }
     }
-    if (localStorageAvailable()) {
-        switchTo('ls', 'yes');
-    } else {
-        switchTo('ls', 'no');
-    }
 
-    // Check for desktop notifications
-    if ('Notification' in window) {
-        switchTo('dn', 'yes');
-    } else {
-        switchTo('dn', 'no');
+    // Helper: Desktop notifications
+    function desktopNotificationsAvailable() {
+        return 'Notification' in window;
     }
 
-    // Check for TURN connectivity
-    var timeout = null;
-    function turnSuccess() {
-        switchTo('turn', 'yes');
-        clearTimeout(timeout);
+    // Helper: Peer connection
+    function peerConnectionAvailable() {
+        return window.RTCPeerConnection;
     }
-    function turnFail() {
-        switchTo('turn', 'no');
-        document.querySelector('#status-turn .results').classList.add('hidden');
-        document.querySelector('#status-turn .status-no .small').classList.remove('hidden');
+
+    // Helper: Data channel
+    function dataChannelAvailable() {
+        return window.RTCPeerConnection && (new RTCPeerConnection()).createDataChannel;
     }
-    function testTurn() {
-        timeout = setTimeout(function() {
-            turnFail();
-        }, 10000);
-        var noop = function() {};
-
-        var uagent = window.navigator.userAgent.toLowerCase();
-        var isSafari  = /safari/.test(uagent) && /applewebkit/.test(uagent) && !/chrome/.test(uagent);
-
-        if (isSafari) {
-            var iceServers = [
-                'turn:turn.threema.ch:443?transport=udp',
-                'turn:turn.threema.ch:443?transport=tcp',
-                'turns:turn.threema.ch:443',
-            ];
+
+    // Run all the checks and update results
+    this.doChecks = () => {
+        // Check for JS
+        this.resultJs.state = 'yes';
+
+        // Check for LocalStorage
+        if (localStorageAvailable()) {
+            this.resultLs.state = 'yes';
         } else {
-            var iceServers = [
-                'turn:ds-turn.threema.ch:443?transport=udp',
-                'turn:ds-turn.threema.ch:443?transport=tcp',
-                'turns:ds-turn.threema.ch:443',
-            ];
+            this.resultLs.state = 'no';
         }
-        console.debug('Using ICE servers: ' + iceServers);
-
-        var pc = new RTCPeerConnection({iceServers: [{
-            urls: iceServers,
-            username: 'threema-angular-test',
-            credential: 'VaoVnhxKGt2wD20F9bTOgiew6yHQmj4P7y7SE4lrahAjTQC0dpnG32FR4fnrlpKa',
-        }]});
-        document.querySelector('#status-turn .results').classList.remove('hidden');
-        var resultData = document.querySelector('#status-turn .result-data');
-        pc.createDataChannel('test');
-        console.info('Creating offer...');
-        pc.createOffer(function(sdp) { pc.setLocalDescription(sdp, noop, noop) }, noop);
-        pc.onicecandidate = function(ice) {
-            if (ice.candidate === null) {
-                console.info('Done collecting candidates.');
-            } else if (ice.candidate.candidate) {
-                var candidate = SDPUtils.parseCandidate(ice.candidate.candidate);
-                console.debug(candidate);
-                if (candidate.type === 'relay') {
-                    var info = '[' + candidate.type + '] ' + candidate.ip + ':' + candidate.port + ' (' + candidate.protocol + ')';
-                    if (candidate.relatedAddress.indexOf(':') !== -1) {
-                        info += ' (ipv6)';
-                    } else if (candidate.relatedAddress.indexOf('.') !== -1) {
-                        info += ' (ipv4)';
-                    } else {
-                        info += ' (?)';
-                    }
-                    resultData.innerHTML += info + '<br>';
-                    turnSuccess();
-                }
+
+        // Check for desktop notifications
+        if (desktopNotificationsAvailable()) {
+            this.resultDn.state = 'yes';
+        } else {
+            this.resultDn.state = 'no';
+        }
+
+        // Check for RTCPeerConnection
+        if (peerConnectionAvailable()) {
+            this.resultPc.state = 'yes';
+        } else {
+            this.resultPc.state = 'no';
+        }
+
+        // Check for RTCDataChannel
+        if (dataChannelAvailable()) {
+            this.resultDc.state = 'yes';
+            this.resultTurn.state = 'loading';
+        } else {
+            this.resultDc.state = 'no';
+            this.resultTurn.state = 'no';
+        }
+
+        // Check for WebSocket connectivity
+        const subprotocol = 'v1.saltyrtc.org';
+        const path = 'ffffffffffffffff00000000000000000000000000000000ffffffffffffffff';
+        this.resultWs.showLogs = true;
+        const ws = new WebSocket('wss://saltyrtc-ff.threema.ch/' + path, subprotocol);
+        ws.binaryType = 'arraybuffer';
+        ws.addEventListener('open', (event) => {
+            $scope.$apply(() => {
+                this.resultWs.logs.push('Connected');
+            });
+        });
+        ws.addEventListener('message', (event) => {
+            console.log('Message from server ', event.data);
+
+            const success = () => {
+                $scope.$apply(() => {
+                    this.resultWs.state = 'yes';
+                    this.resultWs.logs.push('Received server-hello message');
+                });
+                ws.close(1000);
+            };
+            const fail = (msg) => {
+                $scope.$apply(() => {
+                    this.resultWs.state = 'no';
+                    console.error(msg);
+                    this.resultWs.logs.push(`Invalid server-hello message (${msg})`);
+                });
+                ws.close(1000);
+            };
+
+            // This should be the SaltyRTC server-hello message.
+            const bytes = new Uint8Array(event.data);
+            console.log('Message bytes:', bytes);
+
+            // Validate length
+            let valid;
+            if (bytes.length < 81) {
+                valid = false;
+                return fail(`Invalid length: ${bytes.length}`);
+            }
+
+            // Split up message
+            const nonce = bytes.slice(0, 24);
+            const data = bytes.slice(24);
+
+            // Validate nonce
+            if (nonce[16] !== 0) {
+                return fail('Invalid nonce (source != 0)');
+            }
+            if (nonce[17] !== 0) {
+                return fail('Invalid nonce (destination != 0)');
+            }
+            if (nonce[18] !== 0 || nonce[19] !== 0) {
+                return fail('Invalid nonce (overflow != 0)');
+            }
+
+            // Data should start with 0x82 (fixmap with 2 entries) followed by a string
+            // with either the value "type" or "key".
+            if (data[0] !== 0x82) {
+                return fail('Invalid data (does not start with 0x82)');
+            }
+            if (data[1] === 0xa3 && data[2] === 'k'.charCodeAt(0) && data[3] === 'e'.charCodeAt(0) && data[4] === 'y'.charCodeAt(0)) {
+                return success();
+            }
+            if (data[1] === 0xa4 && data[2] === 't'.charCodeAt(0) && data[3] === 'y'.charCodeAt(0) && data[4] === 'p'.charCodeAt(0) && data[5] === 'e'.charCodeAt(0)) {
+                return success();
+            }
+
+            return fail('Invalid data (bad map key)');
+        });
+        ws.addEventListener('error', (event) => {
+            console.error('WS error:', event);
+            $scope.$apply(() => {
+                this.resultWs.state = 'no';
+                this.resultWs.logs.push('Error');
+            });
+        });
+        ws.addEventListener('close', (event) => {
+            $scope.$apply(() => {
+                this.resultWs.logs.push('Connection closed');
+            });
+        });
+        this.resultWs.logs.push('Connecting');
+
+        // Check for TURN connectivity
+        let timeout = null;
+        const testTurn = () => {
+            timeout = $timeout(() => this.turnSuccess = 'no', 10000);
+            const noop = () => {};
+
+            // Detect safari
+            const uagent = window.navigator.userAgent.toLowerCase();
+            const isSafari  = /safari/.test(uagent) && /applewebkit/.test(uagent) && !/chrome/.test(uagent);
+
+            // Determine ICE servers
+            let iceServers;
+            if (isSafari) {
+                iceServers = [
+                    'turn:turn.threema.ch:443?transport=udp',
+                    'turn:turn.threema.ch:443?transport=tcp',
+                    'turns:turn.threema.ch:443',
+                ];
             } else {
-                console.warn('Invalid candidate:', ice.candidate.candidate);
+                iceServers = [
+                    'turn:ds-turn.threema.ch:443?transport=udp',
+                    'turn:ds-turn.threema.ch:443?transport=tcp',
+                    'turns:ds-turn.threema.ch:443',
+                ];
+            }
+            console.debug('Using ICE servers: ' + iceServers);
+
+            const pc = new RTCPeerConnection({iceServers: [{
+                urls: iceServers,
+                username: 'threema-angular-test',
+                credential: 'VaoVnhxKGt2wD20F9bTOgiew6yHQmj4P7y7SE4lrahAjTQC0dpnG32FR4fnrlpKa',
+            }]});
+
+            this.resultTurn.showLogs = true;
+
+            pc.createDataChannel('test');
+
+            this.resultTurn.logs.push('Creating offer...');
+            pc.createOffer(function(sdp) { pc.setLocalDescription(sdp, noop, noop) }, noop);
+
+            pc.onicecandidate = (ice) => {
+                $scope.$apply(() => {
+					if (ice.candidate === null) {
+						this.resultTurn.logs.push('Done collecting candidates.');
+                        if (this.resultTurn.state === 'loading') {
+                            this.resultTurn.state = 'no';
+                            $timeout.cancel(timeout);
+                        }
+					} else if (ice.candidate.candidate) {
+						const candidate = SDPUtils.parseCandidate(ice.candidate.candidate);
+						console.debug(candidate);
+
+                        let info = `[${candidate.type}] ${candidate.ip}:${candidate.port}`;
+                        if (candidate.relatedAddress) {
+                            info += ` via ${candidate.relatedAddress}`;
+                        }
+                        info += ` (${candidate.protocol})`;
+                        this.resultTurn.logs.push(info);
+
+						if (candidate.type === 'relay') {
+                            this.resultTurn.state = 'yes';
+                            $timeout.cancel(timeout);
+						}
+					} else {
+						console.warn('Invalid candidate:', ice.candidate.candidate);
+						this.resultTurn.logs.push('Invalid candidate (see debug log)');
+					}
+                });
             }
         }
-    }
-    testTurn();
-}
-
-if (document.readyState != 'loading') {
-    setupChecks();
-} else {
-    document.addEventListener('DOMContentLoaded', setupChecks);
-}
+        testTurn();
+    };
+
+});