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;
}