code.gitea.io/gitea@v1.22.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-z0-9-][a-z0-9-._]*/)?[a-z0-9-][a-z0-9-._]*$`) 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 Repository: meta.Repository, 227 }, 228 } 229 230 for tag := range upload.DistTags { 231 p.DistTags = append(p.DistTags, tag) 232 } 233 234 p.Filename = strings.ToLower(fmt.Sprintf("%s-%s.tgz", name, p.Version)) 235 236 attachment := func() *PackageAttachment { 237 for _, a := range upload.Attachments { 238 return a 239 } 240 return nil 241 }() 242 if attachment == nil || len(attachment.Data) == 0 { 243 return nil, ErrInvalidAttachment 244 } 245 246 data, err := base64.StdEncoding.DecodeString(attachment.Data) 247 if err != nil { 248 return nil, ErrInvalidAttachment 249 } 250 p.Data = data 251 252 integrity := strings.SplitN(meta.Dist.Integrity, "-", 2) 253 if len(integrity) != 2 { 254 return nil, ErrInvalidIntegrity 255 } 256 integrityHash, err := base64.StdEncoding.DecodeString(integrity[1]) 257 if err != nil { 258 return nil, ErrInvalidIntegrity 259 } 260 var hash []byte 261 switch integrity[0] { 262 case "sha1": 263 tmp := sha1.Sum(data) 264 hash = tmp[:] 265 case "sha512": 266 tmp := sha512.Sum512(data) 267 hash = tmp[:] 268 } 269 if !bytes.Equal(integrityHash, hash) { 270 return nil, ErrInvalidIntegrity 271 } 272 273 return p, nil 274 } 275 276 return nil, ErrInvalidPackage 277 } 278 279 func validateName(name string) bool { 280 if strings.TrimSpace(name) != name { 281 return false 282 } 283 if len(name) == 0 || len(name) > 214 { 284 return false 285 } 286 return nameMatch.MatchString(name) 287 }