mirror of https://github.com/docker/docs.git
Merge pull request #11478 from dmcgowan/v2-vendored-api
Use v2 api from distribution
This commit is contained in:
commit
563041b5c4
|
@ -495,7 +495,7 @@ func (s *TagStore) pullV2Tag(eng *engine.Engine, r *registry.Session, out io.Wri
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
r, l, err := r.GetV2ImageBlobReader(endpoint, repoInfo.RemoteName, di.digest.Algorithm(), di.digest.Hex(), auth)
|
r, l, err := r.GetV2ImageBlobReader(endpoint, repoInfo.RemoteName, di.digest, auth)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
|
@ -8,10 +8,10 @@ import (
|
||||||
"io/ioutil"
|
"io/ioutil"
|
||||||
"os"
|
"os"
|
||||||
"path"
|
"path"
|
||||||
"strings"
|
|
||||||
"sync"
|
"sync"
|
||||||
|
|
||||||
"github.com/Sirupsen/logrus"
|
"github.com/Sirupsen/logrus"
|
||||||
|
"github.com/docker/distribution/digest"
|
||||||
"github.com/docker/docker/engine"
|
"github.com/docker/docker/engine"
|
||||||
"github.com/docker/docker/image"
|
"github.com/docker/docker/image"
|
||||||
"github.com/docker/docker/pkg/progressreader"
|
"github.com/docker/docker/pkg/progressreader"
|
||||||
|
@ -376,13 +376,13 @@ func (s *TagStore) pushV2Repository(r *registry.Session, localRepo Repository, o
|
||||||
|
|
||||||
var exists bool
|
var exists bool
|
||||||
if len(checksum) > 0 {
|
if len(checksum) > 0 {
|
||||||
sumParts := strings.SplitN(checksum, ":", 2)
|
dgst, err := digest.ParseDigest(checksum)
|
||||||
if len(sumParts) < 2 {
|
if err != nil {
|
||||||
return fmt.Errorf("Invalid checksum: %s", checksum)
|
return fmt.Errorf("Invalid checksum %s: %s", checksum, err)
|
||||||
}
|
}
|
||||||
|
|
||||||
// Call mount blob
|
// Call mount blob
|
||||||
exists, err = r.HeadV2ImageBlob(endpoint, repoInfo.RemoteName, sumParts[0], sumParts[1], auth)
|
exists, err = r.HeadV2ImageBlob(endpoint, repoInfo.RemoteName, dgst, auth)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
out.Write(sf.FormatProgress(stringid.TruncateID(layer.ID), "Image push failed", nil))
|
out.Write(sf.FormatProgress(stringid.TruncateID(layer.ID), "Image push failed", nil))
|
||||||
return err
|
return err
|
||||||
|
@ -468,7 +468,7 @@ func (s *TagStore) pushV2Image(r *registry.Session, img *image.Image, endpoint *
|
||||||
// Send the layer
|
// Send the layer
|
||||||
logrus.Debugf("rendered layer for %s of [%d] size", img.ID, size)
|
logrus.Debugf("rendered layer for %s of [%d] size", img.ID, size)
|
||||||
|
|
||||||
if err := r.PutV2ImageBlob(endpoint, imageName, dgst.Algorithm(), dgst.Hex(),
|
if err := r.PutV2ImageBlob(endpoint, imageName, dgst,
|
||||||
progressreader.New(progressreader.Config{
|
progressreader.New(progressreader.Config{
|
||||||
In: tf,
|
In: tf,
|
||||||
Out: out,
|
Out: out,
|
||||||
|
|
|
@ -43,7 +43,7 @@ clone git github.com/kr/pty 05017fcccf
|
||||||
|
|
||||||
clone git github.com/gorilla/context 14f550f51a
|
clone git github.com/gorilla/context 14f550f51a
|
||||||
|
|
||||||
clone git github.com/gorilla/mux 136d54f81f
|
clone git github.com/gorilla/mux e444e69cbd
|
||||||
|
|
||||||
clone git github.com/tchap/go-patricia v1.0.1
|
clone git github.com/tchap/go-patricia v1.0.1
|
||||||
|
|
||||||
|
@ -68,12 +68,15 @@ if [ "$1" = '--go' ]; then
|
||||||
mv tmp-tar src/code.google.com/p/go/src/pkg/archive/tar
|
mv tmp-tar src/code.google.com/p/go/src/pkg/archive/tar
|
||||||
fi
|
fi
|
||||||
|
|
||||||
# get digest package from distribution
|
# get distribution packages
|
||||||
clone git github.com/docker/distribution d957768537c5af40e4f4cd96871f7b2bde9e2923
|
clone git github.com/docker/distribution d957768537c5af40e4f4cd96871f7b2bde9e2923
|
||||||
mv src/github.com/docker/distribution/digest tmp-digest
|
mv src/github.com/docker/distribution/digest tmp-digest
|
||||||
|
mv src/github.com/docker/distribution/registry/api tmp-api
|
||||||
rm -rf src/github.com/docker/distribution
|
rm -rf src/github.com/docker/distribution
|
||||||
mkdir -p src/github.com/docker/distribution
|
mkdir -p src/github.com/docker/distribution
|
||||||
mv tmp-digest src/github.com/docker/distribution/digest
|
mv tmp-digest src/github.com/docker/distribution/digest
|
||||||
|
mkdir -p src/github.com/docker/distribution/registry
|
||||||
|
mv tmp-api src/github.com/docker/distribution/registry/api
|
||||||
|
|
||||||
clone git github.com/docker/libcontainer bd8ec36106086f72b66e1be85a81202b93503e44
|
clone git github.com/docker/libcontainer bd8ec36106086f72b66e1be85a81202b93503e44
|
||||||
# see src/github.com/docker/libcontainer/update-vendor.sh which is the "source of truth" for libcontainer deps (just like this file)
|
# see src/github.com/docker/libcontainer/update-vendor.sh which is the "source of truth" for libcontainer deps (just like this file)
|
||||||
|
|
|
@ -11,8 +11,8 @@ import (
|
||||||
"strings"
|
"strings"
|
||||||
|
|
||||||
"github.com/Sirupsen/logrus"
|
"github.com/Sirupsen/logrus"
|
||||||
|
"github.com/docker/distribution/registry/api/v2"
|
||||||
"github.com/docker/docker/pkg/requestdecorator"
|
"github.com/docker/docker/pkg/requestdecorator"
|
||||||
"github.com/docker/docker/registry/v2"
|
|
||||||
)
|
)
|
||||||
|
|
||||||
// for mocking in unit tests
|
// for mocking in unit tests
|
||||||
|
|
|
@ -11,7 +11,7 @@ import (
|
||||||
|
|
||||||
"github.com/Sirupsen/logrus"
|
"github.com/Sirupsen/logrus"
|
||||||
"github.com/docker/distribution/digest"
|
"github.com/docker/distribution/digest"
|
||||||
"github.com/docker/docker/registry/v2"
|
"github.com/docker/distribution/registry/api/v2"
|
||||||
"github.com/docker/docker/utils"
|
"github.com/docker/docker/utils"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
@ -109,8 +109,8 @@ func (r *Session) GetV2ImageManifest(ep *Endpoint, imageName, tagName string, au
|
||||||
// - Succeeded to head image blob (already exists)
|
// - Succeeded to head image blob (already exists)
|
||||||
// - Failed with no error (continue to Push the Blob)
|
// - Failed with no error (continue to Push the Blob)
|
||||||
// - Failed with error
|
// - Failed with error
|
||||||
func (r *Session) HeadV2ImageBlob(ep *Endpoint, imageName, sumType, sum string, auth *RequestAuthorization) (bool, error) {
|
func (r *Session) HeadV2ImageBlob(ep *Endpoint, imageName string, dgst digest.Digest, auth *RequestAuthorization) (bool, error) {
|
||||||
routeURL, err := getV2Builder(ep).BuildBlobURL(imageName, sumType+":"+sum)
|
routeURL, err := getV2Builder(ep).BuildBlobURL(imageName, dgst)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return false, err
|
return false, err
|
||||||
}
|
}
|
||||||
|
@ -141,11 +141,11 @@ func (r *Session) HeadV2ImageBlob(ep *Endpoint, imageName, sumType, sum string,
|
||||||
return false, nil
|
return false, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
return false, utils.NewHTTPRequestError(fmt.Sprintf("Server error: %d trying head request for %s - %s:%s", res.StatusCode, imageName, sumType, sum), res)
|
return false, utils.NewHTTPRequestError(fmt.Sprintf("Server error: %d trying head request for %s - %s", res.StatusCode, imageName, dgst), res)
|
||||||
}
|
}
|
||||||
|
|
||||||
func (r *Session) GetV2ImageBlob(ep *Endpoint, imageName, sumType, sum string, blobWrtr io.Writer, auth *RequestAuthorization) error {
|
func (r *Session) GetV2ImageBlob(ep *Endpoint, imageName string, dgst digest.Digest, blobWrtr io.Writer, auth *RequestAuthorization) error {
|
||||||
routeURL, err := getV2Builder(ep).BuildBlobURL(imageName, sumType+":"+sum)
|
routeURL, err := getV2Builder(ep).BuildBlobURL(imageName, dgst)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
@ -175,8 +175,8 @@ func (r *Session) GetV2ImageBlob(ep *Endpoint, imageName, sumType, sum string, b
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
func (r *Session) GetV2ImageBlobReader(ep *Endpoint, imageName, sumType, sum string, auth *RequestAuthorization) (io.ReadCloser, int64, error) {
|
func (r *Session) GetV2ImageBlobReader(ep *Endpoint, imageName string, dgst digest.Digest, auth *RequestAuthorization) (io.ReadCloser, int64, error) {
|
||||||
routeURL, err := getV2Builder(ep).BuildBlobURL(imageName, sumType+":"+sum)
|
routeURL, err := getV2Builder(ep).BuildBlobURL(imageName, dgst)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, 0, err
|
return nil, 0, err
|
||||||
}
|
}
|
||||||
|
@ -198,7 +198,7 @@ func (r *Session) GetV2ImageBlobReader(ep *Endpoint, imageName, sumType, sum str
|
||||||
if res.StatusCode == 401 {
|
if res.StatusCode == 401 {
|
||||||
return nil, 0, errLoginRequired
|
return nil, 0, errLoginRequired
|
||||||
}
|
}
|
||||||
return nil, 0, utils.NewHTTPRequestError(fmt.Sprintf("Server error: %d trying to pull %s blob - %s:%s", res.StatusCode, imageName, sumType, sum), res)
|
return nil, 0, utils.NewHTTPRequestError(fmt.Sprintf("Server error: %d trying to pull %s blob - %s", res.StatusCode, imageName, dgst), res)
|
||||||
}
|
}
|
||||||
lenStr := res.Header.Get("Content-Length")
|
lenStr := res.Header.Get("Content-Length")
|
||||||
l, err := strconv.ParseInt(lenStr, 10, 64)
|
l, err := strconv.ParseInt(lenStr, 10, 64)
|
||||||
|
@ -212,7 +212,7 @@ func (r *Session) GetV2ImageBlobReader(ep *Endpoint, imageName, sumType, sum str
|
||||||
// Push the image to the server for storage.
|
// Push the image to the server for storage.
|
||||||
// 'layer' is an uncompressed reader of the blob to be pushed.
|
// 'layer' is an uncompressed reader of the blob to be pushed.
|
||||||
// The server will generate it's own checksum calculation.
|
// The server will generate it's own checksum calculation.
|
||||||
func (r *Session) PutV2ImageBlob(ep *Endpoint, imageName, sumType, sumStr string, blobRdr io.Reader, auth *RequestAuthorization) error {
|
func (r *Session) PutV2ImageBlob(ep *Endpoint, imageName string, dgst digest.Digest, blobRdr io.Reader, auth *RequestAuthorization) error {
|
||||||
location, err := r.initiateBlobUpload(ep, imageName, auth)
|
location, err := r.initiateBlobUpload(ep, imageName, auth)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
|
@ -225,7 +225,7 @@ func (r *Session) PutV2ImageBlob(ep *Endpoint, imageName, sumType, sumStr string
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
queryParams := req.URL.Query()
|
queryParams := req.URL.Query()
|
||||||
queryParams.Add("digest", sumType+":"+sumStr)
|
queryParams.Add("digest", dgst.String())
|
||||||
req.URL.RawQuery = queryParams.Encode()
|
req.URL.RawQuery = queryParams.Encode()
|
||||||
if err := auth.Authorize(req); err != nil {
|
if err := auth.Authorize(req); err != nil {
|
||||||
return err
|
return err
|
||||||
|
@ -245,7 +245,7 @@ func (r *Session) PutV2ImageBlob(ep *Endpoint, imageName, sumType, sumStr string
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
logrus.Debugf("Unexpected response from server: %q %#v", errBody, res.Header)
|
logrus.Debugf("Unexpected response from server: %q %#v", errBody, res.Header)
|
||||||
return utils.NewHTTPRequestError(fmt.Sprintf("Server error: %d trying to push %s blob - %s:%s", res.StatusCode, imageName, sumType, sumStr), res)
|
return utils.NewHTTPRequestError(fmt.Sprintf("Server error: %d trying to push %s blob - %s", res.StatusCode, imageName, dgst), res)
|
||||||
}
|
}
|
||||||
|
|
||||||
return nil
|
return nil
|
||||||
|
|
|
@ -1,144 +0,0 @@
|
||||||
package v2
|
|
||||||
|
|
||||||
import "net/http"
|
|
||||||
|
|
||||||
// TODO(stevvooe): Add route descriptors for each named route, along with
|
|
||||||
// accepted methods, parameters, returned status codes and error codes.
|
|
||||||
|
|
||||||
// ErrorDescriptor provides relevant information about a given error code.
|
|
||||||
type ErrorDescriptor struct {
|
|
||||||
// Code is the error code that this descriptor describes.
|
|
||||||
Code ErrorCode
|
|
||||||
|
|
||||||
// Value provides a unique, string key, often captilized with
|
|
||||||
// underscores, to identify the error code. This value is used as the
|
|
||||||
// keyed value when serializing api errors.
|
|
||||||
Value string
|
|
||||||
|
|
||||||
// Message is a short, human readable decription of the error condition
|
|
||||||
// included in API responses.
|
|
||||||
Message string
|
|
||||||
|
|
||||||
// Description provides a complete account of the errors purpose, suitable
|
|
||||||
// for use in documentation.
|
|
||||||
Description string
|
|
||||||
|
|
||||||
// HTTPStatusCodes provides a list of status under which this error
|
|
||||||
// condition may arise. If it is empty, the error condition may be seen
|
|
||||||
// for any status code.
|
|
||||||
HTTPStatusCodes []int
|
|
||||||
}
|
|
||||||
|
|
||||||
// ErrorDescriptors provides a list of HTTP API Error codes that may be
|
|
||||||
// encountered when interacting with the registry API.
|
|
||||||
var ErrorDescriptors = []ErrorDescriptor{
|
|
||||||
{
|
|
||||||
Code: ErrorCodeUnknown,
|
|
||||||
Value: "UNKNOWN",
|
|
||||||
Message: "unknown error",
|
|
||||||
Description: `Generic error returned when the error does not have an
|
|
||||||
API classification.`,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
Code: ErrorCodeDigestInvalid,
|
|
||||||
Value: "DIGEST_INVALID",
|
|
||||||
Message: "provided digest did not match uploaded content",
|
|
||||||
Description: `When a blob is uploaded, the registry will check that
|
|
||||||
the content matches the digest provided by the client. The error may
|
|
||||||
include a detail structure with the key "digest", including the
|
|
||||||
invalid digest string. This error may also be returned when a manifest
|
|
||||||
includes an invalid layer digest.`,
|
|
||||||
HTTPStatusCodes: []int{http.StatusBadRequest, http.StatusNotFound},
|
|
||||||
},
|
|
||||||
{
|
|
||||||
Code: ErrorCodeSizeInvalid,
|
|
||||||
Value: "SIZE_INVALID",
|
|
||||||
Message: "provided length did not match content length",
|
|
||||||
Description: `When a layer is uploaded, the provided size will be
|
|
||||||
checked against the uploaded content. If they do not match, this error
|
|
||||||
will be returned.`,
|
|
||||||
HTTPStatusCodes: []int{http.StatusBadRequest},
|
|
||||||
},
|
|
||||||
{
|
|
||||||
Code: ErrorCodeNameInvalid,
|
|
||||||
Value: "NAME_INVALID",
|
|
||||||
Message: "manifest name did not match URI",
|
|
||||||
Description: `During a manifest upload, if the name in the manifest
|
|
||||||
does not match the uri name, this error will be returned.`,
|
|
||||||
HTTPStatusCodes: []int{http.StatusBadRequest, http.StatusNotFound},
|
|
||||||
},
|
|
||||||
{
|
|
||||||
Code: ErrorCodeTagInvalid,
|
|
||||||
Value: "TAG_INVALID",
|
|
||||||
Message: "manifest tag did not match URI",
|
|
||||||
Description: `During a manifest upload, if the tag in the manifest
|
|
||||||
does not match the uri tag, this error will be returned.`,
|
|
||||||
HTTPStatusCodes: []int{http.StatusBadRequest, http.StatusNotFound},
|
|
||||||
},
|
|
||||||
{
|
|
||||||
Code: ErrorCodeNameUnknown,
|
|
||||||
Value: "NAME_UNKNOWN",
|
|
||||||
Message: "repository name not known to registry",
|
|
||||||
Description: `This is returned if the name used during an operation is
|
|
||||||
unknown to the registry.`,
|
|
||||||
HTTPStatusCodes: []int{http.StatusNotFound},
|
|
||||||
},
|
|
||||||
{
|
|
||||||
Code: ErrorCodeManifestUnknown,
|
|
||||||
Value: "MANIFEST_UNKNOWN",
|
|
||||||
Message: "manifest unknown",
|
|
||||||
Description: `This error is returned when the manifest, identified by
|
|
||||||
name and tag is unknown to the repository.`,
|
|
||||||
HTTPStatusCodes: []int{http.StatusNotFound},
|
|
||||||
},
|
|
||||||
{
|
|
||||||
Code: ErrorCodeManifestInvalid,
|
|
||||||
Value: "MANIFEST_INVALID",
|
|
||||||
Message: "manifest invalid",
|
|
||||||
Description: `During upload, manifests undergo several checks ensuring
|
|
||||||
validity. If those checks fail, this error may be returned, unless a
|
|
||||||
more specific error is included. The detail will contain information
|
|
||||||
the failed validation.`,
|
|
||||||
HTTPStatusCodes: []int{http.StatusBadRequest},
|
|
||||||
},
|
|
||||||
{
|
|
||||||
Code: ErrorCodeManifestUnverified,
|
|
||||||
Value: "MANIFEST_UNVERIFIED",
|
|
||||||
Message: "manifest failed signature verification",
|
|
||||||
Description: `During manifest upload, if the manifest fails signature
|
|
||||||
verification, this error will be returned.`,
|
|
||||||
HTTPStatusCodes: []int{http.StatusBadRequest},
|
|
||||||
},
|
|
||||||
{
|
|
||||||
Code: ErrorCodeBlobUnknown,
|
|
||||||
Value: "BLOB_UNKNOWN",
|
|
||||||
Message: "blob unknown to registry",
|
|
||||||
Description: `This error may be returned when a blob is unknown to the
|
|
||||||
registry in a specified repository. This can be returned with a
|
|
||||||
standard get or if a manifest references an unknown layer during
|
|
||||||
upload.`,
|
|
||||||
HTTPStatusCodes: []int{http.StatusBadRequest, http.StatusNotFound},
|
|
||||||
},
|
|
||||||
|
|
||||||
{
|
|
||||||
Code: ErrorCodeBlobUploadUnknown,
|
|
||||||
Value: "BLOB_UPLOAD_UNKNOWN",
|
|
||||||
Message: "blob upload unknown to registry",
|
|
||||||
Description: `If a blob upload has been cancelled or was never
|
|
||||||
started, this error code may be returned.`,
|
|
||||||
HTTPStatusCodes: []int{http.StatusNotFound},
|
|
||||||
},
|
|
||||||
}
|
|
||||||
|
|
||||||
var errorCodeToDescriptors map[ErrorCode]ErrorDescriptor
|
|
||||||
var idToDescriptors map[string]ErrorDescriptor
|
|
||||||
|
|
||||||
func init() {
|
|
||||||
errorCodeToDescriptors = make(map[ErrorCode]ErrorDescriptor, len(ErrorDescriptors))
|
|
||||||
idToDescriptors = make(map[string]ErrorDescriptor, len(ErrorDescriptors))
|
|
||||||
|
|
||||||
for _, descriptor := range ErrorDescriptors {
|
|
||||||
errorCodeToDescriptors[descriptor.Code] = descriptor
|
|
||||||
idToDescriptors[descriptor.Value] = descriptor
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -1,22 +0,0 @@
|
||||||
package v2
|
|
||||||
|
|
||||||
import "regexp"
|
|
||||||
|
|
||||||
// This file defines regular expressions for use in route definition. These
|
|
||||||
// are also defined in the registry code base. Until they are in a common,
|
|
||||||
// shared location, and exported, they must be repeated here.
|
|
||||||
|
|
||||||
// RepositoryNameComponentRegexp restricts registtry path components names to
|
|
||||||
// start with at least two letters or numbers, with following parts able to
|
|
||||||
// separated by one period, dash or underscore.
|
|
||||||
var RepositoryNameComponentRegexp = regexp.MustCompile(`[a-z0-9]+(?:[._-][a-z0-9]+)*`)
|
|
||||||
|
|
||||||
// RepositoryNameRegexp builds on RepositoryNameComponentRegexp to allow 1 to
|
|
||||||
// 5 path components, separated by a forward slash.
|
|
||||||
var RepositoryNameRegexp = regexp.MustCompile(`(?:` + RepositoryNameComponentRegexp.String() + `/){0,4}` + RepositoryNameComponentRegexp.String())
|
|
||||||
|
|
||||||
// TagNameRegexp matches valid tag names. From docker/docker:graph/tags.go.
|
|
||||||
var TagNameRegexp = regexp.MustCompile(`[\w][\w.-]{0,127}`)
|
|
||||||
|
|
||||||
// DigestRegexp matches valid digest types.
|
|
||||||
var DigestRegexp = regexp.MustCompile(`[a-zA-Z0-9-_+.]+:[a-zA-Z0-9-_+.=]+`)
|
|
|
@ -1,66 +0,0 @@
|
||||||
package v2
|
|
||||||
|
|
||||||
import "github.com/gorilla/mux"
|
|
||||||
|
|
||||||
// The following are definitions of the name under which all V2 routes are
|
|
||||||
// registered. These symbols can be used to look up a route based on the name.
|
|
||||||
const (
|
|
||||||
RouteNameBase = "base"
|
|
||||||
RouteNameManifest = "manifest"
|
|
||||||
RouteNameTags = "tags"
|
|
||||||
RouteNameBlob = "blob"
|
|
||||||
RouteNameBlobUpload = "blob-upload"
|
|
||||||
RouteNameBlobUploadChunk = "blob-upload-chunk"
|
|
||||||
)
|
|
||||||
|
|
||||||
var allEndpoints = []string{
|
|
||||||
RouteNameManifest,
|
|
||||||
RouteNameTags,
|
|
||||||
RouteNameBlob,
|
|
||||||
RouteNameBlobUpload,
|
|
||||||
RouteNameBlobUploadChunk,
|
|
||||||
}
|
|
||||||
|
|
||||||
// Router builds a gorilla router with named routes for the various API
|
|
||||||
// methods. This can be used directly by both server implementations and
|
|
||||||
// clients.
|
|
||||||
func Router() *mux.Router {
|
|
||||||
router := mux.NewRouter().
|
|
||||||
StrictSlash(true)
|
|
||||||
|
|
||||||
// GET /v2/ Check Check that the registry implements API version 2(.1)
|
|
||||||
router.
|
|
||||||
Path("/v2/").
|
|
||||||
Name(RouteNameBase)
|
|
||||||
|
|
||||||
// GET /v2/<name>/manifest/<reference> Image Manifest Fetch the image manifest identified by name and reference where reference can be a tag or digest.
|
|
||||||
// PUT /v2/<name>/manifest/<reference> Image Manifest Upload the image manifest identified by name and reference where reference can be a tag or digest.
|
|
||||||
// DELETE /v2/<name>/manifest/<reference> Image Manifest Delete the image identified by name and reference where reference can be a tag or digest.
|
|
||||||
router.
|
|
||||||
Path("/v2/{name:" + RepositoryNameRegexp.String() + "}/manifests/{reference:" + TagNameRegexp.String() + "|" + DigestRegexp.String() + "}").
|
|
||||||
Name(RouteNameManifest)
|
|
||||||
|
|
||||||
// GET /v2/<name>/tags/list Tags Fetch the tags under the repository identified by name.
|
|
||||||
router.
|
|
||||||
Path("/v2/{name:" + RepositoryNameRegexp.String() + "}/tags/list").
|
|
||||||
Name(RouteNameTags)
|
|
||||||
|
|
||||||
// GET /v2/<name>/blob/<digest> Layer Fetch the blob identified by digest.
|
|
||||||
router.
|
|
||||||
Path("/v2/{name:" + RepositoryNameRegexp.String() + "}/blobs/{digest:[a-zA-Z0-9-_+.]+:[a-zA-Z0-9-_+.=]+}").
|
|
||||||
Name(RouteNameBlob)
|
|
||||||
|
|
||||||
// POST /v2/<name>/blob/upload/ Layer Upload Initiate an upload of the layer identified by tarsum.
|
|
||||||
router.
|
|
||||||
Path("/v2/{name:" + RepositoryNameRegexp.String() + "}/blobs/uploads/").
|
|
||||||
Name(RouteNameBlobUpload)
|
|
||||||
|
|
||||||
// GET /v2/<name>/blob/upload/<uuid> Layer Upload Get the status of the upload identified by tarsum and uuid.
|
|
||||||
// PUT /v2/<name>/blob/upload/<uuid> Layer Upload Upload all or a chunk of the upload identified by tarsum and uuid.
|
|
||||||
// DELETE /v2/<name>/blob/upload/<uuid> Layer Upload Cancel the upload identified by layer and uuid
|
|
||||||
router.
|
|
||||||
Path("/v2/{name:" + RepositoryNameRegexp.String() + "}/blobs/uploads/{uuid}").
|
|
||||||
Name(RouteNameBlobUploadChunk)
|
|
||||||
|
|
||||||
return router
|
|
||||||
}
|
|
|
@ -1,113 +0,0 @@
|
||||||
package v2
|
|
||||||
|
|
||||||
import (
|
|
||||||
"net/url"
|
|
||||||
"testing"
|
|
||||||
)
|
|
||||||
|
|
||||||
type urlBuilderTestCase struct {
|
|
||||||
description string
|
|
||||||
expectedPath string
|
|
||||||
build func() (string, error)
|
|
||||||
}
|
|
||||||
|
|
||||||
// TestURLBuilder tests the various url building functions, ensuring they are
|
|
||||||
// returning the expected values.
|
|
||||||
func TestURLBuilder(t *testing.T) {
|
|
||||||
var (
|
|
||||||
urlBuilder *URLBuilder
|
|
||||||
err error
|
|
||||||
)
|
|
||||||
|
|
||||||
testCases := []urlBuilderTestCase{
|
|
||||||
{
|
|
||||||
description: "test base url",
|
|
||||||
expectedPath: "/v2/",
|
|
||||||
build: func() (string, error) {
|
|
||||||
return urlBuilder.BuildBaseURL()
|
|
||||||
},
|
|
||||||
},
|
|
||||||
{
|
|
||||||
description: "test tags url",
|
|
||||||
expectedPath: "/v2/foo/bar/tags/list",
|
|
||||||
build: func() (string, error) {
|
|
||||||
return urlBuilder.BuildTagsURL("foo/bar")
|
|
||||||
},
|
|
||||||
},
|
|
||||||
{
|
|
||||||
description: "test manifest url",
|
|
||||||
expectedPath: "/v2/foo/bar/manifests/tag",
|
|
||||||
build: func() (string, error) {
|
|
||||||
return urlBuilder.BuildManifestURL("foo/bar", "tag")
|
|
||||||
},
|
|
||||||
},
|
|
||||||
{
|
|
||||||
description: "build blob url",
|
|
||||||
expectedPath: "/v2/foo/bar/blobs/tarsum.v1+sha256:abcdef0123456789",
|
|
||||||
build: func() (string, error) {
|
|
||||||
return urlBuilder.BuildBlobURL("foo/bar", "tarsum.v1+sha256:abcdef0123456789")
|
|
||||||
},
|
|
||||||
},
|
|
||||||
{
|
|
||||||
description: "build blob upload url",
|
|
||||||
expectedPath: "/v2/foo/bar/blobs/uploads/",
|
|
||||||
build: func() (string, error) {
|
|
||||||
return urlBuilder.BuildBlobUploadURL("foo/bar")
|
|
||||||
},
|
|
||||||
},
|
|
||||||
{
|
|
||||||
description: "build blob upload url with digest and size",
|
|
||||||
expectedPath: "/v2/foo/bar/blobs/uploads/?digest=tarsum.v1%2Bsha256%3Aabcdef0123456789&size=10000",
|
|
||||||
build: func() (string, error) {
|
|
||||||
return urlBuilder.BuildBlobUploadURL("foo/bar", url.Values{
|
|
||||||
"size": []string{"10000"},
|
|
||||||
"digest": []string{"tarsum.v1+sha256:abcdef0123456789"},
|
|
||||||
})
|
|
||||||
},
|
|
||||||
},
|
|
||||||
{
|
|
||||||
description: "build blob upload chunk url",
|
|
||||||
expectedPath: "/v2/foo/bar/blobs/uploads/uuid-part",
|
|
||||||
build: func() (string, error) {
|
|
||||||
return urlBuilder.BuildBlobUploadChunkURL("foo/bar", "uuid-part")
|
|
||||||
},
|
|
||||||
},
|
|
||||||
{
|
|
||||||
description: "build blob upload chunk url with digest and size",
|
|
||||||
expectedPath: "/v2/foo/bar/blobs/uploads/uuid-part?digest=tarsum.v1%2Bsha256%3Aabcdef0123456789&size=10000",
|
|
||||||
build: func() (string, error) {
|
|
||||||
return urlBuilder.BuildBlobUploadChunkURL("foo/bar", "uuid-part", url.Values{
|
|
||||||
"size": []string{"10000"},
|
|
||||||
"digest": []string{"tarsum.v1+sha256:abcdef0123456789"},
|
|
||||||
})
|
|
||||||
},
|
|
||||||
},
|
|
||||||
}
|
|
||||||
|
|
||||||
roots := []string{
|
|
||||||
"http://example.com",
|
|
||||||
"https://example.com",
|
|
||||||
"http://localhost:5000",
|
|
||||||
"https://localhost:5443",
|
|
||||||
}
|
|
||||||
|
|
||||||
for _, root := range roots {
|
|
||||||
urlBuilder, err = NewURLBuilderFromString(root)
|
|
||||||
if err != nil {
|
|
||||||
t.Fatalf("unexpected error creating urlbuilder: %v", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
for _, testCase := range testCases {
|
|
||||||
url, err := testCase.build()
|
|
||||||
if err != nil {
|
|
||||||
t.Fatalf("%s: error building url: %v", testCase.description, err)
|
|
||||||
}
|
|
||||||
|
|
||||||
expectedURL := root + testCase.expectedPath
|
|
||||||
|
|
||||||
if url != expectedURL {
|
|
||||||
t.Fatalf("%s: %q != %q", testCase.description, url, expectedURL)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
File diff suppressed because it is too large
Load Diff
|
@ -5,9 +5,5 @@
|
||||||
//
|
//
|
||||||
// Definitions here are considered to be locked down for the V2 registry api.
|
// Definitions here are considered to be locked down for the V2 registry api.
|
||||||
// Any changes must be considered carefully and should not proceed without a
|
// Any changes must be considered carefully and should not proceed without a
|
||||||
// change proposal.
|
// change proposal in docker core.
|
||||||
//
|
|
||||||
// Currently, while the HTTP API definitions are considered stable, the Go API
|
|
||||||
// exports are considered unstable. Go API consumers should take care when
|
|
||||||
// relying on these definitions until this message is deleted.
|
|
||||||
package v2
|
package v2
|
|
@ -13,6 +13,12 @@ const (
|
||||||
// ErrorCodeUnknown is a catch-all for errors not defined below.
|
// ErrorCodeUnknown is a catch-all for errors not defined below.
|
||||||
ErrorCodeUnknown ErrorCode = iota
|
ErrorCodeUnknown ErrorCode = iota
|
||||||
|
|
||||||
|
// ErrorCodeUnsupported is returned when an operation is not supported.
|
||||||
|
ErrorCodeUnsupported
|
||||||
|
|
||||||
|
// ErrorCodeUnauthorized is returned if a request is not authorized.
|
||||||
|
ErrorCodeUnauthorized
|
||||||
|
|
||||||
// ErrorCodeDigestInvalid is returned when uploading a blob if the
|
// ErrorCodeDigestInvalid is returned when uploading a blob if the
|
||||||
// provided digest does not match the blob contents.
|
// provided digest does not match the blob contents.
|
||||||
ErrorCodeDigestInvalid
|
ErrorCodeDigestInvalid
|
||||||
|
@ -51,6 +57,9 @@ const (
|
||||||
|
|
||||||
// ErrorCodeBlobUploadUnknown is returned when an upload is unknown.
|
// ErrorCodeBlobUploadUnknown is returned when an upload is unknown.
|
||||||
ErrorCodeBlobUploadUnknown
|
ErrorCodeBlobUploadUnknown
|
||||||
|
|
||||||
|
// ErrorCodeBlobUploadInvalid is returned when an upload is invalid.
|
||||||
|
ErrorCodeBlobUploadInvalid
|
||||||
)
|
)
|
||||||
|
|
||||||
// ParseErrorCode attempts to parse the error code string, returning
|
// ParseErrorCode attempts to parse the error code string, returning
|
|
@ -4,12 +4,14 @@ import (
|
||||||
"encoding/json"
|
"encoding/json"
|
||||||
"reflect"
|
"reflect"
|
||||||
"testing"
|
"testing"
|
||||||
|
|
||||||
|
"github.com/docker/distribution/digest"
|
||||||
)
|
)
|
||||||
|
|
||||||
// TestErrorCodes ensures that error code format, mappings and
|
// TestErrorCodes ensures that error code format, mappings and
|
||||||
// marshaling/unmarshaling. round trips are stable.
|
// marshaling/unmarshaling. round trips are stable.
|
||||||
func TestErrorCodes(t *testing.T) {
|
func TestErrorCodes(t *testing.T) {
|
||||||
for _, desc := range ErrorDescriptors {
|
for _, desc := range errorDescriptors {
|
||||||
if desc.Code.String() != desc.Value {
|
if desc.Code.String() != desc.Value {
|
||||||
t.Fatalf("error code string incorrect: %q != %q", desc.Code.String(), desc.Value)
|
t.Fatalf("error code string incorrect: %q != %q", desc.Code.String(), desc.Value)
|
||||||
}
|
}
|
||||||
|
@ -59,7 +61,7 @@ func TestErrorsManagement(t *testing.T) {
|
||||||
|
|
||||||
errs.Push(ErrorCodeDigestInvalid)
|
errs.Push(ErrorCodeDigestInvalid)
|
||||||
errs.Push(ErrorCodeBlobUnknown,
|
errs.Push(ErrorCodeBlobUnknown,
|
||||||
map[string]string{"digest": "sometestblobsumdoesntmatter"})
|
map[string]digest.Digest{"digest": "sometestblobsumdoesntmatter"})
|
||||||
|
|
||||||
p, err := json.Marshal(errs)
|
p, err := json.Marshal(errs)
|
||||||
|
|
|
@ -0,0 +1,100 @@
|
||||||
|
package v2
|
||||||
|
|
||||||
|
import (
|
||||||
|
"fmt"
|
||||||
|
"regexp"
|
||||||
|
"strings"
|
||||||
|
)
|
||||||
|
|
||||||
|
// TODO(stevvooe): Move these definitions back to an exported package. While
|
||||||
|
// they are used with v2 definitions, their relevance expands beyond.
|
||||||
|
// "distribution/names" is a candidate package.
|
||||||
|
|
||||||
|
const (
|
||||||
|
// RepositoryNameComponentMinLength is the minimum number of characters in a
|
||||||
|
// single repository name slash-delimited component
|
||||||
|
RepositoryNameComponentMinLength = 2
|
||||||
|
|
||||||
|
// RepositoryNameMinComponents is the minimum number of slash-delimited
|
||||||
|
// components that a repository name must have
|
||||||
|
RepositoryNameMinComponents = 1
|
||||||
|
|
||||||
|
// RepositoryNameTotalLengthMax is the maximum total number of characters in
|
||||||
|
// a repository name
|
||||||
|
RepositoryNameTotalLengthMax = 255
|
||||||
|
)
|
||||||
|
|
||||||
|
// RepositoryNameComponentRegexp restricts registry path component names to
|
||||||
|
// start with at least one letter or number, with following parts able to
|
||||||
|
// be separated by one period, dash or underscore.
|
||||||
|
var RepositoryNameComponentRegexp = regexp.MustCompile(`[a-z0-9]+(?:[._-][a-z0-9]+)*`)
|
||||||
|
|
||||||
|
// RepositoryNameComponentAnchoredRegexp is the version of
|
||||||
|
// RepositoryNameComponentRegexp which must completely match the content
|
||||||
|
var RepositoryNameComponentAnchoredRegexp = regexp.MustCompile(`^` + RepositoryNameComponentRegexp.String() + `$`)
|
||||||
|
|
||||||
|
// RepositoryNameRegexp builds on RepositoryNameComponentRegexp to allow
|
||||||
|
// multiple path components, separated by a forward slash.
|
||||||
|
var RepositoryNameRegexp = regexp.MustCompile(`(?:` + RepositoryNameComponentRegexp.String() + `/)*` + RepositoryNameComponentRegexp.String())
|
||||||
|
|
||||||
|
// TagNameRegexp matches valid tag names. From docker/docker:graph/tags.go.
|
||||||
|
var TagNameRegexp = regexp.MustCompile(`[\w][\w.-]{0,127}`)
|
||||||
|
|
||||||
|
// TODO(stevvooe): Contribute these exports back to core, so they are shared.
|
||||||
|
|
||||||
|
var (
|
||||||
|
// ErrRepositoryNameComponentShort is returned when a repository name
|
||||||
|
// contains a component which is shorter than
|
||||||
|
// RepositoryNameComponentMinLength
|
||||||
|
ErrRepositoryNameComponentShort = fmt.Errorf("respository name component must be %v or more characters", RepositoryNameComponentMinLength)
|
||||||
|
|
||||||
|
// ErrRepositoryNameMissingComponents is returned when a repository name
|
||||||
|
// contains fewer than RepositoryNameMinComponents components
|
||||||
|
ErrRepositoryNameMissingComponents = fmt.Errorf("repository name must have at least %v components", RepositoryNameMinComponents)
|
||||||
|
|
||||||
|
// ErrRepositoryNameLong is returned when a repository name is longer than
|
||||||
|
// RepositoryNameTotalLengthMax
|
||||||
|
ErrRepositoryNameLong = fmt.Errorf("repository name must not be more than %v characters", RepositoryNameTotalLengthMax)
|
||||||
|
|
||||||
|
// ErrRepositoryNameComponentInvalid is returned when a repository name does
|
||||||
|
// not match RepositoryNameComponentRegexp
|
||||||
|
ErrRepositoryNameComponentInvalid = fmt.Errorf("repository name component must match %q", RepositoryNameComponentRegexp.String())
|
||||||
|
)
|
||||||
|
|
||||||
|
// ValidateRespositoryName ensures the repository name is valid for use in the
|
||||||
|
// registry. This function accepts a superset of what might be accepted by
|
||||||
|
// docker core or docker hub. If the name does not pass validation, an error,
|
||||||
|
// describing the conditions, is returned.
|
||||||
|
//
|
||||||
|
// Effectively, the name should comply with the following grammar:
|
||||||
|
//
|
||||||
|
// alpha-numeric := /[a-z0-9]+/
|
||||||
|
// separator := /[._-]/
|
||||||
|
// component := alpha-numeric [separator alpha-numeric]*
|
||||||
|
// namespace := component ['/' component]*
|
||||||
|
//
|
||||||
|
// The result of the production, known as the "namespace", should be limited
|
||||||
|
// to 255 characters.
|
||||||
|
func ValidateRespositoryName(name string) error {
|
||||||
|
if len(name) > RepositoryNameTotalLengthMax {
|
||||||
|
return ErrRepositoryNameLong
|
||||||
|
}
|
||||||
|
|
||||||
|
components := strings.Split(name, "/")
|
||||||
|
|
||||||
|
if len(components) < RepositoryNameMinComponents {
|
||||||
|
return ErrRepositoryNameMissingComponents
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, component := range components {
|
||||||
|
if len(component) < RepositoryNameComponentMinLength {
|
||||||
|
return ErrRepositoryNameComponentShort
|
||||||
|
}
|
||||||
|
|
||||||
|
if !RepositoryNameComponentAnchoredRegexp.MatchString(component) {
|
||||||
|
return ErrRepositoryNameComponentInvalid
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
|
@ -0,0 +1,100 @@
|
||||||
|
package v2
|
||||||
|
|
||||||
|
import (
|
||||||
|
"strings"
|
||||||
|
"testing"
|
||||||
|
)
|
||||||
|
|
||||||
|
func TestRepositoryNameRegexp(t *testing.T) {
|
||||||
|
for _, testcase := range []struct {
|
||||||
|
input string
|
||||||
|
err error
|
||||||
|
}{
|
||||||
|
{
|
||||||
|
input: "short",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
input: "simple/name",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
input: "library/ubuntu",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
input: "docker/stevvooe/app",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
input: "aa/aa/aa/aa/aa/aa/aa/aa/aa/bb/bb/bb/bb/bb/bb",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
input: "aa/aa/bb/bb/bb",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
input: "a/a/a/b/b",
|
||||||
|
err: ErrRepositoryNameComponentShort,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
input: "a/a/a/a/",
|
||||||
|
err: ErrRepositoryNameComponentShort,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
input: "foo.com/bar/baz",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
input: "blog.foo.com/bar/baz",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
input: "asdf",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
input: "asdf$$^/aa",
|
||||||
|
err: ErrRepositoryNameComponentInvalid,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
input: "aa-a/aa",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
input: "aa/aa",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
input: "a-a/a-a",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
input: "a",
|
||||||
|
err: ErrRepositoryNameComponentShort,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
input: "a-/a/a/a",
|
||||||
|
err: ErrRepositoryNameComponentInvalid,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
input: strings.Repeat("a", 255),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
input: strings.Repeat("a", 256),
|
||||||
|
err: ErrRepositoryNameLong,
|
||||||
|
},
|
||||||
|
} {
|
||||||
|
|
||||||
|
failf := func(format string, v ...interface{}) {
|
||||||
|
t.Logf(testcase.input+": "+format, v...)
|
||||||
|
t.Fail()
|
||||||
|
}
|
||||||
|
|
||||||
|
if err := ValidateRespositoryName(testcase.input); err != testcase.err {
|
||||||
|
if testcase.err != nil {
|
||||||
|
if err != nil {
|
||||||
|
failf("unexpected error for invalid repository: got %v, expected %v", err, testcase.err)
|
||||||
|
} else {
|
||||||
|
failf("expected invalid repository: %v", testcase.err)
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
if err != nil {
|
||||||
|
// Wrong error returned.
|
||||||
|
failf("unexpected error validating repository name: %v, expected %v", err, testcase.err)
|
||||||
|
} else {
|
||||||
|
failf("unexpected error validating repository name: %v", err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,47 @@
|
||||||
|
package v2
|
||||||
|
|
||||||
|
import "github.com/gorilla/mux"
|
||||||
|
|
||||||
|
// The following are definitions of the name under which all V2 routes are
|
||||||
|
// registered. These symbols can be used to look up a route based on the name.
|
||||||
|
const (
|
||||||
|
RouteNameBase = "base"
|
||||||
|
RouteNameManifest = "manifest"
|
||||||
|
RouteNameTags = "tags"
|
||||||
|
RouteNameBlob = "blob"
|
||||||
|
RouteNameBlobUpload = "blob-upload"
|
||||||
|
RouteNameBlobUploadChunk = "blob-upload-chunk"
|
||||||
|
)
|
||||||
|
|
||||||
|
var allEndpoints = []string{
|
||||||
|
RouteNameManifest,
|
||||||
|
RouteNameTags,
|
||||||
|
RouteNameBlob,
|
||||||
|
RouteNameBlobUpload,
|
||||||
|
RouteNameBlobUploadChunk,
|
||||||
|
}
|
||||||
|
|
||||||
|
// Router builds a gorilla router with named routes for the various API
|
||||||
|
// methods. This can be used directly by both server implementations and
|
||||||
|
// clients.
|
||||||
|
func Router() *mux.Router {
|
||||||
|
return RouterWithPrefix("")
|
||||||
|
}
|
||||||
|
|
||||||
|
// RouterWithPrefix builds a gorilla router with a configured prefix
|
||||||
|
// on all routes.
|
||||||
|
func RouterWithPrefix(prefix string) *mux.Router {
|
||||||
|
rootRouter := mux.NewRouter()
|
||||||
|
router := rootRouter
|
||||||
|
if prefix != "" {
|
||||||
|
router = router.PathPrefix(prefix).Subrouter()
|
||||||
|
}
|
||||||
|
|
||||||
|
router.StrictSlash(true)
|
||||||
|
|
||||||
|
for _, descriptor := range routeDescriptors {
|
||||||
|
router.Path(descriptor.Path).Name(descriptor.Name)
|
||||||
|
}
|
||||||
|
|
||||||
|
return rootRouter
|
||||||
|
}
|
|
@ -2,19 +2,24 @@ package v2
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"encoding/json"
|
"encoding/json"
|
||||||
|
"fmt"
|
||||||
|
"math/rand"
|
||||||
"net/http"
|
"net/http"
|
||||||
"net/http/httptest"
|
"net/http/httptest"
|
||||||
"reflect"
|
"reflect"
|
||||||
|
"strings"
|
||||||
"testing"
|
"testing"
|
||||||
|
"time"
|
||||||
|
|
||||||
"github.com/gorilla/mux"
|
"github.com/gorilla/mux"
|
||||||
)
|
)
|
||||||
|
|
||||||
type routeTestCase struct {
|
type routeTestCase struct {
|
||||||
RequestURI string
|
RequestURI string
|
||||||
Vars map[string]string
|
ExpectedURI string
|
||||||
RouteName string
|
Vars map[string]string
|
||||||
StatusCode int
|
RouteName string
|
||||||
|
StatusCode int
|
||||||
}
|
}
|
||||||
|
|
||||||
// TestRouter registers a test handler with all the routes and ensures that
|
// TestRouter registers a test handler with all the routes and ensures that
|
||||||
|
@ -24,28 +29,7 @@ type routeTestCase struct {
|
||||||
//
|
//
|
||||||
// This may go away as the application structure comes together.
|
// This may go away as the application structure comes together.
|
||||||
func TestRouter(t *testing.T) {
|
func TestRouter(t *testing.T) {
|
||||||
|
testCases := []routeTestCase{
|
||||||
router := Router()
|
|
||||||
|
|
||||||
testHandler := http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
|
||||||
testCase := routeTestCase{
|
|
||||||
RequestURI: r.RequestURI,
|
|
||||||
Vars: mux.Vars(r),
|
|
||||||
RouteName: mux.CurrentRoute(r).GetName(),
|
|
||||||
}
|
|
||||||
|
|
||||||
enc := json.NewEncoder(w)
|
|
||||||
|
|
||||||
if err := enc.Encode(testCase); err != nil {
|
|
||||||
http.Error(w, err.Error(), http.StatusInternalServerError)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
})
|
|
||||||
|
|
||||||
// Startup test server
|
|
||||||
server := httptest.NewServer(router)
|
|
||||||
|
|
||||||
for _, testcase := range []routeTestCase{
|
|
||||||
{
|
{
|
||||||
RouteName: RouteNameBase,
|
RouteName: RouteNameBase,
|
||||||
RequestURI: "/v2/",
|
RequestURI: "/v2/",
|
||||||
|
@ -67,6 +51,14 @@ func TestRouter(t *testing.T) {
|
||||||
"reference": "tag",
|
"reference": "tag",
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
RouteName: RouteNameManifest,
|
||||||
|
RequestURI: "/v2/foo/bar/manifests/sha256:abcdef01234567890",
|
||||||
|
Vars: map[string]string{
|
||||||
|
"name": "foo/bar",
|
||||||
|
"reference": "sha256:abcdef01234567890",
|
||||||
|
},
|
||||||
|
},
|
||||||
{
|
{
|
||||||
RouteName: RouteNameTags,
|
RouteName: RouteNameTags,
|
||||||
RequestURI: "/v2/foo/bar/tags/list",
|
RequestURI: "/v2/foo/bar/tags/list",
|
||||||
|
@ -141,14 +133,98 @@ func TestRouter(t *testing.T) {
|
||||||
"name": "foo/bar/manifests",
|
"name": "foo/bar/manifests",
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
checkTestRouter(t, testCases, "", true)
|
||||||
|
checkTestRouter(t, testCases, "/prefix/", true)
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestRouterWithPathTraversals(t *testing.T) {
|
||||||
|
testCases := []routeTestCase{
|
||||||
{
|
{
|
||||||
RouteName: RouteNameBlobUploadChunk,
|
RouteName: RouteNameBlobUploadChunk,
|
||||||
RequestURI: "/v2/foo/../../blob/uploads/D95306FA-FAD3-4E36-8D41-CF1C93EF8286",
|
RequestURI: "/v2/foo/../../blob/uploads/D95306FA-FAD3-4E36-8D41-CF1C93EF8286",
|
||||||
StatusCode: http.StatusNotFound,
|
ExpectedURI: "/blob/uploads/D95306FA-FAD3-4E36-8D41-CF1C93EF8286",
|
||||||
|
StatusCode: http.StatusNotFound,
|
||||||
},
|
},
|
||||||
} {
|
{
|
||||||
|
// Testing for path traversal attack handling
|
||||||
|
RouteName: RouteNameTags,
|
||||||
|
RequestURI: "/v2/foo/../bar/baz/tags/list",
|
||||||
|
ExpectedURI: "/v2/bar/baz/tags/list",
|
||||||
|
Vars: map[string]string{
|
||||||
|
"name": "bar/baz",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}
|
||||||
|
checkTestRouter(t, testCases, "", false)
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestRouterWithBadCharacters(t *testing.T) {
|
||||||
|
if testing.Short() {
|
||||||
|
testCases := []routeTestCase{
|
||||||
|
{
|
||||||
|
RouteName: RouteNameBlobUploadChunk,
|
||||||
|
RequestURI: "/v2/foo/blob/uploads/不95306FA-FAD3-4E36-8D41-CF1C93EF8286",
|
||||||
|
StatusCode: http.StatusNotFound,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
// Testing for path traversal attack handling
|
||||||
|
RouteName: RouteNameTags,
|
||||||
|
RequestURI: "/v2/foo/不bar/tags/list",
|
||||||
|
StatusCode: http.StatusNotFound,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
checkTestRouter(t, testCases, "", true)
|
||||||
|
} else {
|
||||||
|
// in the long version we're going to fuzz the router
|
||||||
|
// with random UTF8 characters not in the 128 bit ASCII range.
|
||||||
|
// These are not valid characters for the router and we expect
|
||||||
|
// 404s on every test.
|
||||||
|
rand.Seed(time.Now().UTC().UnixNano())
|
||||||
|
testCases := make([]routeTestCase, 1000)
|
||||||
|
for idx := range testCases {
|
||||||
|
testCases[idx] = routeTestCase{
|
||||||
|
RouteName: RouteNameTags,
|
||||||
|
RequestURI: fmt.Sprintf("/v2/%v/%v/tags/list", randomString(10), randomString(10)),
|
||||||
|
StatusCode: http.StatusNotFound,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
checkTestRouter(t, testCases, "", true)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func checkTestRouter(t *testing.T, testCases []routeTestCase, prefix string, deeplyEqual bool) {
|
||||||
|
router := RouterWithPrefix(prefix)
|
||||||
|
|
||||||
|
testHandler := http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||||
|
testCase := routeTestCase{
|
||||||
|
RequestURI: r.RequestURI,
|
||||||
|
Vars: mux.Vars(r),
|
||||||
|
RouteName: mux.CurrentRoute(r).GetName(),
|
||||||
|
}
|
||||||
|
|
||||||
|
enc := json.NewEncoder(w)
|
||||||
|
|
||||||
|
if err := enc.Encode(testCase); err != nil {
|
||||||
|
http.Error(w, err.Error(), http.StatusInternalServerError)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
// Startup test server
|
||||||
|
server := httptest.NewServer(router)
|
||||||
|
|
||||||
|
for _, testcase := range testCases {
|
||||||
|
testcase.RequestURI = strings.TrimSuffix(prefix, "/") + testcase.RequestURI
|
||||||
// Register the endpoint
|
// Register the endpoint
|
||||||
router.GetRoute(testcase.RouteName).Handler(testHandler)
|
route := router.GetRoute(testcase.RouteName)
|
||||||
|
if route == nil {
|
||||||
|
t.Fatalf("route for name %q not found", testcase.RouteName)
|
||||||
|
}
|
||||||
|
|
||||||
|
route.Handler(testHandler)
|
||||||
|
|
||||||
u := server.URL + testcase.RequestURI
|
u := server.URL + testcase.RequestURI
|
||||||
|
|
||||||
resp, err := http.Get(u)
|
resp, err := http.Get(u)
|
||||||
|
@ -161,6 +237,10 @@ func TestRouter(t *testing.T) {
|
||||||
// Override default, zero-value
|
// Override default, zero-value
|
||||||
testcase.StatusCode = http.StatusOK
|
testcase.StatusCode = http.StatusOK
|
||||||
}
|
}
|
||||||
|
if testcase.ExpectedURI == "" {
|
||||||
|
// Override default, zero-value
|
||||||
|
testcase.ExpectedURI = testcase.RequestURI
|
||||||
|
}
|
||||||
|
|
||||||
if resp.StatusCode != testcase.StatusCode {
|
if resp.StatusCode != testcase.StatusCode {
|
||||||
t.Fatalf("unexpected status for %s: %v %v", u, resp.Status, resp.StatusCode)
|
t.Fatalf("unexpected status for %s: %v %v", u, resp.Status, resp.StatusCode)
|
||||||
|
@ -180,13 +260,56 @@ func TestRouter(t *testing.T) {
|
||||||
// Needs to be set out of band
|
// Needs to be set out of band
|
||||||
actualRouteInfo.StatusCode = resp.StatusCode
|
actualRouteInfo.StatusCode = resp.StatusCode
|
||||||
|
|
||||||
|
if actualRouteInfo.RequestURI != testcase.ExpectedURI {
|
||||||
|
t.Fatalf("URI %v incorrectly parsed, expected %v", actualRouteInfo.RequestURI, testcase.ExpectedURI)
|
||||||
|
}
|
||||||
|
|
||||||
if actualRouteInfo.RouteName != testcase.RouteName {
|
if actualRouteInfo.RouteName != testcase.RouteName {
|
||||||
t.Fatalf("incorrect route %q matched, expected %q", actualRouteInfo.RouteName, testcase.RouteName)
|
t.Fatalf("incorrect route %q matched, expected %q", actualRouteInfo.RouteName, testcase.RouteName)
|
||||||
}
|
}
|
||||||
|
|
||||||
if !reflect.DeepEqual(actualRouteInfo, testcase) {
|
// when testing deep equality, the actualRouteInfo has an empty ExpectedURI, we don't want
|
||||||
|
// that to make the comparison fail. We're otherwise done with the testcase so empty the
|
||||||
|
// testcase.ExpectedURI
|
||||||
|
testcase.ExpectedURI = ""
|
||||||
|
if deeplyEqual && !reflect.DeepEqual(actualRouteInfo, testcase) {
|
||||||
t.Fatalf("actual does not equal expected: %#v != %#v", actualRouteInfo, testcase)
|
t.Fatalf("actual does not equal expected: %#v != %#v", actualRouteInfo, testcase)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// -------------- START LICENSED CODE --------------
|
||||||
|
// The following code is derivative of https://github.com/google/gofuzz
|
||||||
|
// gofuzz is licensed under the Apache License, Version 2.0, January 2004,
|
||||||
|
// a copy of which can be found in the LICENSE file at the root of this
|
||||||
|
// repository.
|
||||||
|
|
||||||
|
// These functions allow us to generate strings containing only multibyte
|
||||||
|
// characters that are invalid in our URLs. They are used above for fuzzing
|
||||||
|
// to ensure we always get 404s on these invalid strings
|
||||||
|
type charRange struct {
|
||||||
|
first, last rune
|
||||||
|
}
|
||||||
|
|
||||||
|
// choose returns a random unicode character from the given range, using the
|
||||||
|
// given randomness source.
|
||||||
|
func (r *charRange) choose() rune {
|
||||||
|
count := int64(r.last - r.first)
|
||||||
|
return r.first + rune(rand.Int63n(count))
|
||||||
|
}
|
||||||
|
|
||||||
|
var unicodeRanges = []charRange{
|
||||||
|
{'\u00a0', '\u02af'}, // Multi-byte encoded characters
|
||||||
|
{'\u4e00', '\u9fff'}, // Common CJK (even longer encodings)
|
||||||
|
}
|
||||||
|
|
||||||
|
func randomString(length int) string {
|
||||||
|
runes := make([]rune, length)
|
||||||
|
for i := range runes {
|
||||||
|
runes[i] = unicodeRanges[rand.Intn(len(unicodeRanges))].choose()
|
||||||
|
}
|
||||||
|
return string(runes)
|
||||||
|
}
|
||||||
|
|
||||||
|
// -------------- END LICENSED CODE --------------
|
|
@ -3,7 +3,9 @@ package v2
|
||||||
import (
|
import (
|
||||||
"net/http"
|
"net/http"
|
||||||
"net/url"
|
"net/url"
|
||||||
|
"strings"
|
||||||
|
|
||||||
|
"github.com/docker/distribution/digest"
|
||||||
"github.com/gorilla/mux"
|
"github.com/gorilla/mux"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
@ -42,9 +44,40 @@ func NewURLBuilderFromString(root string) (*URLBuilder, error) {
|
||||||
// NewURLBuilderFromRequest uses information from an *http.Request to
|
// NewURLBuilderFromRequest uses information from an *http.Request to
|
||||||
// construct the root url.
|
// construct the root url.
|
||||||
func NewURLBuilderFromRequest(r *http.Request) *URLBuilder {
|
func NewURLBuilderFromRequest(r *http.Request) *URLBuilder {
|
||||||
|
var scheme string
|
||||||
|
|
||||||
|
forwardedProto := r.Header.Get("X-Forwarded-Proto")
|
||||||
|
|
||||||
|
switch {
|
||||||
|
case len(forwardedProto) > 0:
|
||||||
|
scheme = forwardedProto
|
||||||
|
case r.TLS != nil:
|
||||||
|
scheme = "https"
|
||||||
|
case len(r.URL.Scheme) > 0:
|
||||||
|
scheme = r.URL.Scheme
|
||||||
|
default:
|
||||||
|
scheme = "http"
|
||||||
|
}
|
||||||
|
|
||||||
|
host := r.Host
|
||||||
|
forwardedHost := r.Header.Get("X-Forwarded-Host")
|
||||||
|
if len(forwardedHost) > 0 {
|
||||||
|
host = forwardedHost
|
||||||
|
}
|
||||||
|
|
||||||
|
basePath := routeDescriptorsMap[RouteNameBase].Path
|
||||||
|
|
||||||
|
requestPath := r.URL.Path
|
||||||
|
index := strings.Index(requestPath, basePath)
|
||||||
|
|
||||||
u := &url.URL{
|
u := &url.URL{
|
||||||
Scheme: r.URL.Scheme,
|
Scheme: scheme,
|
||||||
Host: r.Host,
|
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 NewURLBuilder(u)
|
return NewURLBuilder(u)
|
||||||
|
@ -74,7 +107,8 @@ func (ub *URLBuilder) BuildTagsURL(name string) (string, error) {
|
||||||
return tagsURL.String(), nil
|
return tagsURL.String(), nil
|
||||||
}
|
}
|
||||||
|
|
||||||
// BuildManifestURL constructs a url for the manifest identified by name and reference.
|
// BuildManifestURL constructs a url for the manifest identified by name and
|
||||||
|
// reference. The argument reference may be either a tag or digest.
|
||||||
func (ub *URLBuilder) BuildManifestURL(name, reference string) (string, error) {
|
func (ub *URLBuilder) BuildManifestURL(name, reference string) (string, error) {
|
||||||
route := ub.cloneRoute(RouteNameManifest)
|
route := ub.cloneRoute(RouteNameManifest)
|
||||||
|
|
||||||
|
@ -87,10 +121,10 @@ func (ub *URLBuilder) BuildManifestURL(name, reference string) (string, error) {
|
||||||
}
|
}
|
||||||
|
|
||||||
// BuildBlobURL constructs the url for the blob identified by name and dgst.
|
// BuildBlobURL constructs the url for the blob identified by name and dgst.
|
||||||
func (ub *URLBuilder) BuildBlobURL(name string, dgst string) (string, error) {
|
func (ub *URLBuilder) BuildBlobURL(name string, dgst digest.Digest) (string, error) {
|
||||||
route := ub.cloneRoute(RouteNameBlob)
|
route := ub.cloneRoute(RouteNameBlob)
|
||||||
|
|
||||||
layerURL, err := route.URL("name", name, "digest", dgst)
|
layerURL, err := route.URL("name", name, "digest", dgst.String())
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return "", err
|
return "", err
|
||||||
}
|
}
|
||||||
|
@ -149,6 +183,10 @@ func (cr clonedRoute) URL(pairs ...string) (*url.URL, error) {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if routeURL.Scheme == "" && routeURL.User == nil && routeURL.Host == "" {
|
||||||
|
routeURL.Path = routeURL.Path[1:]
|
||||||
|
}
|
||||||
|
|
||||||
return cr.root.ResolveReference(routeURL), nil
|
return cr.root.ResolveReference(routeURL), nil
|
||||||
}
|
}
|
||||||
|
|
|
@ -0,0 +1,225 @@
|
||||||
|
package v2
|
||||||
|
|
||||||
|
import (
|
||||||
|
"net/http"
|
||||||
|
"net/url"
|
||||||
|
"testing"
|
||||||
|
)
|
||||||
|
|
||||||
|
type urlBuilderTestCase struct {
|
||||||
|
description string
|
||||||
|
expectedPath string
|
||||||
|
build func() (string, error)
|
||||||
|
}
|
||||||
|
|
||||||
|
func makeURLBuilderTestCases(urlBuilder *URLBuilder) []urlBuilderTestCase {
|
||||||
|
return []urlBuilderTestCase{
|
||||||
|
{
|
||||||
|
description: "test base url",
|
||||||
|
expectedPath: "/v2/",
|
||||||
|
build: urlBuilder.BuildBaseURL,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
description: "test tags url",
|
||||||
|
expectedPath: "/v2/foo/bar/tags/list",
|
||||||
|
build: func() (string, error) {
|
||||||
|
return urlBuilder.BuildTagsURL("foo/bar")
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
description: "test manifest url",
|
||||||
|
expectedPath: "/v2/foo/bar/manifests/tag",
|
||||||
|
build: func() (string, error) {
|
||||||
|
return urlBuilder.BuildManifestURL("foo/bar", "tag")
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
description: "build blob url",
|
||||||
|
expectedPath: "/v2/foo/bar/blobs/tarsum.v1+sha256:abcdef0123456789",
|
||||||
|
build: func() (string, error) {
|
||||||
|
return urlBuilder.BuildBlobURL("foo/bar", "tarsum.v1+sha256:abcdef0123456789")
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
description: "build blob upload url",
|
||||||
|
expectedPath: "/v2/foo/bar/blobs/uploads/",
|
||||||
|
build: func() (string, error) {
|
||||||
|
return urlBuilder.BuildBlobUploadURL("foo/bar")
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
description: "build blob upload url with digest and size",
|
||||||
|
expectedPath: "/v2/foo/bar/blobs/uploads/?digest=tarsum.v1%2Bsha256%3Aabcdef0123456789&size=10000",
|
||||||
|
build: func() (string, error) {
|
||||||
|
return urlBuilder.BuildBlobUploadURL("foo/bar", url.Values{
|
||||||
|
"size": []string{"10000"},
|
||||||
|
"digest": []string{"tarsum.v1+sha256:abcdef0123456789"},
|
||||||
|
})
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
description: "build blob upload chunk url",
|
||||||
|
expectedPath: "/v2/foo/bar/blobs/uploads/uuid-part",
|
||||||
|
build: func() (string, error) {
|
||||||
|
return urlBuilder.BuildBlobUploadChunkURL("foo/bar", "uuid-part")
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
description: "build blob upload chunk url with digest and size",
|
||||||
|
expectedPath: "/v2/foo/bar/blobs/uploads/uuid-part?digest=tarsum.v1%2Bsha256%3Aabcdef0123456789&size=10000",
|
||||||
|
build: func() (string, error) {
|
||||||
|
return urlBuilder.BuildBlobUploadChunkURL("foo/bar", "uuid-part", url.Values{
|
||||||
|
"size": []string{"10000"},
|
||||||
|
"digest": []string{"tarsum.v1+sha256:abcdef0123456789"},
|
||||||
|
})
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// TestURLBuilder tests the various url building functions, ensuring they are
|
||||||
|
// returning the expected values.
|
||||||
|
func TestURLBuilder(t *testing.T) {
|
||||||
|
roots := []string{
|
||||||
|
"http://example.com",
|
||||||
|
"https://example.com",
|
||||||
|
"http://localhost:5000",
|
||||||
|
"https://localhost:5443",
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, root := range roots {
|
||||||
|
urlBuilder, err := NewURLBuilderFromString(root)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("unexpected error creating urlbuilder: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, testCase := range makeURLBuilderTestCases(urlBuilder) {
|
||||||
|
url, err := testCase.build()
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("%s: error building url: %v", testCase.description, err)
|
||||||
|
}
|
||||||
|
|
||||||
|
expectedURL := root + testCase.expectedPath
|
||||||
|
|
||||||
|
if url != expectedURL {
|
||||||
|
t.Fatalf("%s: %q != %q", testCase.description, url, expectedURL)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestURLBuilderWithPrefix(t *testing.T) {
|
||||||
|
roots := []string{
|
||||||
|
"http://example.com/prefix/",
|
||||||
|
"https://example.com/prefix/",
|
||||||
|
"http://localhost:5000/prefix/",
|
||||||
|
"https://localhost:5443/prefix/",
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, root := range roots {
|
||||||
|
urlBuilder, err := NewURLBuilderFromString(root)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("unexpected error creating urlbuilder: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, testCase := range makeURLBuilderTestCases(urlBuilder) {
|
||||||
|
url, err := testCase.build()
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("%s: error building url: %v", testCase.description, err)
|
||||||
|
}
|
||||||
|
|
||||||
|
expectedURL := root[0:len(root)-1] + testCase.expectedPath
|
||||||
|
|
||||||
|
if url != expectedURL {
|
||||||
|
t.Fatalf("%s: %q != %q", testCase.description, url, expectedURL)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
type builderFromRequestTestCase struct {
|
||||||
|
request *http.Request
|
||||||
|
base string
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestBuilderFromRequest(t *testing.T) {
|
||||||
|
u, err := url.Parse("http://example.com")
|
||||||
|
if err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
|
||||||
|
forwardedProtoHeader := make(http.Header, 1)
|
||||||
|
forwardedProtoHeader.Set("X-Forwarded-Proto", "https")
|
||||||
|
|
||||||
|
testRequests := []struct {
|
||||||
|
request *http.Request
|
||||||
|
base string
|
||||||
|
}{
|
||||||
|
{
|
||||||
|
request: &http.Request{URL: u, Host: u.Host},
|
||||||
|
base: "http://example.com",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
request: &http.Request{URL: u, Host: u.Host, Header: forwardedProtoHeader},
|
||||||
|
base: "https://example.com",
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, tr := range testRequests {
|
||||||
|
builder := NewURLBuilderFromRequest(tr.request)
|
||||||
|
|
||||||
|
for _, testCase := range makeURLBuilderTestCases(builder) {
|
||||||
|
url, err := testCase.build()
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("%s: error building url: %v", testCase.description, err)
|
||||||
|
}
|
||||||
|
|
||||||
|
expectedURL := tr.base + testCase.expectedPath
|
||||||
|
|
||||||
|
if url != expectedURL {
|
||||||
|
t.Fatalf("%s: %q != %q", testCase.description, url, expectedURL)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestBuilderFromRequestWithPrefix(t *testing.T) {
|
||||||
|
u, err := url.Parse("http://example.com/prefix/v2/")
|
||||||
|
if err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
|
||||||
|
forwardedProtoHeader := make(http.Header, 1)
|
||||||
|
forwardedProtoHeader.Set("X-Forwarded-Proto", "https")
|
||||||
|
|
||||||
|
testRequests := []struct {
|
||||||
|
request *http.Request
|
||||||
|
base string
|
||||||
|
}{
|
||||||
|
{
|
||||||
|
request: &http.Request{URL: u, Host: u.Host},
|
||||||
|
base: "http://example.com/prefix/",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
request: &http.Request{URL: u, Host: u.Host, Header: forwardedProtoHeader},
|
||||||
|
base: "https://example.com/prefix/",
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, tr := range testRequests {
|
||||||
|
builder := NewURLBuilderFromRequest(tr.request)
|
||||||
|
|
||||||
|
for _, testCase := range makeURLBuilderTestCases(builder) {
|
||||||
|
url, err := testCase.build()
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("%s: error building url: %v", testCase.description, err)
|
||||||
|
}
|
||||||
|
|
||||||
|
expectedURL := tr.base[0:len(tr.base)-1] + testCase.expectedPath
|
||||||
|
|
||||||
|
if url != expectedURL {
|
||||||
|
t.Fatalf("%s: %q != %q", testCase.description, url, expectedURL)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
|
@ -87,10 +87,10 @@ func (r *Router) ServeHTTP(w http.ResponseWriter, req *http.Request) {
|
||||||
setCurrentRoute(req, match.Route)
|
setCurrentRoute(req, match.Route)
|
||||||
}
|
}
|
||||||
if handler == nil {
|
if handler == nil {
|
||||||
if r.NotFoundHandler == nil {
|
|
||||||
r.NotFoundHandler = http.NotFoundHandler()
|
|
||||||
}
|
|
||||||
handler = r.NotFoundHandler
|
handler = r.NotFoundHandler
|
||||||
|
if handler == nil {
|
||||||
|
handler = http.NotFoundHandler()
|
||||||
|
}
|
||||||
}
|
}
|
||||||
if !r.KeepContext {
|
if !r.KeepContext {
|
||||||
defer context.Clear(req)
|
defer context.Clear(req)
|
||||||
|
|
|
@ -462,6 +462,15 @@ func TestQueries(t *testing.T) {
|
||||||
path: "",
|
path: "",
|
||||||
shouldMatch: true,
|
shouldMatch: true,
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
title: "Queries route, match with a query string out of order",
|
||||||
|
route: new(Route).Host("www.example.com").Path("/api").Queries("foo", "bar", "baz", "ding"),
|
||||||
|
request: newRequest("GET", "http://www.example.com/api?baz=ding&foo=bar"),
|
||||||
|
vars: map[string]string{},
|
||||||
|
host: "",
|
||||||
|
path: "",
|
||||||
|
shouldMatch: true,
|
||||||
|
},
|
||||||
{
|
{
|
||||||
title: "Queries route, bad query",
|
title: "Queries route, bad query",
|
||||||
route: new(Route).Queries("foo", "bar", "baz", "ding"),
|
route: new(Route).Queries("foo", "bar", "baz", "ding"),
|
||||||
|
@ -471,6 +480,42 @@ func TestQueries(t *testing.T) {
|
||||||
path: "",
|
path: "",
|
||||||
shouldMatch: false,
|
shouldMatch: false,
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
title: "Queries route with pattern, match",
|
||||||
|
route: new(Route).Queries("foo", "{v1}"),
|
||||||
|
request: newRequest("GET", "http://localhost?foo=bar"),
|
||||||
|
vars: map[string]string{"v1": "bar"},
|
||||||
|
host: "",
|
||||||
|
path: "",
|
||||||
|
shouldMatch: true,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
title: "Queries route with multiple patterns, match",
|
||||||
|
route: new(Route).Queries("foo", "{v1}", "baz", "{v2}"),
|
||||||
|
request: newRequest("GET", "http://localhost?foo=bar&baz=ding"),
|
||||||
|
vars: map[string]string{"v1": "bar", "v2": "ding"},
|
||||||
|
host: "",
|
||||||
|
path: "",
|
||||||
|
shouldMatch: true,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
title: "Queries route with regexp pattern, match",
|
||||||
|
route: new(Route).Queries("foo", "{v1:[0-9]+}"),
|
||||||
|
request: newRequest("GET", "http://localhost?foo=10"),
|
||||||
|
vars: map[string]string{"v1": "10"},
|
||||||
|
host: "",
|
||||||
|
path: "",
|
||||||
|
shouldMatch: true,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
title: "Queries route with regexp pattern, regexp does not match",
|
||||||
|
route: new(Route).Queries("foo", "{v1:[0-9]+}"),
|
||||||
|
request: newRequest("GET", "http://localhost?foo=a"),
|
||||||
|
vars: map[string]string{},
|
||||||
|
host: "",
|
||||||
|
path: "",
|
||||||
|
shouldMatch: false,
|
||||||
|
},
|
||||||
}
|
}
|
||||||
|
|
||||||
for _, test := range tests {
|
for _, test := range tests {
|
||||||
|
|
|
@ -329,35 +329,6 @@ var pathMatcherTests = []pathMatcherTest{
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
|
|
||||||
type queryMatcherTest struct {
|
|
||||||
matcher queryMatcher
|
|
||||||
url string
|
|
||||||
result bool
|
|
||||||
}
|
|
||||||
|
|
||||||
var queryMatcherTests = []queryMatcherTest{
|
|
||||||
{
|
|
||||||
matcher: queryMatcher(map[string]string{"foo": "bar", "baz": "ding"}),
|
|
||||||
url: "http://localhost:8080/?foo=bar&baz=ding",
|
|
||||||
result: true,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
matcher: queryMatcher(map[string]string{"foo": "", "baz": ""}),
|
|
||||||
url: "http://localhost:8080/?foo=anything&baz=anything",
|
|
||||||
result: true,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
matcher: queryMatcher(map[string]string{"foo": "ding", "baz": "bar"}),
|
|
||||||
url: "http://localhost:8080/?foo=bar&baz=ding",
|
|
||||||
result: false,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
matcher: queryMatcher(map[string]string{"bar": "foo", "ding": "baz"}),
|
|
||||||
url: "http://localhost:8080/?foo=bar&baz=ding",
|
|
||||||
result: false,
|
|
||||||
},
|
|
||||||
}
|
|
||||||
|
|
||||||
type schemeMatcherTest struct {
|
type schemeMatcherTest struct {
|
||||||
matcher schemeMatcher
|
matcher schemeMatcher
|
||||||
url string
|
url string
|
||||||
|
@ -519,23 +490,8 @@ func TestPathMatcher(t *testing.T) {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func TestQueryMatcher(t *testing.T) {
|
|
||||||
for _, v := range queryMatcherTests {
|
|
||||||
request, _ := http.NewRequest("GET", v.url, nil)
|
|
||||||
var routeMatch RouteMatch
|
|
||||||
result := v.matcher.Match(request, &routeMatch)
|
|
||||||
if result != v.result {
|
|
||||||
if v.result {
|
|
||||||
t.Errorf("%#v: should match %v.", v.matcher, v.url)
|
|
||||||
} else {
|
|
||||||
t.Errorf("%#v: should not match %v.", v.matcher, v.url)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func TestSchemeMatcher(t *testing.T) {
|
func TestSchemeMatcher(t *testing.T) {
|
||||||
for _, v := range queryMatcherTests {
|
for _, v := range schemeMatcherTests {
|
||||||
request, _ := http.NewRequest("GET", v.url, nil)
|
request, _ := http.NewRequest("GET", v.url, nil)
|
||||||
var routeMatch RouteMatch
|
var routeMatch RouteMatch
|
||||||
result := v.matcher.Match(request, &routeMatch)
|
result := v.matcher.Match(request, &routeMatch)
|
||||||
|
@ -735,7 +691,7 @@ func TestNewRegexp(t *testing.T) {
|
||||||
}
|
}
|
||||||
|
|
||||||
for pattern, paths := range tests {
|
for pattern, paths := range tests {
|
||||||
p, _ = newRouteRegexp(pattern, false, false, false)
|
p, _ = newRouteRegexp(pattern, false, false, false, false)
|
||||||
for path, result := range paths {
|
for path, result := range paths {
|
||||||
matches = p.regexp.FindStringSubmatch(path)
|
matches = p.regexp.FindStringSubmatch(path)
|
||||||
if result == nil {
|
if result == nil {
|
||||||
|
|
|
@ -14,7 +14,7 @@ import (
|
||||||
)
|
)
|
||||||
|
|
||||||
// newRouteRegexp parses a route template and returns a routeRegexp,
|
// newRouteRegexp parses a route template and returns a routeRegexp,
|
||||||
// used to match a host or path.
|
// used to match a host, a path or a query string.
|
||||||
//
|
//
|
||||||
// It will extract named variables, assemble a regexp to be matched, create
|
// It will extract named variables, assemble a regexp to be matched, create
|
||||||
// a "reverse" template to build URLs and compile regexps to validate variable
|
// a "reverse" template to build URLs and compile regexps to validate variable
|
||||||
|
@ -23,7 +23,7 @@ import (
|
||||||
// Previously we accepted only Python-like identifiers for variable
|
// Previously we accepted only Python-like identifiers for variable
|
||||||
// names ([a-zA-Z_][a-zA-Z0-9_]*), but currently the only restriction is that
|
// names ([a-zA-Z_][a-zA-Z0-9_]*), but currently the only restriction is that
|
||||||
// name and pattern can't be empty, and names can't contain a colon.
|
// name and pattern can't be empty, and names can't contain a colon.
|
||||||
func newRouteRegexp(tpl string, matchHost, matchPrefix, strictSlash bool) (*routeRegexp, error) {
|
func newRouteRegexp(tpl string, matchHost, matchPrefix, matchQuery, strictSlash bool) (*routeRegexp, error) {
|
||||||
// Check if it is well-formed.
|
// Check if it is well-formed.
|
||||||
idxs, errBraces := braceIndices(tpl)
|
idxs, errBraces := braceIndices(tpl)
|
||||||
if errBraces != nil {
|
if errBraces != nil {
|
||||||
|
@ -33,11 +33,15 @@ func newRouteRegexp(tpl string, matchHost, matchPrefix, strictSlash bool) (*rout
|
||||||
template := tpl
|
template := tpl
|
||||||
// Now let's parse it.
|
// Now let's parse it.
|
||||||
defaultPattern := "[^/]+"
|
defaultPattern := "[^/]+"
|
||||||
if matchHost {
|
if matchQuery {
|
||||||
|
defaultPattern = "[^?&]+"
|
||||||
|
matchPrefix = true
|
||||||
|
} else if matchHost {
|
||||||
defaultPattern = "[^.]+"
|
defaultPattern = "[^.]+"
|
||||||
matchPrefix, strictSlash = false, false
|
matchPrefix = false
|
||||||
}
|
}
|
||||||
if matchPrefix {
|
// Only match strict slash if not matching
|
||||||
|
if matchPrefix || matchHost || matchQuery {
|
||||||
strictSlash = false
|
strictSlash = false
|
||||||
}
|
}
|
||||||
// Set a flag for strictSlash.
|
// Set a flag for strictSlash.
|
||||||
|
@ -48,7 +52,10 @@ func newRouteRegexp(tpl string, matchHost, matchPrefix, strictSlash bool) (*rout
|
||||||
}
|
}
|
||||||
varsN := make([]string, len(idxs)/2)
|
varsN := make([]string, len(idxs)/2)
|
||||||
varsR := make([]*regexp.Regexp, len(idxs)/2)
|
varsR := make([]*regexp.Regexp, len(idxs)/2)
|
||||||
pattern := bytes.NewBufferString("^")
|
pattern := bytes.NewBufferString("")
|
||||||
|
if !matchQuery {
|
||||||
|
pattern.WriteByte('^')
|
||||||
|
}
|
||||||
reverse := bytes.NewBufferString("")
|
reverse := bytes.NewBufferString("")
|
||||||
var end int
|
var end int
|
||||||
var err error
|
var err error
|
||||||
|
@ -100,6 +107,7 @@ func newRouteRegexp(tpl string, matchHost, matchPrefix, strictSlash bool) (*rout
|
||||||
return &routeRegexp{
|
return &routeRegexp{
|
||||||
template: template,
|
template: template,
|
||||||
matchHost: matchHost,
|
matchHost: matchHost,
|
||||||
|
matchQuery: matchQuery,
|
||||||
strictSlash: strictSlash,
|
strictSlash: strictSlash,
|
||||||
regexp: reg,
|
regexp: reg,
|
||||||
reverse: reverse.String(),
|
reverse: reverse.String(),
|
||||||
|
@ -113,8 +121,10 @@ func newRouteRegexp(tpl string, matchHost, matchPrefix, strictSlash bool) (*rout
|
||||||
type routeRegexp struct {
|
type routeRegexp struct {
|
||||||
// The unmodified template.
|
// The unmodified template.
|
||||||
template string
|
template string
|
||||||
// True for host match, false for path match.
|
// True for host match, false for path or query string match.
|
||||||
matchHost bool
|
matchHost bool
|
||||||
|
// True for query string match, false for path and host match.
|
||||||
|
matchQuery bool
|
||||||
// The strictSlash value defined on the route, but disabled if PathPrefix was used.
|
// The strictSlash value defined on the route, but disabled if PathPrefix was used.
|
||||||
strictSlash bool
|
strictSlash bool
|
||||||
// Expanded regexp.
|
// Expanded regexp.
|
||||||
|
@ -130,7 +140,11 @@ type routeRegexp struct {
|
||||||
// Match matches the regexp against the URL host or path.
|
// Match matches the regexp against the URL host or path.
|
||||||
func (r *routeRegexp) Match(req *http.Request, match *RouteMatch) bool {
|
func (r *routeRegexp) Match(req *http.Request, match *RouteMatch) bool {
|
||||||
if !r.matchHost {
|
if !r.matchHost {
|
||||||
return r.regexp.MatchString(req.URL.Path)
|
if r.matchQuery {
|
||||||
|
return r.regexp.MatchString(req.URL.RawQuery)
|
||||||
|
} else {
|
||||||
|
return r.regexp.MatchString(req.URL.Path)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
return r.regexp.MatchString(getHost(req))
|
return r.regexp.MatchString(getHost(req))
|
||||||
}
|
}
|
||||||
|
@ -196,8 +210,9 @@ func braceIndices(s string) ([]int, error) {
|
||||||
|
|
||||||
// routeRegexpGroup groups the route matchers that carry variables.
|
// routeRegexpGroup groups the route matchers that carry variables.
|
||||||
type routeRegexpGroup struct {
|
type routeRegexpGroup struct {
|
||||||
host *routeRegexp
|
host *routeRegexp
|
||||||
path *routeRegexp
|
path *routeRegexp
|
||||||
|
queries []*routeRegexp
|
||||||
}
|
}
|
||||||
|
|
||||||
// setMatch extracts the variables from the URL once a route matches.
|
// setMatch extracts the variables from the URL once a route matches.
|
||||||
|
@ -234,17 +249,28 @@ func (v *routeRegexpGroup) setMatch(req *http.Request, m *RouteMatch, r *Route)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
// Store query string variables.
|
||||||
|
rawQuery := req.URL.RawQuery
|
||||||
|
for _, q := range v.queries {
|
||||||
|
queryVars := q.regexp.FindStringSubmatch(rawQuery)
|
||||||
|
if queryVars != nil {
|
||||||
|
for k, v := range q.varsN {
|
||||||
|
m.Vars[v] = queryVars[k+1]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// getHost tries its best to return the request host.
|
// getHost tries its best to return the request host.
|
||||||
func getHost(r *http.Request) string {
|
func getHost(r *http.Request) string {
|
||||||
if !r.URL.IsAbs() {
|
if r.URL.IsAbs() {
|
||||||
host := r.Host
|
return r.URL.Host
|
||||||
// Slice off any port information.
|
|
||||||
if i := strings.Index(host, ":"); i != -1 {
|
|
||||||
host = host[:i]
|
|
||||||
}
|
|
||||||
return host
|
|
||||||
}
|
}
|
||||||
return r.URL.Host
|
host := r.Host
|
||||||
|
// Slice off any port information.
|
||||||
|
if i := strings.Index(host, ":"); i != -1 {
|
||||||
|
host = host[:i]
|
||||||
|
}
|
||||||
|
return host
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
|
@ -135,12 +135,12 @@ func (r *Route) addMatcher(m matcher) *Route {
|
||||||
}
|
}
|
||||||
|
|
||||||
// addRegexpMatcher adds a host or path matcher and builder to a route.
|
// addRegexpMatcher adds a host or path matcher and builder to a route.
|
||||||
func (r *Route) addRegexpMatcher(tpl string, matchHost, matchPrefix bool) error {
|
func (r *Route) addRegexpMatcher(tpl string, matchHost, matchPrefix, matchQuery bool) error {
|
||||||
if r.err != nil {
|
if r.err != nil {
|
||||||
return r.err
|
return r.err
|
||||||
}
|
}
|
||||||
r.regexp = r.getRegexpGroup()
|
r.regexp = r.getRegexpGroup()
|
||||||
if !matchHost {
|
if !matchHost && !matchQuery {
|
||||||
if len(tpl) == 0 || tpl[0] != '/' {
|
if len(tpl) == 0 || tpl[0] != '/' {
|
||||||
return fmt.Errorf("mux: path must start with a slash, got %q", tpl)
|
return fmt.Errorf("mux: path must start with a slash, got %q", tpl)
|
||||||
}
|
}
|
||||||
|
@ -148,10 +148,15 @@ func (r *Route) addRegexpMatcher(tpl string, matchHost, matchPrefix bool) error
|
||||||
tpl = strings.TrimRight(r.regexp.path.template, "/") + tpl
|
tpl = strings.TrimRight(r.regexp.path.template, "/") + tpl
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
rr, err := newRouteRegexp(tpl, matchHost, matchPrefix, r.strictSlash)
|
rr, err := newRouteRegexp(tpl, matchHost, matchPrefix, matchQuery, r.strictSlash)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
for _, q := range r.regexp.queries {
|
||||||
|
if err = uniqueVars(rr.varsN, q.varsN); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
}
|
||||||
if matchHost {
|
if matchHost {
|
||||||
if r.regexp.path != nil {
|
if r.regexp.path != nil {
|
||||||
if err = uniqueVars(rr.varsN, r.regexp.path.varsN); err != nil {
|
if err = uniqueVars(rr.varsN, r.regexp.path.varsN); err != nil {
|
||||||
|
@ -165,7 +170,11 @@ func (r *Route) addRegexpMatcher(tpl string, matchHost, matchPrefix bool) error
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
r.regexp.path = rr
|
if matchQuery {
|
||||||
|
r.regexp.queries = append(r.regexp.queries, rr)
|
||||||
|
} else {
|
||||||
|
r.regexp.path = rr
|
||||||
|
}
|
||||||
}
|
}
|
||||||
r.addMatcher(rr)
|
r.addMatcher(rr)
|
||||||
return nil
|
return nil
|
||||||
|
@ -219,7 +228,7 @@ func (r *Route) Headers(pairs ...string) *Route {
|
||||||
// Variable names must be unique in a given route. They can be retrieved
|
// Variable names must be unique in a given route. They can be retrieved
|
||||||
// calling mux.Vars(request).
|
// calling mux.Vars(request).
|
||||||
func (r *Route) Host(tpl string) *Route {
|
func (r *Route) Host(tpl string) *Route {
|
||||||
r.err = r.addRegexpMatcher(tpl, true, false)
|
r.err = r.addRegexpMatcher(tpl, true, false, false)
|
||||||
return r
|
return r
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -278,7 +287,7 @@ func (r *Route) Methods(methods ...string) *Route {
|
||||||
// Variable names must be unique in a given route. They can be retrieved
|
// Variable names must be unique in a given route. They can be retrieved
|
||||||
// calling mux.Vars(request).
|
// calling mux.Vars(request).
|
||||||
func (r *Route) Path(tpl string) *Route {
|
func (r *Route) Path(tpl string) *Route {
|
||||||
r.err = r.addRegexpMatcher(tpl, false, false)
|
r.err = r.addRegexpMatcher(tpl, false, false, false)
|
||||||
return r
|
return r
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -294,35 +303,42 @@ func (r *Route) Path(tpl string) *Route {
|
||||||
// Also note that the setting of Router.StrictSlash() has no effect on routes
|
// Also note that the setting of Router.StrictSlash() has no effect on routes
|
||||||
// with a PathPrefix matcher.
|
// with a PathPrefix matcher.
|
||||||
func (r *Route) PathPrefix(tpl string) *Route {
|
func (r *Route) PathPrefix(tpl string) *Route {
|
||||||
r.err = r.addRegexpMatcher(tpl, false, true)
|
r.err = r.addRegexpMatcher(tpl, false, true, false)
|
||||||
return r
|
return r
|
||||||
}
|
}
|
||||||
|
|
||||||
// Query ----------------------------------------------------------------------
|
// Query ----------------------------------------------------------------------
|
||||||
|
|
||||||
// queryMatcher matches the request against URL queries.
|
|
||||||
type queryMatcher map[string]string
|
|
||||||
|
|
||||||
func (m queryMatcher) Match(r *http.Request, match *RouteMatch) bool {
|
|
||||||
return matchMap(m, r.URL.Query(), false)
|
|
||||||
}
|
|
||||||
|
|
||||||
// Queries adds a matcher for URL query values.
|
// Queries adds a matcher for URL query values.
|
||||||
// It accepts a sequence of key/value pairs. For example:
|
// It accepts a sequence of key/value pairs. Values may define variables.
|
||||||
|
// For example:
|
||||||
//
|
//
|
||||||
// r := mux.NewRouter()
|
// r := mux.NewRouter()
|
||||||
// r.Queries("foo", "bar", "baz", "ding")
|
// r.Queries("foo", "bar", "id", "{id:[0-9]+}")
|
||||||
//
|
//
|
||||||
// The above route will only match if the URL contains the defined queries
|
// The above route will only match if the URL contains the defined queries
|
||||||
// values, e.g.: ?foo=bar&baz=ding.
|
// values, e.g.: ?foo=bar&id=42.
|
||||||
//
|
//
|
||||||
// It the value is an empty string, it will match any value if the key is set.
|
// It the value is an empty string, it will match any value if the key is set.
|
||||||
|
//
|
||||||
|
// Variables can define an optional regexp pattern to me matched:
|
||||||
|
//
|
||||||
|
// - {name} matches anything until the next slash.
|
||||||
|
//
|
||||||
|
// - {name:pattern} matches the given regexp pattern.
|
||||||
func (r *Route) Queries(pairs ...string) *Route {
|
func (r *Route) Queries(pairs ...string) *Route {
|
||||||
if r.err == nil {
|
length := len(pairs)
|
||||||
var queries map[string]string
|
if length%2 != 0 {
|
||||||
queries, r.err = mapFromPairs(pairs...)
|
r.err = fmt.Errorf(
|
||||||
return r.addMatcher(queryMatcher(queries))
|
"mux: number of parameters must be multiple of 2, got %v", pairs)
|
||||||
|
return nil
|
||||||
}
|
}
|
||||||
|
for i := 0; i < length; i += 2 {
|
||||||
|
if r.err = r.addRegexpMatcher(pairs[i]+"="+pairs[i+1], false, true, true); r.err != nil {
|
||||||
|
return r
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
return r
|
return r
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -498,8 +514,9 @@ func (r *Route) getRegexpGroup() *routeRegexpGroup {
|
||||||
} else {
|
} else {
|
||||||
// Copy.
|
// Copy.
|
||||||
r.regexp = &routeRegexpGroup{
|
r.regexp = &routeRegexpGroup{
|
||||||
host: regexp.host,
|
host: regexp.host,
|
||||||
path: regexp.path,
|
path: regexp.path,
|
||||||
|
queries: regexp.queries,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
Loading…
Reference in New Issue