-
Notifications
You must be signed in to change notification settings - Fork 37
fix: route signed Harness log blobs through client #199
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: main
Are you sure you want to change the base?
Changes from all commits
96ce594
404170e
1dfc9f5
7165e43
bc66478
6b65b13
2249342
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -1,5 +1,6 @@ | ||
| import { gunzipSync, inflateRawSync } from "node:zlib"; | ||
| import type { HarnessClient } from "../client/harness-client.js"; | ||
| import { HarnessApiError } from "./errors.js"; | ||
| import { createLogger } from "./logger.js"; | ||
|
|
||
| const log = createLogger("log-resolver"); | ||
|
|
@@ -38,6 +39,7 @@ const EXTERNAL_STORAGE_HOSTS = new Set([ | |
|
|
||
| /** S3-style host pattern: bucket.s3.amazonaws.com or bucket.s3.region.amazonaws.com */ | ||
| const S3_BUCKET_HOST_RE = /^[a-z0-9][a-z0-9.-]*\.s3([.-][a-z0-9-]+)?\.amazonaws\.com$/i; | ||
| const S3_PATH_STYLE_HOST_RE = /^s3([.-][a-z0-9-]+)?\.amazonaws\.com$/i; | ||
|
|
||
| function safeParseUrl(raw: string): URL | undefined { | ||
| try { | ||
|
|
@@ -51,14 +53,18 @@ function isExternalStorageHost(host: string): boolean { | |
| const h = host.toLowerCase(); | ||
| if (EXTERNAL_STORAGE_HOSTS.has(h)) return true; | ||
| if (S3_BUCKET_HOST_RE.test(h)) return true; | ||
| if (S3_PATH_STYLE_HOST_RE.test(h)) return true; | ||
| if (h.endsWith(".storage.googleapis.com")) return true; | ||
| return false; | ||
| } | ||
|
|
||
| function normalizePath(path: string): string { | ||
| return path.startsWith("/") ? path : `/${path}`; | ||
| } | ||
|
|
||
| /** | ||
| * Detect pre-signed URLs that should be fetched directly without auth. | ||
| * GCS and S3 signed URLs carry their credentials in query params — adding | ||
| * extra auth headers or rewriting the host invalidates the signature. | ||
| * Detect signed URLs whose path/query must not be rewritten. | ||
| * Direct fetching is still limited to recognized external storage hosts. | ||
| */ | ||
| function isPresignedUrl(url: URL): boolean { | ||
| if (url.searchParams.has("X-Goog-Signature")) return true; | ||
|
|
@@ -327,31 +333,36 @@ async function downloadBlobContent( | |
| signal: AbortSignal, | ||
| ): Promise<Response> { | ||
| const blobUrl = safeParseUrl(blobLink); | ||
| const isSignedBlobUrl = blobUrl ? isPresignedUrl(blobUrl) : false; | ||
|
Contributor
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. This still misses relative signed links. |
||
|
|
||
| if (blobUrl && (isExternalStorageHost(blobUrl.host) || isPresignedUrl(blobUrl))) { | ||
| if (blobUrl && isExternalStorageHost(blobUrl.hostname)) { | ||
| log.debug("Downloading log blob (direct)", { prefix, url: blobLink.slice(0, 80) }); | ||
| try { | ||
| return await fetch(blobLink, { signal }); | ||
| } catch (err) { | ||
| const cause = err instanceof Error ? `${err.name}: ${err.message}` : String(err); | ||
| throw new Error(`Log download fetch failed for ${blobUrl.host}: ${cause}`); | ||
| throw new Error(`Log download fetch failed for ${blobUrl.hostname}: ${cause}`); | ||
| } | ||
| } | ||
|
|
||
| const rawPath = blobUrl ? blobUrl.pathname + blobUrl.search : blobLink; | ||
| const downloadPath = rawPath.startsWith(LOG_SERVICE_GATEWAY_PREFIX) | ||
| const rawPath = normalizePath(blobUrl ? blobUrl.pathname + blobUrl.search : blobLink); | ||
| const downloadPath = isSignedBlobUrl | ||
| ? rawPath | ||
| : `${LOG_SERVICE_GATEWAY_PREFIX}${rawPath}`; | ||
| : rawPath.startsWith(LOG_SERVICE_GATEWAY_PREFIX) | ||
| ? rawPath | ||
| : `${LOG_SERVICE_GATEWAY_PREFIX}${rawPath}`; | ||
| log.debug("Downloading log blob (client)", { prefix, path: downloadPath.slice(0, 80) }); | ||
| try { | ||
| return await client.requestStream({ | ||
| method: "GET", | ||
| path: downloadPath, | ||
| signal, | ||
| ...(isSignedBlobUrl ? { headerBasedScoping: true } : {}), | ||
| }); | ||
| } catch (err) { | ||
| if (err instanceof HarnessApiError) throw err; | ||
| const cause = err instanceof Error ? `${err.name}: ${err.message}` : String(err); | ||
| throw new Error(`Log download fetch failed for ${blobUrl?.host ?? "harness"}: ${cause}`); | ||
| throw new Error(`Log download fetch failed for ${blobUrl?.hostname ?? "harness"}: ${cause}`); | ||
| } | ||
| } | ||
|
|
||
|
|
||
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Relative signed links are still misclassified here.
safeParseUrl()returnsundefinedfor values like/some/blob/path?X-Amz-Signature=sig, soisSignedBlobUrlbecomes false and the code later prepends/gateway/log-serviceand omitsheaderBasedScoping. Running that through the realHarnessClientproduces a URL likehttps://<base>/gateway/log-service/some/blob/path?accountIdentifier=...&routingId=...&accountID=...&X-Amz-Signature=sig, which mutates the signed blob URL. Please detect signed relative paths too and add a regression test for that case.