UNPKG

@datadome/node-core

Version:

Core package for server-side modules using Node.js

257 lines (219 loc) 9.28 kB
/** * 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); }); } };