This commit is contained in:
Manav Rathi 2024-05-03 21:42:33 +05:30
parent 021ff4611c
commit 90a87a8e4f
No known key found for this signature in database
2 changed files with 106 additions and 1 deletions

View file

@ -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();
};

View file

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