registry/api/urls/urls.go (235 lines of code) (raw):
package urls
import (
"errors"
"net/http"
"net/url"
"strings"
"github.com/docker/distribution/reference"
v1 "github.com/docker/distribution/registry/api/gitlab/v1"
v2 "github.com/docker/distribution/registry/api/v2"
"github.com/gorilla/mux"
)
// Instantiating a router is a costly operation and NewBuilder is called once
// per request, so we can save a lot of CPU time by reusing the same router.
var (
defaultDistributionRouter = v2.Router()
defaultGitLabRouter = v1.Router()
ErrNoTagManifest = errors.New("reference must have a tag or digest")
)
// Builder creates registry API urls from a single base endpoint. It can be
// used to create urls for use in a registry client or server.
//
// All urls will be created from the given base, including the api version.
// For example, if a root of "/foo/" is provided, urls generated will be fall
// under "/foo/v2/...". Most application will only provide a schema, host and
// port, such as "https://localhost:5000/".
type Builder struct {
root *url.URL // url root (ie http://localhost/)
distributionRouter *mux.Router
gitLabRouter *mux.Router
relative bool
}
// NewBuilder creates a Builder with provided root url object.
func NewBuilder(root *url.URL, relative bool) *Builder {
return &Builder{
root: root,
distributionRouter: defaultDistributionRouter,
gitLabRouter: defaultGitLabRouter,
relative: relative,
}
}
// NewBuilderFromString workes identically to NewBuilder except it takes
// a string argument for the root, returning an error if it is not a valid
// url.
func NewBuilderFromString(root string, relative bool) (*Builder, error) {
u, err := url.Parse(root)
if err != nil {
return nil, err
}
return NewBuilder(u, relative), nil
}
// NewBuilderFromRequest uses information from an *http.Request to
// construct the root url.
func NewBuilderFromRequest(r *http.Request, relative bool) *Builder {
var (
scheme = "http"
host = r.Host
)
if r.TLS != nil {
scheme = "https"
} else if len(r.URL.Scheme) > 0 {
scheme = r.URL.Scheme
}
// Handle fowarded headers
// Prefer "Forwarded" header as defined by rfc7239 if given
// see https://tools.ietf.org/html/rfc7239
if forwarded := r.Header.Get("Forwarded"); len(forwarded) > 0 {
forwardedHeader, _, err := parseForwardedHeader(forwarded)
if err == nil {
if fproto := forwardedHeader["proto"]; len(fproto) > 0 {
scheme = fproto
}
if fhost := forwardedHeader["host"]; len(fhost) > 0 {
host = fhost
}
}
} else {
if forwardedProto := r.Header.Get("X-Forwarded-Proto"); len(forwardedProto) > 0 {
scheme = forwardedProto
}
if forwardedHost := r.Header.Get("X-Forwarded-Host"); len(forwardedHost) > 0 {
// According to the Apache mod_proxy docs, X-Forwarded-Host can be a
// comma-separated list of hosts, to which each proxy appends the
// requested host. We want to grab the first from this comma-separated
// list.
hosts := strings.SplitN(forwardedHost, ",", 2)
host = strings.TrimSpace(hosts[0])
}
}
basePath := v2.RouteDescriptors[v2.RouteNameBase].Path
requestPath := r.URL.Path
index := strings.Index(requestPath, basePath)
u := &url.URL{
Scheme: scheme,
Host: host,
}
if index > 0 {
// N.B. index+1 is important because we want to include the trailing /
u.Path = requestPath[0 : index+1]
}
return NewBuilder(u, relative)
}
// BuildBaseURL constructs a base url for the API, typically just "/v2/".
func (ub *Builder) BuildBaseURL() (string, error) {
route := ub.cloneDistributionRoute(v2.RouteNameBase)
baseURL, err := route.URL()
if err != nil {
return "", err
}
return baseURL.String(), nil
}
// BuildCatalogURL constructs a url get a catalog of repositories
func (ub *Builder) BuildCatalogURL(values ...url.Values) (string, error) {
route := ub.cloneDistributionRoute(v2.RouteNameCatalog)
catalogURL, err := route.URL()
if err != nil {
return "", err
}
return appendValuesURL(catalogURL, values...).String(), nil
}
// BuildTagsURL constructs a url to list the tags in the named repository.
func (ub *Builder) BuildTagsURL(name reference.Named, values ...url.Values) (string, error) {
route := ub.cloneDistributionRoute(v2.RouteNameTags)
tagsURL, err := route.URL("name", name.Name())
if err != nil {
return "", err
}
return appendValuesURL(tagsURL, values...).String(), nil
}
// BuildManifestURL constructs a url for the manifest identified by name and
// reference. The argument reference may be either a tag or digest.
func (ub *Builder) BuildManifestURL(ref reference.Named) (string, error) {
route := ub.cloneDistributionRoute(v2.RouteNameManifest)
tagOrDigest := ""
switch v := ref.(type) {
case reference.Tagged:
tagOrDigest = v.Tag()
case reference.Digested:
tagOrDigest = v.Digest().String()
default:
return "", ErrNoTagManifest
}
manifestURL, err := route.URL("name", ref.Name(), "reference", tagOrDigest)
if err != nil {
return "", err
}
return manifestURL.String(), nil
}
// BuildBlobURL constructs the url for the blob identified by name and dgst.
func (ub *Builder) BuildBlobURL(ref reference.Canonical) (string, error) {
route := ub.cloneDistributionRoute(v2.RouteNameBlob)
layerURL, err := route.URL("name", ref.Name(), "digest", ref.Digest().String())
if err != nil {
return "", err
}
return layerURL.String(), nil
}
// BuildBlobUploadURL constructs a url to begin a blob upload in the
// repository identified by name.
func (ub *Builder) BuildBlobUploadURL(name reference.Named, values ...url.Values) (string, error) {
route := ub.cloneDistributionRoute(v2.RouteNameBlobUpload)
uploadURL, err := route.URL("name", name.Name())
if err != nil {
return "", err
}
return appendValuesURL(uploadURL, values...).String(), nil
}
// BuildBlobUploadChunkURL constructs a url for the upload identified by uuid,
// including any url values. This should generally not be used by clients, as
// this url is provided by server implementations during the blob upload
// process.
func (ub *Builder) BuildBlobUploadChunkURL(name reference.Named, uuid string, values ...url.Values) (string, error) {
route := ub.cloneDistributionRoute(v2.RouteNameBlobUploadChunk)
uploadURL, err := route.URL("name", name.Name(), "uuid", uuid)
if err != nil {
return "", err
}
return appendValuesURL(uploadURL, values...).String(), nil
}
// BuildGitlabV1BaseURL constructs a base URL for the Gitlab v1 API.
func (ub *Builder) BuildGitlabV1BaseURL() (string, error) {
route := ub.cloneGitLabRoute(v1.Base)
baseURL, err := route.URL()
if err != nil {
return "", err
}
return baseURL.String(), nil
}
// BuildGitlabV1RepositoryURL constructs a URL for the Gitlab v1 API repository route by name.
func (ub *Builder) BuildGitlabV1RepositoryURL(name reference.Named, values ...url.Values) (string, error) {
route := ub.cloneGitLabRoute(v1.Repositories)
u, err := route.URL("name", name.Name())
if err != nil {
return "", err
}
return appendValuesURL(u, values...).String(), nil
}
// BuildGitlabV1RepositoryTagDetailURL constructs a URL for the Gitlab v1 API repository tag details by tag name.
func (ub *Builder) BuildGitlabV1RepositoryTagDetailURL(name reference.Named, tagName string) (string, error) {
route := ub.cloneGitLabRoute(v1.RepositoryTagDetail)
u, err := route.URL("name", name.Name(), "tagName", tagName)
if err != nil {
return "", err
}
return appendValuesURL(u).String(), nil
}
// BuildGitlabV1RepositoryTagsURL constructs a URL for the Gitlab v1 API repository tags route by name.
func (ub *Builder) BuildGitlabV1RepositoryTagsURL(name reference.Named, values ...url.Values) (string, error) {
route := ub.cloneGitLabRoute(v1.RepositoryTags)
u, err := route.URL("name", name.Name())
if err != nil {
return "", err
}
return appendValuesURL(u, values...).String(), nil
}
// BuildGitlabV1SubRepositoriesURL constructs a URL for the Gitlab v1 API sub-repositories route by name.
func (ub *Builder) BuildGitlabV1SubRepositoriesURL(name reference.Named, values ...url.Values) (string, error) {
route := ub.cloneGitLabRoute(v1.SubRepositories)
u, err := route.URL("name", name.Name())
if err != nil {
return "", err
}
return appendValuesURL(u, values...).String(), nil
}
// BuildGitlabV1StatisticsURL constructs a URL for the Gitlab v1 API statistics route.
func (ub *Builder) BuildGitlabV1StatisticsURL() (string, error) {
route := ub.cloneGitLabRoute(v1.Statistics)
u, err := route.URL()
if err != nil {
return "", err
}
return u.String(), nil
}
// cloneDistributionRoute returns a clone of the named route from the
// distribution router. Routes must be cloned to avoid modifying them during
// url generation.
func (ub *Builder) cloneDistributionRoute(name string) clonedRoute {
route := new(mux.Route)
root := new(url.URL)
*route = *ub.distributionRouter.GetRoute(name) // clone the route
*root = *ub.root
return clonedRoute{Route: route, root: root, relative: ub.relative}
}
// cloneGitLabRoute returns a clone of the named route from the
// distribution router. Routes must be cloned to avoid modifying them during
// url generation.
func (ub *Builder) cloneGitLabRoute(r v1.Route) clonedRoute {
route := new(mux.Route)
root := new(url.URL)
*route = *ub.gitLabRouter.GetRoute(r.Name) // clone the route
*root = *ub.root
return clonedRoute{Route: route, root: root, relative: ub.relative}
}
type clonedRoute struct {
*mux.Route
root *url.URL
relative bool
}
func (cr clonedRoute) URL(pairs ...string) (*url.URL, error) {
routeURL, err := cr.Route.URL(pairs...)
if err != nil {
return nil, err
}
if cr.relative {
return routeURL, nil
}
if routeURL.Scheme == "" && routeURL.User == nil && routeURL.Host == "" {
routeURL.Path = routeURL.Path[1:]
}
resolvedURL := cr.root.ResolveReference(routeURL)
resolvedURL.Scheme = cr.root.Scheme
return resolvedURL, nil
}
// appendValuesURL appends the parameters to the url.
func appendValuesURL(u *url.URL, values ...url.Values) *url.URL {
merged := u.Query()
for _, v := range values {
for k, vv := range v {
merged[k] = append(merged[k], vv...)
}
}
u.RawQuery = merged.Encode()
return u
}