Files
zitadel/apps/login/dockerized/otel/mock-zitadel-server.js
T

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);
});
});
});