mirror of
				https://gitee.com/gitea/gitea
				synced 2025-11-04 00:20:25 +08:00 
			
		
		
		
	Add Chef package registry (#22554)
This PR implements a [Chef registry](https://chef.io/) to manage cookbooks. This package type was a bit complicated because Chef uses RSA signed requests as authentication with the registry.   Co-authored-by: Lunny Xiao <xiaolunwen@gmail.com>
This commit is contained in:
		@@ -5,8 +5,11 @@ package activitypub
 | 
			
		||||
 | 
			
		||||
import (
 | 
			
		||||
	user_model "code.gitea.io/gitea/models/user"
 | 
			
		||||
	"code.gitea.io/gitea/modules/util"
 | 
			
		||||
)
 | 
			
		||||
 | 
			
		||||
const rsaBits = 2048
 | 
			
		||||
 | 
			
		||||
// GetKeyPair function returns a user's private and public keys
 | 
			
		||||
func GetKeyPair(user *user_model.User) (pub, priv string, err error) {
 | 
			
		||||
	var settings map[string]*user_model.Setting
 | 
			
		||||
@@ -14,7 +17,7 @@ func GetKeyPair(user *user_model.User) (pub, priv string, err error) {
 | 
			
		||||
	if err != nil {
 | 
			
		||||
		return
 | 
			
		||||
	} else if len(settings) == 0 {
 | 
			
		||||
		if priv, pub, err = GenerateKeyPair(); err != nil {
 | 
			
		||||
		if priv, pub, err = util.GenerateKeyPair(rsaBits); err != nil {
 | 
			
		||||
			return
 | 
			
		||||
		}
 | 
			
		||||
		if err = user_model.SetUserSetting(user.ID, user_model.UserActivityPubPrivPem, priv); err != nil {
 | 
			
		||||
 
 | 
			
		||||
							
								
								
									
										134
									
								
								modules/packages/chef/metadata.go
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										134
									
								
								modules/packages/chef/metadata.go
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,134 @@
 | 
			
		||||
// Copyright 2023 The Gitea Authors. All rights reserved.
 | 
			
		||||
// SPDX-License-Identifier: MIT
 | 
			
		||||
 | 
			
		||||
package chef
 | 
			
		||||
 | 
			
		||||
import (
 | 
			
		||||
	"archive/tar"
 | 
			
		||||
	"compress/gzip"
 | 
			
		||||
	"io"
 | 
			
		||||
	"regexp"
 | 
			
		||||
	"strings"
 | 
			
		||||
 | 
			
		||||
	"code.gitea.io/gitea/modules/json"
 | 
			
		||||
	"code.gitea.io/gitea/modules/util"
 | 
			
		||||
	"code.gitea.io/gitea/modules/validation"
 | 
			
		||||
)
 | 
			
		||||
 | 
			
		||||
const (
 | 
			
		||||
	KeyBits          = 4096
 | 
			
		||||
	SettingPublicPem = "chef.public_pem"
 | 
			
		||||
)
 | 
			
		||||
 | 
			
		||||
var (
 | 
			
		||||
	ErrMissingMetadataFile = util.NewInvalidArgumentErrorf("metadata.json file is missing")
 | 
			
		||||
	ErrInvalidName         = util.NewInvalidArgumentErrorf("package name is invalid")
 | 
			
		||||
	ErrInvalidVersion      = util.NewInvalidArgumentErrorf("package version is invalid")
 | 
			
		||||
 | 
			
		||||
	namePattern    = regexp.MustCompile(`\A\S+\z`)
 | 
			
		||||
	versionPattern = regexp.MustCompile(`\A\d+\.\d+(?:\.\d+)?\z`)
 | 
			
		||||
)
 | 
			
		||||
 | 
			
		||||
// Package represents a Chef package
 | 
			
		||||
type Package struct {
 | 
			
		||||
	Name     string
 | 
			
		||||
	Version  string
 | 
			
		||||
	Metadata *Metadata
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
// Metadata represents the metadata of a Chef package
 | 
			
		||||
type Metadata struct {
 | 
			
		||||
	Description     string            `json:"description,omitempty"`
 | 
			
		||||
	LongDescription string            `json:"long_description,omitempty"`
 | 
			
		||||
	Author          string            `json:"author,omitempty"`
 | 
			
		||||
	License         string            `json:"license,omitempty"`
 | 
			
		||||
	RepositoryURL   string            `json:"repository_url,omitempty"`
 | 
			
		||||
	Dependencies    map[string]string `json:"dependencies,omitempty"`
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
type chefMetadata struct {
 | 
			
		||||
	Name               string            `json:"name"`
 | 
			
		||||
	Description        string            `json:"description"`
 | 
			
		||||
	LongDescription    string            `json:"long_description"`
 | 
			
		||||
	Maintainer         string            `json:"maintainer"`
 | 
			
		||||
	MaintainerEmail    string            `json:"maintainer_email"`
 | 
			
		||||
	License            string            `json:"license"`
 | 
			
		||||
	Platforms          map[string]string `json:"platforms"`
 | 
			
		||||
	Dependencies       map[string]string `json:"dependencies"`
 | 
			
		||||
	Providing          map[string]string `json:"providing"`
 | 
			
		||||
	Recipes            map[string]string `json:"recipes"`
 | 
			
		||||
	Version            string            `json:"version"`
 | 
			
		||||
	SourceURL          string            `json:"source_url"`
 | 
			
		||||
	IssuesURL          string            `json:"issues_url"`
 | 
			
		||||
	Privacy            bool              `json:"privacy"`
 | 
			
		||||
	ChefVersions       [][]string        `json:"chef_versions"`
 | 
			
		||||
	Gems               [][]string        `json:"gems"`
 | 
			
		||||
	EagerLoadLibraries bool              `json:"eager_load_libraries"`
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
// ParsePackage parses the Chef package file
 | 
			
		||||
func ParsePackage(r io.Reader) (*Package, error) {
 | 
			
		||||
	gzr, err := gzip.NewReader(r)
 | 
			
		||||
	if err != nil {
 | 
			
		||||
		return nil, err
 | 
			
		||||
	}
 | 
			
		||||
	defer gzr.Close()
 | 
			
		||||
 | 
			
		||||
	tr := tar.NewReader(gzr)
 | 
			
		||||
	for {
 | 
			
		||||
		hd, err := tr.Next()
 | 
			
		||||
		if err == io.EOF {
 | 
			
		||||
			break
 | 
			
		||||
		}
 | 
			
		||||
		if err != nil {
 | 
			
		||||
			return nil, err
 | 
			
		||||
		}
 | 
			
		||||
 | 
			
		||||
		if hd.Typeflag != tar.TypeReg {
 | 
			
		||||
			continue
 | 
			
		||||
		}
 | 
			
		||||
 | 
			
		||||
		if strings.Count(hd.Name, "/") != 1 {
 | 
			
		||||
			continue
 | 
			
		||||
		}
 | 
			
		||||
 | 
			
		||||
		if hd.FileInfo().Name() == "metadata.json" {
 | 
			
		||||
			return ParseChefMetadata(tr)
 | 
			
		||||
		}
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	return nil, ErrMissingMetadataFile
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
// ParseChefMetadata parses a metadata.json file to retrieve the metadata of a Chef package
 | 
			
		||||
func ParseChefMetadata(r io.Reader) (*Package, error) {
 | 
			
		||||
	var cm chefMetadata
 | 
			
		||||
	if err := json.NewDecoder(r).Decode(&cm); err != nil {
 | 
			
		||||
		return nil, err
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	if !namePattern.MatchString(cm.Name) {
 | 
			
		||||
		return nil, ErrInvalidName
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	if !versionPattern.MatchString(cm.Version) {
 | 
			
		||||
		return nil, ErrInvalidVersion
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	if !validation.IsValidURL(cm.SourceURL) {
 | 
			
		||||
		cm.SourceURL = ""
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	return &Package{
 | 
			
		||||
		Name:    cm.Name,
 | 
			
		||||
		Version: cm.Version,
 | 
			
		||||
		Metadata: &Metadata{
 | 
			
		||||
			Description:     cm.Description,
 | 
			
		||||
			LongDescription: cm.LongDescription,
 | 
			
		||||
			Author:          cm.Maintainer,
 | 
			
		||||
			License:         cm.License,
 | 
			
		||||
			RepositoryURL:   cm.SourceURL,
 | 
			
		||||
			Dependencies:    cm.Dependencies,
 | 
			
		||||
		},
 | 
			
		||||
	}, nil
 | 
			
		||||
}
 | 
			
		||||
							
								
								
									
										92
									
								
								modules/packages/chef/metadata_test.go
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										92
									
								
								modules/packages/chef/metadata_test.go
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,92 @@
 | 
			
		||||
// Copyright 2023 The Gitea Authors. All rights reserved.
 | 
			
		||||
// SPDX-License-Identifier: MIT
 | 
			
		||||
 | 
			
		||||
package chef
 | 
			
		||||
 | 
			
		||||
import (
 | 
			
		||||
	"archive/tar"
 | 
			
		||||
	"bytes"
 | 
			
		||||
	"compress/gzip"
 | 
			
		||||
	"strings"
 | 
			
		||||
	"testing"
 | 
			
		||||
 | 
			
		||||
	"github.com/stretchr/testify/assert"
 | 
			
		||||
)
 | 
			
		||||
 | 
			
		||||
const (
 | 
			
		||||
	packageName          = "gitea"
 | 
			
		||||
	packageVersion       = "1.0.1"
 | 
			
		||||
	packageAuthor        = "KN4CK3R"
 | 
			
		||||
	packageDescription   = "Package Description"
 | 
			
		||||
	packageRepositoryURL = "https://gitea.io/gitea/gitea"
 | 
			
		||||
)
 | 
			
		||||
 | 
			
		||||
func TestParsePackage(t *testing.T) {
 | 
			
		||||
	t.Run("MissingMetadataFile", func(t *testing.T) {
 | 
			
		||||
		var buf bytes.Buffer
 | 
			
		||||
		zw := gzip.NewWriter(&buf)
 | 
			
		||||
		tw := tar.NewWriter(zw)
 | 
			
		||||
		tw.Close()
 | 
			
		||||
		zw.Close()
 | 
			
		||||
 | 
			
		||||
		p, err := ParsePackage(&buf)
 | 
			
		||||
		assert.Nil(t, p)
 | 
			
		||||
		assert.ErrorIs(t, err, ErrMissingMetadataFile)
 | 
			
		||||
	})
 | 
			
		||||
 | 
			
		||||
	t.Run("Valid", func(t *testing.T) {
 | 
			
		||||
		var buf bytes.Buffer
 | 
			
		||||
		zw := gzip.NewWriter(&buf)
 | 
			
		||||
		tw := tar.NewWriter(zw)
 | 
			
		||||
 | 
			
		||||
		content := `{"name":"` + packageName + `","version":"` + packageVersion + `"}`
 | 
			
		||||
 | 
			
		||||
		hdr := &tar.Header{
 | 
			
		||||
			Name: packageName + "/metadata.json",
 | 
			
		||||
			Mode: 0o600,
 | 
			
		||||
			Size: int64(len(content)),
 | 
			
		||||
		}
 | 
			
		||||
		tw.WriteHeader(hdr)
 | 
			
		||||
		tw.Write([]byte(content))
 | 
			
		||||
 | 
			
		||||
		tw.Close()
 | 
			
		||||
		zw.Close()
 | 
			
		||||
 | 
			
		||||
		p, err := ParsePackage(&buf)
 | 
			
		||||
		assert.NoError(t, err)
 | 
			
		||||
		assert.NotNil(t, p)
 | 
			
		||||
		assert.Equal(t, packageName, p.Name)
 | 
			
		||||
		assert.Equal(t, packageVersion, p.Version)
 | 
			
		||||
		assert.NotNil(t, p.Metadata)
 | 
			
		||||
	})
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
func TestParseChefMetadata(t *testing.T) {
 | 
			
		||||
	t.Run("InvalidName", func(t *testing.T) {
 | 
			
		||||
		for _, name := range []string{" test", "test "} {
 | 
			
		||||
			p, err := ParseChefMetadata(strings.NewReader(`{"name":"` + name + `","version":"1.0.0"}`))
 | 
			
		||||
			assert.Nil(t, p)
 | 
			
		||||
			assert.ErrorIs(t, err, ErrInvalidName)
 | 
			
		||||
		}
 | 
			
		||||
	})
 | 
			
		||||
 | 
			
		||||
	t.Run("InvalidVersion", func(t *testing.T) {
 | 
			
		||||
		for _, version := range []string{"1", "1.2.3.4", "1.0.0 "} {
 | 
			
		||||
			p, err := ParseChefMetadata(strings.NewReader(`{"name":"test","version":"` + version + `"}`))
 | 
			
		||||
			assert.Nil(t, p)
 | 
			
		||||
			assert.ErrorIs(t, err, ErrInvalidVersion)
 | 
			
		||||
		}
 | 
			
		||||
	})
 | 
			
		||||
 | 
			
		||||
	t.Run("Valid", func(t *testing.T) {
 | 
			
		||||
		p, err := ParseChefMetadata(strings.NewReader(`{"name":"` + packageName + `","version":"` + packageVersion + `","description":"` + packageDescription + `","maintainer":"` + packageAuthor + `","source_url":"` + packageRepositoryURL + `"}`))
 | 
			
		||||
		assert.NotNil(t, p)
 | 
			
		||||
		assert.NoError(t, err)
 | 
			
		||||
 | 
			
		||||
		assert.Equal(t, packageName, p.Name)
 | 
			
		||||
		assert.Equal(t, packageVersion, p.Version)
 | 
			
		||||
		assert.Equal(t, packageDescription, p.Metadata.Description)
 | 
			
		||||
		assert.Equal(t, packageAuthor, p.Metadata.Author)
 | 
			
		||||
		assert.Equal(t, packageRepositoryURL, p.Metadata.RepositoryURL)
 | 
			
		||||
	})
 | 
			
		||||
}
 | 
			
		||||
@@ -26,6 +26,7 @@ var (
 | 
			
		||||
		LimitTotalOwnerCount int64
 | 
			
		||||
		LimitTotalOwnerSize  int64
 | 
			
		||||
		LimitSizeCargo       int64
 | 
			
		||||
		LimitSizeChef        int64
 | 
			
		||||
		LimitSizeComposer    int64
 | 
			
		||||
		LimitSizeConan       int64
 | 
			
		||||
		LimitSizeConda       int64
 | 
			
		||||
@@ -67,6 +68,7 @@ func newPackages() {
 | 
			
		||||
 | 
			
		||||
	Packages.LimitTotalOwnerSize = mustBytes(sec, "LIMIT_TOTAL_OWNER_SIZE")
 | 
			
		||||
	Packages.LimitSizeCargo = mustBytes(sec, "LIMIT_SIZE_CARGO")
 | 
			
		||||
	Packages.LimitSizeChef = mustBytes(sec, "LIMIT_SIZE_CHEF")
 | 
			
		||||
	Packages.LimitSizeComposer = mustBytes(sec, "LIMIT_SIZE_COMPOSER")
 | 
			
		||||
	Packages.LimitSizeConan = mustBytes(sec, "LIMIT_SIZE_CONAN")
 | 
			
		||||
	Packages.LimitSizeConda = mustBytes(sec, "LIMIT_SIZE_CONDA")
 | 
			
		||||
 
 | 
			
		||||
@@ -1,7 +1,7 @@
 | 
			
		||||
// Copyright 2021 The Gitea Authors. All rights reserved.
 | 
			
		||||
// SPDX-License-Identifier: MIT
 | 
			
		||||
 | 
			
		||||
package activitypub
 | 
			
		||||
package util
 | 
			
		||||
 | 
			
		||||
import (
 | 
			
		||||
	"crypto/rand"
 | 
			
		||||
@@ -10,11 +10,9 @@ import (
 | 
			
		||||
	"encoding/pem"
 | 
			
		||||
)
 | 
			
		||||
 | 
			
		||||
const rsaBits = 2048
 | 
			
		||||
 | 
			
		||||
// GenerateKeyPair generates a public and private keypair for signing actions by users for activitypub purposes
 | 
			
		||||
func GenerateKeyPair() (string, string, error) {
 | 
			
		||||
	priv, _ := rsa.GenerateKey(rand.Reader, rsaBits)
 | 
			
		||||
// GenerateKeyPair generates a public and private keypair
 | 
			
		||||
func GenerateKeyPair(bits int) (string, string, error) {
 | 
			
		||||
	priv, _ := rsa.GenerateKey(rand.Reader, bits)
 | 
			
		||||
	privPem, err := pemBlockForPriv(priv)
 | 
			
		||||
	if err != nil {
 | 
			
		||||
		return "", "", err
 | 
			
		||||
@@ -1,7 +1,7 @@
 | 
			
		||||
// Copyright 2021 The Gitea Authors. All rights reserved.
 | 
			
		||||
// SPDX-License-Identifier: MIT
 | 
			
		||||
 | 
			
		||||
package activitypub
 | 
			
		||||
package util
 | 
			
		||||
 | 
			
		||||
import (
 | 
			
		||||
	"crypto"
 | 
			
		||||
@@ -17,7 +17,7 @@ import (
 | 
			
		||||
)
 | 
			
		||||
 | 
			
		||||
func TestKeygen(t *testing.T) {
 | 
			
		||||
	priv, pub, err := GenerateKeyPair()
 | 
			
		||||
	priv, pub, err := GenerateKeyPair(2048)
 | 
			
		||||
	assert.NoError(t, err)
 | 
			
		||||
 | 
			
		||||
	assert.NotEmpty(t, priv)
 | 
			
		||||
@@ -28,7 +28,7 @@ func TestKeygen(t *testing.T) {
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
func TestSignUsingKeys(t *testing.T) {
 | 
			
		||||
	priv, pub, err := GenerateKeyPair()
 | 
			
		||||
	priv, pub, err := GenerateKeyPair(2048)
 | 
			
		||||
	assert.NoError(t, err)
 | 
			
		||||
 | 
			
		||||
	privPem, _ := pem.Decode([]byte(priv))
 | 
			
		||||
		Reference in New Issue
	
	Block a user