diff --git a/cmd/notation/internal/plugin/plugin.go b/cmd/notation/internal/plugin/plugin.go index 078f66ea3..e9b44b36c 100644 --- a/cmd/notation/internal/plugin/plugin.go +++ b/cmd/notation/internal/plugin/plugin.go @@ -17,7 +17,11 @@ import ( "context" "fmt" "io" + "mime" "net/http" + "os" + "path" + "path/filepath" "time" "github.com/notaryproject/notation/internal/httputil" @@ -52,35 +56,53 @@ const ( // URL const DownloadPluginFromURLTimeout = 10 * time.Minute -// DownloadPluginFromURL downloads plugin source from url to a tmp file on file -// system -func DownloadPluginFromURL(ctx context.Context, pluginURL string, tmpFile io.Writer) error { +// DownloadPluginFromURL downloads plugin source from url to a tmp dir on file +// system. On success, it returns the downloaded file. +func DownloadPluginFromURL(ctx context.Context, pluginURL, tmpDir string) (*os.File, error) { // Get the data client := httputil.NewAuthClient(ctx, &http.Client{Timeout: DownloadPluginFromURLTimeout}) req, err := http.NewRequest(http.MethodGet, pluginURL, nil) if err != nil { - return err + return nil, err } resp, err := client.Do(req) if err != nil { - return err + return nil, err } defer resp.Body.Close() // Check server response if resp.StatusCode != http.StatusOK { - return fmt.Errorf("%s %q: https response bad status: %s", resp.Request.Method, resp.Request.URL, resp.Status) + return nil, fmt.Errorf("%s %q: https response bad status: %s", resp.Request.Method, resp.Request.URL, resp.Status) + } + var downloadedFilename string + if cd := resp.Header.Get("Content-Disposition"); cd != "" { + _, params, err := mime.ParseMediaType(cd) + if err == nil { // if there's an error, use the filename in URL + downloadedFilename = params["filename"] + } + } + if downloadedFilename == "" { + downloadedFilename = path.Base(req.URL.Path) } // Write the body to file + tmpFilePath := filepath.Join(tmpDir, downloadedFilename) + tmpFile, err := os.Create(tmpFilePath) + if err != nil { + return nil, err + } lr := &io.LimitedReader{ R: resp.Body, N: MaxPluginSourceBytes, } _, err = io.Copy(tmpFile, lr) if err != nil { - return err + tmpFile.Close() + return nil, err } if lr.N == 0 { - return fmt.Errorf("%s %q: https response reached the %d MiB size limit", resp.Request.Method, resp.Request.URL, MaxPluginSourceBytes/1024/1024) + tmpFile.Close() + return nil, fmt.Errorf("%s %q: https response reached the %d MiB size limit", resp.Request.Method, resp.Request.URL, MaxPluginSourceBytes/1024/1024) } - return nil + tmpFile.Close() + return tmpFile, nil } diff --git a/cmd/notation/plugin/install.go b/cmd/notation/plugin/install.go index 6f32c7b0f..fe81ce765 100644 --- a/cmd/notation/plugin/install.go +++ b/cmd/notation/plugin/install.go @@ -37,8 +37,7 @@ import ( ) const ( - notationPluginTmpDir = "notation-plugin" - notationPluginDownloadTmpFile = "notation-plugin-download" + notationPluginTmpDir = "notation-plugin" ) type pluginInstallOpts struct { @@ -136,14 +135,13 @@ func install(command *cobra.Command, opts *pluginInstallOpts) error { if pluginURL.Scheme != "https" { return fmt.Errorf("failed to download plugin from URL: only the HTTPS scheme is supported, but got %s", pluginURL.Scheme) } - tmpFile, err := os.CreateTemp("", notationPluginDownloadTmpFile) + tmpDir, err := os.MkdirTemp("", notationPluginTmpDir) if err != nil { - return fmt.Errorf("failed to create temporary file required for downloading plugin: %w", err) + return fmt.Errorf("failed to create temporary directory: %w", err) } - defer os.Remove(tmpFile.Name()) - defer tmpFile.Close() + defer os.RemoveAll(tmpDir) fmt.Printf("Downloading plugin from %s\n", opts.pluginSource) - err = notationplugin.DownloadPluginFromURL(ctx, opts.pluginSource, tmpFile) + tmpFile, err := notationplugin.DownloadPluginFromURL(ctx, opts.pluginSource, tmpDir) if err != nil { return fmt.Errorf("failed to download plugin from URL %s with error: %w", opts.pluginSource, err) } @@ -200,6 +198,11 @@ func installPlugin(ctx context.Context, inputPath string, inputChecksum string, if inputFileInfo.Size() >= osutil.MaxFileBytes { return fmt.Errorf("file size reached the %d MiB size limit", osutil.MaxFileBytes/1024/1024) } + // set permission to 0700 for plugin validation before installation. + // The eventual plugin permission is updated in notation-go. + if err := os.Chmod(inputPath, 0700); err != nil { + return err + } installOpts := plugin.CLIInstallOptions{ PluginPath: inputPath, Overwrite: force,