mirror of
				https://github.com/go-gitea/gitea.git
				synced 2025-10-31 02:46:04 +01:00 
			
		
		
		
	Fix missing signature key error when pulling Docker images with SERVE_DIRECT enabled (#32365)
				
					
				
			Fix #28121 I did some tests and found that the `missing signature key` error is caused by an incorrect `Content-Type` header. Gitea correctly sets the `Content-Type` header when serving files.348d1d0f32/routers/api/packages/container/container.go (L712-L717)However, when `SERVE_DIRECT` is enabled, the `Content-Type` header may be set to an incorrect value by the storage service. To fix this issue, we can use query parameters to override response header values. https://docs.aws.amazon.com/AmazonS3/latest/API/API_GetObject.html <img width="600px" src="https://github.com/user-attachments/assets/f2ff90f0-f1df-46f9-9680-b8120222c555" /> In this PR, I introduced a new parameter to the `URL` method to support additional parameters. ``` URL(path, name string, reqParams url.Values) (*url.URL, error) ``` --- Most S3-like services support specifying the content type when storing objects. However, Gitea always use `application/octet-stream`. Therefore, I believe we also need to improve the `Save` method to support storing objects with the correct content type.b7fb20e73e/modules/storage/minio.go (L214-L221)
This commit is contained in:
		| @@ -37,8 +37,8 @@ func (s *ContentStore) ShouldServeDirect() bool { | |||||||
| 	return setting.Packages.Storage.ServeDirect() | 	return setting.Packages.Storage.ServeDirect() | ||||||
| } | } | ||||||
|  |  | ||||||
| func (s *ContentStore) GetServeDirectURL(key BlobHash256Key, filename string) (*url.URL, error) { | func (s *ContentStore) GetServeDirectURL(key BlobHash256Key, filename string, reqParams url.Values) (*url.URL, error) { | ||||||
| 	return s.store.URL(KeyToRelativePath(key), filename) | 	return s.store.URL(KeyToRelativePath(key), filename, reqParams) | ||||||
| } | } | ||||||
|  |  | ||||||
| // FIXME: Workaround to be removed in v1.20 | // FIXME: Workaround to be removed in v1.20 | ||||||
|   | |||||||
| @@ -247,7 +247,7 @@ func (a *AzureBlobStorage) Delete(path string) error { | |||||||
| } | } | ||||||
|  |  | ||||||
| // URL gets the redirect URL to a file. The presigned link is valid for 5 minutes. | // URL gets the redirect URL to a file. The presigned link is valid for 5 minutes. | ||||||
| func (a *AzureBlobStorage) URL(path, name string) (*url.URL, error) { | func (a *AzureBlobStorage) URL(path, name string, reqParams url.Values) (*url.URL, error) { | ||||||
| 	blobClient := a.getBlobClient(path) | 	blobClient := a.getBlobClient(path) | ||||||
|  |  | ||||||
| 	startTime := time.Now() | 	startTime := time.Now() | ||||||
|   | |||||||
| @@ -30,7 +30,7 @@ func (s discardStorage) Delete(_ string) error { | |||||||
| 	return fmt.Errorf("%s", s) | 	return fmt.Errorf("%s", s) | ||||||
| } | } | ||||||
|  |  | ||||||
| func (s discardStorage) URL(_, _ string) (*url.URL, error) { | func (s discardStorage) URL(_, _ string, _ url.Values) (*url.URL, error) { | ||||||
| 	return nil, fmt.Errorf("%s", s) | 	return nil, fmt.Errorf("%s", s) | ||||||
| } | } | ||||||
|  |  | ||||||
|   | |||||||
| @@ -37,7 +37,7 @@ func Test_discardStorage(t *testing.T) { | |||||||
| 				assert.Error(t, err, string(tt)) | 				assert.Error(t, err, string(tt)) | ||||||
| 			} | 			} | ||||||
| 			{ | 			{ | ||||||
| 				got, err := tt.URL("path", "name") | 				got, err := tt.URL("path", "name", nil) | ||||||
| 				assert.Nil(t, got) | 				assert.Nil(t, got) | ||||||
| 				assert.Errorf(t, err, string(tt)) | 				assert.Errorf(t, err, string(tt)) | ||||||
| 			} | 			} | ||||||
|   | |||||||
| @@ -114,7 +114,7 @@ func (l *LocalStorage) Delete(path string) error { | |||||||
| } | } | ||||||
|  |  | ||||||
| // URL gets the redirect URL to a file | // URL gets the redirect URL to a file | ||||||
| func (l *LocalStorage) URL(path, name string) (*url.URL, error) { | func (l *LocalStorage) URL(path, name string, reqParams url.Values) (*url.URL, error) { | ||||||
| 	return nil, ErrURLNotSupported | 	return nil, ErrURLNotSupported | ||||||
| } | } | ||||||
|  |  | ||||||
|   | |||||||
| @@ -276,8 +276,12 @@ func (m *MinioStorage) Delete(path string) error { | |||||||
| } | } | ||||||
|  |  | ||||||
| // URL gets the redirect URL to a file. The presigned link is valid for 5 minutes. | // URL gets the redirect URL to a file. The presigned link is valid for 5 minutes. | ||||||
| func (m *MinioStorage) URL(path, name string) (*url.URL, error) { | func (m *MinioStorage) URL(path, name string, serveDirectReqParams url.Values) (*url.URL, error) { | ||||||
| 	reqParams := make(url.Values) | 	// copy serveDirectReqParams | ||||||
|  | 	reqParams, err := url.ParseQuery(serveDirectReqParams.Encode()) | ||||||
|  | 	if err != nil { | ||||||
|  | 		return nil, err | ||||||
|  | 	} | ||||||
| 	// TODO it may be good to embed images with 'inline' like ServeData does, but we don't want to have to read the file, do we? | 	// TODO it may be good to embed images with 'inline' like ServeData does, but we don't want to have to read the file, do we? | ||||||
| 	reqParams.Set("response-content-disposition", "attachment; filename=\""+quoteEscaper.Replace(name)+"\"") | 	reqParams.Set("response-content-disposition", "attachment; filename=\""+quoteEscaper.Replace(name)+"\"") | ||||||
| 	u, err := m.client.PresignedGetObject(m.ctx, m.bucket, m.buildMinioPath(path), 5*time.Minute, reqParams) | 	u, err := m.client.PresignedGetObject(m.ctx, m.bucket, m.buildMinioPath(path), 5*time.Minute, reqParams) | ||||||
|   | |||||||
| @@ -63,7 +63,7 @@ type ObjectStorage interface { | |||||||
| 	Save(path string, r io.Reader, size int64) (int64, error) | 	Save(path string, r io.Reader, size int64) (int64, error) | ||||||
| 	Stat(path string) (os.FileInfo, error) | 	Stat(path string) (os.FileInfo, error) | ||||||
| 	Delete(path string) error | 	Delete(path string) error | ||||||
| 	URL(path, name string) (*url.URL, error) | 	URL(path, name string, reqParams url.Values) (*url.URL, error) | ||||||
| 	IterateObjects(path string, iterator func(path string, obj Object) error) error | 	IterateObjects(path string, iterator func(path string, obj Object) error) error | ||||||
| } | } | ||||||
|  |  | ||||||
|   | |||||||
| @@ -425,7 +425,7 @@ func (ar artifactRoutes) getDownloadArtifactURL(ctx *ArtifactContext) { | |||||||
| 	for _, artifact := range artifacts { | 	for _, artifact := range artifacts { | ||||||
| 		var downloadURL string | 		var downloadURL string | ||||||
| 		if setting.Actions.ArtifactStorage.ServeDirect() { | 		if setting.Actions.ArtifactStorage.ServeDirect() { | ||||||
| 			u, err := ar.fs.URL(artifact.StoragePath, artifact.ArtifactName) | 			u, err := ar.fs.URL(artifact.StoragePath, artifact.ArtifactName, nil) | ||||||
| 			if err != nil && !errors.Is(err, storage.ErrURLNotSupported) { | 			if err != nil && !errors.Is(err, storage.ErrURLNotSupported) { | ||||||
| 				log.Error("Error getting serve direct url: %v", err) | 				log.Error("Error getting serve direct url: %v", err) | ||||||
| 			} | 			} | ||||||
|   | |||||||
| @@ -517,7 +517,7 @@ func (r *artifactV4Routes) getSignedArtifactURL(ctx *ArtifactContext) { | |||||||
| 	respData := GetSignedArtifactURLResponse{} | 	respData := GetSignedArtifactURLResponse{} | ||||||
|  |  | ||||||
| 	if setting.Actions.ArtifactStorage.ServeDirect() { | 	if setting.Actions.ArtifactStorage.ServeDirect() { | ||||||
| 		u, err := storage.ActionsArtifacts.URL(artifact.StoragePath, artifact.ArtifactPath) | 		u, err := storage.ActionsArtifacts.URL(artifact.StoragePath, artifact.ArtifactPath, nil) | ||||||
| 		if u != nil && err == nil { | 		if u != nil && err == nil { | ||||||
| 			respData.SignedUrl = u.String() | 			respData.SignedUrl = u.String() | ||||||
| 		} | 		} | ||||||
|   | |||||||
| @@ -703,7 +703,9 @@ func DeleteManifest(ctx *context.Context) { | |||||||
| } | } | ||||||
|  |  | ||||||
| func serveBlob(ctx *context.Context, pfd *packages_model.PackageFileDescriptor) { | func serveBlob(ctx *context.Context, pfd *packages_model.PackageFileDescriptor) { | ||||||
| 	s, u, _, err := packages_service.GetPackageBlobStream(ctx, pfd.File, pfd.Blob) | 	serveDirectReqParams := make(url.Values) | ||||||
|  | 	serveDirectReqParams.Set("response-content-type", pfd.Properties.GetByName(container_module.PropertyMediaType)) | ||||||
|  | 	s, u, _, err := packages_service.GetPackageBlobStream(ctx, pfd.File, pfd.Blob, serveDirectReqParams) | ||||||
| 	if err != nil { | 	if err != nil { | ||||||
| 		apiError(ctx, http.StatusInternalServerError, err) | 		apiError(ctx, http.StatusInternalServerError, err) | ||||||
| 		return | 		return | ||||||
|   | |||||||
| @@ -215,7 +215,7 @@ func servePackageFile(ctx *context.Context, params parameters, serveContent bool | |||||||
| 		return | 		return | ||||||
| 	} | 	} | ||||||
|  |  | ||||||
| 	s, u, _, err := packages_service.GetPackageBlobStream(ctx, pf, pb) | 	s, u, _, err := packages_service.GetPackageBlobStream(ctx, pf, pb, nil) | ||||||
| 	if err != nil { | 	if err != nil { | ||||||
| 		apiError(ctx, http.StatusInternalServerError, err) | 		apiError(ctx, http.StatusInternalServerError, err) | ||||||
| 		return | 		return | ||||||
|   | |||||||
| @@ -209,7 +209,7 @@ func GetRawFileOrLFS(ctx *context.APIContext) { | |||||||
|  |  | ||||||
| 	if setting.LFS.Storage.ServeDirect() { | 	if setting.LFS.Storage.ServeDirect() { | ||||||
| 		// If we have a signed url (S3, object storage), redirect to this directly. | 		// If we have a signed url (S3, object storage), redirect to this directly. | ||||||
| 		u, err := storage.LFS.URL(pointer.RelativePath(), blob.Name()) | 		u, err := storage.LFS.URL(pointer.RelativePath(), blob.Name(), nil) | ||||||
| 		if u != nil && err == nil { | 		if u != nil && err == nil { | ||||||
| 			ctx.Redirect(u.String()) | 			ctx.Redirect(u.String()) | ||||||
| 			return | 			return | ||||||
| @@ -334,7 +334,7 @@ func download(ctx *context.APIContext, archiveName string, archiver *repo_model. | |||||||
| 	rPath := archiver.RelativePath() | 	rPath := archiver.RelativePath() | ||||||
| 	if setting.RepoArchive.Storage.ServeDirect() { | 	if setting.RepoArchive.Storage.ServeDirect() { | ||||||
| 		// If we have a signed url (S3, object storage), redirect to this directly. | 		// If we have a signed url (S3, object storage), redirect to this directly. | ||||||
| 		u, err := storage.RepoArchives.URL(rPath, downloadName) | 		u, err := storage.RepoArchives.URL(rPath, downloadName, nil) | ||||||
| 		if u != nil && err == nil { | 		if u != nil && err == nil { | ||||||
| 			ctx.Redirect(u.String()) | 			ctx.Redirect(u.String()) | ||||||
| 			return | 			return | ||||||
|   | |||||||
| @@ -39,7 +39,7 @@ func storageHandler(storageSetting *setting.Storage, prefix string, objStore sto | |||||||
| 			rPath := strings.TrimPrefix(req.URL.Path, "/"+prefix+"/") | 			rPath := strings.TrimPrefix(req.URL.Path, "/"+prefix+"/") | ||||||
| 			rPath = util.PathJoinRelX(rPath) | 			rPath = util.PathJoinRelX(rPath) | ||||||
|  |  | ||||||
| 			u, err := objStore.URL(rPath, path.Base(rPath)) | 			u, err := objStore.URL(rPath, path.Base(rPath), nil) | ||||||
| 			if err != nil { | 			if err != nil { | ||||||
| 				if os.IsNotExist(err) || errors.Is(err, os.ErrNotExist) { | 				if os.IsNotExist(err) || errors.Is(err, os.ErrNotExist) { | ||||||
| 					log.Warn("Unable to find %s %s", prefix, rPath) | 					log.Warn("Unable to find %s %s", prefix, rPath) | ||||||
|   | |||||||
| @@ -663,7 +663,7 @@ func ArtifactsDownloadView(ctx *context_module.Context) { | |||||||
| 	if len(artifacts) == 1 && artifacts[0].ArtifactName+".zip" == artifacts[0].ArtifactPath && artifacts[0].ContentEncoding == "application/zip" { | 	if len(artifacts) == 1 && artifacts[0].ArtifactName+".zip" == artifacts[0].ArtifactPath && artifacts[0].ContentEncoding == "application/zip" { | ||||||
| 		art := artifacts[0] | 		art := artifacts[0] | ||||||
| 		if setting.Actions.ArtifactStorage.ServeDirect() { | 		if setting.Actions.ArtifactStorage.ServeDirect() { | ||||||
| 			u, err := storage.ActionsArtifacts.URL(art.StoragePath, art.ArtifactPath) | 			u, err := storage.ActionsArtifacts.URL(art.StoragePath, art.ArtifactPath, nil) | ||||||
| 			if u != nil && err == nil { | 			if u != nil && err == nil { | ||||||
| 				ctx.Redirect(u.String()) | 				ctx.Redirect(u.String()) | ||||||
| 				return | 				return | ||||||
|   | |||||||
| @@ -129,7 +129,7 @@ func ServeAttachment(ctx *context.Context, uuid string) { | |||||||
|  |  | ||||||
| 	if setting.Attachment.Storage.ServeDirect() { | 	if setting.Attachment.Storage.ServeDirect() { | ||||||
| 		// If we have a signed url (S3, object storage), redirect to this directly. | 		// If we have a signed url (S3, object storage), redirect to this directly. | ||||||
| 		u, err := storage.Attachments.URL(attach.RelativePath(), attach.Name) | 		u, err := storage.Attachments.URL(attach.RelativePath(), attach.Name, nil) | ||||||
|  |  | ||||||
| 		if u != nil && err == nil { | 		if u != nil && err == nil { | ||||||
| 			ctx.Redirect(u.String()) | 			ctx.Redirect(u.String()) | ||||||
|   | |||||||
| @@ -55,7 +55,7 @@ func ServeBlobOrLFS(ctx *context.Context, blob *git.Blob, lastModified *time.Tim | |||||||
|  |  | ||||||
| 		if setting.LFS.Storage.ServeDirect() { | 		if setting.LFS.Storage.ServeDirect() { | ||||||
| 			// If we have a signed url (S3, object storage, blob storage), redirect to this directly. | 			// If we have a signed url (S3, object storage, blob storage), redirect to this directly. | ||||||
| 			u, err := storage.LFS.URL(pointer.RelativePath(), blob.Name()) | 			u, err := storage.LFS.URL(pointer.RelativePath(), blob.Name(), nil) | ||||||
| 			if u != nil && err == nil { | 			if u != nil && err == nil { | ||||||
| 				ctx.Redirect(u.String()) | 				ctx.Redirect(u.String()) | ||||||
| 				return nil | 				return nil | ||||||
|   | |||||||
| @@ -494,7 +494,7 @@ func download(ctx *context.Context, archiveName string, archiver *repo_model.Rep | |||||||
| 	rPath := archiver.RelativePath() | 	rPath := archiver.RelativePath() | ||||||
| 	if setting.RepoArchive.Storage.ServeDirect() { | 	if setting.RepoArchive.Storage.ServeDirect() { | ||||||
| 		// If we have a signed url (S3, object storage), redirect to this directly. | 		// If we have a signed url (S3, object storage), redirect to this directly. | ||||||
| 		u, err := storage.RepoArchives.URL(rPath, downloadName) | 		u, err := storage.RepoArchives.URL(rPath, downloadName, nil) | ||||||
| 		if u != nil && err == nil { | 		if u != nil && err == nil { | ||||||
| 			ctx.Redirect(u.String()) | 			ctx.Redirect(u.String()) | ||||||
| 			return | 			return | ||||||
|   | |||||||
| @@ -460,7 +460,7 @@ func buildObjectResponse(rc *requestContext, pointer lfs_module.Pointer, downloa | |||||||
| 			var link *lfs_module.Link | 			var link *lfs_module.Link | ||||||
| 			if setting.LFS.Storage.ServeDirect() { | 			if setting.LFS.Storage.ServeDirect() { | ||||||
| 				// If we have a signed url (S3, object storage), redirect to this directly. | 				// If we have a signed url (S3, object storage), redirect to this directly. | ||||||
| 				u, err := storage.LFS.URL(pointer.RelativePath(), pointer.Oid) | 				u, err := storage.LFS.URL(pointer.RelativePath(), pointer.Oid, nil) | ||||||
| 				if u != nil && err == nil { | 				if u != nil && err == nil { | ||||||
| 					// Presigned url does not need the Authorization header | 					// Presigned url does not need the Authorization header | ||||||
| 					// https://github.com/go-gitea/gitea/issues/21525 | 					// https://github.com/go-gitea/gitea/issues/21525 | ||||||
|   | |||||||
| @@ -596,12 +596,12 @@ func GetPackageFileStream(ctx context.Context, pf *packages_model.PackageFile) ( | |||||||
| 		return nil, nil, nil, err | 		return nil, nil, nil, err | ||||||
| 	} | 	} | ||||||
|  |  | ||||||
| 	return GetPackageBlobStream(ctx, pf, pb) | 	return GetPackageBlobStream(ctx, pf, pb, nil) | ||||||
| } | } | ||||||
|  |  | ||||||
| // GetPackageBlobStream returns the content of the specific package blob | // GetPackageBlobStream returns the content of the specific package blob | ||||||
| // If the storage supports direct serving and it's enabled, only the direct serving url is returned. | // If the storage supports direct serving and it's enabled, only the direct serving url is returned. | ||||||
| func GetPackageBlobStream(ctx context.Context, pf *packages_model.PackageFile, pb *packages_model.PackageBlob) (io.ReadSeekCloser, *url.URL, *packages_model.PackageFile, error) { | func GetPackageBlobStream(ctx context.Context, pf *packages_model.PackageFile, pb *packages_model.PackageBlob, serveDirectReqParams url.Values) (io.ReadSeekCloser, *url.URL, *packages_model.PackageFile, error) { | ||||||
| 	key := packages_module.BlobHash256Key(pb.HashSHA256) | 	key := packages_module.BlobHash256Key(pb.HashSHA256) | ||||||
|  |  | ||||||
| 	cs := packages_module.NewContentStore() | 	cs := packages_module.NewContentStore() | ||||||
| @@ -611,7 +611,7 @@ func GetPackageBlobStream(ctx context.Context, pf *packages_model.PackageFile, p | |||||||
| 	var err error | 	var err error | ||||||
|  |  | ||||||
| 	if cs.ShouldServeDirect() { | 	if cs.ShouldServeDirect() { | ||||||
| 		u, err = cs.GetServeDirectURL(key, pf.Name) | 		u, err = cs.GetServeDirectURL(key, pf.Name, serveDirectReqParams) | ||||||
| 		if err != nil && !errors.Is(err, storage.ErrURLNotSupported) { | 		if err != nil && !errors.Is(err, storage.ErrURLNotSupported) { | ||||||
| 			log.Error("Error getting serve direct url: %v", err) | 			log.Error("Error getting serve direct url: %v", err) | ||||||
| 		} | 		} | ||||||
|   | |||||||
		Reference in New Issue
	
	Block a user