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:
		@@ -15,6 +15,7 @@ import (
 | 
			
		||||
	"code.gitea.io/gitea/modules/setting"
 | 
			
		||||
	"code.gitea.io/gitea/modules/web"
 | 
			
		||||
	"code.gitea.io/gitea/routers/api/packages/cargo"
 | 
			
		||||
	"code.gitea.io/gitea/routers/api/packages/chef"
 | 
			
		||||
	"code.gitea.io/gitea/routers/api/packages/composer"
 | 
			
		||||
	"code.gitea.io/gitea/routers/api/packages/conan"
 | 
			
		||||
	"code.gitea.io/gitea/routers/api/packages/conda"
 | 
			
		||||
@@ -54,6 +55,7 @@ func CommonRoutes(ctx gocontext.Context) *web.Route {
 | 
			
		||||
		&auth.Basic{},
 | 
			
		||||
		&nuget.Auth{},
 | 
			
		||||
		&conan.Auth{},
 | 
			
		||||
		&chef.Auth{},
 | 
			
		||||
	}
 | 
			
		||||
	if setting.Service.EnableReverseProxyAuth {
 | 
			
		||||
		authMethods = append(authMethods, &auth.ReverseProxy{})
 | 
			
		||||
@@ -86,6 +88,25 @@ func CommonRoutes(ctx gocontext.Context) *web.Route {
 | 
			
		||||
				})
 | 
			
		||||
			})
 | 
			
		||||
		}, reqPackageAccess(perm.AccessModeRead))
 | 
			
		||||
		r.Group("/chef", func() {
 | 
			
		||||
			r.Group("/api/v1", func() {
 | 
			
		||||
				r.Get("/universe", chef.PackagesUniverse)
 | 
			
		||||
				r.Get("/search", chef.EnumeratePackages)
 | 
			
		||||
				r.Group("/cookbooks", func() {
 | 
			
		||||
					r.Get("", chef.EnumeratePackages)
 | 
			
		||||
					r.Post("", reqPackageAccess(perm.AccessModeWrite), chef.UploadPackage)
 | 
			
		||||
					r.Group("/{name}", func() {
 | 
			
		||||
						r.Get("", chef.PackageMetadata)
 | 
			
		||||
						r.Group("/versions/{version}", func() {
 | 
			
		||||
							r.Get("", chef.PackageVersionMetadata)
 | 
			
		||||
							r.Delete("", reqPackageAccess(perm.AccessModeWrite), chef.DeletePackageVersion)
 | 
			
		||||
							r.Get("/download", chef.DownloadPackage)
 | 
			
		||||
						})
 | 
			
		||||
						r.Delete("", reqPackageAccess(perm.AccessModeWrite), chef.DeletePackage)
 | 
			
		||||
					})
 | 
			
		||||
				})
 | 
			
		||||
			})
 | 
			
		||||
		}, reqPackageAccess(perm.AccessModeRead))
 | 
			
		||||
		r.Group("/composer", func() {
 | 
			
		||||
			r.Get("/packages.json", composer.ServiceIndex)
 | 
			
		||||
			r.Get("/search.json", composer.SearchPackages)
 | 
			
		||||
 
 | 
			
		||||
							
								
								
									
										270
									
								
								routers/api/packages/chef/auth.go
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										270
									
								
								routers/api/packages/chef/auth.go
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,270 @@
 | 
			
		||||
// Copyright 2023 The Gitea Authors. All rights reserved.
 | 
			
		||||
// SPDX-License-Identifier: MIT
 | 
			
		||||
 | 
			
		||||
package chef
 | 
			
		||||
 | 
			
		||||
import (
 | 
			
		||||
	"crypto"
 | 
			
		||||
	"crypto/rsa"
 | 
			
		||||
	"crypto/sha1"
 | 
			
		||||
	"crypto/sha256"
 | 
			
		||||
	"crypto/x509"
 | 
			
		||||
	"encoding/base64"
 | 
			
		||||
	"encoding/pem"
 | 
			
		||||
	"fmt"
 | 
			
		||||
	"hash"
 | 
			
		||||
	"math/big"
 | 
			
		||||
	"net/http"
 | 
			
		||||
	"path"
 | 
			
		||||
	"regexp"
 | 
			
		||||
	"strconv"
 | 
			
		||||
	"strings"
 | 
			
		||||
	"time"
 | 
			
		||||
 | 
			
		||||
	user_model "code.gitea.io/gitea/models/user"
 | 
			
		||||
	chef_module "code.gitea.io/gitea/modules/packages/chef"
 | 
			
		||||
	"code.gitea.io/gitea/modules/util"
 | 
			
		||||
	"code.gitea.io/gitea/services/auth"
 | 
			
		||||
)
 | 
			
		||||
 | 
			
		||||
const (
 | 
			
		||||
	maxTimeDifference = 10 * time.Minute
 | 
			
		||||
)
 | 
			
		||||
 | 
			
		||||
var (
 | 
			
		||||
	algorithmPattern     = regexp.MustCompile(`algorithm=(\w+)`)
 | 
			
		||||
	versionPattern       = regexp.MustCompile(`version=(\d+\.\d+)`)
 | 
			
		||||
	authorizationPattern = regexp.MustCompile(`\AX-Ops-Authorization-(\d+)`)
 | 
			
		||||
)
 | 
			
		||||
 | 
			
		||||
// Documentation:
 | 
			
		||||
// https://docs.chef.io/server/api_chef_server/#required-headers
 | 
			
		||||
// https://github.com/chef-boneyard/chef-rfc/blob/master/rfc065-sign-v1.3.md
 | 
			
		||||
// https://github.com/chef/mixlib-authentication/blob/bc8adbef833d4be23dc78cb23e6fe44b51ebc34f/lib/mixlib/authentication/signedheaderauth.rb
 | 
			
		||||
 | 
			
		||||
type Auth struct{}
 | 
			
		||||
 | 
			
		||||
func (a *Auth) Name() string {
 | 
			
		||||
	return "chef"
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
// Verify extracts the user from the signed request
 | 
			
		||||
// If the request is signed with the user private key the user is verified.
 | 
			
		||||
func (a *Auth) Verify(req *http.Request, w http.ResponseWriter, store auth.DataStore, sess auth.SessionStore) (*user_model.User, error) {
 | 
			
		||||
	u, err := getUserFromRequest(req)
 | 
			
		||||
	if err != nil {
 | 
			
		||||
		return nil, err
 | 
			
		||||
	}
 | 
			
		||||
	if u == nil {
 | 
			
		||||
		return nil, nil
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	pub, err := getUserPublicKey(u)
 | 
			
		||||
	if err != nil {
 | 
			
		||||
		return nil, err
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	if err := verifyTimestamp(req); err != nil {
 | 
			
		||||
		return nil, err
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	version, err := getSignVersion(req)
 | 
			
		||||
	if err != nil {
 | 
			
		||||
		return nil, err
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	if err := verifySignedHeaders(req, version, pub.(*rsa.PublicKey)); err != nil {
 | 
			
		||||
		return nil, err
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	return u, nil
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
func getUserFromRequest(req *http.Request) (*user_model.User, error) {
 | 
			
		||||
	username := req.Header.Get("X-Ops-Userid")
 | 
			
		||||
	if username == "" {
 | 
			
		||||
		return nil, nil
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	return user_model.GetUserByName(req.Context(), username)
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
func getUserPublicKey(u *user_model.User) (crypto.PublicKey, error) {
 | 
			
		||||
	pubKey, err := user_model.GetSetting(u.ID, chef_module.SettingPublicPem)
 | 
			
		||||
	if err != nil {
 | 
			
		||||
		return nil, err
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	pubPem, _ := pem.Decode([]byte(pubKey))
 | 
			
		||||
 | 
			
		||||
	return x509.ParsePKIXPublicKey(pubPem.Bytes)
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
func verifyTimestamp(req *http.Request) error {
 | 
			
		||||
	hdr := req.Header.Get("X-Ops-Timestamp")
 | 
			
		||||
	if hdr == "" {
 | 
			
		||||
		return util.NewInvalidArgumentErrorf("X-Ops-Timestamp header missing")
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	ts, err := time.Parse(time.RFC3339, hdr)
 | 
			
		||||
	if err != nil {
 | 
			
		||||
		return err
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	diff := time.Now().UTC().Sub(ts)
 | 
			
		||||
	if diff < 0 {
 | 
			
		||||
		diff = -diff
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	if diff > maxTimeDifference {
 | 
			
		||||
		return fmt.Errorf("time difference")
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	return nil
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
func getSignVersion(req *http.Request) (string, error) {
 | 
			
		||||
	hdr := req.Header.Get("X-Ops-Sign")
 | 
			
		||||
	if hdr == "" {
 | 
			
		||||
		return "", util.NewInvalidArgumentErrorf("X-Ops-Sign header missing")
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	m := versionPattern.FindStringSubmatch(hdr)
 | 
			
		||||
	if len(m) != 2 {
 | 
			
		||||
		return "", util.NewInvalidArgumentErrorf("invalid X-Ops-Sign header")
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	switch m[1] {
 | 
			
		||||
	case "1.0", "1.1", "1.2", "1.3":
 | 
			
		||||
	default:
 | 
			
		||||
		return "", util.NewInvalidArgumentErrorf("unsupported version")
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	version := m[1]
 | 
			
		||||
 | 
			
		||||
	m = algorithmPattern.FindStringSubmatch(hdr)
 | 
			
		||||
	if len(m) == 2 && m[1] != "sha1" && !(m[1] == "sha256" && version == "1.3") {
 | 
			
		||||
		return "", util.NewInvalidArgumentErrorf("unsupported algorithm")
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	return version, nil
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
func verifySignedHeaders(req *http.Request, version string, pub *rsa.PublicKey) error {
 | 
			
		||||
	authorizationData, err := getAuthorizationData(req)
 | 
			
		||||
	if err != nil {
 | 
			
		||||
		return err
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	checkData := buildCheckData(req, version)
 | 
			
		||||
 | 
			
		||||
	switch version {
 | 
			
		||||
	case "1.3":
 | 
			
		||||
		return verifyDataNew(authorizationData, checkData, pub, crypto.SHA256)
 | 
			
		||||
	case "1.2":
 | 
			
		||||
		return verifyDataNew(authorizationData, checkData, pub, crypto.SHA1)
 | 
			
		||||
	default:
 | 
			
		||||
		return verifyDataOld(authorizationData, checkData, pub)
 | 
			
		||||
	}
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
func getAuthorizationData(req *http.Request) ([]byte, error) {
 | 
			
		||||
	valueList := make(map[int]string)
 | 
			
		||||
	for k, vs := range req.Header {
 | 
			
		||||
		if m := authorizationPattern.FindStringSubmatch(k); m != nil {
 | 
			
		||||
			index, _ := strconv.Atoi(m[1])
 | 
			
		||||
			var v string
 | 
			
		||||
			if len(vs) == 0 {
 | 
			
		||||
				v = ""
 | 
			
		||||
			} else {
 | 
			
		||||
				v = vs[0]
 | 
			
		||||
			}
 | 
			
		||||
			valueList[index] = v
 | 
			
		||||
		}
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	tmp := make([]string, len(valueList))
 | 
			
		||||
	for k, v := range valueList {
 | 
			
		||||
		if k > len(tmp) {
 | 
			
		||||
			return nil, fmt.Errorf("invalid X-Ops-Authorization headers")
 | 
			
		||||
		}
 | 
			
		||||
		tmp[k-1] = v
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	return base64.StdEncoding.DecodeString(strings.Join(tmp, ""))
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
func buildCheckData(req *http.Request, version string) []byte {
 | 
			
		||||
	username := req.Header.Get("X-Ops-Userid")
 | 
			
		||||
	if version != "1.0" && version != "1.3" {
 | 
			
		||||
		sum := sha1.Sum([]byte(username))
 | 
			
		||||
		username = base64.StdEncoding.EncodeToString(sum[:])
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	var data string
 | 
			
		||||
	if version == "1.3" {
 | 
			
		||||
		data = fmt.Sprintf(
 | 
			
		||||
			"Method:%s\nPath:%s\nX-Ops-Content-Hash:%s\nX-Ops-Sign:version=%s\nX-Ops-Timestamp:%s\nX-Ops-UserId:%s\nX-Ops-Server-API-Version:%s",
 | 
			
		||||
			req.Method,
 | 
			
		||||
			path.Clean(req.URL.Path),
 | 
			
		||||
			req.Header.Get("X-Ops-Content-Hash"),
 | 
			
		||||
			version,
 | 
			
		||||
			req.Header.Get("X-Ops-Timestamp"),
 | 
			
		||||
			username,
 | 
			
		||||
			req.Header.Get("X-Ops-Server-Api-Version"),
 | 
			
		||||
		)
 | 
			
		||||
	} else {
 | 
			
		||||
		sum := sha1.Sum([]byte(path.Clean(req.URL.Path)))
 | 
			
		||||
		data = fmt.Sprintf(
 | 
			
		||||
			"Method:%s\nHashed Path:%s\nX-Ops-Content-Hash:%s\nX-Ops-Timestamp:%s\nX-Ops-UserId:%s",
 | 
			
		||||
			req.Method,
 | 
			
		||||
			base64.StdEncoding.EncodeToString(sum[:]),
 | 
			
		||||
			req.Header.Get("X-Ops-Content-Hash"),
 | 
			
		||||
			req.Header.Get("X-Ops-Timestamp"),
 | 
			
		||||
			username,
 | 
			
		||||
		)
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	return []byte(data)
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
func verifyDataNew(signature, data []byte, pub *rsa.PublicKey, algo crypto.Hash) error {
 | 
			
		||||
	var h hash.Hash
 | 
			
		||||
	if algo == crypto.SHA256 {
 | 
			
		||||
		h = sha256.New()
 | 
			
		||||
	} else {
 | 
			
		||||
		h = sha1.New()
 | 
			
		||||
	}
 | 
			
		||||
	if _, err := h.Write(data); err != nil {
 | 
			
		||||
		return err
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	return rsa.VerifyPKCS1v15(pub, algo, h.Sum(nil), signature)
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
func verifyDataOld(signature, data []byte, pub *rsa.PublicKey) error {
 | 
			
		||||
	c := new(big.Int)
 | 
			
		||||
	m := new(big.Int)
 | 
			
		||||
	m.SetBytes(signature)
 | 
			
		||||
	e := big.NewInt(int64(pub.E))
 | 
			
		||||
	c.Exp(m, e, pub.N)
 | 
			
		||||
 | 
			
		||||
	out := c.Bytes()
 | 
			
		||||
 | 
			
		||||
	skip := 0
 | 
			
		||||
	for i := 2; i < len(out); i++ {
 | 
			
		||||
		if i+1 >= len(out) {
 | 
			
		||||
			break
 | 
			
		||||
		}
 | 
			
		||||
		if out[i] == 0xFF && out[i+1] == 0 {
 | 
			
		||||
			skip = i + 2
 | 
			
		||||
			break
 | 
			
		||||
		}
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	if !util.SliceEqual(out[skip:], data) {
 | 
			
		||||
		return fmt.Errorf("could not verify signature")
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	return nil
 | 
			
		||||
}
 | 
			
		||||
							
								
								
									
										404
									
								
								routers/api/packages/chef/chef.go
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										404
									
								
								routers/api/packages/chef/chef.go
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,404 @@
 | 
			
		||||
// Copyright 2023 The Gitea Authors. All rights reserved.
 | 
			
		||||
// SPDX-License-Identifier: MIT
 | 
			
		||||
 | 
			
		||||
package chef
 | 
			
		||||
 | 
			
		||||
import (
 | 
			
		||||
	"errors"
 | 
			
		||||
	"fmt"
 | 
			
		||||
	"io"
 | 
			
		||||
	"net/http"
 | 
			
		||||
	"net/url"
 | 
			
		||||
	"sort"
 | 
			
		||||
	"strings"
 | 
			
		||||
	"time"
 | 
			
		||||
 | 
			
		||||
	"code.gitea.io/gitea/models/db"
 | 
			
		||||
	packages_model "code.gitea.io/gitea/models/packages"
 | 
			
		||||
	"code.gitea.io/gitea/modules/context"
 | 
			
		||||
	packages_module "code.gitea.io/gitea/modules/packages"
 | 
			
		||||
	chef_module "code.gitea.io/gitea/modules/packages/chef"
 | 
			
		||||
	"code.gitea.io/gitea/modules/setting"
 | 
			
		||||
	"code.gitea.io/gitea/modules/util"
 | 
			
		||||
	"code.gitea.io/gitea/routers/api/packages/helper"
 | 
			
		||||
	packages_service "code.gitea.io/gitea/services/packages"
 | 
			
		||||
)
 | 
			
		||||
 | 
			
		||||
func apiError(ctx *context.Context, status int, obj interface{}) {
 | 
			
		||||
	type Error struct {
 | 
			
		||||
		ErrorMessages []string `json:"error_messages"`
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	helper.LogAndProcessError(ctx, status, obj, func(message string) {
 | 
			
		||||
		ctx.JSON(status, Error{
 | 
			
		||||
			ErrorMessages: []string{message},
 | 
			
		||||
		})
 | 
			
		||||
	})
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
func PackagesUniverse(ctx *context.Context) {
 | 
			
		||||
	pvs, _, err := packages_model.SearchVersions(ctx, &packages_model.PackageSearchOptions{
 | 
			
		||||
		OwnerID:    ctx.Package.Owner.ID,
 | 
			
		||||
		Type:       packages_model.TypeChef,
 | 
			
		||||
		IsInternal: util.OptionalBoolFalse,
 | 
			
		||||
	})
 | 
			
		||||
	if err != nil {
 | 
			
		||||
		apiError(ctx, http.StatusInternalServerError, err)
 | 
			
		||||
		return
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	pds, err := packages_model.GetPackageDescriptors(ctx, pvs)
 | 
			
		||||
	if err != nil {
 | 
			
		||||
		apiError(ctx, http.StatusInternalServerError, err)
 | 
			
		||||
		return
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	type VersionInfo struct {
 | 
			
		||||
		LocationType string            `json:"location_type"`
 | 
			
		||||
		LocationPath string            `json:"location_path"`
 | 
			
		||||
		DownloadURL  string            `json:"download_url"`
 | 
			
		||||
		Dependencies map[string]string `json:"dependencies"`
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	baseURL := setting.AppURL + "api/packages/" + ctx.Package.Owner.Name + "/chef/api/v1"
 | 
			
		||||
 | 
			
		||||
	universe := make(map[string]map[string]*VersionInfo)
 | 
			
		||||
	for _, pd := range pds {
 | 
			
		||||
		if _, ok := universe[pd.Package.Name]; !ok {
 | 
			
		||||
			universe[pd.Package.Name] = make(map[string]*VersionInfo)
 | 
			
		||||
		}
 | 
			
		||||
		universe[pd.Package.Name][pd.Version.Version] = &VersionInfo{
 | 
			
		||||
			LocationType: "opscode",
 | 
			
		||||
			LocationPath: baseURL,
 | 
			
		||||
			DownloadURL:  fmt.Sprintf("%s/cookbooks/%s/versions/%s/download", baseURL, url.PathEscape(pd.Package.Name), pd.Version.Version),
 | 
			
		||||
			Dependencies: pd.Metadata.(*chef_module.Metadata).Dependencies,
 | 
			
		||||
		}
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	ctx.JSON(http.StatusOK, universe)
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
// https://github.com/chef/chef/blob/main/knife/lib/chef/knife/supermarket_list.rb
 | 
			
		||||
// https://github.com/chef/chef/blob/main/knife/lib/chef/knife/supermarket_search.rb
 | 
			
		||||
func EnumeratePackages(ctx *context.Context) {
 | 
			
		||||
	opts := &packages_model.PackageSearchOptions{
 | 
			
		||||
		OwnerID:    ctx.Package.Owner.ID,
 | 
			
		||||
		Type:       packages_model.TypeChef,
 | 
			
		||||
		Name:       packages_model.SearchValue{Value: ctx.FormTrim("q")},
 | 
			
		||||
		IsInternal: util.OptionalBoolFalse,
 | 
			
		||||
		Paginator: db.NewAbsoluteListOptions(
 | 
			
		||||
			ctx.FormInt("start"),
 | 
			
		||||
			ctx.FormInt("items"),
 | 
			
		||||
		),
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	switch strings.ToLower(ctx.FormTrim("order")) {
 | 
			
		||||
	case "recently_updated", "recently_added":
 | 
			
		||||
		opts.Sort = packages_model.SortCreatedDesc
 | 
			
		||||
	default:
 | 
			
		||||
		opts.Sort = packages_model.SortNameAsc
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	pvs, total, err := packages_model.SearchLatestVersions(ctx, opts)
 | 
			
		||||
	if err != nil {
 | 
			
		||||
		apiError(ctx, http.StatusInternalServerError, err)
 | 
			
		||||
		return
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	pds, err := packages_model.GetPackageDescriptors(ctx, pvs)
 | 
			
		||||
	if err != nil {
 | 
			
		||||
		apiError(ctx, http.StatusInternalServerError, err)
 | 
			
		||||
		return
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	type Item struct {
 | 
			
		||||
		CookbookName        string `json:"cookbook_name"`
 | 
			
		||||
		CookbookMaintainer  string `json:"cookbook_maintainer"`
 | 
			
		||||
		CookbookDescription string `json:"cookbook_description"`
 | 
			
		||||
		Cookbook            string `json:"cookbook"`
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	type Result struct {
 | 
			
		||||
		Start int     `json:"start"`
 | 
			
		||||
		Total int     `json:"total"`
 | 
			
		||||
		Items []*Item `json:"items"`
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	baseURL := setting.AppURL + "api/packages/" + ctx.Package.Owner.Name + "/chef/api/v1/cookbooks/"
 | 
			
		||||
 | 
			
		||||
	items := make([]*Item, 0, len(pds))
 | 
			
		||||
	for _, pd := range pds {
 | 
			
		||||
		metadata := pd.Metadata.(*chef_module.Metadata)
 | 
			
		||||
 | 
			
		||||
		items = append(items, &Item{
 | 
			
		||||
			CookbookName:        pd.Package.Name,
 | 
			
		||||
			CookbookMaintainer:  metadata.Author,
 | 
			
		||||
			CookbookDescription: metadata.Description,
 | 
			
		||||
			Cookbook:            baseURL + url.PathEscape(pd.Package.Name),
 | 
			
		||||
		})
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	skip, _ := opts.Paginator.GetSkipTake()
 | 
			
		||||
 | 
			
		||||
	ctx.JSON(http.StatusOK, &Result{
 | 
			
		||||
		Start: skip,
 | 
			
		||||
		Total: int(total),
 | 
			
		||||
		Items: items,
 | 
			
		||||
	})
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
// https://github.com/chef/chef/blob/main/knife/lib/chef/knife/supermarket_show.rb
 | 
			
		||||
func PackageMetadata(ctx *context.Context) {
 | 
			
		||||
	packageName := ctx.Params("name")
 | 
			
		||||
 | 
			
		||||
	pvs, err := packages_model.GetVersionsByPackageName(ctx, ctx.Package.Owner.ID, packages_model.TypeChef, packageName)
 | 
			
		||||
	if err != nil {
 | 
			
		||||
		apiError(ctx, http.StatusInternalServerError, err)
 | 
			
		||||
		return
 | 
			
		||||
	}
 | 
			
		||||
	if len(pvs) == 0 {
 | 
			
		||||
		apiError(ctx, http.StatusNotFound, nil)
 | 
			
		||||
		return
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	pds, err := packages_model.GetPackageDescriptors(ctx, pvs)
 | 
			
		||||
	if err != nil {
 | 
			
		||||
		apiError(ctx, http.StatusInternalServerError, err)
 | 
			
		||||
		return
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	sort.Slice(pds, func(i, j int) bool {
 | 
			
		||||
		return pds[i].SemVer.LessThan(pds[j].SemVer)
 | 
			
		||||
	})
 | 
			
		||||
 | 
			
		||||
	type Result struct {
 | 
			
		||||
		Name          string    `json:"name"`
 | 
			
		||||
		Maintainer    string    `json:"maintainer"`
 | 
			
		||||
		Description   string    `json:"description"`
 | 
			
		||||
		Category      string    `json:"category"`
 | 
			
		||||
		LatestVersion string    `json:"latest_version"`
 | 
			
		||||
		SourceURL     string    `json:"source_url"`
 | 
			
		||||
		CreatedAt     time.Time `json:"created_at"`
 | 
			
		||||
		UpdatedAt     time.Time `json:"updated_at"`
 | 
			
		||||
		Deprecated    bool      `json:"deprecated"`
 | 
			
		||||
		Versions      []string  `json:"versions"`
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	baseURL := fmt.Sprintf("%sapi/packages/%s/chef/api/v1/cookbooks/%s/versions/", setting.AppURL, ctx.Package.Owner.Name, url.PathEscape(packageName))
 | 
			
		||||
 | 
			
		||||
	versions := make([]string, 0, len(pds))
 | 
			
		||||
	for _, pd := range pds {
 | 
			
		||||
		versions = append(versions, baseURL+pd.Version.Version)
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	latest := pds[len(pds)-1]
 | 
			
		||||
 | 
			
		||||
	metadata := latest.Metadata.(*chef_module.Metadata)
 | 
			
		||||
 | 
			
		||||
	ctx.JSON(http.StatusOK, &Result{
 | 
			
		||||
		Name:          latest.Package.Name,
 | 
			
		||||
		Maintainer:    metadata.Author,
 | 
			
		||||
		Description:   metadata.Description,
 | 
			
		||||
		LatestVersion: baseURL + latest.Version.Version,
 | 
			
		||||
		SourceURL:     metadata.RepositoryURL,
 | 
			
		||||
		CreatedAt:     latest.Version.CreatedUnix.AsLocalTime(),
 | 
			
		||||
		UpdatedAt:     latest.Version.CreatedUnix.AsLocalTime(),
 | 
			
		||||
		Deprecated:    false,
 | 
			
		||||
		Versions:      versions,
 | 
			
		||||
	})
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
// https://github.com/chef/chef/blob/main/knife/lib/chef/knife/supermarket_show.rb
 | 
			
		||||
func PackageVersionMetadata(ctx *context.Context) {
 | 
			
		||||
	packageName := ctx.Params("name")
 | 
			
		||||
	packageVersion := strings.ReplaceAll(ctx.Params("version"), "_", ".") // Chef calls this endpoint with "_" instead of "."?!
 | 
			
		||||
 | 
			
		||||
	pv, err := packages_model.GetVersionByNameAndVersion(ctx, ctx.Package.Owner.ID, packages_model.TypeChef, packageName, packageVersion)
 | 
			
		||||
	if err != nil {
 | 
			
		||||
		if err == packages_model.ErrPackageNotExist {
 | 
			
		||||
			apiError(ctx, http.StatusNotFound, err)
 | 
			
		||||
			return
 | 
			
		||||
		}
 | 
			
		||||
		apiError(ctx, http.StatusInternalServerError, err)
 | 
			
		||||
		return
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	pd, err := packages_model.GetPackageDescriptor(ctx, pv)
 | 
			
		||||
	if err != nil {
 | 
			
		||||
		apiError(ctx, http.StatusInternalServerError, err)
 | 
			
		||||
		return
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	type Result struct {
 | 
			
		||||
		Version         string            `json:"version"`
 | 
			
		||||
		TarballFileSize int64             `json:"tarball_file_size"`
 | 
			
		||||
		PublishedAt     time.Time         `json:"published_at"`
 | 
			
		||||
		Cookbook        string            `json:"cookbook"`
 | 
			
		||||
		File            string            `json:"file"`
 | 
			
		||||
		License         string            `json:"license"`
 | 
			
		||||
		Dependencies    map[string]string `json:"dependencies"`
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	baseURL := fmt.Sprintf("%sapi/packages/%s/chef/api/v1/cookbooks/%s", setting.AppURL, ctx.Package.Owner.Name, url.PathEscape(pd.Package.Name))
 | 
			
		||||
 | 
			
		||||
	metadata := pd.Metadata.(*chef_module.Metadata)
 | 
			
		||||
 | 
			
		||||
	ctx.JSON(http.StatusOK, &Result{
 | 
			
		||||
		Version:         pd.Version.Version,
 | 
			
		||||
		TarballFileSize: pd.Files[0].Blob.Size,
 | 
			
		||||
		PublishedAt:     pd.Version.CreatedUnix.AsLocalTime(),
 | 
			
		||||
		Cookbook:        baseURL,
 | 
			
		||||
		File:            fmt.Sprintf("%s/versions/%s/download", baseURL, pd.Version.Version),
 | 
			
		||||
		License:         metadata.License,
 | 
			
		||||
		Dependencies:    metadata.Dependencies,
 | 
			
		||||
	})
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
// https://github.com/chef/chef/blob/main/knife/lib/chef/knife/supermarket_share.rb
 | 
			
		||||
func UploadPackage(ctx *context.Context) {
 | 
			
		||||
	file, _, err := ctx.Req.FormFile("tarball")
 | 
			
		||||
	if err != nil {
 | 
			
		||||
		apiError(ctx, http.StatusBadRequest, err)
 | 
			
		||||
		return
 | 
			
		||||
	}
 | 
			
		||||
	defer file.Close()
 | 
			
		||||
 | 
			
		||||
	buf, err := packages_module.CreateHashedBufferFromReader(file, 32*1024*1024)
 | 
			
		||||
	if err != nil {
 | 
			
		||||
		apiError(ctx, http.StatusInternalServerError, err)
 | 
			
		||||
		return
 | 
			
		||||
	}
 | 
			
		||||
	defer buf.Close()
 | 
			
		||||
 | 
			
		||||
	pck, err := chef_module.ParsePackage(buf)
 | 
			
		||||
	if err != nil {
 | 
			
		||||
		if errors.Is(err, util.ErrInvalidArgument) {
 | 
			
		||||
			apiError(ctx, http.StatusBadRequest, err)
 | 
			
		||||
		} else {
 | 
			
		||||
			apiError(ctx, http.StatusInternalServerError, err)
 | 
			
		||||
		}
 | 
			
		||||
		return
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	if _, err := buf.Seek(0, io.SeekStart); err != nil {
 | 
			
		||||
		apiError(ctx, http.StatusInternalServerError, err)
 | 
			
		||||
		return
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	_, _, err = packages_service.CreatePackageAndAddFile(
 | 
			
		||||
		&packages_service.PackageCreationInfo{
 | 
			
		||||
			PackageInfo: packages_service.PackageInfo{
 | 
			
		||||
				Owner:       ctx.Package.Owner,
 | 
			
		||||
				PackageType: packages_model.TypeChef,
 | 
			
		||||
				Name:        pck.Name,
 | 
			
		||||
				Version:     pck.Version,
 | 
			
		||||
			},
 | 
			
		||||
			Creator:          ctx.Doer,
 | 
			
		||||
			SemverCompatible: true,
 | 
			
		||||
			Metadata:         pck.Metadata,
 | 
			
		||||
		},
 | 
			
		||||
		&packages_service.PackageFileCreationInfo{
 | 
			
		||||
			PackageFileInfo: packages_service.PackageFileInfo{
 | 
			
		||||
				Filename: strings.ToLower(pck.Version + ".tar.gz"),
 | 
			
		||||
			},
 | 
			
		||||
			Creator: ctx.Doer,
 | 
			
		||||
			Data:    buf,
 | 
			
		||||
			IsLead:  true,
 | 
			
		||||
		},
 | 
			
		||||
	)
 | 
			
		||||
	if err != nil {
 | 
			
		||||
		switch err {
 | 
			
		||||
		case packages_model.ErrDuplicatePackageVersion:
 | 
			
		||||
			apiError(ctx, http.StatusBadRequest, err)
 | 
			
		||||
		case packages_service.ErrQuotaTotalCount, packages_service.ErrQuotaTypeSize, packages_service.ErrQuotaTotalSize:
 | 
			
		||||
			apiError(ctx, http.StatusForbidden, err)
 | 
			
		||||
		default:
 | 
			
		||||
			apiError(ctx, http.StatusInternalServerError, err)
 | 
			
		||||
		}
 | 
			
		||||
		return
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	ctx.JSON(http.StatusCreated, make(map[any]any))
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
// https://github.com/chef/chef/blob/main/knife/lib/chef/knife/supermarket_download.rb
 | 
			
		||||
func DownloadPackage(ctx *context.Context) {
 | 
			
		||||
	pv, err := packages_model.GetVersionByNameAndVersion(ctx, ctx.Package.Owner.ID, packages_model.TypeChef, ctx.Params("name"), ctx.Params("version"))
 | 
			
		||||
	if err != nil {
 | 
			
		||||
		if err == packages_model.ErrPackageNotExist {
 | 
			
		||||
			apiError(ctx, http.StatusNotFound, err)
 | 
			
		||||
			return
 | 
			
		||||
		}
 | 
			
		||||
		apiError(ctx, http.StatusInternalServerError, err)
 | 
			
		||||
		return
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	pd, err := packages_model.GetPackageDescriptor(ctx, pv)
 | 
			
		||||
	if err != nil {
 | 
			
		||||
		apiError(ctx, http.StatusInternalServerError, err)
 | 
			
		||||
		return
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	pf := pd.Files[0].File
 | 
			
		||||
 | 
			
		||||
	s, _, err := packages_service.GetPackageFileStream(ctx, pf)
 | 
			
		||||
	if err != nil {
 | 
			
		||||
		apiError(ctx, http.StatusInternalServerError, err)
 | 
			
		||||
		return
 | 
			
		||||
	}
 | 
			
		||||
	defer s.Close()
 | 
			
		||||
 | 
			
		||||
	ctx.ServeContent(s, &context.ServeHeaderOptions{
 | 
			
		||||
		Filename:     pf.Name,
 | 
			
		||||
		LastModified: pf.CreatedUnix.AsLocalTime(),
 | 
			
		||||
	})
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
// https://github.com/chef/chef/blob/main/knife/lib/chef/knife/supermarket_unshare.rb
 | 
			
		||||
func DeletePackageVersion(ctx *context.Context) {
 | 
			
		||||
	packageName := ctx.Params("name")
 | 
			
		||||
	packageVersion := ctx.Params("version")
 | 
			
		||||
 | 
			
		||||
	err := packages_service.RemovePackageVersionByNameAndVersion(
 | 
			
		||||
		ctx.Doer,
 | 
			
		||||
		&packages_service.PackageInfo{
 | 
			
		||||
			Owner:       ctx.Package.Owner,
 | 
			
		||||
			PackageType: packages_model.TypeChef,
 | 
			
		||||
			Name:        packageName,
 | 
			
		||||
			Version:     packageVersion,
 | 
			
		||||
		},
 | 
			
		||||
	)
 | 
			
		||||
	if err != nil {
 | 
			
		||||
		if err == packages_model.ErrPackageNotExist {
 | 
			
		||||
			apiError(ctx, http.StatusNotFound, err)
 | 
			
		||||
		} else {
 | 
			
		||||
			apiError(ctx, http.StatusInternalServerError, err)
 | 
			
		||||
		}
 | 
			
		||||
		return
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	ctx.Status(http.StatusOK)
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
// https://github.com/chef/chef/blob/main/knife/lib/chef/knife/supermarket_unshare.rb
 | 
			
		||||
func DeletePackage(ctx *context.Context) {
 | 
			
		||||
	pvs, err := packages_model.GetVersionsByPackageName(ctx, ctx.Package.Owner.ID, packages_model.TypeChef, ctx.Params("name"))
 | 
			
		||||
	if err != nil {
 | 
			
		||||
		apiError(ctx, http.StatusInternalServerError, err)
 | 
			
		||||
		return
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	if len(pvs) == 0 {
 | 
			
		||||
		apiError(ctx, http.StatusNotFound, err)
 | 
			
		||||
		return
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	for _, pv := range pvs {
 | 
			
		||||
		if err := packages_service.RemovePackageVersion(ctx.Doer, pv); err != nil {
 | 
			
		||||
			apiError(ctx, http.StatusInternalServerError, err)
 | 
			
		||||
			return
 | 
			
		||||
		}
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	ctx.Status(http.StatusOK)
 | 
			
		||||
}
 | 
			
		||||
@@ -40,7 +40,7 @@ func ListPackages(ctx *context.APIContext) {
 | 
			
		||||
	//   in: query
 | 
			
		||||
	//   description: package type filter
 | 
			
		||||
	//   type: string
 | 
			
		||||
	//   enum: [cargo, composer, conan, conda, container, generic, helm, maven, npm, nuget, pub, pypi, rubygems, vagrant]
 | 
			
		||||
	//   enum: [cargo, chef, composer, conan, conda, container, generic, helm, maven, npm, nuget, pub, pypi, rubygems, vagrant]
 | 
			
		||||
	// - name: q
 | 
			
		||||
	//   in: query
 | 
			
		||||
	//   description: name filter
 | 
			
		||||
 
 | 
			
		||||
@@ -5,10 +5,14 @@ package setting
 | 
			
		||||
 | 
			
		||||
import (
 | 
			
		||||
	"net/http"
 | 
			
		||||
	"strings"
 | 
			
		||||
 | 
			
		||||
	user_model "code.gitea.io/gitea/models/user"
 | 
			
		||||
	"code.gitea.io/gitea/modules/base"
 | 
			
		||||
	"code.gitea.io/gitea/modules/context"
 | 
			
		||||
	chef_module "code.gitea.io/gitea/modules/packages/chef"
 | 
			
		||||
	"code.gitea.io/gitea/modules/setting"
 | 
			
		||||
	"code.gitea.io/gitea/modules/util"
 | 
			
		||||
	shared "code.gitea.io/gitea/routers/web/shared/packages"
 | 
			
		||||
)
 | 
			
		||||
 | 
			
		||||
@@ -95,3 +99,21 @@ func RebuildCargoIndex(ctx *context.Context) {
 | 
			
		||||
 | 
			
		||||
	ctx.Redirect(setting.AppSubURL + "/user/settings/packages")
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
func RegenerateChefKeyPair(ctx *context.Context) {
 | 
			
		||||
	priv, pub, err := util.GenerateKeyPair(chef_module.KeyBits)
 | 
			
		||||
	if err != nil {
 | 
			
		||||
		ctx.ServerError("GenerateKeyPair", err)
 | 
			
		||||
		return
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	if err := user_model.SetUserSetting(ctx.Doer.ID, chef_module.SettingPublicPem, pub); err != nil {
 | 
			
		||||
		ctx.ServerError("SetUserSetting", err)
 | 
			
		||||
		return
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	ctx.ServeContent(strings.NewReader(priv), &context.ServeHeaderOptions{
 | 
			
		||||
		ContentType: "application/x-pem-file",
 | 
			
		||||
		Filename:    ctx.Doer.Name + ".priv",
 | 
			
		||||
	})
 | 
			
		||||
}
 | 
			
		||||
 
 | 
			
		||||
@@ -472,6 +472,7 @@ func RegisterRoutes(m *web.Route) {
 | 
			
		||||
				m.Post("/initialize", user_setting.InitializeCargoIndex)
 | 
			
		||||
				m.Post("/rebuild", user_setting.RebuildCargoIndex)
 | 
			
		||||
			})
 | 
			
		||||
			m.Post("/chef/regenerate_keypair", user_setting.RegenerateChefKeyPair)
 | 
			
		||||
		}, packagesEnabled)
 | 
			
		||||
		m.Group("/secrets", func() {
 | 
			
		||||
			m.Get("", user_setting.Secrets)
 | 
			
		||||
 
 | 
			
		||||
		Reference in New Issue
	
	Block a user