fix(dev): share auth across dev frontend proxy
This commit is contained in:
@@ -16,6 +16,7 @@ const providerHttpTunnelMaxAttempts = 3;
|
||||
const microserviceForwardRequestHeaders = [
|
||||
"accept",
|
||||
"content-type",
|
||||
"cookie",
|
||||
"range",
|
||||
"x-auth",
|
||||
"x-requested-with",
|
||||
@@ -28,6 +29,16 @@ const microserviceForwardRequestHeaders = [
|
||||
"upload-metadata",
|
||||
"upload-offset",
|
||||
] as const;
|
||||
const microserviceForwardResponseHeaders = [
|
||||
"cache-control",
|
||||
"content-disposition",
|
||||
"etag",
|
||||
"expires",
|
||||
"last-modified",
|
||||
"location",
|
||||
"set-cookie",
|
||||
"www-authenticate",
|
||||
] as const;
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Internal helpers
|
||||
@@ -140,6 +151,13 @@ function headersFromMicroserviceRequest(requestHeaders: Record<string, JsonValue
|
||||
return headers;
|
||||
}
|
||||
|
||||
function copyForwardedResponseHeaders(from: Headers, to: Headers): void {
|
||||
for (const name of microserviceForwardResponseHeaders) {
|
||||
const value = from.get(name);
|
||||
if (value !== null && value.length > 0) to.set(name, value);
|
||||
}
|
||||
}
|
||||
|
||||
function boundedMicroserviceBodyText(
|
||||
bodyText: string,
|
||||
contentType: string,
|
||||
@@ -622,15 +640,16 @@ async function directMicroserviceResponse(
|
||||
});
|
||||
const upstreamContentType = upstream.headers.get("content-type") ?? "text/plain; charset=utf-8";
|
||||
if (upstreamContentType.toLowerCase().includes("text/event-stream")) {
|
||||
const responseHeaders: Record<string, string> = {
|
||||
const responseHeaders = new Headers({
|
||||
"content-type": upstreamContentType,
|
||||
"cache-control": upstream.headers.get("cache-control") || "no-store, no-transform",
|
||||
"connection": "keep-alive",
|
||||
"x-unidesk-proxy-mode": "direct",
|
||||
"x-unidesk-response-truncated": "false",
|
||||
};
|
||||
});
|
||||
const buffering = upstream.headers.get("x-accel-buffering");
|
||||
if (buffering !== null) responseHeaders["x-accel-buffering"] = buffering;
|
||||
if (buffering !== null) responseHeaders.set("x-accel-buffering", buffering);
|
||||
copyForwardedResponseHeaders(upstream.headers, responseHeaders);
|
||||
return new Response(upstream.body, { status: upstream.status, headers: responseHeaders });
|
||||
}
|
||||
const rawBodyText = await upstream.text();
|
||||
@@ -641,13 +660,15 @@ async function directMicroserviceResponse(
|
||||
status: upstream.status,
|
||||
upstreamBodyBytes: rawBodyText.length,
|
||||
});
|
||||
const responseHeaders = new Headers({
|
||||
"content-type": upstreamContentType,
|
||||
"x-unidesk-proxy-mode": "direct",
|
||||
"x-unidesk-response-truncated": bounded.truncated ? "true" : "false",
|
||||
});
|
||||
copyForwardedResponseHeaders(upstream.headers, responseHeaders);
|
||||
return new Response(bounded.bodyText, {
|
||||
status: upstream.status,
|
||||
headers: {
|
||||
"content-type": upstreamContentType,
|
||||
"x-unidesk-proxy-mode": "direct",
|
||||
"x-unidesk-response-truncated": bounded.truncated ? "true" : "false",
|
||||
},
|
||||
headers: responseHeaders,
|
||||
});
|
||||
} catch (error) {
|
||||
return jsonResponse({ ok: false, error: "direct microservice proxy failed", serviceId: service.id, detail: errorToJson(error) }, 502);
|
||||
|
||||
@@ -35,6 +35,16 @@ const FORWARD_REQUEST_HEADERS: &[&str] = &[
|
||||
"upload-metadata",
|
||||
"upload-offset",
|
||||
];
|
||||
const FORWARD_RESPONSE_HEADERS: &[&str] = &[
|
||||
"cache-control",
|
||||
"content-disposition",
|
||||
"etag",
|
||||
"expires",
|
||||
"last-modified",
|
||||
"location",
|
||||
"set-cookie",
|
||||
"www-authenticate",
|
||||
];
|
||||
|
||||
fn service_by_id(state: &Arc<AppState>, service_id: &str) -> Option<MicroserviceConfig> {
|
||||
state
|
||||
@@ -531,6 +541,44 @@ fn headers_from_microservice_request(request_headers: &Value) -> reqwest::header
|
||||
headers
|
||||
}
|
||||
|
||||
fn forwarded_response_headers_from_reqwest(
|
||||
headers: &reqwest::header::HeaderMap,
|
||||
) -> serde_json::Map<String, Value> {
|
||||
let mut result = serde_json::Map::new();
|
||||
for name in FORWARD_RESPONSE_HEADERS {
|
||||
if let Some(value) = headers
|
||||
.get(*name)
|
||||
.and_then(|value| value.to_str().ok())
|
||||
.filter(|value| !value.is_empty())
|
||||
{
|
||||
result.insert((*name).to_string(), Value::String(truncate_text(value, 8192)));
|
||||
}
|
||||
}
|
||||
result
|
||||
}
|
||||
|
||||
fn copy_forwarded_response_headers_from_json(response: &mut Response, result: &Value) {
|
||||
let Some(headers) = result.get("responseHeaders").and_then(Value::as_object) else {
|
||||
return;
|
||||
};
|
||||
for name in FORWARD_RESPONSE_HEADERS {
|
||||
if let Some(value) = headers.get(*name).and_then(Value::as_str) {
|
||||
add_header(response, name, value);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn copy_forwarded_response_headers_from_reqwest(
|
||||
response: &mut Response,
|
||||
headers: &reqwest::header::HeaderMap,
|
||||
) {
|
||||
for name in FORWARD_RESPONSE_HEADERS {
|
||||
if let Some(value) = headers.get(*name).and_then(|value| value.to_str().ok()) {
|
||||
add_header(response, name, value);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn content_type_is_json(content_type: &str) -> bool {
|
||||
content_type.to_ascii_lowercase().contains("json")
|
||||
}
|
||||
@@ -644,6 +692,7 @@ fn response_from_provider_microservice_result(result: Value, proxy_mode: &str) -
|
||||
"false"
|
||||
},
|
||||
);
|
||||
copy_forwarded_response_headers_from_json(&mut response, &result);
|
||||
response
|
||||
}
|
||||
|
||||
@@ -705,6 +754,7 @@ async fn direct_microservice_response(
|
||||
}
|
||||
};
|
||||
let status = response.status().as_u16();
|
||||
let response_headers = response.headers().clone();
|
||||
let content_type = response
|
||||
.headers()
|
||||
.get(reqwest::header::CONTENT_TYPE)
|
||||
@@ -737,6 +787,7 @@ async fn direct_microservice_response(
|
||||
"x-unidesk-response-truncated",
|
||||
if truncated { "true" } else { "false" },
|
||||
);
|
||||
copy_forwarded_response_headers_from_reqwest(&mut response, &response_headers);
|
||||
response
|
||||
}
|
||||
|
||||
|
||||
@@ -5,6 +5,29 @@ server {
|
||||
|
||||
# The dev frontend is intentionally reached through the existing backend-core
|
||||
# microservice proxy so the public port does not need direct access to D601.
|
||||
# Auth endpoints use the production frontend only to issue/clear the shared
|
||||
# host-scoped UniDesk session cookie. Application routes and APIs still go to
|
||||
# frontend-dev.
|
||||
location = /login {
|
||||
proxy_http_version 1.1;
|
||||
proxy_set_header Host $host;
|
||||
proxy_set_header X-Forwarded-Host $host;
|
||||
proxy_set_header X-Forwarded-Proto $scheme;
|
||||
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
|
||||
proxy_set_header X-Real-IP $remote_addr;
|
||||
proxy_pass http://frontend:8080$request_uri;
|
||||
}
|
||||
|
||||
location = /logout {
|
||||
proxy_http_version 1.1;
|
||||
proxy_set_header Host $host;
|
||||
proxy_set_header X-Forwarded-Host $host;
|
||||
proxy_set_header X-Forwarded-Proto $scheme;
|
||||
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
|
||||
proxy_set_header X-Real-IP $remote_addr;
|
||||
proxy_pass http://frontend:8080$request_uri;
|
||||
}
|
||||
|
||||
location / {
|
||||
proxy_http_version 1.1;
|
||||
proxy_set_header Host $host;
|
||||
|
||||
@@ -95,12 +95,29 @@ const nativeServiceFailures = new Map<string, number>();
|
||||
const nativeServiceEndpointCache = new Map<string, { endpoint: NativeServiceEndpoint; expiresAt: number }>();
|
||||
const nativeServiceTunnels = new Map<string, NativeServiceTunnel>();
|
||||
logWriter?.prune();
|
||||
const forwardedResponseHeaderNames = [
|
||||
"cache-control",
|
||||
"content-disposition",
|
||||
"etag",
|
||||
"expires",
|
||||
"last-modified",
|
||||
"location",
|
||||
"set-cookie",
|
||||
"www-authenticate",
|
||||
] as const;
|
||||
|
||||
function envString(name: string, fallback: string): string {
|
||||
const value = process.env[name];
|
||||
return value === undefined || value.length === 0 ? fallback : value;
|
||||
}
|
||||
|
||||
function copyForwardedResponseHeaders(from: Headers, to: Headers): void {
|
||||
for (const name of forwardedResponseHeaderNames) {
|
||||
const value = from.get(name);
|
||||
if (value !== null && value.length > 0) to.set(name, value);
|
||||
}
|
||||
}
|
||||
|
||||
function envNumber(name: string, fallback: number): number {
|
||||
const raw = process.env[name];
|
||||
if (raw === undefined || raw.trim().length === 0) return fallback;
|
||||
@@ -636,17 +653,19 @@ async function fetchNativeServiceUrl(
|
||||
signal: controller.signal,
|
||||
});
|
||||
const body = await boundedText(upstream, 8 * 1024 * 1024);
|
||||
const responseHeaders = new Headers({
|
||||
"content-type": upstream.headers.get("content-type") ?? "application/octet-stream",
|
||||
"x-unidesk-proxy-mode": "kubernetes-native-service",
|
||||
"x-unidesk-k3s-service": service.id,
|
||||
"x-unidesk-k3s-service-url": upstreamUrl.origin,
|
||||
"x-unidesk-upstream-duration-ms": String(Date.now() - startedAt),
|
||||
"x-unidesk-response-truncated": body.truncated ? "true" : "false",
|
||||
...extraHeaders,
|
||||
});
|
||||
copyForwardedResponseHeaders(upstream.headers, responseHeaders);
|
||||
return new Response(body.text, {
|
||||
status: upstream.status,
|
||||
headers: {
|
||||
"content-type": upstream.headers.get("content-type") ?? "application/octet-stream",
|
||||
"x-unidesk-proxy-mode": "kubernetes-native-service",
|
||||
"x-unidesk-k3s-service": service.id,
|
||||
"x-unidesk-k3s-service-url": upstreamUrl.origin,
|
||||
"x-unidesk-upstream-duration-ms": String(Date.now() - startedAt),
|
||||
"x-unidesk-response-truncated": body.truncated ? "true" : "false",
|
||||
...extraHeaders,
|
||||
},
|
||||
headers: responseHeaders,
|
||||
});
|
||||
} finally {
|
||||
clearTimeout(timer);
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@unidesk/provider-gateway",
|
||||
"version": "0.2.24",
|
||||
"version": "0.2.25",
|
||||
"private": true,
|
||||
"type": "module",
|
||||
"scripts": {
|
||||
|
||||
@@ -130,6 +130,7 @@ const microserviceHttpMaxBodyTextLength = 8 * 1024 * 1024;
|
||||
const microserviceForwardRequestHeaders = [
|
||||
"accept",
|
||||
"content-type",
|
||||
"cookie",
|
||||
"range",
|
||||
"x-auth",
|
||||
"x-requested-with",
|
||||
@@ -142,6 +143,16 @@ const microserviceForwardRequestHeaders = [
|
||||
"upload-metadata",
|
||||
"upload-offset",
|
||||
] as const;
|
||||
const microserviceForwardResponseHeaders = [
|
||||
"cache-control",
|
||||
"content-disposition",
|
||||
"etag",
|
||||
"expires",
|
||||
"last-modified",
|
||||
"location",
|
||||
"set-cookie",
|
||||
"www-authenticate",
|
||||
] as const;
|
||||
|
||||
function readGatewayMetadataFile(path: string): { name: string; version: string } | null {
|
||||
try {
|
||||
@@ -2009,6 +2020,15 @@ function headersFromMicroserviceRequest(requestHeaders: Record<string, JsonValue
|
||||
return headers;
|
||||
}
|
||||
|
||||
function forwardedMicroserviceResponseHeaders(response: Response): Record<string, JsonValue> {
|
||||
const headers: Record<string, JsonValue> = {};
|
||||
for (const name of microserviceForwardResponseHeaders) {
|
||||
const value = response.headers.get(name);
|
||||
if (value !== null && value.length > 0) headers[name] = value.slice(0, 8192);
|
||||
}
|
||||
return headers;
|
||||
}
|
||||
|
||||
async function runMicroserviceHttp(payload: Record<string, JsonValue>): Promise<JsonValue> {
|
||||
const rawMethod = String(payload.method || "GET").toUpperCase();
|
||||
const allowedMethods = new Set(["GET", "HEAD", "POST", "PUT", "PATCH", "DELETE"]);
|
||||
@@ -2081,6 +2101,7 @@ async function runMicroserviceHttp(payload: Record<string, JsonValue>): Promise<
|
||||
returnedBodyBytes: bounded.bodyText.length,
|
||||
responseBodyLimitBytes: microserviceHttpMaxBodyTextLength,
|
||||
truncated: bounded.truncated,
|
||||
responseHeaders: forwardedMicroserviceResponseHeaders(response),
|
||||
transform: transformed.transform,
|
||||
upstreamDurationMs: Date.now() - requestStartedAt,
|
||||
proxyMode: "provider-gateway-http-fetch",
|
||||
|
||||
Reference in New Issue
Block a user