Pair 1
This commit is contained in:
parent
021ff4611c
commit
90a87a8e4f
2 changed files with 106 additions and 1 deletions
103
web/apps/cast/src/services/pair.ts
Normal file
103
web/apps/cast/src/services/pair.ts
Normal 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();
|
||||
};
|
|
@ -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");
|
||||
|
|
Loading…
Add table
Reference in a new issue