Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 3 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -1,3 +1,6 @@
## 24.10.4
- Added a new init time flag `salt` for request tampering protection (should be used in tandem with server options)

## 24.10.3
- Added support for uploading user images by providing path to the local image using `picturePath` parameter in `user_details` method (non-bulk)
- Reduced SDK log verbosity
Expand Down
12 changes: 5 additions & 7 deletions lib/countly-bulk.js
Original file line number Diff line number Diff line change
Expand Up @@ -40,6 +40,7 @@ CountlyBulk.StorageTypes = cc.storageTypeEnums;
* @param {number} [conf.session_update=60] - how often in seconds should session be extended
* @param {number} [conf.max_events=100] - maximum amount of events to send in one batch
* @param {boolean} [conf.force_post=false] - force using post method for all requests
* @param {string} [conf.salt] - shared secret used to append checksum256 to outgoing requests
* @param {string} [conf.storage_path] - where SDK would store data, including id, queues, etc
* @param {string} [conf.http_options=] - function to get http options by reference and overwrite them, before running each request
* @param {number} [conf.max_key_length=128] - maximum size of all string keys
Expand All @@ -59,7 +60,7 @@ CountlyBulk.StorageTypes = cc.storageTypeEnums;
* });
*/
function CountlyBulk(conf) {
var SDK_VERSION = "24.10.3";
var SDK_VERSION = "24.10.4";
var SDK_NAME = "javascript_native_nodejs_bulk";

var empty_queue_callback = null;
Expand Down Expand Up @@ -96,6 +97,7 @@ function CountlyBulk(conf) {
conf.session_update = conf.session_update || 60;
conf.max_events = conf.max_events || 100;
conf.force_post = conf.force_post || false;
conf.salt = conf.salt || null;
conf.persist_queue = conf.persist_queue || false;
conf.http_options = conf.http_options || null;
conf.maxKeyLength = conf.max_key_length || maxKeyLength;
Expand Down Expand Up @@ -467,16 +469,12 @@ function CountlyBulk(conf) {
}

/**
* Convert JSON object to query params
* Convert JSON object to query params and append checksum when configured
* @param {Object} params - object with url params
* @returns {String} query string
*/
function prepareParams(params) {
var str = [];
for (var i in params) {
str.push(`${i}=${encodeURIComponent(params[i])}`);
}
return str.join("&");
return cc.addChecksum(cc.serializeParams(params), conf.salt, false, false);
}

/**
Expand Down
60 changes: 60 additions & 0 deletions lib/countly-common.js
Original file line number Diff line number Diff line change
@@ -1,6 +1,8 @@
/**
* main common functionalities will go in here
*/
var crypto = require("crypto");

var cc = {

// debug value from Countly
Expand Down Expand Up @@ -135,6 +137,64 @@ var cc = {
}
return ob;
},
/**
* Convert params object to URL encoded query parameter string
* @param {Object} params - object with query parameters
* @returns {String} URL encoded query string
*/
serializeParams: function serializeParams(params) {
var str = [];
var keys = Object.keys(params || {});
for (var i = 0; i < keys.length; i++) {
var key = keys[i];
str.push(`${key}=${encodeURIComponent(params[key])}`);
}
return str.join("&");
},
/**
* Calculate the SHA-256 checksum for provided request data and salt
* @param {String} data - serialized request data
* @param {String} salt - developer provided shared secret
* @param {Boolean} decodeBeforeHash - if true decodes serialized data before hashing
* @param {Boolean} uppercase - if true returns uppercase hex
* @returns {String} checksum in hex format
*/
calculateChecksum: function calculateChecksum(data, salt, decodeBeforeHash, uppercase) {
var checksumData = data || "";
if (decodeBeforeHash) {
try {
checksumData = decodeURIComponent(checksumData);
}
catch (e) {
this.log(this.logLevelEnums.WARNING, `calculateChecksum, Failed to decode request data before hashing: [${e}]`);
}
}
var hash = crypto.createHash("sha256");
hash.update(`${checksumData}${salt}`);
var checksum = hash.digest("hex");
if (uppercase) {
return checksum.toUpperCase();
}
return checksum;
},
/**
* Append checksum256 to serialized request data when salt is configured
* @param {String} data - serialized request data
* @param {String} salt - developer provided shared secret
* @param {Boolean} decodeBeforeHash - if true decodes serialized data before hashing
* @param {Boolean} uppercase - if true appends uppercase hex
* @returns {String} serialized request data with checksum when configured
*/
addChecksum: function addChecksum(data, salt, decodeBeforeHash, uppercase) {
if (!salt) {
return data;
}
var checksum = this.calculateChecksum(data, salt, decodeBeforeHash, uppercase);
if (!data) {
return `checksum256=${checksum}`;
}
return `${data}&checksum256=${checksum}`;
},
/**
* Removing trailing slashes
* @memberof Countly._internals
Expand Down
43 changes: 32 additions & 11 deletions lib/countly.js
Original file line number Diff line number Diff line change
Expand Up @@ -35,7 +35,7 @@ Countly.StorageTypes = cc.storageTypeEnums;
Countly.DeviceIdType = cc.deviceIdTypeEnums;
Countly.Bulk = Bulk;
(function() {
var SDK_VERSION = "24.10.3";
var SDK_VERSION = "24.10.4";
var SDK_NAME = "javascript_native_nodejs";

var inited = false;
Expand Down Expand Up @@ -103,6 +103,7 @@ Countly.Bulk = Bulk;
* @param {number} [conf.session_update=60] - how often in seconds should session be extended
* @param {number} [conf.max_events=100] - maximum amount of events to send in one batch
* @param {boolean} [conf.force_post=false] - force using post method for all requests
* @param {string} [conf.salt] - shared secret used to append checksum256 to outgoing requests
* @param {boolean} [conf.clear_stored_device_id=false] - set it to true if you want to erase the stored device ID
* @param {boolean} [conf.test_mode=false] - set it to true if you want to initiate test_mode
* @param {string} [conf.storage_path] - where SDK would store data, including id, queues, etc
Expand Down Expand Up @@ -162,6 +163,7 @@ Countly.Bulk = Bulk;
Countly.city = conf.city || Countly.city || null;
Countly.ip_address = conf.ip_address || Countly.ip_address || null;
Countly.force_post = conf.force_post || Countly.force_post || false;
Countly.salt = conf.salt || Countly.salt || null;
Countly.require_consent = conf.require_consent || Countly.require_consent || false;
Countly.remote_config = conf.remote_config || Countly.remote_config || false;
Countly.http_options = conf.http_options || Countly.http_options || null;
Expand Down Expand Up @@ -215,6 +217,7 @@ Countly.Bulk = Bulk;
cc.log(cc.logLevelEnums.DEBUG, `init, IP address: [${Countly.ip_address}].`);
}
cc.log(cc.logLevelEnums.DEBUG, `init, Force POST requests: [${Countly.force_post}].`);
cc.log(cc.logLevelEnums.DEBUG, `init, Salt is configured: [${!!Countly.salt}].`);
cc.log(cc.logLevelEnums.DEBUG, `init, Storage path: [${CountlyStorage.getStoragePath()}].`);
cc.log(cc.logLevelEnums.DEBUG, `init, Require consent: [${Countly.require_consent}].`);
if (Countly.remote_config) {
Expand Down Expand Up @@ -353,6 +356,7 @@ Countly.Bulk = Bulk;
Countly.city = undefined;
Countly.ip_address = undefined;
Countly.force_post = undefined;
Countly.salt = undefined;
Countly.require_consent = undefined;
Countly.http_options = undefined;
CountlyStorage.resetStorage();
Expand Down Expand Up @@ -1670,17 +1674,36 @@ Countly.Bulk = Bulk;

var boundary = `FormBoundary${Math.random().toString(16).slice(2)}`;
var bodyParts = [];
var uploadParams = {};

for (var p in params) {
if (typeof params[p] !== "undefined" && p !== 'picturePath') {
var value = params[p];
if (Object.prototype.hasOwnProperty.call(params, p) && typeof params[p] !== "undefined" && p !== "picturePath") {
uploadParams[p] = params[p];
}
}

var uploadChecksum = null;
if (Countly.salt) {
uploadChecksum = cc.calculateChecksum(cc.serializeParams(uploadParams), Countly.salt, true);
}

for (var param in uploadParams) {
if (Object.prototype.hasOwnProperty.call(uploadParams, param)) {
var value = uploadParams[param];
bodyParts.push(Buffer.from(`--${boundary}\r\n`));
bodyParts.push(Buffer.from(`Content-Disposition: form-data; name="${p}"\r\n\r\n`));
bodyParts.push(Buffer.from(`Content-Disposition: form-data; name="${param}"\r\n\r\n`));
bodyParts.push(Buffer.from(String(value)));
bodyParts.push(Buffer.from('\r\n'));
}
}

if (uploadChecksum) {
bodyParts.push(Buffer.from(`--${boundary}\r\n`));
bodyParts.push(Buffer.from('Content-Disposition: form-data; name="checksum256"\r\n\r\n'));
bodyParts.push(Buffer.from(uploadChecksum));
bodyParts.push(Buffer.from('\r\n'));
}

bodyParts.push(Buffer.from(`--${boundary}\r\n`));
bodyParts.push(Buffer.from(`Content-Disposition: form-data; name="user_picture"; filename="${fileName}"\r\n`));
bodyParts.push(Buffer.from(`Content-Type: ${contentType}\r\n\r\n`));
Expand Down Expand Up @@ -1779,16 +1802,14 @@ Countly.Bulk = Bulk;
}

/**
* Convert JSON object to query params
* Convert JSON object to query params and append checksum when configured
* @param {Object} params - object with url params
* @param {Boolean} decodeBeforeHash - if true request data is URL-decoded before hashing
* @returns {String} query string
*/
function prepareParams(params) {
var str = [];
for (var i in params) {
str.push(`${i}=${encodeURIComponent(params[i])}`);
}
return str.join("&");
function prepareParams(params, decodeBeforeHash) {
var data = cc.serializeParams(params);
return cc.addChecksum(data, Countly.salt, decodeBeforeHash, false);
}

/**
Expand Down
4 changes: 2 additions & 2 deletions package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

2 changes: 1 addition & 1 deletion package.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"name": "countly-sdk-nodejs",
"version": "24.10.3",
"version": "24.10.4",
"description": "Countly NodeJS SDK",
"main": "lib/countly.js",
"directories": {
Expand Down
39 changes: 29 additions & 10 deletions test/helpers/helper_functions.js
Original file line number Diff line number Diff line change
Expand Up @@ -38,35 +38,54 @@ const sWait = 50;
const mWait = 3000;
const lWait = 10000;

function readStoredValue(storageKey, destination, fileKey, fallbackValue) {
try {
const storedValue = CountlyStorage.storeGet(storageKey, undefined);
if (typeof storedValue !== "undefined") {
return storedValue;
}
}
catch (error) {
// Ignore storage access errors and fall back to direct file reads.
}

try {
const fileData = JSON.parse(fs.readFileSync(destination, "utf-8"));
if (fileData && typeof fileData[fileKey] !== "undefined") {
return fileData[fileKey];
}
}
catch (error) {
// Ignore incomplete or missing file contents and return the fallback value.
}

return fallbackValue;
}

// parsing event queue
function readEventQueue(givenPath = null, isBulk = false) {
var destination = DIR_CLY_event;
if (givenPath !== null) {
destination = givenPath;
}
var a = JSON.parse(fs.readFileSync(destination, "utf-8")).cly_event;
if (isBulk) {
a = JSON.parse(fs.readFileSync(destination, "utf-8")).cly_bulk_event;
return readStoredValue("cly_bulk_event", destination, "cly_bulk_event", {});
}
return a;
return readStoredValue("cly_event", destination, "cly_event", []);
}
// parsing request queue
function readRequestQueue(customPath = false, isBulk = false, isMemory = false) {
var destination = DIR_CLY_request;
if (customPath) {
destination = DIR_Test_request;
}
var a;
if (isBulk) {
a = JSON.parse(fs.readFileSync(destination, "utf-8")).cly_req_queue;
return readStoredValue("cly_req_queue", destination, "cly_req_queue", []);
}
if (isMemory) {
a = CountlyStorage.storeGet("cly_queue");
}
else {
a = JSON.parse(fs.readFileSync(destination, "utf-8")).cly_queue;
return CountlyStorage.storeGet("cly_queue", []);
}
return a;
return readStoredValue("cly_queue", destination, "cly_queue", []);
}
function doesFileStoragePathsExist(callback, isBulk = false, testPath = false) {
var paths = [DIR_CLY_ID, DIR_CLY_ID_type, DIR_CLY_event, DIR_CLY_request];
Expand Down
Loading
Loading