@datadome/node-core
Version:
Core package for server-side modules using Node.js
257 lines (219 loc) • 9.28 kB
JavaScript
/**
* Core functions for DataDome modules.
* @module @datadome/node-core/core
*/
/// <reference path="./_typedefs.js" />
const querystring = require('querystring');
const url = require('url');
const utils = require('./utils.js');
const https = require('https');
const ENDPOINT_PATH = '/validate-request';
/**
* @returns {DataDomeParameters} default module and connection parameters
*/
function getModuleDefaults() {
return {
enableGraphQLSupport: false,
endpointHost: 'api.datadome.co',
urlPatternInclusion: null,
urlPatternExclusion:
/\.(avi|flv|mka|mkv|mov|mp4|mpeg|mpg|mp3|flac|ogg|ogm|opus|wav|webm|webp|bmp|gif|ico|jpeg|jpg|png|svg|svgz|swf|eot|otf|ttf|woff|woff2|css|less|js|map)$/,
logger: console,
maximumBodySize: 25 * 1024,
// Connection parameters.
maxSockets: Infinity,
shouldUseSsl: true,
timeout: 150,
};
}
module.exports = class DataDomeCore {
/**
* @param {string} serverSideKey - Server-side API key.
* @param {DataDomeParameters} parameters - Parameters and optional overrides.
*/
constructor(serverSideKey, parameters = {}) {
const finalParameters = Object.assign({}, getModuleDefaults(), parameters);
const {
moduleName,
moduleVersion,
enableGraphQLSupport,
endpointHost,
urlPatternInclusion,
urlPatternExclusion,
maxSockets,
maximumBodySize,
timeout,
logger,
} = finalParameters;
if (serverSideKey == null || serverSideKey === '') {
throw new Error('Missing API key');
}
if (moduleName == null || moduleName === '') {
throw new Error('Missing module name');
}
if (moduleVersion == null || moduleVersion === '') {
throw new Error('Missing module version');
}
this.serverSideKey = serverSideKey;
this.moduleName = moduleName;
this.moduleVersion = moduleVersion;
this.protocol = https;
this.agent = new this.protocol.Agent({
keepAlive: true,
maxSockets,
timeout,
});
this.requestOptions = {
agent: this.agent,
headers: {
Connection: 'keep-alive',
'Content-Type': 'application/x-www-form-urlencoded',
'User-Agent': 'DataDome',
},
host: endpointHost,
path: ENDPOINT_PATH,
method: 'POST',
timeout,
};
this.urlPatternInclusion = urlPatternInclusion ? urlPatternInclusion : null;
this.urlPatternExclusion = urlPatternExclusion ? urlPatternExclusion : null;
this.logger = logger;
this.enableGraphQLSupport = enableGraphQLSupport;
this.maximumBodySize = maximumBodySize;
}
/**
* See https://docs.datadome.co/reference/validate-request for details.
* @async
* @param {IncomingMessage|null} request - An HTTP request to pick the data from.
* @param {MetadataHandlers|null} [handlers] - Functions to execute to get a specific payload value.
* @returns {Promise<RequestMetadata>} An object gathering all the metadata required by the API server for validation.
*/
async getRequestMetadata(request, handlers = {}) {
const parameters = {
enableGraphQLSupport: this.enableGraphQLSupport,
logger: this.logger,
maximumBodySize: this.maximumBodySize,
moduleName: this.moduleName,
moduleVersion: this.moduleVersion,
serverSideKey: this.serverSideKey,
};
return utils.getRequestMetadata(request, parameters, handlers);
}
/**
* Send a request to the API server for validation.
* @param {RequestMetadata} metadata - Properties of the request to be validated by the API server.
* @param {ValidationParameters} parameters - Parameters and optional overrides.
* @returns {Promise<ValidationResult>}
*/
validateRequest(metadata, parameters) {
let { request, response, headers, nonce } = parameters;
const defaultResponse = { ok: true, apiResponse: null, request, response };
const parsedUrl = url.parse(request.url);
const uri = parsedUrl.host + parsedUrl.pathname;
if (this.urlPatternExclusion !== null) {
if (this.urlPatternExclusion.test(uri) !== false) {
return Promise.resolve(defaultResponse);
}
}
if (this.urlPatternInclusion !== null) {
if (this.urlPatternInclusion.test(uri) === false) {
return Promise.resolve(defaultResponse);
}
}
const postData = querystring.stringify(metadata);
const requestOptions = Object.assign({}, this.requestOptions, {
headers: Object.assign(
request.headers['x-datadome-clientid'] != null
? { 'X-DataDome-X-Set-Cookie': 'true' }
: {},
this.requestOptions.headers,
headers ? headers : {}
),
});
let finished = false;
return new Promise((resolve) => {
this.protocol
.request(requestOptions, (apiResponse) => {
apiResponse.setEncoding('utf8');
let body = '';
apiResponse.on('data', (chunk) => {
body = body + chunk;
});
apiResponse.on('error', (err) => {
if (finished) {
return;
}
finished = true;
this.logger.error(
`DataDome: Error retrieving a response from the DataDome API, request skipped (${err.message})`
);
resolve(defaultResponse);
});
apiResponse.on('end', () => {
if (finished) {
return;
}
finished = true;
if (apiResponse.headers['x-datadomeresponse'] != apiResponse.statusCode) {
this.logger.error(
`DataDome: Invalid X-DataDomeResponse header in a response from the DataDome API (actual: ${apiResponse.headers['x-datadomeresponse']}, expected: ${apiResponse.statusCode})`
);
resolve(defaultResponse);
return;
}
switch (apiResponse.statusCode) {
case 301:
case 302:
case 403:
case 401: {
response = utils.mergeDataDomeResponseHeaders(
apiResponse,
response
);
response.statusCode = apiResponse.statusCode;
if (nonce) {
body = utils.addNonceToResponseBody(body, nonce);
}
response.end(body);
resolve({ ok: false, apiResponse, request, response });
return;
}
case 200: {
response = utils.mergeDataDomeResponseHeaders(
apiResponse,
response
);
request = utils.mergeDataDomeRequestHeaders(apiResponse, request);
resolve({ ok: true, apiResponse, request, response });
return;
}
default:
resolve(defaultResponse);
return;
}
});
})
.setTimeout(this.requestOptions.timeout, () => {
if (finished) {
return;
}
finished = true;
this.logger.error(
`DataDome: The connection to the DataDome API timed out after ${this.requestOptions.timeout} ms, request skipped`
);
resolve(defaultResponse);
})
.on('error', (err) => {
if (finished) {
return;
}
finished = true;
this.logger.error(
`DataDome: Error establishing a connection to the DataDome API, request skipped (${err.message})`
);
resolve(defaultResponse);
})
.end(postData);
});
}
};