216 lines
8.2 KiB
TypeScript
216 lines
8.2 KiB
TypeScript
import { Call } from "@/app/components/Call/Call";
|
||
import { useConsoleLogger } from "@/app/hooks/useConsoleLogger";
|
||
import { createContext, useEffect, useRef, useState } from "react";
|
||
import nacl from 'tweetnacl';
|
||
import { useSender } from "../ProtocolProvider/useSender";
|
||
import { PacketSignal, SignalType } from "../ProtocolProvider/protocol/packets/packet.signal";
|
||
import { usePacket } from "../ProtocolProvider/usePacket";
|
||
import { usePublicKey } from "../AccountProvider/usePublicKey";
|
||
|
||
|
||
export interface CallContextValue {
|
||
call: (callable: string) => void;
|
||
close: () => void;
|
||
activeCall: string;
|
||
callState: CallState;
|
||
muted: boolean;
|
||
sound: boolean;
|
||
setMuted: (muted: boolean) => void;
|
||
setSound: (sound: boolean) => void;
|
||
duration: number;
|
||
setShowCallView: (show: boolean) => void;
|
||
getKeyCast: () => string;
|
||
accept: () => void;
|
||
}
|
||
|
||
export enum CallState {
|
||
CONNECTING,
|
||
KEY_EXCHANGE,
|
||
ACTIVE,
|
||
ENDED,
|
||
INCOMING
|
||
}
|
||
|
||
export enum CallRole {
|
||
/**
|
||
* Вызывающая сторона, которая инициирует звонок
|
||
*/
|
||
CALLER,
|
||
/**
|
||
* Принимающая сторона, которая отвечает на звонок и принимает его
|
||
*/
|
||
CALLEE
|
||
}
|
||
|
||
export const CallContext = createContext<CallContextValue | null>(null);
|
||
export interface CallProviderProps {
|
||
children: React.ReactNode;
|
||
}
|
||
|
||
export function CallProvider(props : CallProviderProps) {
|
||
const [activeCall, setActiveCall] = useState<string>("");
|
||
const [callState, setCallState] = useState<CallState>(CallState.ENDED);
|
||
const [muted, setMuted] = useState<boolean>(false);
|
||
const [sound, setSound] = useState<boolean>(true);
|
||
const [duration, setDuration] = useState<number>(0);
|
||
const [showCallView, setShowCallView] = useState<boolean>(callState == CallState.INCOMING);
|
||
const {info} = useConsoleLogger("CallProvider");
|
||
const [sessionKeys, setSessionKeys] = useState<nacl.BoxKeyPair | null>(null);
|
||
const send = useSender();
|
||
const publicKey = usePublicKey();
|
||
|
||
const roleRef = useRef<CallRole | null>(null);
|
||
const [sharedSecret, setSharedSecret] = useState<string>("");
|
||
|
||
useEffect(() => {
|
||
console.info("TRACE -> ", sharedSecret)
|
||
}, [sharedSecret]);
|
||
|
||
usePacket(26, (packet: PacketSignal) => {
|
||
const signalType = packet.getSignalType();
|
||
if(activeCall){
|
||
/**
|
||
* У нас уже есть активный звонок, игнорируем все сигналы, кроме сигналов от текущего звонка
|
||
*/
|
||
if(packet.getSrc() != activeCall){
|
||
info("Received signal for another call, ignoring");
|
||
return;
|
||
}
|
||
}
|
||
if(signalType == SignalType.CALL){
|
||
/**
|
||
* Нам поступает звонок
|
||
*/
|
||
setActiveCall(packet.getSrc());
|
||
setCallState(CallState.INCOMING);
|
||
setShowCallView(true);
|
||
}
|
||
if(signalType == SignalType.KEY_EXCHANGE && roleRef.current == CallRole.CALLER){
|
||
/**
|
||
* Другая сторона сгенерировала ключи для сессии и отправила нам публичную часть,
|
||
* теперь мы можем создать общую секретную сессию для шифрования звонка
|
||
*/
|
||
const sharedPublic = packet.getSharedPublic();
|
||
if(!sharedPublic){
|
||
info("Received key exchange signal without shared public key");
|
||
return;
|
||
}
|
||
const sessionKeys = generateSessionKeys();
|
||
const computedSharedSecret = nacl.box.before(Buffer.from(sharedPublic, 'hex'), sessionKeys.secretKey);
|
||
setSharedSecret(Buffer.from(computedSharedSecret).toString('hex'));
|
||
info("Generated shared secret for call session: " + Buffer.from(computedSharedSecret).toString('hex'));
|
||
/**
|
||
* Нам нужно отправить свой публичный ключ другой стороне, чтобы она тоже могла создать общую секретную сессию
|
||
*/
|
||
const signalPacket = new PacketSignal();
|
||
signalPacket.setSrc(publicKey);
|
||
signalPacket.setDst(packet.getDst());
|
||
signalPacket.setSignalType(SignalType.KEY_EXCHANGE);
|
||
signalPacket.setSharedPublic(Buffer.from(sessionKeys.publicKey).toString('hex'));
|
||
send(signalPacket);
|
||
}
|
||
if(signalType == SignalType.KEY_EXCHANGE && roleRef.current == CallRole.CALLEE){
|
||
/**
|
||
* Мы отправили свою публичную часть ключа другой стороне,
|
||
* теперь мы получили ее публичную часть и можем создать общую
|
||
* секретную сессию для шифрования звонка
|
||
*/
|
||
const sharedPublic = packet.getSharedPublic();
|
||
if(!sharedPublic){
|
||
info("Received key exchange signal without shared public key");
|
||
return;
|
||
}
|
||
if(!sessionKeys){
|
||
info("Received key exchange signal but session keys are not generated");
|
||
return;
|
||
}
|
||
const computedSharedSecret = nacl.box.before(Buffer.from(sharedPublic, 'hex'), sessionKeys.secretKey);
|
||
info("Generated shared secret for call session: " + Buffer.from(computedSharedSecret).toString('hex'));
|
||
setSharedSecret(Buffer.from(computedSharedSecret).toString('hex'));
|
||
}
|
||
if(signalType == SignalType.ACTIVE_CALL) {
|
||
setCallState(CallState.ACTIVE);
|
||
}
|
||
}, []);
|
||
|
||
const generateSessionKeys = () => {
|
||
const sessionKeys = nacl.box.keyPair();
|
||
info("Generated keys for call session, len: " + sessionKeys.publicKey.length);
|
||
setSessionKeys(sessionKeys);
|
||
return sessionKeys;
|
||
}
|
||
|
||
const call = (dialog: string) => {
|
||
setActiveCall(dialog);
|
||
setCallState(CallState.CONNECTING);
|
||
setShowCallView(true);
|
||
const signalPacket = new PacketSignal();
|
||
signalPacket.setDst(dialog);
|
||
signalPacket.setSignalType(SignalType.CALL);
|
||
send(signalPacket);
|
||
roleRef.current = CallRole.CALLER;
|
||
}
|
||
|
||
const close = () => {
|
||
setActiveCall("");
|
||
setCallState(CallState.ENDED);
|
||
setShowCallView(false);
|
||
setSessionKeys(null);
|
||
setDuration(0);
|
||
roleRef.current = null;
|
||
}
|
||
|
||
const accept = () => {
|
||
if(callState != CallState.INCOMING){
|
||
/**
|
||
* Нечего принимать
|
||
*/
|
||
return;
|
||
}
|
||
/**
|
||
* Звонок принят, генерируем ключи для сессии и отправляем их другой стороне для установления защищенного канала связи
|
||
*/
|
||
const keys = generateSessionKeys();
|
||
const signalPacket = new PacketSignal();
|
||
signalPacket.setDst(activeCall);
|
||
signalPacket.setSignalType(SignalType.KEY_EXCHANGE);
|
||
signalPacket.setSharedPublic(Buffer.from(keys.publicKey).toString('hex'));
|
||
send(signalPacket);
|
||
setCallState(CallState.KEY_EXCHANGE);
|
||
roleRef.current = CallRole.CALLEE;
|
||
}
|
||
|
||
/**
|
||
* Получает слепок ключа для отображения в UI
|
||
* чтобы не показывать настоящий ключ
|
||
* @returns
|
||
*/
|
||
const getKeyCast = () => {
|
||
if(!sessionKeys){
|
||
return "";
|
||
}
|
||
return Buffer.from(sessionKeys.secretKey).toString('hex');
|
||
}
|
||
|
||
const context = {
|
||
call,
|
||
close,
|
||
activeCall,
|
||
callState,
|
||
muted,
|
||
sound,
|
||
setMuted,
|
||
setSound,
|
||
duration,
|
||
setShowCallView,
|
||
getKeyCast,
|
||
accept
|
||
};
|
||
|
||
return (
|
||
<CallContext.Provider value={context}>
|
||
{props.children}
|
||
{showCallView && <Call context={context}></Call>}
|
||
</CallContext.Provider>
|
||
)
|
||
} |