tor-browser

The Tor Browser
git clone https://git.dasho.dev/tor-browser.git
Log | Files | Refs | README | LICENSE

test_beacon_on_pagehide_shutdown.py (9399B)


      1 # This Source Code Form is subject to the terms of the Mozilla Public
      2 # License, v. 2.0. If a copy of the MPL was not distributed with this
      3 # file, You can obtain one at http://mozilla.org/MPL/2.0/.
      4 
      5 import json
      6 import socket
      7 import threading
      8 import time
      9 from http.server import BaseHTTPRequestHandler, HTTPServer
     10 
     11 from marionette_driver import Wait
     12 from marionette_harness import MarionetteTestCase
     13 
     14 
     15 class BeaconHandler(BaseHTTPRequestHandler):
     16    """HTTP request handler that logs beacon requests."""
     17 
     18    received_beacons = []
     19 
     20    def do_POST(self):
     21        if self.path.startswith("/beacon"):
     22            content_length = int(self.headers.get("Content-Length", 0))
     23            body = (
     24                self.rfile.read(content_length).decode("utf-8")
     25                if content_length > 0
     26                else ""
     27            )
     28 
     29            beacon_data = {"path": self.path, "body": body, "timestamp": time.time()}
     30            BeaconHandler.received_beacons.append(beacon_data)
     31 
     32            self.send_response(200)
     33            self.send_header("Access-Control-Allow-Origin", "*")
     34            self.send_header("Access-Control-Allow-Methods", "POST")
     35            self.send_header("Access-Control-Allow-Headers", "Content-Type")
     36            self.end_headers()
     37            self.wfile.write(b"OK")
     38        else:
     39            self.send_response(404)
     40            self.end_headers()
     41 
     42    def do_OPTIONS(self):
     43        self.send_response(200)
     44        self.send_header("Access-Control-Allow-Origin", "*")
     45        self.send_header("Access-Control-Allow-Methods", "POST")
     46        self.send_header("Access-Control-Allow-Headers", "Content-Type")
     47        self.end_headers()
     48 
     49    def log_message(self, format, *args):
     50        # Suppress server logs
     51        pass
     52 
     53 
     54 class BeaconOnPagehideShutdownTestCase(MarionetteTestCase):
     55    """Test that sendBeacon works during pagehide when shutting down."""
     56 
     57    def setUp(self):
     58        super().setUp()
     59 
     60        # Find a free port for our test server
     61        sock = socket.socket()
     62        sock.bind(("", 0))
     63        self.server_port = sock.getsockname()[1]
     64        sock.close()
     65 
     66        # Start HTTP server in a separate thread
     67        self.server = HTTPServer(("localhost", self.server_port), BeaconHandler)
     68        self.server_thread = threading.Thread(target=self.server.serve_forever)
     69        self.server_thread.daemon = True
     70        self.server_thread.start()
     71 
     72        # Clear any previous beacon data
     73        BeaconHandler.received_beacons.clear()
     74 
     75    def tearDown(self):
     76        self.server.shutdown()
     77        self.server.server_close()
     78        super().tearDown()
     79 
     80    def test_beacon_sent_on_regular_pagehide(self):
     81        """Test that a beacon is successfully sent during regular pagehide (navigation)."""
     82 
     83        # Use marionette to inject a test page with sendBeacon functionality
     84        self.marionette.navigate("about:blank")
     85 
     86        # Inject the beacon test script directly into the page
     87        self.marionette.execute_script(
     88            f"""
     89            // Set up beacon test
     90            window.beaconServerPort = {self.server_port};
     91 
     92            // Add the pagehide event listener
     93            window.addEventListener('pagehide', function(event) {{
     94                console.log('PAGEHIDE EVENT FIRED - persisted:', event.persisted);
     95 
     96                const data = JSON.stringify({{
     97                    message: 'beacon from regular pagehide',
     98                    timestamp: Date.now(),
     99                    persisted: event.persisted
    100                }});
    101 
    102                // Send beacon to our test server
    103                const result = navigator.sendBeacon('http://localhost:' + window.beaconServerPort + '/beacon/regular', data);
    104                console.log('SENDBEACON RESULT:', result);
    105            }});
    106 
    107            // Set a title so we can verify the script loaded
    108            document.title = 'Regular Beacon Test Page';
    109            console.log('Regular beacon test page setup complete');
    110        """
    111        )
    112 
    113        # Wait for script execution
    114        Wait(self.marionette, timeout=10).until(
    115            lambda _: self.marionette.title == "Regular Beacon Test Page"
    116        )
    117 
    118        # Record how many beacons we had before navigation
    119        initial_beacon_count = len(BeaconHandler.received_beacons)
    120 
    121        # Navigate to a different page - this should trigger pagehide and send the beacon
    122        self.marionette.navigate("about:blank")
    123 
    124        # Wait for navigation and any pending beacon requests
    125        Wait(self.marionette, timeout=10).until(
    126            lambda _: self.marionette.execute_script("return document.readyState")
    127            == "complete"
    128        )
    129        time.sleep(2)  # Give server time to process the beacon
    130 
    131        # Check that we received the beacon
    132        final_beacon_count = len(BeaconHandler.received_beacons)
    133 
    134        # Log debug information
    135        print(f"Regular pagehide - Initial beacon count: {initial_beacon_count}")
    136        print(f"Regular pagehide - Final beacon count: {final_beacon_count}")
    137        print(f"Regular pagehide - Received beacons: {BeaconHandler.received_beacons}")
    138 
    139        self.assertGreater(
    140            final_beacon_count,
    141            initial_beacon_count,
    142            f"Expected to receive a beacon during regular pagehide (navigation). "
    143            f"Initial: {initial_beacon_count}, Final: {final_beacon_count}",
    144        )
    145 
    146        # Verify the beacon contains expected data
    147        received_beacon = BeaconHandler.received_beacons[-1]
    148        self.assertEqual(received_beacon["path"], "/beacon/regular")
    149 
    150        # Parse the beacon body as JSON
    151        try:
    152            beacon_data = json.loads(received_beacon["body"])
    153            self.assertEqual(beacon_data["message"], "beacon from regular pagehide")
    154            self.assertIn("timestamp", beacon_data)
    155        except json.JSONDecodeError:
    156            self.fail(f"Beacon body was not valid JSON: {received_beacon['body']}")
    157 
    158    def test_beacon_sent_on_pagehide_during_shutdown(self):
    159        """Test that a beacon is successfully sent during pagehide when browser shuts down.
    160 
    161        This is a regression test for bug 1931956 - sendBeacon requests were not reliably
    162        sent during pagehide when the browser was shutting down. The test verifies that
    163        this functionality works correctly.
    164        """
    165 
    166        # Use marionette to inject a test page with sendBeacon functionality
    167        self.marionette.navigate("about:blank")
    168 
    169        # Inject the beacon test script directly into the page
    170        self.marionette.execute_script(
    171            f"""
    172            // Set up beacon test
    173            window.beaconServerPort = {self.server_port};
    174 
    175            // Add the pagehide event listener
    176            window.addEventListener('pagehide', function(event) {{
    177                console.log('SHUTDOWN PAGEHIDE EVENT FIRED - persisted:', event.persisted);
    178 
    179                const data = JSON.stringify({{
    180                    message: 'beacon from pagehide',
    181                    timestamp: Date.now(),
    182                    persisted: event.persisted
    183                }});
    184 
    185                // Send beacon to our test server
    186                const result = navigator.sendBeacon('http://localhost:' + window.beaconServerPort + '/beacon/shutdown', data);
    187                console.log('SHUTDOWN SENDBEACON RESULT:', result);
    188            }});
    189 
    190            // Set a title so we can verify the script loaded
    191            document.title = 'Beacon Test Page';
    192            console.log('Beacon test page loaded');
    193        """
    194        )
    195 
    196        # Wait for script execution
    197        Wait(self.marionette, timeout=10).until(
    198            lambda _: self.marionette.title == "Beacon Test Page"
    199        )
    200 
    201        # Record how many beacons we had before shutdown
    202        initial_beacon_count = len(BeaconHandler.received_beacons)
    203 
    204        # Trigger browser shutdown - this should fire the pagehide event
    205        # and send the beacon before the browser fully closes
    206        self.marionette.quit()
    207 
    208        # Give the server a moment to receive any pending requests
    209        # The beacon should be sent synchronously during pagehide, but we'll
    210        # wait a bit to ensure it's processed by our server
    211        time.sleep(2)
    212 
    213        # Check that we received the beacon
    214        final_beacon_count = len(BeaconHandler.received_beacons)
    215 
    216        # Log debug information
    217        print(f"Initial beacon count: {initial_beacon_count}")
    218        print(f"Final beacon count: {final_beacon_count}")
    219        print(f"Received beacons: {BeaconHandler.received_beacons}")
    220 
    221        self.assertGreater(
    222            final_beacon_count,
    223            initial_beacon_count,
    224            f"Expected to receive a beacon during pagehide on shutdown. "
    225            f"Initial: {initial_beacon_count}, Final: {final_beacon_count}. "
    226            f"If this fails, it indicates a regression of bug 1931956.",
    227        )
    228 
    229        # Verify the beacon contains expected data
    230        received_beacon = BeaconHandler.received_beacons[-1]
    231        self.assertEqual(received_beacon["path"], "/beacon/shutdown")
    232 
    233        # Parse the beacon body as JSON
    234        try:
    235            beacon_data = json.loads(received_beacon["body"])
    236            self.assertEqual(beacon_data["message"], "beacon from pagehide")
    237            self.assertIn("timestamp", beacon_data)
    238        except json.JSONDecodeError:
    239            self.fail(f"Beacon body was not valid JSON: {received_beacon['body']}")