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 CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "fiftyone.pipeline.engines.fiftyone",
3
- "version": "4.4.3",
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.3",
15
- "fiftyone.pipeline.engines": "^4.4.3"
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 https = require('https');
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 percentage of requests to share.
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 = 100
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 (this.sharePercentageCounter === this.sharePercentage) {
119
- this.sharePercentageCounter = 0;
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
- const cacheKey = this.evidenceKeyFilter
127
- .filterEvidence(flowData.evidence.getAll());
143
+ const share = this.tracker.track(cacheKey);
144
+ if (share) {
145
+ this.tracker.put(cacheKey);
128
146
 
129
- const share = this.tracker.track(cacheKey);
147
+ this.addToShareUsage(this.getDataFromEvidence(flowData));
148
+ }
149
+ }
150
+ }
130
151
 
131
- if (share) {
132
- this.tracker.put(cacheKey);
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
- this.addToShareUsage(cacheKey);
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} key key value store of current
223
+ * @param {object} data key value store of current
142
224
  * evidence in FlowData (filtered by the ShareUsageEvidenceKeyFilter)
143
225
  */
144
- addToShareUsage (key) {
226
+ addToShareUsage (data) {
145
227
  let xml = '';
146
-
147
- xml += '<device>';
148
-
149
- xml += '<Version>4</Version>';
150
-
151
- xml += '<Language>Node.JS</Language>';
152
-
153
- xml += '<LanguageVersion>' + process.version + '</LanguageVersion>';
154
-
155
- xml += '<Platform>' + process.platform + ' ' + os.release() + '</Platform>';
156
-
157
- for (const item in key) {
158
- const parts = item.split('.');
159
-
160
- const prefix = parts[0];
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
- .substr(0, 19)
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 += '<dateSent>' + date + '</dateSent>';
175
-
176
- xml += '</device>';
283
+ xml += '</Device>';
177
284
 
178
285
  this.shareData.push(xml);
179
-
180
- if (this.shareData.length === this.requestedPackageSize) {
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 data = '<devices>' + this.shareData.join() + '</devices>';
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: 'devices-v4.51degrees.com',
193
- path: '/new.ashx',
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
- this.pipeline.emit('warning', err);
311
+ usageEngine._log('warning', err);
201
312
  }
202
-
203
- var req = https.request(options, function (res) {
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
- this.pipeline.emit('warning', e);
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, '&amp;')
408
+ .replace(/"/g, '&quot;')
409
+ .replace(/'/g, '&apos;')
410
+ .replace(/</g, '&lt;')
411
+ .replace(/>/g, '&gt;');
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.queryWhitelist = queryWhitelist;
52
- this.headerBlacklist = headerBlacklist;
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
- const allowed = ['header', 'cookie', 'query'];
67
-
68
- // First filter out anything that isn't in the allowed list
69
-
70
- if (!allowed.includes(prefix)) {
71
- return false;
72
- } else {
73
- // Filter out all cookies that aren't either 51D prefixed
74
- // or the tracking cookie
75
-
76
- if (prefix === 'cookie' && (
77
- prefix.indexOf('51D') !== 0 || suffix === this.cookie
78
- )
79
- ) {
80
- return false;
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('&quot;');
458
+ expect(received[0]).toContain('&apos;');
459
+ expect(received[0]).toContain('&lt;');
460
+ expect(received[0]).toContain('&gt;');
461
+ expect(received[0]).toContain('&amp;');
462
+ done();
463
+ }, 2000);
464
+ });
465
+ });