fiftyone.pipeline.engines.fiftyone 4.4.3 → 4.4.4
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/package.json +3 -3
- package/shareUsage.js +301 -60
- package/shareUsageEvidenceKeyFilter.js +41 -33
- package/tests/shareUsage.test.js +465 -0
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "fiftyone.pipeline.engines.fiftyone",
|
|
3
|
-
"version": "4.4.
|
|
3
|
+
"version": "4.4.4",
|
|
4
4
|
"description": "Shared base functions for implementing 51Degrees engines for the 51Degrees Pipeline API",
|
|
5
5
|
"main": "index.js",
|
|
6
6
|
"directories": {
|
|
@@ -11,8 +11,8 @@
|
|
|
11
11
|
},
|
|
12
12
|
"author": "51Degrees <support@51Degrees.com>",
|
|
13
13
|
"dependencies": {
|
|
14
|
-
"fiftyone.pipeline.core": "^4.4.
|
|
15
|
-
"fiftyone.pipeline.engines": "^4.4.
|
|
14
|
+
"fiftyone.pipeline.core": "^4.4.4",
|
|
15
|
+
"fiftyone.pipeline.engines": "^4.4.4"
|
|
16
16
|
},
|
|
17
17
|
"devDependencies": {
|
|
18
18
|
"eslint": "^6.8.0",
|
package/shareUsage.js
CHANGED
|
@@ -34,10 +34,18 @@ const Engine = engines.Engine;
|
|
|
34
34
|
const ShareUsageEvidenceKeyFilter = require('./shareUsageEvidenceKeyFilter');
|
|
35
35
|
const ShareUsageTracker = require('./shareUsageTracker');
|
|
36
36
|
const zlib = require('zlib');
|
|
37
|
-
const
|
|
37
|
+
const url = require('url');
|
|
38
38
|
|
|
39
39
|
const os = require('os');
|
|
40
40
|
|
|
41
|
+
/**
|
|
42
|
+
* The maximum length of a piece of evidence's value which can be
|
|
43
|
+
* added to the usage data being sent.
|
|
44
|
+
*/
|
|
45
|
+
const SHARE_USAGE_MAX_EVIDENCE_LENGTH = 512;
|
|
46
|
+
|
|
47
|
+
const SHARE_USAGE_VERSION = '1.1';
|
|
48
|
+
|
|
41
49
|
/**
|
|
42
50
|
* @typedef {import('fiftyone.pipeline.core').FlowData} FlowData
|
|
43
51
|
*/
|
|
@@ -71,7 +79,8 @@ class ShareUsage extends Engine {
|
|
|
71
79
|
* @param {Array} options.headerBlacklist By default, all HTTP headers
|
|
72
80
|
* (except a few, such as Cookies) are shared. Individual headers can
|
|
73
81
|
* be excluded from sharing by adding them to this list.
|
|
74
|
-
* @param {number} options.sharePercentage
|
|
82
|
+
* @param {number} options.sharePercentage approximate proportion of
|
|
83
|
+
* requests to be shared. 1 = 100%, 0.5 = 50%, etc..
|
|
75
84
|
*/
|
|
76
85
|
constructor (
|
|
77
86
|
{
|
|
@@ -80,7 +89,8 @@ class ShareUsage extends Engine {
|
|
|
80
89
|
cookie,
|
|
81
90
|
queryWhitelist,
|
|
82
91
|
headerBlacklist,
|
|
83
|
-
sharePercentage =
|
|
92
|
+
sharePercentage = 1,
|
|
93
|
+
endpoint = 'https://devices-v4.51degrees.com/new.ashx'
|
|
84
94
|
} = {}) {
|
|
85
95
|
super(...arguments);
|
|
86
96
|
|
|
@@ -101,9 +111,21 @@ class ShareUsage extends Engine {
|
|
|
101
111
|
|
|
102
112
|
this.sharePercentage = sharePercentage;
|
|
103
113
|
|
|
104
|
-
this.sharePercentageCounter = 0;
|
|
105
|
-
|
|
106
114
|
this.shareData = [];
|
|
115
|
+
|
|
116
|
+
if (endpoint.includes('https://') === false &&
|
|
117
|
+
endpoint.includes('http://') == false) {
|
|
118
|
+
endpoint = 'https://' + endpoint;
|
|
119
|
+
}
|
|
120
|
+
this.endpoint = url.parse(endpoint);
|
|
121
|
+
switch (this.endpoint.protocol) {
|
|
122
|
+
case 'http:':
|
|
123
|
+
this.http = require('http');
|
|
124
|
+
break;
|
|
125
|
+
case 'https:':
|
|
126
|
+
this.http = require('https');
|
|
127
|
+
break;
|
|
128
|
+
}
|
|
107
129
|
}
|
|
108
130
|
|
|
109
131
|
/**
|
|
@@ -113,71 +135,156 @@ class ShareUsage extends Engine {
|
|
|
113
135
|
* @param {FlowData} flowData flowData to process
|
|
114
136
|
*/
|
|
115
137
|
processInternal (flowData) {
|
|
116
|
-
this.sharePercentageCounter += 1;
|
|
117
138
|
|
|
118
|
-
if (
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
if (this.sharePercentageCounter !== 0) {
|
|
123
|
-
return;
|
|
124
|
-
}
|
|
139
|
+
if (Math.random() <= this.sharePercentage) {
|
|
140
|
+
const cacheKey = this.evidenceKeyFilter
|
|
141
|
+
.filterEvidence(flowData.evidence.getAll());
|
|
125
142
|
|
|
126
|
-
|
|
127
|
-
|
|
143
|
+
const share = this.tracker.track(cacheKey);
|
|
144
|
+
if (share) {
|
|
145
|
+
this.tracker.put(cacheKey);
|
|
128
146
|
|
|
129
|
-
|
|
147
|
+
this.addToShareUsage(this.getDataFromEvidence(flowData));
|
|
148
|
+
}
|
|
149
|
+
}
|
|
150
|
+
}
|
|
130
151
|
|
|
131
|
-
|
|
132
|
-
|
|
152
|
+
/**
|
|
153
|
+
* Creates a ShareUsageData instance populated from the evidence
|
|
154
|
+
* within the flow data provided.
|
|
155
|
+
* @param {FlowData} flowData the flow data containing the evidence to use
|
|
156
|
+
* @returns a new ShareUsageData instance, populated from the evidence
|
|
157
|
+
* provided
|
|
158
|
+
*/
|
|
159
|
+
getDataFromEvidence(flowData) {
|
|
160
|
+
const data = new ShareUsageData();
|
|
161
|
+
|
|
162
|
+
Object.keys(flowData.evidence.getAll()).forEach(key => {
|
|
163
|
+
const value = flowData.evidence.get(key);
|
|
164
|
+
if (key === 'server.client-ip') {
|
|
165
|
+
// The client IP is dealt with separately for backwards
|
|
166
|
+
// compatibility purposes.
|
|
167
|
+
data.clientIP = value;
|
|
168
|
+
} else if (key === 'query.session-id') {
|
|
169
|
+
// The SessionID is dealt with separately.
|
|
170
|
+
data.sessionId = value;
|
|
171
|
+
} else if (key === 'query.sequence') {
|
|
172
|
+
// The Sequence is dealt with separately.
|
|
173
|
+
var sequence = parseInt(value);
|
|
174
|
+
|
|
175
|
+
if (isNaN(sequence) === false) {
|
|
176
|
+
data.sequence = sequence;
|
|
177
|
+
}
|
|
178
|
+
else {
|
|
179
|
+
this._log('error',
|
|
180
|
+
`The value '${value}' could not be parsed to an integer.`);
|
|
181
|
+
}
|
|
182
|
+
} else {
|
|
183
|
+
// Check if we can send this piece of evidence
|
|
184
|
+
if (this.evidenceKeyFilter.filterEvidenceKey(key)) {
|
|
185
|
+
data.tryAddToData(key, value);
|
|
186
|
+
}
|
|
187
|
+
}
|
|
188
|
+
});
|
|
189
|
+
return data;
|
|
190
|
+
}
|
|
133
191
|
|
|
134
|
-
|
|
192
|
+
getConstantXml() {
|
|
193
|
+
if (!this.constantXml) {
|
|
194
|
+
const versions = this.getPackageVersions();
|
|
195
|
+
const coreVersion = versions['fiftyone.pipeline.core'];
|
|
196
|
+
const osVersion = `${new ReplacedString(process.platform).result} ${new ReplacedString(os.release()).result}`;
|
|
197
|
+
const nodeVersion = new ReplacedString(process.versions.node).result;
|
|
198
|
+
|
|
199
|
+
let xml = '';
|
|
200
|
+
|
|
201
|
+
// The version number of the Pipeline API
|
|
202
|
+
xml += `<Version>${coreVersion}</Version>`;
|
|
203
|
+
// Write Pipeline information
|
|
204
|
+
// The product name
|
|
205
|
+
xml += '<Product>Pipeline</Product>';
|
|
206
|
+
// The flow elements in the current pipeline
|
|
207
|
+
this.getFlowElements().forEach(element => {
|
|
208
|
+
xml += `<FlowElement>${element}</FlowElement>`;
|
|
209
|
+
})
|
|
210
|
+
xml += '<Language>Node.JS</Language>';
|
|
211
|
+
// The software language version
|
|
212
|
+
xml += `<LanguageVersion>${nodeVersion}</LanguageVersion>`;
|
|
213
|
+
// The OS name and version
|
|
214
|
+
xml += `<Platform>${osVersion}</Platform>`;
|
|
215
|
+
this.constantXml = xml;
|
|
135
216
|
}
|
|
217
|
+
return this.constantXml;
|
|
136
218
|
}
|
|
137
219
|
|
|
138
220
|
/**
|
|
139
221
|
* Internal method which adds to the share usage bundle (generating XML)
|
|
140
222
|
*
|
|
141
|
-
* @param {object}
|
|
223
|
+
* @param {object} data key value store of current
|
|
142
224
|
* evidence in FlowData (filtered by the ShareUsageEvidenceKeyFilter)
|
|
143
225
|
*/
|
|
144
|
-
addToShareUsage (
|
|
226
|
+
addToShareUsage (data) {
|
|
145
227
|
let xml = '';
|
|
146
|
-
|
|
147
|
-
xml += '<
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
const name = parts[1];
|
|
162
|
-
const value = key[item];
|
|
163
|
-
|
|
164
|
-
xml += `<${prefix} name="${name}">${value}</${prefix}>`;
|
|
165
|
-
}
|
|
166
|
-
|
|
228
|
+
|
|
229
|
+
xml += '<Device>';
|
|
230
|
+
|
|
231
|
+
// --- write invariant data
|
|
232
|
+
xml += this.getConstantXml();
|
|
233
|
+
|
|
234
|
+
// --- write variable data
|
|
235
|
+
// The SessionID used to track a series of requests
|
|
236
|
+
xml += `<SessionId>${data.sessionId}</SessionId>`;
|
|
237
|
+
// The sequence number of the request in a series of requests.
|
|
238
|
+
xml += `<Sequence>${data.sequence}</Sequence>`;
|
|
239
|
+
// The client IP of the request
|
|
240
|
+
xml += `<ClientIP>${data.clientIP}</ClientIP>`;
|
|
241
|
+
|
|
242
|
+
// The UTC date/time this entry was written
|
|
167
243
|
const date = new Date()
|
|
168
244
|
.toISOString()
|
|
169
|
-
.
|
|
245
|
+
.substring(0, 19)
|
|
170
246
|
.replace('T', ' ')
|
|
171
247
|
.split(' ')
|
|
172
248
|
.join(':');
|
|
249
|
+
xml += `<DateSent>${date}</DateSent>`;
|
|
250
|
+
|
|
251
|
+
// Write all other evidence data that has been included.
|
|
252
|
+
Object.keys(data.evidenceData).forEach(categoryKey => {
|
|
253
|
+
const categoryValue = data.evidenceData[categoryKey];
|
|
254
|
+
Object.keys(categoryValue).forEach(entryKey => {
|
|
255
|
+
const entryValue = categoryValue[entryKey];
|
|
256
|
+
const replacedString = new ReplacedString(entryValue);
|
|
257
|
+
// Write start element
|
|
258
|
+
if (categoryKey.length > 0) {
|
|
259
|
+
xml += `<${categoryKey} Name="${entryKey}"`;
|
|
260
|
+
} else {
|
|
261
|
+
xml += `<${entryKey}`;
|
|
262
|
+
}
|
|
263
|
+
// Write any attributes
|
|
264
|
+
if (replacedString.replaced) {
|
|
265
|
+
xml += ' replaced="true"';
|
|
266
|
+
}
|
|
267
|
+
if (replacedString.truncated) {
|
|
268
|
+
xml += ' truncated="true"';
|
|
269
|
+
}
|
|
270
|
+
// End the start element
|
|
271
|
+
xml += '>';
|
|
272
|
+
// Write the value
|
|
273
|
+
xml += replacedString.result;
|
|
274
|
+
// Write end element
|
|
275
|
+
if (categoryKey.length > 0) {
|
|
276
|
+
xml += `</${categoryKey}>`;
|
|
277
|
+
} else {
|
|
278
|
+
xml += `</${entryKey}>`;
|
|
279
|
+
}
|
|
280
|
+
});
|
|
281
|
+
});
|
|
173
282
|
|
|
174
|
-
xml += '
|
|
175
|
-
|
|
176
|
-
xml += '</device>';
|
|
283
|
+
xml += '</Device>';
|
|
177
284
|
|
|
178
285
|
this.shareData.push(xml);
|
|
179
|
-
|
|
180
|
-
if (this.shareData.length
|
|
286
|
+
|
|
287
|
+
if (this.shareData.length >= this.requestedPackageSize) {
|
|
181
288
|
this.sendShareUsage();
|
|
182
289
|
}
|
|
183
290
|
}
|
|
@@ -186,33 +293,167 @@ class ShareUsage extends Engine {
|
|
|
186
293
|
* Internal method to send the share usage bundle to the 51Degrees servers
|
|
187
294
|
*/
|
|
188
295
|
sendShareUsage () {
|
|
189
|
-
const
|
|
296
|
+
const usageEngine = this;
|
|
297
|
+
var shareData = this.shareData;
|
|
298
|
+
this.shareData = [];
|
|
299
|
+
var data = `<Devices version="${SHARE_USAGE_VERSION}">${shareData.join()}</Devices>`;
|
|
190
300
|
|
|
191
301
|
const options = {
|
|
192
|
-
hostname:
|
|
193
|
-
path:
|
|
302
|
+
hostname: this.endpoint.hostname,
|
|
303
|
+
path: this.endpoint.pathname,
|
|
304
|
+
port: this.endpoint.port,
|
|
194
305
|
method: 'POST',
|
|
195
|
-
headers: { 'Content-Encoding': 'gzip', 'Content-Type': 'text/xml' }
|
|
306
|
+
headers: { 'Content-Encoding': 'gzip', 'Content-Type': 'text/xml; charset=utf-8' }
|
|
196
307
|
};
|
|
197
308
|
|
|
198
309
|
zlib.gzip(data, function (err, buffer) {
|
|
199
310
|
if (err) {
|
|
200
|
-
|
|
311
|
+
usageEngine._log('warning', err);
|
|
201
312
|
}
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
// Reset sharedata collection
|
|
205
|
-
|
|
206
|
-
this.shareData = [];
|
|
313
|
+
var req = usageEngine.http.request(options, function (res) {
|
|
314
|
+
usageEngine._log('debug', `Usage data sent. Response code ${res.statusCode}`);
|
|
207
315
|
});
|
|
208
316
|
req.on('error', function (e) {
|
|
209
|
-
|
|
317
|
+
usageEngine._log('warning', e);
|
|
210
318
|
});
|
|
211
|
-
|
|
212
319
|
req.write(buffer);
|
|
213
320
|
req.end();
|
|
214
321
|
});
|
|
215
322
|
}
|
|
323
|
+
|
|
324
|
+
|
|
325
|
+
/**
|
|
326
|
+
* Return a list of FlowElements in the pipeline.
|
|
327
|
+
* If the list is null then populate from the pipeline.
|
|
328
|
+
* If there are multiple or no pipelines then log an error.
|
|
329
|
+
* @returns list of flow elements
|
|
330
|
+
*/
|
|
331
|
+
getFlowElements() {
|
|
332
|
+
if (!this.flowElements) {
|
|
333
|
+
if (this.pipelines.length === 1) {
|
|
334
|
+
const list = [];
|
|
335
|
+
for (const [key, value] of Object.entries(this.pipelines[0].flowElements)) {
|
|
336
|
+
list.push(value.constructor.name);
|
|
337
|
+
}
|
|
338
|
+
this.flowElements = list;
|
|
339
|
+
} else {
|
|
340
|
+
// This element has somehow been registered to too
|
|
341
|
+
// many (or zero) pipelines.
|
|
342
|
+
// This means we cannot know the flow elements that
|
|
343
|
+
// make up the pipeline so a warning is logged
|
|
344
|
+
// but otherwise, the system can continue as normal.
|
|
345
|
+
this._log('warn', 'Share usage element registered ' +
|
|
346
|
+
`to ${this.pipelines.length > 0 ? "too many" : "no"}` +
|
|
347
|
+
' pipelines. Unable to send share usage information.');
|
|
348
|
+
this.flowElements = [];
|
|
349
|
+
}
|
|
350
|
+
}
|
|
351
|
+
return this.flowElements;
|
|
352
|
+
}
|
|
353
|
+
|
|
354
|
+
/**
|
|
355
|
+
* Get the versions of all packages installed in 'node_modules'.
|
|
356
|
+
* @returns package versions keyed on name
|
|
357
|
+
*/
|
|
358
|
+
getPackageVersions() {
|
|
359
|
+
var fs = require('fs');
|
|
360
|
+
var dirs = fs.readdirSync('node_modules');
|
|
361
|
+
var data = {};
|
|
362
|
+
dirs.forEach(function(dir) {
|
|
363
|
+
try {
|
|
364
|
+
var file = dir + '/package.json';
|
|
365
|
+
var json = require(file);
|
|
366
|
+
var name = json.name;
|
|
367
|
+
var version = json.version;
|
|
368
|
+
data[name] = version;
|
|
369
|
+
} catch(err) {
|
|
370
|
+
// Do nothing here
|
|
371
|
+
}
|
|
372
|
+
});
|
|
373
|
+
return data;
|
|
374
|
+
|
|
375
|
+
}
|
|
376
|
+
}
|
|
377
|
+
|
|
378
|
+
// a set of valid XML character values (ignoring valid controls x09, x0a, x0d, x85)
|
|
379
|
+
const VALID_XML_CHARS =
|
|
380
|
+
Array.from({length: parseInt('0x7F', 16) - parseInt('0x20', 16)}, (v, k) => k + parseInt('0x20', 16))
|
|
381
|
+
.concat(
|
|
382
|
+
Array.from({length: parseInt('0x100', 16) - parseInt('0x40', 16)}, (v, k) => k + parseInt('0x40', 16)));
|
|
383
|
+
|
|
384
|
+
// an array describing whether a character value is valid
|
|
385
|
+
const IS_VALID_XML_CHAR = __getIsValidCharMap();
|
|
386
|
+
|
|
387
|
+
function __getIsValidCharMap() {
|
|
388
|
+
const maxChar = parseInt('0x100', 16);
|
|
389
|
+
const isValidChar = {};
|
|
390
|
+
for (var c = 0; c <= maxChar; c++) {
|
|
391
|
+
isValidChar[c] = VALID_XML_CHARS.includes(c);
|
|
392
|
+
}
|
|
393
|
+
return isValidChar;
|
|
216
394
|
}
|
|
217
395
|
|
|
396
|
+
/**
|
|
397
|
+
* Replace characters that cause problems in XML with the "Replacement character"
|
|
398
|
+
*/
|
|
399
|
+
class ReplacedString {
|
|
400
|
+
|
|
401
|
+
constructor(text) {
|
|
402
|
+
this.result = '';
|
|
403
|
+
this.replaced = false;
|
|
404
|
+
this.truncated = false;
|
|
405
|
+
if (text) {
|
|
406
|
+
const escapedText = text
|
|
407
|
+
.replace(/&/g, '&')
|
|
408
|
+
.replace(/"/g, '"')
|
|
409
|
+
.replace(/'/g, ''')
|
|
410
|
+
.replace(/</g, '<')
|
|
411
|
+
.replace(/>/g, '>');
|
|
412
|
+
const length = Math.min(escapedText.length, SHARE_USAGE_MAX_EVIDENCE_LENGTH);
|
|
413
|
+
this.truncated = escapedText.length > SHARE_USAGE_MAX_EVIDENCE_LENGTH;
|
|
414
|
+
Array.from(escapedText.substring(0, length))
|
|
415
|
+
.map(c => {
|
|
416
|
+
if (c.charCodeAt(0) < Object.keys(IS_VALID_XML_CHAR).length && IS_VALID_XML_CHAR[c.charCodeAt(0)]) {
|
|
417
|
+
this.result += c;
|
|
418
|
+
} else {
|
|
419
|
+
this.result += String.fromCharCode(parseInt('0xFFFD', 16));
|
|
420
|
+
this.replaced = true;
|
|
421
|
+
}
|
|
422
|
+
});
|
|
423
|
+
}
|
|
424
|
+
}
|
|
425
|
+
}
|
|
426
|
+
/**
|
|
427
|
+
* Internal class that is used to store details of data in memory
|
|
428
|
+
* prior to it being sent to 51Degrees.
|
|
429
|
+
*/
|
|
430
|
+
class ShareUsageData {
|
|
431
|
+
|
|
432
|
+
constructor() {
|
|
433
|
+
this.evidenceData = {};
|
|
434
|
+
this.sessionId = '';
|
|
435
|
+
this.clientIp = '';
|
|
436
|
+
this.sequence = '';
|
|
437
|
+
}
|
|
438
|
+
|
|
439
|
+
tryAddToData(key, value) {
|
|
440
|
+
// Get the category and field names from the evidence key.
|
|
441
|
+
let category = '';
|
|
442
|
+
let field = key;
|
|
443
|
+
|
|
444
|
+
const firstSeparator = key.indexOf('.');
|
|
445
|
+
if (firstSeparator > 0) {
|
|
446
|
+
category = key.substring(0, firstSeparator);
|
|
447
|
+
field = key.substring(firstSeparator + 1);
|
|
448
|
+
}
|
|
449
|
+
|
|
450
|
+
// Add the evidence to the dictionary.
|
|
451
|
+
let categoryDict = this.evidenceData[category];
|
|
452
|
+
if (!categoryDict) {
|
|
453
|
+
categoryDict = {};
|
|
454
|
+
this.evidenceData[category] = categoryDict;
|
|
455
|
+
}
|
|
456
|
+
categoryDict[field] = value.toString();
|
|
457
|
+
}
|
|
458
|
+
}
|
|
218
459
|
module.exports = ShareUsage;
|
|
@@ -48,9 +48,17 @@ class ShareUsageEvidenceKeyFilter extends EvidenceKeyFilter {
|
|
|
48
48
|
constructor ({ cookie, queryWhitelist = [], headerBlacklist = [] }) {
|
|
49
49
|
super(...arguments);
|
|
50
50
|
|
|
51
|
-
this.
|
|
52
|
-
this.
|
|
51
|
+
this.includedQueryStringParams = queryWhitelist;
|
|
52
|
+
this.blockedHttpHeaders = headerBlacklist;
|
|
53
53
|
this.cookie = cookie;
|
|
54
|
+
if (!cookie &&
|
|
55
|
+
(!queryWhitelist || queryWhitelist.length === 0) &&
|
|
56
|
+
(!headerBlacklist || headerBlacklist.length === 0)) {
|
|
57
|
+
this.shareAll = true;
|
|
58
|
+
}
|
|
59
|
+
else {
|
|
60
|
+
this.shareAll = false;
|
|
61
|
+
}
|
|
54
62
|
}
|
|
55
63
|
|
|
56
64
|
/**
|
|
@@ -63,39 +71,39 @@ class ShareUsageEvidenceKeyFilter extends EvidenceKeyFilter {
|
|
|
63
71
|
const prefix = key.toLowerCase().split('.')[0];
|
|
64
72
|
const suffix = key.toLowerCase().split('.')[1];
|
|
65
73
|
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
) {
|
|
80
|
-
|
|
74
|
+
let result = this.shareAll;
|
|
75
|
+
|
|
76
|
+
if (!this.shareAll) {
|
|
77
|
+
if (prefix === 'header') {
|
|
78
|
+
// Add the header to the list if the header name does not
|
|
79
|
+
// appear in the list of blocked headers.
|
|
80
|
+
result = this.blockedHttpHeaders
|
|
81
|
+
.includes(suffix) === false;
|
|
82
|
+
} else if (prefix === 'cookie') {
|
|
83
|
+
// Only add cookies that start with the 51Degrees cookie
|
|
84
|
+
// prefix.
|
|
85
|
+
result = suffix.indexOf('51d_') === 0 ||
|
|
86
|
+
(this.includeSession && suffix === this.cookie);
|
|
87
|
+
} else if (prefix === 'session') {
|
|
88
|
+
// Only add session values that start with the 51Degrees
|
|
89
|
+
// cookie prefix.
|
|
90
|
+
result = suffix.indexOf('51d_') === 0;
|
|
91
|
+
} else if (prefix === 'query') {
|
|
92
|
+
// If no query string parameter filter was specified
|
|
93
|
+
// then share all of them.
|
|
94
|
+
// Otherwise, only include query string parameters that
|
|
95
|
+
// start with 51d_ or that have been specified in
|
|
96
|
+
// the constructor.
|
|
97
|
+
result = this.includedQueryStringParams === null ||
|
|
98
|
+
suffix.indexOf('51d_') === 0 ||
|
|
99
|
+
this.includedQueryStringParams.includes(suffix);
|
|
100
|
+
} else {
|
|
101
|
+
// Add anything that is not a cookie, header, session
|
|
102
|
+
// variable or query string parameter.
|
|
103
|
+
result = true;
|
|
81
104
|
}
|
|
82
|
-
|
|
83
|
-
// Filter out any query evidence not in whitelist
|
|
84
|
-
|
|
85
|
-
if (prefix === 'query' && !this.queryWhitelist.includes(suffix)) {
|
|
86
|
-
return false;
|
|
87
|
-
}
|
|
88
|
-
|
|
89
|
-
// Filter out any header evidence in blacklist
|
|
90
|
-
|
|
91
|
-
if (prefix === 'header' && this.headerBlacklist.includes(suffix)) {
|
|
92
|
-
return false;
|
|
93
|
-
}
|
|
94
|
-
|
|
95
|
-
// Passed filtering tests, track
|
|
96
|
-
|
|
97
|
-
return true;
|
|
98
105
|
}
|
|
106
|
+
return result;
|
|
99
107
|
}
|
|
100
108
|
}
|
|
101
109
|
|
|
@@ -0,0 +1,465 @@
|
|
|
1
|
+
/* *********************************************************************
|
|
2
|
+
* This Original Work is copyright of 51 Degrees Mobile Experts Limited.
|
|
3
|
+
* Copyright 2022 51 Degrees Mobile Experts Limited, Davidson House,
|
|
4
|
+
* Forbury Square, Reading, Berkshire, United Kingdom RG1 3EU.
|
|
5
|
+
*
|
|
6
|
+
* This Original Work is licensed under the European Union Public Licence
|
|
7
|
+
* (EUPL) v.1.2 and is subject to its terms as set out below.
|
|
8
|
+
*
|
|
9
|
+
* If a copy of the EUPL was not distributed with this file, You can obtain
|
|
10
|
+
* one at https://opensource.org/licenses/EUPL-1.2.
|
|
11
|
+
*
|
|
12
|
+
* The 'Compatible Licences' set out in the Appendix to the EUPL (as may be
|
|
13
|
+
* amended by the European Commission) shall be deemed incompatible for
|
|
14
|
+
* the purposes of the Work and the provisions of the compatibility
|
|
15
|
+
* clause in Article 5 of the EUPL shall not apply.
|
|
16
|
+
*
|
|
17
|
+
* If using the Work as, or as part of, a network application, by
|
|
18
|
+
* including the attribution notice(s) required under Article 5 of the EUPL
|
|
19
|
+
* in the end user terms of the application under an appropriate heading,
|
|
20
|
+
* such notice(s) shall fulfill the requirements of that article.
|
|
21
|
+
* ********************************************************************* */
|
|
22
|
+
|
|
23
|
+
const { FlowData, PipelineBuilder } = require('fiftyone.pipeline.core');
|
|
24
|
+
const ShareUsage = require('../shareUsage');
|
|
25
|
+
const http = require('http');
|
|
26
|
+
const os = require('os');
|
|
27
|
+
const zlib = require('zlib');
|
|
28
|
+
|
|
29
|
+
const shareUsageVersion = '1.1';
|
|
30
|
+
|
|
31
|
+
// Data received by the local usage receiver.
|
|
32
|
+
let received = [];
|
|
33
|
+
// Local server used to capture usage sent.
|
|
34
|
+
let server = null;
|
|
35
|
+
|
|
36
|
+
/**
|
|
37
|
+
* Local server used to capture usage data sent.
|
|
38
|
+
* @param req the request
|
|
39
|
+
* @param res the response
|
|
40
|
+
*/
|
|
41
|
+
const requestListener = function (req, res) {
|
|
42
|
+
var data = '';
|
|
43
|
+
// Unzip the data.
|
|
44
|
+
var gunzip = zlib.createGunzip();
|
|
45
|
+
req.pipe(gunzip);
|
|
46
|
+
gunzip.on('data', function(chunk) {
|
|
47
|
+
data += chunk;
|
|
48
|
+
});
|
|
49
|
+
gunzip.on('error', (e) => fail(e));
|
|
50
|
+
// Return an OK response.
|
|
51
|
+
gunzip.on('end', () => {
|
|
52
|
+
received.push(data);
|
|
53
|
+
res.writeHead(200);
|
|
54
|
+
res.end();
|
|
55
|
+
});
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
beforeEach(() => {
|
|
59
|
+
received = [];
|
|
60
|
+
// Set up the local server. This is HTTP rather than HTTPS to avoid
|
|
61
|
+
// having to use local certificates.
|
|
62
|
+
server = http.createServer(requestListener);
|
|
63
|
+
server.listen(8080);
|
|
64
|
+
});
|
|
65
|
+
|
|
66
|
+
afterEach(() => {
|
|
67
|
+
server.close();
|
|
68
|
+
});
|
|
69
|
+
|
|
70
|
+
/**
|
|
71
|
+
* Check that data is received from the share usage element.
|
|
72
|
+
*/
|
|
73
|
+
test('share usage - data received', done => {
|
|
74
|
+
const usageEngine = new ShareUsage({
|
|
75
|
+
requestedPackageSize: 1,
|
|
76
|
+
endpoint: 'http://127.0.0.1:8080/'
|
|
77
|
+
});
|
|
78
|
+
|
|
79
|
+
const pipeline = new PipelineBuilder()
|
|
80
|
+
.add(usageEngine)
|
|
81
|
+
.build();
|
|
82
|
+
|
|
83
|
+
const flowData = pipeline.createFlowData();
|
|
84
|
+
|
|
85
|
+
flowData.process().then(() => {
|
|
86
|
+
setTimeout(() => {
|
|
87
|
+
expect(received.length).toBe(1);
|
|
88
|
+
done();
|
|
89
|
+
}, 2000);
|
|
90
|
+
})
|
|
91
|
+
});
|
|
92
|
+
|
|
93
|
+
/**
|
|
94
|
+
* Check that the data received from the share usage element is correct.
|
|
95
|
+
*/
|
|
96
|
+
test('share usage - data correct', done => {
|
|
97
|
+
const usageEngine = new ShareUsage({
|
|
98
|
+
requestedPackageSize: 1,
|
|
99
|
+
endpoint: 'http://127.0.0.1:8080/'
|
|
100
|
+
});
|
|
101
|
+
|
|
102
|
+
const pipeline = new PipelineBuilder()
|
|
103
|
+
.add(usageEngine)
|
|
104
|
+
.build();
|
|
105
|
+
|
|
106
|
+
const flowData = pipeline.createFlowData();
|
|
107
|
+
|
|
108
|
+
flowData.process().then(() => {
|
|
109
|
+
setTimeout(() => {
|
|
110
|
+
expect(received[0]).not.toBeNull();
|
|
111
|
+
expect(received[0].length).toBeGreaterThan(0);
|
|
112
|
+
expect(received[0]).toMatch(new RegExp(`^<Devices version="${shareUsageVersion}"><Device>.*`));
|
|
113
|
+
expect(received[0]).toMatch(new RegExp('.*</Device></Devices>$'));
|
|
114
|
+
expect(received[0]).toContain('<Language>Node.JS</Language>');
|
|
115
|
+
expect(received[0]).toContain(`<LanguageVersion>${process.versions.node}</LanguageVersion>`);
|
|
116
|
+
expect(received[0]).toContain(`<Platform>${process.platform} ${os.release()}</Platform>`);
|
|
117
|
+
expect(received[0]).toContain('<FlowElement>ShareUsage</FlowElement>');
|
|
118
|
+
done();
|
|
119
|
+
}, 2000);
|
|
120
|
+
})
|
|
121
|
+
});
|
|
122
|
+
|
|
123
|
+
/**
|
|
124
|
+
* Check that the data received from the share usage element contains the
|
|
125
|
+
* headers which were in the evidence.
|
|
126
|
+
*/
|
|
127
|
+
test('share usage - includes header', done => {
|
|
128
|
+
const usageEngine = new ShareUsage({
|
|
129
|
+
requestedPackageSize: 1,
|
|
130
|
+
endpoint: 'http://127.0.0.1:8080/'
|
|
131
|
+
});
|
|
132
|
+
|
|
133
|
+
const pipeline = new PipelineBuilder()
|
|
134
|
+
.add(usageEngine)
|
|
135
|
+
.build();
|
|
136
|
+
|
|
137
|
+
const uaValue = 'some user agent..';
|
|
138
|
+
const flowData = pipeline.createFlowData();
|
|
139
|
+
flowData.evidence.add('header.user-agent', uaValue);
|
|
140
|
+
|
|
141
|
+
flowData.process().then(() => {
|
|
142
|
+
setTimeout(() => {
|
|
143
|
+
expect(received[0]).toContain(`<header Name="user-agent">${uaValue}</header>`);
|
|
144
|
+
done();
|
|
145
|
+
}, 2000);
|
|
146
|
+
})
|
|
147
|
+
});
|
|
148
|
+
|
|
149
|
+
/**
|
|
150
|
+
* Check that the data received from the share usage element includes
|
|
151
|
+
* the client IP from the evidence.
|
|
152
|
+
*/
|
|
153
|
+
test('share usage - includes client ip', done => {
|
|
154
|
+
const usageEngine = new ShareUsage({
|
|
155
|
+
requestedPackageSize: 1,
|
|
156
|
+
endpoint: 'http://127.0.0.1:8080/'
|
|
157
|
+
});
|
|
158
|
+
|
|
159
|
+
const pipeline = new PipelineBuilder()
|
|
160
|
+
.add(usageEngine)
|
|
161
|
+
.build();
|
|
162
|
+
|
|
163
|
+
const ip = '1.2.3.4';
|
|
164
|
+
const flowData = pipeline.createFlowData();
|
|
165
|
+
flowData.evidence.add('server.client-ip', ip);
|
|
166
|
+
|
|
167
|
+
flowData.process().then(() => {
|
|
168
|
+
setTimeout(() => {
|
|
169
|
+
expect(received.length).toBe(1);
|
|
170
|
+
expect(received[0]).toContain(`<ClientIP>${ip}</ClientIP>`);
|
|
171
|
+
done();
|
|
172
|
+
}, 2000);
|
|
173
|
+
})
|
|
174
|
+
});
|
|
175
|
+
|
|
176
|
+
/**
|
|
177
|
+
* Check that a single event is not sent when the requested package
|
|
178
|
+
* size is 2.
|
|
179
|
+
*/
|
|
180
|
+
test('share usage - two events - first event', done => {
|
|
181
|
+
const usageEngine = new ShareUsage({
|
|
182
|
+
requestedPackageSize: 2,
|
|
183
|
+
endpoint: 'http://127.0.0.1:8080/'
|
|
184
|
+
});
|
|
185
|
+
|
|
186
|
+
const pipeline = new PipelineBuilder()
|
|
187
|
+
.add(usageEngine)
|
|
188
|
+
.build();
|
|
189
|
+
|
|
190
|
+
const flowData = pipeline.createFlowData();
|
|
191
|
+
|
|
192
|
+
flowData.process().then(() => {
|
|
193
|
+
setTimeout(() => {
|
|
194
|
+
expect(received.length).toBe(0);
|
|
195
|
+
done();
|
|
196
|
+
}, 2000);
|
|
197
|
+
})
|
|
198
|
+
});
|
|
199
|
+
|
|
200
|
+
/**
|
|
201
|
+
* Check that data is sent in a single request when a second request is
|
|
202
|
+
* sent and the requested package size is 2.
|
|
203
|
+
*/
|
|
204
|
+
test('share usage - two events - second event', done => {
|
|
205
|
+
const usageEngine = new ShareUsage({
|
|
206
|
+
requestedPackageSize: 2,
|
|
207
|
+
endpoint: 'http://127.0.0.1:8080/'
|
|
208
|
+
});
|
|
209
|
+
|
|
210
|
+
const pipeline = new PipelineBuilder()
|
|
211
|
+
.add(usageEngine)
|
|
212
|
+
.build();
|
|
213
|
+
|
|
214
|
+
const flowData1 = pipeline.createFlowData();
|
|
215
|
+
flowData1.evidence.add('header.user-agent', 'ua 1');
|
|
216
|
+
const flowData2 = pipeline.createFlowData();
|
|
217
|
+
flowData2.evidence.add('header.user-agent', 'ua 2');
|
|
218
|
+
|
|
219
|
+
flowData1.process().then(() => {
|
|
220
|
+
flowData2.process().then(() => {
|
|
221
|
+
setTimeout(() => {
|
|
222
|
+
expect(received.length).toBe(1);
|
|
223
|
+
expect(received[0]).toContain('ua 1');
|
|
224
|
+
expect(received[0]).toContain('ua 2');
|
|
225
|
+
done();
|
|
226
|
+
}, 2000);
|
|
227
|
+
});
|
|
228
|
+
});
|
|
229
|
+
});
|
|
230
|
+
|
|
231
|
+
/**
|
|
232
|
+
* Check that the share usage element respects the headerBlacklist.
|
|
233
|
+
* When header names are added to the list, values for them should
|
|
234
|
+
* not be included in the data shared.
|
|
235
|
+
*/
|
|
236
|
+
test('share usage - ignore headers', done => {
|
|
237
|
+
const usageEngine = new ShareUsage({
|
|
238
|
+
requestedPackageSize: 1,
|
|
239
|
+
endpoint: 'http://127.0.0.1:8080/',
|
|
240
|
+
headerBlacklist: [ 'x-forwarded-for', 'forwarded-for' ]
|
|
241
|
+
});
|
|
242
|
+
|
|
243
|
+
const pipeline = new PipelineBuilder()
|
|
244
|
+
.add(usageEngine)
|
|
245
|
+
.build();
|
|
246
|
+
|
|
247
|
+
const flowData = pipeline.createFlowData();
|
|
248
|
+
flowData.evidence.add('header.user-agent', 'some user agent');
|
|
249
|
+
flowData.evidence.add('header.x-forwarded-for', '5.6.7.8');
|
|
250
|
+
flowData.evidence.add('header.forwarded-for', '2001::');
|
|
251
|
+
|
|
252
|
+
flowData.process().then(() => {
|
|
253
|
+
setTimeout(() => {
|
|
254
|
+
expect(received.length).toBe(1);
|
|
255
|
+
expect(received[0]).not.toContain('x-forwarded-for');
|
|
256
|
+
expect(received[0]).not.toContain('forwarded-for');
|
|
257
|
+
done();
|
|
258
|
+
}, 2000);
|
|
259
|
+
});
|
|
260
|
+
});
|
|
261
|
+
|
|
262
|
+
test('share usage - queue cleared', done => {
|
|
263
|
+
const usageEngine = new ShareUsage({
|
|
264
|
+
requestedPackageSize: 1,
|
|
265
|
+
endpoint: 'http://127.0.0.1:8080/'
|
|
266
|
+
});
|
|
267
|
+
|
|
268
|
+
const pipeline = new PipelineBuilder()
|
|
269
|
+
.add(usageEngine)
|
|
270
|
+
.build();
|
|
271
|
+
|
|
272
|
+
const flowData = pipeline.createFlowData();
|
|
273
|
+
flowData.evidence.add('header.user-agent', 'some user agent');
|
|
274
|
+
|
|
275
|
+
flowData.process().then(() => {
|
|
276
|
+
setTimeout(() => {
|
|
277
|
+
expect(received.length).toBe(1);
|
|
278
|
+
expect(usageEngine.shareData.length).toBe(0);
|
|
279
|
+
done();
|
|
280
|
+
}, 2000);
|
|
281
|
+
});
|
|
282
|
+
});
|
|
283
|
+
|
|
284
|
+
/**
|
|
285
|
+
* Check that session and sequence values are included.
|
|
286
|
+
*/
|
|
287
|
+
test('share usage - session and sequence', done => {
|
|
288
|
+
const usageEngine = new ShareUsage({
|
|
289
|
+
requestedPackageSize: 1,
|
|
290
|
+
endpoint: 'http://127.0.0.1:8080/'
|
|
291
|
+
});
|
|
292
|
+
|
|
293
|
+
const pipeline = new PipelineBuilder()
|
|
294
|
+
.add(usageEngine)
|
|
295
|
+
.build();
|
|
296
|
+
|
|
297
|
+
const flowData = pipeline.createFlowData();
|
|
298
|
+
flowData.evidence.add('query.session-id', '1');
|
|
299
|
+
flowData.evidence.add('query.sequence', '1');
|
|
300
|
+
|
|
301
|
+
flowData.process().then(() => {
|
|
302
|
+
setTimeout(() => {
|
|
303
|
+
expect(received.length).toBe(1);
|
|
304
|
+
expect(received[0]).toContain('<SessionId>1</SessionId>');
|
|
305
|
+
expect(received[0]).toContain('<Sequence>1</Sequence>');
|
|
306
|
+
done();
|
|
307
|
+
}, 2000);
|
|
308
|
+
});
|
|
309
|
+
});
|
|
310
|
+
|
|
311
|
+
/**
|
|
312
|
+
* Check that invalid characters are escaped correctly.
|
|
313
|
+
*/
|
|
314
|
+
test('share usage - invalid char', done => {
|
|
315
|
+
const usageEngine = new ShareUsage({
|
|
316
|
+
requestedPackageSize: 1,
|
|
317
|
+
endpoint: 'http://127.0.0.1:8080/'
|
|
318
|
+
});
|
|
319
|
+
|
|
320
|
+
const replacementChar = String.fromCharCode(parseInt('0xFFFD', 16));
|
|
321
|
+
const pipeline = new PipelineBuilder()
|
|
322
|
+
.add(usageEngine)
|
|
323
|
+
.build();
|
|
324
|
+
|
|
325
|
+
const flowData = pipeline.createFlowData();
|
|
326
|
+
flowData.evidence.add('header.user-agent', '1ƌ2');
|
|
327
|
+
|
|
328
|
+
flowData.process().then(() => {
|
|
329
|
+
setTimeout(() => {
|
|
330
|
+
expect(received.length).toBe(1);
|
|
331
|
+
expect(received[0]).not.toContain('ƌ');
|
|
332
|
+
expect(received[0]).toContain(replacementChar);
|
|
333
|
+
expect(received[0]).toContain('replaced="true"');
|
|
334
|
+
expect(received[0]).toContain(`<header Name="user-agent" replaced="true">1${replacementChar}2</header>`);
|
|
335
|
+
done();
|
|
336
|
+
}, 2000);
|
|
337
|
+
});
|
|
338
|
+
});
|
|
339
|
+
|
|
340
|
+
/**
|
|
341
|
+
* Check that long values are truncated correctly.
|
|
342
|
+
*/
|
|
343
|
+
test('share usage - truncated value', done => {
|
|
344
|
+
const usageEngine = new ShareUsage({
|
|
345
|
+
requestedPackageSize: 1,
|
|
346
|
+
endpoint: 'http://127.0.0.1:8080/'
|
|
347
|
+
});
|
|
348
|
+
|
|
349
|
+
const ua = 'X'.repeat(1000);
|
|
350
|
+
const pipeline = new PipelineBuilder()
|
|
351
|
+
.add(usageEngine)
|
|
352
|
+
.build();
|
|
353
|
+
|
|
354
|
+
const flowData = pipeline.createFlowData();
|
|
355
|
+
flowData.evidence.add('header.user-agent', ua);
|
|
356
|
+
|
|
357
|
+
flowData.process().then(() => {
|
|
358
|
+
setTimeout(() => {
|
|
359
|
+
expect(received.length).toBe(1);
|
|
360
|
+
expect(received[0]).toContain('truncated="true"');
|
|
361
|
+
expect(received[0]).not.toContain(ua);
|
|
362
|
+
expect(received[0]).toContain(`<header Name="user-agent" truncated="true">${'X'.repeat(512)}</header>`);
|
|
363
|
+
done();
|
|
364
|
+
}, 2000);
|
|
365
|
+
});
|
|
366
|
+
});
|
|
367
|
+
|
|
368
|
+
/**
|
|
369
|
+
* Check that the queue of data is correctly reset, and used for the
|
|
370
|
+
* next set of requests.
|
|
371
|
+
*/
|
|
372
|
+
test('share usage - send more than once', done => {
|
|
373
|
+
const usageEngine = new ShareUsage({
|
|
374
|
+
requestedPackageSize: 1,
|
|
375
|
+
endpoint: 'http://127.0.0.1:8080/'
|
|
376
|
+
});
|
|
377
|
+
|
|
378
|
+
const pipeline = new PipelineBuilder()
|
|
379
|
+
.add(usageEngine)
|
|
380
|
+
.build();
|
|
381
|
+
|
|
382
|
+
const flowData1 = pipeline.createFlowData();
|
|
383
|
+
flowData1.evidence.add('header.user-agent', 'ua 1');
|
|
384
|
+
const flowData2 = pipeline.createFlowData();
|
|
385
|
+
flowData2.evidence.add('header.user-agent', 'ua 2');
|
|
386
|
+
|
|
387
|
+
flowData1.process().then(() => {
|
|
388
|
+
flowData2.process().then(() => {
|
|
389
|
+
setTimeout(() => {
|
|
390
|
+
expect(usageEngine.shareData.length).toBe(0);
|
|
391
|
+
expect(received.length).toBe(2);
|
|
392
|
+
expect(received[0]).toContain('ua 1');
|
|
393
|
+
expect(received[0]).not.toContain('ua 2');
|
|
394
|
+
expect(received[1]).toContain('ua 2');
|
|
395
|
+
expect(received[1]).not.toContain('ua 1');
|
|
396
|
+
done();
|
|
397
|
+
}, 2000);
|
|
398
|
+
});
|
|
399
|
+
});
|
|
400
|
+
});
|
|
401
|
+
|
|
402
|
+
/**
|
|
403
|
+
* Check that small portion sharing is done correctly.
|
|
404
|
+
*/
|
|
405
|
+
test('share usage - low percentage', function(done) {
|
|
406
|
+
const usageEngine = new ShareUsage({
|
|
407
|
+
requestedPackageSize: 10,
|
|
408
|
+
sharePercentage: 0.1,
|
|
409
|
+
endpoint: 'http://127.0.0.1:8080/'
|
|
410
|
+
});
|
|
411
|
+
|
|
412
|
+
const pipeline = new PipelineBuilder()
|
|
413
|
+
.add(usageEngine)
|
|
414
|
+
.build();
|
|
415
|
+
|
|
416
|
+
var requiredEvents = 0;
|
|
417
|
+
while (received.length == 0 && requiredEvents < 10000) {
|
|
418
|
+
let flowData = pipeline.createFlowData();
|
|
419
|
+
flowData.evidence.add('server.client-ip', '1.2.3.4');
|
|
420
|
+
flowData.evidence.add('header.user-agent', `ua ${requiredEvents}`);
|
|
421
|
+
usageEngine.processInternal(flowData);
|
|
422
|
+
requiredEvents++;
|
|
423
|
+
}
|
|
424
|
+
|
|
425
|
+
// Wait for a few seconds to make sure the requests from usage
|
|
426
|
+
// sharing have completed as they are asynchronous.
|
|
427
|
+
setTimeout(() => {
|
|
428
|
+
// On average, the number of required events should be around
|
|
429
|
+
// 100. However, as it's chance based it can vary
|
|
430
|
+
// significantly. We only want to catch any gross errors so just
|
|
431
|
+
// make sure the value is of the expected order of magnitude.
|
|
432
|
+
expect(received.length).toBeGreaterThan(10);
|
|
433
|
+
expect(received.length).toBeLessThan(1000);
|
|
434
|
+
done();
|
|
435
|
+
}, (3000));
|
|
436
|
+
});
|
|
437
|
+
|
|
438
|
+
/**
|
|
439
|
+
* Check that XML characters are escaped correctly.
|
|
440
|
+
*/
|
|
441
|
+
test('share usage - XML characters', done => {
|
|
442
|
+
const usageEngine = new ShareUsage({
|
|
443
|
+
requestedPackageSize: 1,
|
|
444
|
+
endpoint: 'http://127.0.0.1:8080/'
|
|
445
|
+
});
|
|
446
|
+
|
|
447
|
+
const pipeline = new PipelineBuilder()
|
|
448
|
+
.add(usageEngine)
|
|
449
|
+
.build();
|
|
450
|
+
|
|
451
|
+
const flowData = pipeline.createFlowData();
|
|
452
|
+
flowData.evidence.add('header.user-agent', '"\'&><');
|
|
453
|
+
|
|
454
|
+
flowData.process().then(() => {
|
|
455
|
+
setTimeout(() => {
|
|
456
|
+
expect(received.length).toBe(1);
|
|
457
|
+
expect(received[0]).toContain('"');
|
|
458
|
+
expect(received[0]).toContain(''');
|
|
459
|
+
expect(received[0]).toContain('<');
|
|
460
|
+
expect(received[0]).toContain('>');
|
|
461
|
+
expect(received[0]).toContain('&');
|
|
462
|
+
done();
|
|
463
|
+
}, 2000);
|
|
464
|
+
});
|
|
465
|
+
});
|