|
|
|
@@ -202,11 +202,15 @@ function responseFromMicroserviceResult(task: RawTaskRow | null): Response {
|
|
|
|
|
if (task === null) return jsonResponse({ ok: false, error: "microservice proxy task missing" }, 502);
|
|
|
|
|
if (task.status !== "succeeded") return jsonResponse({ ok: false, error: "microservice proxy task failed", task }, 502);
|
|
|
|
|
const result = dockerStatusRecord(task.result);
|
|
|
|
|
return responseFromProviderMicroserviceResult(result, "provider-task");
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
function responseFromProviderMicroserviceResult(result: Record<string, unknown>, proxyMode: string): Response {
|
|
|
|
|
const status = Number(result.status);
|
|
|
|
|
const contentType = typeof result.contentType === "string" ? result.contentType : "application/json; charset=utf-8";
|
|
|
|
|
const bodyText = typeof result.bodyText === "string" ? result.bodyText : "";
|
|
|
|
|
if (!Number.isInteger(status) || status < 100 || status > 599) {
|
|
|
|
|
return jsonResponse({ ok: false, error: "microservice proxy returned invalid upstream status", task }, 502);
|
|
|
|
|
return jsonResponse({ ok: false, error: "microservice proxy returned invalid upstream status", result }, 502);
|
|
|
|
|
}
|
|
|
|
|
if (result.truncated === true && contentTypeIsJson(contentType)) {
|
|
|
|
|
try {
|
|
|
|
@@ -215,8 +219,6 @@ function responseFromMicroserviceResult(task: RawTaskRow | null): Response {
|
|
|
|
|
return jsonResponse({
|
|
|
|
|
ok: false,
|
|
|
|
|
error: "microservice proxy response was truncated before a JSON boundary",
|
|
|
|
|
providerId: task.provider_id,
|
|
|
|
|
command: task.command,
|
|
|
|
|
upstreamStatus: status,
|
|
|
|
|
upstreamBodyBytes: result.upstreamBodyBytes ?? null,
|
|
|
|
|
returnedBodyBytes: result.returnedBodyBytes ?? bodyText.length,
|
|
|
|
@@ -229,6 +231,8 @@ function responseFromMicroserviceResult(task: RawTaskRow | null): Response {
|
|
|
|
|
status,
|
|
|
|
|
headers: {
|
|
|
|
|
"content-type": contentType,
|
|
|
|
|
"x-unidesk-proxy-mode": proxyMode,
|
|
|
|
|
"x-unidesk-upstream-proxy-mode": typeof result.proxyMode === "string" ? result.proxyMode : "",
|
|
|
|
|
"x-unidesk-response-truncated": result.truncated === true ? "true" : "false",
|
|
|
|
|
},
|
|
|
|
|
});
|
|
|
|
@@ -607,6 +611,84 @@ async function k3sctlAdapterMicroserviceResponse(
|
|
|
|
|
return fetchMicroserviceUpstreamResponse(adapter, method, adapterTargetPath, proxyOptions, requestHeaders, bodyText, abortSignal);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
function providerHttpTunnelRequestId(providerId: string): string {
|
|
|
|
|
return `${providerId}:http_${Date.now()}_${Math.random().toString(16).slice(2)}`;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
async function waitForProviderHttpTunnelResponse(
|
|
|
|
|
providerId: string,
|
|
|
|
|
requestId: string,
|
|
|
|
|
timeoutMs: number,
|
|
|
|
|
abortSignal?: AbortSignal,
|
|
|
|
|
): Promise<{ providerId: string; requestId: string; ok: boolean; result: JsonValue } | null> {
|
|
|
|
|
return await new Promise((resolve) => {
|
|
|
|
|
let settled = false;
|
|
|
|
|
let abortHandler: (() => void) | null = null;
|
|
|
|
|
const timer = setTimeout(() => settle(null), Math.max(1, timeoutMs));
|
|
|
|
|
const settle = (message: { providerId: string; requestId: string; ok: boolean; result: JsonValue } | null): void => {
|
|
|
|
|
if (settled) return;
|
|
|
|
|
settled = true;
|
|
|
|
|
clearTimeout(timer);
|
|
|
|
|
if (abortHandler !== null) abortSignal?.removeEventListener("abort", abortHandler);
|
|
|
|
|
ctx.httpTunnelWaiters.delete(requestId);
|
|
|
|
|
resolve(message);
|
|
|
|
|
};
|
|
|
|
|
abortHandler = () => settle(null);
|
|
|
|
|
if (abortSignal !== undefined) {
|
|
|
|
|
if (abortSignal.aborted) {
|
|
|
|
|
settle(null);
|
|
|
|
|
return;
|
|
|
|
|
}
|
|
|
|
|
abortSignal.addEventListener("abort", abortHandler, { once: true });
|
|
|
|
|
}
|
|
|
|
|
ctx.httpTunnelWaiters.set(requestId, (message) => {
|
|
|
|
|
if (message !== null && message.providerId !== providerId) {
|
|
|
|
|
logger("warn", "http_tunnel_provider_mismatch", { requestId, expectedProviderId: providerId, actualProviderId: message.providerId });
|
|
|
|
|
settle(null);
|
|
|
|
|
return;
|
|
|
|
|
}
|
|
|
|
|
settle(message);
|
|
|
|
|
});
|
|
|
|
|
});
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
async function providerHttpTunnelMicroserviceResponse(
|
|
|
|
|
service: MicroserviceConfig,
|
|
|
|
|
method: string,
|
|
|
|
|
targetPath: string,
|
|
|
|
|
proxyOptions: { query: string; jsonArrayLimits: Record<string, JsonValue> },
|
|
|
|
|
requestHeaders: Record<string, JsonValue>,
|
|
|
|
|
bodyText: string,
|
|
|
|
|
abortSignal?: AbortSignal,
|
|
|
|
|
): Promise<Response> {
|
|
|
|
|
const socket = ctx.activeProviders.get(service.providerId);
|
|
|
|
|
if (socket === undefined) return jsonResponse({ ok: false, error: `provider is offline: ${service.providerId}` }, 503);
|
|
|
|
|
const requestId = providerHttpTunnelRequestId(service.providerId);
|
|
|
|
|
const timeoutMs = service.backend.timeoutMs + 3000;
|
|
|
|
|
const waiter = waitForProviderHttpTunnelResponse(service.providerId, requestId, timeoutMs, abortSignal);
|
|
|
|
|
socket.send(JSON.stringify({
|
|
|
|
|
type: "http_tunnel_request",
|
|
|
|
|
requestId,
|
|
|
|
|
payload: {
|
|
|
|
|
source: "microservice-frontend-proxy",
|
|
|
|
|
serviceId: service.id,
|
|
|
|
|
method,
|
|
|
|
|
targetBaseUrl: service.backend.nodeBaseUrl,
|
|
|
|
|
path: targetPath,
|
|
|
|
|
query: proxyOptions.query,
|
|
|
|
|
requestHeaders,
|
|
|
|
|
bodyText,
|
|
|
|
|
jsonArrayLimits: proxyOptions.jsonArrayLimits,
|
|
|
|
|
timeoutMs: service.backend.timeoutMs,
|
|
|
|
|
cacheTtlMs: providerMicroserviceCacheTtlMs(service.id, targetPath),
|
|
|
|
|
},
|
|
|
|
|
}));
|
|
|
|
|
const message = await waiter;
|
|
|
|
|
if (message === null) return jsonResponse({ ok: false, error: "provider HTTP tunnel timed out or disconnected", providerId: service.providerId, requestId }, 504);
|
|
|
|
|
if (!message.ok) return jsonResponse({ ok: false, error: "provider HTTP tunnel failed", providerId: service.providerId, requestId, result: message.result }, 502);
|
|
|
|
|
return responseFromProviderMicroserviceResult(dockerStatusRecord(message.result), "provider-ws-http-tunnel");
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
async function fetchMicroserviceUpstreamResponse(
|
|
|
|
|
service: MicroserviceConfig,
|
|
|
|
|
method: string,
|
|
|
|
@@ -625,6 +707,9 @@ async function fetchMicroserviceUpstreamResponse(
|
|
|
|
|
if (!(await providerSupports(service.providerId, "microservice.http"))) {
|
|
|
|
|
return jsonResponse({ ok: false, error: `provider does not declare microservice.http capability: ${service.providerId}` }, 409);
|
|
|
|
|
}
|
|
|
|
|
if (await providerSupports(service.providerId, "microservice.http.tunnel")) {
|
|
|
|
|
return providerHttpTunnelMicroserviceResponse(service, method, targetPath, proxyOptions, requestHeaders, bodyText, abortSignal);
|
|
|
|
|
}
|
|
|
|
|
const { taskId, providerOnline } = await createAndSendTask(service.providerId, "microservice.http", {
|
|
|
|
|
source: "microservice-frontend-proxy",
|
|
|
|
|
serviceId: service.id,
|
|
|
|
|