@@ -12,9 +12,10 @@ import (
1212)
1313
1414const (
15- npmUpstream = "https://registry.npmjs.org"
16- npmAbbreviatedCT = "application/vnd.npm.install-v1+json"
17- scopedParts = 2 // scope + name in scoped packages
15+ npmUpstream = "https://registry.npmjs.org"
16+ npmAbbreviatedCT = "application/vnd.npm.install-v1+json"
17+ npmTarballShasumParam = "git-pkgs-shasum"
18+ scopedParts = 2 // scope + name in scoped packages
1819)
1920
2021// NPMHandler handles npm registry protocol requests.
@@ -199,13 +200,13 @@ func (h *NPMHandler) rewriteTarballURLs(versions map[string]any, packageName str
199200 continue
200201 }
201202
202- filename := tarball
203- if idx := strings .LastIndex (tarball , "/" ); idx >= 0 {
204- filename = tarball [idx + 1 :]
205- }
203+ filename , useShasum := h .proxyTarballFilename (packageName , version , tarball , dist )
206204
207205 escapedName := url .PathEscape (packageName )
208206 newTarball := fmt .Sprintf ("%s/npm/%s/-/%s" , h .proxyURL , escapedName , filename )
207+ if useShasum != "" {
208+ newTarball += "?" + url.Values {npmTarballShasumParam : []string {useShasum }}.Encode ()
209+ }
209210 dist ["tarball" ] = newTarball
210211
211212 h .proxy .Logger .Debug ("rewrote tarball URL" ,
@@ -214,6 +215,35 @@ func (h *NPMHandler) rewriteTarballURLs(versions map[string]any, packageName str
214215 }
215216}
216217
218+ func (h * NPMHandler ) proxyTarballFilename (packageName , version , tarball string , dist map [string ]any ) (string , string ) {
219+ filename := tarball
220+ if idx := strings .LastIndex (tarball , "/" ); idx >= 0 {
221+ filename = tarball [idx + 1 :]
222+ }
223+ if h .extractVersionFromFilename (packageName , filename ) != "" {
224+ return filename , ""
225+ }
226+
227+ shasum , _ := dist ["shasum" ].(string )
228+ if shasum == "" {
229+ return filename , ""
230+ }
231+
232+ return npmTarballFilename (packageName , version ), shasum
233+ }
234+
235+ func npmTarballFilename (packageName , version string ) string {
236+ return npmPackageShortName (packageName ) + "-" + version + ".tgz"
237+ }
238+
239+ func npmPackageShortName (packageName string ) string {
240+ parts := strings .SplitN (packageName , "/" , scopedParts )
241+ if len (parts ) == scopedParts {
242+ return parts [1 ]
243+ }
244+ return packageName
245+ }
246+
217247// findNewestVersion returns the version string with the most recent timestamp
218248// from the remaining versions, using the time map.
219249func (h * NPMHandler ) findNewestVersion (versions map [string ]any , timeMap map [string ]any ) string {
@@ -265,12 +295,12 @@ func (h *NPMHandler) handleDownload(w http.ResponseWriter, r *http.Request) {
265295 h .proxy .Logger .Info ("npm download request" ,
266296 "package" , packageName , "version" , version , "filename" , filename )
267297
268- downloadURL := fmt . Sprintf (
269- "%s/%s/-/%s" ,
270- h .upstreamURL ,
271- escapeNPMDownloadPackage ( packageName ),
272- url . PathEscape ( filename ),
273- )
298+ downloadURL , err := h . downloadURL ( r , packageName , version , filename )
299+ if err != nil {
300+ h .proxy . Logger . Error ( "failed to resolve npm tarball URL" , "error" , err )
301+ JSONError ( w , http . StatusBadRequest , "invalid tarball request" )
302+ return
303+ }
274304 result , err := h .proxy .GetOrFetchArtifactFromURL (
275305 r .Context (), "npm" , packageName , version , filename , downloadURL ,
276306 )
@@ -287,6 +317,72 @@ func (h *NPMHandler) handleDownload(w http.ResponseWriter, r *http.Request) {
287317 ServeArtifact (w , result )
288318}
289319
320+ func (h * NPMHandler ) downloadURL (r * http.Request , packageName , version , filename string ) (string , error ) {
321+ shasum := r .URL .Query ().Get (npmTarballShasumParam )
322+ if shasum != "" {
323+ return h .tarballURLForShasum (r , packageName , version , shasum )
324+ }
325+
326+ return fmt .Sprintf (
327+ "%s/%s/-/%s" ,
328+ h .upstreamURL ,
329+ escapeNPMDownloadPackage (packageName ),
330+ url .PathEscape (filename ),
331+ ), nil
332+ }
333+
334+ func (h * NPMHandler ) tarballURLForShasum (r * http.Request , packageName , version , shasum string ) (string , error ) {
335+ metadataURL := fmt .Sprintf ("%s/%s" , h .upstreamURL , url .PathEscape (packageName ))
336+ body , _ , err := h .proxy .FetchOrCacheMetadata (r .Context (), "npm" , packageName , metadataURL , npmAbbreviatedCT )
337+ if err != nil {
338+ return "" , fmt .Errorf ("fetching npm metadata: %w" , err )
339+ }
340+
341+ var metadata map [string ]any
342+ if err := json .Unmarshal (body , & metadata ); err != nil {
343+ return "" , fmt .Errorf ("parsing npm metadata: %w" , err )
344+ }
345+
346+ versions , ok := metadata ["versions" ].(map [string ]any )
347+ if ! ok {
348+ return "" , errors .New ("npm metadata has no versions" )
349+ }
350+ vdata , ok := versions [version ].(map [string ]any )
351+ if ! ok {
352+ return "" , fmt .Errorf ("npm metadata has no version %q" , version )
353+ }
354+ dist , ok := vdata ["dist" ].(map [string ]any )
355+ if ! ok {
356+ return "" , fmt .Errorf ("npm metadata version %q has no dist" , version )
357+ }
358+ if actualShasum , _ := dist ["shasum" ].(string ); actualShasum != shasum {
359+ return "" , errors .New ("npm tarball checksum does not match metadata" )
360+ }
361+ tarball , ok := dist ["tarball" ].(string )
362+ if ! ok {
363+ return "" , fmt .Errorf ("npm metadata version %q has no tarball" , version )
364+ }
365+
366+ return h .validateUpstreamTarballURL (tarball )
367+ }
368+
369+ func (h * NPMHandler ) validateUpstreamTarballURL (tarball string ) (string , error ) {
370+ tarballURL , err := url .Parse (tarball )
371+ if err != nil {
372+ return "" , fmt .Errorf ("parsing tarball URL: %w" , err )
373+ }
374+ upstreamURL , err := url .Parse (h .upstreamURL )
375+ if err != nil {
376+ return "" , fmt .Errorf ("parsing upstream URL: %w" , err )
377+ }
378+ if tarballURL .User != nil || tarballURL .Scheme != upstreamURL .Scheme ||
379+ ! strings .EqualFold (tarballURL .Host , upstreamURL .Host ) {
380+ return "" , errors .New ("npm tarball URL does not match upstream registry" )
381+ }
382+
383+ return tarballURL .String (), nil
384+ }
385+
290386func escapeNPMDownloadPackage (packageName string ) string {
291387 scope , name , scoped := strings .Cut (packageName , "/" )
292388 if scoped && strings .HasPrefix (scope , "@" ) && len (scope ) > 1 && name != "" && ! strings .Contains (name , "/" ) {
@@ -345,12 +441,8 @@ func (h *NPMHandler) extractVersionFromFilename(packageName, filename string) st
345441 }
346442 base := strings .TrimSuffix (filename , ".tgz" )
347443
348- // For scoped packages, the filename uses the short name
349- shortName := packageName
350- if strings .Contains (packageName , "/" ) {
351- parts := strings .SplitN (packageName , "/" , scopedParts )
352- shortName = parts [1 ]
353- }
444+ // For scoped packages, the filename uses the short name.
445+ shortName := npmPackageShortName (packageName )
354446
355447 // Expected format: {shortName}-{version}
356448 prefix := shortName + "-"
0 commit comments