code.gitea.io/gitea@v1.21.7/routers/api/packages/api.go (about)

     1  // Copyright 2022 The Gitea Authors. All rights reserved.
     2  // SPDX-License-Identifier: MIT
     3  
     4  package packages
     5  
     6  import (
     7  	"net/http"
     8  	"regexp"
     9  	"strings"
    10  
    11  	auth_model "code.gitea.io/gitea/models/auth"
    12  	"code.gitea.io/gitea/models/perm"
    13  	"code.gitea.io/gitea/modules/context"
    14  	"code.gitea.io/gitea/modules/log"
    15  	"code.gitea.io/gitea/modules/setting"
    16  	"code.gitea.io/gitea/modules/web"
    17  	"code.gitea.io/gitea/routers/api/packages/alpine"
    18  	"code.gitea.io/gitea/routers/api/packages/cargo"
    19  	"code.gitea.io/gitea/routers/api/packages/chef"
    20  	"code.gitea.io/gitea/routers/api/packages/composer"
    21  	"code.gitea.io/gitea/routers/api/packages/conan"
    22  	"code.gitea.io/gitea/routers/api/packages/conda"
    23  	"code.gitea.io/gitea/routers/api/packages/container"
    24  	"code.gitea.io/gitea/routers/api/packages/cran"
    25  	"code.gitea.io/gitea/routers/api/packages/debian"
    26  	"code.gitea.io/gitea/routers/api/packages/generic"
    27  	"code.gitea.io/gitea/routers/api/packages/goproxy"
    28  	"code.gitea.io/gitea/routers/api/packages/helm"
    29  	"code.gitea.io/gitea/routers/api/packages/maven"
    30  	"code.gitea.io/gitea/routers/api/packages/npm"
    31  	"code.gitea.io/gitea/routers/api/packages/nuget"
    32  	"code.gitea.io/gitea/routers/api/packages/pub"
    33  	"code.gitea.io/gitea/routers/api/packages/pypi"
    34  	"code.gitea.io/gitea/routers/api/packages/rpm"
    35  	"code.gitea.io/gitea/routers/api/packages/rubygems"
    36  	"code.gitea.io/gitea/routers/api/packages/swift"
    37  	"code.gitea.io/gitea/routers/api/packages/vagrant"
    38  	"code.gitea.io/gitea/services/auth"
    39  	context_service "code.gitea.io/gitea/services/context"
    40  )
    41  
    42  func reqPackageAccess(accessMode perm.AccessMode) func(ctx *context.Context) {
    43  	return func(ctx *context.Context) {
    44  		if ctx.Data["IsApiToken"] == true {
    45  			scope, ok := ctx.Data["ApiTokenScope"].(auth_model.AccessTokenScope)
    46  			if ok { // it's a personal access token but not oauth2 token
    47  				scopeMatched := false
    48  				var err error
    49  				if accessMode == perm.AccessModeRead {
    50  					scopeMatched, err = scope.HasScope(auth_model.AccessTokenScopeReadPackage)
    51  					if err != nil {
    52  						ctx.Error(http.StatusInternalServerError, "HasScope", err.Error())
    53  						return
    54  					}
    55  				} else if accessMode == perm.AccessModeWrite {
    56  					scopeMatched, err = scope.HasScope(auth_model.AccessTokenScopeWritePackage)
    57  					if err != nil {
    58  						ctx.Error(http.StatusInternalServerError, "HasScope", err.Error())
    59  						return
    60  					}
    61  				}
    62  				if !scopeMatched {
    63  					ctx.Resp.Header().Set("WWW-Authenticate", `Basic realm="Gitea Package API"`)
    64  					ctx.Error(http.StatusUnauthorized, "reqPackageAccess", "user should have specific permission or be a site admin")
    65  					return
    66  				}
    67  			}
    68  		}
    69  
    70  		if ctx.Package.AccessMode < accessMode && !ctx.IsUserSiteAdmin() {
    71  			ctx.Resp.Header().Set("WWW-Authenticate", `Basic realm="Gitea Package API"`)
    72  			ctx.Error(http.StatusUnauthorized, "reqPackageAccess", "user should have specific permission or be a site admin")
    73  			return
    74  		}
    75  	}
    76  }
    77  
    78  func verifyAuth(r *web.Route, authMethods []auth.Method) {
    79  	if setting.Service.EnableReverseProxyAuth {
    80  		authMethods = append(authMethods, &auth.ReverseProxy{})
    81  	}
    82  	authGroup := auth.NewGroup(authMethods...)
    83  
    84  	r.Use(func(ctx *context.Context) {
    85  		var err error
    86  		ctx.Doer, err = authGroup.Verify(ctx.Req, ctx.Resp, ctx, ctx.Session)
    87  		if err != nil {
    88  			log.Error("Failed to verify user: %v", err)
    89  			ctx.Error(http.StatusUnauthorized, "authGroup.Verify")
    90  			return
    91  		}
    92  		ctx.IsSigned = ctx.Doer != nil
    93  	})
    94  }
    95  
    96  // CommonRoutes provide endpoints for most package managers (except containers - see below)
    97  // These are mounted on `/api/packages` (not `/api/v1/packages`)
    98  func CommonRoutes() *web.Route {
    99  	r := web.NewRoute()
   100  
   101  	r.Use(context.PackageContexter())
   102  
   103  	verifyAuth(r, []auth.Method{
   104  		&auth.OAuth2{},
   105  		&auth.Basic{},
   106  		&nuget.Auth{},
   107  		&conan.Auth{},
   108  		&chef.Auth{},
   109  	})
   110  
   111  	r.Group("/{username}", func() {
   112  		r.Group("/alpine", func() {
   113  			r.Get("/key", alpine.GetRepositoryKey)
   114  			r.Group("/{branch}/{repository}", func() {
   115  				r.Put("", reqPackageAccess(perm.AccessModeWrite), alpine.UploadPackageFile)
   116  				r.Group("/{architecture}", func() {
   117  					r.Get("/APKINDEX.tar.gz", alpine.GetRepositoryFile)
   118  					r.Group("/{filename}", func() {
   119  						r.Get("", alpine.DownloadPackageFile)
   120  						r.Delete("", reqPackageAccess(perm.AccessModeWrite), alpine.DeletePackageFile)
   121  					})
   122  				})
   123  			})
   124  		}, reqPackageAccess(perm.AccessModeRead))
   125  		r.Group("/cargo", func() {
   126  			r.Group("/api/v1/crates", func() {
   127  				r.Get("", cargo.SearchPackages)
   128  				r.Put("/new", reqPackageAccess(perm.AccessModeWrite), cargo.UploadPackage)
   129  				r.Group("/{package}", func() {
   130  					r.Group("/{version}", func() {
   131  						r.Get("/download", cargo.DownloadPackageFile)
   132  						r.Delete("/yank", reqPackageAccess(perm.AccessModeWrite), cargo.YankPackage)
   133  						r.Put("/unyank", reqPackageAccess(perm.AccessModeWrite), cargo.UnyankPackage)
   134  					})
   135  					r.Get("/owners", cargo.ListOwners)
   136  				})
   137  			})
   138  			r.Get("/config.json", cargo.RepositoryConfig)
   139  			r.Get("/1/{package}", cargo.EnumeratePackageVersions)
   140  			r.Get("/2/{package}", cargo.EnumeratePackageVersions)
   141  			// Use dummy placeholders because these parts are not of interest
   142  			r.Get("/3/{_}/{package}", cargo.EnumeratePackageVersions)
   143  			r.Get("/{_}/{__}/{package}", cargo.EnumeratePackageVersions)
   144  		}, reqPackageAccess(perm.AccessModeRead))
   145  		r.Group("/chef", func() {
   146  			r.Group("/api/v1", func() {
   147  				r.Get("/universe", chef.PackagesUniverse)
   148  				r.Get("/search", chef.EnumeratePackages)
   149  				r.Group("/cookbooks", func() {
   150  					r.Get("", chef.EnumeratePackages)
   151  					r.Post("", reqPackageAccess(perm.AccessModeWrite), chef.UploadPackage)
   152  					r.Group("/{name}", func() {
   153  						r.Get("", chef.PackageMetadata)
   154  						r.Group("/versions/{version}", func() {
   155  							r.Get("", chef.PackageVersionMetadata)
   156  							r.Delete("", reqPackageAccess(perm.AccessModeWrite), chef.DeletePackageVersion)
   157  							r.Get("/download", chef.DownloadPackage)
   158  						})
   159  						r.Delete("", reqPackageAccess(perm.AccessModeWrite), chef.DeletePackage)
   160  					})
   161  				})
   162  			})
   163  		}, reqPackageAccess(perm.AccessModeRead))
   164  		r.Group("/composer", func() {
   165  			r.Get("/packages.json", composer.ServiceIndex)
   166  			r.Get("/search.json", composer.SearchPackages)
   167  			r.Get("/list.json", composer.EnumeratePackages)
   168  			r.Get("/p2/{vendorname}/{projectname}~dev.json", composer.PackageMetadata)
   169  			r.Get("/p2/{vendorname}/{projectname}.json", composer.PackageMetadata)
   170  			r.Get("/files/{package}/{version}/{filename}", composer.DownloadPackageFile)
   171  			r.Put("", reqPackageAccess(perm.AccessModeWrite), composer.UploadPackage)
   172  		}, reqPackageAccess(perm.AccessModeRead))
   173  		r.Group("/conan", func() {
   174  			r.Group("/v1", func() {
   175  				r.Get("/ping", conan.Ping)
   176  				r.Group("/users", func() {
   177  					r.Get("/authenticate", conan.Authenticate)
   178  					r.Get("/check_credentials", conan.CheckCredentials)
   179  				})
   180  				r.Group("/conans", func() {
   181  					r.Get("/search", conan.SearchRecipes)
   182  					r.Group("/{name}/{version}/{user}/{channel}", func() {
   183  						r.Get("", conan.RecipeSnapshot)
   184  						r.Delete("", reqPackageAccess(perm.AccessModeWrite), conan.DeleteRecipeV1)
   185  						r.Get("/search", conan.SearchPackagesV1)
   186  						r.Get("/digest", conan.RecipeDownloadURLs)
   187  						r.Post("/upload_urls", reqPackageAccess(perm.AccessModeWrite), conan.RecipeUploadURLs)
   188  						r.Get("/download_urls", conan.RecipeDownloadURLs)
   189  						r.Group("/packages", func() {
   190  							r.Post("/delete", reqPackageAccess(perm.AccessModeWrite), conan.DeletePackageV1)
   191  							r.Group("/{package_reference}", func() {
   192  								r.Get("", conan.PackageSnapshot)
   193  								r.Get("/digest", conan.PackageDownloadURLs)
   194  								r.Post("/upload_urls", reqPackageAccess(perm.AccessModeWrite), conan.PackageUploadURLs)
   195  								r.Get("/download_urls", conan.PackageDownloadURLs)
   196  							})
   197  						})
   198  					}, conan.ExtractPathParameters)
   199  				})
   200  				r.Group("/files/{name}/{version}/{user}/{channel}/{recipe_revision}", func() {
   201  					r.Group("/recipe/{filename}", func() {
   202  						r.Get("", conan.DownloadRecipeFile)
   203  						r.Put("", reqPackageAccess(perm.AccessModeWrite), conan.UploadRecipeFile)
   204  					})
   205  					r.Group("/package/{package_reference}/{package_revision}/{filename}", func() {
   206  						r.Get("", conan.DownloadPackageFile)
   207  						r.Put("", reqPackageAccess(perm.AccessModeWrite), conan.UploadPackageFile)
   208  					})
   209  				}, conan.ExtractPathParameters)
   210  			})
   211  			r.Group("/v2", func() {
   212  				r.Get("/ping", conan.Ping)
   213  				r.Group("/users", func() {
   214  					r.Get("/authenticate", conan.Authenticate)
   215  					r.Get("/check_credentials", conan.CheckCredentials)
   216  				})
   217  				r.Group("/conans", func() {
   218  					r.Get("/search", conan.SearchRecipes)
   219  					r.Group("/{name}/{version}/{user}/{channel}", func() {
   220  						r.Delete("", reqPackageAccess(perm.AccessModeWrite), conan.DeleteRecipeV2)
   221  						r.Get("/search", conan.SearchPackagesV2)
   222  						r.Get("/latest", conan.LatestRecipeRevision)
   223  						r.Group("/revisions", func() {
   224  							r.Get("", conan.ListRecipeRevisions)
   225  							r.Group("/{recipe_revision}", func() {
   226  								r.Delete("", reqPackageAccess(perm.AccessModeWrite), conan.DeleteRecipeV2)
   227  								r.Get("/search", conan.SearchPackagesV2)
   228  								r.Group("/files", func() {
   229  									r.Get("", conan.ListRecipeRevisionFiles)
   230  									r.Group("/{filename}", func() {
   231  										r.Get("", conan.DownloadRecipeFile)
   232  										r.Put("", reqPackageAccess(perm.AccessModeWrite), conan.UploadRecipeFile)
   233  									})
   234  								})
   235  								r.Group("/packages", func() {
   236  									r.Delete("", reqPackageAccess(perm.AccessModeWrite), conan.DeletePackageV2)
   237  									r.Group("/{package_reference}", func() {
   238  										r.Delete("", reqPackageAccess(perm.AccessModeWrite), conan.DeletePackageV2)
   239  										r.Get("/latest", conan.LatestPackageRevision)
   240  										r.Group("/revisions", func() {
   241  											r.Get("", conan.ListPackageRevisions)
   242  											r.Group("/{package_revision}", func() {
   243  												r.Delete("", reqPackageAccess(perm.AccessModeWrite), conan.DeletePackageV2)
   244  												r.Group("/files", func() {
   245  													r.Get("", conan.ListPackageRevisionFiles)
   246  													r.Group("/{filename}", func() {
   247  														r.Get("", conan.DownloadPackageFile)
   248  														r.Put("", reqPackageAccess(perm.AccessModeWrite), conan.UploadPackageFile)
   249  													})
   250  												})
   251  											})
   252  										})
   253  									})
   254  								})
   255  							})
   256  						})
   257  					}, conan.ExtractPathParameters)
   258  				})
   259  			})
   260  		}, reqPackageAccess(perm.AccessModeRead))
   261  		r.Group("/conda", func() {
   262  			var (
   263  				downloadPattern = regexp.MustCompile(`\A(.+/)?(.+)/((?:[^/]+(?:\.tar\.bz2|\.conda))|(?:current_)?repodata\.json(?:\.bz2)?)\z`)
   264  				uploadPattern   = regexp.MustCompile(`\A(.+/)?([^/]+(?:\.tar\.bz2|\.conda))\z`)
   265  			)
   266  
   267  			r.Get("/*", func(ctx *context.Context) {
   268  				m := downloadPattern.FindStringSubmatch(ctx.Params("*"))
   269  				if len(m) == 0 {
   270  					ctx.Status(http.StatusNotFound)
   271  					return
   272  				}
   273  
   274  				ctx.SetParams("channel", strings.TrimSuffix(m[1], "/"))
   275  				ctx.SetParams("architecture", m[2])
   276  				ctx.SetParams("filename", m[3])
   277  
   278  				switch m[3] {
   279  				case "repodata.json", "repodata.json.bz2", "current_repodata.json", "current_repodata.json.bz2":
   280  					conda.EnumeratePackages(ctx)
   281  				default:
   282  					conda.DownloadPackageFile(ctx)
   283  				}
   284  			})
   285  			r.Put("/*", reqPackageAccess(perm.AccessModeWrite), func(ctx *context.Context) {
   286  				m := uploadPattern.FindStringSubmatch(ctx.Params("*"))
   287  				if len(m) == 0 {
   288  					ctx.Status(http.StatusNotFound)
   289  					return
   290  				}
   291  
   292  				ctx.SetParams("channel", strings.TrimSuffix(m[1], "/"))
   293  				ctx.SetParams("filename", m[2])
   294  
   295  				conda.UploadPackageFile(ctx)
   296  			})
   297  		}, reqPackageAccess(perm.AccessModeRead))
   298  		r.Group("/cran", func() {
   299  			r.Group("/src", func() {
   300  				r.Group("/contrib", func() {
   301  					r.Get("/PACKAGES", cran.EnumerateSourcePackages)
   302  					r.Get("/PACKAGES{format}", cran.EnumerateSourcePackages)
   303  					r.Get("/{filename}", cran.DownloadSourcePackageFile)
   304  				})
   305  				r.Put("", reqPackageAccess(perm.AccessModeWrite), cran.UploadSourcePackageFile)
   306  			})
   307  			r.Group("/bin", func() {
   308  				r.Group("/{platform}/contrib/{rversion}", func() {
   309  					r.Get("/PACKAGES", cran.EnumerateBinaryPackages)
   310  					r.Get("/PACKAGES{format}", cran.EnumerateBinaryPackages)
   311  					r.Get("/{filename}", cran.DownloadBinaryPackageFile)
   312  				})
   313  				r.Put("", reqPackageAccess(perm.AccessModeWrite), cran.UploadBinaryPackageFile)
   314  			})
   315  		}, reqPackageAccess(perm.AccessModeRead))
   316  		r.Group("/debian", func() {
   317  			r.Get("/repository.key", debian.GetRepositoryKey)
   318  			r.Group("/dists/{distribution}", func() {
   319  				r.Get("/{filename}", debian.GetRepositoryFile)
   320  				r.Get("/by-hash/{algorithm}/{hash}", debian.GetRepositoryFileByHash)
   321  				r.Group("/{component}/{architecture}", func() {
   322  					r.Get("/{filename}", debian.GetRepositoryFile)
   323  					r.Get("/by-hash/{algorithm}/{hash}", debian.GetRepositoryFileByHash)
   324  				})
   325  			})
   326  			r.Group("/pool/{distribution}/{component}", func() {
   327  				r.Get("/{name}_{version}_{architecture}.deb", debian.DownloadPackageFile)
   328  				r.Group("", func() {
   329  					r.Put("/upload", debian.UploadPackageFile)
   330  					r.Delete("/{name}/{version}/{architecture}", debian.DeletePackageFile)
   331  				}, reqPackageAccess(perm.AccessModeWrite))
   332  			})
   333  		}, reqPackageAccess(perm.AccessModeRead))
   334  		r.Group("/go", func() {
   335  			r.Put("/upload", reqPackageAccess(perm.AccessModeWrite), goproxy.UploadPackage)
   336  			r.Get("/sumdb/sum.golang.org/supported", func(ctx *context.Context) {
   337  				ctx.Status(http.StatusNotFound)
   338  			})
   339  
   340  			// Manual mapping of routes because the package name contains slashes which chi does not support
   341  			// https://go.dev/ref/mod#goproxy-protocol
   342  			r.Get("/*", func(ctx *context.Context) {
   343  				path := ctx.Params("*")
   344  
   345  				if strings.HasSuffix(path, "/@latest") {
   346  					ctx.SetParams("name", path[:len(path)-len("/@latest")])
   347  					ctx.SetParams("version", "latest")
   348  
   349  					goproxy.PackageVersionMetadata(ctx)
   350  					return
   351  				}
   352  
   353  				parts := strings.SplitN(path, "/@v/", 2)
   354  				if len(parts) != 2 {
   355  					ctx.Status(http.StatusNotFound)
   356  					return
   357  				}
   358  
   359  				ctx.SetParams("name", parts[0])
   360  
   361  				// <package/name>/@v/list
   362  				if parts[1] == "list" {
   363  					goproxy.EnumeratePackageVersions(ctx)
   364  					return
   365  				}
   366  
   367  				// <package/name>/@v/<version>.zip
   368  				if strings.HasSuffix(parts[1], ".zip") {
   369  					ctx.SetParams("version", parts[1][:len(parts[1])-len(".zip")])
   370  
   371  					goproxy.DownloadPackageFile(ctx)
   372  					return
   373  				}
   374  				// <package/name>/@v/<version>.info
   375  				if strings.HasSuffix(parts[1], ".info") {
   376  					ctx.SetParams("version", parts[1][:len(parts[1])-len(".info")])
   377  
   378  					goproxy.PackageVersionMetadata(ctx)
   379  					return
   380  				}
   381  				// <package/name>/@v/<version>.mod
   382  				if strings.HasSuffix(parts[1], ".mod") {
   383  					ctx.SetParams("version", parts[1][:len(parts[1])-len(".mod")])
   384  
   385  					goproxy.PackageVersionGoModContent(ctx)
   386  					return
   387  				}
   388  
   389  				ctx.Status(http.StatusNotFound)
   390  			})
   391  		}, reqPackageAccess(perm.AccessModeRead))
   392  		r.Group("/generic", func() {
   393  			r.Group("/{packagename}/{packageversion}", func() {
   394  				r.Delete("", reqPackageAccess(perm.AccessModeWrite), generic.DeletePackage)
   395  				r.Group("/{filename}", func() {
   396  					r.Get("", generic.DownloadPackageFile)
   397  					r.Group("", func() {
   398  						r.Put("", generic.UploadPackage)
   399  						r.Delete("", generic.DeletePackageFile)
   400  					}, reqPackageAccess(perm.AccessModeWrite))
   401  				})
   402  			})
   403  		}, reqPackageAccess(perm.AccessModeRead))
   404  		r.Group("/helm", func() {
   405  			r.Get("/index.yaml", helm.Index)
   406  			r.Get("/{filename}", helm.DownloadPackageFile)
   407  			r.Post("/api/charts", reqPackageAccess(perm.AccessModeWrite), helm.UploadPackage)
   408  		}, reqPackageAccess(perm.AccessModeRead))
   409  		r.Group("/maven", func() {
   410  			r.Put("/*", reqPackageAccess(perm.AccessModeWrite), maven.UploadPackageFile)
   411  			r.Get("/*", maven.DownloadPackageFile)
   412  			r.Head("/*", maven.ProvidePackageFileHeader)
   413  		}, reqPackageAccess(perm.AccessModeRead))
   414  		r.Group("/nuget", func() {
   415  			r.Group("", func() { // Needs to be unauthenticated for the NuGet client.
   416  				r.Get("/", nuget.ServiceIndexV2)
   417  				r.Get("/index.json", nuget.ServiceIndexV3)
   418  				r.Get("/$metadata", nuget.FeedCapabilityResource)
   419  			})
   420  			r.Group("", func() {
   421  				r.Get("/query", nuget.SearchServiceV3)
   422  				r.Group("/registration/{id}", func() {
   423  					r.Get("/index.json", nuget.RegistrationIndex)
   424  					r.Get("/{version}", nuget.RegistrationLeafV3)
   425  				})
   426  				r.Group("/package/{id}", func() {
   427  					r.Get("/index.json", nuget.EnumeratePackageVersionsV3)
   428  					r.Get("/{version}/{filename}", nuget.DownloadPackageFile)
   429  				})
   430  				r.Group("", func() {
   431  					r.Put("/", nuget.UploadPackage)
   432  					r.Put("/symbolpackage", nuget.UploadSymbolPackage)
   433  					r.Delete("/{id}/{version}", nuget.DeletePackage)
   434  				}, reqPackageAccess(perm.AccessModeWrite))
   435  				r.Get("/symbols/{filename}/{guid:[0-9a-fA-F]{32}[fF]{8}}/{filename2}", nuget.DownloadSymbolFile)
   436  				r.Get("/Packages(Id='{id:[^']+}',Version='{version:[^']+}')", nuget.RegistrationLeafV2)
   437  				r.Group("/Packages()", func() {
   438  					r.Get("", nuget.SearchServiceV2)
   439  					r.Get("/$count", nuget.SearchServiceV2Count)
   440  				})
   441  				r.Group("/FindPackagesById()", func() {
   442  					r.Get("", nuget.EnumeratePackageVersionsV2)
   443  					r.Get("/$count", nuget.EnumeratePackageVersionsV2Count)
   444  				})
   445  				r.Group("/Search()", func() {
   446  					r.Get("", nuget.SearchServiceV2)
   447  					r.Get("/$count", nuget.SearchServiceV2Count)
   448  				})
   449  			}, reqPackageAccess(perm.AccessModeRead))
   450  		})
   451  		r.Group("/npm", func() {
   452  			r.Group("/@{scope}/{id}", func() {
   453  				r.Get("", npm.PackageMetadata)
   454  				r.Put("", reqPackageAccess(perm.AccessModeWrite), npm.UploadPackage)
   455  				r.Group("/-/{version}/{filename}", func() {
   456  					r.Get("", npm.DownloadPackageFile)
   457  					r.Delete("/-rev/{revision}", reqPackageAccess(perm.AccessModeWrite), npm.DeletePackageVersion)
   458  				})
   459  				r.Get("/-/{filename}", npm.DownloadPackageFileByName)
   460  				r.Group("/-rev/{revision}", func() {
   461  					r.Delete("", npm.DeletePackage)
   462  					r.Put("", npm.DeletePreview)
   463  				}, reqPackageAccess(perm.AccessModeWrite))
   464  			})
   465  			r.Group("/{id}", func() {
   466  				r.Get("", npm.PackageMetadata)
   467  				r.Put("", reqPackageAccess(perm.AccessModeWrite), npm.UploadPackage)
   468  				r.Group("/-/{version}/{filename}", func() {
   469  					r.Get("", npm.DownloadPackageFile)
   470  					r.Delete("/-rev/{revision}", reqPackageAccess(perm.AccessModeWrite), npm.DeletePackageVersion)
   471  				})
   472  				r.Get("/-/{filename}", npm.DownloadPackageFileByName)
   473  				r.Group("/-rev/{revision}", func() {
   474  					r.Delete("", npm.DeletePackage)
   475  					r.Put("", npm.DeletePreview)
   476  				}, reqPackageAccess(perm.AccessModeWrite))
   477  			})
   478  			r.Group("/-/package/@{scope}/{id}/dist-tags", func() {
   479  				r.Get("", npm.ListPackageTags)
   480  				r.Group("/{tag}", func() {
   481  					r.Put("", npm.AddPackageTag)
   482  					r.Delete("", npm.DeletePackageTag)
   483  				}, reqPackageAccess(perm.AccessModeWrite))
   484  			})
   485  			r.Group("/-/package/{id}/dist-tags", func() {
   486  				r.Get("", npm.ListPackageTags)
   487  				r.Group("/{tag}", func() {
   488  					r.Put("", npm.AddPackageTag)
   489  					r.Delete("", npm.DeletePackageTag)
   490  				}, reqPackageAccess(perm.AccessModeWrite))
   491  			})
   492  			r.Group("/-/v1/search", func() {
   493  				r.Get("", npm.PackageSearch)
   494  			})
   495  		}, reqPackageAccess(perm.AccessModeRead))
   496  		r.Group("/pub", func() {
   497  			r.Group("/api/packages", func() {
   498  				r.Group("/versions/new", func() {
   499  					r.Get("", pub.RequestUpload)
   500  					r.Post("/upload", pub.UploadPackageFile)
   501  					r.Get("/finalize/{id}/{version}", pub.FinalizePackage)
   502  				}, reqPackageAccess(perm.AccessModeWrite))
   503  				r.Group("/{id}", func() {
   504  					r.Get("", pub.EnumeratePackageVersions)
   505  					r.Get("/files/{version}", pub.DownloadPackageFile)
   506  					r.Get("/{version}", pub.PackageVersionMetadata)
   507  				})
   508  			})
   509  		}, reqPackageAccess(perm.AccessModeRead))
   510  		r.Group("/pypi", func() {
   511  			r.Post("/", reqPackageAccess(perm.AccessModeWrite), pypi.UploadPackageFile)
   512  			r.Get("/files/{id}/{version}/{filename}", pypi.DownloadPackageFile)
   513  			r.Get("/simple/{id}", pypi.PackageMetadata)
   514  		}, reqPackageAccess(perm.AccessModeRead))
   515  		r.Group("/rpm", func() {
   516  			r.Get(".repo", rpm.GetRepositoryConfig)
   517  			r.Get("/repository.key", rpm.GetRepositoryKey)
   518  			r.Put("/upload", reqPackageAccess(perm.AccessModeWrite), rpm.UploadPackageFile)
   519  			r.Group("/package/{name}/{version}/{architecture}", func() {
   520  				r.Get("", rpm.DownloadPackageFile)
   521  				r.Delete("", reqPackageAccess(perm.AccessModeWrite), rpm.DeletePackageFile)
   522  			})
   523  			r.Group("/repodata/{filename}", func() {
   524  				r.Head("", rpm.CheckRepositoryFileExistence)
   525  				r.Get("", rpm.GetRepositoryFile)
   526  			})
   527  		}, reqPackageAccess(perm.AccessModeRead))
   528  		r.Group("/rubygems", func() {
   529  			r.Get("/specs.4.8.gz", rubygems.EnumeratePackages)
   530  			r.Get("/latest_specs.4.8.gz", rubygems.EnumeratePackagesLatest)
   531  			r.Get("/prerelease_specs.4.8.gz", rubygems.EnumeratePackagesPreRelease)
   532  			r.Get("/quick/Marshal.4.8/{filename}", rubygems.ServePackageSpecification)
   533  			r.Get("/gems/{filename}", rubygems.DownloadPackageFile)
   534  			r.Group("/api/v1/gems", func() {
   535  				r.Post("/", rubygems.UploadPackageFile)
   536  				r.Delete("/yank", rubygems.DeletePackage)
   537  			}, reqPackageAccess(perm.AccessModeWrite))
   538  		}, reqPackageAccess(perm.AccessModeRead))
   539  		r.Group("/swift", func() {
   540  			r.Group("/{scope}/{name}", func() {
   541  				r.Group("", func() {
   542  					r.Get("", swift.EnumeratePackageVersions)
   543  					r.Get(".json", swift.EnumeratePackageVersions)
   544  				}, swift.CheckAcceptMediaType(swift.AcceptJSON))
   545  				r.Group("/{version}", func() {
   546  					r.Get("/Package.swift", swift.CheckAcceptMediaType(swift.AcceptSwift), swift.DownloadManifest)
   547  					r.Put("", reqPackageAccess(perm.AccessModeWrite), swift.CheckAcceptMediaType(swift.AcceptJSON), swift.UploadPackageFile)
   548  					r.Get("", func(ctx *context.Context) {
   549  						// Can't use normal routes here: https://github.com/go-chi/chi/issues/781
   550  
   551  						version := ctx.Params("version")
   552  						if strings.HasSuffix(version, ".zip") {
   553  							swift.CheckAcceptMediaType(swift.AcceptZip)(ctx)
   554  							if ctx.Written() {
   555  								return
   556  							}
   557  							ctx.SetParams("version", version[:len(version)-4])
   558  							swift.DownloadPackageFile(ctx)
   559  						} else {
   560  							swift.CheckAcceptMediaType(swift.AcceptJSON)(ctx)
   561  							if ctx.Written() {
   562  								return
   563  							}
   564  							if strings.HasSuffix(version, ".json") {
   565  								ctx.SetParams("version", version[:len(version)-5])
   566  							}
   567  							swift.PackageVersionMetadata(ctx)
   568  						}
   569  					})
   570  				})
   571  			})
   572  			r.Get("/identifiers", swift.CheckAcceptMediaType(swift.AcceptJSON), swift.LookupPackageIdentifiers)
   573  		}, reqPackageAccess(perm.AccessModeRead))
   574  		r.Group("/vagrant", func() {
   575  			r.Group("/authenticate", func() {
   576  				r.Get("", vagrant.CheckAuthenticate)
   577  			})
   578  			r.Group("/{name}", func() {
   579  				r.Head("", vagrant.CheckBoxAvailable)
   580  				r.Get("", vagrant.EnumeratePackageVersions)
   581  				r.Group("/{version}/{provider}", func() {
   582  					r.Get("", vagrant.DownloadPackageFile)
   583  					r.Put("", reqPackageAccess(perm.AccessModeWrite), vagrant.UploadPackageFile)
   584  				})
   585  			})
   586  		}, reqPackageAccess(perm.AccessModeRead))
   587  	}, context_service.UserAssignmentWeb(), context.PackageAssignment())
   588  
   589  	return r
   590  }
   591  
   592  // ContainerRoutes provides endpoints that implement the OCI API to serve containers
   593  // These have to be mounted on `/v2/...` to comply with the OCI spec:
   594  // https://github.com/opencontainers/distribution-spec/blob/main/spec.md
   595  func ContainerRoutes() *web.Route {
   596  	r := web.NewRoute()
   597  
   598  	r.Use(context.PackageContexter())
   599  
   600  	verifyAuth(r, []auth.Method{
   601  		&auth.Basic{},
   602  		&container.Auth{},
   603  	})
   604  
   605  	r.Get("", container.ReqContainerAccess, container.DetermineSupport)
   606  	r.Group("/token", func() {
   607  		r.Get("", container.Authenticate)
   608  		r.Post("", container.AuthenticateNotImplemented)
   609  	})
   610  	r.Get("/_catalog", container.ReqContainerAccess, container.GetRepositoryList)
   611  	r.Group("/{username}", func() {
   612  		r.Group("/{image}", func() {
   613  			r.Group("/blobs/uploads", func() {
   614  				r.Post("", container.InitiateUploadBlob)
   615  				r.Group("/{uuid}", func() {
   616  					r.Get("", container.GetUploadBlob)
   617  					r.Patch("", container.UploadBlob)
   618  					r.Put("", container.EndUploadBlob)
   619  					r.Delete("", container.CancelUploadBlob)
   620  				})
   621  			}, reqPackageAccess(perm.AccessModeWrite))
   622  			r.Group("/blobs/{digest}", func() {
   623  				r.Head("", container.HeadBlob)
   624  				r.Get("", container.GetBlob)
   625  				r.Delete("", reqPackageAccess(perm.AccessModeWrite), container.DeleteBlob)
   626  			})
   627  			r.Group("/manifests/{reference}", func() {
   628  				r.Put("", reqPackageAccess(perm.AccessModeWrite), container.UploadManifest)
   629  				r.Head("", container.HeadManifest)
   630  				r.Get("", container.GetManifest)
   631  				r.Delete("", reqPackageAccess(perm.AccessModeWrite), container.DeleteManifest)
   632  			})
   633  			r.Get("/tags/list", container.GetTagList)
   634  		}, container.VerifyImageName)
   635  
   636  		var (
   637  			blobsUploadsPattern = regexp.MustCompile(`\A(.+)/blobs/uploads/([a-zA-Z0-9-_.=]+)\z`)
   638  			blobsPattern        = regexp.MustCompile(`\A(.+)/blobs/([^/]+)\z`)
   639  			manifestsPattern    = regexp.MustCompile(`\A(.+)/manifests/([^/]+)\z`)
   640  		)
   641  
   642  		// Manual mapping of routes because {image} can contain slashes which chi does not support
   643  		r.Methods("HEAD,GET,POST,PUT,PATCH,DELETE", "/*", func(ctx *context.Context) {
   644  			path := ctx.Params("*")
   645  			isHead := ctx.Req.Method == "HEAD"
   646  			isGet := ctx.Req.Method == "GET"
   647  			isPost := ctx.Req.Method == "POST"
   648  			isPut := ctx.Req.Method == "PUT"
   649  			isPatch := ctx.Req.Method == "PATCH"
   650  			isDelete := ctx.Req.Method == "DELETE"
   651  
   652  			if isPost && strings.HasSuffix(path, "/blobs/uploads") {
   653  				reqPackageAccess(perm.AccessModeWrite)(ctx)
   654  				if ctx.Written() {
   655  					return
   656  				}
   657  
   658  				ctx.SetParams("image", path[:len(path)-14])
   659  				container.VerifyImageName(ctx)
   660  				if ctx.Written() {
   661  					return
   662  				}
   663  
   664  				container.InitiateUploadBlob(ctx)
   665  				return
   666  			}
   667  			if isGet && strings.HasSuffix(path, "/tags/list") {
   668  				ctx.SetParams("image", path[:len(path)-10])
   669  				container.VerifyImageName(ctx)
   670  				if ctx.Written() {
   671  					return
   672  				}
   673  
   674  				container.GetTagList(ctx)
   675  				return
   676  			}
   677  
   678  			m := blobsUploadsPattern.FindStringSubmatch(path)
   679  			if len(m) == 3 && (isGet || isPut || isPatch || isDelete) {
   680  				reqPackageAccess(perm.AccessModeWrite)(ctx)
   681  				if ctx.Written() {
   682  					return
   683  				}
   684  
   685  				ctx.SetParams("image", m[1])
   686  				container.VerifyImageName(ctx)
   687  				if ctx.Written() {
   688  					return
   689  				}
   690  
   691  				ctx.SetParams("uuid", m[2])
   692  
   693  				if isGet {
   694  					container.GetUploadBlob(ctx)
   695  				} else if isPatch {
   696  					container.UploadBlob(ctx)
   697  				} else if isPut {
   698  					container.EndUploadBlob(ctx)
   699  				} else {
   700  					container.CancelUploadBlob(ctx)
   701  				}
   702  				return
   703  			}
   704  			m = blobsPattern.FindStringSubmatch(path)
   705  			if len(m) == 3 && (isHead || isGet || isDelete) {
   706  				ctx.SetParams("image", m[1])
   707  				container.VerifyImageName(ctx)
   708  				if ctx.Written() {
   709  					return
   710  				}
   711  
   712  				ctx.SetParams("digest", m[2])
   713  
   714  				if isHead {
   715  					container.HeadBlob(ctx)
   716  				} else if isGet {
   717  					container.GetBlob(ctx)
   718  				} else {
   719  					reqPackageAccess(perm.AccessModeWrite)(ctx)
   720  					if ctx.Written() {
   721  						return
   722  					}
   723  					container.DeleteBlob(ctx)
   724  				}
   725  				return
   726  			}
   727  			m = manifestsPattern.FindStringSubmatch(path)
   728  			if len(m) == 3 && (isHead || isGet || isPut || isDelete) {
   729  				ctx.SetParams("image", m[1])
   730  				container.VerifyImageName(ctx)
   731  				if ctx.Written() {
   732  					return
   733  				}
   734  
   735  				ctx.SetParams("reference", m[2])
   736  
   737  				if isHead {
   738  					container.HeadManifest(ctx)
   739  				} else if isGet {
   740  					container.GetManifest(ctx)
   741  				} else {
   742  					reqPackageAccess(perm.AccessModeWrite)(ctx)
   743  					if ctx.Written() {
   744  						return
   745  					}
   746  					if isPut {
   747  						container.UploadManifest(ctx)
   748  					} else {
   749  						container.DeleteManifest(ctx)
   750  					}
   751  				}
   752  				return
   753  			}
   754  
   755  			ctx.Status(http.StatusNotFound)
   756  		})
   757  	}, container.ReqContainerAccess, context_service.UserAssignmentWeb(), context.PackageAssignment(), reqPackageAccess(perm.AccessModeRead))
   758  
   759  	return r
   760  }