This commit is contained in:
Manav Rathi 2024-05-04 08:44:58 +05:30
parent 159d207d1f
commit 54bb32d5e7
No known key found for this signature in database
2 changed files with 93 additions and 182 deletions

View file

@ -1,164 +1,65 @@
import log from "@/next/log";
import EnteSpinner from "@ente/shared/components/EnteSpinner";
import LargeType from "components/LargeType";
import { useRouter } from "next/router";
import { useEffect, useState } from "react";
import { pair, register, type Registration } from "services/cast";
import { advertiseCode, getCastData, register } from "services/cast";
import { storeCastData } from "services/cast/castService";
import { useCastReceiver } from "../utils/useCastReceiver";
export default function PairingMode() {
const [registration, setRegistration] = useState<
Registration | undefined
>();
const [publicKeyB64, setPublicKeyB64] = useState<string | undefined>();
const [privateKeyB64, setPrivateKeyB64] = useState<string | undefined>();
const [pairingCode, setPairingCode] = useState<string | undefined>();
// The returned cast object is a reference to a global instance and can be
// used in a useEffect dependency list.
const cast = useCastReceiver();
const router = useRouter();
// useEffect(() => {
// init();
// }, []);
// const init = async () => {
// try {
// const keypair = await generateKeyPair();
// setPublicKeyB64(await toB64(keypair.publicKey));
// setPrivateKeyB64(await toB64(keypair.privateKey));
// } catch (e) {
// log.error("failed to generate keypair", e);
// throw e;
// }
// };
// useEffect(() => {
// if (!cast) {
// return;
// }
// if (isCastReady) {
// return;
// }
// const context = cast.framework.CastReceiverContext.getInstance();
// try {
// const options = new cast.framework.CastReceiverOptions();
// options.maxInactivity = 3600;
// options.customNamespaces = Object.assign({});
// options.customNamespaces["urn:x-cast:pair-request"] =
// cast.framework.system.MessageType.JSON;
// options.disableIdleTimeout = true;
// context.set;
// context.addCustomMessageListener(
// "urn:x-cast:pair-request",
// messageReceiveHandler,
// );
// // listen to close request and stop the context
// context.addEventListener(
// cast.framework.system.EventType.SENDER_DISCONNECTED,
// // eslint-disable-next-line @typescript-eslint/no-unused-vars
// (_) => {
// context.stop();
// },
// );
// context.start(options);
// setIsCastReady(true);
// } catch (e) {
// log.error("failed to create cast context", e);
// }
// return () => {
// // context.stop();
// };
// }, [cast]);
// const messageReceiveHandler = (message: {
// type: string;
// senderId: string;
// data: any;
// }) => {
// try {
// cast.framework.CastReceiverContext.getInstance().sendCustomMessage(
// "urn:x-cast:pair-request",
// message.senderId,
// {
// code: deviceCode,
// },
// );
// } catch (e) {
// log.error("failed to send message", e);
// }
// };
// const generateKeyPair = async () => {
// await _sodium.ready;
// const keypair = _sodium.crypto_box_keypair();
// return keypair;
// };
// const pollForCastData = async () => {
// if (codePending) {
// return;
// }
// // see if we were acknowledged on the client.
// // the client will send us the encrypted payload using our public key that we advertised.
// // then, we can decrypt this and store all the necessary info locally so we can play the collection slideshow.
// let devicePayload = "";
// try {
// const encDastData = await castGateway.getCastData(`${deviceCode}`);
// if (!encDastData) return;
// devicePayload = encDastData;
// } catch (e) {
// setCodePending(true);
// init();
// return;
// }
// const decryptedPayload = await boxSealOpen(
// devicePayload,
// publicKeyB64,
// privateKeyB64,
// );
// const decryptedPayloadObj = JSON.parse(atob(decryptedPayload));
// return decryptedPayloadObj;
// };
// const advertisePublicKey = async (publicKeyB64: string) => {
// // hey client, we exist!
// try {
// const codeValue = await castGateway.registerDevice(publicKeyB64);
// setDeviceCode(codeValue);
// setCodePending(false);
// } catch (e) {
// // schedule re-try after 5 seconds
// setTimeout(() => {
// init();
// }, 5000);
// return;
// }
// };
const init = () => {
register().then((r) => {
setPublicKeyB64(r.publicKeyB64);
setPrivateKeyB64(r.privateKeyB64);
setPairingCode(r.pairingCode);
});
};
useEffect(() => {
register().then((r) => setRegistration(r));
init();
}, []);
useEffect(() => {
if (!cast || !registration) return;
if (cast) advertiseCode(cast, () => pairingCode);
}, [cast]);
pair(cast, registration).then((data) => {
const pollTick = async () => {
const registration = { publicKeyB64, privateKeyB64, pairingCode };
try {
const data = await getCastData(registration);
if (!data) {
// No one has connected yet
return;
}
log.info("Pairing complete");
storeCastData(data);
router.push("/slideshow");
});
}, [cast, registration]);
await router.push("/slideshow");
} catch (e) {
console.log("Failed to get cast data", e);
// Start again from the beginning
setPairingCode(undefined);
init();
}
};
// console.log([cast, registration]);
// useEffect(() => {
// if (!publicKeyB64) return;
// advertisePublicKey(publicKeyB64);
// }, [publicKeyB64]);
useEffect(() => {
if (!publicKeyB64 || !privateKeyB64 || !pairingCode) return;
const { pairingCode } = registration ?? {};
const interval = setInterval(pollTick, 2000);
return () => clearInterval(interval);
}, [publicKeyB64, privateKeyB64, pairingCode]);
return (
<>

View file

@ -1,5 +1,5 @@
/* eslint has already fixed this warning, we don't have the latest version yet
https://github.com/eslint/eslint/pull/18286 */
// eslint has already fixed this warning, we don't have the latest version yet
// https://github.com/eslint/eslint/pull/18286
/* eslint-disable no-constant-condition */
import log from "@/next/log";
@ -30,21 +30,19 @@ export interface Registration {
*
* The pairing happens in two phases:
*
* Phase 1
* Phase 1 - {@link register}
*
* 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. Show this on the screen.
*
* Phase 2
* Phase 2 - {@link advertiseCode}
*
* There are two ways the client can connect - either by sending us a blank
* message over the Chromecast protocol (to which we'll reply with the pairing
* code), or by the user manually entering the pairing code on their screen.
*
* So there are two parallel processes.
*
* 3. Listen for incoming messages over the Chromecast connection.
*
* 4. The client (our Web or mobile app) will connect using the "sender"
@ -60,11 +58,12 @@ export interface Registration {
* that traverses over the Chromecast connection.
*
* Once the client gets the pairing code (via Chromecast or manual entry),
* they'll let museum know. So
* they'll let museum know. So in parallel with Phase 2, we perform Phase 3.
*
* 7. In parallel, keep polling museum to ask it if anyone has claimed that code
* we vended out and used that to send us an payload encrypted using our
* public key.
* Phase 3 - {@link getCastData} in a setInterval.
*
* 7. Keep polling museum to ask it if anyone has claimed that code we vended
* out and used that to send us an payload encrypted using our public key.
*
* 8. When that happens, decrypt that data with our private key, and return this
* payload. It is a JSON object that contains the data we need to initiate a
@ -73,9 +72,11 @@ export interface Registration {
* Phase 1 (Steps 1 and 2) are done by the {@link register} function, which
* returns a {@link Registration}.
*
* At this time we start showing the pairing code on the UI, and proceed with
* the remaining steps (Phase 2) using the {@link pair} function that returns
* the data we need to start the slideshow.
* At this time we start showing the pairing code on the UI, and start phase 2,
* {@link advertiseCode} to vend out the pairing code to Chromecast connections.
*
* In parallel, we start Phase 3, calling {@link getCastData} in a loop. Once we
* get a response, we decrypt it to get the data we need to start the slideshow.
*/
export const register = async (): Promise<Registration> => {
// Generate keypair.
@ -100,15 +101,16 @@ export const register = async (): Promise<Registration> => {
};
/**
* Listen for pairing requests using the given {@link cast} instance for
* connections for {@link registration}. Phase 2 of the pairing protocol.
* Listen for incoming messages on the given {@link cast} receiver, replying to
* each of them with a pairing code obtained using the given {@link pairingCode}
* callback. Phase 2 of the pairing protocol.
*
* On successful pairing, return the payload (JSON) sent by the sender who
* connected to us. See: [Note: Pairing protocol]
* See: [Note: Pairing protocol].
*/
export const pair = async (cast: Cast, registration: Registration) => {
const { pairingCode, publicKeyB64, privateKeyB64 } = registration;
export const advertiseCode = (
cast: Cast,
pairingCode: () => string | undefined,
) => {
// Prepare the Chromecast "context".
const context = cast.framework.CastReceiverContext.getInstance();
const namespace = "urn:x-cast:pair-request";
@ -121,9 +123,18 @@ export const pair = async (cast: Cast, registration: Registration) => {
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: pairingCode });
// Reply with the code that we have if anyone asks over Chromecast.
const incomingMessageListener = ({ senderId }: { senderId: string }) => {
const code = pairingCode();
if (!code) {
log.warn(
"Ignoring incoming Chromecast message because we do not yet have a pairing code",
);
return;
}
context.sendCustomMessage(namespace, senderId, { code });
};
context.addCustomMessageListener(
namespace,
@ -141,26 +152,25 @@ export const pair = async (cast: Cast, registration: Registration) => {
// Start listening for Chromecast connections.
context.start(options);
};
// Start polling museum.
let encryptedCastData: string | undefined | null;
while (true) {
// The client will send us the encrypted payload using our public key
// that we registered with museum. Then, we can decrypt this using the
// private key of the pair and return the plaintext payload, which'll be
// a JSON object containing the data we need to start a slideshow for
// some collection.
try {
encryptedCastData = await castGateway.getCastData(pairingCode);
} catch (e) {
log.error("Failed to get cast data from server", e);
}
if (encryptedCastData) break;
// Nobody's claimed the code yet (or there was some error). Poll again
// after 2 seconds.
await wait(2000);
}
/**
* Ask museum if anyone has sent a (encrypted) payload corresponding to the
* given pairing code. If so, decrypt it using our private key and return the
* JSON payload. Phase 3 of the pairing protocol.
*
* See: [Note: Pairing protocol].
*/
export const getCastData = async (registration: Registration) => {
const { pairingCode, publicKeyB64, privateKeyB64 } = registration;
// The client will send us the encrypted payload using our public key that
// we registered with museum.
const encryptedCastData = await castGateway.getCastData(pairingCode);
// Decrypt it using the private key of the pair and return the plaintext
// payload, which'll be a JSON object containing the data we need to start a
// slideshow for some collection.
const decryptedCastData = await boxSealOpen(
encryptedCastData,
publicKeyB64,