Split phases
This commit is contained in:
parent
a86a818924
commit
afa8303d91
2 changed files with 169 additions and 155 deletions
|
@ -1,176 +1,164 @@
|
|||
import log from "@/next/log";
|
||||
import EnteSpinner from "@ente/shared/components/EnteSpinner";
|
||||
import { boxSealOpen, toB64 } from "@ente/shared/crypto/internal/libsodium";
|
||||
import castGateway from "@ente/shared/network/cast";
|
||||
import LargeType from "components/LargeType";
|
||||
import _sodium from "libsodium-wrappers";
|
||||
import { useRouter } from "next/router";
|
||||
import { useEffect, useState } from "react";
|
||||
import { pair, register, type Registration } 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 [deviceCode, setDeviceCode] = useState("");
|
||||
const [publicKeyB64, setPublicKeyB64] = useState("");
|
||||
const [privateKeyB64, setPrivateKeyB64] = useState("");
|
||||
const [codePending, setCodePending] = useState(true);
|
||||
const [isCastReady, setIsCastReady] = useState(false);
|
||||
|
||||
const cast = useCastReceiver();
|
||||
|
||||
useEffect(() => {
|
||||
init();
|
||||
}, []);
|
||||
// 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;
|
||||
}
|
||||
};
|
||||
// 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();
|
||||
// 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;
|
||||
// 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;
|
||||
// options.disableIdleTimeout = true;
|
||||
// context.set;
|
||||
|
||||
context.addCustomMessageListener(
|
||||
"urn:x-cast:pair-request",
|
||||
messageReceiveHandler,
|
||||
);
|
||||
// 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);
|
||||
}
|
||||
// // 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]);
|
||||
// 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 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 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 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 decryptedPayload = await boxSealOpen(
|
||||
// devicePayload,
|
||||
// publicKeyB64,
|
||||
// privateKeyB64,
|
||||
// );
|
||||
|
||||
const decryptedPayloadObj = JSON.parse(atob(decryptedPayload));
|
||||
// const decryptedPayloadObj = JSON.parse(atob(decryptedPayload));
|
||||
|
||||
return decryptedPayloadObj;
|
||||
};
|
||||
// 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 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 router = useRouter();
|
||||
|
||||
useEffect(() => {
|
||||
console.log("useEffect for pairing called");
|
||||
if (deviceCode.length < 1 || !publicKeyB64 || !privateKeyB64) return;
|
||||
|
||||
const interval = setInterval(async () => {
|
||||
console.log("polling for cast data");
|
||||
const data = await pollForCastData();
|
||||
if (!data) {
|
||||
console.log("no data");
|
||||
return;
|
||||
}
|
||||
storeCastData(data);
|
||||
console.log("pushing slideshow");
|
||||
await router.push("/slideshow");
|
||||
}, 1000);
|
||||
|
||||
return () => {
|
||||
clearInterval(interval);
|
||||
};
|
||||
}, [deviceCode, publicKeyB64, privateKeyB64, codePending]);
|
||||
register().then((r) => setRegistration(r));
|
||||
}, []);
|
||||
|
||||
useEffect(() => {
|
||||
if (!publicKeyB64) return;
|
||||
advertisePublicKey(publicKeyB64);
|
||||
}, [publicKeyB64]);
|
||||
if (!cast || !registration) return;
|
||||
|
||||
pair(cast, registration).then((data) => {
|
||||
storeCastData(data);
|
||||
router.push("/slideshow");
|
||||
});
|
||||
}, [cast, registration]);
|
||||
|
||||
// useEffect(() => {
|
||||
// if (!publicKeyB64) return;
|
||||
// advertisePublicKey(publicKeyB64);
|
||||
// }, [publicKeyB64]);
|
||||
|
||||
return (
|
||||
<>
|
||||
|
@ -204,12 +192,10 @@ export default function PairingMode() {
|
|||
overflow: "hidden",
|
||||
}}
|
||||
>
|
||||
{codePending ? (
|
||||
<EnteSpinner />
|
||||
{deviceCode ? (
|
||||
<LargeType chars={deviceCode.split("")} />
|
||||
) : (
|
||||
<>
|
||||
<LargeType chars={deviceCode.split("")} />
|
||||
</>
|
||||
<EnteSpinner />
|
||||
)}
|
||||
</div>
|
||||
<p
|
||||
|
|
|
@ -5,10 +5,18 @@ import { wait } from "@ente/shared/utils";
|
|||
import _sodium from "libsodium-wrappers";
|
||||
import { type Cast } from "../utils/useCastReceiver";
|
||||
|
||||
export interface Registration {
|
||||
/** A pairing code shown on the screen. A client can use this to connect. */
|
||||
pairingCode: string;
|
||||
/** The public part of the keypair we registered with the server. */
|
||||
publicKeyB64: string;
|
||||
/** The private part of the keypair we registered with the server. */
|
||||
privateKeyB64: string;
|
||||
}
|
||||
|
||||
/**
|
||||
* Listen for pairing requests using the given {@link cast} instance. On
|
||||
* successful pairing, return the payload (JSON) sent by the sender who
|
||||
* connected to us.
|
||||
* Register a keypair with the server and return a pairing code that can be used
|
||||
* to connect to us. Phase 1 of the pairing protocol.
|
||||
*
|
||||
* [Note: Pairing protocol]
|
||||
*
|
||||
|
@ -43,24 +51,44 @@ import { type Cast } from "../utils/useCastReceiver";
|
|||
* to initiate a slideshow for a particular Ente collection.
|
||||
*
|
||||
* 6. When that happens, decrypt that data with our private key, and return it.
|
||||
*
|
||||
* 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 using the {@link pair} function that returns the data we
|
||||
* need to start the slideshow.
|
||||
*/
|
||||
export const pair = async (cast: Cast) => {
|
||||
export const register = async (): Promise<Registration> => {
|
||||
// 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;
|
||||
let pairingCode: string;
|
||||
do {
|
||||
try {
|
||||
code = await castGateway.registerDevice(publicKeyB64);
|
||||
pairingCode = 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);
|
||||
} while (pairingCode === undefined);
|
||||
|
||||
return { pairingCode, publicKeyB64, privateKeyB64 };
|
||||
};
|
||||
|
||||
/**
|
||||
* Listen for pairing requests using the given {@link cast} instance for
|
||||
* connections for {@link registration}. 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]
|
||||
*/
|
||||
export const pair = async (cast: Cast, registration: Registration) => {
|
||||
const { pairingCode, publicKeyB64, privateKeyB64 } = registration;
|
||||
|
||||
// Prepare the Chromecast "context".
|
||||
const context = cast.framework.CastReceiverContext.getInstance();
|
||||
|
@ -76,7 +104,7 @@ export const pair = async (cast: Cast) => {
|
|||
|
||||
// Reply with the code that we have if anyone asks over chromecast.
|
||||
const incomingMessageListener = ({ senderId }: { senderId: string }) =>
|
||||
context.sendCustomMessage(namespace, senderId, { code });
|
||||
context.sendCustomMessage(namespace, senderId, { code: pairingCode });
|
||||
|
||||
context.addCustomMessageListener(
|
||||
namespace,
|
||||
|
@ -104,7 +132,7 @@ export const pair = async (cast: Cast) => {
|
|||
// a JSON object containing the data we need to start a slideshow for
|
||||
// some collection.
|
||||
try {
|
||||
encryptedCastData = await castGateway.getCastData(code);
|
||||
encryptedCastData = await castGateway.getCastData(pairingCode);
|
||||
} catch (e) {
|
||||
log.error("Failed to get cast data from server", e);
|
||||
// Schedule retry after 10 seconds.
|
Loading…
Add table
Reference in a new issue