noibu-react-native 0.2.1 → 0.2.3
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/README.md +1 -1
- package/android/src/main/java/com/noibu/sessionreplay/reactnative/NoibuSessionReplayModule.kt +10 -0
- package/dist/api/clientConfig.js +225 -217
- package/dist/api/metroplexSocket.js +406 -416
- package/dist/constants.js +14 -2
- package/dist/entry/init.js +58 -56
- package/dist/monitors/appNavigationMonitor.js +2 -3
- package/dist/monitors/clickMonitor.js +16 -9
- package/dist/monitors/errorMonitor.js +30 -8
- package/dist/monitors/gqlErrorValidator.js +4 -4
- package/dist/monitors/httpDataBundler.js +525 -713
- package/dist/monitors/integrations/react-native-navigation-integration.js +4 -2
- package/dist/monitors/requestMonitor.js +350 -365
- package/dist/pageVisit/eventDebouncer.js +110 -0
- package/dist/pageVisit/pageVisitEventError.js +1 -1
- package/dist/pageVisit/pageVisitEventHTTP.js +78 -93
- package/dist/react/ErrorBoundary.js +18 -15
- package/dist/sessionRecorder/nativeSessionRecorderSubscription.js +3 -2
- package/dist/sessionRecorder/sessionRecorder.js +151 -150
- package/dist/{api → src/api}/clientConfig.d.ts +1 -1
- package/dist/{api → src/api}/metroplexSocket.d.ts +25 -25
- package/dist/{constants.d.ts → src/constants.d.ts} +44 -0
- package/dist/{entry → src/entry}/init.d.ts +1 -1
- package/dist/{monitors → src/monitors}/clickMonitor.d.ts +1 -1
- package/dist/{monitors → src/monitors}/gqlErrorValidator.d.ts +6 -6
- package/dist/src/monitors/httpDataBundler.d.ts +127 -0
- package/dist/src/monitors/requestMonitor.d.ts +10 -0
- package/dist/src/pageVisit/eventDebouncer.d.ts +31 -0
- package/dist/src/pageVisit/pageVisitEventHTTP.d.ts +25 -0
- package/dist/{sessionRecorder → src/sessionRecorder}/types.d.ts +1 -1
- package/dist/{storage → src/storage}/rnStorageProvider.d.ts +1 -1
- package/dist/{storage → src/storage}/storage.d.ts +1 -1
- package/dist/{storage → src/storage}/storageProvider.d.ts +3 -3
- package/dist/{utils → src/utils}/function.d.ts +25 -4
- package/dist/{utils → src/utils}/object.d.ts +9 -4
- package/dist/src/utils/piiRedactor.d.ts +11 -0
- package/dist/src/utils/polyfills.d.ts +7 -0
- package/dist/storage/rnStorageProvider.js +7 -5
- package/dist/storage/storage.js +43 -35
- package/dist/storage/storageProvider.js +23 -19
- package/dist/types/Config.d.ts +24 -20
- package/dist/types/PageVisit.types.d.ts +151 -0
- package/dist/types/PageVisitMetrics.types.d.ts +27 -0
- package/dist/types/RRWeb.d.ts +48 -0
- package/dist/types/StoredPageVisit.types.d.ts +2 -4
- package/dist/types/WrappedObjects.d.ts +6 -0
- package/dist/utils/function.js +110 -76
- package/dist/utils/object.js +58 -6
- package/dist/utils/piiRedactor.js +98 -0
- package/dist/utils/polyfills.js +24 -0
- package/package.json +24 -9
- package/dist/monitors/httpDataBundler.d.ts +0 -161
- package/dist/monitors/requestMonitor.d.ts +0 -10
- package/dist/pageVisit/pageVisitEventHTTP.d.ts +0 -18
- package/dist/types/PageVisit.d.ts +0 -22
- package/dist/types/globals.d.ts +0 -45
- /package/dist/{api → src/api}/helpCode.d.ts +0 -0
- /package/dist/{api → src/api}/inputManager.d.ts +0 -0
- /package/dist/{api → src/api}/storedMetrics.d.ts +0 -0
- /package/dist/{api → src/api}/storedPageVisit.d.ts +0 -0
- /package/dist/{const_matchers.d.ts → src/const_matchers.d.ts} +0 -0
- /package/dist/{entry → src/entry}/index.d.ts +0 -0
- /package/dist/{monitors → src/monitors}/appNavigationMonitor.d.ts +0 -0
- /package/dist/{monitors → src/monitors}/errorMonitor.d.ts +0 -0
- /package/dist/{monitors → src/monitors}/inputMonitor.d.ts +0 -0
- /package/dist/{monitors → src/monitors}/integrations/react-native-navigation-integration.d.ts +0 -0
- /package/dist/{monitors → src/monitors}/keyboardInputMonitor.d.ts +0 -0
- /package/dist/{monitors → src/monitors}/pageMonitor.d.ts +0 -0
- /package/dist/{pageVisit → src/pageVisit}/pageVisit.d.ts +0 -0
- /package/dist/{pageVisit → src/pageVisit}/pageVisitEventError.d.ts +0 -0
- /package/dist/{pageVisit → src/pageVisit}/userStep.d.ts +0 -0
- /package/dist/{react → src/react}/ErrorBoundary.d.ts +0 -0
- /package/dist/{sessionRecorder → src/sessionRecorder}/nativeSessionRecorderSubscription.d.ts +0 -0
- /package/dist/{sessionRecorder → src/sessionRecorder}/sessionRecorder.d.ts +0 -0
- /package/dist/{utils → src/utils}/date.d.ts +0 -0
- /package/dist/{utils → src/utils}/eventlistener.d.ts +0 -0
- /package/dist/{utils → src/utils}/log.d.ts +0 -0
- /package/dist/{utils → src/utils}/performance.d.ts +0 -0
- /package/dist/{utils → src/utils}/stacktrace-parser.d.ts +0 -0
|
@@ -1,725 +1,537 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import { __awaiter } from 'tslib';
|
|
2
|
+
import { HUMAN_READABLE_CONTENT_TYPE_REGEX, DEFAULT_WEBSITE_SUBDOMAIN_PATTERN, SEVERITY, HTTP_BODY_NULL_STRING, HTTP_DATA_REQ_HEADERS_ATT_NAME, HTTP_DATA_PAYLOAD_ATT_NAME, HTTP_DATA_RESP_HEADERS_ATT_NAME, HTTP_DATA_RESP_PAYLOAD_ATT_NAME, HTTP_BODY_DROPPED_LENGTH_MSG, HTTP_BODY_DROPPED_TYPE_MSG, CONTENT_TYPE, CONTENT_LENGTH, BLOCKED_HTTP_HEADER_KEYS, PII_REDACTION_REPLACEMENT_STRING, MAX_HTTP_DATA_PAYLOAD_LENGTH, MAX_SUCCESS_HTTP_DATA_PAYLOAD_LENGTH } from '../constants.js';
|
|
2
3
|
import ClientConfig from '../api/clientConfig.js';
|
|
3
4
|
import StoredMetrics from '../api/storedMetrics.js';
|
|
4
|
-
import {
|
|
5
|
-
import {
|
|
6
|
-
|
|
7
|
-
/** @module HTTPDataBundler */
|
|
5
|
+
import { safeFromEntries, safeEntries } from '../utils/object.js';
|
|
6
|
+
import { safeTrim, stringifyJSON, isString } from '../utils/function.js';
|
|
7
|
+
import { removePII } from '../utils/piiRedactor.js';
|
|
8
8
|
|
|
9
9
|
/** Bundles HTTP payloads and headers */
|
|
10
10
|
class HTTPDataBundler {
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
ClientConfig.getInstance().
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
}
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
'lastname',
|
|
77
|
-
'street',
|
|
78
|
-
'fullname',
|
|
79
|
-
'creditcard',
|
|
80
|
-
'postcode',
|
|
81
|
-
'zipcode',
|
|
82
|
-
'city',
|
|
83
|
-
'town',
|
|
84
|
-
'county',
|
|
85
|
-
'cc',
|
|
86
|
-
];
|
|
87
|
-
}
|
|
88
|
-
|
|
89
|
-
/** gets http data payload allowed URLs */
|
|
90
|
-
static getHttpPayloadAllowedURLs() {
|
|
91
|
-
const noibuConfig = ClientConfig.getInstance();
|
|
92
|
-
// return the allowed list or an empty list
|
|
93
|
-
if (
|
|
94
|
-
noibuConfig.listOfUrlsToCollectHttpDataFrom &&
|
|
95
|
-
Array.isArray(noibuConfig.listOfUrlsToCollectHttpDataFrom)
|
|
96
|
-
) {
|
|
97
|
-
return noibuConfig.listOfUrlsToCollectHttpDataFrom;
|
|
98
|
-
}
|
|
99
|
-
return [];
|
|
100
|
-
}
|
|
101
|
-
|
|
102
|
-
/**
|
|
103
|
-
* gets the singleton instance
|
|
104
|
-
* @returns {HTTPDataBundler}
|
|
105
|
-
*/
|
|
106
|
-
static getInstance() {
|
|
107
|
-
if (!this.instance) {
|
|
108
|
-
this.instance = new HTTPDataBundler();
|
|
109
|
-
}
|
|
110
|
-
|
|
111
|
-
return this.instance;
|
|
112
|
-
}
|
|
113
|
-
|
|
114
|
-
/**
|
|
115
|
-
* Builds the HTTP payload allowed regexes for full and relative URLs
|
|
116
|
-
* @param allowedURLs A list of allowed URLs
|
|
117
|
-
* @param {'absolute' | 'relative'} strategy Use only absolute URLs if true, use only relative URL if false
|
|
118
|
-
* @returns a regex of allowed URLs
|
|
119
|
-
*/
|
|
120
|
-
static buildAllowedRegex(allowedURLs, strategy) {
|
|
121
|
-
if (!allowedURLs) return null;
|
|
122
|
-
const allowedURLsFiltered = allowedURLs.filter(url => {
|
|
123
|
-
const isAbsolute = HTTPDataBundler.isAbsoluteURL(url);
|
|
124
|
-
return strategy === 'absolute' ? isAbsolute : !isAbsolute;
|
|
125
|
-
});
|
|
126
|
-
if (allowedURLsFiltered.length > 0) {
|
|
127
|
-
const lowerCasedURLs = allowedURLsFiltered.map(url =>
|
|
128
|
-
url.trim().toLowerCase(),
|
|
129
|
-
);
|
|
130
|
-
return new RegExp(lowerCasedURLs.join('|'));
|
|
131
|
-
}
|
|
132
|
-
return null;
|
|
133
|
-
}
|
|
134
|
-
|
|
135
|
-
/**
|
|
136
|
-
* Takes an iterator and returns a map of strings representing headers.
|
|
137
|
-
* @param {object} headersIterable any iterable object
|
|
138
|
-
* @returns a map of strings (as expected by metroplex) representing HTTP
|
|
139
|
-
* request or response headers
|
|
140
|
-
*/
|
|
141
|
-
static headersMapFromIterable(headersIterable) {
|
|
142
|
-
const headerMap = new Map();
|
|
143
|
-
// eslint-disable-next-line no-restricted-syntax
|
|
144
|
-
for (const header of headersIterable) {
|
|
145
|
-
// Ensure we're working with strings
|
|
146
|
-
// This will also automatically convert null to "null"
|
|
147
|
-
if (typeof header[0] !== 'string') header[0] = String(header[0]);
|
|
148
|
-
if (typeof header[1] !== 'string') header[1] = String(header[1]);
|
|
149
|
-
|
|
150
|
-
headerMap.set(header[0].toLowerCase(), header[1]);
|
|
151
|
-
}
|
|
152
|
-
return headerMap;
|
|
153
|
-
}
|
|
154
|
-
|
|
155
|
-
/**
|
|
156
|
-
* Takes a string of headers with 'name: value' and returns
|
|
157
|
-
* a map of strings representing headers.
|
|
158
|
-
* @param {string} headersString is all the headers in one string
|
|
159
|
-
* @returns a map of strings (as expected by metroplex) representing HTTP
|
|
160
|
-
* request or response headers
|
|
161
|
-
*/
|
|
162
|
-
static headersMapFromString(headersString) {
|
|
163
|
-
const headerMap = new Map();
|
|
164
|
-
if (!headersString || typeof headersString !== 'string') return headerMap;
|
|
165
|
-
const headerStringArray = headersString.split('\r\n').filter(Boolean);
|
|
166
|
-
headerStringArray.forEach(headerString => {
|
|
167
|
-
const split = headerString.split(': ');
|
|
168
|
-
if (split.length === 2 && split[0].length > 0 && split[1].length > 0) {
|
|
169
|
-
headerMap.set(split[0].toLowerCase(), split[1]);
|
|
170
|
-
}
|
|
171
|
-
});
|
|
172
|
-
return headerMap;
|
|
173
|
-
}
|
|
174
|
-
|
|
175
|
-
/**
|
|
176
|
-
* For an XHR object, checks the responseType property and handles the response or
|
|
177
|
-
* responseText property accordingly to return a string representation of the response.
|
|
178
|
-
* @param {object} xhr an XMLHTTPRequest object
|
|
179
|
-
* @returns a string representation of the response, or null if this fails.
|
|
180
|
-
*/
|
|
181
|
-
static responseStringFromXHRResponseType(xhr) {
|
|
182
|
-
// This should not happen, as this function is called from a method on xhr.
|
|
183
|
-
if (xhr === undefined || xhr === null) return null;
|
|
184
|
-
|
|
185
|
-
// If the XHR object exists but the response is null, return null as a string.
|
|
186
|
-
// noinspection PointlessBooleanExpressionJS
|
|
187
|
-
if (xhr.response && xhr.response === null) return HTTP_BODY_NULL_STRING;
|
|
188
|
-
|
|
189
|
-
if (xhr.responseType === '' || xhr.responseType === 'text') {
|
|
190
|
-
return xhr.responseText;
|
|
191
|
-
}
|
|
192
|
-
if (
|
|
193
|
-
xhr.responseType === 'document' &&
|
|
194
|
-
!!xhr.response &&
|
|
195
|
-
!!xhr.response.documentElement &&
|
|
196
|
-
!!xhr.response.documentElement.innerHTML
|
|
197
|
-
) {
|
|
198
|
-
return xhr.response.documentElement.innerHTML;
|
|
199
|
-
}
|
|
200
|
-
if (xhr.responseType === 'json') {
|
|
201
|
-
try {
|
|
202
|
-
return JSON.stringify(xhr.response);
|
|
203
|
-
} catch (e) {
|
|
204
|
-
ClientConfig.getInstance().postNoibuErrorAndOptionallyDisableClient(
|
|
205
|
-
`Unable to stringify JSON response: ${e}`,
|
|
206
|
-
false,
|
|
207
|
-
SEVERITY.warn,
|
|
208
|
-
);
|
|
11
|
+
/**
|
|
12
|
+
* Creates an instance of the ClickMonitor instance
|
|
13
|
+
*/
|
|
14
|
+
constructor() {
|
|
15
|
+
// compile regex only once
|
|
16
|
+
this.contentTypeReadableRegex = new RegExp(HUMAN_READABLE_CONTENT_TYPE_REGEX, 'i');
|
|
17
|
+
// pull out the domain hostname
|
|
18
|
+
const initialURL = ClientConfig.getInstance().globalUrl;
|
|
19
|
+
this.initialURLPartsReversed = [];
|
|
20
|
+
this.hostname = '';
|
|
21
|
+
if (initialURL && initialURL.length > 0) {
|
|
22
|
+
try {
|
|
23
|
+
// Store URL parts to class variable
|
|
24
|
+
this.hostname = new URL(initialURL).hostname;
|
|
25
|
+
this.initialURLPartsReversed = this.hostname.split('.');
|
|
26
|
+
// Remove www. etc.
|
|
27
|
+
// eslint-disable-next-line no-unused-expressions
|
|
28
|
+
DEFAULT_WEBSITE_SUBDOMAIN_PATTERN.test(this.initialURLPartsReversed[0]) && this.initialURLPartsReversed.shift();
|
|
29
|
+
// Reverse array
|
|
30
|
+
this.initialURLPartsReversed.reverse();
|
|
31
|
+
}
|
|
32
|
+
catch (e) {
|
|
33
|
+
ClientConfig.getInstance().postNoibuErrorAndOptionallyDisableClient(`Unable to determine hostname for initial URL: ${e}`, false, SEVERITY.warn);
|
|
34
|
+
}
|
|
35
|
+
}
|
|
36
|
+
this.httpDataCollectionEnabled =
|
|
37
|
+
!!ClientConfig.getInstance().enableHttpDataCollection;
|
|
38
|
+
// compile the relative and full HTTP URL regexes
|
|
39
|
+
const allowedURLs = ClientConfig.getInstance().listOfUrlsToCollectHttpDataFrom;
|
|
40
|
+
this.httpDataAllowedAbsoluteRegex = HTTPDataBundler.buildAllowedRegex(allowedURLs, true);
|
|
41
|
+
this.httpDataAllowedRelativeRegex = HTTPDataBundler.buildAllowedRegex(allowedURLs, false);
|
|
42
|
+
// track unique requests and only capture each once
|
|
43
|
+
// TODO: disabled for beta. NOI-4253
|
|
44
|
+
// this.uniqueRequests = new Set();
|
|
45
|
+
}
|
|
46
|
+
/** gets the singleton instance
|
|
47
|
+
* @returns {HTTPDataBundler}
|
|
48
|
+
* */
|
|
49
|
+
static getInstance() {
|
|
50
|
+
if (!this.instance) {
|
|
51
|
+
this.instance = new HTTPDataBundler();
|
|
52
|
+
}
|
|
53
|
+
return this.instance;
|
|
54
|
+
}
|
|
55
|
+
/**
|
|
56
|
+
* Builds the HTTP payload allowed regexes for full and relative URLs
|
|
57
|
+
* @param allowedURLs A list of allowed URLs
|
|
58
|
+
* @param absolute Use only absolute URLs if true, use only relative URL if false
|
|
59
|
+
* @returns a regex of allowed URLs
|
|
60
|
+
*/
|
|
61
|
+
static buildAllowedRegex(allowedURLs, absolute) {
|
|
62
|
+
if (!allowedURLs || !Array.isArray(allowedURLs))
|
|
63
|
+
return null;
|
|
64
|
+
const allowedURLsFiltered = allowedURLs
|
|
65
|
+
.map(url => safeTrim(url).toLowerCase())
|
|
66
|
+
.filter(url => {
|
|
67
|
+
const isAbsolute = HTTPDataBundler.isAbsoluteURL(url);
|
|
68
|
+
if (absolute) {
|
|
69
|
+
return url && isAbsolute;
|
|
70
|
+
}
|
|
71
|
+
return url && !isAbsolute;
|
|
72
|
+
});
|
|
73
|
+
if (allowedURLsFiltered.length > 0) {
|
|
74
|
+
return new RegExp(allowedURLsFiltered.join('|'));
|
|
75
|
+
}
|
|
209
76
|
return null;
|
|
210
|
-
}
|
|
211
|
-
}
|
|
212
|
-
return null;
|
|
213
|
-
}
|
|
214
|
-
|
|
215
|
-
/**
|
|
216
|
-
* Takes a URL and returns true if it is determined to be on the same domain as the URL
|
|
217
|
-
* the script is running on. Ignores protocol and path, and allows the URL to be a subdomain.
|
|
218
|
-
* @param {string} requestURL the URL of a request to compare to the script website's URL
|
|
219
|
-
*/
|
|
220
|
-
isURLSameDomain(requestURL) {
|
|
221
|
-
if (
|
|
222
|
-
typeof requestURL !== 'string' ||
|
|
223
|
-
!this.initialURLPartsReversed ||
|
|
224
|
-
this.initialURLPartsReversed.length < 1
|
|
225
|
-
) {
|
|
226
|
-
return false;
|
|
227
|
-
}
|
|
228
|
-
|
|
229
|
-
let requestHostname; // has to be declared outside `try`
|
|
230
|
-
|
|
231
|
-
// Use URL constructor to extract hostname
|
|
232
|
-
try {
|
|
233
|
-
requestHostname = new URL(requestURL).hostname;
|
|
234
|
-
} catch (e) {
|
|
235
|
-
// Return false if URL() is unable to parse the request URL
|
|
236
|
-
ClientConfig.getInstance().postNoibuErrorAndOptionallyDisableClient(
|
|
237
|
-
`Unable to determine hostname for request URL: ${e}`,
|
|
238
|
-
false,
|
|
239
|
-
SEVERITY.warn,
|
|
240
|
-
);
|
|
241
|
-
|
|
242
|
-
return false;
|
|
243
|
-
}
|
|
244
|
-
|
|
245
|
-
const requestParts = requestHostname.split('.');
|
|
246
|
-
|
|
247
|
-
// Remove first subdomain from either URL if it is www., www1., etc.
|
|
248
|
-
|
|
249
|
-
if (requestParts.length < 1) return false;
|
|
250
|
-
|
|
251
|
-
/* eslint-disable no-unused-expressions */
|
|
252
|
-
DEFAULT_WEBSITE_SUBDOMAIN_PATTERN.test(requestParts[0]) &&
|
|
253
|
-
requestParts.shift();
|
|
254
|
-
/* eslint-enable no-unused-expressions */
|
|
255
|
-
|
|
256
|
-
if (requestParts.length < this.initialURLPartsReversed.length) return false;
|
|
257
|
-
|
|
258
|
-
// Reverse order array
|
|
259
|
-
requestParts.reverse();
|
|
260
|
-
|
|
261
|
-
// Return true if for every part in the website part, the same part exists
|
|
262
|
-
// in the request part.
|
|
263
|
-
return this.initialURLPartsReversed.every(
|
|
264
|
-
(part, i) => part === requestParts[i],
|
|
265
|
-
);
|
|
266
|
-
}
|
|
267
|
-
|
|
268
|
-
/**
|
|
269
|
-
* Builds an HTTP Data bundle
|
|
270
|
-
* @param url
|
|
271
|
-
* @param requestHeaders is a map of request headers
|
|
272
|
-
* @param rawRequestPayload
|
|
273
|
-
* @param responseHeaders is a map of response headers
|
|
274
|
-
* @param responsePayload
|
|
275
|
-
* @param method
|
|
276
|
-
* @param shouldCollectPayload
|
|
277
|
-
*/
|
|
278
|
-
bundleHTTPData(
|
|
279
|
-
url,
|
|
280
|
-
requestHeaders,
|
|
281
|
-
rawRequestPayload,
|
|
282
|
-
responseHeaders,
|
|
283
|
-
responsePayload,
|
|
284
|
-
method,
|
|
285
|
-
shouldCollectPayload,
|
|
286
|
-
) {
|
|
287
|
-
noibuLog('bundleHTTPData started for', method, url);
|
|
288
|
-
if (!this.isValidRequest(url, method)) {
|
|
289
|
-
noibuLog('quitting');
|
|
290
|
-
return null;
|
|
291
|
-
}
|
|
292
|
-
|
|
293
|
-
// stringify payload if correct type and not too large
|
|
294
|
-
let requestPayload = '';
|
|
295
|
-
if (shouldCollectPayload) {
|
|
296
|
-
noibuLog('bundleHTTPData collecting payload');
|
|
297
|
-
requestPayload =
|
|
298
|
-
this.dropPayloadIfNecessaryFromHeaders(requestHeaders) ||
|
|
299
|
-
this.stringFromRequestBody(rawRequestPayload);
|
|
300
|
-
} else {
|
|
301
|
-
noibuLog('bundleHTTPData not collecting payload');
|
|
302
|
-
}
|
|
303
|
-
|
|
304
|
-
// don't bundle if there is no data
|
|
305
|
-
const safeRequestHeaders = requestHeaders || new Map();
|
|
306
|
-
const safeRequestPayload = requestPayload || '';
|
|
307
|
-
const safeResponseHeaders = responseHeaders || new Map();
|
|
308
|
-
const safeResponsePayload = responsePayload || '';
|
|
309
|
-
if (
|
|
310
|
-
safeRequestHeaders.size === 0 &&
|
|
311
|
-
safeRequestPayload.length === 0 &&
|
|
312
|
-
safeResponseHeaders.size === 0 &&
|
|
313
|
-
safeResponsePayload.length === 0
|
|
314
|
-
) {
|
|
315
|
-
noibuLog('bundleHTTPData payload is empty, quitting');
|
|
316
|
-
return null;
|
|
317
|
-
}
|
|
318
|
-
|
|
319
|
-
// Ensure payloads do not exceed the maximum size and redact PII
|
|
320
|
-
const requestPayloadUpdated = this.restrictPayload(requestPayload, url);
|
|
321
|
-
// Ensure payloads do not exceed the maximum size and redact PII
|
|
322
|
-
const responsePayloadUpdated = this.restrictPayload(responsePayload, url);
|
|
323
|
-
|
|
324
|
-
// Redact PII.
|
|
325
|
-
const cleanRequestHeaders = this.removePIIHeaders(requestHeaders);
|
|
326
|
-
const cleanResponseHeaders = this.removePIIHeaders(responseHeaders);
|
|
327
|
-
|
|
328
|
-
// TODO: disabled for beta. NOI-4253
|
|
329
|
-
// we are capturing this request, so add it to the unique requests set
|
|
330
|
-
// this.uniqueRequests.add(requestSignature);
|
|
331
|
-
// build the http bundle
|
|
332
|
-
return {
|
|
333
|
-
[HTTP_DATA_REQ_HEADERS_ATT_NAME]: cleanRequestHeaders
|
|
334
|
-
? Object.fromEntries(cleanRequestHeaders)
|
|
335
|
-
: {},
|
|
336
|
-
[HTTP_DATA_PAYLOAD_ATT_NAME]: requestPayloadUpdated,
|
|
337
|
-
[HTTP_DATA_RESP_HEADERS_ATT_NAME]: cleanResponseHeaders
|
|
338
|
-
? Object.fromEntries(cleanResponseHeaders)
|
|
339
|
-
: {},
|
|
340
|
-
[HTTP_DATA_RESP_PAYLOAD_ATT_NAME]: responsePayloadUpdated,
|
|
341
|
-
};
|
|
342
|
-
}
|
|
343
|
-
|
|
344
|
-
/**
|
|
345
|
-
* Validates a request based on the URL and method. When enabled, will handle
|
|
346
|
-
* de-duping the requests
|
|
347
|
-
* @param {string} url
|
|
348
|
-
* @param {string} method
|
|
349
|
-
* @returns boolean indicating whether the validation passed
|
|
350
|
-
*/
|
|
351
|
-
isValidRequest(url, method) {
|
|
352
|
-
noibuLog('isValidRequest');
|
|
353
|
-
if (!this.httpDataCollectionEnabled) {
|
|
354
|
-
noibuLog('isValidRequest httpDataCollectionEnabled is off, quitting');
|
|
355
|
-
return false;
|
|
356
|
-
}
|
|
357
|
-
if (!method || typeof method !== 'string') {
|
|
358
|
-
noibuLog('isValidRequest method is invalid, quitting');
|
|
359
|
-
return false;
|
|
360
|
-
}
|
|
361
|
-
// TODO: Check below disabled for beta. NOI-4253
|
|
362
|
-
// only bundle and send once per request/method/response code combination
|
|
363
|
-
// const urlWithoutParams = url.split('?')[0];
|
|
364
|
-
// const requestSignature = `${urlWithoutParams}-${method}-${responseCode}`;
|
|
365
|
-
// if (this.uniqueRequests.has(requestSignature)) {
|
|
366
|
-
// return false;
|
|
367
|
-
// }
|
|
368
|
-
|
|
369
|
-
return true;
|
|
370
|
-
}
|
|
371
|
-
|
|
372
|
-
/**
|
|
373
|
-
* Checks two things: that the URL is either on the same domain (or an address relative to the
|
|
374
|
-
* current domain), and also checks that the config http_data_collection flag is enabled.
|
|
375
|
-
* @param {string} url
|
|
376
|
-
* @returns boolean indicating whether the URL passed is either relative (in which case it is
|
|
377
|
-
* inherently on the current domain) or matches the current domain.
|
|
378
|
-
*/
|
|
379
|
-
shouldContinueForURL(url) {
|
|
380
|
-
noibuLog('shouldContinueForURL started for', url);
|
|
381
|
-
if (!this.httpDataCollectionEnabled) {
|
|
382
|
-
noibuLog('shouldContinueForURL httpDataCollection turned off, quitting');
|
|
383
|
-
return false;
|
|
384
|
-
}
|
|
385
|
-
if (!url || typeof url !== 'string' || !this.initialURLPartsReversed) {
|
|
386
|
-
noibuLog('shouldContinueForURL url is not valid, quitting', {
|
|
387
|
-
initialUrlPartsReversed: this.initialURLPartsReversed,
|
|
388
|
-
});
|
|
389
|
-
return false;
|
|
390
|
-
}
|
|
391
|
-
// URL is absolute; either "http://example.com" or "//example.com"
|
|
392
|
-
if (HTTPDataBundler.isAbsoluteURL(url)) {
|
|
393
|
-
// Only capture requests on the same domain or in the allowed list
|
|
394
|
-
if (!this.isURLSameDomain(url) && !this.shouldCollectPayloadForURL(url)) {
|
|
395
|
-
noibuLog(
|
|
396
|
-
'shouldContinueForURL url is not same domain or is not in allow list, quitting',
|
|
397
|
-
{
|
|
398
|
-
shouldCollectPayloadForURL: this.shouldCollectPayloadForURL(url),
|
|
399
|
-
},
|
|
400
|
-
);
|
|
401
|
-
return false;
|
|
402
|
-
}
|
|
403
|
-
} // end `if` (if URL is relative, it is on the same domain.)
|
|
404
|
-
|
|
405
|
-
noibuLog('shouldContinueForURL - yes');
|
|
406
|
-
return true;
|
|
407
|
-
}
|
|
408
|
-
|
|
409
|
-
/**
|
|
410
|
-
* Determins if the URL is absolute or relative
|
|
411
|
-
* @param {string} url
|
|
412
|
-
* @returns boolean indicating whether the URL passed is either absolute or relative
|
|
413
|
-
*/
|
|
414
|
-
static isAbsoluteURL(url) {
|
|
415
|
-
if (!url || typeof url !== 'string') {
|
|
416
|
-
return false;
|
|
417
|
-
}
|
|
418
|
-
return url.indexOf('://') > 0 || url.indexOf('//') === 0;
|
|
419
|
-
}
|
|
420
|
-
|
|
421
|
-
/**
|
|
422
|
-
* Checks whether HTTP payloads can be collected on this URL
|
|
423
|
-
* @param {string} url
|
|
424
|
-
* @returns boolean indicating whether HTTP payloads can be collected on this URL
|
|
425
|
-
*/
|
|
426
|
-
shouldCollectPayloadForURL(url) {
|
|
427
|
-
noibuLog('shouldCollectPayloadForURL', url);
|
|
428
|
-
if (!url || typeof url !== 'string') {
|
|
429
|
-
noibuLog('shouldCollectPayloadForURL url is invalid, quitting');
|
|
430
|
-
return false;
|
|
431
|
-
}
|
|
432
|
-
// check if in the full URL allowed list
|
|
433
|
-
if (
|
|
434
|
-
this.httpDataAllowedAbsoluteRegex &&
|
|
435
|
-
this.httpDataAllowedAbsoluteRegex.test(url.toLowerCase())
|
|
436
|
-
) {
|
|
437
|
-
noibuLog('shouldCollectPayloadForURL yes, absolute');
|
|
438
|
-
return true;
|
|
439
|
-
}
|
|
440
|
-
// check if in the relative URL allowed list, if on domain
|
|
441
|
-
if (
|
|
442
|
-
this.httpDataAllowedRelativeRegex &&
|
|
443
|
-
// if this is a relative URL (isURLSameDomain will fail) OR a full URL on domain
|
|
444
|
-
(!HTTPDataBundler.isAbsoluteURL(url) || this.isURLSameDomain(url)) &&
|
|
445
|
-
this.httpDataAllowedRelativeRegex.test(url.toLowerCase())
|
|
446
|
-
) {
|
|
447
|
-
noibuLog('shouldCollectPayloadForURL yes, relative');
|
|
448
|
-
return true;
|
|
449
|
-
}
|
|
450
|
-
|
|
451
|
-
noibuLog('shouldCollectPayloadForURL answer is no, reason', {
|
|
452
|
-
httpDataAllowedAbsoluteRegexTest:
|
|
453
|
-
this.httpDataAllowedAbsoluteRegex &&
|
|
454
|
-
this.httpDataAllowedAbsoluteRegex.test(url.toLowerCase()),
|
|
455
|
-
httpDataAllowedRelativeRegexTest:
|
|
456
|
-
this.httpDataAllowedRelativeRegex &&
|
|
457
|
-
this.httpDataAllowedRelativeRegex.test(url.toLowerCase()),
|
|
458
|
-
isAbsoluteURL: HTTPDataBundler.isAbsoluteURL(url),
|
|
459
|
-
isURLSameDomain: this.isURLSameDomain(url),
|
|
460
|
-
});
|
|
461
|
-
return false;
|
|
462
|
-
}
|
|
463
|
-
|
|
464
|
-
/**
|
|
465
|
-
* Double checks content length if we couldn't read the headers, and redacts PII
|
|
466
|
-
* @param {string} payload
|
|
467
|
-
* @param {string} url
|
|
468
|
-
* @returns the restricted payload
|
|
469
|
-
*/
|
|
470
|
-
restrictPayload(payload, url) {
|
|
471
|
-
// if no payload, or too large, payload already dropped,
|
|
472
|
-
// or payloads not allowed for this URL: nothing to do
|
|
473
|
-
if (!payload || !this.shouldCollectPayloadForURL(url)) {
|
|
474
|
-
return HTTP_BODY_NULL_STRING;
|
|
475
|
-
}
|
|
476
|
-
if (
|
|
477
|
-
payload === HTTP_BODY_NULL_STRING ||
|
|
478
|
-
payload.startsWith(HTTP_BODY_DROPPED_LENGTH_MSG) ||
|
|
479
|
-
payload.startsWith(HTTP_BODY_DROPPED_TYPE_MSG)
|
|
480
|
-
) {
|
|
481
|
-
return payload;
|
|
482
77
|
}
|
|
483
|
-
|
|
484
|
-
|
|
485
|
-
|
|
486
|
-
|
|
487
|
-
|
|
488
|
-
|
|
489
|
-
|
|
490
|
-
|
|
491
|
-
|
|
492
|
-
|
|
493
|
-
|
|
494
|
-
|
|
495
|
-
|
|
496
|
-
|
|
497
|
-
|
|
498
|
-
|
|
499
|
-
|
|
500
|
-
|
|
501
|
-
|
|
502
|
-
|
|
503
|
-
|
|
504
|
-
|
|
505
|
-
|
|
506
|
-
|
|
507
|
-
|
|
508
|
-
|
|
509
|
-
|
|
510
|
-
|
|
511
|
-
|
|
512
|
-
|
|
513
|
-
|
|
514
|
-
|
|
515
|
-
|
|
516
|
-
|
|
517
|
-
|
|
518
|
-
|
|
519
|
-
|
|
520
|
-
|
|
521
|
-
|
|
522
|
-
|
|
523
|
-
|
|
524
|
-
|
|
525
|
-
|
|
526
|
-
|
|
527
|
-
|
|
528
|
-
) {
|
|
529
|
-
|
|
530
|
-
|
|
531
|
-
|
|
532
|
-
|
|
533
|
-
|
|
534
|
-
|
|
535
|
-
|
|
536
|
-
|
|
537
|
-
|
|
538
|
-
|
|
539
|
-
|
|
540
|
-
|
|
541
|
-
|
|
542
|
-
|
|
543
|
-
|
|
544
|
-
|
|
545
|
-
|
|
546
|
-
|
|
547
|
-
|
|
548
|
-
|
|
549
|
-
|
|
550
|
-
|
|
551
|
-
|
|
552
|
-
|
|
553
|
-
|
|
554
|
-
|
|
555
|
-
|
|
556
|
-
|
|
557
|
-
|
|
558
|
-
|
|
559
|
-
|
|
560
|
-
|
|
561
|
-
|
|
562
|
-
|
|
563
|
-
|
|
564
|
-
|
|
565
|
-
|
|
566
|
-
|
|
567
|
-
|
|
568
|
-
|
|
569
|
-
|
|
570
|
-
|
|
571
|
-
|
|
572
|
-
|
|
573
|
-
|
|
574
|
-
|
|
575
|
-
|
|
576
|
-
|
|
577
|
-
|
|
578
|
-
|
|
579
|
-
|
|
580
|
-
|
|
581
|
-
|
|
582
|
-
|
|
583
|
-
|
|
584
|
-
|
|
585
|
-
|
|
586
|
-
|
|
587
|
-
|
|
588
|
-
|
|
589
|
-
|
|
590
|
-
|
|
591
|
-
|
|
592
|
-
|
|
593
|
-
|
|
594
|
-
|
|
595
|
-
|
|
596
|
-
|
|
597
|
-
|
|
598
|
-
|
|
599
|
-
|
|
600
|
-
|
|
601
|
-
|
|
602
|
-
|
|
603
|
-
|
|
604
|
-
|
|
605
|
-
|
|
606
|
-
|
|
607
|
-
|
|
608
|
-
|
|
609
|
-
|
|
610
|
-
|
|
611
|
-
|
|
612
|
-
|
|
613
|
-
|
|
614
|
-
|
|
615
|
-
|
|
616
|
-
|
|
617
|
-
|
|
618
|
-
|
|
619
|
-
|
|
620
|
-
|
|
621
|
-
|
|
622
|
-
|
|
623
|
-
|
|
624
|
-
|
|
625
|
-
|
|
626
|
-
|
|
627
|
-
|
|
628
|
-
|
|
629
|
-
|
|
630
|
-
|
|
631
|
-
|
|
632
|
-
|
|
633
|
-
|
|
634
|
-
|
|
635
|
-
|
|
636
|
-
|
|
637
|
-
|
|
638
|
-
|
|
639
|
-
|
|
640
|
-
|
|
641
|
-
|
|
642
|
-
|
|
643
|
-
|
|
644
|
-
|
|
645
|
-
|
|
646
|
-
|
|
647
|
-
|
|
648
|
-
|
|
649
|
-
|
|
650
|
-
|
|
651
|
-
|
|
652
|
-
|
|
653
|
-
|
|
654
|
-
|
|
655
|
-
|
|
656
|
-
|
|
657
|
-
|
|
658
|
-
|
|
659
|
-
|
|
660
|
-
|
|
661
|
-
|
|
662
|
-
|
|
663
|
-
|
|
664
|
-
|
|
665
|
-
|
|
666
|
-
|
|
667
|
-
|
|
668
|
-
|
|
669
|
-
|
|
670
|
-
|
|
671
|
-
|
|
672
|
-
|
|
673
|
-
|
|
674
|
-
|
|
675
|
-
|
|
676
|
-
|
|
677
|
-
|
|
678
|
-
|
|
679
|
-
|
|
680
|
-
|
|
681
|
-
|
|
682
|
-
|
|
683
|
-
|
|
684
|
-
|
|
685
|
-
|
|
686
|
-
|
|
687
|
-
|
|
688
|
-
|
|
689
|
-
|
|
690
|
-
|
|
691
|
-
|
|
692
|
-
|
|
693
|
-
|
|
694
|
-
|
|
695
|
-
|
|
696
|
-
|
|
697
|
-
|
|
78
|
+
/**
|
|
79
|
+
* Takes an iterator and returns a map of strings representing headers.
|
|
80
|
+
* @param {object} headersIterable any iterable object
|
|
81
|
+
* @returns a map of strings (as expected by metroplex) representing HTTP
|
|
82
|
+
* request or response headers
|
|
83
|
+
*/
|
|
84
|
+
static headersMapFromIterable(headersIterable) {
|
|
85
|
+
const headerMap = new Map();
|
|
86
|
+
// eslint-disable-next-line no-restricted-syntax
|
|
87
|
+
for (const header of headersIterable) {
|
|
88
|
+
// Ensure we're working with strings
|
|
89
|
+
// This will also automatically convert null to "null"
|
|
90
|
+
if (typeof header[0] !== 'string')
|
|
91
|
+
header[0] = String(header[0]);
|
|
92
|
+
if (typeof header[1] !== 'string')
|
|
93
|
+
header[1] = String(header[1]);
|
|
94
|
+
headerMap.set(header[0].toLowerCase(), header[1]);
|
|
95
|
+
}
|
|
96
|
+
return headerMap;
|
|
97
|
+
}
|
|
98
|
+
/**
|
|
99
|
+
* Takes a string of headers with 'name: value' and returns
|
|
100
|
+
* a map of strings representing headers.
|
|
101
|
+
* headersString is all the headers in one string
|
|
102
|
+
* returns a map of strings (as expected by metroplex) representing HTTP
|
|
103
|
+
* request or response headers
|
|
104
|
+
*/
|
|
105
|
+
static headersMapFromString(headersString) {
|
|
106
|
+
const headerMap = new Map();
|
|
107
|
+
if (!headersString || typeof headersString !== 'string')
|
|
108
|
+
return headerMap;
|
|
109
|
+
const headerStringArray = headersString.split('\r\n').filter(Boolean);
|
|
110
|
+
headerStringArray.forEach(function (headerString) {
|
|
111
|
+
const split = headerString.split(': ');
|
|
112
|
+
if (split.length === 2 && split[0].length > 0 && split[1].length > 0) {
|
|
113
|
+
headerMap.set(split[0].toLowerCase(), split[1]);
|
|
114
|
+
}
|
|
115
|
+
});
|
|
116
|
+
return headerMap;
|
|
117
|
+
}
|
|
118
|
+
/**
|
|
119
|
+
* For an XHR object, checks the responseType property and handles the response or
|
|
120
|
+
* responseText property accordingly to return a string representation of the response.
|
|
121
|
+
* @returns a string representation of the response, or null if this fails.
|
|
122
|
+
*/
|
|
123
|
+
static getResponseStringFromXHR(xhr) {
|
|
124
|
+
return __awaiter(this, void 0, void 0, function* () {
|
|
125
|
+
var _a;
|
|
126
|
+
// This should not happen, as this function is called from a method on xhr.
|
|
127
|
+
if (!xhr)
|
|
128
|
+
return null;
|
|
129
|
+
if (xhr.responseType === '' || xhr.responseType === 'text') {
|
|
130
|
+
return xhr.responseText;
|
|
131
|
+
}
|
|
132
|
+
// If the XHR object exists but the response is null, return null as a string.
|
|
133
|
+
if (!xhr.response)
|
|
134
|
+
return HTTP_BODY_NULL_STRING;
|
|
135
|
+
if ((_a = xhr.response.documentElement) === null || _a === void 0 ? void 0 : _a.innerHTML) {
|
|
136
|
+
return xhr.response.documentElement.innerHTML;
|
|
137
|
+
}
|
|
138
|
+
if (typeof xhr.response.text === 'function') {
|
|
139
|
+
return yield xhr.response.text();
|
|
140
|
+
}
|
|
141
|
+
if (xhr.responseType === 'json') {
|
|
142
|
+
try {
|
|
143
|
+
const text = stringifyJSON(xhr.response);
|
|
144
|
+
if (text === '{}')
|
|
145
|
+
return null; // stringifyJSON returns {} of it fails to serialize a property
|
|
146
|
+
return text;
|
|
147
|
+
}
|
|
148
|
+
catch (e) {
|
|
149
|
+
ClientConfig.getInstance().postNoibuErrorAndOptionallyDisableClient(`Unable to stringify JSON response: ${e}`, false, SEVERITY.warn);
|
|
150
|
+
return null;
|
|
151
|
+
}
|
|
152
|
+
}
|
|
153
|
+
return null;
|
|
154
|
+
});
|
|
155
|
+
}
|
|
156
|
+
/**
|
|
157
|
+
* Takes a URL and returns true if it is determined to be on the same domain as the URL
|
|
158
|
+
* the script is running on. Ignores protocol and path, and allows the URL to be a subdomain.
|
|
159
|
+
* @param {string} requestURL the URL of a request to compare to the script website's URL
|
|
160
|
+
* @param {bool} isHostnameCheck the URL is a domain hostname, could be a super or sub domain
|
|
161
|
+
*/
|
|
162
|
+
isURLSameDomain(requestURL, isHostnameCheck = false) {
|
|
163
|
+
if (typeof requestURL !== 'string' ||
|
|
164
|
+
!this.initialURLPartsReversed ||
|
|
165
|
+
this.initialURLPartsReversed.length < 1) {
|
|
166
|
+
return false;
|
|
167
|
+
}
|
|
168
|
+
let requestHostname = isHostnameCheck ? requestURL : ''; // has to be declared outside of `try`
|
|
169
|
+
// Use URL constructor to extract hostname
|
|
170
|
+
if (!isHostnameCheck) {
|
|
171
|
+
try {
|
|
172
|
+
let requestURLWithProtocol = requestURL;
|
|
173
|
+
if (requestURL.startsWith('//')) {
|
|
174
|
+
// eslint-disable-next-line prefer-template
|
|
175
|
+
requestURLWithProtocol = 'https:' + requestURL;
|
|
176
|
+
}
|
|
177
|
+
requestHostname = new URL(requestURLWithProtocol).hostname;
|
|
178
|
+
}
|
|
179
|
+
catch (e) {
|
|
180
|
+
// Return false if URL() is unable to parse the request URL
|
|
181
|
+
ClientConfig.getInstance().postNoibuErrorAndOptionallyDisableClient(`Unable to determine hostname for request URL: ${e}`, false, SEVERITY.warn);
|
|
182
|
+
return false;
|
|
183
|
+
}
|
|
184
|
+
}
|
|
185
|
+
const requestParts = requestHostname.split('.');
|
|
186
|
+
// Remove first subdomain from either URL if it is www., www1., etc.
|
|
187
|
+
if (requestParts.length < 1)
|
|
188
|
+
return false;
|
|
189
|
+
/* eslint-disable no-unused-expressions */
|
|
190
|
+
DEFAULT_WEBSITE_SUBDOMAIN_PATTERN.test(requestParts[0]) &&
|
|
191
|
+
requestParts.shift();
|
|
192
|
+
/* eslint-enable no-unused-expressions */
|
|
193
|
+
// Reverse order array
|
|
194
|
+
requestParts.reverse();
|
|
195
|
+
if (!isHostnameCheck &&
|
|
196
|
+
requestParts.length < this.initialURLPartsReversed.length)
|
|
197
|
+
return false;
|
|
198
|
+
// Return true if for every part in the website part, the same part exists
|
|
199
|
+
// in the request part, or if alsoCheckSuperDomain is true.
|
|
200
|
+
if (isHostnameCheck) {
|
|
201
|
+
const isSubDomain = requestParts.length >= this.initialURLPartsReversed.length &&
|
|
202
|
+
this.initialURLPartsReversed.every((part, i) => part === requestParts[i]);
|
|
203
|
+
const isSuperDomain = requestParts.length <= this.initialURLPartsReversed.length &&
|
|
204
|
+
requestParts.every((part, i) => part === this.initialURLPartsReversed[i]);
|
|
205
|
+
return isSubDomain || isSuperDomain;
|
|
206
|
+
}
|
|
207
|
+
// Default return false if alsoCheckSuperDomain is not true
|
|
208
|
+
return this.initialURLPartsReversed.every((part, i) => part === requestParts[i]);
|
|
209
|
+
}
|
|
210
|
+
/**
|
|
211
|
+
* Builds an HTTP Data bundle
|
|
212
|
+
*/
|
|
213
|
+
bundleHTTPData(url, requestHeaders, rawRequestPayload, responseHeaders, rawResponsePayload, method, isError) {
|
|
214
|
+
if (!this.isValidRequest(url, method)) {
|
|
215
|
+
return null;
|
|
216
|
+
}
|
|
217
|
+
// stringify payload if correct type and not too large
|
|
218
|
+
let requestPayload = '';
|
|
219
|
+
let responsePayload = '';
|
|
220
|
+
if (this.shouldCollectPayloadForURL(url)) {
|
|
221
|
+
requestPayload =
|
|
222
|
+
this.getReasonPayloadIsDropped(requestHeaders, isError) ||
|
|
223
|
+
this.stringFromRequestBody(rawRequestPayload, requestHeaders);
|
|
224
|
+
responsePayload =
|
|
225
|
+
this.getReasonPayloadIsDropped(responseHeaders, isError) ||
|
|
226
|
+
this.stringFromRequestBody(rawResponsePayload, responseHeaders);
|
|
227
|
+
}
|
|
228
|
+
// don't bundle if there is no data
|
|
229
|
+
const safeRequestHeaders = requestHeaders || new Map();
|
|
230
|
+
const safeRequestPayload = requestPayload || '';
|
|
231
|
+
const safeResponseHeaders = responseHeaders || new Map();
|
|
232
|
+
const safeResponsePayload = responsePayload || '';
|
|
233
|
+
if (safeRequestHeaders.size === 0 &&
|
|
234
|
+
!safeRequestPayload &&
|
|
235
|
+
safeResponseHeaders.size === 0 &&
|
|
236
|
+
!safeResponsePayload) {
|
|
237
|
+
return null;
|
|
238
|
+
}
|
|
239
|
+
// Ensure payloads do not exceed the maximum size and redact PII
|
|
240
|
+
const requestPayloadUpdated = this.restrictPayload(safeRequestPayload, url, isError);
|
|
241
|
+
// Ensure payloads do not exceed the maximum size and redact PII
|
|
242
|
+
const responsePayloadUpdated = this.restrictPayload(safeResponsePayload, url, isError);
|
|
243
|
+
// Redact PII.
|
|
244
|
+
const cleanRequestHeaders = safeFromEntries(this.removePIIHeaders(requestHeaders));
|
|
245
|
+
const cleanResponseHeaders = safeFromEntries(this.removePIIHeaders(responseHeaders));
|
|
246
|
+
return {
|
|
247
|
+
[HTTP_DATA_REQ_HEADERS_ATT_NAME]: cleanRequestHeaders,
|
|
248
|
+
[HTTP_DATA_PAYLOAD_ATT_NAME]: requestPayloadUpdated,
|
|
249
|
+
[HTTP_DATA_RESP_HEADERS_ATT_NAME]: cleanResponseHeaders,
|
|
250
|
+
[HTTP_DATA_RESP_PAYLOAD_ATT_NAME]: responsePayloadUpdated,
|
|
251
|
+
};
|
|
252
|
+
}
|
|
253
|
+
/**
|
|
254
|
+
* Validates a request based on the URL and method. When enabled, will handle
|
|
255
|
+
* de-duping the requests
|
|
256
|
+
* @param {string} url
|
|
257
|
+
* @param {string} method
|
|
258
|
+
* @returns boolean indicating whether the validation passed
|
|
259
|
+
*/
|
|
260
|
+
isValidRequest(url, method) {
|
|
261
|
+
if (!this.httpDataCollectionEnabled)
|
|
262
|
+
return false;
|
|
263
|
+
if (!method || typeof method !== 'string') {
|
|
264
|
+
return false;
|
|
265
|
+
}
|
|
266
|
+
// TODO: Check below disabled for beta. NOI-4253
|
|
267
|
+
// only bundle and send once per request/method/response code combination
|
|
268
|
+
// const urlWithoutParams = url.split('?')[0];
|
|
269
|
+
// const requestSignature = `${urlWithoutParams}-${method}-${responseCode}`;
|
|
270
|
+
// if (this.uniqueRequests.has(requestSignature)) {
|
|
271
|
+
// return false;
|
|
272
|
+
// }
|
|
273
|
+
return true;
|
|
274
|
+
}
|
|
275
|
+
/**
|
|
276
|
+
* Checks two things: that the URL is either on the same domain (or an address relative to the
|
|
277
|
+
* current domain), and also checks that the config http_data_collection flag is enabled.
|
|
278
|
+
* @param {string} url
|
|
279
|
+
* @returns boolean indicating whether the URL passed is either relative (in which case it is
|
|
280
|
+
* inherently on the current domain) or matches the current domain.
|
|
281
|
+
*/
|
|
282
|
+
shouldContinueForURL(url) {
|
|
283
|
+
if (!this.httpDataCollectionEnabled)
|
|
284
|
+
return false;
|
|
285
|
+
if (!url || typeof url !== 'string' || !this.initialURLPartsReversed) {
|
|
286
|
+
return false;
|
|
287
|
+
}
|
|
288
|
+
// URL is absolute; either "http://example.com" or "//example.com"
|
|
289
|
+
if (HTTPDataBundler.isAbsoluteURL(url)) {
|
|
290
|
+
// Only capture requests on the same domain or in the allowed list
|
|
291
|
+
if (!this.isURLSameDomain(url) && !this.shouldCollectPayloadForURL(url))
|
|
292
|
+
return false;
|
|
293
|
+
} // end `if` (if URL is relative, it is on the same domain.)
|
|
294
|
+
return true;
|
|
295
|
+
}
|
|
296
|
+
/**
|
|
297
|
+
* Determins if the URL is absolute or relative
|
|
298
|
+
* @param {string} url
|
|
299
|
+
* @returns boolean indicating whether the URL passed is either absolute or relative
|
|
300
|
+
*/
|
|
301
|
+
static isAbsoluteURL(url) {
|
|
302
|
+
if (!url || typeof url !== 'string') {
|
|
303
|
+
return false;
|
|
304
|
+
}
|
|
305
|
+
return url.indexOf('://') > 0 || url.indexOf('//') === 0;
|
|
306
|
+
}
|
|
307
|
+
/**
|
|
308
|
+
* Checks whether HTTP payloads can be collected on this URL
|
|
309
|
+
* @returns boolean indicating whether HTTP payloads can be collected on this URL
|
|
310
|
+
*/
|
|
311
|
+
shouldCollectPayloadForURL(url) {
|
|
312
|
+
if (!url || typeof url !== 'string') {
|
|
313
|
+
return false;
|
|
314
|
+
}
|
|
315
|
+
// check if in the full URL allowed list
|
|
316
|
+
const isAllowedAsAbsolute = !!this.httpDataAllowedAbsoluteRegex &&
|
|
317
|
+
this.httpDataAllowedAbsoluteRegex.test(url.toLowerCase());
|
|
318
|
+
const isRelative = !HTTPDataBundler.isAbsoluteURL(url) || this.isURLSameDomain(url);
|
|
319
|
+
// check if in the relative URL allowed list or if it is on the same domain
|
|
320
|
+
const isAllowedAsRelative =
|
|
321
|
+
// if this is a relative URL (isURLSameDomain will fail) OR a full URL on domain
|
|
322
|
+
isRelative &&
|
|
323
|
+
!!this.httpDataAllowedRelativeRegex &&
|
|
324
|
+
this.httpDataAllowedRelativeRegex.test(url.toLowerCase());
|
|
325
|
+
return isAllowedAsAbsolute || isAllowedAsRelative;
|
|
326
|
+
}
|
|
327
|
+
/**
|
|
328
|
+
* Double checks content length if we couldn't read the headers, and redacts PII
|
|
329
|
+
* returns the restricted payload
|
|
330
|
+
*/
|
|
331
|
+
restrictPayload(payload, url, isError) {
|
|
332
|
+
// if no payload, too large, payload already dropped,
|
|
333
|
+
// or payloads not allowed for this URL: nothing to do
|
|
334
|
+
if (!payload || !this.shouldCollectPayloadForURL(url)) {
|
|
335
|
+
return HTTP_BODY_NULL_STRING;
|
|
336
|
+
}
|
|
337
|
+
if (typeof payload !== 'string') {
|
|
338
|
+
ClientConfig.getInstance().postNoibuErrorAndOptionallyDisableClient({
|
|
339
|
+
msg: `restrictPayload received non string payload`,
|
|
340
|
+
payloadType: typeof payload,
|
|
341
|
+
}, false, SEVERITY.error);
|
|
342
|
+
return HTTP_BODY_NULL_STRING;
|
|
343
|
+
}
|
|
344
|
+
if (payload === HTTP_BODY_NULL_STRING ||
|
|
345
|
+
(!!payload.startsWith &&
|
|
346
|
+
(payload.startsWith(HTTP_BODY_DROPPED_LENGTH_MSG) ||
|
|
347
|
+
payload.startsWith(HTTP_BODY_DROPPED_TYPE_MSG))) ||
|
|
348
|
+
(!!payload.indexOf &&
|
|
349
|
+
(payload.indexOf(HTTP_BODY_DROPPED_LENGTH_MSG) === 0 ||
|
|
350
|
+
payload.indexOf(HTTP_BODY_DROPPED_TYPE_MSG) === 0))) {
|
|
351
|
+
return payload;
|
|
352
|
+
}
|
|
353
|
+
let restrictSize = isError
|
|
354
|
+
? MAX_HTTP_DATA_PAYLOAD_LENGTH
|
|
355
|
+
: MAX_SUCCESS_HTTP_DATA_PAYLOAD_LENGTH;
|
|
356
|
+
// TODO CHARLIE: Temporary hack for www.holtrenfrew.com to limit them to 1.5MB to help solve a bug
|
|
357
|
+
if (this.hostname === 'www.holtrenfrew.com') {
|
|
358
|
+
restrictSize = 1.5 * 1024 * 1024;
|
|
359
|
+
}
|
|
360
|
+
if (payload.length > restrictSize) {
|
|
361
|
+
StoredMetrics.getInstance().addHttpDataDropByLength();
|
|
362
|
+
return `${HTTP_BODY_DROPPED_LENGTH_MSG} Payload length: ${payload.length}`;
|
|
363
|
+
}
|
|
364
|
+
// payload is good!
|
|
365
|
+
StoredMetrics.getInstance().addHttpDataPayloadCount();
|
|
366
|
+
return removePII(payload);
|
|
367
|
+
}
|
|
368
|
+
/**
|
|
369
|
+
* Returns true if the content-length header is of acceptable size.
|
|
370
|
+
* Too big gets rejected
|
|
371
|
+
* If the headers are not found, check actual content for length
|
|
372
|
+
* @param {Headers} headers
|
|
373
|
+
* @returns boolean true if acceptable to collect
|
|
374
|
+
*/
|
|
375
|
+
contentLengthAcceptable(headers, isError) {
|
|
376
|
+
// TODO This entire check should move into the parent's parent so there is a single place
|
|
377
|
+
// that restricts paylods and unterstands these 2 values
|
|
378
|
+
let restrictSize = isError
|
|
379
|
+
? MAX_HTTP_DATA_PAYLOAD_LENGTH
|
|
380
|
+
: MAX_SUCCESS_HTTP_DATA_PAYLOAD_LENGTH;
|
|
381
|
+
// TODO CHARLIE: Temporary hack for www.holtrenfrew.com to limit them to 1.5MB to help solve a bug
|
|
382
|
+
if (this.hostname === 'www.holtrenfrew.com') {
|
|
383
|
+
restrictSize = 1.5 * 1024 * 1024;
|
|
384
|
+
}
|
|
385
|
+
return this.contentLength(headers) <= restrictSize;
|
|
386
|
+
}
|
|
387
|
+
/**
|
|
388
|
+
* Returns true if the content type according to the headers is valid for collection.
|
|
389
|
+
* Also returns assumed true if content type is not stated in headers.
|
|
390
|
+
* @param {Map} headersMap
|
|
391
|
+
* @returns boolean true if acceptable to collect
|
|
392
|
+
*/
|
|
393
|
+
contentTypeAcceptable(headersMap) {
|
|
394
|
+
// check content type
|
|
395
|
+
const contentType = headersMap.get(CONTENT_TYPE);
|
|
396
|
+
return !(contentType &&
|
|
397
|
+
!this.contentTypeReadableRegex.test(contentType.toLowerCase()));
|
|
398
|
+
}
|
|
399
|
+
/**
|
|
400
|
+
* Returns a descriptive string if we have to drop payload based on the length
|
|
401
|
+
* or type listed in the headers passed. Returns an empty string otherwise.
|
|
402
|
+
*/
|
|
403
|
+
getReasonPayloadIsDropped(headers, isError) {
|
|
404
|
+
if (!(headers === null || headers === void 0 ? void 0 : headers.get)) {
|
|
405
|
+
return '';
|
|
406
|
+
}
|
|
407
|
+
const bundler = HTTPDataBundler.getInstance();
|
|
408
|
+
if (!bundler.contentTypeAcceptable(headers)) {
|
|
409
|
+
// not a readable request, drop the content
|
|
410
|
+
StoredMetrics.getInstance().addHttpDataDropByType();
|
|
411
|
+
return `${HTTP_BODY_DROPPED_TYPE_MSG} Payload type: ${headers.get(CONTENT_TYPE)}`;
|
|
412
|
+
}
|
|
413
|
+
if (!bundler.contentLengthAcceptable(headers, isError)) {
|
|
414
|
+
StoredMetrics.getInstance().addHttpDataDropByLength();
|
|
415
|
+
return `${HTTP_BODY_DROPPED_LENGTH_MSG} Payload length: ${bundler.contentLength(headers)}`;
|
|
416
|
+
}
|
|
417
|
+
return '';
|
|
418
|
+
}
|
|
419
|
+
/**
|
|
420
|
+
* Returns content length from the headers, if available.
|
|
421
|
+
* If headers are not found or length is not a number, return -1
|
|
422
|
+
*/
|
|
423
|
+
contentLength(headersObject) {
|
|
424
|
+
if (!(headersObject === null || headersObject === void 0 ? void 0 : headersObject.get)) {
|
|
425
|
+
return 0;
|
|
426
|
+
}
|
|
427
|
+
// get content length
|
|
428
|
+
let contentLength = 0;
|
|
429
|
+
const contentValueString = headersObject.get(CONTENT_LENGTH);
|
|
430
|
+
if (!contentValueString) {
|
|
431
|
+
// no content length header found
|
|
432
|
+
return -1;
|
|
433
|
+
}
|
|
434
|
+
try {
|
|
435
|
+
contentLength = parseInt(contentValueString, 10);
|
|
436
|
+
if (Number.isNaN(contentLength)) {
|
|
437
|
+
// bad content length header
|
|
438
|
+
return -1;
|
|
439
|
+
}
|
|
440
|
+
}
|
|
441
|
+
catch (err) {
|
|
442
|
+
// if we can't convert the value to a number, confirm after reading payload
|
|
443
|
+
return -1;
|
|
444
|
+
}
|
|
445
|
+
return contentLength;
|
|
446
|
+
}
|
|
447
|
+
/**
|
|
448
|
+
* Accepts a value that could be any type used as a request payload,
|
|
449
|
+
* and returns a string representation, or null if this fails.
|
|
450
|
+
*/
|
|
451
|
+
stringFromRequestBody(value, requestHeaders) {
|
|
452
|
+
/* eslint-disable no-param-reassign */
|
|
453
|
+
// Nullish check to ensure we don't send 'null' or 'undefined' as a string
|
|
454
|
+
if (value == null)
|
|
455
|
+
return null;
|
|
456
|
+
try {
|
|
457
|
+
if (isString(value) && requestHeaders instanceof Map) {
|
|
458
|
+
const contentType = requestHeaders.get(CONTENT_TYPE);
|
|
459
|
+
if (contentType &&
|
|
460
|
+
contentType
|
|
461
|
+
.toLowerCase()
|
|
462
|
+
.includes('application/x-www-form-urlencoded')) {
|
|
463
|
+
value = new URLSearchParams(value.toString());
|
|
464
|
+
}
|
|
465
|
+
}
|
|
466
|
+
}
|
|
467
|
+
catch (error) {
|
|
468
|
+
// Nothing--try next try block
|
|
469
|
+
}
|
|
470
|
+
try {
|
|
471
|
+
// If the payload is a FormData object, turn it into an object.
|
|
472
|
+
if (value instanceof FormData || value instanceof URLSearchParams) {
|
|
473
|
+
value = safeEntries(value).reduce((obj, [key, val]) => {
|
|
474
|
+
obj[key] = this.stringFromRequestBody(val);
|
|
475
|
+
return obj;
|
|
476
|
+
}, {});
|
|
477
|
+
}
|
|
478
|
+
}
|
|
479
|
+
catch (error) {
|
|
480
|
+
// Nothing--try next try block
|
|
481
|
+
}
|
|
482
|
+
try {
|
|
483
|
+
// Case where value has a .toString() method
|
|
484
|
+
const returnVal = value.toString();
|
|
485
|
+
// Catch case where .toString() is on an object, we don't want that --
|
|
486
|
+
// should be stringified below.
|
|
487
|
+
if (!returnVal.includes('[object'))
|
|
488
|
+
return returnVal;
|
|
489
|
+
}
|
|
490
|
+
catch (error) {
|
|
491
|
+
// Nothing--try next try block
|
|
492
|
+
}
|
|
493
|
+
try {
|
|
494
|
+
// Case where value is a Document type (HTML/XML)
|
|
495
|
+
return value.documentElement.innerHTML;
|
|
496
|
+
}
|
|
497
|
+
catch (error) {
|
|
498
|
+
// Nothing--try next try block
|
|
499
|
+
}
|
|
500
|
+
try {
|
|
501
|
+
// Case where value is any other object type
|
|
502
|
+
return stringifyJSON(value);
|
|
503
|
+
}
|
|
504
|
+
catch (e) {
|
|
505
|
+
ClientConfig.getInstance().postNoibuErrorAndOptionallyDisableClient(`Unable to stringify request body: ${e}`, false, SEVERITY.warn);
|
|
506
|
+
}
|
|
507
|
+
return null;
|
|
698
508
|
}
|
|
699
|
-
|
|
700
|
-
|
|
701
|
-
|
|
702
|
-
|
|
703
|
-
|
|
704
|
-
|
|
705
|
-
//
|
|
706
|
-
if (
|
|
707
|
-
|
|
708
|
-
|
|
709
|
-
|
|
710
|
-
|
|
711
|
-
|
|
712
|
-
|
|
713
|
-
|
|
714
|
-
|
|
715
|
-
|
|
716
|
-
|
|
717
|
-
|
|
718
|
-
|
|
719
|
-
|
|
720
|
-
|
|
509
|
+
/**
|
|
510
|
+
* Removes possible PII from headers.
|
|
511
|
+
* @param {Map} dirtyHeaders a Map of HTTP response or request headers
|
|
512
|
+
* @returns {Map|null} a map of headers with PII redacted
|
|
513
|
+
*/
|
|
514
|
+
removePIIHeaders(dirtyHeaders) {
|
|
515
|
+
// In order to be fail-safe, let's return null if we won't be able to remove PII headers.
|
|
516
|
+
if (!(dirtyHeaders instanceof Map)) {
|
|
517
|
+
return null;
|
|
518
|
+
}
|
|
519
|
+
if (!dirtyHeaders.size) {
|
|
520
|
+
return dirtyHeaders;
|
|
521
|
+
}
|
|
522
|
+
const cleanHeaders = new Map(dirtyHeaders);
|
|
523
|
+
cleanHeaders.forEach((headerVal, headerKey, map) => {
|
|
524
|
+
// If the key is known PII, redact
|
|
525
|
+
if (BLOCKED_HTTP_HEADER_KEYS.includes(headerKey.toLowerCase())) {
|
|
526
|
+
map.set(headerKey, PII_REDACTION_REPLACEMENT_STRING);
|
|
527
|
+
// If not, run the header's value through the string redaction function
|
|
528
|
+
}
|
|
529
|
+
else {
|
|
530
|
+
map.set(headerKey, removePII(headerVal));
|
|
531
|
+
}
|
|
532
|
+
});
|
|
533
|
+
return cleanHeaders;
|
|
721
534
|
}
|
|
722
|
-
}
|
|
723
535
|
}
|
|
724
536
|
|
|
725
537
|
export { HTTPDataBundler };
|