NeuraLegion/sectester-js

View on GitHub
packages/repeater/src/lib/Repeater.ts

Summary

Maintainability
A
0 mins
Test Coverage
A
100%
import {
  RepeaterServer,
  RepeaterErrorCodes,
  RepeaterServerErrorEvent,
  RepeaterServerEvents,
  RepeaterServerReconnectionAttemptedEvent,
  RepeaterServerReconnectionFailedEvent,
  RepeaterServerRequestEvent,
  RepeaterUpgradeAvailableEvent
} from './RepeaterServer';
import { RepeaterCommands } from './RepeaterCommands';
import { Request } from '../request-runner/Request';
import { Logger } from '@sectester/core';
import chalk from 'chalk';
import { inject, injectable, Lifecycle, scoped } from 'tsyringe';

export enum RunningStatus {
  OFF,
  STARTING,
  RUNNING
}

export type RepeaterId = string;
export const RepeaterId = Symbol('RepeaterId');

@scoped(Lifecycle.ContainerScoped)
@injectable()
export class Repeater {
  private _runningStatus = RunningStatus.OFF;

  get runningStatus(): RunningStatus {
    return this._runningStatus;
  }

  constructor(
    @inject(RepeaterId)
    public readonly repeaterId: RepeaterId,
    private readonly logger: Logger,
    @inject(RepeaterServer)
    private readonly repeaterServer: RepeaterServer,
    @inject(RepeaterCommands)
    private readonly repeaterCommands: RepeaterCommands
  ) {}

  public async start(): Promise<void> {
    if (this.runningStatus !== RunningStatus.OFF) {
      throw new Error('Repeater is already active.');
    }

    this._runningStatus = RunningStatus.STARTING;

    try {
      await this.connect();

      this._runningStatus = RunningStatus.RUNNING;
    } catch (e) {
      this._runningStatus = RunningStatus.OFF;
      throw e;
    }
  }

  public async stop(): Promise<void> {
    if (this.runningStatus !== RunningStatus.RUNNING) {
      return;
    }

    this._runningStatus = RunningStatus.OFF;

    this.repeaterServer.disconnect();

    return Promise.resolve();
  }

  private async connect(): Promise<void> {
    this.logger.log('Connecting the Bridges');

    this.subscribeDiagnosticEvents();

    await this.repeaterServer.connect();

    this.logger.log('Deploying the repeater');

    await this.deploy();

    this.logger.log('The Repeater (%s) started', this.repeaterId);

    this.subscribeConnectedEvent();
  }

  private async deploy() {
    await this.repeaterServer.deploy({
      repeaterId: this.repeaterId
    });
  }

  private subscribeConnectedEvent() {
    this.repeaterServer.on(RepeaterServerEvents.CONNECTED, this.deploy);
  }

  private subscribeDiagnosticEvents() {
    this.repeaterServer.on(RepeaterServerEvents.ERROR, this.handleError);

    this.repeaterServer.on(
      RepeaterServerEvents.RECONNECTION_FAILED,
      this.reconnectionFailed
    );
    this.repeaterServer.on(RepeaterServerEvents.REQUEST, this.requestReceived);
    this.repeaterServer.on(
      RepeaterServerEvents.UPDATE_AVAILABLE,
      this.upgradeAvailable
    );
    this.repeaterServer.on(
      RepeaterServerEvents.RECONNECT_ATTEMPT,
      this.reconnectAttempt
    );
    this.repeaterServer.on(RepeaterServerEvents.RECONNECTION_SUCCEEDED, () =>
      this.logger.log('The Repeater (%s) connected', this.repeaterId)
    );
  }

  private handleError = ({
    code,
    message,
    remediation
  }: RepeaterServerErrorEvent) => {
    const normalizedMessage = this.normalizeMessage(message);
    const normalizedRemediation = this.normalizeMessage(remediation ?? '');

    if (this.isCriticalError(code)) {
      this.handleCriticalError(normalizedMessage, normalizedRemediation);
    } else {
      this.logger.error(normalizedMessage);
    }
  };

  private normalizeMessage(message: string): string {
    return message.replace(/\.$/, '');
  }

  private isCriticalError(code: RepeaterErrorCodes): boolean {
    return [
      RepeaterErrorCodes.REPEATER_DEACTIVATED,
      RepeaterErrorCodes.REPEATER_NO_LONGER_SUPPORTED,
      RepeaterErrorCodes.REPEATER_UNAUTHORIZED,
      RepeaterErrorCodes.REPEATER_ALREADY_STARTED,
      RepeaterErrorCodes.REPEATER_NOT_PERMITTED,
      RepeaterErrorCodes.UNEXPECTED_ERROR
    ].includes(code);
  }

  private handleCriticalError(message: string, remediation: string): void {
    this.logger.error(
      '%s: %s. %s',
      chalk.red('(!) CRITICAL'),
      message,
      remediation
    );
    this.stop().catch(this.logger.error);
  }

  private upgradeAvailable = (event: RepeaterUpgradeAvailableEvent) => {
    this.logger.warn(
      '%s: A new Repeater version (%s) is available, for update instruction visit https://docs.brightsec.com/docs/installation-options',
      chalk.yellow('(!) IMPORTANT'),
      event.version
    );
  };

  private reconnectAttempt = ({
    attempt,
    maxAttempts
  }: RepeaterServerReconnectionAttemptedEvent) => {
    this.logger.warn(
      'Failed to connect to Bright cloud (attempt %d/%d)',
      attempt,
      maxAttempts
    );
  };

  private reconnectionFailed = ({
    error
  }: RepeaterServerReconnectionFailedEvent) => {
    this.logger.error(error.message);
    this.stop().catch(this.logger.error);
  };

  private requestReceived = async (event: RepeaterServerRequestEvent) => {
    const response = await this.repeaterCommands.sendRequest(
      new Request({ ...event })
    );

    const {
      statusCode,
      message,
      errorCode,
      body,
      headers,
      protocol,
      encoding
    } = response;

    return {
      protocol,
      body,
      headers,
      statusCode,
      errorCode,
      message,
      encoding
    };
  };
}