mirror of
https://github.com/andwati/zitadel.git
synced 2026-10-29 18:42:15 +00:00
156 lines
5.1 KiB
JavaScript
156 lines
5.1 KiB
JavaScript
/**
|
|
* Mock Zitadel Connect/gRPC server for testing trace propagation.
|
|
* Captures incoming headers and writes them to a file for test assertions.
|
|
*
|
|
* Uses HTTP/2 with cleartext (h2c) to support gRPC protocol.
|
|
*/
|
|
const http2 = require("http2");
|
|
const http = require("http");
|
|
const fs = require("fs");
|
|
const path = require("path");
|
|
|
|
const PORT = process.env.PORT || 8080;
|
|
const OUTPUT_DIR = process.env.OUTPUT_DIR || "/tmp/otel";
|
|
const HEADERS_FILE = path.join(OUTPUT_DIR, "captured-headers.json");
|
|
|
|
/**
|
|
* Safely stringify a value to JSON with XSS-preventing character escapes.
|
|
*
|
|
* Escapes `<`, `>`, `&`, and `/` to their Unicode equivalents to prevent
|
|
* script injection when the output is reflected in HTTP responses or
|
|
* embedded in HTML contexts.
|
|
*
|
|
* @param {unknown} value - The value to stringify
|
|
* @returns {string} JSON string with dangerous characters escaped
|
|
*/
|
|
function safeJsonStringify(value) {
|
|
return JSON.stringify(value, null, 2)
|
|
.replace(/</g, "\\u003c")
|
|
.replace(/>/g, "\\u003e")
|
|
.replace(/&/g, "\\u0026")
|
|
.replace(/\//g, "\\/");
|
|
}
|
|
|
|
// Store captured requests
|
|
const capturedRequests = [];
|
|
|
|
// Handle HTTP/2 request
|
|
function handleRequest(stream, headers, isHttp2 = true) {
|
|
const method = isHttp2 ? headers[":method"] : headers.method;
|
|
const url = isHttp2 ? headers[":path"] : headers.url;
|
|
|
|
// Capture trace headers
|
|
const traceHeaders = {
|
|
timestamp: new Date().toISOString(),
|
|
method: method,
|
|
url: url,
|
|
traceparent: headers["traceparent"] || null,
|
|
tracestate: headers["tracestate"] || null,
|
|
baggage: headers["baggage"] || null,
|
|
grpcAcceptEncoding: headers["grpc-accept-encoding"] || null,
|
|
contentType: headers["content-type"] || null,
|
|
};
|
|
|
|
capturedRequests.push(traceHeaders);
|
|
|
|
// Write to file for test assertions
|
|
fs.mkdirSync(OUTPUT_DIR, { recursive: true });
|
|
fs.writeFileSync(HEADERS_FILE, JSON.stringify(capturedRequests, null, 2));
|
|
|
|
console.log(`[${traceHeaders.timestamp}] ${method} ${url}`);
|
|
console.log(` traceparent: ${traceHeaders.traceparent || "(none)"}`);
|
|
|
|
// Helper to send response
|
|
const sendResponse = (statusCode, contentType, body) => {
|
|
if (isHttp2) {
|
|
stream.respond({
|
|
":status": statusCode,
|
|
"content-type": contentType,
|
|
"grpc-status": "0",
|
|
"grpc-message": "OK",
|
|
});
|
|
stream.end(body);
|
|
} else {
|
|
stream.writeHead(statusCode, {
|
|
"Content-Type": contentType,
|
|
"grpc-status": "0",
|
|
"grpc-message": "OK",
|
|
});
|
|
stream.end(body);
|
|
}
|
|
};
|
|
|
|
// Health check
|
|
if (url.includes("/health") || url === "/") {
|
|
sendResponse(200, "application/json", safeJsonStringify({ status: "ok" }));
|
|
return;
|
|
}
|
|
|
|
// Return captured headers (for debugging)
|
|
if (url === "/captured-headers") {
|
|
sendResponse(200, "application/json", safeJsonStringify(capturedRequests));
|
|
return;
|
|
}
|
|
|
|
// Mock Connect/gRPC responses
|
|
// These are minimal responses to prevent the login app from crashing
|
|
if (url.includes("GetLoginSettings") || url.includes("SettingsService")) {
|
|
sendResponse(200, "application/json", safeJsonStringify({ settings: {} }));
|
|
} else if (url.includes("GetBrandingSettings")) {
|
|
sendResponse(200, "application/json", safeJsonStringify({ settings: {} }));
|
|
} else if (url.includes("SessionService") || url.includes("Session")) {
|
|
sendResponse(200, "application/json", safeJsonStringify({ session: {} }));
|
|
} else if (url.includes("UserService") || url.includes("User")) {
|
|
sendResponse(200, "application/json", safeJsonStringify({ user: {} }));
|
|
} else if (url.includes("OrganizationService") || url.includes("Organization")) {
|
|
sendResponse(200, "application/json", safeJsonStringify({ result: [] }));
|
|
} else if (url.includes("IdentityProviderService") || url.includes("IdentityProvider")) {
|
|
sendResponse(200, "application/json", safeJsonStringify({ identityProviders: [] }));
|
|
} else {
|
|
sendResponse(200, "application/json", safeJsonStringify({}));
|
|
}
|
|
}
|
|
|
|
// Create HTTP/2 server (cleartext for testing)
|
|
const http2Server = http2.createServer();
|
|
|
|
http2Server.on("stream", (stream, headers) => {
|
|
handleRequest(stream, headers, true);
|
|
});
|
|
|
|
http2Server.on("error", (err) => console.error("HTTP/2 error:", err));
|
|
|
|
// Also create HTTP/1.1 server for health checks from wget
|
|
const httpServer = http.createServer((req, res) => {
|
|
// Create a headers object compatible with handleRequest
|
|
const headers = {
|
|
method: req.method,
|
|
url: req.url,
|
|
...req.headers,
|
|
};
|
|
handleRequest(res, headers, false);
|
|
});
|
|
|
|
// Start HTTP/2 server on main port
|
|
http2Server.listen(PORT, "0.0.0.0", () => {
|
|
console.log(`Mock Zitadel HTTP/2 server listening on port ${PORT}`);
|
|
console.log(`Captured headers will be written to: ${HEADERS_FILE}`);
|
|
});
|
|
|
|
// Start HTTP/1.1 server on a different port for health checks
|
|
const HEALTH_PORT = 7432;
|
|
httpServer.listen(HEALTH_PORT, "0.0.0.0", () => {
|
|
console.log(`Mock Zitadel HTTP/1.1 health server listening on port ${HEALTH_PORT}`);
|
|
});
|
|
|
|
// Graceful shutdown
|
|
process.on("SIGTERM", () => {
|
|
console.log("Shutting down mock servers...");
|
|
http2Server.close(() => {
|
|
httpServer.close(() => {
|
|
console.log("Mock servers stopped");
|
|
process.exit(0);
|
|
});
|
|
});
|
|
});
|