tor-browser

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

mozilla_push_dummy_wsh.py (5142B)


      1 #!/usr/bin/python
      2 
      3 """
      4 A dummy push server that emits dummy push subscription such that Firefox can
      5 understand it, but without any data storage nor validation.
      6 
      7 How it works:
      8 1. Firefox will establish a websocket connection with the dummy server via
      9   PushServiceWebSocket.sys.mjs.
     10   The initial connection will also start up an HTTPS server that works
     11   as a push endpoint.
     12 2. A push subscription from a test via Push API will send messageType=register,
     13   which will generate a URL that points to the HTTPS server. It exposes
     14   channel ID as a URL parameter, and the server will pass through that parameter
     15   to Firefox so that it can map it to each push subscription.
     16 3. message=unregister will be sent from Firefox when unsubscription happens,
     17   but the dummy server doesn't process that as it doesn't even remember any
     18   subscription (it just passes through the channel ID and that's it).
     19 
     20 
     21 Caveat:
     22 * It sends CORS headers but that behavior is only observed in Firefox's push server
     23 (autopush-rs).
     24 * The dummy server doesn't throw any error on an invalid VAPID.
     25 * It assumes that Firefox will use websocket to connect to the push server, which is
     26  not true on Firefox for Android as it uses Google FCM.
     27  (See https://firefox-source-docs.mozilla.org/mobile/android/fenix/Firebase-Cloud-Messaging-for-WebPush.html)
     28 """
     29 
     30 import base64
     31 import json
     32 import pathlib
     33 import ssl
     34 import threading
     35 from http.server import BaseHTTPRequestHandler, HTTPServer
     36 from urllib.parse import parse_qs, urlparse
     37 from uuid import uuid4
     38 
     39 from pywebsocket3 import msgutil
     40 
     41 from tools import localpaths
     42 
     43 UAID = "8e1c93a9-139b-419c-b200-e715bb1e8ce8"
     44 
     45 
     46 class DummyEndpointHandler(BaseHTTPRequestHandler):
     47    def do_OPTIONS(self):
     48        self.send_response(200)
     49        self.send_header("Access-Control-Allow-Origin", "*")
     50        self.send_header("Access-Control-Allow-Methods", "*")
     51        self.send_header("Access-Control-Allow-Headers", "*")
     52        self.end_headers()
     53 
     54    def do_POST(self):
     55        url = urlparse(self.path)
     56        query = parse_qs(url.query)
     57 
     58        content_len = int(self.headers.get("Content-Length"))
     59        post_body = self.rfile.read(content_len)
     60        self.send_response(200)
     61        self.send_header("Access-Control-Allow-Origin", "*")
     62        self.send_header("Access-Control-Allow-Methods", "*")
     63        self.send_header("Access-Control-Allow-Headers", "*")
     64        self.end_headers()
     65 
     66        headers = {
     67            "encoding": self.headers.get("Content-Encoding"),
     68        }
     69        msgutil.send_message(
     70            self.server.websocket_request,
     71            json.dumps({
     72                "messageType": "notification",
     73                "channelID": query["channelID"][0],
     74                "data": base64.urlsafe_b64encode(post_body).decode(),
     75                "headers": headers if len(post_body) > 0 else None,
     76                # without a version string the push client thinks it's a duplicate
     77                "version": str(uuid4()),
     78            }),
     79        )
     80 
     81 
     82 def web_socket_do_extra_handshake(request):
     83    request.ws_protocol = "push-notification"
     84 
     85 
     86 def start_endpoint(server):
     87    def impl():
     88        context = ssl.SSLContext(ssl.PROTOCOL_TLS_SERVER)
     89 
     90        certs = pathlib.Path(localpaths.repo_root) / "tools/certs"
     91 
     92        context.load_cert_chain(
     93            certfile=str(certs / "web-platform.test.pem"),
     94            keyfile=str(certs / "web-platform.test.key"),
     95        )
     96 
     97        server.socket = context.wrap_socket(server.socket, server_side=True)
     98 
     99        server.serve_forever()
    100 
    101    return impl
    102 
    103 
    104 def web_socket_transfer_data(request):
    105    server = HTTPServer(("localhost", 0), DummyEndpointHandler)
    106    server.websocket_request = request
    107    port = server.server_port
    108 
    109    endpoint_thread = threading.Thread(target=start_endpoint(server), daemon=True)
    110    endpoint_thread.start()
    111 
    112    while True:
    113        line = request.ws_stream.receive_message()
    114        if line is None:
    115            server.shutdown()
    116            return
    117 
    118        message = json.loads(line)
    119        messageType = message["messageType"]
    120 
    121        if messageType == "hello":
    122            msgutil.send_message(
    123                request,
    124                json.dumps({
    125                    "messageType": "hello",
    126                    "uaid": UAID,
    127                    "status": 200,
    128                    "use_webpush": True,
    129                }),
    130            )
    131        elif messageType == "register":
    132            channelID = message["channelID"]
    133            msgutil.send_message(
    134                request,
    135                json.dumps({
    136                    "messageType": "register",
    137                    "uaid": UAID,
    138                    "channelID": channelID,
    139                    "status": 200,
    140                    "pushEndpoint": f"https://web-platform.test:{port}/push_endpoint?channelID={channelID}",
    141                }),
    142            )
    143        elif messageType == "unregister":
    144            msgutil.send_message(
    145                request,
    146                json.dumps({
    147                    "messageType": "unregister",
    148                    "channelID": message["channelID"],
    149                    "status": 200,
    150                }),
    151            )