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 }