Skip to content

Commit ea9db36

Browse files
authored
Merge pull request #143 from Countly/staging
Staging 24.10.4
2 parents 751e163 + c18f9d6 commit ea9db36

9 files changed

Lines changed: 2423 additions & 220 deletions

File tree

.gitignore

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,3 +2,6 @@
22
**/node_modules
33
bulk_data/**
44
**/data
5+
.nyc_output
6+
coverage
7+
coverage-report.txt

CHANGELOG.md

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,6 @@
1+
## 24.10.4
2+
- Added a new init time flag `salt` for request tampering protection (should be used in tandem with server options)
3+
14
## 24.10.3
25
- Added support for uploading user images by providing path to the local image using `picturePath` parameter in `user_details` method (non-bulk)
36
- Reduced SDK log verbosity

lib/countly-bulk.js

Lines changed: 5 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -40,6 +40,7 @@ CountlyBulk.StorageTypes = cc.storageTypeEnums;
4040
* @param {number} [conf.session_update=60] - how often in seconds should session be extended
4141
* @param {number} [conf.max_events=100] - maximum amount of events to send in one batch
4242
* @param {boolean} [conf.force_post=false] - force using post method for all requests
43+
* @param {string} [conf.salt] - shared secret used to append checksum256 to outgoing requests
4344
* @param {string} [conf.storage_path] - where SDK would store data, including id, queues, etc
4445
* @param {string} [conf.http_options=] - function to get http options by reference and overwrite them, before running each request
4546
* @param {number} [conf.max_key_length=128] - maximum size of all string keys
@@ -59,7 +60,7 @@ CountlyBulk.StorageTypes = cc.storageTypeEnums;
5960
* });
6061
*/
6162
function CountlyBulk(conf) {
62-
var SDK_VERSION = "24.10.3";
63+
var SDK_VERSION = "24.10.4";
6364
var SDK_NAME = "javascript_native_nodejs_bulk";
6465

6566
var empty_queue_callback = null;
@@ -96,6 +97,7 @@ function CountlyBulk(conf) {
9697
conf.session_update = conf.session_update || 60;
9798
conf.max_events = conf.max_events || 100;
9899
conf.force_post = conf.force_post || false;
100+
conf.salt = conf.salt || null;
99101
conf.persist_queue = conf.persist_queue || false;
100102
conf.http_options = conf.http_options || null;
101103
conf.maxKeyLength = conf.max_key_length || maxKeyLength;
@@ -467,16 +469,12 @@ function CountlyBulk(conf) {
467469
}
468470

469471
/**
470-
* Convert JSON object to query params
472+
* Convert JSON object to query params and append checksum when configured
471473
* @param {Object} params - object with url params
472474
* @returns {String} query string
473475
*/
474476
function prepareParams(params) {
475-
var str = [];
476-
for (var i in params) {
477-
str.push(`${i}=${encodeURIComponent(params[i])}`);
478-
}
479-
return str.join("&");
477+
return cc.addChecksum(cc.serializeParams(params), conf.salt, false, false);
480478
}
481479

482480
/**

lib/countly-common.js

Lines changed: 60 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,8 @@
11
/**
22
* main common functionalities will go in here
33
*/
4+
var crypto = require("crypto");
5+
46
var cc = {
57

68
// debug value from Countly
@@ -135,6 +137,64 @@ var cc = {
135137
}
136138
return ob;
137139
},
140+
/**
141+
* Convert params object to URL encoded query parameter string
142+
* @param {Object} params - object with query parameters
143+
* @returns {String} URL encoded query string
144+
*/
145+
serializeParams: function serializeParams(params) {
146+
var str = [];
147+
var keys = Object.keys(params || {});
148+
for (var i = 0; i < keys.length; i++) {
149+
var key = keys[i];
150+
str.push(`${key}=${encodeURIComponent(params[key])}`);
151+
}
152+
return str.join("&");
153+
},
154+
/**
155+
* Calculate the SHA-256 checksum for provided request data and salt
156+
* @param {String} data - serialized request data
157+
* @param {String} salt - developer provided shared secret
158+
* @param {Boolean} decodeBeforeHash - if true decodes serialized data before hashing
159+
* @param {Boolean} uppercase - if true returns uppercase hex
160+
* @returns {String} checksum in hex format
161+
*/
162+
calculateChecksum: function calculateChecksum(data, salt, decodeBeforeHash, uppercase) {
163+
var checksumData = data || "";
164+
if (decodeBeforeHash) {
165+
try {
166+
checksumData = decodeURIComponent(checksumData);
167+
}
168+
catch (e) {
169+
this.log(this.logLevelEnums.WARNING, `calculateChecksum, Failed to decode request data before hashing: [${e}]`);
170+
}
171+
}
172+
var hash = crypto.createHash("sha256");
173+
hash.update(`${checksumData}${salt}`);
174+
var checksum = hash.digest("hex");
175+
if (uppercase) {
176+
return checksum.toUpperCase();
177+
}
178+
return checksum;
179+
},
180+
/**
181+
* Append checksum256 to serialized request data when salt is configured
182+
* @param {String} data - serialized request data
183+
* @param {String} salt - developer provided shared secret
184+
* @param {Boolean} decodeBeforeHash - if true decodes serialized data before hashing
185+
* @param {Boolean} uppercase - if true appends uppercase hex
186+
* @returns {String} serialized request data with checksum when configured
187+
*/
188+
addChecksum: function addChecksum(data, salt, decodeBeforeHash, uppercase) {
189+
if (!salt) {
190+
return data;
191+
}
192+
var checksum = this.calculateChecksum(data, salt, decodeBeforeHash, uppercase);
193+
if (!data) {
194+
return `checksum256=${checksum}`;
195+
}
196+
return `${data}&checksum256=${checksum}`;
197+
},
138198
/**
139199
* Removing trailing slashes
140200
* @memberof Countly._internals

lib/countly.js

Lines changed: 32 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -35,7 +35,7 @@ Countly.StorageTypes = cc.storageTypeEnums;
3535
Countly.DeviceIdType = cc.deviceIdTypeEnums;
3636
Countly.Bulk = Bulk;
3737
(function() {
38-
var SDK_VERSION = "24.10.3";
38+
var SDK_VERSION = "24.10.4";
3939
var SDK_NAME = "javascript_native_nodejs";
4040

4141
var inited = false;
@@ -103,6 +103,7 @@ Countly.Bulk = Bulk;
103103
* @param {number} [conf.session_update=60] - how often in seconds should session be extended
104104
* @param {number} [conf.max_events=100] - maximum amount of events to send in one batch
105105
* @param {boolean} [conf.force_post=false] - force using post method for all requests
106+
* @param {string} [conf.salt] - shared secret used to append checksum256 to outgoing requests
106107
* @param {boolean} [conf.clear_stored_device_id=false] - set it to true if you want to erase the stored device ID
107108
* @param {boolean} [conf.test_mode=false] - set it to true if you want to initiate test_mode
108109
* @param {string} [conf.storage_path] - where SDK would store data, including id, queues, etc
@@ -162,6 +163,7 @@ Countly.Bulk = Bulk;
162163
Countly.city = conf.city || Countly.city || null;
163164
Countly.ip_address = conf.ip_address || Countly.ip_address || null;
164165
Countly.force_post = conf.force_post || Countly.force_post || false;
166+
Countly.salt = conf.salt || Countly.salt || null;
165167
Countly.require_consent = conf.require_consent || Countly.require_consent || false;
166168
Countly.remote_config = conf.remote_config || Countly.remote_config || false;
167169
Countly.http_options = conf.http_options || Countly.http_options || null;
@@ -215,6 +217,7 @@ Countly.Bulk = Bulk;
215217
cc.log(cc.logLevelEnums.DEBUG, `init, IP address: [${Countly.ip_address}].`);
216218
}
217219
cc.log(cc.logLevelEnums.DEBUG, `init, Force POST requests: [${Countly.force_post}].`);
220+
cc.log(cc.logLevelEnums.DEBUG, `init, Salt is configured: [${!!Countly.salt}].`);
218221
cc.log(cc.logLevelEnums.DEBUG, `init, Storage path: [${CountlyStorage.getStoragePath()}].`);
219222
cc.log(cc.logLevelEnums.DEBUG, `init, Require consent: [${Countly.require_consent}].`);
220223
if (Countly.remote_config) {
@@ -353,6 +356,7 @@ Countly.Bulk = Bulk;
353356
Countly.city = undefined;
354357
Countly.ip_address = undefined;
355358
Countly.force_post = undefined;
359+
Countly.salt = undefined;
356360
Countly.require_consent = undefined;
357361
Countly.http_options = undefined;
358362
CountlyStorage.resetStorage();
@@ -1670,17 +1674,36 @@ Countly.Bulk = Bulk;
16701674

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

16741679
for (var p in params) {
1675-
if (typeof params[p] !== "undefined" && p !== 'picturePath') {
1676-
var value = params[p];
1680+
if (Object.prototype.hasOwnProperty.call(params, p) && typeof params[p] !== "undefined" && p !== "picturePath") {
1681+
uploadParams[p] = params[p];
1682+
}
1683+
}
1684+
1685+
var uploadChecksum = null;
1686+
if (Countly.salt) {
1687+
uploadChecksum = cc.calculateChecksum(cc.serializeParams(uploadParams), Countly.salt, true);
1688+
}
1689+
1690+
for (var param in uploadParams) {
1691+
if (Object.prototype.hasOwnProperty.call(uploadParams, param)) {
1692+
var value = uploadParams[param];
16771693
bodyParts.push(Buffer.from(`--${boundary}\r\n`));
1678-
bodyParts.push(Buffer.from(`Content-Disposition: form-data; name="${p}"\r\n\r\n`));
1694+
bodyParts.push(Buffer.from(`Content-Disposition: form-data; name="${param}"\r\n\r\n`));
16791695
bodyParts.push(Buffer.from(String(value)));
16801696
bodyParts.push(Buffer.from('\r\n'));
16811697
}
16821698
}
16831699

1700+
if (uploadChecksum) {
1701+
bodyParts.push(Buffer.from(`--${boundary}\r\n`));
1702+
bodyParts.push(Buffer.from('Content-Disposition: form-data; name="checksum256"\r\n\r\n'));
1703+
bodyParts.push(Buffer.from(uploadChecksum));
1704+
bodyParts.push(Buffer.from('\r\n'));
1705+
}
1706+
16841707
bodyParts.push(Buffer.from(`--${boundary}\r\n`));
16851708
bodyParts.push(Buffer.from(`Content-Disposition: form-data; name="user_picture"; filename="${fileName}"\r\n`));
16861709
bodyParts.push(Buffer.from(`Content-Type: ${contentType}\r\n\r\n`));
@@ -1779,16 +1802,14 @@ Countly.Bulk = Bulk;
17791802
}
17801803

17811804
/**
1782-
* Convert JSON object to query params
1805+
* Convert JSON object to query params and append checksum when configured
17831806
* @param {Object} params - object with url params
1807+
* @param {Boolean} decodeBeforeHash - if true request data is URL-decoded before hashing
17841808
* @returns {String} query string
17851809
*/
1786-
function prepareParams(params) {
1787-
var str = [];
1788-
for (var i in params) {
1789-
str.push(`${i}=${encodeURIComponent(params[i])}`);
1790-
}
1791-
return str.join("&");
1810+
function prepareParams(params, decodeBeforeHash) {
1811+
var data = cc.serializeParams(params);
1812+
return cc.addChecksum(data, Countly.salt, decodeBeforeHash, false);
17921813
}
17931814

17941815
/**

0 commit comments

Comments
 (0)