fix: proxy local storage uploads through hub HTTP endpoint#102
fix: proxy local storage uploads through hub HTTP endpoint#102meatballs wants to merge 5 commits intoGoogleCloudPlatform:mainfrom
Conversation
Local storage generates file:// signed URLs that reference server-side filesystem paths. When the client and hub are on different machines, the client cannot write to these paths, causing "mkdir /home/scion: permission denied" errors during template sync. For local storage, rewrite file:// upload URLs to HTTP proxy URLs pointing to the hub's own template files endpoint (PUT /api/v1/templates/<id>/files/<path>). The client's authenticated HTTP transport handles auth automatically. Changes: - Add requestBaseURL() to derive external URL from request headers - Add rewriteLocalUploadURLs() to convert file:// URLs to HTTP proxy URLs - Add raw binary PUT support to handleTemplateFileWrite (Content-Type detection: non-JSON bodies are written directly to storage) - Wire up URL rewriting in template create and upload handlers Fixes GoogleCloudPlatform#100
Extends the upload proxy from the previous commit to cover downloads and fix authentication for both directions. Downloads: When local storage generates file:// download URLs, rewrite them to HTTP proxy URLs (GET /api/v1/templates/<id>/files/<path>?raw=1). The raw query parameter triggers streaming binary content instead of the default JSON-wrapped response with a 1MB size limit. Auth: The transfer client (used for signed URL uploads/downloads) was created with a plain *http.Client that didn't carry authentication. This worked for GCS signed URLs (auth is in the URL) and file:// URLs (no HTTP), but proxy URLs to the hub need auth. Add an AuthenticatedHTTPClient() method to apiclient.Transport that returns an *http.Client with an auth-injecting RoundTripper. All three service transfer clients (templates, workspaces, harness configs) now use it.
pkg/hub/storage_helpers.go
Outdated
| for i := range urls { | ||
| if strings.HasPrefix(urls[i].URL, "file://") { | ||
| urls[i].URL = hubEndpoint + "/api/v1/" + resourceType + "/" + resourceID + "/files/" + urls[i].Path | ||
| urls[i].Method = "PUT" |
There was a problem hiding this comment.
should use http.MethodPut here instead of plaintext "PUT"
| return urls | ||
| } | ||
| hubEndpoint = strings.TrimRight(hubEndpoint, "/") | ||
| for i := range urls { |
There was a problem hiding this comment.
This might be a better approach:
type UploadURL struct {
URL string
Method string
Path string
// ...
}
for i := range urls {
if strings.HasPrefix(urls[i].URL, "file://") {
urls[i].URL = fmt.Sprintf("%s/api/v1/%s/%s/files/%s",
strings.TrimRight(hubEndpoint, "/"),
resourceType,
resourceID,
urls[i].Path,
)
urls[i].Method = http.MethodPut
}
}
| // Raw binary download for local storage proxy flow | ||
| if r.URL.Query().Get("raw") != "" || strings.Contains(r.Header.Get("Accept"), "application/octet-stream") { | ||
| objectPath := template.StoragePath + "/" + filePath | ||
| reader, _, err := stor.Download(ctx, objectPath) |
There was a problem hiding this comment.
prefer errors.Is here, if errors.Is(err, storage.ErrNotFound) {
pkg/hub/template_file_handlers.go
Outdated
|
|
||
| w.Header().Set("Content-Type", "application/octet-stream") | ||
| w.WriteHeader(http.StatusOK) | ||
| io.Copy(w, reader) |
There was a problem hiding this comment.
should log error here,
// Log the error but don't send another response (headers already sent)
log.Printf("Error streaming file %s: %v", objectPath, err)
}
| } | ||
| defer reader.Close() | ||
|
|
||
| w.Header().Set("Content-Type", "application/octet-stream") |
There was a problem hiding this comment.
nit: add
w.Header().Set("Content-Disposition", attachment; filename="+filePath+"`)
below the Content-type line
pkg/hub/template_file_handlers.go
Outdated
|
|
||
| w.Header().Set("Content-Type", "application/octet-stream") | ||
| w.WriteHeader(http.StatusOK) | ||
| io.Copy(w, reader) |
There was a problem hiding this comment.
consider using http.ServeContent(w, r, filePath, meta.ModTime, seeker) instead here
- Use http.MethodPut instead of string literal "PUT" - Use fmt.Sprintf for URL construction - Use errors.Is for error comparison - Add Content-Disposition header on raw file downloads - Log io.Copy errors during file streaming - Add errors and slog imports
pkg/hub/storage_helpers.go
Outdated
| // back to the request's TLS state and Host header. | ||
| func requestBaseURL(r *http.Request) string { | ||
| scheme := "http" | ||
| if proto := r.Header.Get("X-Forwarded-Proto"); proto != "" { |
There was a problem hiding this comment.
consider using guard clauses here
PR-102 Review: fix: proxy local storage uploads through hub HTTP endpointReviewer: Scion Code Review Agent Executive SummaryThis PR solves a real architectural gap where local-storage Critical Issues1. Header Injection via
|
| Priority | Issue | Effort |
|---|---|---|
| Must fix | #2: RoundTrip must clone the request before mutating headers |
Small (~5 lines) |
| Must fix | #1: Sanitize filePath in Content-Disposition header |
Small (~3 lines) |
| Should fix | #3: Investigate whether auth header in URL response is redundant | Investigation |
| Consider | #4: Document proxy header trust assumption | Documentation |
| Consider | #5: Set Content-Length on raw downloads |
Small (~1 line) |
|
Addressed the must-fix review items locally, but I cannot push directly to the PR head branch because
|
|
Thanks @mfreeman451 — I've cherry-picked your review-fix commit (
|
|
merged commits directly after fixing lint |
Summary
Fixes #100 —
scion template syncfails withmkdir /home/scion: permission deniedwhen the hub uses local filesystem storage.Root cause: Local storage generates
file://signed URLs containing server-side absolute paths (e.g.file:///home/scion/.scion/storage/local/templates/...). The client receives these URLs and attempts to write/read at those paths on the local machine, which fails because the hub's filesystem isn't accessible remotely.Fix: When the hub's storage backend is local, rewrite
file://signed URLs to HTTP proxy URLs pointing to the hub's own template files endpoint. Both uploads (PUT) and downloads (GET) are proxied. The client's existing HTTP code handles these transparently.Commit 1: Upload proxy
storage_helpers.go: AddrequestBaseURL()(derives external URL from request headers, supports reverse proxies) andrewriteLocalUploadURLs()(convertsfile://to HTTP PUT URLs with auth headers)template_file_handlers.go: Add raw binary PUT support viahandleTemplateFileWriteRaw— detects non-JSON Content-Type and writes body directly to storagetemplate_handlers.go: Wire up URL rewriting in template create and upload handlersCommit 2: Download proxy + authenticated transfer clients
storage_helpers.go: AddrewriteLocalDownloadURLs()— convertsfile://to HTTP GET URLs with?raw=1paramtemplate_file_handlers.go: Add raw binary streaming tohandleTemplateFileRead— when?raw=1orAccept: application/octet-stream, streams file content directly instead of JSON wrapper (bypasses 1MB limit)template_handlers.go: Wire up download URL rewritingapiclient/transport.go: AddAuthenticatedHTTPClient()— returns an*http.Clientwith an auth-injecting RoundTripper, so proxy URLs carry proper authhubclient/{templates,workspace,harness_configs}.go: UseAuthenticatedHTTPClient()for transfer clientsNo client-side changes required
The client already handles HTTP upload/download URLs (used with GCS storage). The fix is entirely server-side — the hub returns different URLs, and existing client code works unchanged.
Test plan
go build ./...— compiles cleanlygo vet— no warningsscion template syncuploads all files successfullyfile://URLs generated, rewrite is a no-op)