Skip to content

Appendix - Signing a POST request sent to the RGS

import { Headers } from 'node-fetch';
import fetch from 'node-fetch';
import { v4 as uuidV4 } from 'uuid';
import * as crypto from 'crypto';

import { V2_GameAPIReply } from '@hoelle/apiv2-definitions/lib/v2_interfaces';

enum HTTP_CUSTOMHEADERS {
  XHEADERID = 'X-Response-Id',
  XAUTHID = 'X-H-AUTH-ID',
  XAUTHSIG = 'X-H-AUTH-SIG',
  XTIMESTAMP = 'X-H-TIMESTAMP',
  XFORWARDEDHEADERID = 'X-Forwarded-Response-Id',
}

type RecordData = Record<string, unknown>;
type CustomHeaders = Record<string, string | Date>;

interface GameResponseData {
  status: number;
  reply: V2_GameAPIReply;
  headers: CustomHeaders;
}

// eslint-disable-next-line @typescript-eslint/no-explicit-any
export async function sendSignedV2Request(
  gameProviderId: string,
  gameProviderSecret: string,
  url: string,
  requestId?: string,
  body?: RecordData,
  headers?: CustomHeaders,
): Promise<GameResponseData> {
  const customHeaders: CustomHeaders = headers ?? {} as CustomHeaders;
  setHeaderField(customHeaders, HTTP_CUSTOMHEADERS.XHEADERID, uuidV4());
  setHeaderField(customHeaders, HTTP_CUSTOMHEADERS.XAUTHID, gameProviderId);
  setHeaderField(customHeaders, HTTP_CUSTOMHEADERS.XTIMESTAMP, (new Date()).toISOString());
  if (requestId) {
    setHeaderField(customHeaders, HTTP_CUSTOMHEADERS.XFORWARDEDHEADERID, requestId);
  }
  setHeaderField(customHeaders, HTTP_CUSTOMHEADERS.XAUTHSIG, calcSignature(
    gameProviderSecret,
    [
      JSON.stringify(body),
      getHeaderField(customHeaders, HTTP_CUSTOMHEADERS.XTIMESTAMP),
    ],
    '#',
  ));
  try {
    if (
      url.toLowerCase().startsWith('http://')||
      url.toLowerCase().startsWith('https://')
    ) {
      setHeaderField(customHeaders, 'Content-Type', 'application/json');
      setHeaderField(customHeaders, 'accept', 'application/json');
      const rawFetchResponse = await fetch(
        url,
        {
          method: 'POST',
          body: JSON.stringify(body),
          headers: customHeaders as Record<string, string>,
        },
      );
      const apiResponse = await rawFetchResponse.json();
      const responseHeaders: CustomHeaders = mapHttpHeadersToQueueHeaders(rawFetchResponse.headers as unknown as Headers) as CustomHeaders;
      const gameResponse: Record<string, unknown> = {
        headers: responseHeaders,
        status: (rawFetchResponse && rawFetchResponse.status) ? rawFetchResponse.status : 500,
        reply: apiResponse,
      };
      console.log('sent signed request via http', JSON.stringify({
        url: url,
        body: body,
        headers: customHeaders,
        response: gameResponse,
        requestId: requestId,
      }, null, 2));
      return gameResponse as unknown as GameResponseData;
    } else {
      console.error('invalid url provided for sending a signed V2 request', {
        url,
      });
      throw new Error('invalid url provided for sending a signed V2 request');
    }
  } catch (error) {
    console.error('error in processing a signed V2 request', {
      error,
    });
    throw(error);
  }
}

function calcSignature(secretToUse: string, fieldsToSign: Array<string | Date>, separatorToUse = '#'): string {
  // check if we got an array to sign
  const hmacGenerated: crypto.Hmac = crypto.createHmac('sha256', secretToUse);
  const fieldsToSignArray: Array<unknown> =
    (fieldsToSign instanceof Array) ?
      fieldsToSign :
      Object.values(fieldsToSign);
  // convert any date objects passed to an ISO String
  const fieldsToSignArrayConverted: Array<string | Date | unknown> = fieldsToSignArray.map(function(e: Date | string | unknown) {
    if ((e instanceof Date)&&(typeof e.toISOString === 'function')) return e.toISOString();
    return e;
  });
  hmacGenerated.update(fieldsToSignArrayConverted.join(separatorToUse));
  return hmacGenerated.digest('base64');
}

function setHeaderField(headers: CustomHeaders, key: string, value: string | Date): void {
  headers[key] = value;
}

function getHeaderField(headers: CustomHeaders, key: string): string | Date {
  return headers[key];
}

function mapHttpHeadersToQueueHeaders(httpHeaders: Headers): CustomHeaders {
  const headersToReturn: CustomHeaders = {};
  const allowedHeaders: string[] = Object.values(HTTP_CUSTOMHEADERS);
  for (const allowedHeader of allowedHeaders) {
    if (httpHeaders.has(allowedHeader)) {
      setHeaderField(headersToReturn, allowedHeader, httpHeaders.get(allowedHeader) as string);
    }
  }
  return headersToReturn;
}