code.gitea.io/gitea@v1.19.3/modules/packages/npm/creator.go (about)

     1  // Copyright 2021 The Gitea Authors. All rights reserved.
     2  // SPDX-License-Identifier: MIT
     3  
     4  package npm
     5  
     6  import (
     7  	"bytes"
     8  	"crypto/sha1"
     9  	"crypto/sha512"
    10  	"encoding/base64"
    11  	"fmt"
    12  	"io"
    13  	"regexp"
    14  	"strings"
    15  	"time"
    16  
    17  	"code.gitea.io/gitea/modules/json"
    18  	"code.gitea.io/gitea/modules/util"
    19  	"code.gitea.io/gitea/modules/validation"
    20  
    21  	"github.com/hashicorp/go-version"
    22  )
    23  
    24  var (
    25  	// ErrInvalidPackage indicates an invalid package
    26  	ErrInvalidPackage = util.NewInvalidArgumentErrorf("package is invalid")
    27  	// ErrInvalidPackageName indicates an invalid name
    28  	ErrInvalidPackageName = util.NewInvalidArgumentErrorf("package name is invalid")
    29  	// ErrInvalidPackageVersion indicates an invalid version
    30  	ErrInvalidPackageVersion = util.NewInvalidArgumentErrorf("package version is invalid")
    31  	// ErrInvalidAttachment indicates a invalid attachment
    32  	ErrInvalidAttachment = util.NewInvalidArgumentErrorf("package attachment is invalid")
    33  	// ErrInvalidIntegrity indicates an integrity validation error
    34  	ErrInvalidIntegrity = util.NewInvalidArgumentErrorf("failed to validate integrity")
    35  )
    36  
    37  var nameMatch = regexp.MustCompile(`\A((@[^\s\/~'!\(\)\*]+?)[\/])?([^_.][^\s\/~'!\(\)\*]+)\z`)
    38  
    39  // Package represents a npm package
    40  type Package struct {
    41  	Name     string
    42  	Version  string
    43  	DistTags []string
    44  	Metadata Metadata
    45  	Filename string
    46  	Data     []byte
    47  }
    48  
    49  // PackageMetadata https://github.com/npm/registry/blob/master/docs/REGISTRY-API.md#package
    50  type PackageMetadata struct {
    51  	ID             string                             `json:"_id"`
    52  	Name           string                             `json:"name"`
    53  	Description    string                             `json:"description"`
    54  	DistTags       map[string]string                  `json:"dist-tags,omitempty"`
    55  	Versions       map[string]*PackageMetadataVersion `json:"versions"`
    56  	Readme         string                             `json:"readme,omitempty"`
    57  	Maintainers    []User                             `json:"maintainers,omitempty"`
    58  	Time           map[string]time.Time               `json:"time,omitempty"`
    59  	Homepage       string                             `json:"homepage,omitempty"`
    60  	Keywords       []string                           `json:"keywords,omitempty"`
    61  	Repository     Repository                         `json:"repository,omitempty"`
    62  	Author         User                               `json:"author"`
    63  	ReadmeFilename string                             `json:"readmeFilename,omitempty"`
    64  	Users          map[string]bool                    `json:"users,omitempty"`
    65  	License        string                             `json:"license,omitempty"`
    66  }
    67  
    68  // PackageMetadataVersion documentation: https://github.com/npm/registry/blob/master/docs/REGISTRY-API.md#version
    69  // PackageMetadataVersion response: https://github.com/npm/registry/blob/master/docs/responses/package-metadata.md#abbreviated-version-object
    70  type PackageMetadataVersion struct {
    71  	ID                   string              `json:"_id"`
    72  	Name                 string              `json:"name"`
    73  	Version              string              `json:"version"`
    74  	Description          string              `json:"description"`
    75  	Author               User                `json:"author"`
    76  	Homepage             string              `json:"homepage,omitempty"`
    77  	License              string              `json:"license,omitempty"`
    78  	Repository           Repository          `json:"repository,omitempty"`
    79  	Keywords             []string            `json:"keywords,omitempty"`
    80  	Dependencies         map[string]string   `json:"dependencies,omitempty"`
    81  	DevDependencies      map[string]string   `json:"devDependencies,omitempty"`
    82  	PeerDependencies     map[string]string   `json:"peerDependencies,omitempty"`
    83  	Bin                  map[string]string   `json:"bin,omitempty"`
    84  	OptionalDependencies map[string]string   `json:"optionalDependencies,omitempty"`
    85  	Readme               string              `json:"readme,omitempty"`
    86  	Dist                 PackageDistribution `json:"dist"`
    87  	Maintainers          []User              `json:"maintainers,omitempty"`
    88  }
    89  
    90  // PackageDistribution https://github.com/npm/registry/blob/master/docs/REGISTRY-API.md#version
    91  type PackageDistribution struct {
    92  	Integrity    string `json:"integrity"`
    93  	Shasum       string `json:"shasum"`
    94  	Tarball      string `json:"tarball"`
    95  	FileCount    int    `json:"fileCount,omitempty"`
    96  	UnpackedSize int    `json:"unpackedSize,omitempty"`
    97  	NpmSignature string `json:"npm-signature,omitempty"`
    98  }
    99  
   100  type PackageSearch struct {
   101  	Objects []*PackageSearchObject `json:"objects"`
   102  	Total   int64                  `json:"total"`
   103  }
   104  
   105  type PackageSearchObject struct {
   106  	Package *PackageSearchPackage `json:"package"`
   107  }
   108  
   109  type PackageSearchPackage struct {
   110  	Scope       string                     `json:"scope"`
   111  	Name        string                     `json:"name"`
   112  	Version     string                     `json:"version"`
   113  	Date        time.Time                  `json:"date"`
   114  	Description string                     `json:"description"`
   115  	Author      User                       `json:"author"`
   116  	Publisher   User                       `json:"publisher"`
   117  	Maintainers []User                     `json:"maintainers"`
   118  	Keywords    []string                   `json:"keywords,omitempty"`
   119  	Links       *PackageSearchPackageLinks `json:"links"`
   120  }
   121  
   122  type PackageSearchPackageLinks struct {
   123  	Registry   string `json:"npm"`
   124  	Homepage   string `json:"homepage,omitempty"`
   125  	Repository string `json:"repository,omitempty"`
   126  }
   127  
   128  // User https://github.com/npm/registry/blob/master/docs/REGISTRY-API.md#package
   129  type User struct {
   130  	Username string `json:"username,omitempty"`
   131  	Name     string `json:"name"`
   132  	Email    string `json:"email,omitempty"`
   133  	URL      string `json:"url,omitempty"`
   134  }
   135  
   136  // UnmarshalJSON is needed because User objects can be strings or objects
   137  func (u *User) UnmarshalJSON(data []byte) error {
   138  	switch data[0] {
   139  	case '"':
   140  		if err := json.Unmarshal(data, &u.Name); err != nil {
   141  			return err
   142  		}
   143  	case '{':
   144  		var tmp struct {
   145  			Username string `json:"username"`
   146  			Name     string `json:"name"`
   147  			Email    string `json:"email"`
   148  			URL      string `json:"url"`
   149  		}
   150  		if err := json.Unmarshal(data, &tmp); err != nil {
   151  			return err
   152  		}
   153  		u.Username = tmp.Username
   154  		u.Name = tmp.Name
   155  		u.Email = tmp.Email
   156  		u.URL = tmp.URL
   157  	}
   158  	return nil
   159  }
   160  
   161  // Repository https://github.com/npm/registry/blob/master/docs/REGISTRY-API.md#version
   162  type Repository struct {
   163  	Type string `json:"type"`
   164  	URL  string `json:"url"`
   165  }
   166  
   167  // PackageAttachment https://github.com/npm/registry/blob/master/docs/REGISTRY-API.md#package
   168  type PackageAttachment struct {
   169  	ContentType string `json:"content_type"`
   170  	Data        string `json:"data"`
   171  	Length      int    `json:"length"`
   172  }
   173  
   174  type packageUpload struct {
   175  	PackageMetadata
   176  	Attachments map[string]*PackageAttachment `json:"_attachments"`
   177  }
   178  
   179  // ParsePackage parses the content into a npm package
   180  func ParsePackage(r io.Reader) (*Package, error) {
   181  	var upload packageUpload
   182  	if err := json.NewDecoder(r).Decode(&upload); err != nil {
   183  		return nil, err
   184  	}
   185  
   186  	for _, meta := range upload.Versions {
   187  		if !validateName(meta.Name) {
   188  			return nil, ErrInvalidPackageName
   189  		}
   190  
   191  		v, err := version.NewSemver(meta.Version)
   192  		if err != nil {
   193  			return nil, ErrInvalidPackageVersion
   194  		}
   195  
   196  		scope := ""
   197  		name := meta.Name
   198  		nameParts := strings.SplitN(meta.Name, "/", 2)
   199  		if len(nameParts) == 2 {
   200  			scope = nameParts[0]
   201  			name = nameParts[1]
   202  		}
   203  
   204  		if !validation.IsValidURL(meta.Homepage) {
   205  			meta.Homepage = ""
   206  		}
   207  
   208  		p := &Package{
   209  			Name:     meta.Name,
   210  			Version:  v.String(),
   211  			DistTags: make([]string, 0, 1),
   212  			Metadata: Metadata{
   213  				Scope:                   scope,
   214  				Name:                    name,
   215  				Description:             meta.Description,
   216  				Author:                  meta.Author.Name,
   217  				License:                 meta.License,
   218  				ProjectURL:              meta.Homepage,
   219  				Keywords:                meta.Keywords,
   220  				Dependencies:            meta.Dependencies,
   221  				DevelopmentDependencies: meta.DevDependencies,
   222  				PeerDependencies:        meta.PeerDependencies,
   223  				OptionalDependencies:    meta.OptionalDependencies,
   224  				Bin:                     meta.Bin,
   225  				Readme:                  meta.Readme,
   226  			},
   227  		}
   228  
   229  		for tag := range upload.DistTags {
   230  			p.DistTags = append(p.DistTags, tag)
   231  		}
   232  
   233  		p.Filename = strings.ToLower(fmt.Sprintf("%s-%s.tgz", name, p.Version))
   234  
   235  		attachment := func() *PackageAttachment {
   236  			for _, a := range upload.Attachments {
   237  				return a
   238  			}
   239  			return nil
   240  		}()
   241  		if attachment == nil || len(attachment.Data) == 0 {
   242  			return nil, ErrInvalidAttachment
   243  		}
   244  
   245  		data, err := base64.StdEncoding.DecodeString(attachment.Data)
   246  		if err != nil {
   247  			return nil, ErrInvalidAttachment
   248  		}
   249  		p.Data = data
   250  
   251  		integrity := strings.SplitN(meta.Dist.Integrity, "-", 2)
   252  		if len(integrity) != 2 {
   253  			return nil, ErrInvalidIntegrity
   254  		}
   255  		integrityHash, err := base64.StdEncoding.DecodeString(integrity[1])
   256  		if err != nil {
   257  			return nil, ErrInvalidIntegrity
   258  		}
   259  		var hash []byte
   260  		switch integrity[0] {
   261  		case "sha1":
   262  			tmp := sha1.Sum(data)
   263  			hash = tmp[:]
   264  		case "sha512":
   265  			tmp := sha512.Sum512(data)
   266  			hash = tmp[:]
   267  		}
   268  		if !bytes.Equal(integrityHash, hash) {
   269  			return nil, ErrInvalidIntegrity
   270  		}
   271  
   272  		return p, nil
   273  	}
   274  
   275  	return nil, ErrInvalidPackage
   276  }
   277  
   278  func validateName(name string) bool {
   279  	if strings.TrimSpace(name) != name {
   280  		return false
   281  	}
   282  	if len(name) == 0 || len(name) > 214 {
   283  		return false
   284  	}
   285  	return nameMatch.MatchString(name)
   286  }