All files / lib/icloud/mfa mfa-server.ts

100% Statements 167/167
100% Branches 23/23
100% Functions 8/8
100% Lines 167/167

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 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 1671x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 172x 172x 172x 172x 172x 172x 172x 172x 172x 172x 172x 172x 172x 172x 172x 172x 172x 172x 172x 172x 172x 172x 172x 172x 172x 172x 172x 172x 172x 172x 172x 172x 172x 172x 172x 172x 172x 172x 172x 2x 1x 1x 1x 1x 2x 2x 172x 172x 172x 172x 172x 172x 172x 5x 1x 1x 1x 4x 5x 1x 1x 1x 1x 1x 1x 1x 3x 5x 1x 5x 1x 1x 1x 1x 1x 1x 1x 1x 5x 172x 172x 172x 172x 172x 172x 172x 2x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 172x 172x 172x 172x 172x 172x 172x 8x 8x 1x 1x 1x 1x 1x 7x 7x 7x 7x 7x 8x 2x 8x 5x 5x 7x 7x 7x 7x 172x 172x 172x 172x 172x 172x 172x 172x 1x 1x 1x 172x 172x 172x 172x 172x 2x 2x 2x 2x 2x 2x 172x
import EventEmitter from 'events';
import http from 'http';
import * as MFA_SERVER from './constants.js';
import {getLogger} from '../../logger.js';
import {MFAMethod} from './mfa-method.js';
import * as PACKAGE from '../../package.js';
import {HANDLER_EVENT} from '../../../app/event/error-handler.js';
import {iCPSError} from '../../../app/error/error.js';
import {MFA_ERR} from '../../../app/error/error-codes.js';
 
/**
 * This objects starts a server, that will listen to incoming MFA codes and other MFA related commands
 * todo - Implement re-request of MFA code
 */
export class MFAServer extends EventEmitter {
    /**
     * Default logger for this class
     */
    private logger = getLogger(this);
 
    /**
     * The server object
     */
    server: http.Server;
 
    /**
     * Port to start server on
     */
    port: number;
 
    /**
     * Holds the MFA method used for this server
     */
    mfaMethod: MFAMethod;
 
    /**
     * Creates the server object
     * @param port - The port to listen on, defaults to 80
     */
    constructor(port: number = 80) {
        super();
        this.port = port;
 
        this.logger.debug(`Preparing MFA server on port ${this.port}`);
        this.server = http.createServer(this.handleRequest.bind(this));
 
        // Default MFA request always goes to device
        this.mfaMethod = new MFAMethod();
    }
 
    /**
     * Starts the server and listens for incoming requests to perform MFA actions
     */
    startServer() {
        this.server.listen(this.port, () => {
            /* c8 ignore start */
            // Never starting the server just to see logger message
            this.logger.info(`Exposing endpoints: ${JSON.stringify(Object.values(MFA_SERVER.ENDPOINT))}`);
            /* c8 ignore stop */
        });
    }
 
    /**
     * Handles incoming http requests
     * @param req - The HTTP request object
     * @param res - The HTTP response object
     */
    handleRequest(req: http.IncomingMessage, res: http.ServerResponse) {
        if (req.method === `GET` && req.url === `/`) {
            this.sendResponse(res, 200, `MFA Server up & running - ${PACKAGE.NAME}@v${PACKAGE.VERSION}`);
            return;
        }
 
        if (req.method !== `POST`) {
            this.emit(HANDLER_EVENT, new iCPSError(MFA_ERR.METHOD_NOT_FOUND)
                .setWarning()
                .addMessage(`endpoint ${req.url}, method ${req.method}`)
                .addContext(`request`, req));
            this.sendResponse(res, 400, `Method not supported: ${req.method}`);
            return;
        }
 
        if (req.url.startsWith(MFA_SERVER.ENDPOINT.CODE_INPUT)) {
            this.handleMFACode(req, res);
        } else if (req.url.startsWith(MFA_SERVER.ENDPOINT.RESEND_CODE)) {
            this.handleMFAResend(req, res);
        } else {
            this.emit(HANDLER_EVENT, new iCPSError(MFA_ERR.ROUTE_NOT_FOUND)
                .addMessage(req.url)
                .setWarning()
                .addContext(`request`, req));
            this.sendResponse(res, 404, `Route not found, available endpoints: ${JSON.stringify(Object.values(MFA_SERVER.ENDPOINT))}`);
        }
    }
 
    /**
     * This function will handle requests send to the MFA Code Input Endpoint
     * @param req - The HTTP request object
     * @param res - The HTTP response object
     */
    handleMFACode(req: http.IncomingMessage, res: http.ServerResponse) {
        if (!req.url.match(/\?code=\d{6}$/)) {
            this.emit(HANDLER_EVENT, new iCPSError(MFA_ERR.CODE_FORMAT)
                .addMessage(req.url)
                .setWarning()
                .addContext(`request`, req));
            this.sendResponse(res, 400, `Unexpected MFA code format! Expecting 6 digits`);
            return;
        }
 
        const mfa: string = req.url.slice(-6);
 
        this.logger.debug(`Received MFA: ${mfa}`);
        this.sendResponse(res, 200, `Read MFA code: ${mfa}`);
        this.emit(MFA_SERVER.EVENTS.MFA_RECEIVED, this.mfaMethod, mfa);
    }
 
    /**
     * This function will handle the request send to the MFA Code Resend Endpoint
     * @param req - The HTTP request object
     * @param res - The HTTP response object
     */
    handleMFAResend(req: http.IncomingMessage, res: http.ServerResponse) {
        const methodMatch = req.url.match(/method=(?:sms|voice|device)/);
        if (!methodMatch) {
            this.sendResponse(res, 400, `Resend method does not match expected format`);
            this.emit(HANDLER_EVENT, new iCPSError(MFA_ERR.RESEND_METHOD_FORMAT)
                .addContext(`requestURL`, req.url));
            return;
        }
 
        const methodString = methodMatch[0].slice(7);
 
        const phoneNumberIdMatch = req.url.match(/phoneNumberId=\d+/);
 
        if (phoneNumberIdMatch && methodString !== `device`) {
            this.mfaMethod.update(methodString, parseInt(phoneNumberIdMatch[0].slice(14), 10));
        } else {
            this.mfaMethod.update(methodString);
        }
 
        this.sendResponse(res, 200, `Requesting MFA resend with method ${this.mfaMethod}`);
        this.emit(MFA_SERVER.EVENTS.MFA_RESEND, this.mfaMethod);
    }
 
    /**
     * This function will send a response, based on its input variables
     * @param res - The response object, to send the response to
     * @param code - The status code for the response
     * @param msg - The message included in the response
     */
    sendResponse(res: http.ServerResponse, code: number, msg: string) {
        res.writeHead(code, {"Content-Type": `application/json`});
        res.end(JSON.stringify({"message": msg}));
    }
 
    /**
     * Stops the server
     */
    stopServer() {
        this.logger.debug(`Stopping server`);
        if (this.server) {
            this.server.close();
            this.server = undefined;
        }
    }
}