3 phase
This commit is contained in:
parent
159d207d1f
commit
54bb32d5e7
2 changed files with 93 additions and 182 deletions
|
@ -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 (
|
||||
<>
|
||||
|
|
|
@ -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,
|
||||
|
|
Loading…
Add table
Reference in a new issue