Manav Rathi 1 年之前
父節點
當前提交
90a87a8e4f
共有 2 個文件被更改,包括 106 次插入1 次删除
  1. 103 0
      web/apps/cast/src/services/pair.ts
  2. 3 1
      web/apps/cast/src/utils/useCastReceiver.tsx

+ 103 - 0
web/apps/cast/src/services/pair.ts

@@ -0,0 +1,103 @@
+import log from "@/next/log";
+import { toB64 } from "@ente/shared/crypto/internal/libsodium";
+import castGateway from "@ente/shared/network/cast";
+import { wait } from "@ente/shared/utils";
+import _sodium from "libsodium-wrappers";
+import { type Cast } from "../utils/useCastReceiver";
+
+/**
+ * Listen for pairing requests using the given {@link cast} instance.
+ *
+ * [Note: Cast protocol]
+ *
+ * The Chromecast Framework (represented here by our handle to the Chromecast
+ * Web SDK, {@link cast}) itself is used for only the initial handshake, none of
+ * the data, even encrypted passes over it thereafter.
+ *
+ * The entire protocol is quite simple.
+ *
+ * 1. We (the receiver) generate a public/private keypair. and register the
+ *    public part of it with museum.
+ *
+ * 2. Museum gives us a pairing "code" in lieu.
+ *
+ * 3. Listen for incoming messages over the Chromecast connection.
+ *
+ * 4. The client (our Web or mobile app) will connect using the "sender"
+ *    Chromecast SDK. This will result in a bi-directional channel between us
+ *    ("receiver") and the Ente client app ("sender").
+ *
+ * 5. Thereafter, if at any time the sender disconnects, close the Chromecast
+ *    context. This effectively shuts us down, causing the entire page to get
+ *    reloaded.
+ *
+ * 6. After connecting, the sender sends an (empty) message. We reply by sending
+ *    them a message containing the pairing code. This exchange is the only data
+ *    that traverses over the Chromecast connection.
+ *
+ * 5. If at anytime the
+ *
+ *
+ *
+ * in our custom //    "urn:x-cast:pair-request" namespace. over Chromecast
+        protocol is minimal:
+ *
+ * 1. Client (Web or mobile) sends an (empty) message in our custom //
+        "urn:x-cast:pair-request" namespace.
+        //
+        // 2. We reply with the device code.
+*/
+export const listenForPairingRequest = async (cast: Cast) => {
+    // Generate keypair
+    const keypair = await generateKeyPair();
+    const publicKeyB64 = await toB64(keypair.publicKey);
+    const privateKeyB64 = await toB64(keypair.privateKey);
+
+    // Register keypair with museum to get a pairing code.
+    let code: string;
+    do {
+        try {
+            code = await castGateway.registerDevice(publicKeyB64);
+        } catch (e) {
+            log.error("Failed to register public key with server", e);
+            // Schedule retry after 10 seconds.
+            await wait(10000);
+        }
+    } while (code === undefined);
+
+    // Listen for incoming messages sent via the Chromecast SDK
+    const context = cast.framework.CastReceiverContext.getInstance();
+    const namespace = "urn:x-cast:pair-request";
+
+    const options = new cast.framework.CastReceiverOptions();
+    // TODO(MR): Are any of these options required?
+    options.maxInactivity = 3600;
+    options.customNamespaces = Object.assign({});
+    options.customNamespaces[namespace] =
+        cast.framework.system.MessageType.JSON;
+    options.disableIdleTimeout = true;
+
+    // Reply with the code that we have if anyone asks over chromecast.
+    const incomingMessageListener = ({ senderId }: { senderId: string }) =>
+        context.sendCustomMessage(namespace, senderId, { code });
+
+    context.addCustomMessageListener(
+        namespace,
+        // We need to cast, the `senderId` is present in the message we get but
+        // not present in the TypeScript type.
+        incomingMessageListener as unknown as SystemEventHandler,
+    );
+
+    // Shutdown ourselves if the "sender" disconnects.
+    context.addEventListener(
+        cast.framework.system.EventType.SENDER_DISCONNECTED,
+        () => context.stop(),
+    );
+
+    context.start(options);
+};
+
+const generateKeyPair = async () => {
+    await _sodium.ready;
+    return _sodium.crypto_box_keypair();
+};

+ 3 - 1
web/apps/cast/src/utils/useCastReceiver.tsx

@@ -1,6 +1,8 @@
 /// <reference types="chromecast-caf-receiver" />
 import { useEffect, useState } from "react";
 
+export type Cast = typeof cast;
+
 /**
  * Load the Chromecast Web Receiver SDK and return a reference to the `cast`
  * global object that the SDK attaches to the window.
@@ -8,7 +10,7 @@ import { useEffect, useState } from "react";
  * https://developers.google.com/cast/docs/web_receiver/basic
  */
 export const useCastReceiver = () => {
-    const [receiver, setReceiver] = useState<typeof cast | undefined>();
+    const [receiver, setReceiver] = useState<Cast | undefined>();
 
     useEffect(() => {
         const script = document.createElement("script");