From e2cbf044cc6b7d5bba9b11a39358325e46b06bad Mon Sep 17 00:00:00 2001 From: Suleiman Dibirov Date: Sat, 18 Oct 2025 15:01:36 +0300 Subject: [PATCH] Fixes Signed-off-by: Suleiman Dibirov --- internal/plugin/installer/extractor.go | 4 ++ internal/plugin/installer/gzip.go | 50 +++++++++++++++ internal/plugin/installer/installer.go | 61 +++++++++---------- internal/plugin/installer/oci_installer.go | 2 +- .../plugin/installer/oci_installer_test.go | 2 +- 5 files changed, 84 insertions(+), 35 deletions(-) create mode 100644 internal/plugin/installer/gzip.go diff --git a/internal/plugin/installer/extractor.go b/internal/plugin/installer/extractor.go index 407138197..aadcbfefa 100644 --- a/internal/plugin/installer/extractor.go +++ b/internal/plugin/installer/extractor.go @@ -68,6 +68,10 @@ func NewExtractor(source string) (Extractor, error) { return extractor, nil } } + if strings.HasPrefix(source, "http") && isGzipArchiveFromURL(source) { + return &TarGzExtractor{}, nil + } + return nil, fmt.Errorf("no extractor implemented yet for %s", source) } diff --git a/internal/plugin/installer/gzip.go b/internal/plugin/installer/gzip.go new file mode 100644 index 000000000..d247d542f --- /dev/null +++ b/internal/plugin/installer/gzip.go @@ -0,0 +1,50 @@ +/* +Copyright The Helm Authors. +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + +http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +package installer + +import "net/http" + +// isGzipArchive checks if data represents a gzip archive by checking the magic bytes +func isGzipArchive(data []byte) bool { + return len(data) >= 2 && data[0] == 0x1f && data[1] == 0x8b +} + +// isGzipArchiveFromURL checks if a URL points to a gzip archive by reading the magic bytes +func isGzipArchiveFromURL(url string) bool { + // Make a GET request to read the first few bytes + req, err := http.NewRequest(http.MethodGet, url, nil) + if err != nil { + return false + } + + // Request only the first 512 bytes to check magic bytes + req.Header.Set("Range", "bytes=0-511") + + resp, err := http.DefaultClient.Do(req) + if err != nil { + return false + } + defer resp.Body.Close() + + // Read the first few bytes + buf := make([]byte, 2) + n, err := resp.Body.Read(buf) + if err != nil || n < 2 { + return false + } + + return isGzipArchive(buf) +} diff --git a/internal/plugin/installer/installer.go b/internal/plugin/installer/installer.go index c4aeebc35..62654f5ac 100644 --- a/internal/plugin/installer/installer.go +++ b/internal/plugin/installer/installer.go @@ -174,49 +174,44 @@ func isLocalReference(source string) bool { // It works by checking whether the source looks like a URL and, if it does, running a // HEAD operation to see if the remote resource is a file that we understand. func isRemoteHTTPArchive(source string) bool { - if !isHTTPURL(source) { - return false - } - - contentType, err := getRemoteContentType(source) - if err != nil { - return false - } - - // Handle octet-stream specially by checking file extension - if contentType == "application/octet-stream" { + if strings.HasPrefix(source, "http://") || strings.HasPrefix(source, "https://") { + // First, check if the URL ends with a known archive suffix + // This is more reliable than content-type detection for suffix := range Extractors { if strings.HasSuffix(source, suffix) { return true } } - return false - } - - // Check if we have an extractor for this media type - if suffix, ok := mediaTypeToExtension(contentType); ok { - _, hasExtractor := Extractors[suffix] - return hasExtractor - } + // If no suffix match, try HEAD request to check content type + res, err := http.Head(source) + if err != nil { + // If we get an error at the network layer, we can't install it. So + // we return false. + return false + } - return false -} + // Next, we look for the content type or content disposition headers to see + // if they have matching extractors. + contentType := res.Header.Get("content-type") + foundSuffix, ok := mediaTypeToExtension(contentType) + if !ok { + if contentType == "application/octet-stream" && isGzipArchiveFromURL(source) { + // For generic binary content, try to detect the actual file type + // by reading the first few bytes (magic bytes) + return true + } -// isHTTPURL checks if the source is an HTTP or HTTPS URL -func isHTTPURL(source string) bool { - return strings.HasPrefix(source, "http://") || strings.HasPrefix(source, "https://") -} + return false + } -// getRemoteContentType performs a HEAD request and returns the content-type -func getRemoteContentType(url string) (string, error) { - res, err := http.Head(url) - if err != nil { - return "", err + for suffix := range Extractors { + if strings.HasSuffix(foundSuffix, suffix) { + return true + } + } } - defer res.Body.Close() - - return res.Header.Get("content-type"), nil + return false } // isPlugin checks if the directory contains a plugin.yaml file. diff --git a/internal/plugin/installer/oci_installer.go b/internal/plugin/installer/oci_installer.go index afbb42ca5..856ebb5c8 100644 --- a/internal/plugin/installer/oci_installer.go +++ b/internal/plugin/installer/oci_installer.go @@ -128,7 +128,7 @@ func (i *OCIInstaller) Install() error { } // Check if this is a gzip compressed file - if len(i.pluginData) < 2 || i.pluginData[0] != 0x1f || i.pluginData[1] != 0x8b { + if !isGzipArchive(i.pluginData) { return fmt.Errorf("plugin data is not a gzip compressed archive") } diff --git a/internal/plugin/installer/oci_installer_test.go b/internal/plugin/installer/oci_installer_test.go index 1280cf97d..aa58ef2d3 100644 --- a/internal/plugin/installer/oci_installer_test.go +++ b/internal/plugin/installer/oci_installer_test.go @@ -792,7 +792,7 @@ func TestOCIInstaller_Install_ValidationErrors(t *testing.T) { for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { // Test the gzip validation logic that's used in the Install method - if len(tt.layerData) < 2 || tt.layerData[0] != 0x1f || tt.layerData[1] != 0x8b { + if !isGzipArchive(tt.layerData) { // This matches the validation in the Install method if !tt.expectError { t.Error("expected valid gzip data")