Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Remote config initial commit - fetchConfig, add risk parameter and in… #300

Merged
merged 10 commits into from
Jan 28, 2024
2 changes: 1 addition & 1 deletion index.js
Original file line number Diff line number Diff line change
Expand Up @@ -29,4 +29,4 @@ module.exports = {
PxCdEnforcer: require('./lib/pxcdenforcer'),
PxCdFirstParty: require('./lib/pxcdfirstparty'),
addNonce: require('./lib/nonce')
};
};
2 changes: 1 addition & 1 deletion lib/enums/CIVersion.js
Original file line number Diff line number Diff line change
Expand Up @@ -6,4 +6,4 @@ const CIVersion = {

module.exports = {
CIVersion
};
};
4 changes: 4 additions & 0 deletions lib/enums/ErrorType.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
const ErrorType = {
AsafAklerPX marked this conversation as resolved.
Show resolved Hide resolved
WRITE_REMOTE_CONFIG: 'write_remote_config',
};
module.exports = { ErrorType };
10 changes: 9 additions & 1 deletion lib/pxapi.js
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
'use strict';
const pxUtil = require('./pxutil');
const pxHttpc = require('./pxhttpc');

const os = require('os');
const S2SErrorInfo = require('./models/S2SErrorInfo');
const { ModuleMode } = require('./enums/ModuleMode');
const PassReason = require('./enums/PassReason');
Expand Down Expand Up @@ -66,9 +66,13 @@ function buildRequestData(ctx, config) {
cookie_origin: ctx.cookieOrigin,
request_cookie_names: ctx.requestCookieNames,
request_id: ctx.requestId,
hostname: os.hostname()
AsafAklerPX marked this conversation as resolved.
Show resolved Hide resolved
},
};

if (config.REMOTE_CONFIG_ENABLED && config.REMOTE_CONFIG_ID) {
data.additional.px_remote_config_id = config.REMOTE_CONFIG_ID;
}
if (ctx.graphqlData) {
data.additional[GQL_OPERATIONS_FIELD] = ctx.graphqlData;
}
Expand Down Expand Up @@ -190,6 +194,10 @@ function evalByServerCall(ctx, config, callback) {
return callback(ScoreEvaluateAction.UNEXPECTED_RESULT);
}
ctx.pxhdServer = res.pxhd;
if (config.REMOTE_CONFIG_ENABLED && res.remote_config && res.remote_config.id === config.REMOTE_CONFIG_ID) {
ctx.remoteConfigLatestVersion = res.remote_config.version;
}

if (res.data_enrichment) {
ctx.pxde = res.data_enrichment;
ctx.pxdeVerified = true;
Expand Down
86 changes: 83 additions & 3 deletions lib/pxclient.js
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,11 @@ const pxUtil = require('./pxutil');
const pxHttpc = require('./pxhttpc');
const { ActivityType } = require('./enums/ActivityType');
const { CIVersion } = require('./enums/CIVersion');
const { makeAsyncRequest } = require('./request');
const { LoggerSeverity } = require('./enums/LoggerSeverity');
const { PxExternalLogsParser } = require('./pxexternallogparser');
const PxLogger = require('./pxlogger');
const PxConfig = require('./pxconfig');
const {
CI_VERSION_FIELD,
CI_SSO_STEP_FIELD,
Expand All @@ -12,18 +17,26 @@ const {
GQL_OPERATIONS_FIELD,
APP_USER_ID_FIELD_NAME,
JWT_ADDITIONAL_FIELDS_FIELD_NAME,
CROSS_TAB_SESSION,
CROSS_TAB_SESSION, HOST_NAME, EXTERNAL_LOGGER_SERVICE_PATH, INVALID_VERSION_NUMBER,
} = require('./utils/constants');
const { ErrorType } = require('./enums/ErrorType');

class PxClient {
constructor() {
this.activitiesBuffer = [];
this._remoteConfigLatestVersion = INVALID_VERSION_NUMBER;
}

init() {
//stub for overriding
}
get remoteConfigLatestVersion() {
return this._remoteConfigLatestVersion;
}

set remoteConfigLatestVersion(value) {
this._remoteConfigLatestVersion = value;
}
/**
* generateActivity - returns a JSON representing the activity.
* @param {string} activityType - name of the activity
Expand All @@ -43,7 +56,7 @@ class PxClient {
};
details['request_id'] = ctx.requestId;

this.addAdditionalFieldsToActivity(details, ctx);
this.addAdditionalFieldsToActivity(details, ctx, config);
if (activityType !== ActivityType.ADDITIONAL_S2S) {
activity.headers = pxUtil.formatHeaders(ctx.headers, config.SENSITIVE_HEADERS);
activity.pxhd = (ctx.pxhdServer ? ctx.pxhdServer : ctx.pxhdClient) || undefined;
Expand All @@ -58,7 +71,47 @@ class PxClient {
return activity;
}

addAdditionalFieldsToActivity(details, ctx) {
async fetchRemoteConfig(config) {
const maxRetries = 5;
for (let i = 0; i < maxRetries; i++) {
AsafAklerPX marked this conversation as resolved.
Show resolved Hide resolved
try {

const remoteConfigObject = await this.getRemoteConfigObject(config);
return {
px_remote_config_id: remoteConfigObject.id,
px_remote_config_version: remoteConfigObject.version,
...remoteConfigObject.configValue
};
} catch (e) {
const message = `Error fetching remote configurations: ${e.message}`;
this.sendRemoteLog(message, LoggerSeverity.DEBUG, ErrorType.WRITE_REMOTE_CONFIG, config);
if (i < maxRetries - 1) { // if it's not the last retry
await new Promise(resolve => setTimeout(resolve, 1000)); // wait for 1 second before retrying
} else {
config.logger.error('Failed to fetch remote configuration after 5 attempts');
}
}
}
}

async getRemoteConfigObject(config) {
const callData = {
url: `https://sapi-${config.px_app_id}.perimeterx.net/config/`,
headers: { 'Authorization': `Bearer ${config.px_remote_config_secret}`, 'Accept-Encoding': '' },
timeout: 20000,
};
const res = await makeAsyncRequest({ url: callData.url, headers: callData.headers, timeout: callData.timeout, method: 'GET' }, config);
const remoteConfigObject = JSON.parse(res.body);
if (remoteConfigObject.id !== config.px_remote_config_id) {
throw new Error(`Remote configuration id mismatch. Expected: ${config.px_remote_config_id}, Actual: ${remoteConfigObject.id}`);
}
if (this._remoteConfigLatestVersion !== INVALID_VERSION_NUMBER && remoteConfigObject.version !== this._remoteConfigLatestVersion) {
throw new Error(`Remote configuration version mismatch. Expected: ${this._remoteConfigLatestVersion}, Actual: ${remoteConfigObject.version}`);
}
return remoteConfigObject;
}

addAdditionalFieldsToActivity(details, ctx, config) {
if (ctx.additionalFields && ctx.additionalFields.loginCredentials) {
const { loginCredentials } = ctx.additionalFields;
details[CI_VERSION_FIELD] = loginCredentials.version;
Expand All @@ -84,6 +137,12 @@ class PxClient {
}
}

if (config.remoteConfigVersion !== INVALID_VERSION_NUMBER) {
details['px_remote_config_version'] = config.remoteConfigVersion;
}

details[HOST_NAME] = os.hostname();
AsafAklerPX marked this conversation as resolved.
Show resolved Hide resolved

if (ctx.cts) {
details[CROSS_TAB_SESSION] = ctx.cts;
}
Expand Down Expand Up @@ -182,5 +241,26 @@ class PxClient {
cb();
}
}

sendRemoteLog(message, severity, errorType, config) {
const pxLogger = config.logger ? config.logger : new PxLogger(config);
const enforcerConfig = new PxConfig(config, pxLogger);
const reqHeaders = {
'Authorization': 'Bearer ' + enforcerConfig.config.LOGGER_AUTH_TOKEN,
'Content-Type': 'application/json',
};
const logParser = new PxExternalLogsParser( { appId: config.PX_APP_ID, remoteConfigId: config.REMOTE_CONFIG_ID, remoteConfigVersion: config.REMOTE_CONFIG_VERSION });
const logs = [{ message, severity, errorType }];
const enrichedLogs = logParser.enrichLogs(logs);
pxHttpc.callServer(
enrichedLogs,
reqHeaders,
EXTERNAL_LOGGER_SERVICE_PATH,
'remote-log',
enforcerConfig.conf,
null,
false,
);
}
}
module.exports = PxClient;
17 changes: 13 additions & 4 deletions lib/pxconfig.js
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@ const { LoggerSeverity } = require('./enums/LoggerSeverity');
const { DEFAULT_COMPROMISED_CREDENTIALS_HEADER_NAME } = require('./utils/constants');
const { CIVersion } = require('./enums/CIVersion');
const { LoginSuccessfulReportingMethod } = require('./enums/LoginSuccessfulReportingMethod');

const { INVALID_VERSION_NUMBER } = require('./utils/constants');
class PxConfig {
constructor(params, logger) {
this.PX_INTERNAL = pxInternalConfig();
Expand All @@ -16,7 +16,6 @@ class PxConfig {
this.config = this.mergeParams(params);
this.config.FILTER_BY_METHOD = this.config.FILTER_BY_METHOD.map((v) => v.toUpperCase());
this.config.logger = this.logger;

this.config.WHITELIST_EXT = [...this.PX_INTERNAL.STATIC_FILES_EXT, ...this.PX_DEFAULT.WHITELIST_EXT];

if (this.PX_DEFAULT.TESTING_MODE) {
Expand Down Expand Up @@ -104,7 +103,12 @@ class PxConfig {
['JWT_HEADER_ADDITIONAL_FIELD_NAMES', 'px_jwt_header_additional_field_names'],
['CUSTOM_IS_SENSITIVE_REQUEST', 'px_custom_is_sensitive_request'],
['FIRST_PARTY_TIMEOUT_MS', 'px_first_party_timeout_ms'],
['URL_DECODE_RESERVED_CHARACTERS', 'px_url_decode_reserved_characters']
['URL_DECODE_RESERVED_CHARACTERS', 'px_url_decode_reserved_characters'],
['REMOTE_CONFIG_ENABLED', 'px_remote_config_enabled'],
['REMOTE_CONFIG_AUTH_TOKEN', 'px_remote_config_auth_token'],
['REMOTE_CONFIG_ID', 'px_remote_config_id'],
['REMOTE_CONFIG_VERSION', 'px_remote_config_version'],
['LOGGER_AUTH_TOKEN', 'px_logger_auth_token']
];

configKeyMapping.forEach(([targetKey, sourceKey]) => {
Expand Down Expand Up @@ -365,7 +369,12 @@ function pxDefaultConfig() {
JWT_HEADER_ADDITIONAL_FIELD_NAMES: [],
CUSTOM_IS_SENSITIVE_REQUEST: '',
FIRST_PARTY_TIMEOUT_MS: 4000,
URL_DECODE_RESERVED_CHARACTERS: false
URL_DECODE_RESERVED_CHARACTERS: false,
REMOTE_CONFIG_ENABLED: false,
REMOTE_CONFIG_AUTH_TOKEN: '',
REMOTE_CONFIG_ID: '',
REMOTE_CONFIG_VERSION: INVALID_VERSION_NUMBER,
LOGGER_AUTH_TOKEN: ''
};
}

Expand Down
7 changes: 5 additions & 2 deletions lib/pxenforcer.js
Original file line number Diff line number Diff line change
Expand Up @@ -261,6 +261,10 @@ class PxEnforcer {
pxApi.evalByServerCall(ctx, this._config, (action) => {
ctx.riskRtt = Date.now() - startRiskRtt;

if (this.config.config.REMOTE_CONFIG_ENABLED) {
this.pxClient.remoteConfigLatestVersion = ctx.remoteConfigLatestVersion;
}

if (action === ScoreEvaluateAction.UNEXPECTED_RESULT) {
this.logger.debug('perimeterx score evaluation failed. unexpected error. passing traffic');
return callback(ScoreEvaluateAction.S2S_PASS_TRAFFIC);
Expand Down Expand Up @@ -308,7 +312,6 @@ class PxEnforcer {

handleVerification(ctx, req, res, cb) {
const verified = ctx.score < this._config.BLOCKING_SCORE;

if (res) {
const setCookie = res.getHeader('Set-Cookie') ? res.getHeader('Set-Cookie') : '';
const secure = this._config.PXHD_SECURE ? '; Secure' : '';
Expand Down Expand Up @@ -639,7 +642,7 @@ class PxEnforcer {
cb(htmlTemplate);
});
}

sendHeaderBasedLogs(pxCtx, config, req) { // eslint-disable-line
// Feature has been removed, function definition for backwards compatibility.
}
Expand Down
25 changes: 25 additions & 0 deletions lib/pxexternallogparser.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
class PxExternalLogsParser {
constructor(appId, remoteConfigId, remoteConfigVersion) {
this.appId = appId;
this.remoteConfigId = remoteConfigId;
this.remoteConfigVersion = remoteConfigVersion;
}
enrichLogs(logs) {
const enrichedLogs = logs.map((log) => {
return this.enrichLogRecord(log);
});
return enrichedLogs;
}

enrichLogRecord(log) {
return {...log, ...{
messageTimestamp: new Date().toISOString(),
appID: this.appId,
container: 'enforcer',
configID: this.remoteConfigId,
configVersion: this.remoteConfigVersion
}};
}
}

module.exports = { PxExternalLogsParser };
3 changes: 3 additions & 0 deletions lib/pxhttpc.js
Original file line number Diff line number Diff line change
Expand Up @@ -33,6 +33,9 @@ function callServer(data, headers, uri, callType, config, callback, failOnEmptyB

try {
request.post(callData, config, function (err, response) {
if (callType === 'remote-log') {
AsafAklerPX marked this conversation as resolved.
Show resolved Hide resolved
return;
}
if (err) {
if (err.toString().toLowerCase().includes('timeout')) {
return callback('timeout');
Expand Down
12 changes: 11 additions & 1 deletion lib/request.js
Original file line number Diff line number Diff line change
Expand Up @@ -16,11 +16,21 @@ exports.post = (options, config, cb) => {
return makeRequest(options, config, cb);
};

exports.makeAsyncRequest = (options, config) => {
return new Promise((resolve, reject) => {
makeRequest(options, config, (err, res) => {
if (err) {
return reject(err);
}
return resolve(res);
});
});
};
function makeRequest(options, config, cb) {
if (options.url && options.url.startsWith('https://')) {
options.agent = config.agent || httpsKeepAliveAgent;
} else {
options.agent = new http.Agent();
}
p(options, cb);
}
}
7 changes: 7 additions & 0 deletions lib/utils/constants.js
Original file line number Diff line number Diff line change
Expand Up @@ -36,6 +36,10 @@ const JWT_ADDITIONAL_FIELDS_FIELD_NAME = 'jwt_additional_fields';
const CROSS_TAB_SESSION = 'cross_tab_session';
const COOKIE_SEPARATOR = ';';

const EXTERNAL_LOGGER_SERVICE_PATH = '/enforcer-logs/';
AsafAklerPX marked this conversation as resolved.
Show resolved Hide resolved
const INVALID_VERSION_NUMBER = -1;
const HOST_NAME = 'hostname';

module.exports = {
MILLISECONDS_IN_SECOND,
SECONDS_IN_MINUTE,
Expand Down Expand Up @@ -66,4 +70,7 @@ module.exports = {
JWT_ADDITIONAL_FIELDS_FIELD_NAME,
CROSS_TAB_SESSION,
COOKIE_SEPARATOR,
EXTERNAL_LOGGER_SERVICE_PATH,
INVALID_VERSION_NUMBER,
HOST_NAME
};
Loading