-
-
Notifications
You must be signed in to change notification settings - Fork 1.8k
Expand file tree
/
Copy pathrequest.ts
More file actions
238 lines (204 loc) · 7.61 KB
/
request.ts
File metadata and controls
238 lines (204 loc) · 7.61 KB
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
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
import { getClient } from '../currentScopes';
import type { PolymorphicRequest } from '../types-hoist/polymorphics';
import type { RequestEventData } from '../types-hoist/request';
import type { WebFetchHeaders, WebFetchRequest } from '../types-hoist/webfetchapi';
/**
* Transforms a `Headers` object that implements the `Web Fetch API` (https://developer.mozilla.org/en-US/docs/Web/API/Headers) into a simple key-value dict.
* The header keys will be lower case: e.g. A "Content-Type" header will be stored as "content-type".
*/
export function winterCGHeadersToDict(winterCGHeaders: WebFetchHeaders): Record<string, string> {
const headers: Record<string, string> = {};
try {
winterCGHeaders.forEach((value, key) => {
if (typeof value === 'string') {
// We check that value is a string even though it might be redundant to make sure prototype pollution is not possible.
headers[key] = value;
}
});
} catch {
// just return the empty headers
}
return headers;
}
/**
* Convert common request headers to a simple dictionary.
*/
export function headersToDict(reqHeaders: Record<string, string | string[] | undefined>): Record<string, string> {
const headers: Record<string, string> = Object.create(null);
try {
Object.entries(reqHeaders).forEach(([key, value]) => {
if (typeof value === 'string') {
headers[key] = value;
}
});
} catch {
// just return the empty headers
}
return headers;
}
/**
* Converts a `Request` object that implements the `Web Fetch API` (https://developer.mozilla.org/en-US/docs/Web/API/Headers) into the format that the `RequestData` integration understands.
*/
export function winterCGRequestToRequestData(req: WebFetchRequest): RequestEventData {
const headers = winterCGHeadersToDict(req.headers);
return {
method: req.method,
url: req.url,
query_string: extractQueryParamsFromUrl(req.url),
headers,
// TODO: Can we extract body data from the request?
};
}
/**
* Convert a HTTP request object to RequestEventData to be passed as normalizedRequest.
* Instead of allowing `PolymorphicRequest` to be passed,
* we want to be more specific and generally require a http.IncomingMessage-like object.
*/
export function httpRequestToRequestData(request: {
method?: string;
url?: string;
headers?: {
[key: string]: string | string[] | undefined;
};
protocol?: string;
socket?: {
encrypted?: boolean;
remoteAddress?: string;
};
}): RequestEventData {
const headers = request.headers || {};
// Check for x-forwarded-host first, then fall back to host header
const forwardedHost = typeof headers['x-forwarded-host'] === 'string' ? headers['x-forwarded-host'] : undefined;
const host = forwardedHost || (typeof headers.host === 'string' ? headers.host : undefined);
// Check for x-forwarded-proto first, then fall back to existing protocol detection
const forwardedProto = typeof headers['x-forwarded-proto'] === 'string' ? headers['x-forwarded-proto'] : undefined;
const protocol = forwardedProto || request.protocol || (request.socket?.encrypted ? 'https' : 'http');
const url = request.url || '';
const absoluteUrl = getAbsoluteUrl({
url,
host,
protocol,
});
// This is non-standard, but may be sometimes set
// It may be overwritten later by our own body handling
const data = (request as PolymorphicRequest).body || undefined;
// This is non-standard, but may be set on e.g. Next.js or Express requests
const cookies = (request as PolymorphicRequest).cookies;
return {
url: absoluteUrl,
method: request.method,
query_string: extractQueryParamsFromUrl(url),
headers: headersToDict(headers),
cookies,
data,
};
}
function getAbsoluteUrl({
url,
protocol,
host,
}: {
url?: string;
protocol: string;
host?: string;
}): string | undefined {
if (url?.startsWith('http')) {
return url;
}
if (url && host) {
return `${protocol}://${host}${url}`;
}
return undefined;
}
const SENSITIVE_HEADER_SNIPPETS = [
'auth',
'token',
'secret',
'session', // for the user_session cookie
'password',
'passwd',
'pwd',
'key',
'jwt',
'bearer',
'sso',
'saml',
'crsf',
'xsrf',
'credentials',
// Always treat cookie headers as sensitive in case individual key-value cookie pairs cannot properly be extracted
'set-cookie',
'cookie',
];
const PII_HEADER_SNIPPETS = ['x-forwarded-', '-user'];
/**
* Converts incoming HTTP request headers to OpenTelemetry span attributes following semantic conventions.
* Header names are converted to the format: http.request.header.<key>
* where <key> is the header name in lowercase with dashes converted to underscores.
*
* @see https://opentelemetry.io/docs/specs/semconv/registry/attributes/http/#http-request-header
*/
export function httpHeadersToSpanAttributes(
headers: Record<string, string | string[] | undefined>,
sendDefaultPii: boolean = false,
): Record<string, string> {
const spanAttributes: Record<string, string> = {};
try {
Object.entries(headers).forEach(([key, value]) => {
if (value == null) {
return;
}
const lowerCasedHeaderKey = key.toLowerCase();
const isCookieHeader = lowerCasedHeaderKey === 'cookie' || lowerCasedHeaderKey === 'set-cookie';
if (isCookieHeader && typeof value === 'string' && value !== '') {
const cookies = value.split('; ');
for (const cookie of cookies) {
// Split only at the first '=' to preserve '=' characters in cookie values
const equalSignIndex = cookie.indexOf('=');
const cookieKey = equalSignIndex !== -1 ? cookie.substring(0, equalSignIndex) : cookie;
const cookieValue = equalSignIndex !== -1 ? cookie.substring(equalSignIndex + 1) : '';
const lowerCasedCookieKey = String(cookieKey).toLowerCase();
const normalizedKey = `http.request.header.${normalizeAttributeKey(lowerCasedHeaderKey)}.${normalizeAttributeKey(lowerCasedCookieKey)}`;
spanAttributes[normalizedKey] = handleHttpHeader(lowerCasedCookieKey, cookieValue, sendDefaultPii);
}
} else {
const normalizedKey = `http.request.header.${normalizeAttributeKey(lowerCasedHeaderKey)}`;
spanAttributes[normalizedKey] = handleHttpHeader(lowerCasedHeaderKey, value, sendDefaultPii);
}
});
} catch {
// Return empty object if there's an error
}
return spanAttributes;
}
function normalizeAttributeKey(key: string): string {
return key.replace(/-/g, '_');
}
function handleHttpHeader(lowerCasedKey: string, value: string | string[] | undefined, sendPii: boolean): string {
const isSensitive = sendPii
? SENSITIVE_HEADER_SNIPPETS.some(snippet => lowerCasedKey.includes(snippet))
: [...PII_HEADER_SNIPPETS, ...SENSITIVE_HEADER_SNIPPETS].some(snippet => lowerCasedKey.includes(snippet));
if (isSensitive) {
return '[Filtered]';
} else if (Array.isArray(value)) {
return value.map(v => (v != null ? String(v) : v)).join(';');
} else if (typeof value === 'string') {
return value;
}
return ''; // Fallback for unexpected types
}
/** Extract the query params from an URL. */
export function extractQueryParamsFromUrl(url: string): string | undefined {
// url is path and query string
if (!url) {
return;
}
try {
// The `URL` constructor can't handle internal URLs of the form `/some/path/here`, so stick a dummy protocol and
// hostname as the base. Since the point here is just to grab the query string, it doesn't matter what we use.
const queryParams = new URL(url, 'http://s.io').search.slice(1);
return queryParams.length ? queryParams : undefined;
} catch {
return undefined;
}
}