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 )