All files / src/services/handle call-handler.ts

100% Statements 36/36
100% Branches 12/12
100% Functions 7/7
100% Lines 36/36

Press n or j to go to the next uncovered block, b, p or k for the previous block.

1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134              20x                                                                                                       20x                 4x 4x 1x     1x   3x 3x       3x 3x 1x 1x   2x 2x       2x 2x 2x 2x 2x 2x 2x 1x 1x   1x 1x     100x   3x 2x 1x         1x             4x 1x   3x 1x   2x     1x        
import { CloseCallData } from '../../models/close-call-data';
import { CallData } from '../../models/infrasctructure/call-data';
import { CallPayload } from '../../models/infrasctructure/call-payload';
import { CallRequest } from '../../models/infrasctructure/call-request';
import { Base64 } from '../../utils/base64';
import { Cryptography } from '../../utils/cryptography';
import { Logger } from '../../utils/logger';
import { newError } from '../../utils/new-error';
import { Utf8 } from '../../utils/utf8';
import { SessionService } from '../session-service';
import { TimeService } from '../time-service';
 
/**
 * Generic interface for handling different types of call requests.
 * Provides methods for parsing, validating, and processing call requests.
 *
 * @template TypeData - The specific type of call data this handler processes
 */
export interface CallHandler<TypeData extends CallData> {
    /**
     * Parses a raw call payload into a typed call request.
     *
     * @param payloadData - The raw call payload to parse
     * @returns A properly typed call request object
     */
    parse(payloadData: CallPayload): CallRequest<TypeData>;
 
    /**
     * Validates a call request to ensure it is legitimate and intended for this recipient.
     * Performs timestamp validation, recipient validation, and signature verification.
     *
     * @param request - The call request to validate
     * @returns Boolean indicating if the request is valid
     */
    validate(request: CallRequest<TypeData>): boolean;
 
    /**
     * Processes a validated call request and performs the necessary actions.
     * This method must be implemented by specific call handlers.
     *
     * @param request - The validated call request to handle
     * @returns Promise resolving to a boolean indicating if handling was successful
     */
    handle(request: CallRequest<TypeData>): Promise<boolean>;
}
 
/**
 * Factory function that creates a base implementation of the CallHandler interface.
 * Provides common functionality for parsing and validating call requests.
 *
 * @template TypeData - The specific type of call data this handler processes
 * @param logger - Logger instance for error and debugging information
 * @param timeService - Service for managing time synchronization
 * @param sessionService - Service for accessing session information
 * @param base64 - Utility for Base64 encoding/decoding
 * @param utf8 - Utility for UTF-8 encoding/decoding
 * @param cryptography - Cryptography service for signature verification
 * @returns A partial implementation of the CallHandler interface
 */
export function getCallHandler<TypeData extends CloseCallData>(
    logger: Logger,
    timeService: TimeService,
    sessionService: SessionService,
    base64: Base64,
    utf8: Utf8,
    cryptography: Cryptography,
): CallHandler<TypeData> {
    function validateTimestamp(request: CallRequest<TypeData>): boolean {
        const delta = request.b.b - timeService.serverTime;
        if (Math.abs(delta) > 5 * 1000) {
            logger.debug(
                `[${request.a}-call-handler] Request timestamp is more than 5 seconds stale (delta ${delta}ms).`,
            );
            return false;
        }
        logger.debug(`[${request.a}-call-handler] Request timestamp is valid (delta ${delta}ms).`);
        return true;
    }
 
    function validatePublicKey(request: CallRequest<TypeData>): boolean {
        const isIntendedForMe = sessionService.signingPublicKeyBase64 === request.b.c;
        if (!isIntendedForMe) {
            logger.debug(`[${request.a}-call-handler] Message is not intended for this user.`);
            return false;
        }
        logger.debug(`[${request.a}-call-handler] Message is intended for this user.`);
        return true;
    }
 
    function validateSignature(request: CallRequest<TypeData>): boolean {
        const message = JSON.stringify(request.b);
        const messageBytes = utf8.decode(message);
        const signature = base64.decode(request.c);
        const singingPublicKeyBase64 = request.b.a;
        const signingPublicKey = base64.decode(singingPublicKeyBase64);
        const isSignatureVerified = cryptography.verifySignature(messageBytes, signature, signingPublicKey);
        if (!isSignatureVerified) {
            logger.debug(`[${request.a}-call-handler] Signature is not valid.`);
            return false;
        }
        logger.debug(`[${request.a}-call-handler] Signature is valid.`);
        return true;
    }
 
    return {
        parse(payloadData: CallPayload): CallRequest<TypeData> {
            const data = JSON.parse(payloadData.b) as TypeData;
            if (!data) {
                throw newError(
                    logger,
                    `[${payloadData.a}-call-handler] Unable to parse call data for call '${payloadData.a}'. Data: ${payloadData.b}`,
                );
            }
            return {
                a: payloadData.a,
                b: data,
                c: payloadData.c,
            };
        },
        validate(request: CallRequest<TypeData>): boolean {
            if (!validateTimestamp(request)) {
                return false;
            }
            if (!validatePublicKey(request)) {
                return false;
            }
            return validateSignature(request);
        },
        handle(request: CallRequest<TypeData>): Promise<boolean> {
            throw newError(logger, `[${request.a}-call-handler] Base handle method is not implemented.`);
        },
    };
}