@sentry/core
Version:
Base implementation for all Sentry JavaScript SDKs
228 lines (224 loc) • 8.06 kB
JavaScript
Object.defineProperty(exports, Symbol.toStringTag, { value: 'Module' });
const api = require('../api.js');
const currentScopes = require('../currentScopes.js');
const trace = require('../tracing/trace.js');
const debugLogger = require('./debug-logger.js');
const dsn = require('./dsn.js');
const tunnel = require('./tunnel.js');
function handleTunnelRequest(requestOrOptions, responseOrOptions, nodeOptions) {
if (isNodeTunnelResponse(responseOrOptions)) {
const nodeResponse = responseOrOptions;
return forwardTunnelRequest(
toFetchRequest(requestOrOptions),
getAllowedDsns(nodeOptions)
).then((fetchResponse) => writeNodeResponse(nodeResponse, fetchResponse));
}
const isLegacyCall = "request" in requestOrOptions && !("method" in requestOrOptions);
const request = isLegacyCall ? requestOrOptions.request : requestOrOptions;
const options = isLegacyCall ? requestOrOptions : responseOrOptions;
return forwardTunnelRequest(request, getAllowedDsns(options));
}
function handleTunnelRequestIfMatched(request, responseOrOptions, nodeOptions) {
const response = isNodeTunnelResponse(responseOrOptions) ? responseOrOptions : void 0;
const tunnel$1 = tunnel.resolveTunnelMatch(response ? nodeOptions : responseOrOptions);
if (!tunnel$1.allowedDsns.length || !tunnel.matchesTunnel(request, tunnel$1)) {
return void 0;
}
if (!response) {
return forwardTunnelRequest(request, tunnel$1.allowedDsns);
}
return forwardTunnelRequest(toFetchRequest(request), tunnel$1.allowedDsns).then(
(fetchResponse) => writeNodeResponse(response, fetchResponse)
);
}
function getAllowedDsns(options) {
return options?.allowedDsns ? tunnel.resolveAllowedDsns(options.allowedDsns, currentScopes.getClient()?.getDsn()) : tunnel.resolveTunnelMatch().allowedDsns;
}
const FORWARDED_RESPONSE_HEADERS = ["content-type", "retry-after", "x-sentry-rate-limits"];
function isNodeTunnelResponse(value) {
return typeof value?.writeHead === "function";
}
function toFetchRequest(request) {
const headers = new Headers();
for (const [name, value] of Object.entries(request.headers)) {
if (value !== void 0 && !name.startsWith(":")) {
headers.set(name, Array.isArray(value) ? value.join(", ") : value);
}
}
const iterator = request[Symbol.asyncIterator]();
const encoder = new TextEncoder();
const body = new ReadableStream({
async pull(controller) {
const { done, value } = await iterator.next();
if (done) {
controller.close();
return;
}
controller.enqueue(typeof value === "string" ? encoder.encode(value) : value);
},
// Node only reads the rest of a body nobody started reading, and unread bytes stall a keep-alive connection.
async cancel() {
while (!(await iterator.next()).done) {
}
}
});
return new Request("http://localhost/", {
method: "POST",
headers,
body,
duplex: "half"
});
}
async function writeNodeResponse(response, fetchResponse) {
const body = await fetchResponse.arrayBuffer().catch(() => void 0);
if (!body) {
response.writeHead(500);
response.end(new Uint8Array());
return;
}
const responseHeaders = {};
for (const name of FORWARDED_RESPONSE_HEADERS) {
const value = fetchResponse.headers.get(name);
if (value) {
responseHeaders[name] = value;
}
}
response.writeHead(fetchResponse.status, responseHeaders);
response.end(new Uint8Array(body));
}
function withDecodedBodyHeaders(response) {
const headers = new Headers(response.headers);
headers.delete("content-encoding");
headers.delete("content-length");
return new Response(response.body, { status: response.status, statusText: response.statusText, headers });
}
async function forwardTunnelRequest(request, allowedDsns) {
return withDecodedBodyHeaders(await forwardEnvelope(request, allowedDsns));
}
async function forwardEnvelope(request, allowedDsns) {
if (allowedDsns.length === 0) {
return new Response("Tunnel not configured", { status: 500 });
}
const contentLength = Number(request.headers.get("content-length"));
if (!contentLength) {
return new Response("Length required", { status: 411 });
}
const reader = request.body?.getReader();
const head = reader && await readEnvelopeHeaderLine(reader).catch(() => void 0);
if (!reader || !head) {
return new Response("Invalid envelope", { status: 400 });
}
const sentryIngestUrl = resolveIngestUrl(head.line, allowedDsns);
if (typeof sentryIngestUrl !== "string") {
reader.cancel().catch(() => void 0);
return sentryIngestUrl;
}
try {
return await trace.suppressTracing(
() => fetch(sentryIngestUrl, {
method: "POST",
headers: forwardHeaders(request),
body: prependToStream(head.read, reader),
duplex: "half"
})
);
} catch (error) {
debugLogger.debug.error("Sentry tunnel: failed to forward envelope", error);
return new Response("Failed to forward envelope to Sentry", { status: 500 });
}
}
function resolveIngestUrl(headerLine, allowedDsns) {
let envelopeHeader;
try {
envelopeHeader = JSON.parse(new TextDecoder().decode(headerLine));
} catch {
return new Response("Invalid envelope", { status: 400 });
}
if (!envelopeHeader) {
return new Response("Invalid envelope: missing header", { status: 400 });
}
const dsn$1 = envelopeHeader.dsn;
if (!dsn$1 || typeof dsn$1 !== "string") {
return new Response("Invalid envelope: missing DSN", { status: 400 });
}
const isAllowed = allowedDsns.some((allowed) => allowed === dsn$1);
if (!isAllowed) {
debugLogger.debug.warn(`Sentry tunnel: rejected request with unauthorized DSN (${dsn$1})`);
return new Response("DSN not allowed", { status: 403 });
}
const dsnComponents = dsn.makeDsn(dsn$1);
if (!dsnComponents) {
debugLogger.debug.warn(`Could not extract DSN Components from: ${dsn$1}`);
return new Response("Invalid DSN", { status: 403 });
}
return api.getEnvelopeEndpointWithUrlEncodedAuth(dsnComponents);
}
const MAX_ENVELOPE_HEADER_BYTES = 64 * 1024;
const FORWARDED_CLIENT_HEADERS = [
"user-agent",
"sec-ch-ua",
"sec-ch-ua-model",
"sec-ch-ua-platform",
"sec-ch-ua-platform-version",
"origin",
"referer"
];
async function readEnvelopeHeaderLine(reader) {
let read = new Uint8Array(0);
while (read.length <= MAX_ENVELOPE_HEADER_BYTES) {
const { done, value } = await reader.read();
if (done) {
return read.length ? { line: read, read } : void 0;
}
const searchFrom = read.length;
read = concatBytes(read, value);
const newline = read.indexOf(10, searchFrom);
if (newline !== -1) {
return { line: read.subarray(0, newline), read };
}
}
await reader.cancel();
return void 0;
}
function concatBytes(a, b) {
const merged = new Uint8Array(a.length + b.length);
merged.set(a);
merged.set(b, a.length);
return merged;
}
function prependToStream(prefix, reader) {
return new ReadableStream({
start(controller) {
controller.enqueue(prefix);
},
async pull(controller) {
const { done, value } = await reader.read();
if (done) {
controller.close();
return;
}
controller.enqueue(value);
},
cancel(reason) {
return reader.cancel(reason);
}
});
}
function forwardHeaders(request) {
const headers = { "Content-Type": "application/x-sentry-envelope" };
const clientIp = request.headers.get("cf-connecting-ip") || request.headers.get("x-vercel-forwarded-for") || request.headers.get("x-forwarded-for");
if (clientIp) {
headers["X-Sentry-Forwarded-For"] = clientIp;
}
for (const name of FORWARDED_CLIENT_HEADERS) {
const value = request.headers.get(name);
if (value) {
headers[name] = value;
}
}
return headers;
}
exports.forwardTunnelRequest = forwardTunnelRequest;
exports.handleTunnelRequest = handleTunnelRequest;
exports.handleTunnelRequestIfMatched = handleTunnelRequestIfMatched;
//# sourceMappingURL=tunnelForward.js.map