-
-
Notifications
You must be signed in to change notification settings - Fork 1.6k
/
http.ts
208 lines (188 loc) · 8.34 KB
/
http.ts
1
2
3
4
5
6
7
8
9
10
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
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
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
import { getCurrentHub } from '@sentry/core';
import type * as http from 'http';
import type * as https from 'https';
import { URL } from 'url';
import { NODE_VERSION } from '../../nodeVersion';
/**
* Checks whether given url points to Sentry server
* @param url url to verify
*/
export function isSentryRequest(url: string): boolean {
const dsn = getCurrentHub().getClient()?.getDsn();
return dsn ? url.includes(dsn.host) : false;
}
/**
* Assembles a URL that's passed to the users to filter on.
* It can include raw (potentially PII containing) data, which we'll allow users to access to filter
* but won't include in spans or breadcrumbs.
*
* @param requestOptions RequestOptions object containing the component parts for a URL
* @returns Fully-formed URL
*/
// TODO (v8): This function should include auth, query and fragment (it's breaking, so we need to wait for v8)
export function extractRawUrl(requestOptions: RequestOptions): string {
const protocol = requestOptions.protocol || '';
const hostname = requestOptions.hostname || requestOptions.host || '';
// Don't log standard :80 (http) and :443 (https) ports to reduce the noise
const port =
!requestOptions.port || requestOptions.port === 80 || requestOptions.port === 443 ? '' : `:${requestOptions.port}`;
const path = requestOptions.path ? requestOptions.path : '/';
return `${protocol}//${hostname}${port}${path}`;
}
/**
* Assemble a URL to be used for breadcrumbs and spans.
*
* @param requestOptions RequestOptions object containing the component parts for a URL
* @returns Fully-formed URL
*/
export function extractUrl(requestOptions: RequestOptions): string {
const protocol = requestOptions.protocol || '';
const hostname = requestOptions.hostname || requestOptions.host || '';
// Don't log standard :80 (http) and :443 (https) ports to reduce the noise
const port =
!requestOptions.port || requestOptions.port === 80 || requestOptions.port === 443 ? '' : `:${requestOptions.port}`;
// do not include search or hash in span descriptions, per https://develop.sentry.dev/sdk/data-handling/#structuring-data
const path = requestOptions.pathname || '/';
// always filter authority, see https://develop.sentry.dev/sdk/data-handling/#structuring-data
const authority = requestOptions.auth ? redactAuthority(requestOptions.auth) : '';
return `${protocol}//${authority}${hostname}${port}${path}`;
}
function redactAuthority(auth: string): string {
const [user, password] = auth.split(':');
return `${user ? '[Filtered]' : ''}:${password ? '[Filtered]' : ''}@`;
}
/**
* Handle various edge cases in the span description (for spans representing http(s) requests).
*
* @param description current `description` property of the span representing the request
* @param requestOptions Configuration data for the request
* @param Request Request object
*
* @returns The cleaned description
*/
export function cleanSpanDescription(
description: string | undefined,
requestOptions: RequestOptions,
request: http.ClientRequest,
): string | undefined {
// nothing to clean
if (!description) {
return description;
}
// eslint-disable-next-line prefer-const
let [method, requestUrl] = description.split(' ');
// superagent sticks the protocol in a weird place (we check for host because if both host *and* protocol are missing,
// we're likely dealing with an internal route and this doesn't apply)
if (requestOptions.host && !requestOptions.protocol) {
// eslint-disable-next-line @typescript-eslint/no-unsafe-member-access, @typescript-eslint/no-explicit-any
requestOptions.protocol = (request as any)?.agent?.protocol; // worst comes to worst, this is undefined and nothing changes
// This URL contains the filtered authority ([filtered]:[filtered]@example.com) but no fragment or query params
requestUrl = extractUrl(requestOptions);
}
// internal routes can end up starting with a triple slash rather than a single one
if (requestUrl?.startsWith('///')) {
requestUrl = requestUrl.slice(2);
}
return `${method} ${requestUrl}`;
}
// the node types are missing a few properties which node's `urlToOptions` function spits out
export type RequestOptions = http.RequestOptions & { hash?: string; search?: string; pathname?: string; href?: string };
type RequestCallback = (response: http.IncomingMessage) => void;
export type RequestMethodArgs =
| [RequestOptions | string | URL, RequestCallback?]
| [string | URL, RequestOptions, RequestCallback?];
export type RequestMethod = (...args: RequestMethodArgs) => http.ClientRequest;
/**
* Convert a URL object into a RequestOptions object.
*
* Copied from Node's internals (where it's used in http(s).request() and http(s).get()), modified only to use the
* RequestOptions type above.
*
* See https://github.com/nodejs/node/blob/master/lib/internal/url.js.
*/
export function urlToOptions(url: URL): RequestOptions {
const options: RequestOptions = {
protocol: url.protocol,
hostname:
typeof url.hostname === 'string' && url.hostname.startsWith('[') ? url.hostname.slice(1, -1) : url.hostname,
hash: url.hash,
search: url.search,
pathname: url.pathname,
path: `${url.pathname || ''}${url.search || ''}`,
href: url.href,
};
if (url.port !== '') {
options.port = Number(url.port);
}
if (url.username || url.password) {
options.auth = `${url.username}:${url.password}`;
}
return options;
}
/**
* Normalize inputs to `http(s).request()` and `http(s).get()`.
*
* Legal inputs to `http(s).request()` and `http(s).get()` can take one of ten forms:
* [ RequestOptions | string | URL ],
* [ RequestOptions | string | URL, RequestCallback ],
* [ string | URL, RequestOptions ], and
* [ string | URL, RequestOptions, RequestCallback ].
*
* This standardizes to one of two forms: [ RequestOptions ] and [ RequestOptions, RequestCallback ]. A similar thing is
* done as the first step of `http(s).request()` and `http(s).get()`; this just does it early so that we can interact
* with the args in a standard way.
*
* @param requestArgs The inputs to `http(s).request()` or `http(s).get()`, as an array.
*
* @returns Equivalent args of the form [ RequestOptions ] or [ RequestOptions, RequestCallback ].
*/
export function normalizeRequestArgs(
httpModule: typeof http | typeof https,
requestArgs: RequestMethodArgs,
): [RequestOptions] | [RequestOptions, RequestCallback] {
let callback, requestOptions;
// pop off the callback, if there is one
if (typeof requestArgs[requestArgs.length - 1] === 'function') {
callback = requestArgs.pop() as RequestCallback;
}
// create a RequestOptions object of whatever's at index 0
if (typeof requestArgs[0] === 'string') {
requestOptions = urlToOptions(new URL(requestArgs[0]));
} else if (requestArgs[0] instanceof URL) {
requestOptions = urlToOptions(requestArgs[0]);
} else {
requestOptions = requestArgs[0];
}
// if the options were given separately from the URL, fold them in
if (requestArgs.length === 2) {
requestOptions = { ...requestOptions, ...requestArgs[1] };
}
// Figure out the protocol if it's currently missing
if (requestOptions.protocol === undefined) {
// Worst case we end up populating protocol with undefined, which it already is
/* eslint-disable @typescript-eslint/no-unsafe-member-access, @typescript-eslint/no-explicit-any */
// NOTE: Prior to Node 9, `https` used internals of `http` module, thus we don't patch it.
// Because of that, we cannot rely on `httpModule` to provide us with valid protocol,
// as it will always return `http`, even when using `https` module.
//
// See test/integrations/http.test.ts for more details on Node <=v8 protocol issue.
if (NODE_VERSION.major && NODE_VERSION.major > 8) {
requestOptions.protocol =
(httpModule?.globalAgent as any)?.protocol ||
(requestOptions.agent as any)?.protocol ||
(requestOptions._defaultAgent as any)?.protocol;
} else {
requestOptions.protocol =
(requestOptions.agent as any)?.protocol ||
(requestOptions._defaultAgent as any)?.protocol ||
(httpModule?.globalAgent as any)?.protocol;
}
/* eslint-enable @typescript-eslint/no-unsafe-member-access, @typescript-eslint/no-explicit-any */
}
// return args in standardized form
if (callback) {
return [requestOptions, callback];
} else {
return [requestOptions];
}
}